''' 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 math from mathutils import Vector from mathutils.geometry import intersect_point_tri_2d, intersect_point_tri_2d from ..rftool import RFTool from ...addon_common.common.bezier import CubicBezierSpline, CubicBezier from ...addon_common.common.debug import dprint from ...addon_common.common.drawing import Drawing, Cursors from ...addon_common.common.profiler import profiler from ...addon_common.common.utils import iter_pairs from ..rfwidget import RFWidget from .polystrips_utils import ( RFTool_PolyStrips_Strip, hash_face_pair, crawl_strip, is_boundaryvert, is_boundaryedge, process_stroke_filter, process_stroke_source, process_stroke_get_next, process_stroke_get_marks, mark_info, ) class PolyStrips_Ops: @RFWidget.on_action('PolyStrips stroke') @RFTool.dirty_when_done def new_brushstroke(self): # called when artist finishes a stroke radius = self.rfwidgets['brushstroke'].radius Point_to_Point2D = self.rfcontext.Point_to_Point2D Point2D_to_Ray = self.rfcontext.Point2D_to_Ray nearest_sources_Point = self.rfcontext.nearest_sources_Point raycast = lambda p: self.rfcontext.raycast_sources_Point2D(p, correct_mirror=False) vis_verts = self.rfcontext.visible_verts() vis_edges = self.rfcontext.visible_edges(verts=vis_verts) vis_faces = self.rfcontext.visible_faces(verts=vis_verts) vis_edges2D,vis_faces2D = [],[] new_geom = [] def add_edge(bme): vis_edges2D.append((bme, [Point_to_Point2D(bmv.co) for bmv in bme.verts])) def add_face(bmf): vis_faces2D.append((bmf, [Point_to_Point2D(bmv.co) for bmv in bmf.verts])) def intersect_face(pt): # todo: rewrite! inefficient! nonlocal vis_faces2D for f,vs in vis_faces2D: v0 = vs[0] for v1,v2 in iter_pairs(vs[1:], False): if intersect_point_tri_2d(pt, v0, v1, v2): return f return None def snap_point(p2D_init, dist): p = raycast(p2D_init)[0] if p: return p # did not hit source, so find nearest point on source to where the point would have been r = Point2D_to_Ray(p2D_init) p = r.eval(dist) return nearest_sources_Point(p)[0] def create_edge(center, tangent, mult, perpendicular): nonlocal new_geom rad = radius hd,mmult = None,mult while not hd: p = center + tangent * mmult hp,hn,hi,hd = raycast(p) mmult -= 0.1 p0 = snap_point(center + tangent * mult + perpendicular * rad, hd) p1 = snap_point(center + tangent * mult - perpendicular * rad, hd) bmv0 = self.rfcontext.new_vert_point(p0) bmv1 = self.rfcontext.new_vert_point(p1) if not bmv0 or not bmv1: return None bme = self.rfcontext.new_edge([bmv0,bmv1]) add_edge(bme) new_geom += [bme] return bme def create_face_in_l(bme0, bme1): ''' creates a face strip between edges that share a vertex (L-shaped) ''' # find shared vert nonlocal new_geom bmv1 = bme0.shared_vert(bme1) bmv0,bmv2 = bme0.other_vert(bmv1),bme1.other_vert(bmv1) c0,c1,c2 = bmv0.co,bmv1.co,bmv2.co c3 = nearest_sources_Point(c1 + (c0-c1) + (c2-c1))[0] bmv3 = self.rfcontext.new_vert_point(c3) bmf = self.rfcontext.new_face([bmv0,bmv1,bmv2,bmv3]) # TODO: what if bmf is None?? bme2,bme3 = bmv2.shared_edge(bmv3),bmv3.shared_edge(bmv0) add_face(bmf) add_edge(bme2) add_edge(bme3) new_geom += [bme2,bme3,bmf] return bmf def create_face(bme01, bme23): # 0 3 0--3 # | | -> | | # 1 2 1--2 nonlocal new_geom if not bme01 or not bme23: return None if bme01.share_vert(bme23): return create_face_in_l(bme01, bme23) bmv0,bmv1 = bme01.verts bmv2,bmv3 = bme23.verts if bme01.vector().dot(bme23.vector()) > 0: bmv2,bmv3 = bmv3,bmv2 bmf = self.rfcontext.new_face([bmv0,bmv1,bmv2,bmv3]) # TODO: what if bmf is None? bme12 = bmv1.shared_edge(bmv2) bme30 = bmv3.shared_edge(bmv0) add_edge(bme12) add_edge(bme30) add_face(bmf) new_geom += [bme12, bme30, bmf] return bmf for bme in vis_edges: add_edge(bme) for bmf in vis_faces: add_face(bmf) self.rfcontext.undo_push('stroke') stroke = list(self.rfwidgets['brushstroke'].stroke2D) # filter stroke down where each pt is at least 1px away to eliminate local wiggling stroke = process_stroke_filter(stroke) stroke = process_stroke_source(stroke, self.rfcontext.raycast_sources_Point2D, self.rfcontext.is_point_on_mirrored_side) from_edge = None while len(stroke) > 2: # get stroke segment to work on from_edge,cstroke,to_edge,cont,stroke = process_stroke_get_next(stroke, from_edge, vis_edges2D) # filter cstroke to contain unique points while True: ncstroke = [cstroke[0]] for cp,np in iter_pairs(cstroke,False): if (cp-np).length > 0: ncstroke += [np] if len(cstroke) == len(ncstroke): break cstroke = ncstroke # discard stroke segment if it lies in a face if intersect_face(cstroke[1]): # dprint('stroke is on face (1)') pass from_edge = to_edge continue if intersect_face(cstroke[-2]): # dprint('stroke is on face (-2)') pass from_edge = to_edge continue # estimate length of stroke (used with radius to determine num of quads) stroke_len = sum((p0-p1).length for (p0,p1) in iter_pairs(cstroke,False)) # marks start and end at center of quad, and alternate with # edge and face, each approx radius distance apart # +---+---+---+---+---+ # | | | | | | # +---+---+---+---+---+ # ^ ^ ^ ^ ^ ^ ^ ^ ^ <-----marks (nmarks: 9, nquads: 5) # ^ ^ ^ ^ ^ ^ ^ ^ <- if from_edge not None # ^ ^ ^ ^ ^ ^ ^ ^ <- if to_edge not None # ^ ^ ^ ^ ^ ^ ^ <- if from_edge and to_edge are not None # mark counts: # min marks = 3 [ | ] (2 quads) # marks = 5 [ | | ] (3 quads) # marks = 7 [ | | | ] (4 quads) # marks must be odd # if from_edge is not None, then stroke starts at edge # if to_edge is not None, then stroke ends at edge markoff0 = 0 if from_edge is None else 1 markoff1 = 0 if to_edge is None else 1 nmarks = int(math.ceil(stroke_len / radius)) # approx num of marks nmarks = nmarks + (1 - ((nmarks+markoff0+markoff1) % 2)) # make sure odd count nmarks = max(nmarks, 3-markoff0-markoff1) # min marks = 3 nmarks = max(nmarks, 2) # fix div by 0 :( # marks are found at dists along stroke at_dists = [stroke_len*i/(nmarks-1) for i in range(nmarks)] # compute marks marks = process_stroke_get_marks(cstroke, at_dists) # compute number of quads nquads = int(((nmarks-markoff0-markoff1) + 1) / 2) # dprint('nmarks = %d, markoff0 = %d, markoff1 = %d, nquads = %d' % (nmarks, markoff0, markoff1, nquads)) pass if from_edge and to_edge and nquads == 1: if from_edge.share_vert(to_edge): create_face_in_l(from_edge, to_edge) continue # add edges if from_edge is None: # create from_edge # dprint('creating from_edge') pass pt,tn,pe = mark_info(marks, 0) from_edge = create_edge(pt, -tn, radius, pe) else: new_geom += list(from_edge.link_faces) if to_edge is None: # dprint('creating to_edge') pass pt,tn,pe = mark_info(marks, nmarks-1) to_edge = create_edge(pt, tn, radius, pe) else: new_geom += list(to_edge.link_faces) for iquad in range(1, nquads): #print('creating edge') pt,tn,pe = mark_info(marks, iquad*2+markoff0-1) bme = create_edge(pt, tn, 0.0, pe) bmf = create_face(from_edge, bme) from_edge = bme bmf = create_face(from_edge, to_edge) from_edge = to_edge if cont else None self.rfcontext.select(new_geom, supparts=False) def clear_count_data(self): self.count_data = { 'delta': 0, 'delta adjust': 0, 'update fns': [], 'nfaces': [], 'splines': [], 'points': [], } def setup_change_count(self): self.clear_count_data() def process(bmfs, bmes): # find edge strips strip0,strip1 = [bmes[0].verts[0]], [bmes[0].verts[1]] edges0,edges1 = [],[] for bmf,bme0 in zip(bmfs,bmes): bme1,bme2 = bmf.neighbor_edges(bme0) if strip0[-1] in bme2.verts: bme1,bme2 = bme2,bme1 strip0.append(bme1.other_vert(strip0[-1])) strip1.append(bme2.other_vert(strip1[-1])) edges0.append(bme1) edges1.append(bme2) if len(strip0) < 3: return pts0,pts1 = [v.co for v in strip0],[v.co for v in strip1] lengths0,lengths1 = [e.length for e in edges0],[e.length for e in edges1] #length0,length1 = sum(lengths0),sum(lengths1) max_error = min(min(lengths0),min(lengths1)) / 100.0 # arbitrary! spline0 = CubicBezierSpline.create_from_points([pts0], max_error, min_count_split=3) spline1 = CubicBezierSpline.create_from_points([pts1], max_error, min_count_split=3) spline0.tessellate_uniform(lambda a,b: (a-b).length, 50) spline1.tessellate_uniform(lambda a,b: (a-b).length, 50) len0,len1 = len(spline0), len(spline1) self.count_data['splines'] += [spline0, spline1] self.count_data['points'] += pts0 + pts1 ccount = len(bmfs) nfaces = [] nedges = [] nverts = [bmv for bme in bmes[1:-1] for bmv in bme.verts] def fn(count=None, delta=None): nonlocal nverts if count is not None: ncount = count else: ncount = ccount + delta if ncount < 1: self.count_data['delta adjust'] = max(self.count_data['delta adjust'], 1 - ncount) ncount = 1 ncount = max(1, ncount) # approximate ts along each strip def approx_ts(spline_len, lengths): nonlocal ncount,ccount accum_ts_old = [0] for l in lengths: accum_ts_old.append(accum_ts_old[-1] + l) total_ts_old = sum(lengths) ts_old = [Vector((i, t / total_ts_old, 0)) for i,t in enumerate(accum_ts_old)] spline_ts_old = CubicBezierSpline.create_from_points([ts_old], 0.01) spline_ts_old_len = len(spline_ts_old) ts = [spline_len * spline_ts_old.eval(spline_ts_old_len * i / ncount).y for i in range(ncount+1)] return ts ts0 = approx_ts(len0, lengths0) ts1 = approx_ts(len1, lengths1) if not nverts: #self.rfcontext.delete_faces(nfaces) self.rfcontext.delete_edges(nedges) else: self.rfcontext.delete_verts(nverts) nverts.clear() nedges.clear() nfaces.clear() # self.rfcontext.delete_edges(edges0 + edges1 + bmes[1:-1]) def new_vert(p): v = self.rfcontext.new_vert_point(p) nverts.append(v) return v verts0 = strip0[:1] + [new_vert(spline0.eval(t)) for t in ts0[1:-1]] + strip0[-1:] verts1 = strip1[:1] + [new_vert(spline1.eval(t)) for t in ts1[1:-1]] + strip1[-1:] for (v00,v01),(v10,v11) in zip(iter_pairs(verts0,False), iter_pairs(verts1,False)): nf = self.rfcontext.new_face([v00,v01,v11,v10]) assert nf self.count_data['nfaces'].append(nf) nfaces.append(nf) for (v00, v01) in iter_pairs(verts0, False): nedges.append(v00.shared_edge(v01)) for (v10, v11) in iter_pairs(verts1, False): nedges.append(v10.shared_edge(v11)) self.count_data['update fns'].append(fn) # find selected faces that are not part of strips # [ | | | | | | | ] # |O| |O| <- with either of these selected, split into two # [ | | | ] rffaces = self.rfcontext.get_selected_faces() bmquads = [bmf for bmf in rffaces if len(bmf.verts) == 4] bmquads = [bmq for bmq in bmquads if not any(bmq in strip for strip in self.strips)] for bmf in bmquads: bmes = list(bmf.edges) boundaries = [len(bme.link_faces) == 2 for bme in bmf.edges] if (boundaries[0] or boundaries[2]) and not boundaries[1] and not boundaries[3]: process([bmf], [bmes[0],bmes[2]]) continue if (boundaries[1] or boundaries[3]) and not boundaries[0] and not boundaries[2]: process([bmf], [bmes[1],bmes[3]]) continue # find boundary portions of each strip # TODO: what if there are multiple boundary portions?? # [ | |O| | ] # |O| <- # |O| <- only working on this part of strip # |O| <- # |O| | ] # [ | |O| | ] for strip in self.strips: bmfs,bmes = [],[] bme0 = strip.bme0 for bmf in strip: bme2 = bmf.opposite_edge(bme0) bme1,bme3 = bmf.neighbor_edges(bme0) if len(bme1.link_faces) == 1 and len(bme3.link_faces) == 1: bmes.append(bme0) bmfs.append(bmf) else: # if we've already seen a portion of the strip that can be modified, break! if bmfs: bmes.append(bme0) break bme0 = bme2 else: bmes.append(bme0) if not bmfs: continue process(bmfs, bmes) @RFTool.dirty_when_done def change_count(self, *, count=None, delta=None): ''' find parallel strips of boundary edges, fit curve to verts of strips, then recompute faces based on curves. note: this op will only change counts along boundaries. otherwise, use loop cut ''' self.rfcontext.undo_push('change segment count', repeatable=True) self.count_data['nfaces'].clear() self.count_data['delta adjust'] = 0 if delta is not None: self.count_data['delta'] += delta delta = self.count_data['delta'] for fn in self.count_data['update fns']: fn(count=count, delta=delta) if self.count_data['nfaces']: self.rfcontext.select(self.count_data['nfaces'], supparts=False, only=False) if delta is not None: self.count_data['delta'] += self.count_data['delta adjust']