# -*- coding:utf-8 -*- # ##### BEGIN GPL LICENSE BLOCK ##### # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. # # ##### END GPL LICENSE BLOCK ##### # # ---------------------------------------------------------- # Author: Stephen Leger (s-leger) # # ---------------------------------------------------------- # noinspection PyUnresolvedReferences import bpy # noinspection PyUnresolvedReferences from bpy.types import Operator, PropertyGroup, Mesh, Panel from bpy.props import ( FloatProperty, BoolProperty, IntProperty, StringProperty, EnumProperty, CollectionProperty ) import bmesh from mathutils import Vector, Matrix from mathutils.geometry import interpolate_bezier from math import sin, cos, pi, atan2 from .archipack_manipulator import Manipulable, archipack_manipulator from .archipack_object import ArchipackCreateTool, ArchipackObject from .archipack_2d import Line, Arc from .archipack_cutter import ( CutAblePolygon, CutAbleGenerator, ArchipackCutter, ArchipackCutterPart ) class Slab(): def __init__(self): # self.colour_inactive = (1, 1, 1, 1) pass def set_offset(self, offset, last=None): """ Offset line and compute intersection point between segments """ self.line = self.make_offset(offset, last) def straight_slab(self, a0, length): s = self.straight(length).rotate(a0) return StraightSlab(s.p, s.v) def curved_slab(self, a0, da, radius): n = self.normal(1).rotate(a0).scale(radius) if da < 0: n.v = -n.v a0 = n.angle c = n.p - n.v return CurvedSlab(c, radius, a0, da) class StraightSlab(Slab, Line): def __init__(self, p, v): Line.__init__(self, p, v) Slab.__init__(self) class CurvedSlab(Slab, Arc): def __init__(self, c, radius, a0, da): Arc.__init__(self, c, radius, a0, da) Slab.__init__(self) class SlabGenerator(CutAblePolygon, CutAbleGenerator): def __init__(self, parts): self.parts = parts self.segs = [] self.holes = [] self.convex = True self.xsize = 0 def add_part(self, part): if len(self.segs) < 1: s = None else: s = self.segs[-1] # start a new slab if s is None: if part.type == 'S_SEG': p = Vector((0, 0)) v = part.length * Vector((cos(part.a0), sin(part.a0))) s = StraightSlab(p, v) elif part.type == 'C_SEG': c = -part.radius * Vector((cos(part.a0), sin(part.a0))) s = CurvedSlab(c, part.radius, part.a0, part.da) else: if part.type == 'S_SEG': s = s.straight_slab(part.a0, part.length) elif part.type == 'C_SEG': s = s.curved_slab(part.a0, part.da, part.radius) self.segs.append(s) self.last_type = part.type def set_offset(self): last = None for i, seg in enumerate(self.segs): seg.set_offset(self.parts[i].offset, last) last = seg.line def close(self, closed): # Make last segment implicit closing one if closed: part = self.parts[-1] w = self.segs[-1] dp = self.segs[0].p0 - self.segs[-1].p0 if "C_" in part.type: dw = (w.p1 - w.p0) w.r = part.radius / dw.length * dp.length # angle pt - p0 - angle p0 p1 da = atan2(dp.y, dp.x) - atan2(dw.y, dw.x) a0 = w.a0 + da if a0 > pi: a0 -= 2 * pi if a0 < -pi: a0 += 2 * pi w.a0 = a0 else: w.v = dp if len(self.segs) > 1: w.line = w.make_offset(self.parts[-1].offset, self.segs[-2].line) p1 = self.segs[0].line.p1 self.segs[0].line = self.segs[0].make_offset(self.parts[0].offset, w.line) self.segs[0].line.p1 = p1 def locate_manipulators(self): """ setup manipulators """ for i, f in enumerate(self.segs): manipulators = self.parts[i].manipulators p0 = f.p0.to_3d() p1 = f.p1.to_3d() # angle from last to current segment if i > 0: v0 = self.segs[i - 1].straight(-1, 1).v.to_3d() v1 = f.straight(1, 0).v.to_3d() manipulators[0].set_pts([p0, v0, v1]) if type(f).__name__ == "StraightSlab": # segment length manipulators[1].type_key = 'SIZE' manipulators[1].prop1_name = "length" manipulators[1].set_pts([p0, p1, (1, 0, 0)]) else: # segment radius + angle v0 = (f.p0 - f.c).to_3d() v1 = (f.p1 - f.c).to_3d() manipulators[1].type_key = 'ARC_ANGLE_RADIUS' manipulators[1].prop1_name = "da" manipulators[1].prop2_name = "radius" manipulators[1].set_pts([f.c.to_3d(), v0, v1]) # snap manipulator, don't change index ! manipulators[2].set_pts([p0, p1, (1, 0, 0)]) # dumb segment id manipulators[3].set_pts([p0, p1, (1, 0, 0)]) def get_verts(self, verts): for s in self.segs: if "Curved" in type(s).__name__: for i in range(16): # x, y = slab.line.lerp(i / 16) verts.append(s.lerp(i / 16).to_3d()) else: # x, y = s.line.p0 verts.append(s.p0.to_3d()) """ for i in range(33): x, y = slab.line.lerp(i / 32) verts.append((x, y, 0)) """ def rotate(self, idx_from, a): """ apply rotation to all following segs """ self.segs[idx_from].rotate(a) ca = cos(a) sa = sin(a) rM = Matrix([ [ca, -sa], [sa, ca] ]) # rotation center p0 = self.segs[idx_from].p0 for i in range(idx_from + 1, len(self.segs)): seg = self.segs[i] # rotate seg seg.rotate(a) # rotate delta from rotation center to segment start dp = rM @ (seg.p0 - p0) seg.translate(dp) def translate(self, idx_from, dp): """ apply translation to all following segs """ self.segs[idx_from].p1 += dp for i in range(idx_from + 1, len(self.segs)): self.segs[i].translate(dp) def draw(self, context): """ draw generator using gl """ for seg in self.segs: seg.draw(context, render=False) def limits(self): x_size = [s.p0.x for s in self.segs] self.xsize = max(x_size) - min(x_size) def cut(self, context, o): """ either external or holes cuts """ self.limits() self.as_lines(step_angle=0.0502) # self.segs = [s.line for s in self.segs] for b in o.children: d = archipack_slab_cutter.datablock(b) if d is not None: g = d.ensure_direction() g.change_coordsys(b.matrix_world, o.matrix_world) self.slice(g) def slab(self, context, o, d): verts = [] self.get_verts(verts) if len(verts) > 2: bm = bmesh.new() for v in verts: bm.verts.new(v) bm.verts.ensure_lookup_table() for i in range(1, len(verts)): bm.edges.new((bm.verts[i - 1], bm.verts[i])) bm.edges.new((bm.verts[-1], bm.verts[0])) bm.edges.ensure_lookup_table() bmesh.ops.contextual_create(bm, geom=bm.edges) self.cut_holes(bm, self) bmesh.ops.dissolve_limit(bm, angle_limit=0.01, use_dissolve_boundaries=False, verts=bm.verts, edges=bm.edges, delimit={'MATERIAL'}) bm.to_mesh(o.data) bm.free() # geom = bm.faces[:] # verts = bm.verts[:] # bmesh.ops.solidify(bm, geom=geom, thickness=d.z) # merge with object # bmed.bmesh_join(context, o, [bm], normal_update=True) bpy.ops.object.mode_set(mode='OBJECT') def update(self, context): self.update(context) def update_manipulators(self, context): self.update(context, manipulable_refresh=True) def update_path(self, context): self.update_path(context) materials_enum = ( ('0', 'Ceiling', '', 0), ('1', 'White', '', 1), ('2', 'Concrete', '', 2), ('3', 'Wood', '', 3), ('4', 'Metal', '', 4), ('5', 'Glass', '', 5) ) class archipack_slab_material(PropertyGroup): index : EnumProperty( items=materials_enum, default='4', update=update ) def find_in_selection(self, context): """ find witch selected object this instance belongs to provide support for "copy to selected" """ selected = context.selected_objects[:] for o in selected: props = archipack_slab.datablock(o) if props: for part in props.rail_mat: if part == self: return props return None def update(self, context): props = self.find_in_selection(context) if props is not None: props.update(context) class archipack_slab_child(PropertyGroup): """ Store child fences to be able to sync """ child_name : StringProperty() idx : IntProperty() def get_child(self, context): d = None child = context.scene.objects.get(self.child_name.strip()) if child is not None and child.data is not None: if 'archipack_fence' in child.data: d = child.data.archipack_fence[0] return child, d def update_type(self, context): d = self.find_in_selection(context) if d is not None and d.auto_update: d.auto_update = False # find part index idx = 0 for i, part in enumerate(d.parts): if part == self: idx = i break part = d.parts[idx] a0 = 0 if idx > 0: g = d.get_generator() w0 = g.segs[idx - 1] a0 = w0.straight(1).angle if "C_" in self.type: w = w0.straight_slab(part.a0, part.length) else: w = w0.curved_slab(part.a0, part.da, part.radius) else: if "C_" in self.type: p = Vector((0, 0)) v = self.length * Vector((cos(self.a0), sin(self.a0))) w = StraightSlab(p, v) a0 = pi / 2 else: c = -self.radius * Vector((cos(self.a0), sin(self.a0))) w = CurvedSlab(c, self.radius, self.a0, pi) # w0 - w - w1 if idx + 1 == d.n_parts: dp = - w.p0 else: dp = w.p1 - w.p0 if "C_" in self.type: part.radius = 0.5 * dp.length part.da = pi a0 = atan2(dp.y, dp.x) - pi / 2 - a0 else: part.length = dp.length a0 = atan2(dp.y, dp.x) - a0 if a0 > pi: a0 -= 2 * pi if a0 < -pi: a0 += 2 * pi part.a0 = a0 if idx + 1 < d.n_parts: # adjust rotation of next part part1 = d.parts[idx + 1] if "C_" in part.type: a0 = part1.a0 - pi / 2 else: a0 = part1.a0 + w.straight(1).angle - atan2(dp.y, dp.x) if a0 > pi: a0 -= 2 * pi if a0 < -pi: a0 += 2 * pi part1.a0 = a0 d.auto_update = True class ArchipackSegment(): """ A single manipulable polyline like segment polyline like segment line or arc based @TODO: share this base class with stair, wall, fence, slab """ type : EnumProperty( items=( ('S_SEG', 'Straight', '', 0), ('C_SEG', 'Curved', '', 1), ), default='S_SEG', update=update_type ) length : FloatProperty( name="Length", min=0.01, default=2.0, update=update ) radius : FloatProperty( name="Radius", min=0.5, default=0.7, update=update ) da : FloatProperty( name="Angle", min=-pi, max=pi, default=pi / 2, subtype='ANGLE', unit='ROTATION', update=update ) a0 : FloatProperty( name="Start angle", min=-2 * pi, max=2 * pi, default=0, subtype='ANGLE', unit='ROTATION', update=update ) offset : FloatProperty( name="Offset", description="Add to current segment offset", default=0, unit='LENGTH', subtype='DISTANCE', update=update ) linked_idx : IntProperty(default=-1) # @TODO: # flag to handle wall's x_offset # when set add wall offset value to segment offset # pay attention at allowing per wall segment offset manipulators : CollectionProperty(type=archipack_manipulator) def find_in_selection(self, context): raise NotImplementedError def update(self, context, manipulable_refresh=False): props = self.find_in_selection(context) if props is not None: props.update(context, manipulable_refresh) def draw_insert(self, context, layout, index): """ May implement draw for insert / remove segment operators """ pass def draw(self, context, layout, index): box = layout.box() box.prop(self, "type", text=str(index + 1)) self.draw_insert(context, box, index) if self.type in ['C_SEG']: box.prop(self, "radius") box.prop(self, "da") else: box.prop(self, "length") box.prop(self, "a0") # box.prop(self, "offset") class archipack_slab_part(ArchipackSegment, PropertyGroup): def draw_insert(self, context, layout, index): row = layout.row(align=True) row.operator("archipack.slab_insert", text="Split").index = index row.operator("archipack.slab_balcony", text="Balcony").index = index row.operator("archipack.slab_remove", text="Remove").index = index def find_in_selection(self, context): """ find witch selected object this instance belongs to provide support for "copy to selected" """ selected = context.selected_objects[:] for o in selected: props = archipack_slab.datablock(o) if props: for part in props.parts: if part == self: return props return None class archipack_slab(ArchipackObject, Manipulable, PropertyGroup): # boundary n_parts : IntProperty( name="Parts", min=1, default=1, update=update_manipulators ) parts : CollectionProperty(type=archipack_slab_part) closed : BoolProperty( default=True, name="Close", options={'SKIP_SAVE'}, update=update_manipulators ) # UI layout related parts_expand : BoolProperty( options={'SKIP_SAVE'}, default=False ) x_offset : FloatProperty( name="Offset", min=-1000, max=1000, default=0.0, precision=2, step=1, unit='LENGTH', subtype='DISTANCE', update=update ) z : FloatProperty( name="Thickness", default=0.3, precision=2, step=1, unit='LENGTH', subtype='DISTANCE', update=update ) auto_synch : BoolProperty( name="Auto-Synch", description="Keep wall in synch when editing", default=True, update=update_manipulators ) # @TODO: # Global slab offset # will only affect slab parts sharing a wall childs : CollectionProperty(type=archipack_slab_child) # Flag to prevent mesh update while making bulk changes over variables # use : # .auto_update = False # bulk changes # .auto_update = True auto_update : BoolProperty( options={'SKIP_SAVE'}, default=True, update=update_manipulators ) def get_generator(self): g = SlabGenerator(self.parts) for part in self.parts: # type, radius, da, length g.add_part(part) g.set_offset() g.close(self.closed) g.locate_manipulators() return g def insert_part(self, context, where): self.manipulable_disable(context) self.auto_update = False # the part we do split part_0 = self.parts[where] part_0.length /= 2 part_0.da /= 2 self.parts.add() part_1 = self.parts[len(self.parts) - 1] part_1.type = part_0.type part_1.length = part_0.length part_1.offset = part_0.offset part_1.da = part_0.da part_1.a0 = 0 # move after current one self.parts.move(len(self.parts) - 1, where + 1) self.n_parts += 1 for c in self.childs: if c.idx > where: c.idx += 1 self.setup_manipulators() self.auto_update = True def insert_balcony(self, context, where): self.manipulable_disable(context) self.auto_update = False # the part we do split part_0 = self.parts[where] part_0.length /= 3 part_0.da /= 3 # 1st part 90deg self.parts.add() part_1 = self.parts[len(self.parts) - 1] part_1.type = "S_SEG" part_1.length = 1.5 part_1.da = part_0.da part_1.a0 = -pi / 2 # move after current one self.parts.move(len(self.parts) - 1, where + 1) # 2nd part -90deg self.parts.add() part_1 = self.parts[len(self.parts) - 1] part_1.type = part_0.type part_1.length = part_0.length part_1.radius = part_0.radius + 1.5 part_1.da = part_0.da part_1.a0 = pi / 2 # move after current one self.parts.move(len(self.parts) - 1, where + 2) # 3nd part -90deg self.parts.add() part_1 = self.parts[len(self.parts) - 1] part_1.type = "S_SEG" part_1.length = 1.5 part_1.da = part_0.da part_1.a0 = pi / 2 # move after current one self.parts.move(len(self.parts) - 1, where + 3) # 4nd part -90deg self.parts.add() part_1 = self.parts[len(self.parts) - 1] part_1.type = part_0.type part_1.length = part_0.length part_1.radius = part_0.radius part_1.offset = part_0.offset part_1.da = part_0.da part_1.a0 = -pi / 2 # move after current one self.parts.move(len(self.parts) - 1, where + 4) self.n_parts += 4 self.setup_manipulators() for c in self.childs: if c.idx > where: c.idx += 4 self.auto_update = True g = self.get_generator() o = context.active_object bpy.ops.archipack.fence(auto_manipulate=False) c = context.active_object c.select_set(state=True) c.data.archipack_fence[0].n_parts = 3 c.select_set(state=False) # link to o c.location = Vector((0, 0, 0)) c.parent = o c.location = g.segs[where + 1].p0.to_3d() self.add_child(c.name, where + 1) # c.matrix_world.translation = g.segs[where].p1.to_3d() o.select_set(state=True) context.view_layer.objects.active = o self.relocate_childs(context, o, g) def add_part(self, context, length): self.manipulable_disable(context) self.auto_update = False p = self.parts.add() p.length = length self.n_parts += 1 self.setup_manipulators() self.auto_update = True return p def add_child(self, name, idx): c = self.childs.add() c.child_name = name c.idx = idx def setup_childs(self, o, g): """ Store childs call after a boolean oop """ # print("setup_childs") self.childs.clear() itM = o.matrix_world.inverted() dmax = 0.2 for c in o.children: if (c.data and 'archipack_fence' in c.data): pt = (itM @ c.matrix_world.translation).to_2d() for idx, seg in enumerate(g.segs): # may be optimized with a bound check res, d, t = seg.point_sur_segment(pt) # p1 # |-- x # p0 dist = abs(t) * seg.length if dist < dmax and abs(d) < dmax: # print("%s %s %s %s" % (idx, dist, d, c.name)) self.add_child(c.name, idx) # synch wall # store index of segments with p0 match if self.auto_synch: if o.parent is not None: for i, part in enumerate(self.parts): part.linked_idx = -1 # find first child wall d = None for c in o.parent.children: if c.data and "archipack_wall2" in c.data: d = c.data.archipack_wall2[0] break if d is not None: og = d.get_generator() j = 0 for i, part in enumerate(self.parts): ji = j while ji < d.n_parts + 1: if (g.segs[i].p0 - og.segs[ji].p0).length < 0.005: j = ji + 1 part.linked_idx = ji # print("link: %s to %s" % (i, ji)) break ji += 1 def relocate_childs(self, context, o, g): """ Move and resize childs after edition """ # print("relocate_childs") # Wall child syncro # must store - idx of shared segs # -> store this in parts provide 1:1 map # share type: full, start only, end only # -> may compute on the fly with idx stored # when full segment does match # -update type, radius, length, a0, and da # when start only does match # -update type, radius, a0 # when end only does match # -compute length/radius # @TODO: # handle p0 and p1 changes right in Generator (archipack_2d) # and retrieve params from there if self.auto_synch: if o.parent is not None: wall = None for child in o.parent.children: if child.data and "archipack_wall2" in child.data: wall = child break if wall is not None: d = wall.data.archipack_wall2[0] d.auto_update = False w = d.get_generator() last_idx = -1 # update og from g for i, part in enumerate(self.parts): idx = part.linked_idx seg = g.segs[i] if i + 1 < self.n_parts: next_idx = self.parts[i + 1].linked_idx elif d.closed: next_idx = self.parts[0].linked_idx else: next_idx = -1 if idx > -1: # start and shared: update rotation a = seg.angle - w.segs[idx].angle if abs(a) > 0.00001: w.rotate(idx, a) if last_idx > -1: w.segs[last_idx].p1 = seg.p0 if next_idx > -1: if (idx + 1 == next_idx) or (next_idx == 0 and i + 1 == self.n_parts): # shared: should move last point # and apply to next segments # this is overridden for common segs # but translate non common ones dp = seg.p1 - w.segs[idx].p1 w.translate(idx, dp) # shared: transfer type too if "C_" in part.type: d.parts[idx].type = 'C_WALL' w.segs[idx] = CurvedSlab(seg.c, seg.r, seg.a0, seg.da) else: d.parts[idx].type = 'S_WALL' w.segs[idx] = StraightSlab(seg.p.copy(), seg.v.copy()) last_idx = -1 elif next_idx > -1: # only last is shared # note: on next run will be part of start last_idx = next_idx - 1 # update d from og last_seg = None for i, seg in enumerate(w.segs): d.parts[i].a0 = seg.delta_angle(last_seg) last_seg = seg if "C_" in d.parts[i].type: d.parts[i].radius = seg.r d.parts[i].da = seg.da else: d.parts[i].length = max(0.01, seg.length) wall.select_set(state=True) context.view_layer.objects.active = wall d.auto_update = True wall.select_set(state=False) o.select_set(state=True) context.view_layer.objects.active = o wall.matrix_world = o.matrix_world.copy() tM = o.matrix_world for child in self.childs: c, d = child.get_child(context) if c is None: continue a = g.segs[child.idx].angle x, y = g.segs[child.idx].p0 sa = sin(a) ca = cos(a) if d is not None: c.select_set(state=True) # auto_update need object to be active to # setup manipulators on the right object context.view_layer.objects.active = c d.auto_update = False for i, part in enumerate(d.parts): if "C_" in self.parts[i + child.idx].type: part.type = "C_FENCE" else: part.type = "S_FENCE" part.a0 = self.parts[i + child.idx].a0 part.da = self.parts[i + child.idx].da part.length = self.parts[i + child.idx].length part.radius = self.parts[i + child.idx].radius d.parts[0].a0 = pi / 2 d.auto_update = True c.select_set(state=False) context.view_layer.objects.active = o # preTranslate c.matrix_world = tM @ Matrix([ [sa, ca, 0, x], [-ca, sa, 0, y], [0, 0, 1, 0], [0, 0, 0, 1] ]) def remove_part(self, context, where): self.manipulable_disable(context) self.auto_update = False # preserve shape # using generator if where > 0: g = self.get_generator() w = g.segs[where - 1] w.p1 = g.segs[where].p1 if where + 1 < self.n_parts: self.parts[where + 1].a0 = g.segs[where + 1].delta_angle(w) part = self.parts[where - 1] if "C_" in part.type: part.radius = w.r else: part.length = w.length if where > 1: part.a0 = w.delta_angle(g.segs[where - 2]) else: part.a0 = w.straight(1, 0).angle for c in self.childs: if c.idx >= where: c.idx -= 1 self.parts.remove(where) self.n_parts -= 1 # fix snap manipulators index self.setup_manipulators() self.auto_update = True def update_parts(self, o, update_childs=False): # print("update_parts") # remove rows # NOTE: # n_parts+1 # as last one is end point of last segment or closing one row_change = False for i in range(len(self.parts), self.n_parts, -1): row_change = True self.parts.remove(i - 1) # add rows for i in range(len(self.parts), self.n_parts): row_change = True self.parts.add() self.setup_manipulators() g = self.get_generator() if o is not None and (row_change or update_childs): self.setup_childs(o, g) return g def setup_manipulators(self): if len(self.manipulators) < 1: s = self.manipulators.add() s.type_key = "SIZE" s.prop1_name = "z" s.normal = Vector((0, 1, 0)) for i in range(self.n_parts): p = self.parts[i] n_manips = len(p.manipulators) if n_manips < 1: s = p.manipulators.add() s.type_key = "ANGLE" s.prop1_name = "a0" p.manipulators[0].type_key = 'ANGLE' if n_manips < 2: s = p.manipulators.add() s.type_key = "SIZE" s.prop1_name = "length" if n_manips < 3: s = p.manipulators.add() s.type_key = 'WALL_SNAP' s.prop1_name = str(i) s.prop2_name = 'z' if n_manips < 4: s = p.manipulators.add() s.type_key = 'DUMB_STRING' s.prop1_name = str(i + 1) p.manipulators[2].prop1_name = str(i) p.manipulators[3].prop1_name = str(i + 1) self.parts[-1].manipulators[0].type_key = 'DUMB_ANGLE' def is_cw(self, pts): p0 = pts[0] d = 0 for p in pts[1:]: d += (p.x * p0.y - p.y * p0.x) p0 = p return d > 0 def interpolate_bezier(self, pts, wM, p0, p1, resolution): # straight segment, worth testing here # since this can lower points count by a resolution factor # use normalized to handle non linear t if resolution == 0: pts.append(wM @ p0.co.to_3d()) else: v = (p1.co - p0.co).normalized() d1 = (p0.handle_right - p0.co).normalized() d2 = (p1.co - p1.handle_left).normalized() if d1 == v and d2 == v: pts.append(wM @ p0.co.to_3d()) else: seg = interpolate_bezier(wM @ p0.co, wM @ p0.handle_right, wM @ p1.handle_left, wM @ p1.co, resolution + 1) for i in range(resolution): pts.append(seg[i].to_3d()) def from_spline(self, wM, resolution, spline): pts = [] if spline.type == 'POLY': pts = [wM @ p.co.to_3d() for p in spline.points] if spline.use_cyclic_u: pts.append(pts[0]) elif spline.type == 'BEZIER': points = spline.bezier_points for i in range(1, len(points)): p0 = points[i - 1] p1 = points[i] self.interpolate_bezier(pts, wM, p0, p1, resolution) if spline.use_cyclic_u: p0 = points[-1] p1 = points[0] self.interpolate_bezier(pts, wM, p0, p1, resolution) pts.append(pts[0]) else: pts.append(wM @ points[-1].co) self.from_points(pts, spline.use_cyclic_u) def from_points(self, pts, closed): if self.is_cw(pts): pts = list(reversed(pts)) self.auto_update = False self.n_parts = len(pts) - 1 self.update_parts(None) p0 = pts.pop(0) a0 = 0 for i, p1 in enumerate(pts): dp = p1 - p0 da = atan2(dp.y, dp.x) - a0 if da > pi: da -= 2 * pi if da < -pi: da += 2 * pi if i >= len(self.parts): break p = self.parts[i] p.length = dp.to_2d().length p.dz = dp.z p.a0 = da a0 += da p0 = p1 self.closed = closed self.auto_update = True def make_surface(self, o, verts): bm = bmesh.new() for v in verts: bm.verts.new(v) bm.verts.ensure_lookup_table() for i in range(1, len(verts)): bm.edges.new((bm.verts[i - 1], bm.verts[i])) bm.edges.new((bm.verts[-1], bm.verts[0])) bm.edges.ensure_lookup_table() bmesh.ops.contextual_create(bm, geom=bm.edges) bm.to_mesh(o.data) bm.free() def unwrap_uv(self, o): bm = bmesh.new() bm.from_mesh(o.data) for face in bm.faces: face.select = face.material_index > 0 bm.to_mesh(o.data) bpy.ops.uv.cube_project(scale_to_bounds=False, correct_aspect=True) for face in bm.faces: face.select = face.material_index < 1 bm.to_mesh(o.data) bpy.ops.uv.smart_project(use_aspect=True, stretch_to_bounds=False) bm.free() def update(self, context, manipulable_refresh=False, update_childs=False): o = self.find_in_selection(context, self.auto_update) if o is None: return # clean up manipulators before any data model change if manipulable_refresh: self.manipulable_disable(context) g = self.update_parts(o, update_childs) # relocate before cutting segs self.relocate_childs(context, o, g) o.select_set(state=True) context.view_layer.objects.active = o g.cut(context, o) g.slab(context, o, self) modif = o.modifiers.get('Slab') if modif is None: modif = o.modifiers.new('Slab', 'SOLIDIFY') modif.use_quality_normals = True modif.use_even_offset = True modif.material_offset_rim = 2 modif.material_offset = 1 modif.thickness = self.z modif.offset = 1.0 o.data.use_auto_smooth = True bpy.ops.object.shade_smooth() # Height self.manipulators[0].set_pts([ (0, 0, 0), (0, 0, -self.z), (-1, 0, 0) ], normal=g.segs[0].straight(-1, 0).v.to_3d()) # enable manipulators rebuild if manipulable_refresh: self.manipulable_refresh = True # restore context self.restore_context(context) def manipulable_setup(self, context): """ NOTE: this one assume context.active_object is the instance this data belongs to, failing to do so will result in wrong manipulators set on active object """ self.manipulable_disable(context) o = context.active_object self.setup_manipulators() for i, part in enumerate(self.parts): if i >= self.n_parts: break if i > 0: # start angle self.manip_stack.append(part.manipulators[0].setup(context, o, part)) # length / radius + angle self.manip_stack.append(part.manipulators[1].setup(context, o, part)) # snap point self.manip_stack.append(part.manipulators[2].setup(context, o, self)) # index self.manip_stack.append(part.manipulators[3].setup(context, o, self)) for m in self.manipulators: self.manip_stack.append(m.setup(context, o, self)) def manipulable_invoke(self, context): """ call this in operator invoke() """ # print("manipulable_invoke") if self.manipulate_mode: self.manipulable_disable(context) return False o = context.active_object g = self.get_generator() # setup childs manipulators self.setup_childs(o, g) self.manipulable_setup(context) self.manipulate_mode = True self._manipulable_invoke(context) return True def update_hole(self, context): # update parent's roof only when manipulated self.update(context, update_parent=True) def update_operation(self, context): self.reverse(context, make_ccw=(self.operation == 'INTERSECTION')) class archipack_slab_cutter_segment(ArchipackCutterPart, PropertyGroup): manipulators : CollectionProperty(type=archipack_manipulator) type : EnumProperty( name="Type", items=( ('DEFAULT', 'Side', 'Side with rake', 0), ('BOTTOM', 'Bottom', 'Bottom with gutter', 1), ('LINK', 'Side link', 'Side without decoration', 2), ('AXIS', 'Top', 'Top part with hip and beam', 3) # ('LINK_VALLEY', 'Side valley', 'Side with valley', 3), # ('LINK_HIP', 'Side hip', 'Side with hip', 4) ), default='DEFAULT', update=update_hole ) def find_in_selection(self, context): selected = context.selected_objects[:] for o in selected: d = archipack_slab_cutter.datablock(o) if d: for part in d.parts: if part == self: return d return None def draw(self, layout, context, index): box = layout.box() box.label(text="Part:" + str(index + 1)) # box.prop(self, "type", text=str(index + 1)) box.prop(self, "length") box.prop(self, "a0") class archipack_slab_cutter(ArchipackCutter, ArchipackObject, Manipulable, PropertyGroup): parts : CollectionProperty(type=archipack_slab_cutter_segment) def update_points(self, context, o, pts, update_parent=False): self.auto_update = False self.from_points(pts) self.auto_update = True if update_parent: self.update_parent(context, o) def update_parent(self, context, o): d = archipack_slab.datablock(o.parent) if d is not None: o.parent.select_set(state=True) context.view_layer.objects.active = o.parent d.update(context) o.parent.select_set(state=False) context.view_layer.objects.active = o class ARCHIPACK_PT_slab(Panel): """Archipack Slab""" bl_idname = "ARCHIPACK_PT_slab" bl_label = "Slab" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' # bl_context = 'object' bl_category = 'Archipack' @classmethod def poll(cls, context): return archipack_slab.filter(context.active_object) def draw(self, context): o = context.active_object prop = archipack_slab.datablock(o) if prop is None: return layout = self.layout layout.operator('archipack.slab_manipulate', icon='VIEW_PAN') box = layout.box() box.operator('archipack.slab_cutter').parent = o.name box = layout.box() box.prop(prop, 'z') box = layout.box() box.prop(prop, 'auto_synch') box = layout.box() row = box.row() if prop.parts_expand: row.prop(prop, 'parts_expand', icon="TRIA_DOWN", text="Parts", emboss=False) box.prop(prop, 'n_parts') # box.prop(prop, 'closed') for i, part in enumerate(prop.parts): part.draw(context, layout, i) else: row.prop(prop, 'parts_expand', icon="TRIA_RIGHT", text="Parts", emboss=False) class ARCHIPACK_PT_slab_cutter(Panel): bl_idname = "ARCHIPACK_PT_slab_cutter" bl_label = "Slab Cutter" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = 'Archipack' @classmethod def poll(cls, context): return archipack_slab_cutter.filter(context.active_object) def draw(self, context): prop = archipack_slab_cutter.datablock(context.active_object) if prop is None: return layout = self.layout scene = context.scene box = layout.box() box.operator('archipack.slab_cutter_manipulate', icon='VIEW_PAN') box.prop(prop, 'operation', text="") box = layout.box() box.label(text="From curve") box.prop_search(prop, "user_defined_path", scene, "objects", text="", icon='OUTLINER_OB_CURVE') if prop.user_defined_path != "": box.prop(prop, 'user_defined_resolution') # box.prop(prop, 'x_offset') # box.prop(prop, 'angle_limit') """ box.prop_search(prop, "boundary", scene, "objects", text="", icon='OUTLINER_OB_CURVE') """ prop.draw(layout, context) # ------------------------------------------------------------------ # Define operator class to create object # ------------------------------------------------------------------ class ARCHIPACK_OT_slab_insert(Operator): bl_idname = "archipack.slab_insert" bl_label = "Insert" bl_description = "Insert part" bl_category = 'Archipack' bl_options = {'REGISTER', 'UNDO'} index : IntProperty(default=0) def execute(self, context): if context.mode == "OBJECT": d = archipack_slab.datablock(context.active_object) if d is None: return {'CANCELLED'} d.insert_part(context, self.index) return {'FINISHED'} else: self.report({'WARNING'}, "Archipack: Option only valid in Object mode") return {'CANCELLED'} class ARCHIPACK_OT_slab_balcony(Operator): bl_idname = "archipack.slab_balcony" bl_label = "Insert" bl_description = "Insert part" bl_category = 'Archipack' bl_options = {'REGISTER', 'UNDO'} index : IntProperty(default=0) def execute(self, context): if context.mode == "OBJECT": d = archipack_slab.datablock(context.active_object) if d is None: return {'CANCELLED'} d.insert_balcony(context, self.index) return {'FINISHED'} else: self.report({'WARNING'}, "Archipack: Option only valid in Object mode") return {'CANCELLED'} class ARCHIPACK_OT_slab_remove(Operator): bl_idname = "archipack.slab_remove" bl_label = "Remove" bl_description = "Remove part" bl_category = 'Archipack' bl_options = {'REGISTER', 'UNDO'} index : IntProperty(default=0) def execute(self, context): if context.mode == "OBJECT": d = archipack_slab.datablock(context.active_object) if d is None: return {'CANCELLED'} d.remove_part(context, self.index) return {'FINISHED'} else: self.report({'WARNING'}, "Archipack: Option only valid in Object mode") return {'CANCELLED'} # ------------------------------------------------------------------ # Define operator class to create object # ------------------------------------------------------------------ class ARCHIPACK_OT_slab(ArchipackCreateTool, Operator): bl_idname = "archipack.slab" bl_label = "Slab" bl_description = "Slab" bl_category = 'Archipack' bl_options = {'REGISTER', 'UNDO'} def create(self, context): m = bpy.data.meshes.new("Slab") o = bpy.data.objects.new("Slab", m) d = m.archipack_slab.add() # make manipulators selectable d.manipulable_selectable = True self.link_object_to_scene(context, o) o.select_set(state=True) context.view_layer.objects.active = o self.load_preset(d) self.add_material(o) return o # ----------------------------------------------------- # Execute # ----------------------------------------------------- def execute(self, context): if context.mode == "OBJECT": bpy.ops.object.select_all(action="DESELECT") o = self.create(context) o.location = bpy.context.scene.cursor.location o.select_set(state=True) context.view_layer.objects.active = o self.manipulate() return {'FINISHED'} else: self.report({'WARNING'}, "Archipack: Option only valid in Object mode") return {'CANCELLED'} class ARCHIPACK_OT_slab_from_curve(Operator): bl_idname = "archipack.slab_from_curve" bl_label = "Slab curve" bl_description = "Create a slab from a curve" bl_category = 'Archipack' bl_options = {'REGISTER', 'UNDO'} auto_manipulate : BoolProperty(default=True) @classmethod def poll(self, context): return context.active_object is not None and context.active_object.type == 'CURVE' # ----------------------------------------------------- # Draw (create UI interface) # ----------------------------------------------------- # noinspection PyUnusedLocal def draw(self, context): layout = self.layout row = layout.row() row.label(text="Use Properties panel (N) to define parms", icon='INFO') def create(self, context): curve = context.active_object bpy.ops.archipack.slab(auto_manipulate=self.auto_manipulate) o = context.view_layer.objects.active d = archipack_slab.datablock(o) spline = curve.data.splines[0] d.from_spline(curve.matrix_world, 12, spline) if spline.type == 'POLY': pt = spline.points[0].co elif spline.type == 'BEZIER': pt = spline.bezier_points[0].co else: pt = Vector((0, 0, 0)) # pretranslate o.matrix_world = curve.matrix_world @ Matrix.Translation(pt) return o # ----------------------------------------------------- # Execute # ----------------------------------------------------- def execute(self, context): if context.mode == "OBJECT": bpy.ops.object.select_all(action="DESELECT") self.create(context) return {'FINISHED'} else: self.report({'WARNING'}, "Archipack: Option only valid in Object mode") return {'CANCELLED'} class ARCHIPACK_OT_slab_from_wall(Operator): bl_idname = "archipack.slab_from_wall" bl_label = "->Slab" bl_description = "Create a slab from a wall" bl_category = 'Archipack' bl_options = {'REGISTER', 'UNDO'} auto_manipulate : BoolProperty(default=True) ceiling : BoolProperty(default=False) @classmethod def poll(self, context): o = context.active_object return o is not None and o.data is not None and 'archipack_wall2' in o.data def create(self, context): wall = context.active_object wd = wall.data.archipack_wall2[0] bpy.ops.archipack.slab(auto_manipulate=False) o = context.view_layer.objects.active d = archipack_slab.datablock(o) d.auto_update = False d.closed = True d.parts.clear() d.n_parts = wd.n_parts + 1 for part in wd.parts: p = d.parts.add() if "S_" in part.type: p.type = "S_SEG" else: p.type = "C_SEG" p.length = part.length p.radius = part.radius p.da = part.da p.a0 = part.a0 d.auto_update = True # pretranslate if self.ceiling: o.matrix_world = Matrix([ [1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, wd.z + d.z], [0, 0, 0, 1], ]) @ wall.matrix_world else: o.matrix_world = wall.matrix_world.copy() bpy.ops.object.select_all(action='DESELECT') # parenting childs to wall reference point if wall.parent is None: x, y, z = wall.bound_box[0] context.scene.cursor.location = wall.matrix_world @ Vector((x, y, z)) # fix issue #9 context.view_layer.objects.active = wall bpy.ops.archipack.reference_point() else: wall.parent.select_set(state=True) context.view_layer.objects.active = wall.parent wall.select_set(state=True) o.select_set(state=True) bpy.ops.archipack.parent_to_reference() wall.parent.select_set(state=False) return o # ----------------------------------------------------- # Execute # ----------------------------------------------------- def execute(self, context): if context.mode == "OBJECT": bpy.ops.object.select_all(action="DESELECT") o = self.create(context) o.select_set(state=True) context.view_layer.objects.active = o if self.auto_manipulate: bpy.ops.archipack.slab_manipulate('INVOKE_DEFAULT') return {'FINISHED'} else: self.report({'WARNING'}, "Archipack: Option only valid in Object mode") return {'CANCELLED'} class ARCHIPACK_OT_slab_cutter(ArchipackCreateTool, Operator): bl_idname = "archipack.slab_cutter" bl_label = "Slab Cutter" bl_description = "Slab Cutter" bl_category = 'Archipack' bl_options = {'REGISTER', 'UNDO'} parent : StringProperty("") def create(self, context): m = bpy.data.meshes.new("Slab Cutter") o = bpy.data.objects.new("Slab Cutter", m) d = m.archipack_slab_cutter.add() parent = context.scene.objects.get(self.parent.strip()) if parent is not None: o.parent = parent bbox = parent.bound_box angle_90 = pi / 2 x0, y0, z = bbox[0] x1, y1, z = bbox[6] x = 0.2 * (x1 - x0) y = 0.2 * (y1 - y0) o.matrix_world = parent.matrix_world @ Matrix([ [1, 0, 0, -3 * x], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ]) p = d.parts.add() p.a0 = - angle_90 p.length = y p = d.parts.add() p.a0 = angle_90 p.length = x p = d.parts.add() p.a0 = angle_90 p.length = y d.n_parts = 3 # d.close = True pd = archipack_slab.datablock(parent) pd.boundary = o.name else: o.location = context.scene.cursor.location # make manipulators selectable d.manipulable_selectable = True self.link_object_to_scene(context, o) o.select_set(state=True) context.view_layer.objects.active = o # self.add_material(o) self.load_preset(d) update_operation(d, context) return o # ----------------------------------------------------- # Execute # ----------------------------------------------------- def execute(self, context): if context.mode == "OBJECT": bpy.ops.object.select_all(action="DESELECT") o = self.create(context) o.select_set(state=True) context.view_layer.objects.active = o self.manipulate() return {'FINISHED'} else: self.report({'WARNING'}, "Archipack: Option only valid in Object mode") return {'CANCELLED'} # ------------------------------------------------------------------ # Define operator class to manipulate object # ------------------------------------------------------------------ class ARCHIPACK_OT_slab_manipulate(Operator): bl_idname = "archipack.slab_manipulate" bl_label = "Manipulate" bl_description = "Manipulate" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(self, context): return archipack_slab.filter(context.active_object) def invoke(self, context, event): d = archipack_slab.datablock(context.active_object) d.manipulable_invoke(context) return {'FINISHED'} class ARCHIPACK_OT_slab_cutter_manipulate(Operator): bl_idname = "archipack.slab_cutter_manipulate" bl_label = "Manipulate" bl_description = "Manipulate" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(self, context): return archipack_slab_cutter.filter(context.active_object) def invoke(self, context, event): d = archipack_slab_cutter.datablock(context.active_object) d.manipulable_invoke(context) return {'FINISHED'} def register(): bpy.utils.register_class(archipack_slab_cutter_segment) bpy.utils.register_class(archipack_slab_cutter) Mesh.archipack_slab_cutter = CollectionProperty(type=archipack_slab_cutter) bpy.utils.register_class(ARCHIPACK_OT_slab_cutter) bpy.utils.register_class(ARCHIPACK_PT_slab_cutter) bpy.utils.register_class(ARCHIPACK_OT_slab_cutter_manipulate) bpy.utils.register_class(archipack_slab_material) bpy.utils.register_class(archipack_slab_child) bpy.utils.register_class(archipack_slab_part) bpy.utils.register_class(archipack_slab) Mesh.archipack_slab = CollectionProperty(type=archipack_slab) bpy.utils.register_class(ARCHIPACK_PT_slab) bpy.utils.register_class(ARCHIPACK_OT_slab) bpy.utils.register_class(ARCHIPACK_OT_slab_insert) bpy.utils.register_class(ARCHIPACK_OT_slab_balcony) bpy.utils.register_class(ARCHIPACK_OT_slab_remove) # bpy.utils.register_class(ARCHIPACK_OT_slab_manipulate_ctx) bpy.utils.register_class(ARCHIPACK_OT_slab_manipulate) bpy.utils.register_class(ARCHIPACK_OT_slab_from_curve) bpy.utils.register_class(ARCHIPACK_OT_slab_from_wall) def unregister(): bpy.utils.unregister_class(archipack_slab_material) bpy.utils.unregister_class(archipack_slab_child) bpy.utils.unregister_class(archipack_slab_part) bpy.utils.unregister_class(archipack_slab) del Mesh.archipack_slab bpy.utils.unregister_class(ARCHIPACK_PT_slab) bpy.utils.unregister_class(ARCHIPACK_OT_slab) bpy.utils.unregister_class(ARCHIPACK_OT_slab_insert) bpy.utils.unregister_class(ARCHIPACK_OT_slab_balcony) bpy.utils.unregister_class(ARCHIPACK_OT_slab_remove) # bpy.utils.unregister_class(ARCHIPACK_OT_slab_manipulate_ctx) bpy.utils.unregister_class(ARCHIPACK_OT_slab_manipulate) bpy.utils.unregister_class(ARCHIPACK_OT_slab_from_curve) bpy.utils.unregister_class(ARCHIPACK_OT_slab_from_wall) del Mesh.archipack_slab_cutter bpy.utils.unregister_class(archipack_slab_cutter_segment) bpy.utils.unregister_class(archipack_slab_cutter) bpy.utils.unregister_class(ARCHIPACK_OT_slab_cutter) bpy.utils.unregister_class(ARCHIPACK_PT_slab_cutter) bpy.utils.unregister_class(ARCHIPACK_OT_slab_cutter_manipulate)