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

758 lines
32 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 os
import math
import time
import random
from mathutils import Matrix
from ..rftool import RFTool
from ..rfwidget import RFWidget
from ...addon_common.common import gpustate
from ...addon_common.common.globals import Globals
from ...addon_common.common.debug import dprint
from ...addon_common.common.fsm import FSM
from ...addon_common.common.drawing import Drawing, DrawCallbacks
from ...addon_common.common.maths import Point, Normal, Vec2D, Plane, Vec
from ...addon_common.common.profiler import profiler
from ...addon_common.common.timerhandler import CallGovernor, StopwatchHandler
from ...addon_common.common.utils import iter_pairs
from ...addon_common.common import blender_preferences as bprefs
from ...addon_common.common.blender import tag_redraw_all
from ...config.options import options
from .contours_ops import Contours_Ops
from .contours_props import Contours_Props
from .contours_utils import (
find_loops,
find_strings,
loop_plane, loop_radius,
Contours_Loop,
Contours_Utils,
)
from ..rfwidgets.rfwidget_default import RFWidget_Default_Factory
from ..rfwidgets.rfwidget_linecut import RFWidget_LineCut_Factory
class Contours(RFTool, Contours_Ops, Contours_Props, Contours_Utils):
name = 'Contours'
description = 'Retopologize cylindrical forms, like arms and legs'
icon = 'contours-icon.png'
help = 'contours.md'
shortcut = 'contours tool'
statusbar = '{{insert}} Insert contour\t{{increase count}} Increase segments\t{{decrease count}} Decrease segments\t{{fill}} Bridge'
ui_config = 'contours_options.html'
RFWidget_Default = RFWidget_Default_Factory.create()
RFWidget_Move = RFWidget_Default_Factory.create(cursor='HAND')
RFWidget_LineCut = RFWidget_LineCut_Factory.create('Contours line cut')
@RFTool.on_init
def init(self):
self.rfwidgets = {
'default': self.RFWidget_Default(self),
'cut': self.RFWidget_LineCut(self),
'hover': self.RFWidget_Move(self),
}
self.clear_widget()
@RFTool.on_reset
def reset(self):
self.show_cut = False
self.show_arrows = False
self.pts = []
self.cut_pts = []
self.connected = False
self.cuts = []
self.crawl_viz = [] # for debugging
self.hovering_sel_edge = None
self.ui_initial_count = None
@RFTool.on_target_change
#@FSM.onlyinstate('main')
def update_target(self):
self.sel_edges = set(self.rfcontext.get_selected_edges())
#sel_faces = self.rfcontext.get_selected_faces()
# disable initial count input box if anything is selected
if not self.ui_initial_count:
self.ui_initial_count = self.document.body.getElementById('contours-initial-count')
if self.ui_initial_count:
self.ui_initial_count.disabled = bool(self.sel_edges)
# find verts along selected loops and strings
sel_loops = find_loops(self.sel_edges)
sel_strings = find_strings(self.sel_edges)
# filter out any loops or strings that are in the middle of a selected patch
def in_middle(bmvs, is_loop):
return any(len(bmv0.shared_edge(bmv1).link_faces) > 1 for bmv0,bmv1 in iter_pairs(bmvs, is_loop))
sel_loops = [loop for loop in sel_loops if not in_middle(loop, True)]
sel_strings = [string for string in sel_strings if not in_middle(string, False)]
# filter out long loops that wrap around patches, sharing edges with other strings
bmes = {bmv0.shared_edge(bmv1) for string in sel_strings for bmv0,bmv1 in iter_pairs(string,False)}
sel_loops = [loop for loop in sel_loops if not any(bmv0.shared_edge(bmv1) in bmes for bmv0,bmv1 in iter_pairs(loop,True))]
mirror_mod = self.rfcontext.rftarget.mirror_mod
symmetry_threshold = mirror_mod.symmetry_threshold
def get_string_length(string):
nonlocal mirror_mod, symmetry_threshold
c = len(string)
if c == 0: return 0
touches_mirror = False
(x0,y0,z0),(x1,y1,z1) = string[0].co,string[-1].co
if mirror_mod.x:
if abs(x0) < symmetry_threshold or abs(x1) < symmetry_threshold:
c = (c - 1) * 2
touches_mirror = True
if mirror_mod.y:
if abs(y0) < symmetry_threshold or abs(y1) < symmetry_threshold:
c = (c - 1) * 2
touches_mirror = True
if mirror_mod.z:
if abs(z0) < symmetry_threshold or abs(z1) < symmetry_threshold:
c = (c - 1) * 2
touches_mirror = True
if not touches_mirror: c -= 1
return c
self.loops_data = [{
'loop': loop,
'plane': loop_plane(loop),
'count': len(loop),
'radius': loop_radius(loop),
'cl': Contours_Loop(loop, True),
} for loop in sel_loops]
self.strings_data = [{
'string': string,
'plane': loop_plane(string),
'count': get_string_length(string),
'cl': Contours_Loop(string, False),
} for string in sel_strings]
self.sel_loops = [Contours_Loop(loop, True) for loop in sel_loops]
self._var_cut_count.disabled = True
if len(self.loops_data) == 1 and len(self.strings_data) == 0:
self._var_cut_count_value = self.loops_data[0]['count']
self._var_cut_count.disabled = any(len(e.link_edges)!=2 for e in self.loops_data[0]['loop'])
if len(self.strings_data) == 1 and len(self.loops_data) == 0:
self._var_cut_count_value = self.strings_data[0]['count']
self._var_cut_count.disabled = False
@FSM.on_state('main')
def main(self):
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_edge,_ = self.rfcontext.accel_nearest2D_edge(max_dist=options['action dist'], selected_only=True)
if self.actions.using_onlymods('insert'):
self.set_widget('cut')
elif self.hovering_sel_edge:
self.set_widget('hover')
else:
self.set_widget('default')
if self.handle_inactive_passthrough(): return
if self.actions.pressed('grab'):
''' grab for translations '''
self.move_done_pressed = 'confirm'
self.move_done_released = None
return 'grab'
if self.hovering_sel_edge:
if self.actions.pressed('action'):
# return self.action_setup()
self.move_done_pressed = None
self.move_done_released = 'action'
return 'grab'
if self.rfcontext.actions.pressed('rotate plane'):
''' rotation of loops (NOT strips) about plane normal '''
return 'rotate plane'
if self.rfcontext.actions.pressed('rotate screen'):
''' screen-space rotation of loops about plane origin '''
return 'rotate screen'
if self.rfcontext.actions.pressed('fill'):
self.fill()
return
if self.rfcontext.actions.pressed({'increase count', 'decrease count'}, unpress=False):
delta = 1 if self.rfcontext.actions.pressed('increase count') else -1
self.rfcontext.undo_push('change segment count', repeatable=True)
self.change_count(delta=delta)
return
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(
{'edge'},
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 path add'}):
return self.rfcontext.select_path(
{'edge'},
fn_filter_bmelem=self.filter_edge_selection,
kwargs_select={'supparts': False},
)
if self.actions.pressed({'select single', 'select single add'}, unpress=False):
self.sel_only = self.actions.pressed('select single')
self.actions.unpress()
self.select_single()
return
if self.rfcontext.actions.pressed({'select smart', 'select smart add'}, unpress=False):
self.select_single.cancel()
sel_only = self.rfcontext.actions.pressed('select smart')
self.rfcontext.actions.unpress()
self.rfcontext.undo_push('select smart')
edge,_ = self.rfcontext.accel_nearest2D_edge(max_dist=10)
if not edge:
if sel_only: self.rfcontext.deselect_all()
return
self.rfcontext.select_edge_loop(edge, only=sel_only, supparts=False)
return
@StopwatchHandler.delayed(time_delay=0.1)
def select_single(self):
bme,_ = self.rfcontext.accel_nearest2D_edge(max_dist=options['select dist'])
if not bme and not self.sel_only: return
self.rfcontext.undo_push('select')
if self.sel_only: self.rfcontext.deselect_all()
if not bme: return
if bme.select: self.rfcontext.deselect(bme, subparts=False)
else: self.rfcontext.select(bme, supparts=False, only=self.sel_only)
self.rfcontext.dirty(selectionOnly=True)
@FSM.on_state('rotate plane', 'can enter')
def rotateplane_can_enter(self):
sel_edges = self.rfcontext.get_selected_edges()
sel_loops = find_loops(sel_edges)
if not sel_loops:
if self.strings_data:
self.rfcontext.alert_user('Can only plane-rotate complete loops that do not cross the symmetry plane')
else:
self.rfcontext.alert_user('Could not find valid loops to plane-rotate')
return False
self.move_cloops = [Contours_Loop(loop, True) for loop in sel_loops]
self.move_verts = [[bmv for bmv in cloop.verts] for cloop in self.move_cloops]
self.move_pts = [[Point(pt) for pt in cloop.pts] for cloop in self.move_cloops]
self.move_dists = [list(cloop.dists) for cloop in self.move_cloops]
self.move_circumferences = [cloop.circumference for cloop in self.move_cloops]
self.move_origins = [cloop.plane.o for cloop in self.move_cloops]
self.move_proj_dists = [list(cloop.proj_dists) for cloop in self.move_cloops]
self.move_cuts = []
for cloop in self.move_cloops:
xy = self.rfcontext.Point_to_Point2D(cloop.plane.o)
ray = self.rfcontext.Point2D_to_Ray(xy)
crawl = self.rfcontext.plane_intersection_crawl(ray, cloop.plane, walk_to_plane=True)
if not crawl:
# dprint('could not crawl around sources for loop')
pass
self.move_cuts += [None]
continue
crawl_pts = [c for _,c,_ in crawl]
connected = cloop.connected # XXX why was `crawl[0][0] is not None` here?
crawl_pts,connected = self.rfcontext.clip_pointloop(crawl_pts, connected)
if not crawl_pts or connected != cloop.connected:
# dprint('could not clip loop to symmetry')
pass
self.move_cuts += [None]
continue
cl_cut = Contours_Loop(crawl_pts, connected)
cl_cut.align_to(cloop)
self.move_cuts += [cl_cut]
if not any(self.move_cuts):
self.rfcontext.alert_user('Could not find valid loops to plane-rotate')
# dprint('Found no loops to shift')
pass
return False
@FSM.on_state('rotate plane', 'enter')
def rotateplane_enter(self):
self.rot_axis = Vec((0,0,0))
self.rot_origin = Point.average(cut.get_origin() for cut in self.move_cuts if cut)
self.shift_about = self.rfcontext.Point_to_Point2D(self.rot_origin)
for cut in self.move_cuts:
if not cut: continue
a = cut.get_normal()
o = cut.get_origin()
if self.rot_axis.dot(a) < 0: a = -a
self.rot_axis += a
self.rot_axis.normalize()
p0 = next(iter(cut.get_origin() for cut in self.move_cuts if cut))
p1 = p0 + self.rot_axis * 0.001
self.rot_axis2D = (self.rfcontext.Point_to_Point2D(p1) - self.rfcontext.Point_to_Point2D(p0))
self.rot_axis2D.normalize()
self.rot_perp2D = Vec2D((self.rot_axis2D.y, -self.rot_axis2D.x))
# print(self.rot_axis, self.rot_axis2D, self.rot_perp2D)
self.rfcontext.undo_push('rotate plane contours')
self.mousedown = self.rfcontext.actions.mouse
self._timer = self.actions.start_timer(120.0)
self.rfcontext.split_target_visualization(verts=[v for vs in self.move_verts for v in vs])
self.rfcontext.set_accel_defer(True)
@FSM.on_state('rotate plane')
# @profiler.function
def rotateplane_main(self):
if self.rfcontext.actions.pressed('confirm'):
return 'main'
if self.rfcontext.actions.pressed('cancel'):
self.rfcontext.undo_cancel()
return 'main'
if self.rfcontext.actions.pressed('rotate screen'):
self.rfcontext.undo_cancel()
return 'rotate screen'
if not self.actions.mousemove_stop: return
# # only update cut on timer events and when mouse has moved
# if not self.rfcontext.actions.timer: return
delta = Vec2D(self.rfcontext.actions.mouse - self.mousedown)
shift_offset = self.rfcontext.drawing.unscale(self.rot_perp2D.dot(delta)) / 1000
up_dir = self.rfcontext.Vec_up()
raycast,project = self.rfcontext.raycast_sources_Point2D,self.rfcontext.Point_to_Point2D
for i_cloop in range(len(self.move_cloops)):
cloop = self.move_cloops[i_cloop]
cl_cut = self.move_cuts[i_cloop]
if not cl_cut: continue
shift_dir = 1 if cl_cut.get_normal().dot(self.rot_axis) > 0 else -1
verts = self.move_verts[i_cloop]
dists = self.move_dists[i_cloop]
proj_dists = self.move_proj_dists[i_cloop]
circumference = self.move_circumferences[i_cloop]
lc = cl_cut.circumference
shft = (cl_cut.offset + shift_offset * shift_dir * lc) % lc
ndists = [shft] + [0.999 * lc * (d/circumference) for d in dists]
i,dist = 0,ndists[0]
l = len(ndists)-1 if cloop.connected else len(ndists)
for c0,c1 in cl_cut.iter_pts(repeat=True):
d = (c1-c0).length
while dist - d <= 0:
# create new vert between c0 and c1
p = c0 + (c1 - c0) * (dist / d) + (cloop.plane.n * proj_dists[i])
p,n,_,_ = self.rfcontext.nearest_sources_Point(p)
verts[i].co = p
verts[i].normal = n
i += 1
if i == l: break
dist += ndists[i]
dist -= d
if i == l: break
self.rfcontext.update_verts_faces(verts)
self.rfcontext.dirty()
@FSM.on_state('rotate plane', 'exit')
def rotateplane_exit(self):
self._timer.done()
self.rfcontext.clear_split_target_visualization()
self.rfcontext.set_accel_defer(False)
tag_redraw_all('Contours finish rotate')
def action_setup(self):
sel_edges = self.rfcontext.get_selected_edges()
sel_loops = find_loops(sel_edges)
sel_strings = find_strings(sel_edges, min_length=2)
if not sel_loops or sel_strings: return
# prefer to move loops over strings
if sel_loops: self.move_cloops = [Contours_Loop(loop, True) for loop in sel_loops]
else: self.move_cloops = [Contours_Loop(string, False) for string in sel_strings]
self.move_verts = [[bmv for bmv in cloop.verts] for cloop in self.move_cloops]
self.move_pts = [[Point(pt) for pt in cloop.pts] for cloop in self.move_cloops]
self.move_dists = [list(cloop.dists) for cloop in self.move_cloops]
self.move_circumferences = [cloop.circumference for cloop in self.move_cloops]
self.move_origins = [cloop.plane.o for cloop in self.move_cloops]
self.move_orig_origins = [Point(p) for p in self.move_origins]
self.move_proj_dists = [list(cloop.proj_dists) for cloop in self.move_cloops]
#self.grab_along = self.rfcontext.Point_to_Point2D(sum(self.move_origins, Vec((0,0,0))) / len(self.move_origins))
#self.rotate_start = math.atan2(self.rotate_about.y - self.mousedown.y, self.rotate_about.x - self.mousedown.x)
self.mousedown = self.actions.mouse
self.move_prevmouse = None
return self.rfcontext.setup_action()
def action_callback(self, val):
pass
@FSM.on_state('grab', 'can enter')
def grab_can_enter(self):
sel_edges = self.rfcontext.get_selected_edges()
sel_loops = find_loops(sel_edges)
sel_strings = find_strings(sel_edges, min_length=2)
return bool(sel_loops or sel_strings)
@FSM.on_state('grab', 'enter')
def grab_enter(self):
sel_edges = self.rfcontext.get_selected_edges()
sel_loops = find_loops(sel_edges)
sel_strings = find_strings(sel_edges, min_length=2)
# prefer to move loops over strings
if sel_loops: self.move_cloops = [Contours_Loop(loop, True) for loop in sel_loops]
else: self.move_cloops = [Contours_Loop(string, False) for string in sel_strings]
self.move_verts = [[bmv for bmv in cloop.verts] for cloop in self.move_cloops]
self.move_pts = [[Point(pt) for pt in cloop.pts] for cloop in self.move_cloops]
self.move_dists = [list(cloop.dists) for cloop in self.move_cloops]
self.move_circumferences = [cloop.circumference for cloop in self.move_cloops]
self.move_origins = [cloop.plane.o for cloop in self.move_cloops]
self.move_orig_origins = [Point(p) for p in self.move_origins]
self.move_proj_dists = [list(cloop.proj_dists) for cloop in self.move_cloops]
self.rfcontext.undo_push('grab contours')
#self.grab_along = self.rfcontext.Point_to_Point2D(sum(self.move_origins, Vec((0,0,0))) / len(self.move_origins))
#self.rotate_start = math.atan2(self.rotate_about.y - self.mousedown.y, self.rotate_about.x - self.mousedown.x)
self.grab_opts = {
'mousedown': self.actions.mouse,
'timer': self.actions.start_timer(120.0),
}
self.rfcontext.split_target_visualization(verts=[v for vs in self.move_verts for v in vs])
self.rfcontext.set_accel_defer(True)
@FSM.on_state('grab')
# @profiler.function
def grab(self):
opts = self.grab_opts
if self.rfcontext.actions.pressed(self.move_done_pressed):
return 'main'
if self.rfcontext.actions.released(self.move_done_released, ignoredrag=True):
return 'main'
if self.rfcontext.actions.pressed('cancel'):
self.rfcontext.actions.unuse(self.move_done_released, ignoremods=True, ignoremulti=True)
self.rfcontext.undo_cancel()
return 'main'
if not self.actions.mousemove_stop: return
# # only update cut on timer events and when mouse has moved
# if not self.rfcontext.actions.timer: return
delta = Vec2D(self.actions.mouse - opts['mousedown'])
# print(f'contours.grab: {delta}')
# self.crawl_viz = []
raycast,project = self.rfcontext.raycast_sources_Point2D,self.rfcontext.Point_to_Point2D
for i_cloop in range(len(self.move_cloops)):
cloop = self.move_cloops[i_cloop]
verts = self.move_verts[i_cloop]
pts = self.move_pts[i_cloop]
dists = self.move_dists[i_cloop]
origin = self.move_origins[i_cloop]
proj_dists = self.move_proj_dists[i_cloop]
circumference = self.move_circumferences[i_cloop]
depth = self.rfcontext.Point_to_depth(origin)
if depth is None: continue
origin2D_new = self.rfcontext.Point_to_Point2D(origin) + delta
origin_new = self.rfcontext.Point2D_to_Point(origin2D_new, depth)
plane_new = Plane(origin_new, cloop.plane.n)
ray_new = self.rfcontext.Point2D_to_Ray(origin2D_new)
crawl = self.rfcontext.plane_intersection_crawl(ray_new, plane_new, walk_to_plane=True)
if not crawl: continue
crawl_pts = [c for _,c,_ in crawl]
# self.crawl_viz += [crawl_pts]
connected = crawl[0][0] is not None
crawl_pts,nconnected = self.rfcontext.clip_pointloop(crawl_pts, connected)
connected = nconnected
if not crawl_pts or connected != cloop.connected: continue
cl_cut = Contours_Loop(crawl_pts, connected)
cl_cut.align_to(cloop)
lc = cl_cut.circumference
ndists = [cl_cut.offset] + [0.999 * lc * (d/circumference) for d in dists]
i,dist = 0,ndists[0]
l = len(ndists)-1 if cloop.connected else len(ndists)
for c0,c1 in cl_cut.iter_pts(repeat=True):
d = max(0.000001, (c1-c0).length)
while dist - d <= 0:
# create new vert between c0 and c1
p = c0 + (c1 - c0) * (dist / d) + (cloop.plane.n * proj_dists[i])
p,_,_,_ = self.rfcontext.nearest_sources_Point(p)
verts[i].co = p
i += 1
if i == l: break
dist += ndists[i]
dist -= d
if i == l: break
self.rfcontext.update_verts_faces(verts)
self.rfcontext.dirty()
@FSM.on_state('grab', 'exit')
def grab_exit(self):
self.grab_opts['timer'].done()
self.rfcontext.set_accel_defer(False)
self.rfcontext.clear_split_target_visualization()
tag_redraw_all('Contours finish grab')
@FSM.on_state('rotate screen', 'can enter')
def rotatescreen_can_enter(self):
sel_edges = self.rfcontext.get_selected_edges()
sel_loops = find_loops(sel_edges)
sel_strings = find_strings(sel_edges, min_length=2)
return sel_loops or sel_strings
@FSM.on_state('rotate screen', 'enter')
def rotatescreen_enter(self):
sel_edges = self.rfcontext.get_selected_edges()
sel_loops = find_loops(sel_edges)
sel_strings = find_strings(sel_edges, min_length=2)
# prefer to move loops over strings
if sel_loops: self.move_cloops = [Contours_Loop(loop, True) for loop in sel_loops]
else: self.move_cloops = [Contours_Loop(string, False) for string in sel_strings]
self.move_verts = [[bmv for bmv in cloop.verts] for cloop in self.move_cloops]
self.move_pts = [[Point(pt) for pt in cloop.pts] for cloop in self.move_cloops]
self.move_dists = [list(cloop.dists) for cloop in self.move_cloops]
self.move_circumferences = [cloop.circumference for cloop in self.move_cloops]
self.move_origins = [cloop.plane.o for cloop in self.move_cloops]
self.move_proj_dists = [list(cloop.proj_dists) for cloop in self.move_cloops]
self.rfcontext.undo_push('rotate screen contours')
self.mousedown = self.rfcontext.actions.mouse
self.rotate_about = self.rfcontext.Point_to_Point2D(sum(self.move_origins, Vec((0,0,0))) / len(self.move_origins))
self.rotate_start = math.atan2(self.rotate_about.y - self.mousedown.y, self.rotate_about.x - self.mousedown.x)
self._timer = self.actions.start_timer(120.0)
self.rfcontext.split_target_visualization(verts=[v for vs in self.move_verts for v in vs])
self.rfcontext.set_accel_defer(True)
@FSM.on_state('rotate screen')
# @profiler.function
def rotatescreen_main(self):
if self.rfcontext.actions.pressed('confirm'):
return 'main'
if self.rfcontext.actions.pressed('cancel'):
self.rfcontext.undo_cancel()
return 'main'
if self.rfcontext.actions.pressed('rotate plane'):
self.rfcontext.undo_cancel()
return 'rotate plane'
if not self.actions.mousemove_stop: return
# # only update cut on timer events and when mouse has moved
# if not self.rfcontext.actions.timer: return
delta = Vec2D(self.rfcontext.actions.mouse - self.rotate_about)
rotate = (math.atan2(delta.y, delta.x) - self.rotate_start + math.pi) % (math.pi * 2)
raycast,project = self.rfcontext.raycast_sources_Point2D,self.rfcontext.Point_to_Point2D
for i_cloop in range(len(self.move_cloops)):
cloop = self.move_cloops[i_cloop]
verts = self.move_verts[i_cloop]
pts = self.move_pts[i_cloop]
dists = self.move_dists[i_cloop]
origin = self.move_origins[i_cloop]
proj_dists = self.move_proj_dists[i_cloop]
circumference = self.move_circumferences[i_cloop]
origin2D = self.rfcontext.Point_to_Point2D(origin)
ray = self.rfcontext.Point_to_Ray(origin)
rmat = Matrix.Rotation(rotate, 4, -ray.d)
normal = rmat @ cloop.plane.n
plane = Plane(cloop.plane.o, normal)
ray = self.rfcontext.Point2D_to_Ray(origin2D)
crawl = self.rfcontext.plane_intersection_crawl(ray, plane, walk_to_plane=True)
if not crawl: continue
crawl_pts = [c for _,c,_ in crawl]
connected = crawl[0][0] is not None
crawl_pts,connected = self.rfcontext.clip_pointloop(crawl_pts, connected)
if not crawl_pts or connected != cloop.connected: continue
cl_cut = Contours_Loop(crawl_pts, connected)
cl_cut.align_to(cloop)
lc = cl_cut.circumference
ndists = [cl_cut.offset] + [0.999 * lc * (d/circumference) for d in dists]
i,dist = 0,ndists[0]
l = len(ndists)-1 if cloop.connected else len(ndists)
for c0,c1 in cl_cut.iter_pts(repeat=True):
d = (c1-c0).length
d = max(0.00000001, d)
while dist - d <= 0:
# create new vert between c0 and c1
p = c0 + (c1 - c0) * (dist / d) + (cloop.plane.n * proj_dists[i])
p,_,_,_ = self.rfcontext.nearest_sources_Point(p)
verts[i].co = p
i += 1
if i == l: break
dist += ndists[i]
dist -= d
if i == l: break
self.rfcontext.update_verts_faces(verts)
self.rfcontext.dirty()
@FSM.on_state('rotate screen', 'exit')
def rotatescreen_exit(self):
self._timer.done()
self.rfcontext.clear_split_target_visualization()
self.rfcontext.set_accel_defer(False)
tag_redraw_all('Contours finish rotate')
@RFWidget.on_action('Contours line cut')
def new_line(self):
xy0,xy1 = self.rfwidgets['cut'].line2D
if not xy0 or not xy1: return
if (xy1-xy0).length < 0.001: return
xy01 = xy0 + (xy1-xy0) / 2
plane = self.rfcontext.Point2D_to_Plane(xy0, xy1)
ray = self.rfcontext.Point2D_to_Ray(xy01)
self.new_cut(ray, plane, walk_to_plane=False, check_hit=xy01)
@DrawCallbacks.on_draw('post2d')
@FSM.onlyinstate('rotate screen')
def draw_post2d_rotate_screenspace(self):
gpustate.blend('ALPHA')
Globals.drawing.draw2D_line(
self.rotate_about,
self.rfcontext.actions.mouse,
(1.0, 1.0, 0.1, 1.0), color1=(1.0, 1.0, 0.1, 0.0),
width=2, stipple=[2, 2]
)
@DrawCallbacks.on_draw('post2d')
@FSM.onlyinstate('rotate plane')
def draw_post2d_rotate_plane(self):
gpustate.blend('ALPHA')
Globals.drawing.draw2D_line(
self.shift_about + self.rot_axis2D * 1000,
self.shift_about - self.rot_axis2D * 1000,
(0.1, 1.0, 1.0, 1.0), color1=(0.1, 1.0, 1.0, 0.0),
width=2, stipple=[2,2],
)
@DrawCallbacks.on_draw('post2d')
@FSM.onlyinstate('grab')
def draw_post2d_grab(self):
project = self.rfcontext.Point_to_Point2D
intersect = self.rfcontext.raycast_sources_Point2D
delta = Vec2D(self.actions.mouse - self.grab_opts['mousedown'])
c0_good, c1_good = (1.0, 0.1, 1.0, 0.5), (1.0, 0.1, 1.0, 0.0)
c0_bad, c1_bad = (1.0, 0.1, 0.1, 1.0), (1.0, 0.1, 0.1, 0.0)
gpustate.blend('ALPHA')
for o in self.move_origins:
p0, p1 = project(o), project(o) + delta
_p,_,_,_ = intersect(p1)
Globals.drawing.draw2D_line(
p0, p1,
(c0_good if _p else c0_bad), color1=(c1_good if _p else c1_bad),
width=2, stipple=[2,2],
)
@DrawCallbacks.on_draw('post2d')
def draw_post2d(self):
point_to_point2d = self.rfcontext.Point_to_Point2D
is_visible = lambda p: self.rfcontext.is_visible(p, occlusion_test_override=True)
up = self.rfcontext.Vec_up()
size_to_size2D = self.rfcontext.size_to_size2D
text_draw2D = self.rfcontext.drawing.text_draw2D
self.rfcontext.drawing.set_font_size(12)
bmv_count = set()
bmv_count_loops = {}
bmv_count_strings = {}
for loop_data in self.loops_data:
loop = loop_data['loop']
radius = loop_data['radius']
count = loop_data['count']
plane = loop_data['plane']
cl = loop_data['cl']
# draw segment count label
loop = [vert for vert in loop if vert.is_valid]
loop = [(vert, point_to_point2d(vert.co)) for vert in loop if is_visible(vert.co)]
if loop:
bmv = max(loop, key=lambda bmvp2d:bmvp2d[1].y)[0]
if bmv not in bmv_count_loops: bmv_count_loops[bmv] = []
bmv_count_loops[bmv].append(count)
bmv_count.add(bmv)
for string_data in self.strings_data:
string = string_data['string']
count = string_data['count']
plane = string_data['plane']
# draw segment count label
string = [vert for vert in string if vert.is_valid]
string = [(vert, point_to_point2d(vert.co)) for vert in string if is_visible(vert.co)]
if string:
bmv = max(string, key=lambda bmvp2d:bmvp2d[1].y)[0]
if bmv not in bmv_count_strings: bmv_count_strings[bmv] = []
bmv_count_strings[bmv].append(count)
bmv_count.add(bmv)
for bmv in bmv_count:
counts_loops = sorted(bmv_count_loops.get(bmv, []))
counts_strings = sorted(bmv_count_strings.get(bmv, []))
s_loops = ','.join(map(str, counts_loops))
s_strings = ','.join(map(str, counts_strings))
xy = point_to_point2d(bmv.co)
xy.y += 10
if s_loops:
text_draw2D('O: ' + s_loops, xy, color=(1,1,0,1), dropshadow=(0,0,0,0.5))
xy.y += 10
if s_strings:
text_draw2D('C: ' + s_strings, xy, color=(0,1,1,1), dropshadow=(0,0,0,0.5))