''' 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 . ''' 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)