Files
blender-portable-repo/scripts/addons/RetopoFlow/retopoflow/rftool_polystrips/polystrips_ops.py
T
2026-03-17 14:30:01 -06:00

414 lines
17 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 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']