blender-addons/archipack/archipack_wall2.py

2438 lines
79 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)
#
# ----------------------------------------------------------
import bpy
import bmesh
import time
from bpy.types import Operator, PropertyGroup, Mesh, Panel
from bpy.props import (
FloatProperty, BoolProperty, IntProperty, StringProperty,
FloatVectorProperty, CollectionProperty, EnumProperty
)
from .bmesh_utils import BmeshEdit as bmed
from mathutils import Vector, Matrix
from mathutils.geometry import (
interpolate_bezier
)
from math import sin, cos, pi, atan2
from .archipack_manipulator import (
Manipulable, archipack_manipulator,
GlPolygon, GlPolyline,
GlLine, GlText, FeedbackPanel
)
from .archipack_object import ArchipackObject, ArchipackCreateTool, ArchipackDrawTool
from .archipack_2d import Line, Arc
from .archipack_snap import snap_point
from .archipack_keymaps import Keymaps
import logging
logger = logging.getLogger("archipack")
class Wall():
def __init__(self, wall_z, z, t, flip):
self.z = z
self.wall_z = wall_z
self.t = t
self.flip = flip
self.z_step = len(z)
def set_offset(self, offset, last=None):
"""
Offset line and compute intersection point
between segments
"""
self.line = self.make_offset(offset, last)
def get_z(self, t):
t0 = self.t[0]
z0 = self.z[0]
for i in range(1, self.z_step):
t1 = self.t[i]
z1 = self.z[i]
if t <= t1:
return z0 + (t - t0) / (t1 - t0) * (z1 - z0)
t0, z0 = t1, z1
return self.z[-1]
def make_faces(self, i, f, faces):
if i < self.n_step:
# 1 3 5 7
# 0 2 4 6
if self.flip:
faces.append((f + 2, f, f + 1, f + 3))
else:
faces.append((f, f + 2, f + 3, f + 1))
def p3d(self, verts, t):
x, y = self.lerp(t)
z = self.wall_z + self.get_z(t)
verts.append((x, y, 0))
verts.append((x, y, z))
def make_wall(self, i, verts, faces):
t = self.t_step[i]
f = len(verts)
self.p3d(verts, t)
self.make_faces(i, f, faces)
def make_hole(self, i, verts, z0):
t = self.t_step[i]
x, y = self.line.lerp(t)
verts.append((x, y, z0))
def straight_wall(self, a0, length, wall_z, z, t):
r = self.straight(length).rotate(a0)
return StraightWall(r.p, r.v, wall_z, z, t, self.flip)
def curved_wall(self, a0, da, radius, wall_z, z, t):
n = self.normal(1).rotate(a0).scale(radius)
if da < 0:
n.v = -n.v
a0 = n.angle
c = n.p - n.v
return CurvedWall(c, radius, a0, da, wall_z, z, t, self.flip)
class StraightWall(Wall, Line):
def __init__(self, p, v, wall_z, z, t, flip):
Line.__init__(self, p, v)
Wall.__init__(self, wall_z, z, t, flip)
def param_t(self, step_angle):
self.t_step = self.t
self.n_step = len(self.t) - 1
class CurvedWall(Wall, Arc):
def __init__(self, c, radius, a0, da, wall_z, z, t, flip):
Arc.__init__(self, c, radius, a0, da)
Wall.__init__(self, wall_z, z, t, flip)
def param_t(self, step_angle):
t_step, n_step = self.steps_by_angle(step_angle)
self.t_step = list(sorted([i * t_step for i in range(1, n_step)] + self.t))
self.n_step = len(self.t_step) - 1
class WallGenerator():
def __init__(self, parts):
self.last_type = 'NONE'
self.segs = []
self.parts = parts
self.faces_type = 'NONE'
self.closed = False
def set_offset(self, offset):
last = None
for i, seg in enumerate(self.segs):
seg.set_offset(offset, last)
last = seg.line
if self.closed:
w = self.segs[-1]
if len(self.segs) > 1:
w.line = w.make_offset(offset, self.segs[-2].line)
p1 = self.segs[0].line.p1
self.segs[0].line = self.segs[0].make_offset(offset, w.line)
self.segs[0].line.p1 = p1
def add_part(self, part, wall_z, flip):
# TODO:
# refactor this part (height manipulators)
manip_index = []
if len(self.segs) < 1:
s = None
z = [part.z[0]]
manip_index.append(0)
else:
s = self.segs[-1]
z = [s.z[-1]]
t_cur = 0
z_last = part.n_splits - 1
t = [0]
for i in range(part.n_splits):
t_try = t[-1] + part.t[i]
if t_try == t_cur:
continue
if t_try <= 1:
t_cur = t_try
t.append(t_cur)
z.append(part.z[i])
manip_index.append(i)
else:
z_last = i
break
if t_cur < 1:
t.append(1)
manip_index.append(z_last)
z.append(part.z[z_last])
# start a new wall
if s is None:
if part.type == 'S_WALL':
p = Vector((0, 0))
v = part.length * Vector((cos(part.a0), sin(part.a0)))
s = StraightWall(p, v, wall_z, z, t, flip)
elif part.type == 'C_WALL':
c = -part.radius * Vector((cos(part.a0), sin(part.a0)))
s = CurvedWall(c, part.radius, part.a0, part.da, wall_z, z, t, flip)
else:
if part.type == 'S_WALL':
s = s.straight_wall(part.a0, part.length, wall_z, z, t)
elif part.type == 'C_WALL':
s = s.curved_wall(part.a0, part.da, part.radius, wall_z, z, t)
self.segs.append(s)
self.last_type = part.type
return manip_index
def close(self, closed):
# Make last segment implicit closing one
if closed:
part = self.parts[-1]
w = self.segs[-1]
dp = self.segs[0].p0 - self.segs[-1].p0
if "C_" in part.type:
dw = (w.p1 - w.p0)
w.r = part.radius / dw.length * dp.length
# angle pt - p0 - angle p0 p1
da = atan2(dp.y, dp.x) - atan2(dw.y, dw.x)
a0 = w.a0 + da
if a0 > pi:
a0 -= 2 * pi
if a0 < -pi:
a0 += 2 * pi
w.a0 = a0
else:
w.v = dp
def locate_manipulators(self, side):
for i, wall in enumerate(self.segs):
manipulators = self.parts[i].manipulators
p0 = wall.p0.to_3d()
p1 = wall.p1.to_3d()
# angle from last to current segment
if i > 0:
if i < len(self.segs) - 1:
manipulators[0].type_key = 'ANGLE'
else:
manipulators[0].type_key = 'DUMB_ANGLE'
v0 = self.segs[i - 1].straight(-side, 1).v.to_3d()
v1 = wall.straight(side, 0).v.to_3d()
manipulators[0].set_pts([p0, v0, v1])
if type(wall).__name__ == "StraightWall":
# segment length
manipulators[1].type_key = 'SIZE'
manipulators[1].prop1_name = "length"
manipulators[1].set_pts([p0, p1, (side, 0, 0)])
else:
# segment radius + angle
# scale to fix overlap with drag
v0 = side * (wall.p0 - wall.c).to_3d()
v1 = side * (wall.p1 - wall.c).to_3d()
scale = 1.0 + (0.5 / v0.length)
manipulators[1].type_key = 'ARC_ANGLE_RADIUS'
manipulators[1].prop1_name = "da"
manipulators[1].prop2_name = "radius"
manipulators[1].set_pts([wall.c.to_3d(), scale * v0, scale * v1])
# snap manipulator, don't change index !
manipulators[2].set_pts([p0, p1, (1, 0, 0)])
# dumb, segment index
z = Vector((0, 0, 0.75 * wall.wall_z))
manipulators[3].set_pts([p0 + z, p1 + z, (1, 0, 0)])
def make_wall(self, step_angle, flip, closed, verts, faces):
# swap manipulators so they always face outside
side = 1
if flip:
side = -1
# Make last segment implicit closing one
nb_segs = len(self.segs) - 1
if closed:
nb_segs += 1
for i, wall in enumerate(self.segs):
wall.param_t(step_angle)
if i < nb_segs:
for j in range(wall.n_step + 1):
wall.make_wall(j, verts, faces)
else:
# last segment
for j in range(wall.n_step):
continue
# print("%s" % (wall.n_step))
# wall.make_wall(j, verts, faces)
self.locate_manipulators(side)
def rotate(self, idx_from, a):
"""
apply rotation to all following segs
"""
self.segs[idx_from].rotate(a)
ca = cos(a)
sa = sin(a)
rM = Matrix([
[ca, -sa],
[sa, ca]
])
# rotation center
p0 = self.segs[idx_from].p0
for i in range(idx_from + 1, len(self.segs)):
seg = self.segs[i]
seg.rotate(a)
dp = rM @ (seg.p0 - p0)
seg.translate(dp)
def translate(self, idx_from, dp):
"""
apply translation to all following segs
"""
self.segs[idx_from].p1 += dp
for i in range(idx_from + 1, len(self.segs)):
self.segs[i].translate(dp)
def 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 draw(self, context):
for seg in self.segs:
seg.draw(context, render=False)
def debug(self, verts):
for wall in self.segs:
for i in range(33):
x, y = wall.lerp(i / 32)
verts.append((x, y, 0))
def make_surface(self, o, verts, height):
bm = bmesh.new()
for v in verts:
bm.verts.new(v)
bm.verts.ensure_lookup_table()
for i in range(1, len(verts)):
bm.edges.new((bm.verts[i - 1], bm.verts[i]))
bm.edges.new((bm.verts[-1], bm.verts[0]))
bm.edges.ensure_lookup_table()
bmesh.ops.contextual_create(bm, geom=bm.edges)
geom = bm.faces[:]
bmesh.ops.solidify(bm, geom=geom, thickness=height)
bm.to_mesh(o.data)
bm.free()
def make_hole(self, context, hole_obj, d):
offset = -0.5 * (1 - d.x_offset) * d.width
z0 = 0.1
self.set_offset(offset)
nb_segs = len(self.segs) - 1
if d.closed:
nb_segs += 1
verts = []
for i, wall in enumerate(self.segs):
wall.param_t(d.step_angle)
if i < nb_segs:
for j in range(wall.n_step + 1):
wall.make_hole(j, verts, -z0)
self.make_surface(hole_obj, verts, d.z + z0)
def update(self, context):
self.update(context)
def update_childs(self, context):
self.update(context, update_childs=True, manipulable_refresh=True)
def update_manipulators(self, context):
self.update(context, manipulable_refresh=True)
def update_t_part(self, context):
"""
Make this wall a T child of parent wall
orient child so y points inside wall and x follow wall segment
set child a0 according
"""
o = self.find_in_selection(context)
if o is not None:
# w is parent wall
w = context.scene.objects.get(self.t_part.strip())
wd = archipack_wall2.datablock(w)
if wd is not None:
og = self.get_generator()
self.setup_childs(o, og)
bpy.ops.object.select_all(action="DESELECT")
# 5 cases here:
# 1 No parents at all
# 2 o has parent
# 3 w has parent
# 4 o and w share same parent already
# 5 o and w doesn't share parent
link_to_parent = False
# when both walls do have a reference point, we may delete one of them
to_delete = None
# select childs and make parent reference point active
if w.parent is None:
# Either link to o.parent or create new parent
link_to_parent = True
if o.parent is None:
# create a reference point and make it active
x, y, z = w.bound_box[0]
context.scene.cursor.location = w.matrix_world @ Vector((x, y, z))
# fix issue #9
context.view_layer.objects.active = o
bpy.ops.archipack.reference_point()
o.select_set(state=True)
else:
context.view_layer.objects.active = o.parent
w.select_set(state=True)
else:
# w has parent
if o.parent is not w.parent:
link_to_parent = True
context.view_layer.objects.active = w.parent
o.select_set(state=True)
if o.parent is not None:
# store o.parent to delete it
to_delete = o.parent
for c in o.parent.children:
if c is not o:
c.hide_select = False
c.select_set(state=True)
parent = context.active_object
dmax = 2 * wd.width
wg = wd.get_generator()
otM = o.matrix_world
orM = Matrix([
otM[0].to_2d(),
otM[1].to_2d()
])
wtM = w.matrix_world
wrM = Matrix([
wtM[0].to_2d(),
wtM[1].to_2d()
])
# dir in absolute world coordsys
dir = orM @ og.segs[0].straight(1, 0).v
# pt in w coordsys
pos = otM.translation
pt = (wtM.inverted() @ pos).to_2d()
for wall_idx, wall in enumerate(wg.segs):
res, dist, t = wall.point_sur_segment(pt)
# outside is on the right side of the wall
# p1
# |-- x
# p0
# NOTE:
# rotation here is wrong when w has not parent while o has parent
if res and t > 0 and t < 1 and abs(dist) < dmax:
x = wrM @ wall.straight(1, t).v
y = wrM @ wall.normal(t).v.normalized()
self.parts[0].a0 = dir.angle_signed(x)
o.matrix_world = Matrix([
[x.x, -y.x, 0, pos.x],
[x.y, -y.y, 0, pos.y],
[0, 0, 1, pos.z],
[0, 0, 0, 1]
])
break
if link_to_parent and bpy.ops.archipack.parent_to_reference.poll():
bpy.ops.archipack.parent_to_reference('INVOKE_DEFAULT')
# update generator to take new rotation in account
# use this to relocate windows on wall after reparenting
g = self.get_generator()
self.relocate_childs(context, o, g)
# hide holes from select
for c in parent.children:
if "archipack_hybridhole" in c:
c.hide_select = True
# delete unneeded reference point
if to_delete is not None:
bpy.ops.object.select_all(action="DESELECT")
to_delete.select_set(state=True)
context.view_layer.objects.active = to_delete
if bpy.ops.object.delete.poll():
bpy.ops.object.delete(use_global=False)
elif self.t_part != "":
self.t_part = ""
self.restore_context(context)
def set_splits(self, value):
if self.n_splits != value:
self.auto_update = False
self._set_t(value)
self.auto_update = True
self.n_splits = value
return None
def get_splits(self):
return self.n_splits
def update_type(self, context):
d = self.find_datablock_in_selection(context)
if d is not None and d.auto_update:
d.auto_update = False
idx = 0
for i, part in enumerate(d.parts):
if part == self:
idx = i
break
a0 = 0
if idx > 0:
g = d.get_generator()
w0 = g.segs[idx - 1]
a0 = w0.straight(1).angle
if "C_" in self.type:
w = w0.straight_wall(self.a0, self.length, d.z, self.z, self.t)
else:
w = w0.curved_wall(self.a0, self.da, self.radius, d.z, self.z, self.t)
else:
if "C_" in self.type:
p = Vector((0, 0))
v = self.length * Vector((cos(self.a0), sin(self.a0)))
w = StraightWall(p, v, d.z, self.z, self.t, d.flip)
a0 = pi / 2
else:
c = -self.radius * Vector((cos(self.a0), sin(self.a0)))
w = CurvedWall(c, self.radius, self.a0, pi, d.z, self.z, self.t, d.flip)
# w0 - w - w1
if d.closed and idx == d.n_parts:
dp = - w.p0
else:
dp = w.p1 - w.p0
if "C_" in self.type:
self.radius = 0.5 * dp.length
self.da = pi
a0 = atan2(dp.y, dp.x) - pi / 2 - a0
else:
self.length = dp.length
a0 = atan2(dp.y, dp.x) - a0
if a0 > pi:
a0 -= 2 * pi
if a0 < -pi:
a0 += 2 * pi
self.a0 = a0
if idx + 1 < d.n_parts:
# adjust rotation of next part
part1 = d.parts[idx + 1]
if "C_" in self.type:
a0 = part1.a0 - pi / 2
else:
a0 = part1.a0 + w.straight(1).angle - atan2(dp.y, dp.x)
if a0 > pi:
a0 -= 2 * pi
if a0 < -pi:
a0 += 2 * pi
part1.a0 = a0
d.auto_update = True
class archipack_wall2_part(PropertyGroup):
type : EnumProperty(
items=(
('S_WALL', 'Straight', '', 0),
('C_WALL', 'Curved', '', 1)
),
default='S_WALL',
update=update_type
)
length : FloatProperty(
name="Length",
min=0.01,
default=2.0,
unit='LENGTH', subtype='DISTANCE',
update=update
)
radius : FloatProperty(
name="Radius",
min=0.5,
default=0.7,
unit='LENGTH', subtype='DISTANCE',
update=update
)
a0 : FloatProperty(
name="Start angle",
min=-pi,
max=pi,
default=pi / 2,
subtype='ANGLE', unit='ROTATION',
update=update
)
da : FloatProperty(
name="Angle",
min=-pi,
max=pi,
default=pi / 2,
subtype='ANGLE', unit='ROTATION',
update=update
)
z : FloatVectorProperty(
name="Height",
default=[
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0
],
size=31,
update=update
)
t : FloatVectorProperty(
name="Position",
min=0,
max=1,
default=[
1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1
],
size=31,
update=update
)
splits : IntProperty(
name="Splits",
default=1,
min=1,
max=31,
get=get_splits, set=set_splits
)
n_splits : IntProperty(
name="Splits",
default=1,
min=1,
max=31,
update=update
)
auto_update : BoolProperty(default=True)
manipulators : CollectionProperty(type=archipack_manipulator)
# ui related
expand : BoolProperty(default=False)
def _set_t(self, splits):
t = 1 / splits
for i in range(splits):
self.t[i] = t
def find_datablock_in_selection(self, context):
"""
find witch selected object this instance belongs to
provide support for "copy to selected"
"""
selected = context.selected_objects[:]
for o in selected:
props = archipack_wall2.datablock(o)
if props:
for part in props.parts:
if part == self:
return props
return None
def update(self, context, manipulable_refresh=False):
if not self.auto_update:
return
props = self.find_datablock_in_selection(context)
if props is not None:
props.update(context, manipulable_refresh)
def draw(self, layout, context, index):
row = layout.row(align=True)
if self.expand:
row.prop(self, 'expand', icon="TRIA_DOWN", text="Part " + str(index + 1), emboss=False)
else:
row.prop(self, 'expand', icon="TRIA_RIGHT", text="Part " + str(index + 1), emboss=False)
row.prop(self, "type", text="")
if self.expand:
row = layout.row(align=True)
row.operator("archipack.wall2_insert", text="Split").index = index
row.operator("archipack.wall2_remove", text="Remove").index = index
if self.type == 'C_WALL':
row = layout.row()
row.prop(self, "radius")
row = layout.row()
row.prop(self, "da")
else:
row = layout.row()
row.prop(self, "length")
row = layout.row()
row.prop(self, "a0")
row = layout.row()
row.prop(self, "splits")
for split in range(self.n_splits):
row = layout.row()
row.prop(self, "z", text="alt", index=split)
row.prop(self, "t", text="pos", index=split)
class archipack_wall2_child(PropertyGroup):
# Size Loc
# Delta Loc
manipulators : CollectionProperty(type=archipack_manipulator)
child_name : StringProperty()
wall_idx : IntProperty()
pos : FloatVectorProperty(subtype='XYZ')
flip : BoolProperty(default=False)
def get_child(self, context):
d = None
child = context.scene.objects.get(self.child_name.strip())
if child is not None and child.data is not None:
cd = child.data
if 'archipack_window' in cd:
d = cd.archipack_window[0]
elif 'archipack_door' in cd:
d = cd.archipack_door[0]
return child, d
class archipack_wall2(ArchipackObject, Manipulable, PropertyGroup):
parts : CollectionProperty(type=archipack_wall2_part)
n_parts : IntProperty(
name="Parts",
min=1,
max=1024,
default=1, update=update_manipulators
)
step_angle : FloatProperty(
description="Curved parts segmentation",
name="Step angle",
min=1 / 180 * pi,
max=pi,
default=6 / 180 * pi,
subtype='ANGLE', unit='ROTATION',
update=update
)
width : FloatProperty(
name="Width",
min=0.01,
default=0.2,
unit='LENGTH', subtype='DISTANCE',
update=update
)
z : FloatProperty(
name='Height',
min=0.1,
default=2.7, precision=2,
unit='LENGTH', subtype='DISTANCE',
description='height', update=update,
)
x_offset : FloatProperty(
name="Offset",
min=-1, max=1,
default=-1, precision=2, step=1,
update=update
)
radius : FloatProperty(
name="Radius",
min=0.5,
default=0.7,
unit='LENGTH', subtype='DISTANCE',
update=update
)
da : FloatProperty(
name="Angle",
min=-pi,
max=pi,
default=pi / 2,
subtype='ANGLE', unit='ROTATION',
update=update
)
flip : BoolProperty(
name="Flip",
default=False,
update=update_childs
)
closed : BoolProperty(
default=False,
name="Close",
update=update_manipulators
)
auto_update : BoolProperty(
options={'SKIP_SAVE'},
default=True,
update=update_manipulators
)
realtime : BoolProperty(
options={'SKIP_SAVE'},
default=True,
name="Real Time",
description="Relocate childs in realtime"
)
# dumb manipulators to show sizes between childs
childs_manipulators : CollectionProperty(type=archipack_manipulator)
# store to manipulate windows and doors
childs : CollectionProperty(type=archipack_wall2_child)
t_part : StringProperty(
name="Parent wall",
description="This part will follow parent when set",
default="",
update=update_t_part
)
def insert_part(self, context, o, where):
self.manipulable_disable(context)
self.auto_update = False
# the part we do split
part_0 = self.parts[where]
part_0.length /= 2
part_0.da /= 2
self.parts.add()
part_1 = self.parts[len(self.parts) - 1]
part_1.type = part_0.type
part_1.length = part_0.length
part_1.da = part_0.da
part_1.a0 = 0
# move after current one
self.parts.move(len(self.parts) - 1, where + 1)
self.n_parts += 1
# re-eval childs location
g = self.get_generator()
self.setup_childs(o, g)
self.setup_manipulators()
self.auto_update = True
def add_part(self, context, length):
self.manipulable_disable(context)
self.auto_update = False
p = self.parts.add()
p.length = length
self.parts.move(len(self.parts) - 1, self.n_parts)
self.n_parts += 1
self.setup_manipulators()
self.auto_update = True
return self.parts[self.n_parts - 1]
def remove_part(self, context, o, where):
self.manipulable_disable(context)
self.auto_update = False
# preserve shape
# using generator
if where > 0:
g = self.get_generator()
w = g.segs[where - 1]
w.p1 = g.segs[where].p1
if where + 1 < self.n_parts:
self.parts[where + 1].a0 = g.segs[where + 1].delta_angle(w)
part = self.parts[where - 1]
if "C_" in part.type:
part.radius = w.r
else:
part.length = w.length
if where > 1:
part.a0 = w.delta_angle(g.segs[where - 2])
else:
part.a0 = w.straight(1, 0).angle
self.parts.remove(where)
self.n_parts -= 1
# re-eval child location
g = self.get_generator()
self.setup_childs(o, g)
# fix snap manipulators index
self.setup_manipulators()
self.auto_update = True
def get_generator(self):
# print("get_generator")
g = WallGenerator(self.parts)
for part in self.parts:
g.add_part(part, self.z, self.flip)
g.close(self.closed)
return g
def update_parts(self, o, update_childs=False):
# print("update_parts")
# remove rows
# NOTE:
# n_parts+1
# as last one is end point of last segment or closing one
row_change = False
for i in range(len(self.parts), self.n_parts + 1, -1):
row_change = True
self.parts.remove(i - 1)
# add rows
for i in range(len(self.parts), self.n_parts + 1):
row_change = True
self.parts.add()
self.setup_manipulators()
g = self.get_generator()
if o is not None and (row_change or update_childs):
self.setup_childs(o, g)
return g
def setup_manipulators(self):
if len(self.manipulators) == 0:
# make manipulators selectable
s = self.manipulators.add()
s.prop1_name = "width"
s = self.manipulators.add()
s.prop1_name = "n_parts"
s.type_key = 'COUNTER'
s = self.manipulators.add()
s.prop1_name = "z"
s.normal = (0, 1, 0)
if self.t_part != "" and len(self.manipulators) < 4:
s = self.manipulators.add()
s.prop1_name = "x"
s.type_key = 'DELTA_LOC'
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)
p.manipulators[2].prop1_name = str(i)
p.manipulators[3].prop1_name = str(i + 1)
def interpolate_bezier(self, pts, wM, p0, p1, resolution):
if resolution == 0:
pts.append(wM @ p0.co.to_3d())
else:
v = (p1.co - p0.co).normalized()
d1 = (p0.handle_right - p0.co).normalized()
d2 = (p1.co - p1.handle_left).normalized()
if d1 == v and d2 == v:
pts.append(wM @ p0.co.to_3d())
else:
seg = interpolate_bezier(wM @ p0.co,
wM @ p0.handle_right,
wM @ p1.handle_left,
wM @ p1.co,
resolution + 1)
for i in range(resolution):
pts.append(seg[i].to_3d())
def is_cw(self, pts):
p0 = pts[0]
d = 0
for p in pts[1:]:
d += (p.x * p0.y - p.y * p0.x)
p0 = p
return d > 0
def from_spline(self, wM, resolution, spline):
pts = []
if spline.type == 'POLY':
pts = [wM @ p.co.to_3d() for p in spline.points]
if spline.use_cyclic_u:
pts.append(pts[0])
elif spline.type == 'BEZIER':
points = spline.bezier_points
for i in range(1, len(points)):
p0 = points[i - 1]
p1 = points[i]
self.interpolate_bezier(pts, wM, p0, p1, resolution)
if spline.use_cyclic_u:
p0 = points[-1]
p1 = points[0]
self.interpolate_bezier(pts, wM, p0, p1, resolution)
pts.append(pts[0])
else:
pts.append(wM @ points[-1].co)
if self.is_cw(pts):
pts = list(reversed(pts))
self.auto_update = False
self.from_points(pts, spline.use_cyclic_u)
self.auto_update = True
def from_points(self, pts, closed):
self.n_parts = len(pts) - 1
if closed:
self.n_parts -= 1
self.update_parts(None)
p0 = pts.pop(0)
a0 = 0
for i, p1 in enumerate(pts):
dp = p1 - p0
da = atan2(dp.y, dp.x) - a0
if da > pi:
da -= 2 * pi
if da < -pi:
da += 2 * pi
if i >= len(self.parts):
print("Too many pts for parts")
break
p = self.parts[i]
p.length = dp.to_2d().length
p.dz = dp.z
p.a0 = da
a0 += da
p0 = p1
self.closed = closed
def reverse(self, context, o):
g = self.get_generator()
self.auto_update = False
pts = [seg.p0.to_3d() for seg in g.segs]
if not self.closed:
g.segs.pop()
g_segs = list(reversed(g.segs))
last_seg = None
for i, seg in enumerate(g_segs):
s = seg.oposite
if "Curved" in type(seg).__name__:
self.parts[i].type = "C_WALL"
self.parts[i].radius = s.r
self.parts[i].da = s.da
else:
self.parts[i].type = "S_WALL"
self.parts[i].length = s.length
self.parts[i].a0 = s.delta_angle(last_seg)
last_seg = s
if self.closed:
pts.append(pts[0])
pts = list(reversed(pts))
# location wont change for closed walls
if not self.closed:
dp = pts[0] - pts[-1]
# pre-translate as dp is in local coordsys
o.matrix_world = o.matrix_world @ Matrix([
[1, 0, 0, dp.x],
[0, 1, 0, dp.y],
[0, 0, 1, 0],
[0, 0, 0, 1],
])
# self.from_points(pts, self.closed)
g = self.get_generator()
self.setup_childs(o, g)
self.auto_update = True
# flip does trigger relocate and keep childs orientation
self.flip = not self.flip
def update(self, context, manipulable_refresh=False, update_childs=False):
o = self.find_in_selection(context, self.auto_update)
if o is None:
return
if manipulable_refresh:
# prevent crash by removing all manipulators refs to datablock before changes
self.manipulable_disable(context)
verts = []
faces = []
g = self.update_parts(o, update_childs)
# print("make_wall")
g.make_wall(self.step_angle, self.flip, self.closed, verts, faces)
if self.closed:
f = len(verts)
if self.flip:
faces.append((0, f - 2, f - 1, 1))
else:
faces.append((f - 2, 0, 1, f - 1))
# print("buildmesh")
bmed.buildmesh(context, o, verts, faces, matids=None, uvs=None, weld=True)
side = 1
if self.flip:
side = -1
# Width
offset = side * (0.5 * self.x_offset) * self.width
self.manipulators[0].set_pts([
g.segs[0].sized_normal(0, offset + 0.5 * side * self.width).v.to_3d(),
g.segs[0].sized_normal(0, offset - 0.5 * side * self.width).v.to_3d(),
(-side, 0, 0)
])
# Parts COUNTER
self.manipulators[1].set_pts([g.segs[-2].lerp(1.1).to_3d(),
g.segs[-2].lerp(1.1 + 0.5 / g.segs[-2].length).to_3d(),
(-side, 0, 0)
])
# Height
self.manipulators[2].set_pts([
(0, 0, 0),
(0, 0, self.z),
(-1, 0, 0)
], normal=g.segs[0].straight(side, 0).v.to_3d())
if self.t_part != "":
t = 0.3 / g.segs[0].length
self.manipulators[3].set_pts([
g.segs[0].sized_normal(t, 0.1).p1.to_3d(),
g.segs[0].sized_normal(t, -0.1).p1.to_3d(),
(1, 0, 0)
])
if self.realtime:
# update child location and size
self.relocate_childs(context, o, g)
# store gl points
self.update_childs(context, o, g)
else:
bpy.ops.archipack.wall2_throttle_update(name=o.name)
modif = o.modifiers.get('Wall')
if modif is None:
modif = o.modifiers.new('Wall', 'SOLIDIFY')
modif.use_quality_normals = True
modif.use_even_offset = True
modif.material_offset_rim = 2
modif.material_offset = 1
modif.thickness = self.width
modif.offset = self.x_offset
if manipulable_refresh:
# print("manipulable_refresh=True")
self.manipulable_refresh = True
self.restore_context(context)
# manipulable children objects like windows and doors
def child_partition(self, array, begin, end):
pivot = begin
for i in range(begin + 1, end + 1):
# wall idx
if array[i][1] < array[begin][1]:
pivot += 1
array[i], array[pivot] = array[pivot], array[i]
# param t on the wall
elif array[i][1] == array[begin][1] and array[i][4] <= array[begin][4]:
pivot += 1
array[i], array[pivot] = array[pivot], array[i]
array[pivot], array[begin] = array[begin], array[pivot]
return pivot
def sort_child(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.child_partition(array, begin, end)
_quicksort(array, begin, pivot - 1)
_quicksort(array, pivot + 1, end)
return _quicksort(array, begin, end)
def add_child(self, name, wall_idx, pos, flip):
# print("add_child %s %s" % (name, wall_idx))
c = self.childs.add()
c.child_name = name
c.wall_idx = wall_idx
c.pos = pos
c.flip = flip
m = c.manipulators.add()
m.type_key = 'DELTA_LOC'
m.prop1_name = "x"
m = c.manipulators.add()
m.type_key = 'SNAP_SIZE_LOC'
m.prop1_name = "x"
m.prop2_name = "x"
def setup_childs(self, o, g):
"""
Store childs
create manipulators
call after a boolean oop
"""
# tim = time.time()
self.childs.clear()
self.childs_manipulators.clear()
if o.parent is None:
return
wall_with_childs = [0 for i in range(self.n_parts + 1)]
relocate = []
dmax = 2 * self.width
wtM = o.matrix_world
wrM = Matrix([
wtM[0].to_2d(),
wtM[1].to_2d()
])
witM = wtM.inverted()
for child in o.parent.children:
# filter allowed childs
cd = child.data
wd = archipack_wall2.datablock(child)
if (child != o and cd is not None and (
'archipack_window' in cd or
'archipack_door' in cd or (
wd is not None and
o.name in wd.t_part
)
)):
# setup on T linked walls
if wd is not None:
wg = wd.get_generator()
wd.setup_childs(child, wg)
ctM = child.matrix_world
crM = Matrix([
ctM[0].to_2d(),
ctM[1].to_2d()
])
# pt in w coordsys
pos = ctM.translation
pt = (witM @ pos).to_2d()
for wall_idx, wall in enumerate(g.segs):
# may be optimized with a bound check
res, dist, t = wall.point_sur_segment(pt)
# outside is on the right side of the wall
# p1
# |-- x
# p0
if res and t > 0 and t < 1 and abs(dist) < dmax:
# dir in world coordsys
dir = wrM @ wall.sized_normal(t, 1).v
wall_with_childs[wall_idx] = 1
m = self.childs_manipulators.add()
m.type_key = 'DUMB_SIZE'
# always make window points outside
if "archipack_window" in cd:
flip = self.flip
else:
dir_y = crM @ Vector((0, -1))
# let door orient where user want
flip = (dir_y - dir).length > 0.5
# store z in wall space
relocate.append((
child.name,
wall_idx,
(t * wall.length, dist, (witM @ pos).z),
flip,
t))
break
self.sort_child(relocate)
for child in relocate:
name, wall_idx, pos, flip, t = child
self.add_child(name, wall_idx, pos, flip)
# add a dumb size from last child to end of wall segment
for i in range(sum(wall_with_childs)):
m = self.childs_manipulators.add()
m.type_key = 'DUMB_SIZE'
# print("setup_childs:%1.4f" % (time.time()-tim))
def relocate_childs(self, context, o, g):
"""
Move and resize childs after wall edition
"""
# print("relocate_childs")
# tim = time.time()
w = -self.x_offset * self.width
if self.flip:
w = -w
tM = o.matrix_world
for child in self.childs:
c, d = child.get_child(context)
if c is None:
continue
t = child.pos.x / g.segs[child.wall_idx].length
n = g.segs[child.wall_idx].sized_normal(t, 1)
rx, ry = -n.v
rx, ry = ry, -rx
if child.flip:
rx, ry = -rx, -ry
if d is not None:
# print("change flip:%s width:%s" % (d.flip != child.flip, d.y != self.width))
if d.y != self.width or d.flip != child.flip:
c.select_set(state=True)
d.auto_update = False
d.flip = child.flip
d.y = self.width
d.auto_update = True
c.select_set(state=False)
x, y = n.p - (0.5 * w * n.v)
else:
x, y = n.p - (child.pos.y * n.v)
context.view_layer.objects.active = o
# preTranslate
c.matrix_world = tM @ Matrix([
[rx, -ry, 0, x],
[ry, rx, 0, y],
[0, 0, 1, child.pos.z],
[0, 0, 0, 1]
])
# Update T linked wall's childs
if archipack_wall2.filter(c):
d = archipack_wall2.datablock(c)
cg = d.get_generator()
d.relocate_childs(context, c, cg)
# print("relocate_childs:%1.4f" % (time.time()-tim))
def update_childs(self, context, o, g):
"""
setup gl points for childs
"""
# print("update_childs")
if o.parent is None:
return
# swap manipulators so they always face outside
manip_side = 1
if self.flip:
manip_side = -1
itM = o.matrix_world.inverted()
m_idx = 0
for wall_idx, wall in enumerate(g.segs):
p0 = wall.lerp(0)
wall_has_childs = False
for child in self.childs:
if child.wall_idx == wall_idx:
c, d = child.get_child(context)
if d is not None:
# child is either a window or a door
wall_has_childs = True
dt = 0.5 * d.x / wall.length
pt = (itM @ c.matrix_world.translation).to_2d()
res, y, t = wall.point_sur_segment(pt)
child.pos = (wall.length * t, y, child.pos.z)
p1 = wall.lerp(t - dt)
# dumb size between childs
self.childs_manipulators[m_idx].set_pts([
(p0.x, p0.y, 0),
(p1.x, p1.y, 0),
(manip_side * 0.5, 0, 0)])
m_idx += 1
x, y = 0.5 * d.x, -self.x_offset * 0.5 * d.y
if child.flip:
side = -manip_side
else:
side = manip_side
# delta loc
child.manipulators[0].set_pts([(-x, side * -y, 0), (x, side * -y, 0), (side, 0, 0)])
# loc size
child.manipulators[1].set_pts([
(-x, side * -y, 0),
(x, side * -y, 0),
(0.5 * side, 0, 0)])
p0 = wall.lerp(t + dt)
p1 = wall.lerp(1)
if wall_has_childs:
# dub size after all childs
self.childs_manipulators[m_idx].set_pts([
(p0.x, p0.y, 0),
(p1.x, p1.y, 0),
(manip_side * 0.5, 0, 0)])
m_idx += 1
def manipulate_childs(self, context):
"""
setup child manipulators
"""
# print("manipulate_childs")
n_parts = self.n_parts
if self.closed:
n_parts += 1
for wall_idx in range(n_parts):
for child in self.childs:
if child.wall_idx == wall_idx:
c, d = child.get_child(context)
if d is not None:
# delta loc
self.manip_stack.append(child.manipulators[0].setup(context, c, d, self.manipulate_callback))
# loc size
self.manip_stack.append(child.manipulators[1].setup(context, c, d, self.manipulate_callback))
def manipulate_callback(self, context, o=None, manipulator=None):
found = False
if o.parent is not None:
for c in o.parent.children:
if (archipack_wall2.datablock(c) == self):
context.view_layer.objects.active = c
found = True
break
if found:
self.manipulable_manipulate(context, manipulator=manipulator)
def manipulable_manipulate(self, context, event=None, manipulator=None):
type_name = type(manipulator).__name__
# print("manipulable_manipulate %s" % (type_name))
if type_name in [
'DeltaLocationManipulator',
'SizeLocationManipulator',
'SnapSizeLocationManipulator'
]:
# update manipulators pos of childs
o = context.active_object
if o.parent is None:
return
g = self.get_generator()
itM = o.matrix_world.inverted() @ o.parent.matrix_world
for child in self.childs:
c, d = child.get_child(context)
if d is not None:
wall = g.segs[child.wall_idx]
pt = (itM @ c.location).to_2d()
res, d, t = wall.point_sur_segment(pt)
child.pos = (t * wall.length, d, child.pos.z)
# update childs manipulators
self.update_childs(context, o, g)
def manipulable_move_t_part(self, context, o=None, manipulator=None):
"""
Callback for t_parts childs
"""
type_name = type(manipulator).__name__
# print("manipulable_manipulate %s" % (type_name))
if type_name in [
'DeltaLocationManipulator'
]:
# update manipulators pos of childs
if archipack_wall2.datablock(o) != self:
return
g = self.get_generator()
# update childs
self.relocate_childs(context, o, g)
def manipulable_release(self, context):
"""
Override with action to do on mouse release
eg: big update
"""
return
def manipulable_setup(self, context):
# print("manipulable_setup")
self.manipulable_disable(context)
o = context.active_object
# setup childs manipulators
self.manipulate_childs(context)
n_parts = self.n_parts
if self.closed:
n_parts += 1
# update manipulators on version change
self.setup_manipulators()
for i, part in enumerate(self.parts):
if i < n_parts:
if i > 0:
# start angle
self.manip_stack.append(part.manipulators[0].setup(context, o, part))
# length / radius + angle
self.manip_stack.append(part.manipulators[1].setup(context, o, part))
# segment index
self.manip_stack.append(part.manipulators[3].setup(context, o, self))
# snap point
self.manip_stack.append(part.manipulators[2].setup(context, o, self))
# height as per segment will be here when done
# width + counter
for m in self.manipulators:
self.manip_stack.append(m.setup(context, o, self, self.manipulable_move_t_part))
# dumb between childs
for m in self.childs_manipulators:
self.manip_stack.append(m.setup(context, o, self))
def manipulable_exit(self, context):
"""
Override with action to do when modal exit
"""
return
def manipulable_invoke(self, context):
"""
call this in operator invoke()
"""
# print("manipulable_invoke")
if self.manipulate_mode:
self.manipulable_disable(context)
return False
# self.manip_stack = []
o = context.active_object
g = self.get_generator()
# setup childs manipulators
self.setup_childs(o, g)
# store gl points
self.update_childs(context, o, g)
# don't do anything ..
# self.manipulable_release(context)
# self.manipulate_mode = True
self.manipulable_setup(context)
self.manipulate_mode = True
self._manipulable_invoke(context)
return True
def find_roof(self, context, o, g):
tM = o.matrix_world
up = Vector((0, 0, 1))
for seg in g.segs:
p = tM @ seg.p0.to_3d()
p.z = 0.01
# prevent self intersect
o.hide_viewport = True
res, pos, normal, face_index, r, matrix_world = context.scene.ray_cast(
depsgraph=context.view_layer.depsgraph,
origin=p,
direction=up)
o.hide_viewport = False
# print("res:%s" % res)
if res and r.data is not None and "archipack_roof" in r.data:
return r, r.data.archipack_roof[0]
return None, None
# Update throttle (hack)
# use 2 globals to store a timer and state of update_action
# Use to update floor boolean on edit
update_timer = None
update_timer_updating = False
throttle_delay = 0.5
throttle_start = 0
class ARCHIPACK_OT_wall2_throttle_update(Operator):
bl_idname = "archipack.wall2_throttle_update"
bl_label = "Update childs with a delay"
name : StringProperty()
def modal(self, context, event):
global update_timer_updating
if event.type == 'TIMER' and not update_timer_updating:
# can't rely on TIMER event as another timer may run
if time.time() - throttle_start > throttle_delay:
update_timer_updating = True
o = context.scene.objects.get(self.name.strip())
if o is not None:
m = o.modifiers.get("AutoBoolean")
if m is not None:
o.hide_viewport = False
# o.display_type = 'TEXTURED'
# m.show_viewport = True
return self.cancel(context)
return {'PASS_THROUGH'}
def execute(self, context):
global update_timer
global update_timer_updating
global throttle_delay
global throttle_start
if update_timer is not None:
context.window_manager.event_timer_remove(update_timer)
if update_timer_updating:
return {'CANCELLED'}
# reset update_timer so it only occurs once 0.1s after last action
throttle_start = time.time()
update_timer = context.window_manager.event_timer_add(throttle_delay, context.window)
return {'CANCELLED'}
throttle_start = time.time()
update_timer_updating = False
context.window_manager.modal_handler_add(self)
update_timer = context.window_manager.event_timer_add(throttle_delay, context.window)
return {'RUNNING_MODAL'}
def cancel(self, context):
global update_timer
context.window_manager.event_timer_remove(update_timer)
update_timer = None
return {'CANCELLED'}
class ARCHIPACK_PT_wall2(Panel):
bl_idname = "ARCHIPACK_PT_wall2"
bl_label = "Wall"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Archipack'
def draw(self, context):
prop = archipack_wall2.datablock(context.object)
if prop is None:
return
layout = self.layout
row = layout.row(align=True)
row.operator("archipack.wall2_manipulate", icon='VIEW_PAN')
# row = layout.row(align=True)
# row.prop(prop, 'realtime')
box = layout.box()
box.prop(prop, 'n_parts')
box.prop(prop, 'step_angle')
box.prop(prop, 'width')
box.prop(prop, 'z')
box.prop(prop, 'flip')
box.prop(prop, 'x_offset')
row = layout.row()
row.prop(prop, "closed")
row = layout.row()
row.prop_search(prop, "t_part", context.scene, "objects", text="T link", icon='OBJECT_DATAMODE')
layout.operator("archipack.wall2_reverse", icon='FILE_REFRESH')
row = layout.row(align=True)
row.operator("archipack.wall2_fit_roof")
# row.operator("archipack.wall2_fit_roof", text="Inside").inside = True
n_parts = prop.n_parts
if prop.closed:
n_parts += 1
for i, part in enumerate(prop.parts):
if i < n_parts:
box = layout.box()
part.draw(box, context, i)
@classmethod
def poll(cls, context):
return archipack_wall2.filter(context.active_object)
# ------------------------------------------------------------------
# Define operator class to create object
# ------------------------------------------------------------------
class ARCHIPACK_OT_wall2(ArchipackCreateTool, Operator):
bl_idname = "archipack.wall2"
bl_label = "Wall"
bl_description = "Create a Wall"
bl_category = 'Archipack'
bl_options = {'REGISTER', 'UNDO'}
def create(self, context):
m = bpy.data.meshes.new("Wall")
o = bpy.data.objects.new("Wall", m)
d = m.archipack_wall2.add()
d.manipulable_selectable = True
self.link_object_to_scene(context, o)
o.select_set(state=True)
# around 12 degree
m.auto_smooth_angle = 0.20944
context.view_layer.objects.active = o
self.load_preset(d)
self.add_material(o)
return o
def execute(self, context):
if context.mode == "OBJECT":
bpy.ops.object.select_all(action="DESELECT")
o = self.create(context)
o.location = bpy.context.scene.cursor.location
o.select_set(state=True)
context.view_layer.objects.active = o
self.manipulate()
return {'FINISHED'}
else:
self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
return {'CANCELLED'}
class ARCHIPACK_OT_wall2_from_curve(Operator):
bl_idname = "archipack.wall2_from_curve"
bl_label = "Wall curve"
bl_description = "Create a wall 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 create(self, context):
curve = context.active_object
for spline in curve.data.splines:
bpy.ops.archipack.wall2(auto_manipulate=self.auto_manipulate)
o = context.view_layer.objects.active
d = archipack_wall2.datablock(o)
d.from_spline(curve.matrix_world, 12, spline)
if spline.type == 'POLY':
pt = spline.points[0].co
elif spline.type == 'BEZIER':
pt = spline.bezier_points[0].co
else:
pt = Vector((0, 0, 0))
# pretranslate
o.matrix_world = curve.matrix_world @ Matrix([
[1, 0, 0, pt.x],
[0, 1, 0, pt.y],
[0, 0, 1, pt.z],
[0, 0, 0, 1]
])
return o
# -----------------------------------------------------
# Execute
# -----------------------------------------------------
def execute(self, context):
if context.mode == "OBJECT":
bpy.ops.object.select_all(action="DESELECT")
o = self.create(context)
if o is not None:
o.select_set(state=True)
context.view_layer.objects.active = o
return {'FINISHED'}
else:
self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
return {'CANCELLED'}
class ARCHIPACK_OT_wall2_from_slab(Operator):
bl_idname = "archipack.wall2_from_slab"
bl_label = "->Wall"
bl_description = "Create a wall from a slab"
bl_category = 'Archipack'
bl_options = {'REGISTER', 'UNDO'}
auto_manipulate : BoolProperty(default=True)
@classmethod
def poll(self, context):
o = context.active_object
return o is not None and o.data is not None and 'archipack_slab' in o.data
def create(self, context):
slab = context.active_object
wd = slab.data.archipack_slab[0]
bpy.ops.archipack.wall2(auto_manipulate=self.auto_manipulate)
o = context.view_layer.objects.active
d = archipack_wall2.datablock(o)
d.auto_update = False
d.parts.clear()
d.n_parts = wd.n_parts - 1
d.closed = True
for part in wd.parts:
p = d.parts.add()
if "S_" in part.type:
p.type = "S_WALL"
else:
p.type = "C_WALL"
p.length = part.length
p.radius = part.radius
p.da = part.da
p.a0 = part.a0
o.select_set(state=True)
context.view_layer.objects.active = o
d.auto_update = True
# pretranslate
o.matrix_world = slab.matrix_world.copy()
bpy.ops.object.select_all(action='DESELECT')
# parenting childs to wall reference point
if o.parent is None:
x, y, z = o.bound_box[0]
context.scene.cursor.location = o.matrix_world @ Vector((x, y, z))
# fix issue #9
context.view_layer.objects.active = o
bpy.ops.archipack.reference_point()
else:
o.parent.select_set(state=True)
context.view_layer.objects.active = o.parent
o.select_set(state=True)
slab.select_set(state=True)
bpy.ops.archipack.parent_to_reference()
o.parent.select_set(state=False)
return o
# -----------------------------------------------------
# Execute
# -----------------------------------------------------
def execute(self, context):
if context.mode == "OBJECT":
bpy.ops.object.select_all(action="DESELECT")
o = self.create(context)
o.select_set(state=True)
context.view_layer.objects.active = o
return {'FINISHED'}
else:
self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
return {'CANCELLED'}
class ARCHIPACK_OT_wall2_fit_roof(Operator):
bl_idname = "archipack.wall2_fit_roof"
bl_label = "Fit roof"
bl_description = "Fit roof"
bl_category = 'Archipack'
bl_options = {'REGISTER', 'UNDO'}
inside : BoolProperty(default=False)
@classmethod
def poll(self, context):
return archipack_wall2.filter(context.active_object)
def execute(self, context):
o = context.active_object
d = archipack_wall2.datablock(o)
g = d.get_generator()
r, rd = d.find_roof(context, o, g)
if rd is not None:
d.setup_childs(o, g)
rd.make_wall_fit(context, r, o, self.inside)
return {'FINISHED'}
# ------------------------------------------------------------------
# Define operator class to draw a wall
# ------------------------------------------------------------------
class ARCHIPACK_OT_wall2_draw(ArchipackDrawTool, Operator):
bl_idname = "archipack.wall2_draw"
bl_label = "Draw a Wall"
bl_description = "Create a wall by drawing its baseline in 3D view"
bl_category = 'Archipack'
o = None
state = 'RUNNING'
flag_create = False
flag_next = False
wall_part1 = None
wall_line1 = None
line = None
label = None
feedback = None
takeloc = Vector((0, 0, 0))
sel = []
act = None
# constraint to other wall and make a T child
parent = None
takemat = None
@classmethod
def poll(cls, context):
return True
def draw_callback(self, _self, context):
self.feedback.draw(context)
def sp_draw(self, sp, context):
z = 2.7
if self.state == 'CREATE':
p0 = self.takeloc
else:
p0 = sp.takeloc
p1 = sp.placeloc
delta = p1 - p0
# print("sp_draw state:%s delta:%s p0:%s p1:%s" % (self.state, delta.length, p0, p1))
if delta.length == 0:
return
self.wall_part1.set_pos([p0, p1, Vector((p1.x, p1.y, p1.z + z)), Vector((p0.x, p0.y, p0.z + z))])
self.wall_line1.set_pos([p0, p1, Vector((p1.x, p1.y, p1.z + z)), Vector((p0.x, p0.y, p0.z + z))])
self.wall_part1.draw(context)
self.wall_line1.draw(context)
self.line.p = p0
self.line.v = delta
self.label.set_pos(context, self.line.length, self.line.lerp(0.5), self.line.v, normal=Vector((0, 0, 1)))
self.label.draw(context)
self.line.draw(context)
def sp_callback(self, context, event, state, sp):
logger.debug("ARCHIPACK_OT_wall2_draw.sp_callback event %s %s state:%s", event.type, event.value, state)
if state == 'SUCCESS':
if self.state == 'CREATE':
takeloc = self.takeloc
delta = sp.placeloc - self.takeloc
else:
takeloc = sp.takeloc
delta = sp.delta
old = context.object
if self.o is None:
bpy.ops.archipack.wall2(auto_manipulate=False)
o = context.object
o.location = takeloc
self.o = o
d = archipack_wall2.datablock(o)
part = d.parts[0]
part.length = delta.length
else:
o = self.o
# select and make active
o.select_set(state=True)
context.view_layer.objects.active = o
d = archipack_wall2.datablock(o)
# Check for end close to start and close when applicable
dp = sp.placeloc - o.location
if dp.length < 0.01:
d.closed = True
self.state = 'CANCEL'
return
part = d.add_part(context, delta.length)
# print("self.o :%s" % o.name)
rM = o.matrix_world.inverted().to_3x3()
g = d.get_generator()
w = g.segs[-2]
dp = rM @ delta
da = atan2(dp.y, dp.x) - w.straight(1).angle
a0 = part.a0 + da
if a0 > pi:
a0 -= 2 * pi
if a0 < -pi:
a0 += 2 * pi
part.a0 = a0
d.update(context)
old.select_set(state=True)
context.view_layer.objects.active = old
self.flag_next = True
context.area.tag_redraw()
# print("feedback.on:%s" % self.feedback.on)
self.state = state
def sp_init(self, context, event, state, sp):
# print("sp_init event %s %s %s" % (event.type, event.value, state))
if state == 'SUCCESS':
# point placed, check if a wall was under mouse
res, tM, wall, width, y, z_offset = self.mouse_hover_wall(context, event)
if res:
d = archipack_wall2.datablock(wall)
if event.ctrl:
# user snap, use direction as constraint
tM.translation = sp.placeloc.copy()
else:
# without snap, use wall's bottom
tM.translation -= y.normalized() * (0.5 * d.width)
self.takeloc = tM.translation
self.parent = wall.name
self.takemat = tM
else:
self.takeloc = sp.placeloc.copy()
self.state = 'RUNNING'
# print("feedback.on:%s" % self.feedback.on)
elif state == 'CANCEL':
self.state = state
return
def ensure_ccw(self):
"""
Wall to slab expect wall vertex order to be ccw
so reverse order here when needed
"""
d = archipack_wall2.datablock(self.o)
g = d.get_generator(axis=False)
pts = [seg.p0 for seg in g.segs]
if d.closed:
pts.append(pts[0])
if d.is_cw(pts):
d.x_offset = 1
pts = list(reversed(pts))
self.o.location += pts[0] - pts[-1]
d.from_points(pts, d.closed)
def modal(self, context, event):
context.area.tag_redraw()
if event.type in {'NONE', 'TIMER', 'TIMER_REPORT', 'EVT_TWEAK_L', 'WINDOW_DEACTIVATE'}:
return {'PASS_THROUGH'}
if self.keymap.check(event, self.keymap.delete):
self.feedback.disable()
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
self.o = None
return {'FINISHED', 'PASS_THROUGH'}
if self.state == 'STARTING' and event.type not in {'ESC', 'RIGHTMOUSE'}:
# wait for takeloc being visible when button is over horizon
takeloc = self.mouse_to_plane(context, event)
if takeloc is not None:
logger.debug("ARCHIPACK_OT_wall2_draw.modal(STARTING) location:%s", takeloc)
snap_point(takeloc=takeloc,
callback=self.sp_init,
constraint_axis=(True, True, False),
release_confirm=True)
return {'RUNNING_MODAL'}
elif self.state == 'RUNNING':
# print("RUNNING")
logger.debug("ARCHIPACK_OT_wall2_draw.modal(RUNNING) location:%s", self.takeloc)
self.state = 'CREATE'
snap_point(takeloc=self.takeloc,
draw=self.sp_draw,
takemat=self.takemat,
transform_orientation=context.scene.transform_orientation_slots[0].type,
callback=self.sp_callback,
constraint_axis=(True, True, False),
release_confirm=self.max_style_draw_tool)
return {'RUNNING_MODAL'}
elif self.state != 'CANCEL' and event.type in {'C', 'c'}:
logger.debug("ARCHIPACK_OT_wall2_draw.modal(%s) C pressed", self.state)
self.feedback.disable()
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
o = self.o
# select and make active
o.select_set(state=True)
context.view_layer.objects.active = o
d = archipack_wall2.datablock(o)
d.closed = True
if bpy.ops.archipack.manipulate.poll():
bpy.ops.archipack.manipulate('INVOKE_DEFAULT')
return {'FINISHED'}
elif self.state != 'CANCEL' and event.type in {'LEFTMOUSE', 'RET', 'NUMPAD_ENTER', 'SPACE'}:
# print('LEFTMOUSE %s' % (event.value))
self.feedback.instructions(context, "Draw a wall", "Click & Drag to add a segment", [
('ENTER', 'Add part'),
('BACK_SPACE', 'Remove part'),
('CTRL', 'Snap'),
('C', 'Close wall and exit'),
('MMBTN', 'Constraint to axis'),
('X Y', 'Constraint to axis'),
('RIGHTCLICK or ESC', 'exit')
])
# press with max mode release with blender mode
if self.max_style_draw_tool:
evt_value = 'PRESS'
else:
evt_value = 'RELEASE'
if event.value == evt_value:
if self.flag_next:
self.flag_next = False
o = self.o
# select and make active
o.select_set(state=True)
context.view_layer.objects.active = o
d = archipack_wall2.datablock(o)
g = d.get_generator()
p0 = g.segs[-2].p0
p1 = g.segs[-2].p1
dp = p1 - p0
takemat = o.matrix_world @ Matrix([
[dp.x, dp.y, 0, p1.x],
[dp.y, -dp.x, 0, p1.y],
[0, 0, 1, 0],
[0, 0, 0, 1]
])
takeloc = o.matrix_world @ p1.to_3d()
o.select_set(state=False)
else:
takemat = None
takeloc = self.mouse_to_plane(context, event)
if takeloc is not None:
logger.debug("ARCHIPACK_OT_wall2_draw.modal(CREATE) location:%s", takeloc)
snap_point(takeloc=takeloc,
takemat=takemat,
draw=self.sp_draw,
callback=self.sp_callback,
constraint_axis=(True, True, False),
release_confirm=self.max_style_draw_tool)
return {'RUNNING_MODAL'}
if self.keymap.check(event, self.keymap.undo) or (
event.type in {'BACK_SPACE'} and event.value == 'RELEASE'
):
if self.o is not None:
o = self.o
# select and make active
o.select_set(state=True)
context.view_layer.objects.active = o
d = archipack_wall2.datablock(o)
if d.n_parts > 1:
d.n_parts -= 1
return {'RUNNING_MODAL'}
if self.state == 'CANCEL' or (event.type in {'ESC', 'RIGHTMOUSE'} and
event.value == 'RELEASE'):
self.feedback.disable()
bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
logger.debug("ARCHIPACK_OT_wall2_draw.modal(CANCEL) %s", event.type)
if self.o is None:
for o in self.sel:
o.select_set(state=True)
# select and make active
if self.act is not None:
self.act.select_set(state=True)
context.view_layer.objects.active = self.act
else:
o = self.o
o.select_set(state=True)
context.view_layer.objects.active = o
# remove last segment with blender mode
d = archipack_wall2.datablock(o)
if not self.max_style_draw_tool:
if not d.closed and d.n_parts > 1:
d.n_parts -= 1
o.select_set(state=True)
context.view_layer.objects.active = o
# make T child
if self.parent is not None:
d.t_part = self.parent
if bpy.ops.archipack.manipulate.poll():
bpy.ops.archipack.manipulate('INVOKE_DEFAULT', object_name=o.name)
return {'FINISHED'}
return {'PASS_THROUGH'}
def invoke(self, context, event):
if context.mode == "OBJECT":
prefs = context.preferences.addons[__name__.split('.')[0]].preferences
self.max_style_draw_tool = prefs.max_style_draw_tool
self.keymap = Keymaps(context)
self.wall_part1 = GlPolygon((0.5, 0, 0, 0.2))
self.wall_line1 = GlPolyline((0.5, 0, 0, 0.8))
self.line = GlLine()
self.label = GlText()
self.feedback = FeedbackPanel()
self.feedback.instructions(context, "Draw a wall", "Click & Drag to start", [
('CTRL', 'Snap'),
('MMBTN', 'Constraint to axis'),
('X Y', 'Constraint to axis'),
('SHIFT+CTRL+TAB', 'Switch snap mode'),
('RIGHTCLICK or ESC', 'exit without change')
])
self.feedback.enable()
args = (self, context)
self.sel = context.selected_objects[:]
self.act = context.active_object
bpy.ops.object.select_all(action="DESELECT")
self.state = 'STARTING'
self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback, args, 'WINDOW', 'POST_PIXEL')
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
else:
self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
return {'CANCELLED'}
# ------------------------------------------------------------------
# Define operator class to manage parts
# ------------------------------------------------------------------
class ARCHIPACK_OT_wall2_insert(Operator):
bl_idname = "archipack.wall2_insert"
bl_label = "Insert"
bl_description = "Insert part"
bl_category = 'Archipack'
bl_options = {'REGISTER', 'UNDO'}
index : IntProperty(default=0)
def execute(self, context):
if context.mode == "OBJECT":
o = context.active_object
d = archipack_wall2.datablock(o)
if d is None:
return {'CANCELLED'}
d.insert_part(context, o, self.index)
return {'FINISHED'}
else:
self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
return {'CANCELLED'}
class ARCHIPACK_OT_wall2_remove(Operator):
bl_idname = "archipack.wall2_remove"
bl_label = "Remove"
bl_description = "Remove part"
bl_category = 'Archipack'
bl_options = {'REGISTER', 'UNDO'}
index : IntProperty(default=0)
def execute(self, context):
if context.mode == "OBJECT":
o = context.active_object
d = archipack_wall2.datablock(o)
if d is None:
return {'CANCELLED'}
d.remove_part(context, o, self.index)
return {'FINISHED'}
else:
self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
return {'CANCELLED'}
class ARCHIPACK_OT_wall2_reverse(Operator):
bl_idname = "archipack.wall2_reverse"
bl_label = "Reverse"
bl_description = "Reverse parts order"
bl_category = 'Archipack'
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
if context.mode == "OBJECT":
o = context.active_object
d = archipack_wall2.datablock(o)
if d is None:
return {'CANCELLED'}
d.reverse(context, o)
return {'FINISHED'}
else:
self.report({'WARNING'}, "Archipack: Option only valid in Object mode")
return {'CANCELLED'}
# ------------------------------------------------------------------
# Define operator class to manipulate object
# ------------------------------------------------------------------
class ARCHIPACK_OT_wall2_manipulate(Operator):
bl_idname = "archipack.wall2_manipulate"
bl_label = "Manipulate"
bl_description = "Manipulate"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(self, context):
return archipack_wall2.filter(context.active_object)
def invoke(self, context, event):
d = archipack_wall2.datablock(context.active_object)
d.manipulable_invoke(context)
return {'FINISHED'}
def execute(self, context):
"""
For use in boolean ops
"""
if archipack_wall2.filter(context.active_object):
o = context.active_object
d = archipack_wall2.datablock(o)
g = d.get_generator()
d.setup_childs(o, g)
d.update_childs(context, o, g)
d.update(context)
o.select_set(state=True)
context.view_layer.objects.active = o
return {'FINISHED'}
def register():
bpy.utils.register_class(archipack_wall2_part)
bpy.utils.register_class(archipack_wall2_child)
bpy.utils.register_class(archipack_wall2)
Mesh.archipack_wall2 = CollectionProperty(type=archipack_wall2)
bpy.utils.register_class(ARCHIPACK_PT_wall2)
bpy.utils.register_class(ARCHIPACK_OT_wall2)
bpy.utils.register_class(ARCHIPACK_OT_wall2_draw)
bpy.utils.register_class(ARCHIPACK_OT_wall2_insert)
bpy.utils.register_class(ARCHIPACK_OT_wall2_remove)
bpy.utils.register_class(ARCHIPACK_OT_wall2_reverse)
bpy.utils.register_class(ARCHIPACK_OT_wall2_manipulate)
bpy.utils.register_class(ARCHIPACK_OT_wall2_from_curve)
bpy.utils.register_class(ARCHIPACK_OT_wall2_from_slab)
bpy.utils.register_class(ARCHIPACK_OT_wall2_throttle_update)
bpy.utils.register_class(ARCHIPACK_OT_wall2_fit_roof)
def unregister():
bpy.utils.unregister_class(archipack_wall2_part)
bpy.utils.unregister_class(archipack_wall2_child)
bpy.utils.unregister_class(archipack_wall2)
del Mesh.archipack_wall2
bpy.utils.unregister_class(ARCHIPACK_PT_wall2)
bpy.utils.unregister_class(ARCHIPACK_OT_wall2)
bpy.utils.unregister_class(ARCHIPACK_OT_wall2_draw)
bpy.utils.unregister_class(ARCHIPACK_OT_wall2_insert)
bpy.utils.unregister_class(ARCHIPACK_OT_wall2_remove)
bpy.utils.unregister_class(ARCHIPACK_OT_wall2_reverse)
bpy.utils.unregister_class(ARCHIPACK_OT_wall2_manipulate)
bpy.utils.unregister_class(ARCHIPACK_OT_wall2_from_curve)
bpy.utils.unregister_class(ARCHIPACK_OT_wall2_from_slab)
bpy.utils.unregister_class(ARCHIPACK_OT_wall2_throttle_update)
bpy.utils.unregister_class(ARCHIPACK_OT_wall2_fit_roof)