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

792 lines
33 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
from itertools import chain
from ..rftool import RFTool
from ..rfwidgets.rfwidget_default import RFWidget_Default_Factory
from ..rfwidgets.rfwidget_hidden import RFWidget_Hidden_Factory
from ...addon_common.common.drawing import (
CC_DRAW,
CC_2D_POINTS,
CC_2D_LINES, CC_2D_LINE_LOOP,
CC_2D_TRIANGLES, CC_2D_TRIANGLE_FAN,
)
from ...addon_common.common.profiler import profiler
from ...addon_common.common.maths import (
Point, Vec, Direction,
Point2D, Vec2D,
mid,
)
from ...addon_common.common import gpustate
from ...addon_common.common.fsm import FSM
from ...addon_common.common.globals import Globals
from ...addon_common.common.utils import iter_pairs
from ...addon_common.common.blender import tag_redraw_all
from ...addon_common.common.boundvar import BoundInt
from ...addon_common.common.drawing import DrawCallbacks
from ...config.options import options, themes, visualization
class Patches(RFTool):
name = 'Patches'
description = 'Fill holes in your topology'
icon = 'patches-icon.png'
help = 'patches.md'
shortcut = 'patches tool'
statusbar = '{{action alt1}} Toggle vertex as a corner\t{{increase count}} Increase segments\t{{decrease count}} Decrease Segments\t{{fill}} Create patch'
ui_config = 'patches_options.html'
RFWidget_Default = RFWidget_Default_Factory.create()
RFWidget_Move = RFWidget_Default_Factory.create(cursor='HAND')
RFWidget_Hidden = RFWidget_Hidden_Factory.create()
@RFTool.on_init
def init(self):
self.rfwidgets = {
'default': self.RFWidget_Default(self),
'hover': self.RFWidget_Move(self),
'hidden': self.RFWidget_Hidden(self),
}
self.rfwidget = None
self.corners = {}
self.crosses = None
self._var_angle = BoundInt('''options['patches angle']''', min_value=0, max_value=180)
self._var_crosses = BoundInt('''self.var_crosses''', min_value=1, max_value=500)
@RFTool.on_reset
def reset(self):
self.defer_recomputing = False
@property
def var_crosses(self):
if self.crosses is None: return 1
return self.crosses - 1
@var_crosses.setter
def var_crosses(self, v):
nv = max(1, int(v)+1)
if self.crosses == nv: return
self.crosses = nv
self._recompute()
def update_ui(self):
self._var_crosses.disabled = (self.crosses is None)
def filter_edge_selection(self, bme, no_verts_select=True, ratio=0.33):
if bme.select:
# edge is already selected
return True
bmv0, bmv1 = bme.verts
s0, s1 = bmv0.select, bmv1.select
if s0 and s1:
# both verts are selected, so return True
return True
if not s0 and not s1:
if no_verts_select:
# neither are selected, so return True by default
return True
else:
# return True if none are selected; otherwise return False
return self.rfcontext.none_selected()
# if mouse is at least a ratio of the distance toward unselected vert, return True
if s1: bmv0, bmv1 = bmv1, bmv0
p = self.actions.mouse
p0 = self.rfcontext.Point_to_Point2D(bmv0.co)
p1 = self.rfcontext.Point_to_Point2D(bmv1.co)
v01 = p1 - p0
l01 = v01.length
d01 = v01 / l01
dot = d01.dot(p - p0)
return dot / l01 > ratio
@RFTool.on_reset
@RFTool.on_target_change
def update(self):
if self.defer_recomputing: return
self.rfcontext.get_accel_visible()
self.crosses = None
self._recompute()
self.update_ui()
@FSM.on_state('main')
def main(self):
self.hovering_sel_edge,_ = self.rfcontext.accel_nearest2D_edge(max_dist=options['action dist'], selected_only=True)
self.hovering_sel_face,_ = self.rfcontext.accel_nearest2D_face(max_dist=options['action dist'], selected_only=True)
if self.hovering_sel_edge or self.hovering_sel_face:
self.set_widget('hover')
else:
self.set_widget('default')
if self.handle_inactive_passthrough(): return
if self.hovering_sel_edge or self.hovering_sel_face:
if self.actions.pressed('action'):
self.move_done_pressed = None
self.move_done_released = 'action'
self.move_cancelled = 'cancel'
return 'move'
if self.rfcontext.actions.pressed('action alt1'):
vert,_ = self.rfcontext.accel_nearest2D_vert(max_dist=10)
if not vert or not vert.select: return
if vert in self.shapes['corners']:
self.corners[vert] = False
else:
self.corners[vert] = not self.corners.get(vert, False)
self.update()
return
if self.rfcontext.actions.pressed('fill'):
self.fill_patch()
return
if self.rfcontext.actions.pressed('grab'):
self.move_done_pressed = 'confirm'
self.move_done_released = None
self.move_cancelled = 'cancel'
return 'move'
if self.rfcontext.actions.pressed('increase count'):
if self.crosses is not None:
self.crosses += 1
self._recompute()
if self.rfcontext.actions.pressed('decrease count'):
if self.crosses is not None and self.crosses > 2:
self.crosses -= 1
self._recompute()
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 paint', 'select paint add'}, unpress=False):
sel_only = self.actions.pressed('select paint')
self.actions.unpress()
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 single', 'select single add'}, unpress=False):
sel_only = self.actions.pressed('select single')
hovering_edge,_ = self.rfcontext.accel_nearest2D_edge(max_dist=options['action dist'])
if not sel_only and not hovering_edge: return
self.rfcontext.undo_push('select')
if sel_only: self.rfcontext.deselect_all()
if not hovering_edge: return
if sel_only or hovering_edge.select == False:
self.rfcontext.select(hovering_edge, supparts=False, only=False)
else:
self.rfcontext.deselect(hovering_edge)
return
if self.rfcontext.actions.pressed({'select smart', 'select smart add'}, unpress=False):
sel_only = self.rfcontext.actions.pressed('select smart')
self.rfcontext.actions.unpress()
self.rfcontext.undo_push('select smart')
selectable_edges = [e for e in self.rfcontext.visible_edges() if len(e.link_faces) < 2]
edge,_ = self.rfcontext.nearest2D_edge(edges=selectable_edges, max_dist=options['action dist'])
if not edge: return
#self.rfcontext.select_inner_edge_loop(edge, supparts=False, only=sel_only)
self.rfcontext.select_edge_loop(edge, supparts=False, only=sel_only)
@FSM.on_state('move', 'enter')
def move_enter(self):
self.sel_verts = self.rfcontext.get_selected_verts()
self.vis_accel = self.rfcontext.get_accel_visible()
self.vis_verts = self.rfcontext.accel_vis_verts
Point_to_Point2D = self.rfcontext.Point_to_Point2D
self.bmverts = [(bmv, Point_to_Point2D(bmv.co)) for bmv in self.sel_verts]
self.vis_bmverts = [(bmv, Point_to_Point2D(bmv.co)) for bmv in self.vis_verts if bmv and bmv not in self.sel_verts]
self.mousedown = self.rfcontext.actions.mouse
self.defer_recomputing = True
self.rfcontext.undo_push('move grabbed')
self.rfcontext.set_accel_defer(True)
self._timer = self.actions.start_timer(120)
if options['hide cursor on tweak']: self.set_widget('hidden')
@FSM.on_state('move')
def move_main(self):
released = self.rfcontext.actions.released
if self.move_done_pressed and self.rfcontext.actions.pressed(self.move_done_pressed):
self.defer_recomputing = False
self.update()
return 'main'
if self.actions.released(self.move_done_released, ignoredrag=True):
self.defer_recomputing = False
self.update()
return 'main'
if self.move_cancelled and self.rfcontext.actions.pressed('cancel'):
self.defer_recomputing = False
self.actions.unuse(self.move_done_released, ignoremods=True, ignoremulti=True)
self.rfcontext.undo_cancel()
return 'main'
if not self.actions.mousemove_stop: return
# if not self.rfcontext.actions.timer: return
# if self.actions.mouse_prev == self.actions.mouse: return
# # if not self.actions.mousemove: return
delta = Vec2D(self.actions.mouse - self.mousedown)
set2D_vert = self.rfcontext.set2D_vert
for bmv,xy in self.bmverts:
xy_updated = xy + delta
set2D_vert(bmv, xy_updated)
# # check if xy_updated is "close" to any visible verts (in image plane)
# # if so, snap xy_updated to vert position (in image plane)
# if options['polypen automerge']:
# for bmv1,xy1 in self.vis_bmverts:
# if (xy_updated - xy1).length < self.rfcontext.drawing.scale(10):
# set2D_vert(bmv, xy1)
# break
# else:
# set2D_vert(bmv, xy_updated)
# else:
# set2D_vert(bmv, xy_updated)
self.rfcontext.update_verts_faces(v for v,_ in self.bmverts)
self.rfcontext.dirty()
@FSM.on_state('move', 'exit')
def move_exit(self):
self._timer.done()
self.rfcontext.set_accel_defer(False)
@RFTool.dirty_when_done
def fill_patch(self):
if not self.previz: return
new_vert = self.rfcontext.new_vert_point
new_face = self.rfcontext.new_face
self.rfcontext.undo_push('fill')
for previz in self.previz:
verts,faces = previz['verts'],previz['faces']
verts = [(new_vert(v) if type(v) is Point else v) for v in verts]
for face in faces: new_face([verts[iv] for iv in face])
self.update()
def draw_previz(self, previz, poly_alpha=0.2):
point_to_point2D = self.rfcontext.Point_to_Point2D
line_color = themes['new']
poly_color = [line_color[0], line_color[1], line_color[2], line_color[3] * poly_alpha]
verts = [point_to_point2D(v if type(v) is Point else (v.co if v.is_valid else None)) for v in previz['verts']]
with Globals.drawing.draw(CC_2D_LINES) as draw:
draw.color(line_color)
for i0,i1 in previz['edges']:
v0,v1 = verts[i0],verts[i1]
if v0 and v1:
draw.vertex(v0)
draw.vertex(v1)
with Globals.drawing.draw(CC_2D_TRIANGLES) as draw:
draw.color(poly_color)
for f in previz['faces']:
coords = [verts[i] for i in f]
if all(coords):
co0 = coords[0]
for i in range(1, len(coords)-1):
draw.vertex(co0)
draw.vertex(coords[i])
draw.vertex(coords[i+1])
@DrawCallbacks.on_draw('post2d')
@FSM.onlyinstate('main')
def draw_postpixel(self):
point_to_point2D = self.rfcontext.Point_to_Point2D
self.rfcontext.drawing.set_font_size(12)
def get_pos(strips):
#xy = max((point_to_point2D(bmv.co) for strip in strips for bme in strip for bmv in bme.verts), key=lambda xy:xy.y+xy.x/2)
bmvs = [bmv for strip in strips for bme in strip for bmv in bme.verts]
vs = [point_to_point2D(bmv.co) for bmv in bmvs]
vs = [Vec2D(v) for v in vs if v]
if not vs: return None
xy = sum(vs, Vec2D((0,0))) / len(vs)
return xy+Vec2D((2,14))
def text_draw2D(s, strips):
if not strips: return
xy = get_pos(strips)
if not xy: return
self.rfcontext.drawing.text_draw2D(s, xy, color=(1,1,0,1), dropshadow=(0,0,0,0.5))
for rect_strips in self.shapes['rect']:
c0,c1,c2,c3 = map(len, rect_strips)
if c0==c2 and c1==c3:
s = 'rect: %dx%d' % (c0,c1)
text_draw2D(s, rect_strips)
else:
for strip in rect_strips:
s = 'bad rect: %d' % len(strip)
text_draw2D(s, [strip])
for I_strips in self.shapes['I']:
c = len(I_strips[0])
s = 'I: %d' % (c,)
text_draw2D(s, I_strips)
for L_strips in self.shapes['L']:
c0,c1 = map(len, L_strips)
s = 'L: %dx%d' % (c0,c1)
text_draw2D(s, L_strips)
for C_strips in self.shapes['C']:
c0,c1,c2 = map(len, C_strips)
if c0==c2:
s = 'C: %dx%d' % (c0,c1)
text_draw2D(s, C_strips)
else:
for strip in C_strips:
s = 'bad C: %d' % len(strip)
text_draw2D(s, [strip])
gpustate.blend('ALPHA')
CC_DRAW.stipple(pattern=[4,4])
CC_DRAW.point_size(4)
CC_DRAW.line_width(2)
for previz in self.previz: self.draw_previz(previz)
gpustate.blend('ALPHA')
CC_DRAW.stipple()
CC_DRAW.point_size(visualization['point size highlight'])
with Globals.drawing.draw(CC_2D_POINTS) as draw:
draw.color(visualization['point color highlight'])
for corner in self.shapes['corners']:
p = point_to_point2D(corner.co)
if p: draw.vertex(p)
def _clear_shapes(self):
self.shapes = {
'O': [], # special loop
'eye': [], # loops
'tri': [],
'rect': [],
'ngon': [],
'C': [], # strings
'L': [],
'I': [],
'else': [],
'corners': [],
}
self.previz = []
def _recompute(self):
min_angle = options['patches angle']
def nearest_sources_Point(p):
p,n,i,d = self.rfcontext.nearest_sources_Point(p)
return self.rfcontext.clamp_point_to_symmetry(p)
self._clear_shapes()
# remove old corners that are no longer valid or selected
self.corners = {v:corner for (v, corner) in self.corners.items() if v.is_valid and v.select}
##############################################
# find edges that could be part of a strip
edges = set(e for e in self.rfcontext.get_selected_edges() if len(e.link_faces) < 2)
###################
# find strips
remaining_edges = set(edges)
strips = []
neighbors = { e:[] for e in edges }
while remaining_edges:
strip = set()
working = { next(iter(remaining_edges)) }
while working:
edge = working.pop()
strip.add(edge)
remaining_edges.remove(edge)
v0,v1 = edge.verts
for e in chain(v0.link_edges, v1.link_edges):
if e not in remaining_edges: continue
bmv1 = edge.shared_vert(e)
if self.corners.get(bmv1, False): continue
bmv0 = edge.other_vert(bmv1)
bmv2 = e.other_vert(bmv1)
d10 = Direction(bmv0.co-bmv1.co)
d12 = Direction(bmv2.co-bmv1.co)
angle = math.degrees(math.acos(mid(-1,1,d10.dot(d12))))
if self.corners.get(bmv1, True) and angle < min_angle: continue
neighbors[edge].append(e)
neighbors[e].append(edge)
working.add(e)
strips += [strip]
##############################################
# order strips to find corners and O-shapes
nstrips = []
corners = dict()
for edges in strips:
if len(edges) == 1:
# single edge in strip
edge = next(iter(edges))
strip = [edge]
v0,v1 = edge.verts
nstrips.append(strip)
corners[v0] = corners.get(v0, []) + [strip]
corners[v1] = corners.get(v1, []) + [strip]
continue
end_edges = [edge for edge in edges if len(neighbors[edge])==1]
if not end_edges:
# could not find corners: O-shaped!
strip = [next(iter(edges))]
strip.append(next(iter(neighbors[strip[0]])))
remaining_edges = set(edges) - set(strip)
isbad = False
while remaining_edges:
next_edges = [edge for edge in neighbors[strip[-1]] if edge in remaining_edges]
if len(next_edges) != 1:
# unexpected number of edges found!
isbad = True
break
strip.append(next_edges[0])
remaining_edges.remove(next_edges[0])
if isbad: continue
self.shapes['O'].append(strip)
continue
strip = [end_edges[0]]
remaining_edges = set(edges) - set(strip)
isbad = False
while remaining_edges:
next_edges = [edge for edge in neighbors[strip[-1]] if edge in remaining_edges]
if len(next_edges) != 1:
# unexpected number of edges found
# see GitHub issue #481 (https://github.com/CGCookie/retopoflow/issues/481)
isbad = True
break
strip.append(next_edges[0])
remaining_edges.remove(next_edges[0])
if isbad: continue
v0 = strip[0].other_vert(strip[0].shared_vert(strip[1]))
v1 = strip[-1].other_vert(strip[-1].shared_vert(strip[-2]))
corners[v0] = corners.get(v0, []) + [strip]
corners[v1] = corners.get(v1, []) + [strip]
nstrips.append(strip)
strips = nstrips
##################################################################
# find all strings (I,L,C,else) and loops (cat,tri,rect,ngon)
# note: all corner verts with one strip are *not* in a loop
# ignore corners with 3+ strips
ignore_corners = {c for c in corners if len(corners[c]) > 2}
def align_strips(strips):
''' make sure that the edges at the end of adjacent strips share a vertex '''
if len(strips) == 1: return strips
strip0,strip1 = strips[:2]
if strip0[0].share_vert(strip1[0]) or strip0[0].share_vert(strip1[-1]): strip0.reverse()
assert strip0[-1].share_vert(strip1[0]) or strip0[-1].share_vert(strip1[-1])
for strip0,strip1 in zip(strips[:-1],strips[1:]):
if strip1[-1].share_vert(strip0[-1]): strip1.reverse()
assert strip1[0].share_vert(strip0[-1])
return strips
remaining_corners = set(corners.keys())
string_corners = set()
loop_corners = set()
strings_strips = list()
loops_strips = list(self.shapes['O'])
# find strips
while remaining_corners:
c = next((c for c in remaining_corners if len(corners[c]) == 1), None)
if not c: break
remaining_corners.remove(c)
string_corners.add(c)
string_strips = [corners[c][0]]
ignore = c in ignore_corners
while True:
s = string_strips[-1]
c = next((c for c in remaining_corners if s in corners[c]), None)
if not c: break
ignore |= c in ignore_corners
remaining_corners.remove(c)
string_corners.add(c)
if len(corners[c]) != 2: break
ns = next(ns for ns in corners[c] if ns != s)
string_strips.append(ns)
string_strips = align_strips(string_strips)
if ignore: continue
strings_strips.append(string_strips)
if len(string_strips) == 1:
self.shapes['I'].append(string_strips)
elif len(string_strips) == 2:
self.shapes['L'].append(string_strips)
elif len(string_strips) == 3:
self.shapes['C'].append(string_strips)
else:
self.shapes['else'].append(string_strips)
# find loops
while remaining_corners:
c = next(iter(remaining_corners))
remaining_corners.remove(c)
loop_corners.add(c)
loop_strips = [corners[c][0]]
ignore = c in ignore_corners
while True:
s = loop_strips[-1]
c = next((c for c in remaining_corners if s in corners[c]), None)
if not c: break
ignore |= c in ignore_corners
remaining_corners.remove(c)
loop_corners.add(c)
ns = next((ns for ns in corners[c] if ns != s), None)
if not ns: break
loop_strips.append(ns)
loop_strips = align_strips(loop_strips)
if ignore: continue
# make sure loop is actually closed
s0,s1 = loop_strips[0],loop_strips[-1]
shared_verts = sum(1 if e0.share_vert(e1) else 0 for e0 in s0 for e1 in s1)
if len(loop_strips) == 2 and shared_verts != 2: continue
if len(loop_strips) > 2 and shared_verts != 1: continue
loops_strips.append(loop_strips)
if len(loop_strips) == 2:
self.shapes['eye'].append(loop_strips)
elif len(loop_strips) == 3:
self.shapes['tri'].append(loop_strips)
elif len(loop_strips) == 4:
self.shapes['rect'].append(loop_strips)
else:
self.shapes['ngon'].append(loop_strips)
self.shapes['corners'] = (string_corners | loop_corners)
###################
# generate previz
def get_verts(strip, rev=False):
if len(strip) == 1: return list(strip[0].verts)
bmvs = [strip[0].nonshared_vert(strip[1])]
bmvs += [e0.shared_vert(e1) for e0,e1 in zip(strip[:-1], strip[1:])]
bmvs += [strip[-1].nonshared_vert(strip[-2])]
if rev: bmvs.reverse()
return bmvs
# rect
for shape in self.shapes['rect']:
s0,s1,s2,s3 = shape
if len(s0) != len(s2) or len(s1) != len(s3): continue # invalid rect
sv0,sv1,sv2,sv3 = get_verts(s0),get_verts(s1),get_verts(s2,True),get_verts(s3,True)
l0,l1 = len(sv0),len(sv1)
# make sure each strip is in the correct order
if sv0[-1] not in sv1: sv0.reverse()
if sv1[-1] not in sv2: sv1.reverse()
if sv2[-1] not in sv1: sv2.reverse()
if sv3[-1] not in sv2: sv3.reverse()
verts,edges,faces = [],[],[]
for i in range(l0):
l,r = sv0[i],sv2[i]
for j in range(l1):
t,b = sv1[j],sv3[j]
if i == 0: verts += [b]
elif i == l0-1: verts += [t]
elif j == 0: verts += [l]
elif j == l1-1: verts += [r]
else:
pi,pj = i / (l0-1), j / (l1-1)
lr = Vec(l.co)*(1-pj) + Vec(r.co)*pj
tb = Vec(b.co)*(1-pi) + Vec(t.co)*pi
verts += [nearest_sources_Point((lr+tb)/2.0)]
edges += [(i*l1+(j+0), i*l1+(j+1)) for i in range(1,l0-1) for j in range(l1-1)]
edges += [((i+0)*l1+j, (i+1)*l1+j) for j in range(1,l1-1) for i in range(l0-1)]
faces += [( (i+0)*l1+(j+0), (i+1)*l1+(j+0), (i+1)*l1+(j+1), (i+0)*l1+(j+1) ) for i in range(l0-1) for j in range(l1-1)]
self.previz += [{ 'type': 'rect', 'data': shape, 'verts': verts, 'edges': edges, 'faces': faces }]
for shape in self.shapes['L']:
s0,s1 = shape
sv0,sv1 = get_verts(s0),get_verts(s1)
l0,l1 = len(sv0),len(sv1)
# make sure each strip is in the correct order
if sv0[-1] not in sv1: sv0.reverse()
if sv1[0] not in sv0: sv1.reverse()
symmetry0 = self.rfcontext.get_point_symmetry(sv0[0].co)
symmetry1 = self.rfcontext.get_point_symmetry(sv1[-1].co)
if symmetry0 and symmetry1:
# both are at symmetry... artist is trying to fill a triangle
# we cannot do that, yet, so bail!
continue
off0,off1 = sv0[-1].co-sv0[0].co, sv1[-1].co-sv1[0].co
verts,edges,faces = [],[],[]
for i in range(l0):
for j in range(l1):
if i == l0-1: verts += [sv1[j]]
elif j == 0: verts += [sv0[i]]
else:
l,r = sv0[i].co,sv0[i].co+off1
t,b = sv1[j].co,sv1[j].co-off0
pi,pj = i / (l0-1), j / (l1-1)
lr = Vec(l)*(1-pj) + Vec(r)*pj
tb = Vec(b)*(1-pi) + Vec(t)*pi
point = nearest_sources_Point((lr+tb)/2.0)
if i == 0: point = self.rfcontext.snap_to_symmetry(point, symmetry0)
if j == l1-1: point = self.rfcontext.snap_to_symmetry(point, symmetry1)
verts += [point]
edges += [(i*l1+(j+0), i*l1+(j+1)) for i in range(l0-1) for j in range(l1-1)]
edges += [((i+0)*l1+j, (i+1)*l1+j) for j in range(1,l1) for i in range(l0-1)]
faces += [( (i+0)*l1+(j+0), (i+1)*l1+(j+0), (i+1)*l1+(j+1), (i+0)*l1+(j+1) ) for i in range(l0-1) for j in range(l1-1)]
self.previz += [{ 'type': 'L', 'data': shape, 'verts': verts, 'edges': edges, 'faces': faces }]
for shape in self.shapes['C']:
s0,s1,s2 = shape
if len(s0) != len(s2): continue # invalid C-shape
sv0,sv1,sv2 = get_verts(s0),get_verts(s1),get_verts(s2,True)
l0,l1 = len(sv0),len(sv1)
# make sure each strip is in the correct order
if sv0[-1] not in sv1: sv0.reverse()
if sv1[-1] not in sv2: sv1.reverse()
if sv2[-1] not in sv1: sv2.reverse()
symmetry0 = self.rfcontext.get_point_symmetry(sv0[0].co)
symmetry2 = self.rfcontext.get_point_symmetry(sv2[0].co)
use_symmetry = (symmetry0 == symmetry2)
#print([v.co for v in sv0])
#print([v.co for v in sv2])
#print(symmetry0, symmetry2, use_symmetry)
off0,off2 = sv0[0].co-sv0[-1].co, sv2[0].co-sv2[-1].co
verts,edges,faces = [],[],[]
for i in range(l0):
for j in range(l1):
if i == l0-1: verts += [sv1[j]]
elif j == 0: verts += [sv0[i]]
elif j == l1-1: verts += [sv2[i]]
else:
pi,pj = i / (l0-1), j / (l1-1)
off = off0*(1-pj)+off2*pj
l,r = sv0[i].co,sv2[i].co
t,b = sv1[j].co,sv1[j].co+off
lr = Vec(l)*(1-pj) + Vec(r)*pj
tb = Vec(b)*(1-pi) + Vec(t)*pi
point = nearest_sources_Point((lr+tb)/2.0)
if use_symmetry and i == 0: point = self.rfcontext.snap_to_symmetry(point, symmetry0)
verts += [point]
edges += [(i*l1+(j+0), i*l1+(j+1)) for i in range(l0-1) for j in range(l1-1)]
edges += [((i+0)*l1+j, (i+1)*l1+j) for j in range(1,l1-1) for i in range(l0-1)]
faces += [( (i+0)*l1+(j+0), (i+1)*l1+(j+0), (i+1)*l1+(j+1), (i+0)*l1+(j+1) ) for i in range(l0-1) for j in range(l1-1)]
self.previz += [{ 'type': 'C', 'data': shape, 'verts': verts, 'edges': edges, 'faces': faces }]
# TODO: check sides to make sure that we aren't creating geometry
# on a side that already has geometry!
for i0,shape0 in enumerate(self.shapes['I']):
sv0 = get_verts(shape0[0])
dir0 = Direction(sv0[0].co-sv0[-1].co)
best_sv1,best_dist = None,0
for i1,shape1 in enumerate(self.shapes['I']):
if i1 <= i0: continue
sv1 = get_verts(shape1[0])
dir1 = Direction(sv1[0].co-sv1[-1].co)
if len(sv0) != len(sv1): continue
if dir0.dot(dir1) < 0:
sv1 = list(reversed(sv1))
dir1.reverse()
# make sure the I strip are good candidates for bridging
# if math.degrees(dir0.angleBetween(dir1)) > 80: continue # make sure strips are parallel enough
if math.degrees(dir0.angleBetween(Direction(sv1[0].co-sv0[0].co))) < 45: continue
if math.degrees(dir1.angleBetween(Direction(sv0[0].co-sv1[0].co))) < 45: continue
dist = min((v0.co-v1.co).length for v0 in sv0 for v1 in sv1)
if best_sv1 and best_dist < dist: continue
best_sv1 = sv1
best_dist = dist
if not best_sv1: continue
sv1,dist = best_sv1,best_dist
avg0 = (sv0[0].co-sv0[-1].co).length / (len(sv0)-1)
avg1 = (sv1[0].co-sv1[-1].co).length / (len(sv1)-1)
l0 = len(sv0)
if getattr(self, 'crosses', None) is None:
self.crosses = max(2, math.floor(dist / max(avg0,avg1)))
l1 = self.crosses
verts,edges,faces = [],[],[]
for i in range(l0):
for j in range(l1):
if j == 0: verts += [sv0[i]]
elif j == l1-1: verts += [sv1[i]]
else:
pi,pj = i / (l0-1), j / (l1-1)
l,r = sv0[i].co,sv1[i].co
lr = Vec(l)*(1-pj) + Vec(r)*pj
verts += [nearest_sources_Point(lr)]
edges += [(i*l1+(j+0), i*l1+(j+1)) for i in range(l0) for j in range(l1-1)]
edges += [((i+0)*l1+j, (i+1)*l1+j) for j in range(1,l1-1) for i in range(l0-1)]
faces += [( (i+0)*l1+(j+0), (i+1)*l1+(j+0), (i+1)*l1+(j+1), (i+0)*l1+(j+1) ) for i in range(l0-1) for j in range(l1-1)]
self.previz += [{ 'type': 'I', 'data': shape0, 'verts': verts, 'edges': edges, 'faces': faces }]
if False:
print('')
print('patches info:')
print(' %d edges' % len(edges))
print(' %d strips' % len(strips))
print(' %d corners' % len(corners))
print(' %d string corners' % len(string_corners))
print(' %d loop corners' % len(loop_corners))
print(' %d strings' % len(strings_strips))
print(' %d loops' % len(loops_strips))
for d,k in [('loop','O'),('loop','eye'),('loop','tri'),('loop','rect'),('loop','ngon'),('string','I'),('string','L'),('string','C'),('string','else')]:
print(' %d %s-shaped %s' % (len(self.shapes[k]), k, d))
tag_redraw_all('Patches recompute')
self.update_ui()