blender-addons/archipack/archipack_roof.py

5414 lines
182 KiB
Python

# -*- 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 #####
# <pep8 compliant>
# ----------------------------------------------------------
# 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)