332 lines
12 KiB
Python
332 lines
12 KiB
Python
'''
|
|
Copyright (C) 2023 CG Cookie
|
|
http://cgcookie.com
|
|
hello@cgcookie.com
|
|
|
|
Created by Jonathan Denning, Jonathan Williamson, and Patrick Moore
|
|
|
|
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 3 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, see <http://www.gnu.org/licenses/>.
|
|
'''
|
|
|
|
|
|
import bpy
|
|
import math
|
|
from mathutils import Vector, Matrix
|
|
from mathutils.geometry import intersect_line_line_2d
|
|
from ...addon_common.common.debug import dprint
|
|
from ...addon_common.common.maths import Point,Point2D,Vec2D,Vec, Normal, clamp
|
|
from ...addon_common.common.bezier import CubicBezierSpline, CubicBezier
|
|
from ...addon_common.common.utils import iter_pairs
|
|
|
|
def is_boundaryedge(bme, only_bmfs):
|
|
return len(set(bme.link_faces) & only_bmfs) == 1
|
|
def is_boundaryvert(bmv, only_bmfs):
|
|
return len(set(bmv.link_faces) - only_bmfs) > 0 or bmv.is_boundary
|
|
|
|
def crawl_strip(bmf0, bme0_2, only_bmfs, stop_bmfs, touched=None):
|
|
#
|
|
# *------*------*
|
|
# ===> | bmf0 | bmf1 | ===>
|
|
# *------*------*
|
|
# ^ ^
|
|
# bme0_2=bme1_0 / \ bme1_2
|
|
#
|
|
bmfs = [bmf for bmf in bme0_2.link_faces if bmf in only_bmfs and bmf != bmf0]
|
|
if len(bmfs) != 1: return [bmf0]
|
|
bmf1 = bmfs[0]
|
|
# rotate bmedges so bme1_0 is where we came from, bme1_2 is where we are going
|
|
bmf1_edges = bmf1.edges
|
|
if bme0_2 == bmf1_edges[0]: bme1_0,bme1_1,bme1_2,bme1_3 = bmf1_edges
|
|
elif bme0_2 == bmf1_edges[1]: bme1_3,bme1_0,bme1_1,bme1_2 = bmf1_edges
|
|
elif bme0_2 == bmf1_edges[2]: bme1_2,bme1_3,bme1_0,bme1_1 = bmf1_edges
|
|
elif bme0_2 == bmf1_edges[3]: bme1_1,bme1_2,bme1_3,bme1_0 = bmf1_edges
|
|
else: assert False, 'Something very unexpected happened!'
|
|
|
|
if bmf1 not in only_bmfs: return [bmf0]
|
|
if bmf1 in stop_bmfs: return [bmf0, bmf1]
|
|
if touched and bmf1 in touched: return None
|
|
if not touched: touched = set()
|
|
touched.add(bmf0)
|
|
next_part = crawl_strip(bmf1, bme1_2, only_bmfs, stop_bmfs, touched)
|
|
if next_part is None: return None
|
|
return [bmf0] + next_part
|
|
|
|
def strip_centers(strip):
|
|
return [bmf.center() for bmf in strip]
|
|
pts = []
|
|
radius = 0
|
|
for bmf in strip:
|
|
bmvs = bmf.verts
|
|
pts = [bmv.co for bmv in bmf.verts]
|
|
v = Point.average(pts)
|
|
r = sum((pt - v).length for pt in pts) / 4
|
|
# r = ((pts[0] - pts[1]).length + (pts[1] - pts[2]).length + (pts[2] - pts[3]).length + (pts[3] - pts[0]).length) / 8
|
|
if not pts: radius = r
|
|
else: radius = max(radius, r)
|
|
pts += [v]
|
|
if False:
|
|
tesspts = []
|
|
tess_count = 2 if len(strip)>2 else 4
|
|
for pt0,pt1 in zip(pts[:-1],pts[1:]):
|
|
for i in range(tess_count):
|
|
p = i / tess_count
|
|
tesspts += [pt0 + (pt1-pt0)*p]
|
|
pts = tesspts + [pts[-1]]
|
|
return (pts, radius)
|
|
|
|
def hash_face_pair(bmf0, bmf1):
|
|
return str(bmf0.__hash__()) + str(bmf1.__hash__())
|
|
|
|
|
|
def process_stroke_filter(stroke, min_distance=1.0, max_distance=2.0):
|
|
''' filter stroke to pts that are at least min_distance apart '''
|
|
nstroke = stroke[:1]
|
|
for p in stroke[1:]:
|
|
v = p - nstroke[-1]
|
|
l = v.length
|
|
if l < min_distance: continue
|
|
d = v / l
|
|
while l > 0:
|
|
q = nstroke[-1] + d * min(l, max_distance)
|
|
nstroke.append(q)
|
|
l -= max_distance
|
|
return nstroke
|
|
|
|
def process_stroke_source(stroke, raycast, is_point_on_mirrored_side):
|
|
''' filter out pts that don't hit source on non-mirrored side '''
|
|
pts = [(pt, raycast(pt)[0]) for pt in stroke]
|
|
return [pt for pt,p3d in pts if p3d and not is_point_on_mirrored_side(p3d)]
|
|
|
|
def process_stroke_split_at_crossings(stroke):
|
|
strokes = []
|
|
stroke = list(stroke)
|
|
l = len(stroke)
|
|
cstroke = [stroke.pop()]
|
|
while stroke:
|
|
if not stroke[-1]:
|
|
strokes.append(cstroke)
|
|
stroke.pop()
|
|
cstroke = [stroke.pop()]
|
|
continue
|
|
p0,p1 = cstroke[-1],stroke[-1]
|
|
# see if p0-p1 segment crosses any other segment
|
|
for i in range(len(stroke)-3):
|
|
q0,q1 = stroke[i+0],stroke[i+1]
|
|
if q0 is None or q1 is None: continue
|
|
p = intersect_line_line_2d(p0,p1, q0,q1)
|
|
if not p: continue
|
|
if (p-p0).length < 0.000001 or (p-p1).length < 0.000001: continue
|
|
# intersection!
|
|
strokes.append(cstroke + [p])
|
|
cstroke = [p]
|
|
# note: inserting None to indicate broken stroke
|
|
stroke = stroke[:i+1] + [p,None,p] + stroke[i+1:]
|
|
break
|
|
else:
|
|
# no intersections!
|
|
cstroke.append(stroke.pop())
|
|
if cstroke: strokes.append(cstroke)
|
|
return strokes
|
|
|
|
def process_stroke_get_next(stroke, from_edge, edges2D):
|
|
# returns the next chunk of stroke to be processed
|
|
# stops at...
|
|
# - discontinuity
|
|
# - intersection with self
|
|
# - intersection with edges (ignoring from_edge)
|
|
# - "strong" corners
|
|
|
|
cstroke = []
|
|
to_edge = None
|
|
curve_distance, curve_threshold = 25.0, math.cos(60.0 * math.pi/180.0)
|
|
discontinuity_distance = 10.0
|
|
|
|
def compute_cosangle_at_index(idx):
|
|
nonlocal stroke
|
|
if idx >= len(stroke): return 1.0
|
|
p0 = stroke[idx]
|
|
for iprev in range(idx-1, -1, -1):
|
|
pprev = stroke[iprev]
|
|
if (p0-pprev).length < curve_distance: continue
|
|
break
|
|
else:
|
|
return 1.0
|
|
for inext in range(idx+1, len(stroke)):
|
|
pnext = stroke[inext]
|
|
if (p0-pnext).length < curve_distance: continue
|
|
break
|
|
else:
|
|
return 1.0
|
|
dprev = (p0 - pprev).normalized()
|
|
dnext = (pnext - p0).normalized()
|
|
cosangle = dprev.dot(dnext)
|
|
return cosangle
|
|
|
|
for i0 in range(1, len(stroke)-1):
|
|
i1 = i0 + 1
|
|
p0,p1 = stroke[i0],stroke[i1]
|
|
|
|
# check for discontinuity
|
|
if (p0-p1).length > discontinuity_distance:
|
|
# dprint('frag: %d %d %d' % (i0, len(stroke), len(stroke)-i1))
|
|
pass
|
|
return (from_edge, stroke[:i1], None, False, stroke[i1:])
|
|
|
|
# check for self-intersection
|
|
for j0 in range(i0+3, len(stroke)-1):
|
|
q0,q1 = stroke[j0],stroke[j0+1]
|
|
p = intersect_line_line_2d(p0,p1, q0,q1)
|
|
if not p: continue
|
|
# dprint('self: %d %d %d' % (i0, len(stroke), len(stroke)-i1))
|
|
pass
|
|
return (from_edge, stroke[:i1], None, False, stroke[i1:])
|
|
|
|
# check for intersections with edges
|
|
for bme,(q0,q1) in edges2D:
|
|
if bme is from_edge: continue
|
|
p = intersect_line_line_2d(p0,p1, q0,q1)
|
|
if not p: continue
|
|
# dprint('edge: %d %d %d' % (i0, len(stroke), len(stroke)-i1))
|
|
pass
|
|
return (from_edge, stroke[:i1], bme, True, stroke[i1:])
|
|
|
|
# check for strong angles
|
|
cosangle = compute_cosangle_at_index(i0)
|
|
if cosangle > curve_threshold: continue
|
|
# found a strong angle, but there may be a stronger angle coming up...
|
|
minangle = cosangle
|
|
for i0_plus in range(i0+1, len(stroke)):
|
|
p0_plus = stroke[i0_plus]
|
|
if (p0-p0_plus).length > curve_distance: break
|
|
minangle = min(compute_cosangle_at_index(i0_plus), minangle)
|
|
if minangle < cosangle: break
|
|
if minangle < cosangle: continue
|
|
# dprint('bend: %d %d %d' % (i0, len(stroke), len(stroke)-i1))
|
|
pass
|
|
return (from_edge, stroke[:i1], None, False, stroke[i1:])
|
|
|
|
# dprint('full: %d %d' % (len(stroke), len(stroke)))
|
|
pass
|
|
return (from_edge, stroke, None, False, [])
|
|
|
|
def process_stroke_get_marks(stroke, at_dists):
|
|
marks = []
|
|
tot_dist = 0
|
|
i_at_dists = 0
|
|
i_stroke = 1
|
|
cp = stroke[0]
|
|
np = stroke[1]
|
|
dist_to_np = (np-cp).length
|
|
dir_to_np = (np-cp).normalized()
|
|
|
|
while len(marks) < len(at_dists):
|
|
# can we go to np without passing next mark?
|
|
dratio = (at_dists[i_at_dists] - tot_dist) / dist_to_np if dist_to_np > 0 else 1
|
|
if dratio >= 1:
|
|
tot_dist += dist_to_np
|
|
i_stroke += 1
|
|
if i_stroke == len(stroke): break
|
|
cp,np = np,stroke[i_stroke]
|
|
dist_to_np = (np - cp).length
|
|
dir_to_np = (np - cp).normalized()
|
|
continue
|
|
dist_traveled = dist_to_np * dratio
|
|
cp = cp + dir_to_np * dist_traveled
|
|
marks.append(cp)
|
|
dist_to_np -= dist_traveled
|
|
tot_dist += dist_traveled
|
|
i_at_dists += 1
|
|
|
|
while len(marks) < len(at_dists):
|
|
marks.append(stroke[-1])
|
|
|
|
return marks
|
|
|
|
def mark_info(marks, imark):
|
|
imark0 = max(imark-1, 0)
|
|
imark1 = min(imark+1, len(marks)-1)
|
|
#assert imark0!=imark1, '%d %d %d %d' % (marks, imark, imark0, imark1)
|
|
tangent = (marks[imark1] - marks[imark0]).normalized()
|
|
perpendicular = Vec2D((-tangent.y, tangent.x))
|
|
return (marks[imark], tangent, perpendicular)
|
|
|
|
|
|
class RFTool_PolyStrips_Strip:
|
|
def __init__(self, bmf_strip):
|
|
self.bmf_strip = bmf_strip
|
|
self.recompute_curve()
|
|
self.capture_edges()
|
|
|
|
def __len__(self): return len(self.bmf_strip)
|
|
|
|
def __iter__(self): return iter(self.bmf_strip)
|
|
|
|
def __getitem__(self, key): return self.bmf_strip[key]
|
|
|
|
def end_faces(self): return (self.bmf_strip[0], self.bmf_strip[-1])
|
|
|
|
def recompute_curve(self):
|
|
pts = strip_centers(self.bmf_strip)
|
|
self.curve = CubicBezier.create_from_points(pts)
|
|
self.curve.tessellate_uniform(lambda p,q:(p-q).length, split=50)
|
|
|
|
def capture_edges(self):
|
|
self.bmes = []
|
|
bmes = [(bmf0.shared_edge(bmf1), Normal(bmf0.normal+bmf1.normal)) for bmf0,bmf1 in iter_pairs(self.bmf_strip, False)]
|
|
self.bme0 = self.bmf_strip[0].opposite_edge(bmes[0][0])
|
|
self.bme1 = self.bmf_strip[-1].opposite_edge(bmes[-1][0])
|
|
if len(self.bme0.link_faces) == 1: bmes = [(self.bme0, self.bmf_strip[0].normal)] + bmes
|
|
if len(self.bme1.link_faces) == 1: bmes = bmes + [(self.bme1, self.bmf_strip[-1].normal)]
|
|
if any(not bme.is_valid for (bme,_) in bmes):
|
|
# filter out invalid edges (see commit 88e4fde4)
|
|
bmes = [(bme,norm) for (bme,norm) in bmes if bme.is_valid]
|
|
for bme,norm in bmes:
|
|
bmvs = bme.verts
|
|
halfdiff = (bmvs[1].co - bmvs[0].co) / 2.0
|
|
diffdir = halfdiff.normalized()
|
|
center = bmvs[0].co + halfdiff
|
|
|
|
t = self.curve.approximate_t_at_point_tessellation(center, lambda p,q:(p-q).length)
|
|
pos,der = self.curve.eval(t),self.curve.eval_derivative(t).normalized()
|
|
|
|
rad = halfdiff.length
|
|
cross = der.cross(norm).normalized()
|
|
off = center - pos
|
|
off_cross,off_der,off_norm = cross.dot(off),der.dot(off),norm.dot(off)
|
|
rot = math.acos(clamp(diffdir.dot(cross), -0.9999999, 0.9999999))
|
|
if diffdir.dot(der) < 0: rot = -rot
|
|
self.bmes += [(bme, t, rad, rot, off_cross, off_der, off_norm)]
|
|
|
|
def update(self, nearest_sources_Point, raycast_sources_Point, update_face_normal):
|
|
self.curve.tessellate_uniform(lambda p,q:(p-q).length, split=50)
|
|
length = self.curve.approximate_totlength_tessellation()
|
|
for bme,t,rad,rot,off_cross,off_der,off_norm in self.bmes:
|
|
pos,norm,_,_ = raycast_sources_Point(self.curve.eval(t))
|
|
if not norm: continue
|
|
der = self.curve.eval_derivative(t).normalized()
|
|
cross = der.cross(norm).normalized()
|
|
center = pos + der * off_der + cross * off_cross + norm * off_norm
|
|
rotcross = (Matrix.Rotation(rot, 3, norm) @ cross).normalized()
|
|
p0 = center - rotcross * rad
|
|
p1 = center + rotcross * rad
|
|
bmv0,bmv1 = bme.verts
|
|
v0,n0,_,_ = raycast_sources_Point(p0)
|
|
v1,n1,_,_ = raycast_sources_Point(p1)
|
|
if not v0: v0,n0,_,_ = nearest_sources_Point(p0)
|
|
if not v1: v1,n1,_,_ = nearest_sources_Point(p1)
|
|
if v0: bmv0.co_normal = (v0, n0)
|
|
if v1: bmv1.co_normal = (v1, n1)
|
|
for bmf in self.bmf_strip:
|
|
update_face_normal(bmf)
|