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

661 lines
26 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
import time
import random
import bpy
import gpu
from mathutils import Matrix, Vector
from mathutils.geometry import intersect_point_tri_2d, intersect_point_tri_2d
from ..rftool import RFTool
from ...addon_common.common.decorators import timed_call
from ...addon_common.common import gpustate
################################################################################################
# following imports must happen *after* the above class, because each subclass depends on
# above class to be defined
from .polystrips_ops import PolyStrips_Ops
from .polystrips_props import PolyStrips_Props
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,
)
from ...addon_common.common.bezier import CubicBezierSpline, CubicBezier
from ...addon_common.common.blender import tag_redraw_all
from ...addon_common.common.debug import dprint
from ...addon_common.common.drawing import Drawing, Cursors, DrawCallbacks
from ...addon_common.common.fsm import FSM
from ...addon_common.common.maths import Vec2D, Point, rotate2D, Direction2D, Point2D, RelPoint2D
from ...addon_common.common.profiler import profiler
from ...addon_common.common.utils import iter_pairs
from ...config.options import options, themes
from ..rfwidgets.rfwidget_brushstroke import RFWidget_BrushStroke_Factory
from ..rfwidgets.rfwidget_default import RFWidget_Default_Factory
from ..rfwidgets.rfwidget_hidden import RFWidget_Hidden_Factory
from ...addon_common.common.boundvar import BoundBool, BoundInt, BoundFloat, BoundString
class PolyStrips(RFTool, PolyStrips_Props, PolyStrips_Ops):
name = 'PolyStrips'
description = 'Create and edit strips of quads'
icon = 'polystrips-icon.png'
help = 'polystrips.md'
shortcut = 'polystrips tool'
statusbar = '{{insert}} Insert strip of quads\t{{brush radius}} Brush size\t{{action}} Grab selection\t{{increase count}} Increase segments\t{{decrease count}} Decrease segments'
ui_config = 'polystrips_options.html'
RFWidget_Default = RFWidget_Default_Factory.create()
RFWidget_Move = RFWidget_Default_Factory.create(cursor='HAND')
RFWidget_Hidden = RFWidget_Hidden_Factory.create()
RFWidget_BrushStroke = RFWidget_BrushStroke_Factory.create(
'PolyStrips stroke',
BoundInt('''options['polystrips radius']''', min_value=1),
outer_border_color=themes['polystrips']
)
@RFTool.on_init
def init(self):
self.rfwidgets = {
'default': self.RFWidget_Default(self),
'brushstroke': self.RFWidget_BrushStroke(self),
'move': self.RFWidget_Move(self),
'hidden': self.RFWidget_Hidden(self),
}
self.rfwidget = None
@RFTool.on_reset
def reset(self):
self.strips = []
self.strip_pts = []
self.hovering_strips = set()
self.hovering_handles = []
self.hovering_sel_face = None
self.sel_cbpts = []
self.stroke_cbs = CubicBezierSpline()
self.clear_count_data()
@RFTool.on_target_change
# @profiler.function
def update_target(self, force=False):
if not force and self._fsm.state in {'move handle', 'rotate', 'scale'}: return
self.strips = []
self._var_cut_count.disabled = True
# get selected quads
bmquads = set(bmf for bmf in self.rfcontext.get_selected_faces() if len(bmf.verts) == 4)
if not bmquads: return
# find junctions at corners
junctions = set()
for bmf in bmquads:
# skip if in middle of a selection
if not any(is_boundaryvert(bmv, bmquads) for bmv in bmf.verts): continue
# skip if in middle of possible strip
edge0,edge1,edge2,edge3 = [is_boundaryedge(bme, bmquads) for bme in bmf.edges]
if (edge0 or edge2) and not (edge1 or edge3): continue
if (edge1 or edge3) and not (edge0 or edge2): continue
junctions.add(bmf)
# find junctions that might be in middle of strip but are ends to other strips
boundaries = set((bme,bmf) for bmf in bmquads for bme in bmf.edges if is_boundaryedge(bme, bmquads))
while boundaries:
bme,bmf = boundaries.pop()
for bme_ in bmf.neighbor_edges(bme):
strip = crawl_strip(bmf, bme_, bmquads, junctions)
if strip is None: continue
junctions.add(strip[-1])
# find strips between junctions
touched = set()
for bmf0 in junctions:
bme0,bme1,bme2,bme3 = bmf0.edges
edge0,edge1,edge2,edge3 = [is_boundaryedge(bme, bmquads) for bme in bmf0.edges]
def add_strip(bme):
strip = crawl_strip(bmf0, bme, bmquads, junctions)
if not strip:
return
bmf1 = strip[-1]
if len(strip) > 1 and hash_face_pair(bmf0, bmf1) not in touched:
touched.add(hash_face_pair(bmf0,bmf1))
touched.add(hash_face_pair(bmf1,bmf0))
self.strips.append(RFTool_PolyStrips_Strip(strip))
if not edge0: add_strip(bme0)
if not edge1: add_strip(bme1)
if not edge2: add_strip(bme2)
if not edge3: add_strip(bme3)
if options['polystrips max strips'] and len(self.strips) > options['polystrips max strips']:
self.strips = []
break
self.update_strip_viz()
if len(self.strips) == 1:
self._var_cut_count.set(len(self.strips[0]))
self._var_cut_count.disabled = False
if self.rfcontext.get_last_action() != 'change segment count':
self.setup_change_count()
# @profiler.function
def update_strip_viz(self):
self.strip_pts = [[strip.curve.eval(i/10) for i in range(10+1)] for strip in self.strips]
@FSM.on_state('main')
def main(self):
Point_to_Point2D = self.rfcontext.Point_to_Point2D
mouse = self.actions.mouse
if not self.actions.using('action', ignoredrag=True):
# only update while not pressing action, because action includes drag, and
# the artist might move mouse off selected edge before drag kicks in!
self.hovering_sel_face,_ = self.rfcontext.accel_nearest2D_face(max_dist=options['action dist'], selected_only=True)
self.hovering_handles.clear()
self.hovering_strips.clear()
for strip in self.strips:
for i,cbpt in enumerate(strip.curve):
v = Point_to_Point2D(cbpt)
if v is None: continue
if (mouse - v).length > self.drawing.scale(options['select dist']): continue
# do not filter out non-visible handles, because otherwise
# they might not be movable if they are inside the model
self.hovering_handles.append(cbpt)
self.hovering_strips.add(strip)
if self.actions.using_onlymods('insert'):
self.set_widget('brushstroke')
elif self.hovering_handles:
self.set_widget('move')
elif self.hovering_sel_face:
self.set_widget('move')
else:
self.set_widget('default')
if self.handle_inactive_passthrough(): return
# handle edits
if self.hovering_handles:
if self.actions.pressed('action'):
return 'move handle'
if self.actions.pressed('action alt0'):
return 'rotate'
if self.actions.pressed('action alt1'):
return 'scale'
if self.hovering_sel_face:
if self.actions.pressed('action', unpress=False):
return 'move all'
if self.actions.pressed('grab', unpress=False):
return 'move all'
if self.actions.pressed('increase count'):
self.change_count(delta=1)
return
if self.actions.pressed('decrease count'):
self.change_count(delta=-1)
return
if self.actions.pressed({'select path add'}):
return self.rfcontext.select_path(
{'face'},
kwargs_select={'supparts': False},
)
if self.actions.pressed({'select paint', 'select paint add'}, unpress=False):
sel_only = self.actions.pressed('select paint')
return self.rfcontext.setup_smart_selection_painting(
{'face'},
use_select_tool=True,
selecting=not sel_only,
deselect_all=sel_only,
# fn_filter_bmelem=self.filter_edge_selection,
kwargs_select={'supparts': False},
kwargs_deselect={'subparts': False},
)
if self.actions.pressed({'select single', 'select single add'}, unpress=False):
sel_only = self.actions.pressed('select single')
self.actions.unpress()
bmf,_ = self.rfcontext.accel_nearest2D_face(max_dist=options['select dist'])
if not sel_only and not bmf: return
self.rfcontext.undo_push('select')
if sel_only: self.rfcontext.deselect_all()
if not bmf: return
if bmf.select: self.rfcontext.deselect(bmf, subparts=False)
else: self.rfcontext.select(bmf, supparts=False, only=sel_only)
return
@FSM.on_state('move handle', 'can enter')
def movehandle_canenter(self):
return len(self.hovering_handles) > 0
@FSM.on_state('move handle', 'enter')
def movehandle_enter(self):
self.sel_cbpts = []
self.mod_strips = set()
cbpts = list(self.hovering_handles)
self.mod_strips |= self.hovering_strips
for strip in self.strips:
p0,p1,p2,p3 = strip.curve.points()
if p0 in cbpts and p1 not in cbpts:
cbpts.append(p1)
self.mod_strips.add(strip)
if p3 in cbpts and p2 not in cbpts:
cbpts.append(p2)
self.mod_strips.add(strip)
for strip in self.mod_strips: strip.capture_edges()
inners = [ p for strip in self.strips for p in strip.curve.points()[1:3] ]
self.sel_cbpts = [(cbpt, cbpt in inners, Point(cbpt), self.rfcontext.Point_to_Point2D(cbpt)) for cbpt in cbpts]
self.mousedown = self.actions.mouse
self.move_done_pressed = 'confirm'
self.move_done_released = 'action'
self.move_cancelled = 'cancel'
self.rfcontext.undo_push('manipulate bezier')
self.set_widget('hidden' if options['hide cursor on tweak'] else 'move')
self._timer = self.actions.start_timer(120.0)
self.rfcontext.split_target_visualization(verts=self.rfcontext.get_selected_verts())
self.rfcontext.set_accel_defer(True)
@FSM.on_state('move handle')
def movehandle(self):
if self.actions.pressed(self.move_done_pressed):
return 'main'
if self.actions.released(self.move_done_released):
return 'main'
if self.actions.pressed(self.move_cancelled):
self.rfcontext.undo_cancel()
self.actions.unuse(self.move_done_released, ignoremods=True, ignoremulti=True)
return 'main'
if self.actions.mousemove or not self.actions.mousemove_prev: return
delta = Vec2D(self.actions.mouse - self.mousedown)
up,rt,fw = self.rfcontext.Vec_up(),self.rfcontext.Vec_right(),self.rfcontext.Vec_forward()
for cbpt,inner,oco,oco2D in self.sel_cbpts:
nco2D = oco2D + delta
if not inner:
xyz,_,_,_ = self.rfcontext.raycast_sources_Point2D(nco2D)
if xyz: cbpt.xyz = xyz
else:
ov = self.rfcontext.Point2D_to_Vec(oco2D)
nr = self.rfcontext.Point2D_to_Ray(nco2D)
od = self.rfcontext.Point_to_depth(oco)
cbpt.xyz = nr.eval(od / ov.dot(nr.d))
for strip in self.hovering_strips:
strip.update(self.rfcontext.nearest_sources_Point, self.rfcontext.raycast_sources_Point, self.rfcontext.update_face_normal)
self.update_strip_viz()
self.rfcontext.dirty()
@FSM.on_state('move handle', 'exit')
def movehandle_exit(self):
self._timer.done()
self.rfcontext.clear_split_target_visualization()
self.rfcontext.set_accel_defer(False)
self.update_target(force=True)
tag_redraw_all('PolyStrips done moving handles')
@FSM.on_state('rotate', 'can enter')
def rotate_canenter(self):
if not self.hovering_handles: return False
self.sel_cbpts = []
self.mod_strips = set()
Point_to_Point2D = self.rfcontext.Point_to_Point2D
# find hovered inner point, the corresponding outer point and its face
innerP,outerP,outerF = None,None,None
for strip in self.strips:
bmf0,bmf1 = strip.end_faces()
p0,p1,p2,p3 = strip.curve.points()
if p1 in self.hovering_handles: innerP,outerP,outerF = p1,p0,bmf0
if p2 in self.hovering_handles: innerP,outerP,outerF = p2,p3,bmf1
if not innerP or not outerP or not outerF: return False
# scan through all selected strips and collect all inner points next to outerP
for strip in self.strips:
bmf0,bmf3 = strip.end_faces()
if outerF != bmf0 and outerF != bmf3: continue
p0,p1,p2,p3 = strip.curve.points()
if outerF == bmf0: self.sel_cbpts.append( (p1, Point(p1), Point_to_Point2D(p1)) )
else: self.sel_cbpts.append( (p2, Point(p2), Point_to_Point2D(p2)) )
self.mod_strips.add(strip)
self.rotate_about = Point_to_Point2D(outerP)
if not self.rotate_about: return False
@FSM.on_state('rotate', 'enter')
def rotate_enter(self):
for strip in self.mod_strips: strip.capture_edges()
self.mousedown = self.actions.mouse
self.move_done_pressed = 'confirm'
self.move_done_released = 'action alt0'
self.move_cancelled = 'cancel'
self.rfcontext.undo_push('rotate')
self.set_widget('hidden' if options['hide cursor on tweak'] else 'move')
self._timer = self.actions.start_timer(120.0)
self.rfcontext.split_target_visualization(verts=self.rfcontext.get_selected_verts())
self.rfcontext.set_accel_defer(True)
@FSM.on_state('rotate')
# @profiler.function
def rotate(self):
if not self.rotate_about: return 'main'
if self.actions.pressed(self.move_done_pressed):
return 'main'
if self.actions.released(self.move_done_released):
return 'main'
if self.actions.pressed(self.move_cancelled):
self.rfcontext.undo_cancel()
self.actions.unuse(self.move_done_released, ignoremods=True, ignoremulti=True)
return 'main'
if self.actions.mousemove or not self.actions.mousemove_prev: return
prev_diff = self.mousedown - self.rotate_about
prev_rot = math.atan2(prev_diff.x, prev_diff.y)
cur_diff = self.actions.mouse - self.rotate_about
cur_rot = math.atan2(cur_diff.x, cur_diff.y)
angle = prev_rot - cur_rot
for cbpt,oco,oco2D in self.sel_cbpts:
xy = rotate2D(oco2D, angle, origin=self.rotate_about)
xyz,_,_,_ = self.rfcontext.raycast_sources_Point2D(xy)
if xyz: cbpt.xyz = xyz
for strip in self.mod_strips:
strip.update(self.rfcontext.nearest_sources_Point, self.rfcontext.raycast_sources_Point, self.rfcontext.update_face_normal)
self.update_strip_viz()
self.rfcontext.dirty()
@FSM.on_state('rotate', 'exit')
def rotate_exit(self):
self._timer.done()
self.rfcontext.set_accel_defer(False)
self.rfcontext.clear_split_target_visualization()
self.update_target(force=True)
@FSM.on_state('scale', 'can enter')
# @profiler.function
def scale_canenter(self):
if not self.hovering_handles: return False
self.mod_strips = set()
Point_to_Point2D = self.rfcontext.Point_to_Point2D
innerP,outerP,outerF = None,None,None
for strip in self.strips:
bmf0,bmf1 = strip.end_faces()
p0,p1,p2,p3 = strip.curve.points()
if p1 in self.hovering_handles: innerP,outerP,outerF = p1,p0,bmf0
if p2 in self.hovering_handles: innerP,outerP,outerF = p2,p3,bmf1
if not innerP or not outerP or not outerF: return False
self.scale_strips = []
for strip in self.strips:
bmf0,bmf1 = strip.end_faces()
if bmf0 == outerF:
self.scale_strips.append((strip, 1))
self.mod_strips.add(strip)
if bmf1 == outerF:
self.scale_strips.append((strip, 2))
self.mod_strips.add(strip)
for strip in self.mod_strips: strip.capture_edges()
if not self.scale_strips: return False
self.scale_from = Point_to_Point2D(outerP)
@FSM.on_state('scale', 'enter')
def scale_enter(self):
self.mousedown = self.actions.mouse
self.rfcontext.undo_push('scale')
self.move_done_pressed = None
self.move_done_released = 'action'
self.move_cancelled = 'cancel'
falloff = options['polystrips scale falloff']
self.scale_bmf = {}
self.scale_bmv = {}
for strip,iinner in self.scale_strips:
iend = 0 if iinner == 1 else 3
s0,s1 = (1,0) if iend == 0 else (0,1)
l = len(strip.bmf_strip)
for ibmf,bmf in enumerate(strip.bmf_strip):
if bmf in self.scale_bmf: continue
p = ibmf/(l-1)
s = (s0 + (s1-s0) * p) ** falloff
self.scale_bmf[bmf] = s
for bmf in self.scale_bmf.keys():
c = bmf.center()
s = self.scale_bmf[bmf]
for bmv in bmf.verts:
if bmv not in self.scale_bmv:
self.scale_bmv[bmv] = []
self.scale_bmv[bmv] += [(c, bmv.co-c, s)]
self.set_widget('hidden' if options['hide cursor on tweak'] else 'default') # None
self._timer = self.actions.start_timer(120.0)
self.rfcontext.split_target_visualization(verts=self.rfcontext.get_selected_verts())
self.rfcontext.set_accel_defer(True)
@FSM.on_state('scale')
# @profiler.function
def scale(self):
if self.actions.pressed(self.move_done_pressed):
return 'main'
if self.actions.released(self.move_done_released, ignoremods=True):
return 'main'
if self.actions.pressed(self.move_cancelled):
self.rfcontext.undo_cancel()
self.actions.unuse(self.move_done_released, ignoremods=True, ignoremulti=True)
return 'main'
if self.actions.mousemove or not self.actions.mousemove_prev: return
vec0 = self.mousedown - self.scale_from
vec1 = self.actions.mouse - self.scale_from
scale = vec1.length / vec0.length
snap2D_vert = self.rfcontext.snap2D_vert
snap_vert = self.rfcontext.snap_vert
for bmv in self.scale_bmv.keys():
l = self.scale_bmv[bmv]
n = Vector()
for c,v,sc in l:
n += c + v * max(0, 1 + (scale-1) * sc)
bmv.co = n / len(l)
snap_vert(bmv)
self.rfcontext.dirty()
@FSM.on_state('scale', 'exit')
def scale_exit(self):
self._timer.done()
self.rfcontext.set_accel_defer(False)
self.rfcontext.clear_split_target_visualization()
self.update_target(force=True)
@FSM.on_state('move all', 'can enter')
# @profiler.function
def moveall_canenter(self):
bmfaces = self.rfcontext.get_selected_faces()
if not bmfaces: return False
bmverts = set(bmv for bmf in bmfaces for bmv in bmf.verts)
self.bmverts = [(bmv, self.rfcontext.Point_to_Point2D(bmv.co)) for bmv in bmverts]
@FSM.on_state('move all', 'enter')
def moveall_enter(self):
lmb_drag = self.actions.using('action')
self.actions.unpress()
self.rfcontext.undo_push('move grabbed')
self.moveall_opts = {
'mousedown': self.actions.mouse,
'move_done_pressed': None if lmb_drag else 'confirm',
'move_done_released': 'action' if lmb_drag else None,
'move_cancelled': 'cancel',
'timer': self.actions.start_timer(120.0),
}
self.rfcontext.split_target_visualization_selected()
self.rfcontext.set_accel_defer(True)
self.set_widget('hidden' if options['hide cursor on tweak'] else 'default') # None
@FSM.on_state('move all')
# @profiler.function
def moveall(self):
opts = self.moveall_opts
if self.actions.pressed(opts['move_done_pressed']):
return 'main'
if self.actions.released(opts['move_done_released']):
return 'main'
if self.actions.pressed(opts['move_cancelled']):
self.rfcontext.undo_cancel()
self.actions.unuse(opts['move_done_released'], ignoremods=True, ignoremulti=True)
return 'main'
if self.actions.mousemove or not self.actions.mousemove_prev: return
delta = Vec2D(self.actions.mouse - opts['mousedown'])
set2D_vert = self.rfcontext.set2D_vert
for bmv,xy in self.bmverts:
if not bmv.is_valid: continue
set2D_vert(bmv, xy + delta)
self.rfcontext.update_verts_faces(v for v,_ in self.bmverts)
self.rfcontext.dirty()
#self.update()
@FSM.on_state('move all', 'exit')
def moveall_exit(self):
self.moveall_opts['timer'].done()
self.rfcontext.set_accel_defer(False)
self.rfcontext.clear_split_target_visualization()
self.update_target(force=True)
@DrawCallbacks.on_draw('post3d')
@FSM.onlyinstate({'main', 'move handle', 'rotate', 'scale'})
def draw_post3d_spline(self):
if not self.strips: return
strips = self.strips
hov_strips = self.hovering_strips
Point_to_Point2D = self.rfcontext.Point_to_Point2D
def is_visible(v):
return True # self.rfcontext.is_visible(v, None)
def draw(alphamult, hov_alphamult, hover):
nonlocal strips
if not hover: hov_alphamult = alphamult
size_outer = options['polystrips handle outer size']
size_inner = options['polystrips handle inner size']
border_outer = options['polystrips handle border']
border_inner = options['polystrips handle border']
gpustate.blend('ALPHA')
# draw outer-inner lines
pts = [Point_to_Point2D(p) for strip in strips for p in strip.curve.points()]
self.rfcontext.drawing.draw2D_lines(pts, (1,1,1,0.45), width=2)
# draw junction handles (outer control points of curve)
faces_drawn = set() # keep track of faces, so don't draw same handles 2+ times
pts_outer,pts_inner = [],[]
for strip in strips:
bmf0,bmf1 = strip.end_faces()
p0,p1,p2,p3 = strip.curve.points()
if bmf0 not in faces_drawn:
if is_visible(p0): pts_outer += [Point_to_Point2D(p0)]
faces_drawn.add(bmf0)
if bmf1 not in faces_drawn:
if is_visible(p3): pts_outer += [Point_to_Point2D(p3)]
faces_drawn.add(bmf1)
if is_visible(p1): pts_inner += [Point_to_Point2D(p1)]
if is_visible(p2): pts_inner += [Point_to_Point2D(p2)]
pts_outer = [p for p in pts_outer if p]
pts_inner = [p for p in pts_inner if p]
self.rfcontext.drawing.draw2D_points(pts_outer, (1.00,1.00,1.00,1.0), radius=size_outer, border=border_outer, borderColor=(0.00,0.00,0.00,0.5))
self.rfcontext.drawing.draw2D_points(pts_inner, (0.25,0.25,0.25,0.8), radius=size_inner, border=border_inner, borderColor=(0.75,0.75,0.75,0.4))
gpustate.blend('ALPHA')
gpustate.depth_test('NONE')
gpustate.depth_mask(False)
draw(1.0, 1.0, False)
gpustate.depth_mask(True)
gpustate.depth_test('LESS_EQUAL')
@DrawCallbacks.on_draw('post2d')
@FSM.onlyinstate({'main', 'move handle', 'rotate', 'scale'})
def draw_post2d(self):
self.rfcontext.drawing.set_font_size(12)
Point_to_Point2D = self.rfcontext.Point_to_Point2D
text_draw2D = self.rfcontext.drawing.text_draw2D
for strip in self.strips:
strip = [f for f in strip if f.is_valid]
c = len(strip)
vs = [Point_to_Point2D(f.center()) for f in strip]
vs = [Vec2D(v) for v in vs if v]
if not vs: continue
ctr = sum(vs, Vec2D((0,0))) / len(vs)
text_draw2D('%d' % c, ctr+Vec2D((2,14)), color=(1,1,0,1), dropshadow=(0,0,0,0.5))