Brecht Van Lommel
ed161459d1
Patch contributed by luzpaz. Differential Revision: https://developer.blender.org/D5800
930 lines
28 KiB
Python
930 lines
28 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)
|
|
# Cutter / CutAble shared by roof, slab, and floor
|
|
# ----------------------------------------------------------
|
|
from mathutils import Vector, Matrix
|
|
from mathutils.geometry import interpolate_bezier
|
|
from math import cos, sin, pi, atan2
|
|
import bmesh
|
|
from random import uniform
|
|
from bpy.props import (
|
|
FloatProperty, IntProperty, BoolProperty,
|
|
StringProperty, EnumProperty
|
|
)
|
|
from .archipack_2d import Line
|
|
|
|
|
|
class CutterSegment(Line):
|
|
|
|
def __init__(self, p, v, type='DEFAULT'):
|
|
Line.__init__(self, p, v)
|
|
self.type = type
|
|
self.is_hole = True
|
|
|
|
@property
|
|
def copy(self):
|
|
return CutterSegment(self.p.copy(), self.v.copy(), self.type)
|
|
|
|
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):
|
|
s = self.copy
|
|
s.p += self.sized_normal(0, offset).v
|
|
return s
|
|
|
|
@property
|
|
def oposite(self):
|
|
s = self.copy
|
|
s.p += s.v
|
|
s.v = -s.v
|
|
return s
|
|
|
|
|
|
class CutterGenerator():
|
|
|
|
def __init__(self, d):
|
|
self.parts = d.parts
|
|
self.operation = d.operation
|
|
self.segs = []
|
|
|
|
def add_part(self, part):
|
|
|
|
if len(self.segs) < 1:
|
|
s = None
|
|
else:
|
|
s = self.segs[-1]
|
|
|
|
# start a new Cutter
|
|
if s is None:
|
|
v = part.length * Vector((cos(part.a0), sin(part.a0)))
|
|
s = CutterSegment(Vector((0, 0)), v, part.type)
|
|
else:
|
|
s = s.straight(part.length).rotate(part.a0)
|
|
s.type = part.type
|
|
|
|
self.segs.append(s)
|
|
|
|
def set_offset(self):
|
|
last = None
|
|
for i, seg in enumerate(self.segs):
|
|
seg.set_offset(self.parts[i].offset, last)
|
|
last = seg.line
|
|
|
|
def close(self):
|
|
# Make last segment implicit closing one
|
|
s0 = self.segs[-1]
|
|
s1 = self.segs[0]
|
|
dp = s1.p0 - s0.p0
|
|
s0.v = dp
|
|
|
|
if len(self.segs) > 1:
|
|
s0.line = s0.make_offset(self.parts[-1].offset, self.segs[-2].line)
|
|
|
|
p1 = s1.line.p1
|
|
s1.line = s1.make_offset(self.parts[0].offset, s0.line)
|
|
s1.line.p1 = p1
|
|
|
|
def locate_manipulators(self):
|
|
if self.operation == 'DIFFERENCE':
|
|
side = -1
|
|
else:
|
|
side = 1
|
|
for i, f in enumerate(self.segs):
|
|
|
|
manipulators = self.parts[i].manipulators
|
|
p0 = f.p0.to_3d()
|
|
p1 = f.p1.to_3d()
|
|
# angle from last to current segment
|
|
if i > 0:
|
|
|
|
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 = f.straight(side, 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, (side, 0, 0)])
|
|
|
|
# snap manipulator, don't change index !
|
|
manipulators[2].set_pts([p0, p1, (side, 0, 0)])
|
|
# dumb segment id
|
|
manipulators[3].set_pts([p0, p1, (side, 0, 0)])
|
|
|
|
# offset
|
|
manipulators[4].set_pts([
|
|
p0,
|
|
p0 + f.sized_normal(0, max(0.0001, self.parts[i].offset)).v.to_3d(),
|
|
(0.5, 0, 0)
|
|
])
|
|
|
|
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 get_index(self, index):
|
|
n_segs = len(self.segs)
|
|
if index >= n_segs:
|
|
index -= n_segs
|
|
return index
|
|
|
|
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 get_verts(self, verts, edges):
|
|
|
|
n_segs = len(self.segs) - 1
|
|
|
|
for s in self.segs:
|
|
verts.append(s.line.p0.to_3d())
|
|
|
|
for i in range(n_segs):
|
|
edges.append([i, i + 1])
|
|
|
|
|
|
class CutAblePolygon():
|
|
"""
|
|
Simple boolean operations
|
|
Cutable generator / polygon
|
|
Object MUST have properties
|
|
- segs
|
|
- holes
|
|
- convex
|
|
"""
|
|
def as_lines(self, step_angle=0.104):
|
|
"""
|
|
Convert curved segments to straight lines
|
|
"""
|
|
segs = []
|
|
for s in self.segs:
|
|
if "Curved" in type(s).__name__:
|
|
dt, steps = s.steps_by_angle(step_angle)
|
|
segs.extend(s.as_lines(steps))
|
|
else:
|
|
segs.append(s)
|
|
self.segs = segs
|
|
|
|
def inside(self, pt, segs=None):
|
|
"""
|
|
Point inside poly (raycast method)
|
|
support concave polygons
|
|
TODO:
|
|
make s1 angle different than all othr segs
|
|
"""
|
|
s1 = Line(pt, Vector((min(10000, 100 * self.xsize), uniform(-0.5, 0.5))))
|
|
counter = 0
|
|
if segs is None:
|
|
segs = self.segs
|
|
for s in segs:
|
|
res, p, t, u = s.intersect_ext(s1)
|
|
if res:
|
|
counter += 1
|
|
return counter % 2 == 1
|
|
|
|
def get_index(self, index):
|
|
n_segs = len(self.segs)
|
|
if index >= n_segs:
|
|
index -= n_segs
|
|
return index
|
|
|
|
def is_convex(self):
|
|
n_segs = len(self.segs)
|
|
self.convex = True
|
|
sign = False
|
|
s0 = self.segs[-1]
|
|
for i in range(n_segs):
|
|
s1 = self.segs[i]
|
|
if "Curved" in type(s1).__name__:
|
|
self.convex = False
|
|
return
|
|
c = s0.v.cross(s1.v)
|
|
if i == 0:
|
|
sign = (c > 0)
|
|
elif sign != (c > 0):
|
|
self.convex = False
|
|
return
|
|
s0 = s1
|
|
|
|
def get_intersections(self, border, cutter, s_start, segs, start_by_hole):
|
|
"""
|
|
Detect all intersections
|
|
for boundary: store intersection point, t, idx of segment, idx of cutter
|
|
sort by t
|
|
"""
|
|
s_segs = border.segs
|
|
b_segs = cutter.segs
|
|
s_nsegs = len(s_segs)
|
|
b_nsegs = len(b_segs)
|
|
inter = []
|
|
|
|
# find all intersections
|
|
for idx in range(s_nsegs):
|
|
s_idx = border.get_index(s_start + idx)
|
|
s = s_segs[s_idx]
|
|
for b_idx, b in enumerate(b_segs):
|
|
res, p, u, v = s.intersect_ext(b)
|
|
if res:
|
|
inter.append((s_idx, u, b_idx, v, p))
|
|
|
|
# print("%s" % (self.side))
|
|
# print("%s" % (inter))
|
|
|
|
if len(inter) < 1:
|
|
return True
|
|
|
|
# sort by seg and param t of seg
|
|
inter.sort()
|
|
|
|
# reorder so we really start from s_start
|
|
for i, it in enumerate(inter):
|
|
if it[0] >= s_start:
|
|
order = i
|
|
break
|
|
|
|
inter = inter[order:] + inter[:order]
|
|
|
|
# print("%s" % (inter))
|
|
p0 = border.segs[s_start].p0
|
|
|
|
n_inter = len(inter) - 1
|
|
|
|
for i in range(n_inter):
|
|
s_end, u, b_start, v, p = inter[i]
|
|
s_idx = border.get_index(s_start)
|
|
s = s_segs[s_idx].copy
|
|
s.is_hole = not start_by_hole
|
|
segs.append(s)
|
|
idx = s_idx
|
|
max_iter = s_nsegs
|
|
# walk through s_segs until intersection
|
|
while s_idx != s_end and max_iter > 0:
|
|
idx += 1
|
|
s_idx = border.get_index(idx)
|
|
s = s_segs[s_idx].copy
|
|
s.is_hole = not start_by_hole
|
|
segs.append(s)
|
|
max_iter -= 1
|
|
segs[-1].p1 = p
|
|
|
|
s_start, u, b_end, v, p = inter[i + 1]
|
|
b_idx = cutter.get_index(b_start)
|
|
s = b_segs[b_idx].copy
|
|
s.is_hole = start_by_hole
|
|
segs.append(s)
|
|
idx = b_idx
|
|
max_iter = b_nsegs
|
|
# walk through b_segs until intersection
|
|
while b_idx != b_end and max_iter > 0:
|
|
idx += 1
|
|
b_idx = cutter.get_index(idx)
|
|
s = b_segs[b_idx].copy
|
|
s.is_hole = start_by_hole
|
|
segs.append(s)
|
|
max_iter -= 1
|
|
segs[-1].p1 = p
|
|
|
|
# add part between last intersection and start point
|
|
idx = s_start
|
|
s_idx = border.get_index(s_start)
|
|
s = s_segs[s_idx].copy
|
|
s.is_hole = not start_by_hole
|
|
segs.append(s)
|
|
max_iter = s_nsegs
|
|
# go until end of segment is near start of first one
|
|
while (s_segs[s_idx].p1 - p0).length > 0.0001 and max_iter > 0:
|
|
idx += 1
|
|
s_idx = border.get_index(idx)
|
|
s = s_segs[s_idx].copy
|
|
s.is_hole = not start_by_hole
|
|
segs.append(s)
|
|
max_iter -= 1
|
|
|
|
if len(segs) > s_nsegs + b_nsegs + 1:
|
|
# print("slice failed found:%s of:%s" % (len(segs), s_nsegs + b_nsegs))
|
|
return False
|
|
|
|
for i, s in enumerate(segs):
|
|
s.p0 = segs[i - 1].p1
|
|
|
|
return True
|
|
|
|
def slice(self, cutter):
|
|
"""
|
|
Simple 2d Boolean between boundary and roof part
|
|
Doesn't handle slicing roof into multiple parts
|
|
|
|
4 cases:
|
|
1 pitch has point in boundary -> start from this point
|
|
2 boundary has point in pitch -> start from this point
|
|
3 no points inside -> find first crossing segment
|
|
4 not points inside and no crossing segments
|
|
"""
|
|
# print("************")
|
|
|
|
# keep inside or cut inside
|
|
# keep inside must be CCW
|
|
# cut inside must be CW
|
|
keep_inside = (cutter.operation == 'INTERSECTION')
|
|
|
|
start = -1
|
|
|
|
f_segs = self.segs
|
|
c_segs = cutter.segs
|
|
store = []
|
|
|
|
slice_res = True
|
|
is_inside = False
|
|
|
|
# find if either a cutter or
|
|
# cutter intersects
|
|
# (at least one point of any must be inside other one)
|
|
|
|
# find a point of this pitch inside cutter
|
|
for i, s in enumerate(f_segs):
|
|
res = self.inside(s.p0, c_segs)
|
|
if res:
|
|
is_inside = True
|
|
if res == keep_inside:
|
|
start = i
|
|
# print("pitch pt %sside f_start:%s %s" % (in_out, start, self.side))
|
|
slice_res = self.get_intersections(self, cutter, start, store, True)
|
|
break
|
|
|
|
# seek for point of cutter inside pitch
|
|
for i, s in enumerate(c_segs):
|
|
res = self.inside(s.p0)
|
|
if res:
|
|
is_inside = True
|
|
# no pitch point found inside cutter
|
|
if start < 0 and res == keep_inside:
|
|
start = i
|
|
# print("cutter pt %sside c_start:%s %s" % (in_out, start, self.side))
|
|
# swap cutter / pitch so we start from cutter
|
|
slice_res = self.get_intersections(cutter, self, start, store, False)
|
|
break
|
|
|
|
# no points found at all
|
|
if start < 0:
|
|
# print("no pt inside")
|
|
return not keep_inside
|
|
|
|
if not slice_res:
|
|
# print("slice fails")
|
|
# found more segments than input
|
|
# cutter made more than one loop
|
|
return True
|
|
|
|
if len(store) < 1:
|
|
if is_inside:
|
|
# print("not touching, add as hole")
|
|
if keep_inside:
|
|
self.segs = cutter.segs
|
|
else:
|
|
self.holes.append(cutter)
|
|
|
|
return True
|
|
|
|
self.segs = store
|
|
self.is_convex()
|
|
|
|
return True
|
|
|
|
|
|
class CutAbleGenerator():
|
|
|
|
def bissect(self, bm,
|
|
plane_co,
|
|
plane_no,
|
|
dist=0.001,
|
|
use_snap_center=False,
|
|
clear_outer=True,
|
|
clear_inner=False
|
|
):
|
|
geom = bm.verts[:]
|
|
geom.extend(bm.edges[:])
|
|
geom.extend(bm.faces[:])
|
|
|
|
bmesh.ops.bisect_plane(bm,
|
|
geom=geom,
|
|
dist=dist,
|
|
plane_co=plane_co,
|
|
plane_no=plane_no,
|
|
use_snap_center=False,
|
|
clear_outer=clear_outer,
|
|
clear_inner=clear_inner
|
|
)
|
|
|
|
def cut_holes(self, bm, cutable, offset={'DEFAULT': 0}):
|
|
o_keys = offset.keys()
|
|
has_offset = len(o_keys) > 1 or offset['DEFAULT'] != 0
|
|
# cut holes
|
|
for hole in cutable.holes:
|
|
|
|
if has_offset:
|
|
|
|
for s in hole.segs:
|
|
if s.length > 0:
|
|
if s.type in o_keys:
|
|
of = offset[s.type]
|
|
else:
|
|
of = offset['DEFAULT']
|
|
n = s.sized_normal(0, 1).v
|
|
p0 = s.p0 + n * of
|
|
self.bissect(bm, p0.to_3d(), n.to_3d(), clear_outer=False)
|
|
|
|
# compute boundary with offset
|
|
new_s = None
|
|
segs = []
|
|
for s in hole.segs:
|
|
if s.length > 0:
|
|
if s.type in o_keys:
|
|
of = offset[s.type]
|
|
else:
|
|
of = offset['DEFAULT']
|
|
new_s = s.make_offset(of, new_s)
|
|
segs.append(new_s)
|
|
# last / first intersection
|
|
if len(segs) > 0:
|
|
res, p0, t = segs[0].intersect(segs[-1])
|
|
if res:
|
|
segs[0].p0 = p0
|
|
segs[-1].p1 = p0
|
|
|
|
else:
|
|
for s in hole.segs:
|
|
if s.length > 0:
|
|
n = s.sized_normal(0, 1).v
|
|
self.bissect(bm, s.p0.to_3d(), n.to_3d(), clear_outer=False)
|
|
# use hole boundary
|
|
segs = hole.segs
|
|
if len(segs) > 0:
|
|
# when hole segs are found clear parts inside hole
|
|
f_geom = [f for f in bm.faces
|
|
if cutable.inside(
|
|
f.calc_center_median().to_2d(),
|
|
segs=segs)]
|
|
if len(f_geom) > 0:
|
|
bmesh.ops.delete(bm, geom=f_geom, context='FACES')
|
|
|
|
def cut_boundary(self, bm, cutable, offset={'DEFAULT': 0}):
|
|
o_keys = offset.keys()
|
|
has_offset = len(o_keys) > 1 or offset['DEFAULT'] != 0
|
|
# cut outside parts
|
|
if has_offset:
|
|
for s in cutable.segs:
|
|
if s.length > 0:
|
|
if s.type in o_keys:
|
|
of = offset[s.type]
|
|
else:
|
|
of = offset['DEFAULT']
|
|
n = s.sized_normal(0, 1).v
|
|
p0 = s.p0 + n * of
|
|
self.bissect(bm, p0.to_3d(), n.to_3d(), clear_outer=cutable.convex)
|
|
else:
|
|
for s in cutable.segs:
|
|
if s.length > 0:
|
|
n = s.sized_normal(0, 1).v
|
|
self.bissect(bm, s.p0.to_3d(), n.to_3d(), clear_outer=cutable.convex)
|
|
|
|
if not cutable.convex:
|
|
f_geom = [f for f in bm.faces
|
|
if not cutable.inside(f.calc_center_median().to_2d())]
|
|
if len(f_geom) > 0:
|
|
bmesh.ops.delete(bm, geom=f_geom, context='FACES')
|
|
|
|
|
|
def update_hole(self, context):
|
|
# update parent's only when manipulated
|
|
self.update(context, update_parent=True)
|
|
|
|
|
|
class ArchipackCutterPart():
|
|
"""
|
|
Cutter segment PropertyGroup
|
|
|
|
Childs MUST implements
|
|
-find_in_selection
|
|
Childs MUST define
|
|
-type EnumProperty
|
|
"""
|
|
length : FloatProperty(
|
|
name="Length",
|
|
min=0.01,
|
|
max=1000.0,
|
|
default=2.0,
|
|
update=update_hole
|
|
)
|
|
a0 : FloatProperty(
|
|
name="Angle",
|
|
min=-2 * pi,
|
|
max=2 * pi,
|
|
default=0,
|
|
subtype='ANGLE', unit='ROTATION',
|
|
update=update_hole
|
|
)
|
|
offset : FloatProperty(
|
|
name="Offset",
|
|
min=0,
|
|
default=0,
|
|
update=update_hole
|
|
)
|
|
|
|
def find_in_selection(self, context):
|
|
raise NotImplementedError
|
|
|
|
def draw(self, layout, context, index):
|
|
box = layout.box()
|
|
box.prop(self, "type", text=str(index + 1))
|
|
box.prop(self, "length")
|
|
# box.prop(self, "offset")
|
|
box.prop(self, "a0")
|
|
|
|
def update(self, context, update_parent=False):
|
|
props = self.find_in_selection(context)
|
|
if props is not None:
|
|
props.update(context, update_parent=update_parent)
|
|
|
|
|
|
def update_operation(self, context):
|
|
self.reverse(context, make_ccw=(self.operation == 'INTERSECTION'))
|
|
|
|
|
|
def update_path(self, context):
|
|
self.update_path(context)
|
|
|
|
|
|
def update(self, context):
|
|
self.update(context)
|
|
|
|
|
|
def update_manipulators(self, context):
|
|
self.update(context, manipulable_refresh=True)
|
|
|
|
|
|
class ArchipackCutter():
|
|
n_parts : IntProperty(
|
|
name="Parts",
|
|
min=1,
|
|
default=1, update=update_manipulators
|
|
)
|
|
z : FloatProperty(
|
|
name="dumb z",
|
|
description="Dumb z for manipulator placeholder",
|
|
default=0.01,
|
|
options={'SKIP_SAVE'}
|
|
)
|
|
user_defined_path : StringProperty(
|
|
name="User defined",
|
|
update=update_path
|
|
)
|
|
user_defined_resolution : IntProperty(
|
|
name="Resolution",
|
|
min=1,
|
|
max=128,
|
|
default=12, update=update_path
|
|
)
|
|
operation : EnumProperty(
|
|
items=(
|
|
('DIFFERENCE', 'Difference', 'Cut inside part', 0),
|
|
('INTERSECTION', 'Intersection', 'Keep inside part', 1)
|
|
),
|
|
default='DIFFERENCE',
|
|
update=update_operation
|
|
)
|
|
auto_update : BoolProperty(
|
|
options={'SKIP_SAVE'},
|
|
default=True,
|
|
update=update_manipulators
|
|
)
|
|
# UI layout related
|
|
parts_expand : BoolProperty(
|
|
default=False
|
|
)
|
|
|
|
closed = True
|
|
|
|
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 update_parent(self, context):
|
|
raise NotImplementedError
|
|
|
|
def setup_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)
|
|
|
|
def get_generator(self):
|
|
g = CutterGenerator(self)
|
|
for i, part in enumerate(self.parts):
|
|
g.add_part(part)
|
|
g.set_offset()
|
|
g.close()
|
|
return g
|
|
|
|
def interpolate_bezier(self, pts, wM, p0, p1, resolution):
|
|
# straight segment, worth testing here
|
|
# since this can lower points count by a resolution factor
|
|
# use normalized to handle non linear t
|
|
if resolution == 0:
|
|
pts.append(wM @ p0.co.to_3d())
|
|
else:
|
|
v = (p1.co - p0.co).normalized()
|
|
d1 = (p0.handle_right - p0.co).normalized()
|
|
d2 = (p1.co - p1.handle_left).normalized()
|
|
if d1 == v and d2 == v:
|
|
pts.append(wM @ p0.co.to_3d())
|
|
else:
|
|
seg = interpolate_bezier(wM @ p0.co,
|
|
wM @ p0.handle_right,
|
|
wM @ p1.handle_left,
|
|
wM @ p1.co,
|
|
resolution + 1)
|
|
for i in range(resolution):
|
|
pts.append(seg[i].to_3d())
|
|
|
|
def 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 ensure_direction(self):
|
|
# get segs ensure they are cw or ccw depending on operation
|
|
# whatever the user do with points
|
|
g = self.get_generator()
|
|
pts = [seg.p0.to_3d() for seg in g.segs]
|
|
if self.is_cw(pts) != (self.operation == 'INTERSECTION'):
|
|
return g
|
|
g.segs = [s.oposite for s in reversed(g.segs)]
|
|
return g
|
|
|
|
def from_spline(self, context, 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) == (self.operation == 'INTERSECTION'):
|
|
pts = list(reversed(pts))
|
|
|
|
pt = wM.inverted() @ pts[0]
|
|
|
|
# pretranslate
|
|
o = self.find_in_selection(context, self.auto_update)
|
|
o.matrix_world = wM @ Matrix.Translation(pt)
|
|
self.auto_update = False
|
|
self.from_points(pts)
|
|
self.auto_update = True
|
|
self.update_parent(context, o)
|
|
|
|
def from_points(self, pts):
|
|
|
|
self.n_parts = len(pts) - 2
|
|
|
|
self.update_parts()
|
|
|
|
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
|
|
|
|
def reverse(self, context, make_ccw=False):
|
|
|
|
o = self.find_in_selection(context, self.auto_update)
|
|
|
|
g = self.get_generator()
|
|
|
|
pts = [seg.p0.to_3d() for seg in g.segs]
|
|
|
|
if self.is_cw(pts) != make_ccw:
|
|
return
|
|
|
|
types = [p.type for p in self.parts]
|
|
|
|
pts.append(pts[0])
|
|
|
|
pts = list(reversed(pts))
|
|
self.auto_update = False
|
|
|
|
self.from_points(pts)
|
|
|
|
for i, type in enumerate(reversed(types)):
|
|
self.parts[i].type = type
|
|
self.auto_update = True
|
|
self.update_parent(context, o)
|
|
|
|
def update_path(self, context):
|
|
|
|
user_def_path = context.scene.objects.get(self.user_defined_path.strip())
|
|
if user_def_path is not None and user_def_path.type == 'CURVE':
|
|
self.from_spline(context,
|
|
user_def_path.matrix_world,
|
|
self.user_defined_resolution,
|
|
user_def_path.data.splines[0])
|
|
|
|
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.new((bm.verts[-1], bm.verts[0]))
|
|
bm.edges.ensure_lookup_table()
|
|
bm.to_mesh(o.data)
|
|
bm.free()
|
|
|
|
def update(self, context, manipulable_refresh=False, update_parent=False):
|
|
|
|
o = self.find_in_selection(context, self.auto_update)
|
|
|
|
if o is None:
|
|
return
|
|
|
|
# clean up manipulators before any data model change
|
|
if manipulable_refresh:
|
|
self.manipulable_disable(context)
|
|
|
|
self.update_parts()
|
|
|
|
verts = []
|
|
edges = []
|
|
|
|
g = self.get_generator()
|
|
g.locate_manipulators()
|
|
|
|
# vertex index in order to build axis
|
|
g.get_verts(verts, edges)
|
|
|
|
if len(verts) > 2:
|
|
self.make_surface(o, verts, edges)
|
|
|
|
# enable manipulators rebuild
|
|
if manipulable_refresh:
|
|
self.manipulable_refresh = True
|
|
|
|
# update parent on direct edit
|
|
if manipulable_refresh or update_parent:
|
|
self.update_parent(context, o)
|
|
|
|
# restore context
|
|
self.restore_context(context)
|
|
|
|
def manipulable_setup(self, context):
|
|
|
|
self.manipulable_disable(context)
|
|
o = context.object
|
|
|
|
n_parts = self.n_parts + 1
|
|
|
|
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
|
|
self.manip_stack.append(part.manipulators[1].setup(context, o, part))
|
|
# index
|
|
self.manip_stack.append(part.manipulators[3].setup(context, o, self))
|
|
# offset
|
|
# self.manip_stack.append(part.manipulators[4].setup(context, o, part))
|
|
|
|
# snap point
|
|
self.manip_stack.append(part.manipulators[2].setup(context, o, self))
|