2025-07-01

This commit is contained in:
2026-03-17 14:30:01 -06:00
parent f9a22056dd
commit 62b5978595
4579 changed files with 1257472 additions and 0 deletions
@@ -0,0 +1,660 @@
'''
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))
@@ -0,0 +1,413 @@
'''
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']
@@ -0,0 +1,17 @@
<details id="polystrips-options">
<summary>PolyStrips</summary>
<div class="contents">
<div class="labeled-input-text">
<label for="polystrips-cut-count">
Cut Count
</label>
<input type="text" value="self._var_cut_count" id="polystrips-cut-count" title="Number of cuts along selected strip">
</div>
<div class="labeled-input-text">
<label for="polystrips-scale-falloff">
Scale Falloff
</label>
<input id="polystrips-scale-falloff" type="text" value="BoundFloat('''options['polystrips scale falloff']''', min_value=0.25, max_value=4.0)" title="Controls how quickly control point scaling falls off">
</div>
</div>
</details>
@@ -0,0 +1,53 @@
'''
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 os
import re
import math
from itertools import chain
import bpy
from mathutils import Matrix
from ..rftool import RFTool
from ...addon_common.common.boundvar import BoundInt, BoundFloat
from ...addon_common.common.utils import delay_exec
from ...config.options import options
class PolyStrips_Props:
@RFTool.on_init
def init_props(self):
self._var_cut_count = BoundInt('''self.var_cut_count''', min_value=2, max_value=500)
self._var_scale_falloff = BoundFloat('''options['polystrips scale falloff']''', min_value=0.25, max_value=4.0)
@property
def var_cut_count(self):
return getattr(self, '_var_cut_count_value', 0)
@var_cut_count.setter
def var_cut_count(self, v):
if self.var_cut_count == v: return
self._var_cut_count_value = v
if self._var_cut_count.disabled: return
self.rfcontext.undo_push('change segment count', repeatable=True)
self.change_count(count=v)
@@ -0,0 +1,331 @@
'''
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)