# -*- 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 import time # noinspection PyUnresolvedReferences from bpy.types import Operator, PropertyGroup, Mesh, Panel from bpy.props import ( FloatProperty, BoolProperty, IntProperty, StringProperty, EnumProperty, CollectionProperty ) from .bmesh_utils import BmeshEdit as bmed from random import randint import bmesh from mathutils import Vector, Matrix from math import sin, cos, pi, atan2, sqrt, tan from .archipack_manipulator import Manipulable, archipack_manipulator from .archipack_2d import Line, Arc from .archipack_preset import ArchipackPreset, PresetMenuOperator from .archipack_object import ArchipackCreateTool, ArchipackObject from .archipack_cutter import ( CutAblePolygon, CutAbleGenerator, ArchipackCutter, ArchipackCutterPart ) class Roof(): def __init__(self): self.angle_0 = 0 self.v0_idx = 0 self.v1_idx = 0 self.constraint_type = None self.slope_left = 1 self.slope_right = 1 self.width_left = 1 self.width_right = 1 self.auto_left = 'AUTO' self.auto_right = 'AUTO' self.type = 'SIDE' # force hip or valley self.enforce_part = 'AUTO' self.triangular_end = False # seg is part of hole self.is_hole = False def copy_params(self, s): s.angle_0 = self.angle_0 s.v0_idx = self.v0_idx s.v1_idx = self.v1_idx s.constraint_type = self.constraint_type s.slope_left = self.slope_left s.slope_right = self.slope_right s.width_left = self.width_left s.width_right = self.width_right s.auto_left = self.auto_left s.auto_right = self.auto_right s.type = self.type s.enforce_part = self.enforce_part s.triangular_end = self.triangular_end # segment is part of hole / slice s.is_hole = self.is_hole @property def copy(self): s = StraightRoof(self.p.copy(), self.v.copy()) self.copy_params(s) return s def straight(self, length, t=1): s = self.copy s.p = self.lerp(t) s.v = self.v.normalized() * length return s def set_offset(self, offset, last=None): """ Offset line and compute intersection point between segments """ self.line = self.make_offset(offset, last) def offset(self, offset): o = self.copy o.p += offset * self.cross_z.normalized() return o @property def oposite(self): o = self.copy o.p += o.v o.v = -o.v return o @property def t_diff(self): return self.t_end - self.t_start def straight_roof(self, a0, length): s = self.straight(length).rotate(a0) r = StraightRoof(s.p, s.v) r.angle_0 = a0 return r def curved_roof(self, a0, da, radius): n = self.normal(1).rotate(a0).scale(radius) if da < 0: n.v = -n.v c = n.p - n.v r = CurvedRoof(c, radius, n.angle, da) r.angle_0 = a0 return r class StraightRoof(Roof, Line): def __str__(self): return "p0:{} p1:{}".format(self.p0, self.p1) def __init__(self, p, v): Line.__init__(self, p, v) Roof.__init__(self) class CurvedRoof(Roof, Arc): def __str__(self): return "t_start:{} t_end:{} dist:{}".format(self.t_start, self.t_end, self.dist) def __init__(self, c, radius, a0, da): Arc.__init__(self, c, radius, a0, da) Roof.__init__(self) class RoofSegment(): """ Roof part with 2 polygons and "axis" StraightRoof segment """ def __init__(self, seg, left, right): self.seg = seg self.left = left self.right = right self.a0 = 0 self.reversed = False class RoofAxisNode(): """ Connection between parts for radial analysis """ def __init__(self): # axis segments self.segs = [] self.root = None self.center = 0 # store count of horizontal segs self.n_horizontal = 0 # store count of slopes segs self.n_slope = 0 @property def count(self): return len(self.segs) @property def last(self): """ last segments in this node """ return self.segs[-1] def left(self, index): if index + 1 >= self.count: return self.segs[0] return self.segs[index + 1] def right(self, index): return self.segs[index - 1] def add(self, a0, reversed, seg, left, right): if seg.constraint_type == 'HORIZONTAL': self.n_horizontal += 1 elif seg.constraint_type == 'SLOPE': self.n_slope += 1 s = RoofSegment(seg, left, right) s.a0 = a0 s.reversed = reversed if reversed: self.root = s self.segs.append(s) def update_center(self): for i, s in enumerate(self.segs): if s is self.root: self.center = i return # sort tree segments by angle def partition(self, array, begin, end): pivot = begin for i in range(begin + 1, end + 1): if array[i].a0 < array[begin].a0: pivot += 1 array[i], array[pivot] = array[pivot], array[i] array[pivot], array[begin] = array[begin], array[pivot] return pivot def sort(self): def _quicksort(array, begin, end): if begin >= end: return pivot = self.partition(array, begin, end) _quicksort(array, begin, pivot - 1) _quicksort(array, pivot + 1, end) end = len(self.segs) - 1 _quicksort(self.segs, 0, end) # index of root in segs array self.update_center() class RoofPolygon(CutAblePolygon): """ ccw roof pitch boundary closed by explicit segment handle triangular shape with zero axis length mov <_________________ | /\ | | rot | | left last <> next \/_____axis_______>| node <_____axis________ next | /\ | | rot | | right last <> next mov \/________________>| side angle """ def __init__(self, axis, side, fake_axis=None): """ Create a default rectangle axis from node to next slope float -z for 1 in side direction side in ['LEFT', 'RIGHT'] in axis direction NOTE: when axis length is null (eg: triangular shape) use "fake_axis" with a 1 length to handle distance from segment """ if side == 'LEFT': # slope self.slope = axis.slope_left # width self.width = axis.width_left # constraint width self.auto_mode = axis.auto_left else: # slope self.slope = axis.slope_right # width self.width = axis.width_right # constraint width self.auto_mode = axis.auto_right self.side = side # backward deps self.backward = False # pointers to neighbors along axis self.last = None self.next = None self.other_side = None # axis segment if side == 'RIGHT': self.axis = axis.oposite else: self.axis = axis self.fake_axis = None # _axis is either a fake one or real one # to prevent further check if fake_axis is None: self._axis = self.axis self.fake_axis = self.axis self.next_cross = axis self.last_cross = axis else: if side == 'RIGHT': self.fake_axis = fake_axis.oposite else: self.fake_axis = fake_axis self._axis = self.fake_axis # unit vector perpendicular to axis # looking at outside part v = self.fake_axis.sized_normal(0, -1) self.cross = v self.next_cross = v self.last_cross = v self.convex = True # segments from axis end in ccw order # closed by explicit segment self.segs = [] # holes self.holes = [] # Triangular ends self.node_tri = False self.next_tri = False self.is_tri = False # sizes self.tmin = 0 self.tmax = 1 self.dt = 1 self.ysize = 0 self.xsize = 0 self.vx = Vector() self.vy = Vector() self.vz = Vector() def move_node(self, p): """ Move slope point in node side """ if self.side == 'LEFT': self.segs[-1].p0 = p self.segs[2].p1 = p else: self.segs[2].p0 = p self.segs[1].p1 = p def move_next(self, p): """ Move slope point in next side """ if self.side == 'LEFT': self.segs[2].p0 = p self.segs[1].p1 = p else: self.segs[-1].p0 = p self.segs[2].p1 = p def node_link(self, da): angle_90 = round(pi / 2, 4) if self.side == 'LEFT': idx = -1 else: idx = 1 da = abs(round(da, 4)) type = "LINK" if da < angle_90: type += "_VALLEY" elif da > angle_90: type += "_HIP" self.segs[idx].type = type def next_link(self, da): angle_90 = round(pi / 2, 4) if self.side == 'LEFT': idx = 1 else: idx = -1 da = abs(round(da, 4)) type = "LINK" if da < angle_90: type += "_VALLEY" elif da > angle_90: type += "_HIP" self.segs[idx].type = type def bind(self, last, ccw=False): """ always in axis real direction """ # backward dependency relative to axis if last.backward: self.backward = self.side == last.side if self.side == last.side: last.next_cross = self.cross else: last.last_cross = self.cross self.last_cross = last.cross # axis of last / next segments if self.backward: self.next = last last.last = self else: self.last = last last.next = self # width auto if self.auto_mode == 'AUTO': self.width = last.width self.slope = last.slope elif self.auto_mode == 'WIDTH' and self.width != 0: self.slope = last.slope * last.width / self.width elif self.auto_mode == 'SLOPE' and self.slope != 0: self.width = last.width * last.slope / self.slope self.make_segments() last.make_segments() res, p, t = self.segs[2].intersect(last.segs[2]) if res: # dont move anything when no intersection found # aka when delta angle == 0 self.move_node(p) if self.side != last.side: last.move_node(p) else: last.move_next(p) # Free mode # move border # and find intersections # with sides if self.auto_mode == 'ALL': s0 = self._axis.offset(-self.width) res, p0, t = self.segs[1].intersect(s0) if res: self.segs[2].p0 = p0 self.segs[1].p1 = p0 res, p1, t = self.segs[-1].intersect(s0) if res: self.segs[2].p1 = p1 self.segs[-1].p0 = p1 # /\ # | angle # |____> # # v1 node -> next if self.side == 'LEFT': v1 = self._axis.v else: v1 = -self._axis.v if last.side == self.side: # contiguous, v0 node <- next # half angle between segments if self.side == 'LEFT': v0 = -last._axis.v else: v0 = last._axis.v da = v0.angle_signed(v1) if ccw: if da < 0: da = 2 * pi + da elif da > 0: da = da - 2 * pi last.next_link(0.5 * da) else: # alternate v0 node -> next # half angle between segments if last.side == 'LEFT': v0 = last._axis.v else: v0 = -last._axis.v da = v0.angle_signed(v1) # angle always ccw if ccw: if da < 0: da = 2 * pi + da elif da > 0: da = da - 2 * pi last.node_link(0.5 * da) self.node_link(-0.5 * da) def next_seg(self, index): idx = self.get_index(index + 1) return self.segs[idx] def last_seg(self, index): return self.segs[index - 1] def make_segments(self): if len(self.segs) < 1: s0 = self._axis w = self.width s1 = s0.straight(w, 1).rotate(pi / 2) s1.type = 'SIDE' s3 = s0.straight(w, 0).rotate(pi / 2).oposite s3.type = 'SIDE' s2 = StraightRoof(s1.p1, s3.p0 - s1.p1) s2.type = 'BOTTOM' self.segs = [s0, s1, s2, s3] def move_side(self, pt): """ offset side to point """ s2 = self.segs[2] d0, t = self.distance(s2.p0) d1, t = self.distance(pt) # adjust width and slope according self.width = d1 self.slope = self.slope * d0 / d1 self.segs[2] = s2.offset(d1 - d0) def propagate_backward(self, pt): """ Propagate slope, keep 2d angle of slope Move first point and border keep border parallel adjust slope and next shape """ # distance of p # offset side to point self.move_side(pt) # move verts on node side self.move_next(pt) if self.side == 'LEFT': # move verts on next side res, p, t = self.segs[-1].intersect(self.segs[2]) else: # move verts on next side res, p, t = self.segs[1].intersect(self.segs[2]) if res: self.move_node(p) if self.next is not None and self.next.auto_mode in {'AUTO'}: self.next.propagate_backward(p) def propagate_forward(self, pt): """ Propagate slope, keep 2d angle of slope Move first point and border keep border parallel adjust slope and next shape """ # offset side to point self.move_side(pt) # move verts on node side self.move_node(pt) if self.side == 'LEFT': # move verts on next side res, p, t = self.segs[1].intersect(self.segs[2]) else: # move verts on next side res, p, t = self.segs[-1].intersect(self.segs[2]) if res: self.move_next(p) if self.next is not None and self.next.auto_mode in {'AUTO'}: self.next.propagate_forward(p) def rotate_next_slope(self, a0): """ Rotate next slope part """ if self.side == 'LEFT': s0 = self.segs[1].rotate(a0) s1 = self.segs[2] res, p, t = s1.intersect(s0) else: s0 = self.segs[2] s1 = self.segs[-1] res, p, t = s1.oposite.rotate(-a0).intersect(s0) if res: s1.p0 = p s0.p1 = p if self.next is not None: if self.next.auto_mode == 'ALL': return if self.next.backward: self.next.propagate_backward(p) else: self.next.propagate_forward(p) def rotate_node_slope(self, a0): """ Rotate node slope part """ if self.side == 'LEFT': s0 = self.segs[2] s1 = self.segs[-1] res, p, t = s1.oposite.rotate(-a0).intersect(s0) else: s0 = self.segs[1].rotate(a0) s1 = self.segs[2] res, p, t = s1.intersect(s0) if res: s1.p0 = p s0.p1 = p if self.next is not None: if self.next.auto_mode == 'ALL': return if self.next.backward: self.next.propagate_backward(p) else: self.next.propagate_forward(p) def distance(self, pt): """ distance from axis always use fake_axis here to allow axis being cut and still work """ res, d, t = self.fake_axis.point_sur_segment(pt) return d, t def altitude(self, pt): d, t = self.distance(pt) return -d * self.slope def uv(self, pt): d, t = self.distance(pt) return ((t - self.tmin) * self.xsize, d) def intersect(self, seg): """ compute intersections of a segment with boundaries segment must start on axis return segments inside """ it = [] for s in self.segs: res, p, t, u = seg.intersect_ext(s) if res: it.append((t, p)) return it def merge(self, other): raise NotImplementedError def draw(self, context, z, verts, edges): f = len(verts) # # 0_______1 # |_______| # 3 2 verts.extend([(s.p0.x, s.p0.y, z + self.altitude(s.p0)) for s in self.segs]) n_segs = len(self.segs) - 1 edges.extend([[f + i, f + i + 1] for i in range(n_segs)]) edges.append([f + n_segs, f]) """ f = len(verts) verts.extend([(s.p1.x, s.p1.y, z + self.altitude(s.p1)) for s in self.segs]) n_segs = len(self.segs) - 1 edges.extend([[f + i, f + i + 1] for i in range(n_segs)]) edges.append([f + n_segs, f]) """ # holes for hole in self.holes: f = len(verts) # # 0_______1 # |_______| # 3 2 verts.extend([(s.p0.x, s.p0.y, z + self.altitude(s.p0)) for s in hole.segs]) n_segs = len(hole.segs) - 1 edges.extend([[f + i, f + i + 1] for i in range(n_segs)]) edges.append([f + n_segs, f]) # axis """ f = len(verts) verts.extend([self.axis.p0.to_3d(), self.axis.p1.to_3d()]) edges.append([f, f + 1]) # cross f = len(verts) verts.extend([self.axis.lerp(0.5).to_3d(), (self.axis.lerp(0.5) + self.cross.v).to_3d()]) edges.append([f, f + 1]) """ # relationships arrows if self.next or self.last: w = 0.2 s0 = self._axis.offset(-0.5 * self.ysize) p0 = s0.lerp(0.4).to_3d() p0.z = z p1 = s0.lerp(0.6).to_3d() p1.z = z if self.side == 'RIGHT': p0, p1 = p1, p0 if self.backward: p0, p1 = p1, p0 s1 = s0.sized_normal(0.5, w) s2 = s0.sized_normal(0.5, -w) f = len(verts) p2 = s1.p1.to_3d() p2.z = z p3 = s2.p1.to_3d() p3.z = z verts.extend([p1, p0, p2, p3]) edges.extend([[f + 1, f], [f + 2, f], [f + 3, f]]) def as_string(self): """ Print strips relationships """ if self.backward: dir = "/\\" print("%s next" % (dir)) else: dir = "\\/" print("%s node" % (dir)) print("%s %s" % (dir, self.side)) if self.backward: print("%s node" % (dir)) else: print("%s next" % (dir)) if self.next: print("_________") self.next.as_string() else: print("#########") def limits(self): dist = [] param_t = [] for s in self.segs: res, d, t = self.fake_axis.point_sur_segment(s.p0) param_t.append(t) dist.append(d) if len(param_t) > 0: self.tmin = min(param_t) self.tmax = max(param_t) else: self.tmin = 0 self.tmax = 1 self.dt = self.tmax - self.tmin if len(dist) > 0: self.ysize = max(dist) else: self.ysize = 0 self.xsize = self.fake_axis.length * self.dt # vectors components of part matrix # where x is is axis direction # y down # z up vx = -self.fake_axis.v.normalized().to_3d() vy = Vector((-vx.y, vx.x, self.slope)).normalized() self.vx = vx self.vy = vy self.vz = vx.cross(vy) """ import bmesh m = C.object.data [(round(v.co.x, 3), round(v.co.y, 3), round(v.co.z, 3)) for v in m.vertices] [tuple(p.vertices) for p in m.polygons] uvs = [] bpy.ops.object.mode_set(mode='EDIT') bm = bmesh.from_edit_mesh(m) [tuple(i.index for i in edge.verts) for edge in bm.edges] layer = bm.loops.layers.uv.verify() for i, face in enumerate(bm.faces): uv = [] for j, loop in enumerate(face.loops): co = loop[layer].uv uv.append((round(co.x, 2), round(co.y, 2))) uvs.append(uv) uvs """ class RoofGenerator(CutAbleGenerator): def __init__(self, d, origin=Vector((0, 0, 0))): self.parts = d.parts self.segs = [] self.nodes = [] self.pans = [] self.length = 0 self.origin = origin.to_2d() self.z = origin.z self.width_right = d.width_right self.width_left = d.width_left self.slope_left = d.slope_left self.slope_right = d.slope_right self.user_defined_tile = None self.user_defined_uvs = None self.user_defined_mat = None self.is_t_child = d.t_parent != "" def add_part(self, part): if len(self.segs) < 1 or part.bound_idx < 1: s = None else: s = self.segs[part.bound_idx - 1] a0 = part.a0 if part.constraint_type == 'SLOPE' and a0 == 0: a0 = 90 # start a new roof if s is None: v = part.length * Vector((cos(a0), sin(a0))) s = StraightRoof(self.origin, v) else: s = s.straight_roof(a0, part.length) # parent segment (root) index is v0_idx - 1 s.v0_idx = min(len(self.segs), part.bound_idx) s.constraint_type = part.constraint_type if part.constraint_type == 'SLOPE': s.enforce_part = part.enforce_part else: s.enforce_part = 'AUTO' s.angle_0 = a0 s.take_precedence = part.take_precedence s.auto_right = part.auto_right s.auto_left = part.auto_left s.width_left = part.width_left s.width_right = part.width_right s.slope_left = part.slope_left s.slope_right = part.slope_right s.type = 'AXIS' s.triangular_end = part.triangular_end self.segs.append(s) def locate_manipulators(self): """ """ for i, f in enumerate(self.segs): manipulators = self.parts[i].manipulators p0 = f.p0.to_3d() p0.z = self.z p1 = f.p1.to_3d() p1.z = self.z # angle from last to current segment if i > 0: manipulators[0].type_key = 'ANGLE' v0 = self.segs[f.v0_idx - 1].straight(-1, 1).v.to_3d() v1 = f.straight(1, 0).v.to_3d() manipulators[0].set_pts([p0, v0, v1]) # segment length manipulators[1].type_key = 'SIZE' manipulators[1].prop1_name = "length" manipulators[1].set_pts([p0, p1, (1.0, 0, 0)]) # dumb segment id manipulators[2].set_pts([p0, p1, (1, 0, 0)]) p0 = f.lerp(0.5).to_3d() p0.z = self.z # size left p1 = f.sized_normal(0.5, -self.parts[i].width_left).p1.to_3d() p1.z = self.z manipulators[3].set_pts([p0, p1, (1, 0, 0)]) # size right p1 = f.sized_normal(0.5, self.parts[i].width_right).p1.to_3d() p1.z = self.z manipulators[4].set_pts([p0, p1, (-1, 0, 0)]) # slope left n0 = f.sized_normal(0.5, -1) p0 = n0.p1.to_3d() p0.z = self.z p1 = p0.copy() p1.z = self.z - self.parts[i].slope_left manipulators[5].set_pts([p0, p1, (-1, 0, 0)], normal=n0.v.to_3d()) # slope right n0 = f.sized_normal(0.5, 1) p0 = n0.p1.to_3d() p0.z = self.z p1 = p0.copy() p1.z = self.z - self.parts[i].slope_right manipulators[6].set_pts([p0, p1, (1, 0, 0)], normal=n0.v.to_3d()) def seg_partition(self, array, begin, end): """ sort tree segments by angle """ pivot = begin for i in range(begin + 1, end + 1): if array[i].a0 < array[begin].a0: pivot += 1 array[i], array[pivot] = array[pivot], array[i] array[pivot], array[begin] = array[begin], array[pivot] return pivot def sort_seg(self, array, begin=0, end=None): # print("sort_child") if end is None: end = len(array) - 1 def _quicksort(array, begin, end): if begin >= end: return pivot = self.seg_partition(array, begin, end) _quicksort(array, begin, pivot - 1) _quicksort(array, pivot + 1, end) return _quicksort(array, begin, end) def make_roof(self, context): """ Init data structure for possibly multi branched nodes nodes : radial relationships pans : quad strip linear relationships """ pans = [] # node are connected segments # node # (segment idx) # (angle from root part > 0 right) # (reversed) a seg connected by p1 # "root" of node nodes = [RoofAxisNode() for s in range(len(self.segs) + 1)] # Init width on seg 0 s0 = self.segs[0] if self.parts[0].auto_left in {'AUTO', 'SLOPE'}: s0.width_left = self.width_left if self.parts[0].auto_right in {'AUTO', 'SLOPE'}: s0.width_right = self.width_right if self.parts[0].auto_left in {'AUTO', 'WIDTH'}: s0.slope_left = self.slope_left if self.parts[0].auto_left in {'AUTO', 'WIDTH'}: s0.slope_right = self.slope_right # make nodes with HORIZONTAL constraints for idx, s in enumerate(self.segs): s.v1_idx = idx + 1 if s.constraint_type == 'HORIZONTAL': left = RoofPolygon(s, 'LEFT') right = RoofPolygon(s, 'RIGHT') left.other_side = right right.other_side = left rs = RoofSegment(s, left, right) pans.append(rs) nodes[s.v0_idx].add(s.angle_0, False, s, left, right) nodes[s.v1_idx].add(-pi, True, s, left, right) # set first node root # so regular sort does work nodes[0].root = nodes[0].segs[0] self.nodes = nodes # Propagate slope and width # on node basis along axis # bi-direction Radial around node # from left and right to center # contiguous -> same # T: and (x % 2 == 1) # First one take precedence over others # others inherit from side # # l / rb l = left # 3 r = right # l _1_ / b = backward # r \ # 2 # r\ l # # X: right one or left one l (x % 2 == 0) # inherits from side # # l 3 lb l = left # l__1_|_2_l r = right # r | r b = backward -> propagate in reverse axis direction # r 4 rb # # for idx, node in enumerate(nodes): # print("idx:%s node:%s" % (idx, node.root)) for idx, node in enumerate(nodes): node.sort() nb_segs = node.count if node.root is None: continue left = node.root.left right = node.root.right # basic one single node if nb_segs < 2: left.make_segments() right.make_segments() continue # get "root" slope and width l_bind = left r_bind = right # simple case: 2 contiguous segments if nb_segs == 2: s = node.last s.right.bind(r_bind, ccw=False) s.left.bind(l_bind, ccw=True) continue # More than 2 segments, uneven distribution if nb_segs % 2 == 1: # find which child does take precedence # first one on rootline (arbitrary) center = (nb_segs - 1) / 2 else: # even distribution center = nb_segs / 2 # user defined precedence if any for i, s in enumerate(node.segs): if s.seg.take_precedence: center = i break # bind right side to center for i, s in enumerate(node.segs): # skip axis if i > 0: if i < center: # right contiguous with last s.right.bind(r_bind, ccw=False) # next bind to left r_bind = s.left # left backward, not bound # so setup width and slope if s.left.auto_mode in {'AUTO', 'WIDTH'}: s.left.slope = right.slope if s.left.auto_mode in {'AUTO', 'SLOPE'}: s.left.width = right.width s.left.backward = True else: # right bound to last s.right.bind(r_bind, ccw=False) break # bind left side to center for i, s in enumerate(reversed(node.segs)): # skip axis if i < nb_segs - center - 1: # left contiguous with last s.left.bind(l_bind, ccw=True) # next bind to right l_bind = s.right # right backward, not bound # so setup width and slope if s.right.auto_mode in {'AUTO', 'WIDTH'}: s.right.slope = left.slope if s.right.auto_mode in {'AUTO', 'SLOPE'}: s.right.width = left.width s.right.backward = True else: # right bound to last s.left.bind(l_bind, ccw=True) break # slope constraints allowed between segments # multiple (up to 2) on start and end # single between others # # 2 slope 2 slope 2 slope # | | | # |______section_1___|___section_2_____| # | | | # | | | # multiple single multiple # add slopes constraints to nodes for i, s in enumerate(self.segs): if s.constraint_type == 'SLOPE': nodes[s.v0_idx].add(s.angle_0, False, s, None, None) # sort nodes, remove duplicate slopes between # horizontal, keeping only first one for idx, node in enumerate(nodes): to_remove = [] node.sort() # remove dup between all # but start / end nodes if node.n_horizontal > 1: last = None for i, s in enumerate(node.segs): if s.seg.constraint_type == last: if s.seg.constraint_type == 'SLOPE': to_remove.append(i) last = s.seg.constraint_type for i in reversed(to_remove): node.segs.pop(i) node.update_center() for idx, node in enumerate(nodes): # a node may contain many slopes # 2 * (part starting from node - 1) # # s0 # root 0 |_______ # | # s1 # # s1 # root _______| # | # s0 # # s3 3 s2 # l \l|r/ l # root ___\|/___ 2 # r /|\ r # /r|l\ # s0 1 s1 # # s2 s1=slope # |r / # | / l # |/____s # # root to first child -> equal side # any other childs -> oposite sides if node.n_horizontal == 1: # slopes at start or end of segment # segment slope is not affected if node.n_slope > 0: # node has user def slope s = node.root s0 = node.left(node.center) a0 = s0.seg.delta_angle(s.seg) if node.root.reversed: # slope at end of segment # first one is right or left if a0 < 0: # right side res, p, t = s0.seg.intersect(s.right.segs[2]) s.right.segs[-1].p0 = p s.right.segs[2].p1 = p else: # left side res, p, t = s0.seg.intersect(s.left.segs[2]) s.left.segs[1].p1 = p s.left.segs[2].p0 = p if node.n_slope > 1: # last one must be left s1 = node.right(node.center) a1 = s1.seg.delta_angle(s.seg) # both slopes on same side: # skip this one if a0 > 0 and a1 < 0: # right side res, p, t = s1.seg.intersect(s.right.segs[2]) s.right.segs[-1].p0 = p s.right.segs[2].p1 = p if a0 < 0 and a1 > 0: # left side res, p, t = s1.seg.intersect(s.left.segs[2]) s.left.segs[1].p1 = p s.left.segs[2].p0 = p else: # slope at start of segment if a0 < 0: # right side res, p, t = s0.seg.intersect(s.right.segs[2]) s.right.segs[1].p1 = p s.right.segs[2].p0 = p else: # left side res, p, t = s0.seg.intersect(s.left.segs[2]) s.left.segs[-1].p0 = p s.left.segs[2].p1 = p if node.n_slope > 1: # last one must be right s1 = node.right(node.center) a1 = s1.seg.delta_angle(s.seg) # both slopes on same side: # skip this one if a0 > 0 and a1 < 0: # right side res, p, t = s1.seg.intersect(s.right.segs[2]) s.right.segs[1].p1 = p s.right.segs[2].p0 = p if a0 < 0 and a1 > 0: # left side res, p, t = s1.seg.intersect(s.left.segs[2]) s.left.segs[-1].p0 = p s.left.segs[2].p1 = p else: # slopes between segments # does change next segment slope for i, s0 in enumerate(node.segs): s1 = node.left(i) s2 = node.left(i + 1) if s1.seg.constraint_type == 'SLOPE': # 3 cases: # s0 is root contiguous -> sides are same # s2 is root contiguous -> sides are same # back to back -> sides are not same if s0.reversed: # contiguous right / right # 2 cases # right is backward # right is forward if s2.right.backward: # s0 depends on s2 main = s2.right v = main.segs[1].v else: # s2 depends on s0 main = s0.right v = -main.segs[-1].v res, p, t = s1.seg.intersect(main.segs[2]) if res: # slope vector dp = p - s1.seg.p0 a0 = dp.angle_signed(v) if s2.right.backward: main.rotate_node_slope(a0) else: main.rotate_next_slope(-a0) elif s2.reversed: # contiguous left / left # 2 cases # left is backward # left is forward if s0.left.backward: # s0 depends on s2 main = s0.left v = -main.segs[-1].v else: # s2 depends on s0 main = s2.left v = main.segs[1].v res, p, t = s1.seg.intersect(main.segs[2]) if res: # slope vector dp = p - s1.seg.p0 a0 = dp.angle_signed(v) if s0.left.backward: main.rotate_node_slope(-a0) else: main.rotate_next_slope(a0) else: # back left / right # 2 cases # left is backward # left is forward if s0.left.backward: # s2 depends on s0 main = s0.left v = -main.segs[-1].v else: # s0 depends on s2 main = s2.right v = main.segs[1].v res, p, t = s1.seg.intersect(main.segs[2]) if res: # slope vector dp = p - s1.seg.p0 a0 = dp.angle_signed(v) if s0.left.backward: main.rotate_node_slope(-a0) else: main.rotate_node_slope(a0) self.pans = [] # triangular ends for node in self.nodes: if node.root is None: continue if node.n_horizontal == 1 and node.root.seg.triangular_end: if node.root.reversed: # Next side (segment end) left = node.root.left right = node.root.right left.next_tri = True right.next_tri = True s0 = left.segs[1] s1 = left.segs[2] s2 = right.segs[-1] s3 = right.segs[2] p0 = s1.lerp(-left.width / s1.length) p1 = s0.p0 p2 = s3.lerp(1 + right.width / s3.length) # compute slope from points p3 = p0.to_3d() p3.z = -left.width * left.slope p4 = p1.to_3d() p5 = p2.to_3d() p5.z = -right.width * right.slope n = (p3 - p4).normalized().cross((p5 - p4).normalized()) v = n.cross(Vector((0, 0, 1))) dz = n.cross(v) # compute axis s = StraightRoof(p1, v) res, d0, t = s.point_sur_segment(p0) res, d1, t = s.point_sur_segment(p2) p = RoofPolygon(s, 'RIGHT') p.make_segments() p.slope = -dz.z / dz.to_2d().length p.is_tri = True p.cross = StraightRoof(p1, (p2 - p0)).sized_normal(0, -1) p.next_cross = left.cross p.last_cross = right.cross right.next_cross = p.cross left.next_cross = p.cross # remove axis seg of tri p.segs[-1].p0 = p0 p.segs[-1].p1 = p1 p.segs[2].p0 = p2 p.segs[2].p1 = p0 p.segs[1].p1 = p2 p.segs[1].p0 = p1 p.segs[1].type = 'LINK_HIP' p.segs[-1].type = 'LINK_HIP' p.segs.pop(0) # adjust left and side borders s0.p1 = p0 s1.p0 = p0 s2.p0 = p2 s3.p1 = p2 s0.type = 'LINK_HIP' s2.type = 'LINK_HIP' self.pans.append(p) elif not self.is_t_child: # no triangular part with t_child # on "node" parent roof side left = node.root.left right = node.root.right left.node_tri = True right.node_tri = True s0 = right.segs[1] s1 = right.segs[2] s2 = left.segs[-1] s3 = left.segs[2] p0 = s1.lerp(-right.width / s1.length) p1 = s0.p0 p2 = s3.lerp(1 + left.width / s3.length) # compute axis and slope from points p3 = p0.to_3d() p3.z = -right.width * right.slope p4 = p1.to_3d() p5 = p2.to_3d() p5.z = -left.width * left.slope n = (p3 - p4).normalized().cross((p5 - p4).normalized()) v = n.cross(Vector((0, 0, 1))) dz = n.cross(v) s = StraightRoof(p1, v) p = RoofPolygon(s, 'RIGHT') p.make_segments() p.slope = -dz.z / dz.to_2d().length p.is_tri = True p.cross = StraightRoof(p1, (p2 - p0)).sized_normal(0, -1) p.next_cross = right.cross p.last_cross = left.cross right.last_cross = p.cross left.last_cross = p.cross # remove axis seg of tri p.segs[-1].p0 = p0 p.segs[-1].p1 = p1 p.segs[2].p0 = p2 p.segs[2].p1 = p0 p.segs[1].p1 = p2 p.segs[1].p0 = p1 p.segs[1].type = 'LINK_HIP' p.segs[-1].type = 'LINK_HIP' p.segs.pop(0) # adjust left and side borders s0.p1 = p0 s1.p0 = p0 s2.p0 = p2 s3.p1 = p2 s0.type = 'LINK_HIP' s2.type = 'LINK_HIP' self.pans.append(p) # make flat array for pan in pans: self.pans.extend([pan.left, pan.right]) # merge contiguous with 0 angle diff to_remove = [] for i, pan in enumerate(self.pans): if pan.backward: next = pan.last if next is not None: # same side only can merge if next.side == pan.side: if round(next._axis.delta_angle(pan._axis), 4) == 0: to_remove.append(i) next.next = pan.next next.last_cross = pan.last_cross next.node_tri = pan.node_tri next.slope = pan.slope if pan.side == 'RIGHT': if next.backward: next._axis.p1 = pan._axis.p1 next.segs[1] = pan.segs[1] next.segs[2].p0 = pan.segs[2].p0 else: next._axis.p0 = pan._axis.p0 next.segs[-1] = pan.segs[-1] next.segs[2].p1 = pan.segs[2].p1 else: if next.backward: next._axis.p0 = pan._axis.p0 next.segs[-1] = pan.segs[-1] next.segs[2].p1 = pan.segs[2].p1 else: next._axis.p1 = pan._axis.p1 next.segs[1] = pan.segs[1] next.segs[2].p0 = pan.segs[2].p0 else: next = pan.next if next is not None: # same side only can merge if next.side == pan.side: if round(next._axis.delta_angle(pan._axis), 4) == 0: to_remove.append(i) next.last = pan.last next.last_cross = pan.last_cross next.node_tri = pan.node_tri next.slope = pan.slope if pan.side == 'LEFT': if next.backward: next._axis.p1 = pan._axis.p1 next.segs[1] = pan.segs[1] next.segs[2].p0 = pan.segs[2].p0 else: next._axis.p0 = pan._axis.p0 next.segs[-1] = pan.segs[-1] next.segs[2].p1 = pan.segs[2].p1 else: if next.backward: next._axis.p0 = pan._axis.p0 next.segs[-1] = pan.segs[-1] next.segs[2].p1 = pan.segs[2].p1 else: next._axis.p1 = pan._axis.p1 next.segs[1] = pan.segs[1] next.segs[2].p0 = pan.segs[2].p0 for i in reversed(to_remove): self.pans.pop(i) # compute limits for pan in self.pans: pan.limits() """ for pan in self.pans: if pan.last is None: pan.as_string() """ return def lambris(self, context, o, d): idmat = 0 lambris_height = 0.02 alt = self.z - lambris_height for pan in self.pans: verts = [] faces = [] matids = [] uvs = [] f = len(verts) verts.extend([(s.p0.x, s.p0.y, alt + pan.altitude(s.p0)) for s in pan.segs]) uvs.append([pan.uv(s.p0) for s in pan.segs]) n_segs = len(pan.segs) face = [f + i for i in range(n_segs)] faces.append(face) matids.append(idmat) bm = bmed.buildmesh( context, o, verts, faces, matids=matids, uvs=uvs, weld=False, clean=False, auto_smooth=True, temporary=True) self.cut_holes(bm, pan) bmesh.ops.dissolve_limit(bm, angle_limit=0.01, use_dissolve_boundaries=False, verts=bm.verts, edges=bm.edges, delimit={'MATERIAL'}) geom = bm.faces[:] verts = bm.verts[:] bmesh.ops.solidify(bm, geom=geom, thickness=0.0001) bmesh.ops.translate(bm, vec=Vector((0, 0, lambris_height)), space=o.matrix_world, verts=verts) # merge with object bmed.bmesh_join(context, o, [bm], normal_update=True) bpy.ops.object.mode_set(mode='OBJECT') def couverture(self, context, o, d): idmat = 7 rand = 3 ttl = len(self.pans) if ttl < 1: return sx, sy, sz = d.tile_size_x, d.tile_size_y, d.tile_size_z """ /* Bevel offset_type slot values */ enum { BEVEL_AMT_OFFSET, BEVEL_AMT_WIDTH, BEVEL_AMT_DEPTH, BEVEL_AMT_PERCENT }; """ offset_type = 'PERCENT' if d.tile_offset > 0: offset = - d.tile_offset / 100 else: offset = 0 if d.tile_model == 'BRAAS2': t_pts = [Vector(p) for p in [ (0.06, -1.0, 1.0), (0.19, -1.0, 0.5), (0.31, -1.0, 0.5), (0.44, -1.0, 1.0), (0.56, -1.0, 1.0), (0.69, -1.0, 0.5), (0.81, -1.0, 0.5), (0.94, -1.0, 1.0), (0.06, 0.0, 0.5), (0.19, 0.0, 0.0), (0.31, 0.0, 0.0), (0.44, 0.0, 0.5), (0.56, 0.0, 0.5), (0.69, 0.0, 0.0), (0.81, 0.0, 0.0), (0.94, 0.0, 0.5), (-0.0, -1.0, 1.0), (-0.0, 0.0, 0.5), (1.0, -1.0, 1.0), (1.0, 0.0, 0.5)]] t_faces = [ (16, 0, 8, 17), (0, 1, 9, 8), (1, 2, 10, 9), (2, 3, 11, 10), (3, 4, 12, 11), (4, 5, 13, 12), (5, 6, 14, 13), (6, 7, 15, 14), (7, 18, 19, 15)] elif d.tile_model == 'BRAAS1': t_pts = [Vector(p) for p in [ (0.1, -1.0, 1.0), (0.2, -1.0, 0.5), (0.6, -1.0, 0.5), (0.7, -1.0, 1.0), (0.1, 0.0, 0.5), (0.2, 0.0, 0.0), (0.6, 0.0, 0.0), (0.7, 0.0, 0.5), (-0.0, -1.0, 1.0), (-0.0, 0.0, 0.5), (1.0, -1.0, 1.0), (1.0, 0.0, 0.5)]] t_faces = [(8, 0, 4, 9), (0, 1, 5, 4), (1, 2, 6, 5), (2, 3, 7, 6), (3, 10, 11, 7)] elif d.tile_model == 'ETERNIT': t_pts = [Vector(p) for p in [ (0.11, -1.0, 1.0), (0.9, -1.0, 1.0), (0.0, -0.79, 0.79), (1.0, -0.79, 0.79), (0.0, 2.0, -2.0), (1.0, 2.0, -2.0)]] t_faces = [(0, 1, 3, 5, 4, 2)] elif d.tile_model == 'ONDULEE': t_pts = [Vector(p) for p in [ (0.0, -1.0, 0.1), (0.05, -1.0, 1.0), (0.1, -1.0, 0.1), (0.15, -1.0, 1.0), (0.2, -1.0, 0.1), (0.25, -1.0, 1.0), (0.3, -1.0, 0.1), (0.35, -1.0, 1.0), (0.4, -1.0, 0.1), (0.45, -1.0, 1.0), (0.5, -1.0, 0.1), (0.55, -1.0, 1.0), (0.6, -1.0, 0.1), (0.65, -1.0, 1.0), (0.7, -1.0, 0.1), (0.75, -1.0, 1.0), (0.8, -1.0, 0.1), (0.85, -1.0, 1.0), (0.9, -1.0, 0.1), (0.95, -1.0, 1.0), (1.0, -1.0, 0.1), (0.0, 0.0, 0.0), (0.05, 0.0, 0.9), (0.1, 0.0, 0.0), (0.15, 0.0, 0.9), (0.2, 0.0, 0.0), (0.25, 0.0, 0.9), (0.3, 0.0, 0.0), (0.35, 0.0, 0.9), (0.4, 0.0, 0.0), (0.45, 0.0, 0.9), (0.5, 0.0, 0.0), (0.55, 0.0, 0.9), (0.6, 0.0, 0.0), (0.65, 0.0, 0.9), (0.7, 0.0, 0.0), (0.75, 0.0, 0.9), (0.8, 0.0, 0.0), (0.85, 0.0, 0.9), (0.9, 0.0, 0.0), (0.95, 0.0, 0.9), (1.0, 0.0, 0.0)]] t_faces = [ (0, 1, 22, 21), (1, 2, 23, 22), (2, 3, 24, 23), (3, 4, 25, 24), (4, 5, 26, 25), (5, 6, 27, 26), (6, 7, 28, 27), (7, 8, 29, 28), (8, 9, 30, 29), (9, 10, 31, 30), (10, 11, 32, 31), (11, 12, 33, 32), (12, 13, 34, 33), (13, 14, 35, 34), (14, 15, 36, 35), (15, 16, 37, 36), (16, 17, 38, 37), (17, 18, 39, 38), (18, 19, 40, 39), (19, 20, 41, 40)] elif d.tile_model == 'METAL': t_pts = [Vector(p) for p in [ (0.0, -1.0, 0.0), (0.99, -1.0, 0.0), (1.0, -1.0, 0.0), (0.0, 0.0, 0.0), (0.99, 0.0, 0.0), (1.0, 0.0, 0.0), (0.99, -1.0, 1.0), (1.0, -1.0, 1.0), (1.0, 0.0, 1.0), (0.99, 0.0, 1.0)]] t_faces = [(0, 1, 4, 3), (7, 2, 5, 8), (1, 6, 9, 4), (6, 7, 8, 9)] elif d.tile_model == 'LAUZE': t_pts = [Vector(p) for p in [ (0.75, -0.8, 0.8), (0.5, -1.0, 1.0), (0.25, -0.8, 0.8), (0.0, -0.5, 0.5), (1.0, -0.5, 0.5), (0.0, 0.5, -0.5), (1.0, 0.5, -0.5)]] t_faces = [(1, 0, 4, 6, 5, 3, 2)] elif d.tile_model == 'PLACEHOLDER': t_pts = [Vector(p) for p in [(0.0, -1.0, 1.0), (1.0, -1.0, 1.0), (0.0, 0.0, 0.0), (1.0, 0.0, 0.0)]] t_faces = [(0, 1, 3, 2)] elif d.tile_model == 'ROMAN': t_pts = [Vector(p) for p in [ (0.18, 0.0, 0.3), (0.24, 0.0, 0.58), (0.76, 0.0, 0.58), (0.82, 0.0, 0.3), (0.05, -1.0, 0.5), (0.14, -1.0, 0.8), (0.86, -1.0, 0.8), (0.95, -1.0, 0.5), (0.45, 0.0, 0.5), (0.36, 0.0, 0.2), (-0.36, 0.0, 0.2), (-0.45, -0.0, 0.5), (0.32, -1.0, 0.7), (0.26, -1.0, 0.42), (-0.26, -1.0, 0.42), (-0.32, -1.0, 0.7), (0.5, 0.0, 0.74), (0.5, -1.0, 1.0), (-0.0, -1.0, 0.26), (-0.0, 0.0, 0.0)] ] t_faces = [ (0, 4, 5, 1), (16, 17, 6, 2), (2, 6, 7, 3), (13, 12, 8, 9), (18, 13, 9, 19), (15, 14, 10, 11), (14, 18, 19, 10), (1, 5, 17, 16) ] elif d.tile_model == 'ROUND': t_pts = [Vector(p) for p in [ (0.0, -0.5, 0.5), (1.0, -0.5, 0.5), (0.0, 0.0, 0.0), (1.0, 0.0, 0.0), (0.93, -0.71, 0.71), (0.78, -0.88, 0.88), (0.39, -0.97, 0.97), (0.61, -0.97, 0.97), (0.07, -0.71, 0.71), (0.22, -0.88, 0.88)] ] t_faces = [(6, 7, 5, 4, 1, 3, 2, 0, 8, 9)] else: return n_faces = len(t_faces) t_uvs = [[(t_pts[i].x, t_pts[i].y) for i in f] for f in t_faces] dx, dy = d.tile_space_x, d.tile_space_y step = 100 / ttl # if d.quick_edit: # context.scene.archipack_progress_text = "Build tiles:" for i, pan in enumerate(self.pans): seg = pan.fake_axis # compute base matrix top left of face vx = pan.vx vy = pan.vy vz = pan.vz x0, y0 = seg.lerp(pan.tmax) z0 = self.z + d.tile_altitude ysize_2d = (d.tile_border + pan.ysize) space_x = pan.xsize + 2 * d.tile_side space_y = ysize_2d * sqrt(1 + pan.slope * pan.slope) n_x = 1 + int(space_x / dx) n_y = 1 + int(space_y / dy) if d.tile_fit_x: dx = space_x / n_x if d.tile_fit_y: dy = space_y / n_y if d.tile_alternate: n_y += 1 tM = Matrix([ [vx.x, vy.x, vz.x, x0], [vx.y, vy.y, vz.y, y0], [vx.z, vy.z, vz.z, z0], [0, 0, 0, 1] ]) verts = [] faces = [] matids = [] uvs = [] # steps for this pan substep = step / n_y # print("step:%s sub:%s" % (step, substep)) for k in range(n_y): progress = step * i + substep * k # print("progress %s" % (progress)) # if d.quick_edit: # context.scene.archipack_progress = progress y = k * dy x0 = offset * dx - d.tile_side nx = n_x if d.tile_alternate and k % 2 == 1: x0 -= 0.5 * dx nx += 1 if d.tile_offset > 0: nx += 1 for j in range(nx): x = x0 + j * dx lM = tM @ Matrix([ [sx, 0, 0, x], [0, sy, 0, -y], [0, 0, sz, 0], [0, 0, 0, 1] ]) v = len(verts) verts.extend([lM @ p for p in t_pts]) faces.extend([tuple(i + v for i in f) for f in t_faces]) mid = randint(idmat, idmat + rand) t_mats = [mid for i in range(n_faces)] matids.extend(t_mats) uvs.extend(t_uvs) # build temp bmesh and bissect bm = bmed.buildmesh( context, o, verts, faces, matids=matids, uvs=uvs, weld=False, clean=False, auto_smooth=True, temporary=True) # clean outer on convex parts # pan.convex = False remove = pan.convex for s in pan.segs: # seg without length lead to invalid normal if s.length > 0: if s.type == 'AXIS': self.bissect(bm, s.p1.to_3d(), s.cross_z.to_3d(), clear_outer=remove) elif s.type == 'BOTTOM': s0 = s.offset(d.tile_border) dz = pan.altitude(s0.p0) vx = s0.v.to_3d() vx.z = pan.altitude(s0.p1) - dz vy = vz.cross(vx.normalized()) x, y = s0.p0 z = z0 + dz self.bissect(bm, Vector((x, y, z)), -vy, clear_outer=remove) elif s.type == 'SIDE': p0 = s.p0 + s.cross_z.normalized() * d.tile_side self.bissect(bm, p0.to_3d(), s.cross_z.to_3d(), clear_outer=remove) elif s.type == 'LINK_VALLEY': p0 = s.p0 - s.cross_z.normalized() * d.tile_couloir self.bissect(bm, p0.to_3d(), s.cross_z.to_3d(), clear_outer=remove) elif s.type in {'LINK_HIP', 'LINK'}: self.bissect(bm, s.p0.to_3d(), s.cross_z.to_3d(), clear_outer=remove) # when not convex, select and remove outer parts if not pan.convex: """ /* del "context" slot values, used for operator too */ enum { DEL_VERTS = 1, DEL_EDGES, DEL_ONLYFACES, DEL_EDGESFACES, DEL_FACES, /* A version of 'DEL_FACES' that keeps edges on face boundaries, * allowing the surrounding edge-loop to be kept from removed face regions. */ DEL_FACES_KEEP_BOUNDARY, DEL_ONLYTAGGED }; """ # Build boundary including borders and bottom offsets new_s = None segs = [] for s in pan.segs: if s.length > 0: if s.type == 'LINK_VALLEY': offset = -d.tile_couloir elif s.type == 'BOTTOM': offset = d.tile_border elif s.type == 'SIDE': offset = d.tile_side else: offset = 0 new_s = s.make_offset(offset, new_s) segs.append(new_s) if len(segs) > 0: # last / first intersection res, p, t = segs[0].intersect(segs[-1]) if res: segs[0].p0 = p segs[-1].p1 = p f_geom = [f for f in bm.faces if not pan.inside(f.calc_center_median().to_2d(), segs)] if len(f_geom) > 0: bmesh.ops.delete(bm, geom=f_geom, context="FACES") self.cut_holes(bm, pan) bmesh.ops.dissolve_limit(bm, angle_limit=0.01, use_dissolve_boundaries=False, verts=bm.verts[:], edges=bm.edges[:], delimit={'MATERIAL'}) if d.tile_bevel: geom = bm.verts[:] geom.extend(bm.edges[:]) bmesh.ops.bevel(bm, geom=geom, offset=d.tile_bevel_amt, offset_type=offset_type, segments=d.tile_bevel_segs, profile=0.5, vertex_only=False, clamp_overlap=True, material=-1) if d.tile_solidify: geom = bm.faces[:] verts = bm.verts[:] bmesh.ops.solidify(bm, geom=geom, thickness=0.0001) bmesh.ops.translate(bm, vec=vz * d.tile_height, space=o.matrix_world, verts=verts) # merge with object bmed.bmesh_join(context, o, [bm], normal_update=True) bpy.ops.object.mode_set(mode='OBJECT') # if d.quick_edit: # context.scene.archipack_progress = -1 def _bargeboard(self, s, i, boundary, pan, width, height, altitude, offset, idmat, verts, faces, edges, matids, uvs): f = len(verts) s0 = s.offset(offset - width) s1 = s.offset(offset) p0 = s0.p0 p1 = s1.p0 p2 = s0.p1 p3 = s1.p1 s2 = boundary.last_seg(i) s3 = boundary.next_seg(i) if s2.type == 'SIDE': # intersect last seg offset s4 = s2.offset(offset - width) s5 = s2.offset(offset) res, p, t = s4.intersect(s0) if res: p0 = p res, p, t = s5.intersect(s1) if res: p1 = p elif s2.type == 'AXIS' or 'LINK' in s2.type: # intersect axis or link seg res, p, t = s2.intersect(s0) if res: p0 = p res, p, t = s2.intersect(s1) if res: p1 = p if s3.type == 'SIDE': # intersect next seg offset s4 = s3.offset(offset - width) s5 = s3.offset(offset) res, p, t = s4.intersect(s0) if res: p2 = p res, p, t = s5.intersect(s1) if res: p3 = p elif s3.type == 'AXIS' or 'LINK' in s3.type: # intersect axis or link seg res, p, t = s3.intersect(s0) if res: p2 = p res, p, t = s3.intersect(s1) if res: p3 = p x0, y0 = p0 x1, y1 = p1 x2, y2 = p3 x3, y3 = p2 z0 = self.z + altitude + pan.altitude(p0) z1 = self.z + altitude + pan.altitude(p1) z2 = self.z + altitude + pan.altitude(p3) z3 = self.z + altitude + pan.altitude(p2) verts.extend([ (x0, y0, z0), (x1, y1, z1), (x2, y2, z2), (x3, y3, z3), ]) z0 -= height z1 -= height z2 -= height z3 -= height verts.extend([ (x0, y0, z0), (x1, y1, z1), (x2, y2, z2), (x3, y3, z3), ]) faces.extend([ # top (f, f + 1, f + 2, f + 3), # sides (f, f + 4, f + 5, f + 1), (f + 1, f + 5, f + 6, f + 2), (f + 2, f + 6, f + 7, f + 3), (f + 3, f + 7, f + 4, f), # bottom (f + 4, f + 7, f + 6, f + 5) ]) edges.append([f, f + 3]) edges.append([f + 1, f + 2]) edges.append([f + 4, f + 7]) edges.append([f + 5, f + 6]) matids.extend([idmat, idmat, idmat, idmat, idmat, idmat]) uvs.extend([ [(0, 0), (0, 1), (1, 1), (1, 0)], [(0, 0), (0, 1), (1, 1), (1, 0)], [(0, 0), (0, 1), (1, 1), (1, 0)], [(0, 0), (0, 1), (1, 1), (1, 0)], [(0, 0), (0, 1), (1, 1), (1, 0)], [(0, 0), (0, 1), (1, 1), (1, 0)] ]) def bargeboard(self, d, verts, faces, edges, matids, uvs): ##################### # Vire-vents ##################### idmat = 1 for pan in self.pans: for hole in pan.holes: for i, s in enumerate(hole.segs): if s.type == 'SIDE': self._bargeboard(s, i, hole, pan, d.bargeboard_width, d.bargeboard_height, d.bargeboard_altitude, d.bargeboard_offset, idmat, verts, faces, edges, matids, uvs) for i, s in enumerate(pan.segs): if s.type == 'SIDE': self._bargeboard(s, i, pan, pan, d.bargeboard_width, d.bargeboard_height, d.bargeboard_altitude, d.bargeboard_offset, idmat, verts, faces, edges, matids, uvs) def _fascia(self, s, i, boundary, pan, tri_0, tri_1, width, height, altitude, offset, idmat, verts, faces, edges, matids, uvs): f = len(verts) s0 = s.offset(offset) s1 = s.offset(offset + width) s2 = boundary.last_seg(i) s3 = boundary.next_seg(i) s4 = s2 s5 = s3 p0 = s0.p0 p1 = s1.p0 p2 = s0.p1 p3 = s1.p1 # find last neighbor depending on type if s2.type == 'AXIS' or 'LINK' in s2.type: # apply only on boundaries if not s.is_hole: # use last axis if pan.side == 'LEFT': s6 = pan.next_cross else: s6 = pan.last_cross if tri_0: s2 = s.copy else: s2 = s2.oposite s2.v = (s.sized_normal(0, 1).v + s6.v).normalized() s4 = s2 elif s2.type == 'SIDE': s2 = s.copy s2.type = 'SIDE' s2.v = s.sized_normal(0, 1).v s4 = s2 else: s2 = s2.offset(offset) s4 = s2.offset(offset + width) # find next neighbor depending on type if s3.type == 'AXIS' or 'LINK' in s3.type: if not s.is_hole: # use last axis if pan.side == 'LEFT': s6 = pan.last_cross else: s6 = pan.next_cross if tri_1: s3 = s.oposite else: s3 = s3.copy s3.v = (s.sized_normal(0, 1).v + s6.v).normalized() s5 = s3 elif s3.type == 'SIDE': # when next is side, use perpendicular s3 = s.oposite s3.type = 'SIDE' s3.v = s.sized_normal(0, 1).v s5 = s3 else: s3 = s3.offset(offset) s5 = s3.offset(offset + width) # units vectors and scale # is unit normal on sides # print("s.p:%s, s.v:%s s1.p::%s s1.v::%s" % (s.p, s.v, s1.p, s1.v)) res, p, t = s0.intersect(s2) if res: p0 = p res, p, t = s0.intersect(s3) if res: p1 = p res, p, t = s1.intersect(s4) if res: p2 = p res, p, t = s1.intersect(s5) if res: p3 = p x0, y0 = p0 x1, y1 = p2 x2, y2 = p3 x3, y3 = p1 z0 = self.z + altitude + pan.altitude(p0) z1 = self.z + altitude + pan.altitude(p2) z2 = self.z + altitude + pan.altitude(p3) z3 = self.z + altitude + pan.altitude(p1) verts.extend([ (x0, y0, z0), (x1, y1, z1), (x2, y2, z2), (x3, y3, z3), ]) z0 -= height z1 -= height z2 -= height z3 -= height verts.extend([ (x0, y0, z0), (x1, y1, z1), (x2, y2, z2), (x3, y3, z3), ]) faces.extend([ # top (f, f + 1, f + 2, f + 3), # sides (f, f + 4, f + 5, f + 1), (f + 1, f + 5, f + 6, f + 2), (f + 2, f + 6, f + 7, f + 3), (f + 3, f + 7, f + 4, f), # bottom (f + 4, f + 7, f + 6, f + 5) ]) edges.append([f, f + 3]) edges.append([f + 1, f + 2]) edges.append([f + 4, f + 7]) edges.append([f + 5, f + 6]) matids.extend([idmat, idmat, idmat, idmat, idmat, idmat]) uvs.extend([ [(0, 0), (0, 1), (1, 1), (1, 0)], [(0, 0), (0, 1), (1, 1), (1, 0)], [(0, 0), (0, 1), (1, 1), (1, 0)], [(0, 0), (0, 1), (1, 1), (1, 0)], [(0, 0), (0, 1), (1, 1), (1, 0)], [(0, 0), (0, 1), (1, 1), (1, 0)] ]) def fascia(self, d, verts, faces, edges, matids, uvs): ##################### # Larmiers ##################### idmat = 2 for pan in self.pans: for hole in pan.holes: for i, s in enumerate(hole.segs): if s.type == 'BOTTOM': self._fascia(s, i, hole, pan, False, False, d.fascia_width, d.fascia_height, d.fascia_altitude, d.fascia_offset, idmat, verts, faces, edges, matids, uvs) for i, s in enumerate(pan.segs): if s.type == 'BOTTOM': tri_0 = pan.node_tri tri_1 = pan.next_tri # triangular ends apply on boundary only # unless cut, boundary is parallel to axis # except for triangular ends if pan.side == 'LEFT': tri_0, tri_1 = tri_1, tri_0 self._fascia(s, i, pan, pan, tri_0, tri_1, d.fascia_width, d.fascia_height, d.fascia_altitude, d.fascia_offset, idmat, verts, faces, edges, matids, uvs) continue f = len(verts) s0 = s.offset(d.fascia_width) s1 = pan.last_seg(i) s2 = pan.next_seg(i) # triangular ends apply on boundary only # unless cut, boundary is parallel to axis # except for triangular ends tri_0 = (pan.node_tri and not s.is_hole) or pan.is_tri tri_1 = (pan.next_tri and not s.is_hole) or pan.is_tri if pan.side == 'LEFT': tri_0, tri_1 = tri_1, tri_0 # tiangular use bottom segment direction # find last neighbor depending on type if s1.type == 'AXIS' or 'LINK' in s1.type: # apply only on boundaries if not s.is_hole: # use last axis if pan.side == 'LEFT': s3 = pan.next_cross else: s3 = pan.last_cross if tri_0: s1 = s.copy else: s1 = s1.oposite s1.v = (s.sized_normal(0, 1).v + s3.v).normalized() elif s1.type == 'SIDE': s1 = s.copy s1.type = 'SIDE' s1.v = s.sized_normal(0, 1).v else: s1 = s1.offset(d.fascia_width) # find next neighbor depending on type if s2.type == 'AXIS' or 'LINK' in s2.type: if not s.is_hole: # use last axis if pan.side == 'LEFT': s3 = pan.last_cross else: s3 = pan.next_cross if tri_1: s2 = s.oposite else: s2 = s2.copy s2.v = (s.sized_normal(0, 1).v + s3.v).normalized() elif s2.type == 'SIDE': s2 = s.oposite s2.type = 'SIDE' s2.v = s.sized_normal(0, 1).v else: s2 = s2.offset(d.fascia_width) # units vectors and scale # is unit normal on sides # print("s.p:%s, s.v:%s s1.p::%s s1.v::%s" % (s.p, s.v, s1.p, s1.v)) res, p0, t = s0.intersect(s1) res, p1, t = s0.intersect(s2) x0, y0 = s.p0 x1, y1 = p0 x2, y2 = p1 x3, y3 = s.p1 z0 = self.z + d.fascia_altitude + pan.altitude(s.p0) z1 = self.z + d.fascia_altitude + pan.altitude(s.p1) verts.extend([ (x0, y0, z0), (x1, y1, z0), (x2, y2, z1), (x3, y3, z1), ]) z0 -= d.fascia_height z1 -= d.fascia_height verts.extend([ (x0, y0, z0), (x1, y1, z0), (x2, y2, z1), (x3, y3, z1), ]) faces.extend([ # top (f, f + 1, f + 2, f + 3), # sides (f, f + 4, f + 5, f + 1), (f + 1, f + 5, f + 6, f + 2), (f + 2, f + 6, f + 7, f + 3), (f + 3, f + 7, f + 4, f), # bottom (f + 4, f + 7, f + 6, f + 5) ]) edges.append([f, f + 3]) edges.append([f + 1, f + 2]) edges.append([f + 4, f + 7]) edges.append([f + 5, f + 6]) matids.extend([idmat, idmat, idmat, idmat, idmat, idmat]) uvs.extend([ [(0, 0), (0, 1), (1, 1), (1, 0)], [(0, 0), (0, 1), (1, 1), (1, 0)], [(0, 0), (0, 1), (1, 1), (1, 0)], [(0, 0), (0, 1), (1, 1), (1, 0)], [(0, 0), (0, 1), (1, 1), (1, 0)], [(0, 0), (0, 1), (1, 1), (1, 0)] ]) def gutter(self, d, verts, faces, edges, matids, uvs): ##################### # Chenaux ##################### idmat = 5 # caps at start and end if d.gutter_segs % 2 == 1: n_faces = int((d.gutter_segs - 1) / 2) else: n_faces = int((d.gutter_segs / 2) - 1) df = 2 * d.gutter_segs + 1 for pan in self.pans: for i, s in enumerate(pan.segs): if s.type == 'BOTTOM': f = len(verts) s0 = s.offset(d.gutter_dist + d.gutter_width) s1 = pan.last_seg(i) s2 = pan.next_seg(i) p0 = s0.p0 p1 = s0.p1 tri_0 = pan.node_tri or pan.is_tri tri_1 = pan.next_tri or pan.is_tri if pan.side == 'LEFT': tri_0, tri_1 = tri_1, tri_0 f = len(verts) # tiangular use segment direction # find last neighbor depending on type if s1.type == 'AXIS' or 'LINK' in s1.type: # apply only on boundaries if not s.is_hole: # use last axis if pan.side == 'LEFT': s3 = pan.next_cross else: s3 = pan.last_cross if tri_0: s1 = s.copy else: s1 = s1.oposite s1.v = (s.sized_normal(0, 1).v + s3.v).normalized() elif s1.type == 'SIDE': s1 = s.copy s1.type = 'SIDE' s1.v = s.sized_normal(0, 1).v else: s1 = s1.offset(d.gutter_dist + d.gutter_width) # find next neighbor depending on type if s2.type == 'AXIS' or 'LINK' in s2.type: if not s.is_hole: # use last axis if pan.side == 'LEFT': s3 = pan.last_cross else: s3 = pan.next_cross if tri_1: s2 = s.oposite else: s2 = s2.copy s2.v = (s.sized_normal(0, 1).v + s3.v).normalized() elif s2.type == 'SIDE': s2 = s.oposite s2.type = 'SIDE' s2.v = s.sized_normal(0, 1).v else: s2 = s2.offset(d.gutter_dist + d.gutter_width) # units vectors and scale # is unit normal on sides # print("s.p:%s, s.v:%s s1.p::%s s1.v::%s" % (s.p, s.v, s1.p, s1.v)) res, p, t = s0.intersect(s1) if res: p0 = p res, p, t = s0.intersect(s2) if res: p1 = p """ f = len(verts) verts.extend([s1.p0.to_3d(), s1.p1.to_3d()]) edges.append([f, f + 1]) f = len(verts) verts.extend([s2.p0.to_3d(), s2.p1.to_3d()]) edges.append([f, f + 1]) continue """ v0 = p0 - s.p0 v1 = p1 - s.p1 scale_0 = v0.length / (d.gutter_dist + d.gutter_width) scale_1 = v1.length / (d.gutter_dist + d.gutter_width) s3 = Line(s.p0, v0.normalized()) s4 = Line(s.p1, v1.normalized()) zt = self.z + d.fascia_altitude + pan.altitude(s3.p0) z0 = self.z + d.gutter_alt + pan.altitude(s3.p0) z1 = z0 - 0.5 * d.gutter_width z2 = z1 - 0.5 * d.gutter_width z3 = z1 - 0.5 * d.gutter_boudin dz0 = z2 - z1 dz1 = z3 - z1 tt = scale_0 * d.fascia_width t0 = scale_0 * d.gutter_dist t1 = t0 + scale_0 * (0.5 * d.gutter_width) t2 = t1 + scale_0 * (0.5 * d.gutter_width) t3 = t2 + scale_0 * (0.5 * d.gutter_boudin) # bord tablette xt, yt = s3.lerp(tt) # bord x0, y0 = s3.lerp(t0) # axe chenaux x1, y1 = s3.lerp(t1) # bord boudin interieur x2, y2 = s3.lerp(t2) # axe boudin x3, y3 = s3.lerp(t3) dx = x0 - x1 dy = y0 - y1 verts.append((xt, yt, zt)) # chenaux da = pi / d.gutter_segs for i in range(d.gutter_segs): sa = sin(i * da) ca = cos(i * da) verts.append((x1 + dx * ca, y1 + dy * ca, z1 + dz0 * sa)) dx = x2 - x3 dy = y2 - y3 # boudin da = -pi / (0.75 * d.gutter_segs) for i in range(d.gutter_segs): sa = sin(i * da) ca = cos(i * da) verts.append((x3 + dx * ca, y3 + dy * ca, z1 + dz1 * sa)) zt = self.z + d.fascia_altitude + pan.altitude(s4.p0) z0 = self.z + d.gutter_alt + pan.altitude(s4.p0) z1 = z0 - 0.5 * d.gutter_width z2 = z1 - 0.5 * d.gutter_width z3 = z1 - 0.5 * d.gutter_boudin dz0 = z2 - z1 dz1 = z3 - z1 tt = scale_1 * d.fascia_width t0 = scale_1 * d.gutter_dist t1 = t0 + scale_1 * (0.5 * d.gutter_width) t2 = t1 + scale_1 * (0.5 * d.gutter_width) t3 = t2 + scale_1 * (0.5 * d.gutter_boudin) # bord tablette xt, yt = s4.lerp(tt) # bord x0, y0 = s4.lerp(t0) # axe chenaux x1, y1 = s4.lerp(t1) # bord boudin interieur x2, y2 = s4.lerp(t2) # axe boudin x3, y3 = s4.lerp(t3) dx = x0 - x1 dy = y0 - y1 # tablette verts.append((xt, yt, zt)) faces.append((f + df, f, f + 1, f + df + 1)) uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)]) matids.append(idmat) # chenaux da = pi / d.gutter_segs for i in range(d.gutter_segs): sa = sin(i * da) ca = cos(i * da) verts.append((x1 + dx * ca, y1 + dy * ca, z1 + dz0 * sa)) dx = x2 - x3 dy = y2 - y3 # boudin da = -pi / (0.75 * d.gutter_segs) for i in range(d.gutter_segs): sa = sin(i * da) ca = cos(i * da) verts.append((x3 + dx * ca, y3 + dy * ca, z1 + dz1 * sa)) df = 2 * d.gutter_segs + 1 for i in range(1, 2 * d.gutter_segs): j = i + f faces.append((j, j + df, j + df + 1, j + 1)) uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)]) matids.append(idmat) """ segs = 6 n_faces = segs / 2 - 1 0 6 1 5 2 4 3 """ # close start if s1.type == 'SIDE': if d.gutter_segs % 2 == 0: faces.append((f + n_faces + 3, f + n_faces + 1, f + n_faces + 2)) uvs.append([(0, 0), (1, 0), (0.5, -0.5)]) matids.append(idmat) for i in range(n_faces): j = i + f + 1 k = f + d.gutter_segs - i faces.append((j + 1, k, k + 1, j)) uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)]) matids.append(idmat) # close end if s2.type == 'SIDE': f += 2 * d.gutter_segs + 1 if d.gutter_segs % 2 == 0: faces.append((f + n_faces + 1, f + n_faces + 3, f + n_faces + 2)) uvs.append([(0, 0), (1, 0), (0.5, -0.5)]) matids.append(idmat) for i in range(n_faces): j = i + f + 1 k = f + d.gutter_segs - i faces.append((j, k + 1, k, j + 1)) uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)]) matids.append(idmat) def beam_primary(self, d, verts, faces, edges, matids, uvs): idmat = 3 for pan in self.pans: for i, s in enumerate(pan.segs): if s.type == 'AXIS': #################### # Poutre Faitiere #################### """ 1___________________2 left 0|___________________|3 axis |___________________| right 5 4 """ f = len(verts) s2 = s.offset(-0.5 * d.beam_width) # offset from roof border s0 = pan.last_seg(i) s1 = pan.next_seg(i) t0 = 0 t1 = 1 s0_tri = pan.next_tri s1_tri = pan.node_tri if pan.side == 'LEFT': s0_tri, s1_tri = s1_tri, s0_tri if s0.type == 'SIDE' and s.length > 0: s0 = s0.offset(d.beam_offset) t0 = -d.beam_offset / s.length if s0_tri: p0 = s2.p0 t0 = 0 else: res, p0, t = s2.intersect(s0) if not res: continue if s1.type == 'SIDE' and s.length > 0: s1 = s1.offset(d.beam_offset) t1 = 1 + d.beam_offset / s.length if s1_tri: t1 = 1 p1 = s2.p1 else: res, p1, t = s2.intersect(s1) if not res: continue x0, y0 = p0 x1, y1 = s.lerp(t0) x2, y2 = p1 x3, y3 = s.lerp(t1) z0 = self.z + d.beam_alt + pan.altitude(p0) z1 = z0 - d.beam_height z2 = self.z + d.beam_alt + pan.altitude(p1) z3 = z2 - d.beam_height verts.extend([ (x0, y0, z0), (x1, y1, z0), (x2, y2, z2), (x3, y3, z2), (x0, y0, z1), (x1, y1, z1), (x2, y2, z3), (x3, y3, z3), ]) if s0_tri or s0.type == 'SIDE': faces.append((f + 4, f + 5, f + 1, f)) uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)]) matids.append(idmat) if s1_tri or s1.type == 'SIDE': faces.append((f + 2, f + 3, f + 7, f + 6)) uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)]) matids.append(idmat) faces.extend([ # internal side # (f + 1, f + 5, f + 7, f + 3), # external side (f + 2, f + 6, f + 4, f), # top (f, f + 1, f + 3, f + 2), # bottom (f + 5, f + 4, f + 6, f + 7) ]) matids.extend([ idmat, idmat, idmat ]) uvs.extend([ [(0, 0), (0, 1), (1, 1), (1, 0)], [(0, 0), (0, 1), (1, 1), (1, 0)], [(0, 0), (0, 1), (1, 1), (1, 0)] ]) def rafter(self, context, o, d): idmat = 4 # Rafters / Chevrons start = max(0.001 + 0.5 * d.rafter_width, d.rafter_start) holes_offset = -d.rafter_width # build temp bmesh and bissect for pan in self.pans: tmin, tmax, ysize = pan.tmin, pan.tmax, pan.ysize # print("tmin:%s tmax:%s ysize:%s" % (tmin, tmax, ysize)) f = 0 verts = [] faces = [] matids = [] uvs = [] alt = d.rafter_alt seg = pan.fake_axis t0 = tmin + (start - 0.5 * d.rafter_width) / seg.length t1 = tmin + (start + 0.5 * d.rafter_width) / seg.length tx = start / seg.length dt = d.rafter_spacing / seg.length n_items = max(1, round((tmax - tmin) / dt, 0)) dt = ((tmax - tmin) - 2 * tx) / n_items for j in range(int(n_items) + 1): n0 = seg.sized_normal(t1 + j * dt, - ysize) n1 = seg.sized_normal(t0 + j * dt, - ysize) f = len(verts) z0 = self.z + alt + pan.altitude(n0.p0) x0, y0 = n0.p0 z1 = self.z + alt + pan.altitude(n0.p1) x1, y1 = n0.p1 z2 = self.z + alt + pan.altitude(n1.p0) x2, y2 = n1.p0 z3 = self.z + alt + pan.altitude(n1.p1) x3, y3 = n1.p1 verts.extend([ (x0, y0, z0), (x1, y1, z1), (x2, y2, z2), (x3, y3, z3) ]) faces.append((f + 1, f, f + 2, f + 3)) matids.append(idmat) uvs.append([(0, 0), (1, 0), (1, 1), (0, 1)]) bm = bmed.buildmesh( context, o, verts, faces, matids=matids, uvs=uvs, weld=False, clean=False, auto_smooth=True, temporary=True) self.cut_boundary(bm, pan) self.cut_holes(bm, pan, offset={'DEFAULT': holes_offset}) bmesh.ops.dissolve_limit(bm, angle_limit=0.01, use_dissolve_boundaries=False, verts=bm.verts, edges=bm.edges, delimit={'MATERIAL'}) geom = bm.faces[:] verts = bm.verts[:] bmesh.ops.solidify(bm, geom=geom, thickness=0.0001) bmesh.ops.translate(bm, vec=Vector((0, 0, -d.rafter_height)), space=o.matrix_world, verts=verts) # uvs for sides uvs = [(0, 0), (1, 0), (1, 1), (0, 1)] layer = bm.loops.layers.uv.verify() for i, face in enumerate(bm.faces): if len(face.loops) == 4: for j, loop in enumerate(face.loops): loop[layer].uv = uvs[j] # merge with object bmed.bmesh_join(context, o, [bm], normal_update=True) bpy.ops.object.mode_set(mode='OBJECT') def hips(self, d, verts, faces, edges, matids, uvs): idmat_valley = 5 idmat = 6 idmat_poutre = 4 sx, sy, sz = d.hip_size_x, d.hip_size_y, d.hip_size_z if d.hip_model == 'ROUND': # round hips t_pts = [Vector((sx * x, sy * y, sz * z)) for x, y, z in [ (-0.5, 0.34, 0.08), (-0.5, 0.32, 0.19), (0.5, -0.4, -0.5), (0.5, 0.4, -0.5), (-0.5, 0.26, 0.28), (-0.5, 0.16, 0.34), (-0.5, 0.05, 0.37), (-0.5, -0.05, 0.37), (-0.5, -0.16, 0.34), (-0.5, -0.26, 0.28), (-0.5, -0.32, 0.19), (-0.5, -0.34, 0.08), (-0.5, -0.25, -0.5), (-0.5, 0.25, -0.5), (0.5, -0.08, 0.5), (0.5, -0.5, 0.08), (0.5, -0.24, 0.47), (0.5, -0.38, 0.38), (0.5, -0.47, 0.24), (0.5, 0.5, 0.08), (0.5, 0.08, 0.5), (0.5, 0.47, 0.24), (0.5, 0.38, 0.38), (0.5, 0.24, 0.47) ]] t_faces = [ (23, 22, 4, 5), (3, 19, 21, 22, 23, 20, 14, 16, 17, 18, 15, 2), (14, 20, 6, 7), (18, 17, 9, 10), (15, 18, 10, 11), (21, 19, 0, 1), (17, 16, 8, 9), (13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 1, 0), (19, 3, 13, 0), (20, 23, 5, 6), (22, 21, 1, 4), (3, 2, 12, 13), (2, 15, 11, 12), (16, 14, 7, 8) ] t_uvs = [ [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], [(0.5, 1.0), (0.75, 0.93), (0.93, 0.75), (1.0, 0.5), (0.93, 0.25), (0.75, 0.07), (0.5, 0.0), (0.25, 0.07), (0.07, 0.25), (0.0, 0.5), (0.07, 0.75), (0.25, 0.93)], [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], [(0.5, 1.0), (0.75, 0.93), (0.93, 0.75), (1.0, 0.5), (0.93, 0.25), (0.75, 0.07), (0.5, 0.0), (0.25, 0.07), (0.07, 0.25), (0.0, 0.5), (0.07, 0.75), (0.25, 0.93)], [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] ] # affect vertex with slope t_left = [] t_right = [] elif d.hip_model == 'ETERNIT': # square hips "eternit like" t_pts = [Vector((sx * x, sy * y, sz * z)) for x, y, z in [ (0.5, 0.5, 0.0), (-0.5, 0.5, -0.5), (0.5, -0.5, 0.0), (-0.5, -0.5, -0.5), (0.5, 0.0, 0.0), (-0.5, -0.0, -0.5), (0.5, 0.0, 0.5), (0.5, -0.5, 0.5), (-0.5, -0.5, 0.0), (-0.5, -0.0, 0.0), (0.5, 0.5, 0.5), (-0.5, 0.5, 0.0)] ] t_faces = [ (4, 2, 3, 5), (0, 4, 5, 1), (6, 9, 8, 7), (10, 11, 9, 6), (0, 10, 6, 4), (5, 9, 11, 1), (2, 7, 8, 3), (1, 11, 10, 0), (4, 6, 7, 2), (3, 8, 9, 5) ] t_uvs = [ [(0.0, 0.5), (0.0, 1.0), (1.0, 1.0), (1.0, 0.5)], [(0.0, 0.0), (0.0, 0.5), (1.0, 0.5), (1.0, 0.0)], [(0.0, 0.5), (1.0, 0.5), (1.0, 1.0), (0.0, 1.0)], [(0.0, 0.0), (1.0, 0.0), (1.0, 0.5), (0.0, 0.5)], [(0.0, 0.5), (0.0, 1.0), (0.5, 1.0), (0.5, 0.5)], [(0.5, 0.5), (0.5, 1.0), (0.0, 1.0), (0.0, 0.5)], [(0.0, 0.5), (0.0, 1.0), (1.0, 1.0), (1.0, 0.5)], [(0.0, 0.5), (0.0, 1.0), (-1.0, 1.0), (-1.0, 0.5)], [(0.5, 0.5), (0.5, 1.0), (1.0, 1.0), (1.0, 0.5)], [(0.0, 0.5), (0.0, 1.0), (-0.5, 1.0), (-0.5, 0.5)] ] t_left = [2, 3, 7, 8] t_right = [0, 1, 10, 11] elif d.hip_model == 'FLAT': # square hips "eternit like" t_pts = [Vector((sx * x, sy * y, sz * z)) for x, y, z in [ (-0.5, -0.4, 0.0), (-0.5, -0.4, 0.5), (-0.5, 0.4, 0.0), (-0.5, 0.4, 0.5), (0.5, -0.5, 0.5), (0.5, -0.5, 1.0), (0.5, 0.5, 0.5), (0.5, 0.5, 1.0), (-0.5, 0.33, 0.0), (-0.5, -0.33, 0.0), (0.5, -0.33, 0.5), (0.5, 0.33, 0.5), (-0.5, 0.33, -0.5), (-0.5, -0.33, -0.5), (0.5, -0.33, -0.5), (0.5, 0.33, -0.5)] ] t_faces = [ (0, 1, 3, 2, 8, 9), (2, 3, 7, 6), (6, 7, 5, 4, 10, 11), (4, 5, 1, 0), (9, 10, 4, 0), (7, 3, 1, 5), (2, 6, 11, 8), (9, 8, 12, 13), (12, 15, 14, 13), (8, 11, 15, 12), (10, 9, 13, 14), (11, 10, 14, 15)] t_uvs = [ [(0.5, 1.0), (0.93, 0.75), (0.93, 0.25), (0.5, 0.0), (0.07, 0.25), (0.07, 0.75)], [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], [(0.5, 1.0), (0.93, 0.75), (0.93, 0.25), (0.5, 0.0), (0.07, 0.25), (0.07, 0.75)], [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)], [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] ] t_left = [] t_right = [] t_idmats = [idmat for f in t_faces] for pan in self.pans: for i, s in enumerate(pan.segs): if ('LINK' in s.type and d.beam_sec_enable): ############## # beam inside ############## f = len(verts) s0 = s.offset(-0.5 * d.beam_sec_width) s2 = pan.last_seg(i) s3 = pan.next_seg(i) p0 = s0.p0 p1 = s0.p1 t0 = 0 t1 = 1 res, p, t = s0.intersect(s2) if res: t0 = t p0 = p res, p, t = s0.intersect(s3) if res: t1 = t p1 = p p0 = s.lerp(t0) p1 = s.lerp(t1) x0, y0 = s0.lerp(t0) x1, y1 = s.p0 z0 = self.z + d.beam_sec_alt + pan.altitude(p0) z1 = z0 - d.beam_sec_height z2 = self.z + d.beam_sec_alt + pan.altitude(s.p0) z3 = z2 - d.beam_sec_height verts.extend([ (x0, y0, z0), (x0, y0, z1), (x1, y1, z2), (x1, y1, z3) ]) x2, y2 = s0.lerp(t1) x3, y3 = s.p1 z0 = self.z + d.beam_sec_alt + pan.altitude(p1) z1 = z0 - d.beam_sec_height z2 = self.z + d.beam_sec_alt + pan.altitude(s.p1) z3 = z2 - d.beam_sec_height verts.extend([ (x2, y2, z0), (x2, y2, z1), (x3, y3, z2), (x3, y3, z3) ]) faces.extend([ (f, f + 4, f + 5, f + 1), (f + 1, f + 5, f + 7, f + 3), (f + 2, f + 3, f + 7, f + 6), (f + 2, f + 6, f + 4, f), (f, f + 1, f + 3, f + 2), (f + 5, f + 4, f + 6, f + 7) ]) matids.extend([ idmat_poutre, idmat_poutre, idmat_poutre, idmat_poutre, idmat_poutre, idmat_poutre ]) uvs.extend([ [(0, 0), (1, 0), (1, 1), (0, 1)], [(0, 0), (1, 0), (1, 1), (0, 1)], [(0, 0), (1, 0), (1, 1), (0, 1)], [(0, 0), (1, 0), (1, 1), (0, 1)], [(0, 0), (1, 0), (1, 1), (0, 1)], [(0, 0), (1, 0), (1, 1), (0, 1)] ]) if s.type == 'LINK_HIP': # TODO: # Slice borders properly if d.hip_enable: s0 = pan.last_seg(i) s1 = pan.next_seg(i) s2 = s p0 = s0.p1 p1 = s1.p0 z0 = pan.altitude(p0) z1 = pan.altitude(p1) # s0 is top seg if z1 > z0: p0, p1 = p1, p0 z0, z1 = z1, z0 s2 = s2.oposite dz = pan.altitude(s2.sized_normal(0, 1).p1) - z0 if dz < 0: s1 = s1.offset(d.tile_border) # vx from p0 to p1 x, y = p1 - p0 v = Vector((x, y, z1 - z0)) vx = v.normalized() vy = vx.cross(Vector((0, 0, 1))) vz = vy.cross(vx) x0, y0 = p0 + d.hip_alt * vz.to_2d() z2 = z0 + self.z + d.hip_alt * vz.z tM = Matrix([ [vx.x, vy.x, vz.x, x0], [vx.y, vy.y, vz.y, y0], [vx.z, vy.z, vz.z, z2], [0, 0, 0, 1] ]) space_x = v.length - d.tile_border n_x = 1 + int(space_x / d.hip_space_x) dx = space_x / n_x x0 = 0.5 * dx t_verts = [p for p in t_pts] # apply slope for i in t_left: t_verts[i] = t_verts[i].copy() t_verts[i].z -= dz * t_verts[i].y for i in t_right: t_verts[i] = t_verts[i].copy() t_verts[i].z += dz * t_verts[i].y for k in range(n_x): lM = tM @ Matrix([ [1, 0, 0, x0 + k * dx], [0, -1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ]) f = len(verts) verts.extend([lM @ p for p in t_verts]) faces.extend([tuple(i + f for i in p) for p in t_faces]) matids.extend(t_idmats) uvs.extend(t_uvs) elif s.type == 'LINK_VALLEY': if d.valley_enable: f = len(verts) s0 = s.offset(-2 * d.tile_couloir) s1 = pan.last_seg(i) s2 = pan.next_seg(i) p0 = s0.p0 p1 = s0.p1 res, p, t = s0.intersect(s1) if res: p0 = p res, p, t = s0.intersect(s2) if res: p1 = p alt = self.z + d.valley_altitude x0, y0 = s1.p1 x1, y1 = p0 x2, y2 = p1 x3, y3 = s2.p0 z0 = alt + pan.altitude(s1.p1) z1 = alt + pan.altitude(p0) z2 = alt + pan.altitude(p1) z3 = alt + pan.altitude(s2.p0) verts.extend([ (x0, y0, z0), (x1, y1, z1), (x2, y2, z2), (x3, y3, z3), ]) faces.extend([ (f, f + 3, f + 2, f + 1) ]) matids.extend([ idmat_valley ]) uvs.extend([ [(0, 0), (1, 0), (1, 1), (0, 1)] ]) elif s.type == 'AXIS' and d.hip_enable and pan.side == 'LEFT': tmin = 0 tmax = 1 s0 = pan.last_seg(i) if s0.type == 'SIDE' and s.length > 0: tmin = 0 - d.tile_side / s.length s1 = pan.next_seg(i) if s1.type == 'SIDE' and s.length > 0: tmax = 1 + d.tile_side / s.length # print("tmin:%s tmax:%s" % (tmin, tmax)) #################### # Faitiere #################### f = len(verts) s_len = (tmax - tmin) * s.length n_obj = 1 + int(s_len / d.hip_space_x) dx = s_len / n_obj x0 = 0.5 * dx v = s.v.normalized() p0 = s.lerp(tmin) tM = Matrix([ [v.x, v.y, 0, p0.x], [v.y, -v.x, 0, p0.y], [0, 0, 1, self.z + d.hip_alt], [0, 0, 0, 1] ]) t_verts = [p.copy() for p in t_pts] # apply slope for i in t_left: t_verts[i].z += t_verts[i].y * (pan.other_side.slope - d.tile_size_z / d.tile_size_y) for i in t_right: t_verts[i].z -= t_verts[i].y * (pan.slope - d.tile_size_z / d.tile_size_y) for k in range(n_obj): lM = tM @ Matrix([ [1, 0, 0, x0 + k * dx], [0, -1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1] ]) v = len(verts) verts.extend([lM @ p for p in t_verts]) faces.extend([tuple(i + v for i in f) for f in t_faces]) matids.extend(t_idmats) uvs.extend(t_uvs) def make_hole(self, context, hole_obj, o, d, update_parent=False): """ Hole for t child on parent create / update a RoofCutter on parent assume context object is child roof with parent set """ # print("Make hole :%s hole_obj:%s" % (o.name, hole_obj)) if o.parent is None: return # root is a RoofSegment root = self.nodes[0].root r_pan = root.right l_pan = root.left # merge : # 5 ____________ 4 # / | # / left | # /_____axis_____| 3 <- kill axis and this one # 0\ | # \ right | # 1 \____________| 2 # # degenerate case: # # /| # / | # \ | # \| # segs = [] last = len(r_pan.segs) - 1 for i, seg in enumerate(r_pan.segs): # r_pan start parent roof side if i == last: to_merge = seg.copy elif seg.type != 'AXIS': segs.append(seg.copy) for i, seg in enumerate(l_pan.segs): # l_pan end parent roof side if i == 1: # 0 is axis to_merge.p1 = seg.p1 segs.append(to_merge) elif seg.type != 'AXIS': segs.append(seg.copy) # if there is side offset: # create an arrow # # 4 s4 # /| # / |___s1_______ # / p3 | p2 s3 # 0\ p0___s0_______| p1 # \ | # 1 \| s0 = root.left._axis.offset( max(0.001, min( root.right.ysize - 0.001, root.right.ysize - d.hole_offset_right ) )) s1 = root.left._axis.offset( -max(0.001, min( root.left.ysize - 0.001, root.left.ysize - d.hole_offset_left ) )) s3 = segs[2].offset( -min(root.left.xsize - 0.001, d.hole_offset_front) ) s4 = segs[0].copy p1 = s4.p1 s4.p1 = segs[-1].p0 s4.p0 = p1 res, p0, t = s4.intersect(s0) res, p1, t = s0.intersect(s3) res, p2, t = s1.intersect(s3) res, p3, t = s4.intersect(s1) pts = [] # pts in cw order for 'DIFFERENCE' mode pts.extend([segs[-1].p1, segs[-1].p0]) if (segs[-1].p0 - p3).length > 0.001: pts.append(p3) pts.extend([p2, p1]) if (segs[0].p1 - p0).length > 0.001: pts.append(p0) pts.extend([segs[0].p1, segs[0].p0]) pts = [p.to_3d() for p in pts] if hole_obj is None: context.view_layer.objects.active = o.parent bpy.ops.archipack.roof_cutter(parent=d.t_parent, auto_manipulate=False) hole_obj = context.active_object else: context.view_layer.objects.active = hole_obj hole_obj.select_set(state=True) if d.parts[0].a0 < 0: y = -d.t_dist_y else: y = d.t_dist_y hole_obj.matrix_world = o.matrix_world @ Matrix([ [1, 0, 0, 0], [0, 1, 0, y], [0, 0, 1, 0], [0, 0, 0, 1] ]) hd = archipack_roof_cutter.datablock(hole_obj) hd.boundary = o.name hd.update_points(context, hole_obj, pts, update_parent=update_parent) hole_obj.select_set(state=False) context.view_layer.objects.active = o def change_coordsys(self, fromTM, toTM): """ move shape fromTM into toTM coordsys """ dp = (toTM.inverted() @ fromTM.translation).to_2d() da = toTM.row[1].to_2d().angle_signed(fromTM.row[1].to_2d()) ca = cos(da) sa = sin(da) rM = Matrix([ [ca, -sa], [sa, ca] ]) for s in self.segs: tp = (rM @ s.p0) - s.p0 + dp s.rotate(da) s.translate(tp) def t_partition(self, array, begin, end): pivot = begin for i in range(begin + 1, end + 1): # wall idx if array[i][0] < array[begin][0]: pivot += 1 array[i], array[pivot] = array[pivot], array[i] array[pivot], array[begin] = array[begin], array[pivot] return pivot def sort_t(self, array, begin=0, end=None): # print("sort_child") if end is None: end = len(array) - 1 def _quicksort(array, begin, end): if begin >= end: return pivot = self.t_partition(array, begin, end) _quicksort(array, begin, pivot - 1) _quicksort(array, pivot + 1, end) return _quicksort(array, begin, end) def make_wall_fit(self, context, o, wall, inside): wd = wall.data.archipack_wall2[0] wg = wd.get_generator() z0 = self.z - wd.z # wg in roof coordsys wg.change_coordsys(wall.matrix_world, o.matrix_world) if inside: # fit inside offset = -0.5 * (1 - wd.x_offset) * wd.width else: # fit outside offset = 0 wg.set_offset(offset) wall_t = [[] for w in wg.segs] for pan in self.pans: # walls segment for widx, wseg in enumerate(wg.segs): ls = wseg.line.length for seg in pan.segs: # intersect with a roof segment # any linked or axis intersection here # will be dup as they are between 2 roof parts res, p, t, v = wseg.line.intersect_ext(seg) if res: z = z0 + pan.altitude(p) wall_t[widx].append((t, z, t * ls)) # lie under roof if type(wseg).__name__ == "CurvedWall": for step in range(12): t = step / 12 p = wseg.line.lerp(t) if pan.inside(p): z = z0 + pan.altitude(p) wall_t[widx].append((t, z, t * ls)) else: if pan.inside(wseg.line.p0): z = z0 + pan.altitude(wseg.line.p0) wall_t[widx].append((0, z, 0)) old = context.active_object old_sel = wall.select_get() wall.select_set(state=True) context.view_layer.objects.active = wall wd.auto_update = False # setup splits count and first split to 0 for widx, seg in enumerate(wall_t): self.sort_t(seg) # print("seg: %s" % seg) for s in seg: t, z, d = s wd.parts[widx].n_splits = len(seg) + 1 wd.parts[widx].z[0] = 0 wd.parts[widx].t[0] = 0 break # add splits, skip dups for widx, seg in enumerate(wall_t): t0 = 0 last_d = -1 sid = 1 for s in seg: t, z, d = s if t == 0: # add at end of last segment if widx > 0: lid = wd.parts[widx - 1].n_splits - 1 wd.parts[widx - 1].z[lid] = z wd.parts[widx - 1].t[lid] = 1 else: wd.parts[widx].z[0] = z wd.parts[widx].t[0] = t sid = 1 else: if d - last_d < 0.001: wd.parts[widx].n_splits -= 1 continue wd.parts[widx].z[sid] = z wd.parts[widx].t[sid] = t - t0 t0 = t sid += 1 last_d = d if wd.closed: last = wd.parts[wd.n_parts].n_splits - 1 wd.parts[wd.n_parts].z[last] = wd.parts[0].z[0] wd.parts[wd.n_parts].t[last] = 1.0 wd.auto_update = True """ for s in self.segs: s.as_curve(context) for s in wg.segs: s.as_curve(context) """ wall.select_set(state=old_sel) context.view_layer.objects.active = old def boundary(self, context, o): """ either external or holes cuts """ to_remove = [] for b in o.children: d = archipack_roof_cutter.datablock(b) if d is not None: g = d.ensure_direction() g.change_coordsys(b.matrix_world, o.matrix_world) for i, pan in enumerate(self.pans): keep = pan.slice(g) if not keep: if i not in to_remove: to_remove.append(i) pan.limits() to_remove.sort() for i in reversed(to_remove): self.pans.pop(i) def draft(self, context, verts, edges): for pan in self.pans: pan.draw(context, self.z, verts, edges) for s in self.segs: if s.constraint_type == 'SLOPE': f = len(verts) p0 = s.p0.to_3d() p0.z = self.z p1 = s.p1.to_3d() p1.z = self.z verts.extend([p0, p1]) edges.append([f, f + 1]) 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) def update_parent(self, context): # update part a0 o = context.active_object p, d = self.find_parent(context) if d is not None: o.parent = p # trigger object update # hole creation and parent's update self.parts[0].a0 = pi / 2 elif self.t_parent != "": self.t_parent = "" def update_cutter(self, context): self.update(context, update_hole=True) def update_childs(self, context): self.update(context, update_childs=True, update_hole=True) def update_components(self, context): self.update(context, update_parent=False, update_hole=False) class ArchipackSegment(): length : FloatProperty( name="Length", min=0.01, max=1000.0, default=4.0, update=update ) a0 : FloatProperty( name="Angle", min=-2 * pi, max=2 * pi, default=0, subtype='ANGLE', unit='ROTATION', update=update_cutter ) manipulators : CollectionProperty(type=archipack_manipulator) class ArchipackLines(): n_parts : IntProperty( name="Parts", min=1, default=1, update=update_manipulators ) # UI layout related parts_expand : BoolProperty( default=False ) def draw(self, layout, context): box = layout.box() row = box.row() if self.parts_expand: row.prop(self, 'parts_expand', icon="TRIA_DOWN", text="Parts", emboss=False) box.prop(self, 'n_parts') for i, part in enumerate(self.parts): part.draw(layout, context, i) else: row.prop(self, 'parts_expand', icon="TRIA_RIGHT", text="Parts", emboss=False) def update_parts(self): # print("update_parts") # remove rows # NOTE: # n_parts+1 # as last one is end point of last segment or closing one for i in range(len(self.parts), self.n_parts + 1, -1): self.parts.remove(i - 1) # add rows for i in range(len(self.parts), self.n_parts + 1): self.parts.add() self.setup_manipulators() def setup_parts_manipulators(self): for i in range(self.n_parts + 1): 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" 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) if n_manips < 5: s = p.manipulators.add() s.type_key = "SIZE" s.prop1_name = "offset" p.manipulators[2].prop1_name = str(i) p.manipulators[3].prop1_name = str(i + 1) class archipack_roof_segment(ArchipackSegment, PropertyGroup): bound_idx : IntProperty( name="Link to", default=0, min=0, update=update_manipulators ) width_left : FloatProperty( name="L Width", min=0.01, default=3.0, update=update_cutter ) width_right : FloatProperty( name="R Width", min=0.01, default=3.0, update=update_cutter ) slope_left : FloatProperty( name="L slope", min=0.0, default=0.3, update=update_cutter ) slope_right : FloatProperty( name="R slope", min=0.0, default=0.3, update=update_cutter ) auto_left : EnumProperty( description="Left mode", name="Left", items=( ('AUTO', 'Auto', '', 0), ('WIDTH', 'Width', '', 1), ('SLOPE', 'Slope', '', 2), ('ALL', 'All', '', 3), ), default="AUTO", update=update_manipulators ) auto_right : EnumProperty( description="Right mode", name="Right", items=( ('AUTO', 'Auto', '', 0), ('WIDTH', 'Width', '', 1), ('SLOPE', 'Slope', '', 2), ('ALL', 'All', '', 3), ), default="AUTO", update=update_manipulators ) triangular_end : BoolProperty( name="Triangular end", default=False, update=update ) take_precedence : BoolProperty( name="Take precedence", description="On T segment take width precedence", default=False, update=update ) constraint_type : EnumProperty( items=( ('HORIZONTAL', 'Horizontal', '', 0), ('SLOPE', 'Slope', '', 1) ), default='HORIZONTAL', update=update_manipulators ) enforce_part : EnumProperty( name="Enforce part", items=( ('AUTO', 'Auto', '', 0), ('VALLEY', 'Valley', '', 1), ('HIP', 'Hip', '', 2) ), default='AUTO', 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: d = archipack_roof.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() if index > 0: box.prop(self, "constraint_type", text=str(index + 1)) if self.constraint_type == 'SLOPE': box.prop(self, "enforce_part", text="") else: box.label(text="Part 1:") box.prop(self, "length") box.prop(self, "a0") if index > 0: box.prop(self, 'bound_idx') if self.constraint_type == 'HORIZONTAL': box.prop(self, "triangular_end") row = box.row(align=True) row.prop(self, "auto_left", text="") row.prop(self, "auto_right", text="") if self.auto_left in {'ALL', 'WIDTH'}: box.prop(self, "width_left") if self.auto_left in {'ALL', 'SLOPE'}: box.prop(self, "slope_left") if self.auto_right in {'ALL', 'WIDTH'}: box.prop(self, "width_right") if self.auto_right in {'ALL', 'SLOPE'}: box.prop(self, "slope_right") elif self.constraint_type == 'HORIZONTAL': box.prop(self, "triangular_end") def update(self, context, manipulable_refresh=False, update_hole=False): props = self.find_in_selection(context) if props is not None: props.update(context, manipulable_refresh, update_parent=True, update_hole=True, update_childs=True) class archipack_roof(ArchipackLines, ArchipackObject, Manipulable, PropertyGroup): parts : CollectionProperty(type=archipack_roof_segment) z : FloatProperty( name="Altitude", default=3, precision=2, step=1, unit='LENGTH', subtype='DISTANCE', update=update_childs ) slope_left : FloatProperty( name="L slope", default=0.5, precision=2, step=1, update=update_childs ) slope_right : FloatProperty( name="R slope", default=0.5, precision=2, step=1, update=update_childs ) width_left : FloatProperty( name="L width", default=3, precision=2, step=1, unit='LENGTH', subtype='DISTANCE', update=update_cutter ) width_right : FloatProperty( name="R width", default=3, precision=2, step=1, unit='LENGTH', subtype='DISTANCE', update=update_cutter ) draft : BoolProperty( options={'SKIP_SAVE'}, name="Draft mode", default=False, update=update_manipulators ) auto_update : BoolProperty( options={'SKIP_SAVE'}, default=True, update=update_manipulators ) quick_edit : BoolProperty( options={'SKIP_SAVE'}, name="Quick Edit", default=False ) tile_enable : BoolProperty( name="Enable", default=True, update=update_components ) tile_solidify : BoolProperty( name="Solidify", default=True, update=update_components ) tile_height : FloatProperty( name="Height", description="Amount for solidify", min=0, default=0.02, unit='LENGTH', subtype='DISTANCE', update=update_components ) tile_bevel : BoolProperty( name="Bevel", default=False, update=update_components ) tile_bevel_amt : FloatProperty( name="Amount", description="Amount for bevel", min=0, default=0.02, unit='LENGTH', subtype='DISTANCE', update=update_components ) tile_bevel_segs : IntProperty( name="Segs", description="Bevel Segs", min=1, default=2, update=update_components ) tile_alternate : BoolProperty( name="Alternate", default=False, update=update_components ) tile_offset : FloatProperty( name="Offset", description="Offset from start", min=0, max=100, subtype="PERCENTAGE", update=update_components ) tile_altitude : FloatProperty( name="Altitude", description="Altitude from roof", default=0.1, unit='LENGTH', subtype='DISTANCE', update=update_components ) tile_size_x : FloatProperty( name="Width", description="Size of tiles on x axis", min=0.01, default=0.2, unit='LENGTH', subtype='DISTANCE', update=update_components ) tile_size_y : FloatProperty( name="Length", description="Size of tiles on y axis", min=0.01, default=0.3, unit='LENGTH', subtype='DISTANCE', update=update_components ) tile_size_z : FloatProperty( name="Thickness", description="Size of tiles on z axis", min=0.0, default=0.02, unit='LENGTH', subtype='DISTANCE', update=update_components ) tile_space_x : FloatProperty( name="Width", description="Space between tiles on x axis", min=0.01, default=0.2, unit='LENGTH', subtype='DISTANCE', update=update_components ) tile_space_y : FloatProperty( name="Length", description="Space between tiles on y axis", min=0.01, default=0.3, unit='LENGTH', subtype='DISTANCE', update=update_components ) tile_fit_x : BoolProperty( name="Fit x", description="Fit roof on x axis", default=True, update=update_components ) tile_fit_y : BoolProperty( name="Fit y", description="Fit roof on y axis", default=True, update=update_components ) tile_expand : BoolProperty( options={'SKIP_SAVE'}, name="Tiles", description="Expand tiles panel", default=False ) tile_model : EnumProperty( name="Model", items=( ('BRAAS1', 'Braas 1', '', 0), ('BRAAS2', 'Braas 2', '', 1), ('ETERNIT', 'Eternit', '', 2), ('LAUZE', 'Lauze', '', 3), ('ROMAN', 'Roman', '', 4), ('ROUND', 'Round', '', 5), ('PLACEHOLDER', 'Square', '', 6), ('ONDULEE', 'Ondule', '', 7), ('METAL', 'Metal', '', 8), # ('USER', 'User defined', '', 7) ), default="BRAAS2", update=update_components ) tile_side : FloatProperty( name="Side", description="Space on side", default=0, unit='LENGTH', subtype='DISTANCE', update=update_components ) tile_couloir : FloatProperty( name="Valley", description="Space between tiles on valley", min=0, default=0.05, unit='LENGTH', subtype='DISTANCE', update=update_components ) tile_border : FloatProperty( name="Bottom", description="Tiles offset from bottom", default=0, unit='LENGTH', subtype='DISTANCE', update=update_components ) gutter_expand : BoolProperty( options={'SKIP_SAVE'}, name="Gutter", description="Expand gutter panel", default=False ) gutter_enable : BoolProperty( name="Enable", default=True, update=update_components ) gutter_alt : FloatProperty( name="Altitude", description="altitude", default=0, unit='LENGTH', subtype='DISTANCE', update=update_components ) gutter_width : FloatProperty( name="Width", description="Width", min=0.01, default=0.15, unit='LENGTH', subtype='DISTANCE', update=update_components ) gutter_dist : FloatProperty( name="Spacing", description="Spacing", min=0, default=0.05, unit='LENGTH', subtype='DISTANCE', update=update_components ) gutter_boudin : FloatProperty( name="Small width", description="Small width", min=0, default=0.015, unit='LENGTH', subtype='DISTANCE', update=update_components ) gutter_segs : IntProperty( default=6, min=1, name="Segs", update=update_components ) beam_expand : BoolProperty( options={'SKIP_SAVE'}, name="Beam", description="Expand beam panel", default=False ) beam_enable : BoolProperty( name="Ridge pole", default=True, update=update_components ) beam_width : FloatProperty( name="Width", description="Width", min=0.01, default=0.2, unit='LENGTH', subtype='DISTANCE', update=update_components ) beam_height : FloatProperty( name="Height", description="Height", min=0.01, default=0.35, unit='LENGTH', subtype='DISTANCE', update=update_components ) beam_offset : FloatProperty( name="Offset", description="Distance from roof border", default=0.02, unit='LENGTH', subtype='DISTANCE', update=update_components ) beam_alt : FloatProperty( name="Altitude", description="Altitude from roof", default=-0.15, unit='LENGTH', subtype='DISTANCE', update=update_components ) beam_sec_enable : BoolProperty( name="Hip rafter", default=True, update=update_components ) beam_sec_width : FloatProperty( name="Width", description="Width", min=0.01, default=0.15, unit='LENGTH', subtype='DISTANCE', update=update_components ) beam_sec_height : FloatProperty( name="Height", description="Height", min=0.01, default=0.2, unit='LENGTH', subtype='DISTANCE', update=update_components ) beam_sec_alt : FloatProperty( name="Altitude", description="Distance from roof", default=-0.1, unit='LENGTH', subtype='DISTANCE', update=update_components ) rafter_enable : BoolProperty( name="Rafter", default=True, update=update_components ) rafter_width : FloatProperty( name="Width", description="Width", min=0.01, default=0.1, unit='LENGTH', subtype='DISTANCE', update=update_components ) rafter_height : FloatProperty( name="Height", description="Height", min=0.01, default=0.2, unit='LENGTH', subtype='DISTANCE', update=update_components ) rafter_spacing : FloatProperty( name="Spacing", description="Spacing", min=0.1, default=0.7, unit='LENGTH', subtype='DISTANCE', update=update_components ) rafter_start : FloatProperty( name="Offset", description="Spacing from roof border", min=0, default=0.1, unit='LENGTH', subtype='DISTANCE', update=update_components ) rafter_alt : FloatProperty( name="Altitude", description="Altitude from roof", max=-0.0001, default=-0.001, unit='LENGTH', subtype='DISTANCE', update=update_components ) hip_enable : BoolProperty( name="Enable", default=True, update=update_components ) hip_expand : BoolProperty( options={'SKIP_SAVE'}, name="Hips", description="Expand hips panel", default=False ) hip_alt : FloatProperty( name="Altitude", description="Hip altitude from roof", default=0.1, unit='LENGTH', subtype='DISTANCE', update=update_components ) hip_space_x : FloatProperty( name="Spacing", description="Space between hips", min=0.01, default=0.4, unit='LENGTH', subtype='DISTANCE', update=update_components ) hip_size_x : FloatProperty( name="Length", description="Length of hip", min=0.01, default=0.4, unit='LENGTH', subtype='DISTANCE', update=update_components ) hip_size_y : FloatProperty( name="Width", description="Width of hip", min=0.01, default=0.15, unit='LENGTH', subtype='DISTANCE', update=update_components ) hip_size_z : FloatProperty( name="Height", description="Height of hip", min=0.0, default=0.15, unit='LENGTH', subtype='DISTANCE', update=update_components ) hip_model : EnumProperty( name="Model", items=( ('ROUND', 'Round', '', 0), ('ETERNIT', 'Eternit', '', 1), ('FLAT', 'Flat', '', 2) ), default="ROUND", update=update_components ) valley_altitude : FloatProperty( name="Altitude", description="Valley altitude from roof", default=0.1, unit='LENGTH', subtype='DISTANCE', update=update_components ) valley_enable : BoolProperty( name="Valley", default=True, update=update_components ) fascia_enable : BoolProperty( name="Enable", description="Enable Fascia", default=True, update=update_components ) fascia_expand : BoolProperty( options={'SKIP_SAVE'}, name="Fascia", description="Expand fascia panel", default=False ) fascia_height : FloatProperty( name="Height", description="Height", min=0.01, default=0.3, unit='LENGTH', subtype='DISTANCE', update=update_components ) fascia_width : FloatProperty( name="Width", description="Width", min=0.01, default=0.02, unit='LENGTH', subtype='DISTANCE', update=update_components ) fascia_offset : FloatProperty( name="Offset", description="Offset from roof border", default=0, unit='LENGTH', subtype='DISTANCE', update=update_components ) fascia_altitude : FloatProperty( name="Altitude", description="Fascia altitude from roof", default=0.1, unit='LENGTH', subtype='DISTANCE', update=update_components ) bargeboard_enable : BoolProperty( name="Enable", description="Enable Bargeboard", default=True, update=update_components ) bargeboard_expand : BoolProperty( options={'SKIP_SAVE'}, name="Bargeboard", description="Expand Bargeboard panel", default=False ) bargeboard_height : FloatProperty( name="Height", description="Height", min=0.01, default=0.3, unit='LENGTH', subtype='DISTANCE', update=update_components ) bargeboard_width : FloatProperty( name="Width", description="Width", min=0.01, default=0.02, unit='LENGTH', subtype='DISTANCE', update=update_components ) bargeboard_offset : FloatProperty( name="Offset", description="Offset from roof border", default=0.001, unit='LENGTH', subtype='DISTANCE', update=update_components ) bargeboard_altitude : FloatProperty( name="Altitude", description="Fascia altitude from roof", default=0.1, unit='LENGTH', subtype='DISTANCE', update=update_components ) t_parent : StringProperty( name="Parent", default="", update=update_parent ) t_part : IntProperty( name="Part", description="Parent part index", default=0, min=0, update=update_cutter ) t_dist_x : FloatProperty( name="Dist x", description="Location on axis ", default=0, update=update_cutter ) t_dist_y : FloatProperty( name="Dist y", description="Lateral distance from axis", min=0.0001, default=0.0001, update=update_cutter ) z_parent: FloatProperty( description="Delta z of t child for grand childs", default=0 ) hole_offset_left : FloatProperty( name="Left", description="Left distance from border", min=0, default=0, update=update_cutter ) hole_offset_right : FloatProperty( name="Right", description="Right distance from border", min=0, default=0, update=update_cutter ) hole_offset_front : FloatProperty( name="Front", description="Front distance from border", default=0, update=update_cutter ) def make_wall_fit(self, context, o, wall, inside=False): origin = Vector((0, 0, self.z)) g = self.get_generator(origin) g.make_roof(context) g.make_wall_fit(context, o, wall, inside) def update_parts(self): # NOTE: # n_parts+1 # as last one is end point of last segment or closing one for i in range(len(self.parts), self.n_parts, -1): self.parts.remove(i - 1) # add rows for i in range(len(self.parts), self.n_parts): bound_idx = len(self.parts) self.parts.add() self.parts[-1].bound_idx = bound_idx self.setup_manipulators() def setup_manipulators(self): if len(self.manipulators) < 1: s = self.manipulators.add() s.type_key = "SIZE" s.prop1_name = "z" s.normal = (0, 1, 0) if len(self.manipulators) < 2: s = self.manipulators.add() s.type_key = "SIZE" s.prop1_name = "width_left" if len(self.manipulators) < 3: s = self.manipulators.add() s.type_key = "SIZE" s.prop1_name = "width_right" 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" 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 = 'DUMB_STRING' s.prop1_name = str(i + 1) p.manipulators[2].prop1_name = str(i + 1) if n_manips < 4: s = p.manipulators.add() s.type_key = 'SIZE' s.prop1_name = "width_left" if n_manips < 5: s = p.manipulators.add() s.type_key = 'SIZE' s.prop1_name = "width_right" if n_manips < 6: s = p.manipulators.add() s.type_key = 'SIZE' s.prop1_name = "slope_left" if n_manips < 7: s = p.manipulators.add() s.type_key = 'SIZE' s.prop1_name = "slope_right" def get_generator(self, origin=Vector((0, 0, 0))): g = RoofGenerator(self, origin) # TODO: sort part by bound idx so deps always find parent for i, part in enumerate(self.parts): # skip part if bound_idx > parent # so deps always see parent if part.bound_idx <= i: g.add_part(part) g.locate_manipulators() return g def make_surface(self, o, verts, edges): bm = bmesh.new() for v in verts: bm.verts.new(v) bm.verts.ensure_lookup_table() for ed in edges: bm.edges.new((bm.verts[ed[0]], bm.verts[ed[1]])) bm.edges.ensure_lookup_table() # bmesh.ops.contextual_create(bm, geom=bm.edges) bm.to_mesh(o.data) bm.free() def find_parent(self, context): o = context.scene.objects.get(self.t_parent.strip()) return o, archipack_roof.datablock(o) def intersection_angle(self, t_slope, t_width, p_slope, angle): # 2d intersection angle between two roofs parts dy = abs(t_slope * t_width / p_slope) ca = cos(angle) ta = tan(angle) if ta == 0: w0 = 0 else: w0 = dy * ta if ca == 0: w1 = 0 else: w1 = t_width / ca dx = w1 - w0 return atan2(dy, dx) def relocate_child(self, context, o, g, child): d = archipack_roof.datablock(child) if d is not None and d.t_part - 1 < len(g.segs): # print("relocate_child(%s)" % (child.name)) seg = g.segs[d.t_part] # adjust T part matrix_world from parent # T part origin located on parent axis # with y in parent direction t = (d.t_dist_x / seg.length) x, y, z = seg.lerp(t).to_3d() dy = -seg.v.normalized() child.matrix_world = o.matrix_world @ Matrix([ [dy.x, -dy.y, 0, x], [dy.y, dy.x, 0, y], [0, 0, 1, z], [0, 0, 0, 1] ]) def relocate_childs(self, context, o, g): for child in o.children: d = archipack_roof.datablock(child) if d is not None and d.t_parent == o.name: self.relocate_child(context, o, g, child) def update_childs(self, context, o, g): for child in o.children: d = archipack_roof.datablock(child) if d is not None and d.t_parent == o.name: # print("upate_childs(%s)" % (child.name)) child.select_set(state=True) context.view_layer.objects.active = child # regenerate hole d.update(context, update_hole=True, update_parent=False) child.select_set(state=False) o.select_set(state=True) context.view_layer.objects.active = o def update(self, context, manipulable_refresh=False, update_childs=False, update_parent=True, update_hole=False, force_update=False): """ update_hole: on t_child must update parent update_childs: force childs update force_update: skip throttle """ # print("update") 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) self.update_parts() verts, edges, faces, matids, uvs = [], [], [], [], [] y = 0 z = self.z p, d = self.find_parent(context) g = None # t childs: use parent to relocate # setup slopes into generator if d is not None: pg = d.get_generator() pg.make_roof(context) if self.t_part - 1 < len(pg.segs): seg = pg.nodes[self.t_part].root d.relocate_child(context, p, pg, o) a0 = self.parts[0].a0 a_axis = a0 - pi / 2 a_offset = 0 s_left = self.slope_left w_left = -self.width_left s_right = self.slope_right w_right = self.width_right if a0 > 0: # a_axis est mesure depuis la perpendiculaire à l'axe slope = seg.right.slope y = self.t_dist_y else: a_offset = pi slope = seg.left.slope y = -self.t_dist_y s_left, s_right = s_right, s_left w_left, w_right = -w_right, -w_left if slope == 0: slope = 0.0001 # print("slope: %s" % (slope)) z = d.z_parent + d.z - self.t_dist_y * slope self.z_parent = z - self.z # a_right from axis cross z b_right = self.intersection_angle( s_left, w_left, slope, a_axis) a_right = b_right + a_offset b_left = self.intersection_angle( s_right, w_right, slope, a_axis) a_left = b_left + a_offset g = self.get_generator(origin=Vector((0, y, z))) # override by user defined slope if any make_right = True make_left = True for s in g.segs: if (s.constraint_type == 'SLOPE' and s.v0_idx == 0): da = g.segs[0].v.angle_signed(s.v) if da > 0: make_left = False else: make_right = False if make_left: # Add 'SLOPE' constraints for segment 0 v = Vector((cos(a_left), sin(a_left))) s = StraightRoof(g.origin, v) s.v0_idx = 0 s.constraint_type = 'SLOPE' # s.enforce_part = 'VALLEY' s.angle_0 = a_left s.take_precedence = False g.segs.append(s) if make_right: v = Vector((cos(a_right), sin(a_right))) s = StraightRoof(g.origin, v) s.v0_idx = 0 s.constraint_type = 'SLOPE' # s.enforce_part = 'VALLEY' s.angle_0 = a_right s.take_precedence = False g.segs.append(s) if g is None: g = self.get_generator(origin=Vector((0, y, z))) # setup per segment manipulators if len(g.segs) > 0: f = g.segs[0] # z n = f.straight(-1, 0).v.to_3d() self.manipulators[0].set_pts([(0, 0, 0), (0, 0, self.z), (1, 0, 0)], normal=n) # left width n = f.sized_normal(0, -self.width_left) self.manipulators[1].set_pts([n.p0.to_3d(), n.p1.to_3d(), (-1, 0, 0)]) # right width n = f.sized_normal(0, self.width_right) self.manipulators[2].set_pts([n.p0.to_3d(), n.p1.to_3d(), (1, 0, 0)]) g.make_roof(context) # update childs here so parent may use # new holes when parent shape does change if update_childs: self.update_childs(context, o, g) # on t_child if d is not None and update_hole: hole_obj = self.find_hole(context, o) g.make_hole(context, hole_obj, o, self, update_parent) # print("make_hole") # add cutters g.boundary(context, o) if self.draft: g.draft(context, verts, edges) g.gutter(self, verts, faces, edges, matids, uvs) self.make_surface(o, verts, edges) else: if self.bargeboard_enable: g.bargeboard(self, verts, faces, edges, matids, uvs) if self.fascia_enable: g.fascia(self, verts, faces, edges, matids, uvs) if self.beam_enable: g.beam_primary(self, verts, faces, edges, matids, uvs) g.hips(self, verts, faces, edges, matids, uvs) if self.gutter_enable: g.gutter(self, verts, faces, edges, matids, uvs) bmed.buildmesh( context, o, verts, faces, matids=matids, uvs=uvs, weld=False, clean=False, auto_smooth=True, temporary=False) # bpy.ops.object.mode_set(mode='EDIT') g.lambris(context, o, self) # print("lambris") if self.rafter_enable: # bpy.ops.object.mode_set(mode='EDIT') g.rafter(context, o, self) # print("rafter") if self.quick_edit and not force_update: if self.tile_enable: bpy.ops.archipack.roof_throttle_update(name=o.name) else: # throttle here if self.tile_enable: g.couverture(context, o, self) # print("couverture") # enable manipulators rebuild if manipulable_refresh: self.manipulable_refresh = True # print("rafter") # restore context self.restore_context(context) # print("restore context") def find_hole(self, context, o): p, d = self.find_parent(context) if d is not None: for child in p.children: cd = archipack_roof_cutter.datablock(child) if cd is not None and cd.boundary == o.name: return child return None 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 > 0: # start angle self.manip_stack.append(part.manipulators[0].setup(context, o, part)) if part.constraint_type == 'HORIZONTAL': # length / radius + angle self.manip_stack.append(part.manipulators[1].setup(context, o, part)) # index self.manip_stack.append(part.manipulators[2].setup(context, o, self)) # size left if part.auto_left in {'WIDTH', 'ALL'}: self.manip_stack.append(part.manipulators[3].setup(context, o, part)) # size right if part.auto_right in {'WIDTH', 'ALL'}: self.manip_stack.append(part.manipulators[4].setup(context, o, part)) # slope left if part.auto_left in {'SLOPE', 'ALL'}: self.manip_stack.append(part.manipulators[5].setup(context, o, part)) # slope right if part.auto_right in {'SLOPE', 'ALL'}: self.manip_stack.append(part.manipulators[6].setup(context, o, part)) for m in self.manipulators: self.manip_stack.append(m.setup(context, o, self)) def draw(self, layout, context): box = layout.box() row = box.row() if self.parts_expand: row.prop(self, 'parts_expand', icon="TRIA_DOWN", text="Parts", emboss=False) box.prop(self, 'n_parts') # box.prop(self, 'closed') for i, part in enumerate(self.parts): part.draw(layout, context, i) else: row.prop(self, 'parts_expand', icon="TRIA_RIGHT", text="Parts", emboss=False) 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_roof_cutter_segment(ArchipackCutterPart, PropertyGroup): manipulators : CollectionProperty(type=archipack_manipulator) type : EnumProperty( name="Type", items=( ('SIDE', 'Side', 'Side with bargeboard', 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='SIDE', update=update_hole ) def find_in_selection(self, context): selected = context.selected_objects[:] for o in selected: d = archipack_roof_cutter.datablock(o) if d: for part in d.parts: if part == self: return d return None class archipack_roof_cutter(ArchipackCutter, ArchipackObject, Manipulable, PropertyGroup): # boundary parts : CollectionProperty(type=archipack_roof_cutter_segment) boundary : StringProperty( default="", name="Boundary", description="Boundary of t child to cut parent" ) def update_points(self, context, o, pts, update_parent=False): """ Create boundary from roof """ self.auto_update = False self.manipulable_disable(context) self.from_points(pts) self.manipulable_refresh = True self.auto_update = True if update_parent: self.update_parent(context, o) # print("update_points") def update_parent(self, context, o): d = archipack_roof.datablock(o.parent) if d is not None: o.parent.select_set(state=True) context.view_layer.objects.active = o.parent d.update(context, update_childs=False, update_hole=False) o.parent.select_set(state=False) context.view_layer.objects.active = o # print("update_parent") class ARCHIPACK_PT_roof_cutter(Panel): bl_idname = "ARCHIPACK_PT_roof_cutter" bl_label = "Roof Cutter" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = 'Archipack' @classmethod def poll(cls, context): return archipack_roof_cutter.filter(context.active_object) def draw(self, context): prop = archipack_roof_cutter.datablock(context.active_object) if prop is None: return layout = self.layout scene = context.scene box = layout.box() if prop.boundary != "": box.label(text="Auto Cutter:") box.label(text=prop.boundary) else: box.operator('archipack.roof_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) class ARCHIPACK_PT_roof(Panel): bl_idname = "ARCHIPACK_PT_roof" bl_label = "Roof" bl_space_type = 'VIEW_3D' bl_region_type = 'UI' bl_category = 'Archipack' @classmethod def poll(cls, context): return archipack_roof.filter(context.active_object) def draw(self, context): o = context.active_object prop = archipack_roof.datablock(o) if prop is None: return scene = context.scene layout = self.layout row = layout.row(align=True) row.operator('archipack.roof_manipulate', icon='VIEW_PAN') box = layout.box() row = box.row(align=True) row.operator("archipack.roof_preset_menu", text=bpy.types.ARCHIPACK_OT_roof_preset_menu.bl_label) row.operator("archipack.roof_preset", text="", icon='ADD') row.operator("archipack.roof_preset", text="", icon='REMOVE').remove_active = True box = layout.box() box.prop_search(prop, "t_parent", scene, "objects", text="Parent", icon='OBJECT_DATA') layout.operator('archipack.roof_cutter').parent = o.name p, d = prop.find_parent(context) if d is not None: box.prop(prop, 't_part') box.prop(prop, 't_dist_x') box.prop(prop, 't_dist_y') box.label(text="Hole") box.prop(prop, 'hole_offset_front') box.prop(prop, 'hole_offset_left') box.prop(prop, 'hole_offset_right') box = layout.box() box.prop(prop, 'quick_edit', icon="MOD_MULTIRES") box.prop(prop, 'draft') if d is None: box.prop(prop, 'z') box.prop(prop, 'slope_left') box.prop(prop, 'slope_right') box.prop(prop, 'width_left') box.prop(prop, 'width_right') # parts prop.draw(layout, context) # tiles box = layout.box() row = box.row(align=True) if prop.tile_expand: row.prop(prop, 'tile_expand', icon="TRIA_DOWN", text="Covering", emboss=False) else: row.prop(prop, 'tile_expand', icon="TRIA_RIGHT", text="Covering", emboss=False) row.prop(prop, 'tile_enable') if prop.tile_expand: box.prop(prop, 'tile_model', text="") box.prop(prop, 'tile_solidify', icon='MOD_SOLIDIFY') if prop.tile_solidify: box.prop(prop, 'tile_height') box.separator() box.prop(prop, 'tile_bevel', icon='MOD_BEVEL') if prop.tile_bevel: box.prop(prop, 'tile_bevel_amt') box.prop(prop, 'tile_bevel_segs') box.separator() box.label(text="Tile size") box.prop(prop, 'tile_size_x') box.prop(prop, 'tile_size_y') box.prop(prop, 'tile_size_z') box.prop(prop, 'tile_altitude') box.separator() box.label(text="Distribution") box.prop(prop, 'tile_alternate', icon='NLA') row = box.row(align=True) row.prop(prop, 'tile_fit_x', icon='ALIGN') row.prop(prop, 'tile_fit_y', icon='ALIGN') box.prop(prop, 'tile_offset') box.label(text="Spacing") box.prop(prop, 'tile_space_x') box.prop(prop, 'tile_space_y') box.separator() # hip box.label(text="Borders") box.prop(prop, 'tile_side') box.prop(prop, 'tile_couloir') box.prop(prop, 'tile_border') box = layout.box() row = box.row(align=True) if prop.hip_expand: row.prop(prop, 'hip_expand', icon="TRIA_DOWN", text="Hip", emboss=False) else: row.prop(prop, 'hip_expand', icon="TRIA_RIGHT", text="Hip", emboss=False) row.prop(prop, 'hip_enable') if prop.hip_expand: box.prop(prop, 'hip_model', text="") box.prop(prop, 'hip_size_x') box.prop(prop, 'hip_size_y') box.prop(prop, 'hip_size_z') box.prop(prop, 'hip_alt') box.prop(prop, 'hip_space_x') box.separator() box.prop(prop, 'valley_enable') box.prop(prop, 'valley_altitude') box = layout.box() row = box.row(align=True) if prop.beam_expand: row.prop(prop, 'beam_expand', icon="TRIA_DOWN", text="Beam", emboss=False) else: row.prop(prop, 'beam_expand', icon="TRIA_RIGHT", text="Beam", emboss=False) if prop.beam_expand: box.prop(prop, 'beam_enable') if prop.beam_enable: box.prop(prop, 'beam_width') box.prop(prop, 'beam_height') box.prop(prop, 'beam_offset') box.prop(prop, 'beam_alt') box.separator() box.prop(prop, 'beam_sec_enable') if prop.beam_sec_enable: box.prop(prop, 'beam_sec_width') box.prop(prop, 'beam_sec_height') box.prop(prop, 'beam_sec_alt') box.separator() box.prop(prop, 'rafter_enable') if prop.rafter_enable: box.prop(prop, 'rafter_height') box.prop(prop, 'rafter_width') box.prop(prop, 'rafter_spacing') box.prop(prop, 'rafter_start') box.prop(prop, 'rafter_alt') box = layout.box() row = box.row(align=True) if prop.gutter_expand: row.prop(prop, 'gutter_expand', icon="TRIA_DOWN", text="Gutter", emboss=False) else: row.prop(prop, 'gutter_expand', icon="TRIA_RIGHT", text="Gutter", emboss=False) row.prop(prop, 'gutter_enable') if prop.gutter_expand: box.prop(prop, 'gutter_alt') box.prop(prop, 'gutter_width') box.prop(prop, 'gutter_dist') box.prop(prop, 'gutter_boudin') box.prop(prop, 'gutter_segs') box = layout.box() row = box.row(align=True) if prop.fascia_expand: row.prop(prop, 'fascia_expand', icon="TRIA_DOWN", text="Fascia", emboss=False) else: row.prop(prop, 'fascia_expand', icon="TRIA_RIGHT", text="Fascia", emboss=False) row.prop(prop, 'fascia_enable') if prop.fascia_expand: box.prop(prop, 'fascia_altitude') box.prop(prop, 'fascia_width') box.prop(prop, 'fascia_height') box.prop(prop, 'fascia_offset') box = layout.box() row = box.row(align=True) if prop.bargeboard_expand: row.prop(prop, 'bargeboard_expand', icon="TRIA_DOWN", text="Bargeboard", emboss=False) else: row.prop(prop, 'bargeboard_expand', icon="TRIA_RIGHT", text="Bargeboard", emboss=False) row.prop(prop, 'bargeboard_enable') if prop.bargeboard_expand: box.prop(prop, 'bargeboard_altitude') box.prop(prop, 'bargeboard_width') box.prop(prop, 'bargeboard_height') box.prop(prop, 'bargeboard_offset') """ box = layout.box() row.prop_search(prop, "user_defined_path", scene, "objects", text="", icon='OUTLINER_OB_CURVE') box.prop(prop, 'user_defined_resolution') box.prop(prop, 'angle_limit') """ # ------------------------------------------------------------------ # Define operator class to create object # ------------------------------------------------------------------ class ARCHIPACK_OT_roof(ArchipackCreateTool, Operator): bl_idname = "archipack.roof" bl_label = "Roof" bl_description = "Roof" bl_category = 'Archipack' bl_options = {'REGISTER', 'UNDO'} def create(self, context): m = bpy.data.meshes.new("Roof") o = bpy.data.objects.new("Roof", m) d = m.archipack_roof.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.add_material(o) # disable progress bar when # background render thumbs if not self.auto_manipulate: d.quick_edit = False self.load_preset(d) return o # ----------------------------------------------------- # Execute # ----------------------------------------------------- def execute(self, context): if context.mode == "OBJECT": bpy.ops.object.select_all(action="DESELECT") o = self.create(context) o.location = 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_roof_cutter(ArchipackCreateTool, Operator): bl_idname = "archipack.roof_cutter" bl_label = "Roof Cutter" bl_description = "Roof Cutter" bl_category = 'Archipack' bl_options = {'REGISTER', 'UNDO'} parent : StringProperty("") def create(self, context): m = bpy.data.meshes.new("Roof Cutter") o = bpy.data.objects.new("Roof Cutter", m) d = m.archipack_roof_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_roof.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 create object # ------------------------------------------------------------------ class ARCHIPACK_OT_roof_from_curve(ArchipackCreateTool, Operator): bl_idname = "archipack.roof_from_curve" bl_label = "Roof curve" bl_description = "Create a roof 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' 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 m = bpy.data.meshes.new("Roof") o = bpy.data.objects.new("Roof", m) d = m.archipack_roof.add() # make manipulators selectable d.manipulable_selectable = True d.user_defined_path = curve.name self.link_object_to_scene(context, o) o.select_set(state=True) context.view_layer.objects.active = o d.update_path(context) spline = curve.data.splines[0] 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([ [1, 0, 0, pt.x], [0, 1, 0, pt.y], [0, 0, 1, pt.z], [0, 0, 0, 1] ]) o.select_set(state=True) context.view_layer.objects.active = o return o # ----------------------------------------------------- # Execute # ----------------------------------------------------- def execute(self, context): if context.mode == "OBJECT": bpy.ops.object.select_all(action="DESELECT") self.create(context) if self.auto_manipulate: bpy.ops.archipack.roof_manipulate('INVOKE_DEFAULT') return {'FINISHED'} else: self.report({'WARNING'}, "Archipack: Option only valid in Object mode") return {'CANCELLED'} # ------------------------------------------------------------------ # Define operator class to manipulate object # ------------------------------------------------------------------ class ARCHIPACK_OT_roof_manipulate(Operator): bl_idname = "archipack.roof_manipulate" bl_label = "Manipulate" bl_description = "Manipulate" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(self, context): return archipack_roof.filter(context.active_object) def invoke(self, context, event): d = archipack_roof.datablock(context.active_object) d.manipulable_invoke(context) return {'FINISHED'} class ARCHIPACK_OT_roof_cutter_manipulate(Operator): bl_idname = "archipack.roof_cutter_manipulate" bl_label = "Manipulate" bl_description = "Manipulate" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(self, context): return archipack_roof_cutter.filter(context.active_object) def invoke(self, context, event): d = archipack_roof_cutter.datablock(context.active_object) d.manipulable_invoke(context) return {'FINISHED'} # Update throttle class ArchipackThrottleHandler(): """ One modal runs for each object at time when call for 2nd one update timer so first one wait more and kill 2nd one """ def __init__(self, context, delay): self._timer = None self.start = 0 self.update_state = False self.delay = delay def start_timer(self, context): self.start = time.time() self._timer = context.window_manager.event_timer_add(self.delay, window=context.window) def stop_timer(self, context): if self._timer is not None: context.window_manager.event_timer_remove(self._timer) self._timer = None def execute(self, context): """ refresh timer on execute return True if modal should run False on complete """ if self._timer is None: self.update_state = False self.start_timer(context) return True # already a timer running self.stop_timer(context) # prevent race conditions when already in update mode if self.is_updating: return False self.start_timer(context) return False def modal(self, context, event): if event.type == 'TIMER' and not self.is_updating: if time.time() - self.start > self.delay: self.update_state = True self.stop_timer(context) return True return False @property def is_updating(self): return self.update_state throttle_handlers = {} throttle_delay = 1 class ARCHIPACK_OT_roof_throttle_update(Operator): bl_idname = "archipack.roof_throttle_update" bl_label = "Update childs with a delay" name : StringProperty() def kill_handler(self, context, name): if name in throttle_handlers.keys(): throttle_handlers[name].stop_timer(context) del throttle_handlers[self.name] def get_handler(self, context, delay): global throttle_handlers if self.name not in throttle_handlers.keys(): throttle_handlers[self.name] = ArchipackThrottleHandler(context, delay) return throttle_handlers[self.name] def modal(self, context, event): global throttle_handlers if self.name in throttle_handlers.keys(): if throttle_handlers[self.name].modal(context, event): act = context.active_object o = context.scene.objects.get(self.name.strip()) # print("delay update of %s" % (self.name)) if o is not None: selected = o.select_get() o.select_set(state=True) context.view_layer.objects.active = o d = o.data.archipack_roof[0] d.update(context, force_update=True, update_parent=False) # skip_parent_update=self.skip_parent_update) o.select_set(state=selected) context.view_layer.objects.active = act del throttle_handlers[self.name] return {'FINISHED'} else: return {'PASS_THROUGH'} else: return {'FINISHED'} def execute(self, context): global throttle_delay handler = self.get_handler(context, throttle_delay) if handler.execute(context): context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} return {'FINISHED'} # ------------------------------------------------------------------ # Define operator class to load / save presets # ------------------------------------------------------------------ class ARCHIPACK_OT_roof_preset_menu(PresetMenuOperator, Operator): bl_description = "Show Roof presets" bl_idname = "archipack.roof_preset_menu" bl_label = "Roof Styles" preset_subdir = "archipack_roof" class ARCHIPACK_OT_roof_preset(ArchipackPreset, Operator): """Add a Roof Styles""" bl_idname = "archipack.roof_preset" bl_label = "Add Roof Style" preset_menu = "ARCHIPACK_OT_roof_preset_menu" @property def blacklist(self): return ['n_parts', 'parts', 'manipulators', 'user_defined_path', 'quick_edit', 'draft'] def register(): # bpy.utils.register_class(archipack_roof_material) bpy.utils.register_class(archipack_roof_cutter_segment) bpy.utils.register_class(archipack_roof_cutter) bpy.utils.register_class(ARCHIPACK_PT_roof_cutter) bpy.utils.register_class(ARCHIPACK_OT_roof_cutter) bpy.utils.register_class(ARCHIPACK_OT_roof_cutter_manipulate) Mesh.archipack_roof_cutter = CollectionProperty(type=archipack_roof_cutter) bpy.utils.register_class(archipack_roof_segment) bpy.utils.register_class(archipack_roof) Mesh.archipack_roof = CollectionProperty(type=archipack_roof) bpy.utils.register_class(ARCHIPACK_OT_roof_preset_menu) bpy.utils.register_class(ARCHIPACK_PT_roof) bpy.utils.register_class(ARCHIPACK_OT_roof) bpy.utils.register_class(ARCHIPACK_OT_roof_preset) bpy.utils.register_class(ARCHIPACK_OT_roof_manipulate) bpy.utils.register_class(ARCHIPACK_OT_roof_from_curve) bpy.utils.register_class(ARCHIPACK_OT_roof_throttle_update) def unregister(): # bpy.utils.unregister_class(archipack_roof_material) bpy.utils.unregister_class(archipack_roof_cutter_segment) bpy.utils.unregister_class(archipack_roof_cutter) bpy.utils.unregister_class(ARCHIPACK_PT_roof_cutter) bpy.utils.unregister_class(ARCHIPACK_OT_roof_cutter) bpy.utils.unregister_class(ARCHIPACK_OT_roof_cutter_manipulate) del Mesh.archipack_roof_cutter bpy.utils.unregister_class(archipack_roof_segment) bpy.utils.unregister_class(archipack_roof) del Mesh.archipack_roof bpy.utils.unregister_class(ARCHIPACK_OT_roof_preset_menu) bpy.utils.unregister_class(ARCHIPACK_PT_roof) bpy.utils.unregister_class(ARCHIPACK_OT_roof) bpy.utils.unregister_class(ARCHIPACK_OT_roof_preset) bpy.utils.unregister_class(ARCHIPACK_OT_roof_manipulate) bpy.utils.unregister_class(ARCHIPACK_OT_roof_from_curve) bpy.utils.unregister_class(ARCHIPACK_OT_roof_throttle_update)