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

380 lines
14 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/>.
'''
class Contours_Utils:
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
#def get_count(self): return
import math
from itertools import chain
from mathutils import Vector, Quaternion
import bpy
from ..rfmesh.rfmesh import RFVert
from ...addon_common.common.utils import iter_pairs, max_index
from ...addon_common.common.hasher import hash_cycle
from ...addon_common.common.maths import (
Point, Vec, Normal, Direction,
Point2D, Vec2D,
Plane, Frame,
)
from ...addon_common.common.profiler import profiler
def to_point(item):
t = type(item)
if t is RFVert: return item.co
if t is Point or t is Vector or t is Vec: return item
if t is tuple: return Point(item)
return item.co
def next_edge_in_string(edge0, vert01, ignore_two_faced=False):
faces0 = edge0.link_faces
edges1 = vert01.link_edges
# ignore edge0
edges1 = [edge for edge in edges1 if edge != edge0]
if ignore_two_faced:
# ignore edges that have two faces already
edges1 = [edge for edge in edges1 if len(edge.link_faces) <= 1]
# ignore edges that share face with previous edge
edges1 = [edge for edge in edges1 if not faces0 or not any(f in faces0 for f in edge.link_faces)]
return edges1[0] if len(edges1) == 1 else []
def find_loops(edges):
if not edges: return []
edges = set(edges)
touched, loops = set(), []
def crawl(v0, edge01):
nonlocal edges, touched, loops
vert_list = []
while True:
# ... -- v0 -- edge01 -- v1 -- edge12 -- ...
# > came-^-from-^ ^-going-^-to >
vert_list.append(v0)
touched.add(edge01)
v1 = edge01.other_vert(v0)
if v1 == vert_list[0]:
# found a loop!
loops.append(vert_list)
return
next_edges = [e for e in v1.link_edges if e in edges and e != edge01]
if not next_edges:
# could not find a loop
return
if len(next_edges) == 1: edge12 = next_edges[0]
else: edge12 = next_edge_in_string(edge01, v1)
if not edge12 or edge12 in touched or edge12 not in edges:
# could not find a loop
return
v0, edge01 = v1, edge12
for edge in edges:
if edge in touched: continue
crawl(edge.verts[0], edge)
return loops
def find_parallel_loops(loop, wrap=True):
def find_opposite_loop(loop, bmf):
# find edge loop on opposite side of given face from given edge
bmv0,bmv1 = loop[:2]
bme01 = bmv0.shared_edge(bmv1)
bme03 = next((bme for bme in bmf.neighbor_edges(bme01) if bmv0 in bme.verts), None)
if not bme03: return None
bmv_opposite = bme03.other_vert(bmv0)
ploop = []
for bmv0,bmv1 in iter_pairs(loop, wrap):
if not bmf: return None
if len(bmf.verts) != 4: return None
ploop.append(bmv_opposite)
bmv_opposite = next(iter(set(bmf.verts)-{bmv0,bmv1,bmv_opposite}), None)
if not bmv_opposite: return None
bme = bmv1.shared_edge(bmv_opposite)
if not bme: return None
bmf = next(iter(set(bme.link_faces) - {bmf}), None)
if not ploop: return None
if not wrap: ploop.append(bmv_opposite)
return ploop
ploops = []
bmv0,bmv1 = loop[:2]
bme01 = bmv0.shared_edge(bmv1)
bmfs = [bmf for bmf in bme01.link_faces]
touched = set()
for bmf in bmfs:
bme0 = bme01
lloop = loop
while bmf:
if bmf in touched: break
touched.add(bmf)
ploop = find_opposite_loop(lloop, bmf)
if not ploop: break
ploops.append(ploop)
bme1 = bmf.opposite_edge(bme0)
if not bme1: break
bmf = next((bmf_ for bmf_ in bme1.link_faces if bmf_ != bmf), None)
bme0 = bme1
lloop = ploop
return ploops
def find_strings(edges, min_length=3):
if not edges: return []
touched,strings = set(),[]
def crawl(v0, edge01, vert_list):
nonlocal edges, touched
# ... -- v0 -- edge01 -- v1 -- edge12 -- ...
# came ^ from ^
vert_list.append(v0)
touched.add(edge01)
v1 = edge01.other_vert(v0)
if v1 == vert_list[0]: return []
edge12 = next_edge_in_string(edge01, v1)
if not edge12 or edge12 not in edges: return vert_list + [v1]
return crawl(v1, edge12, vert_list)
for edge in edges:
if edge in touched: continue
vert_list0 = crawl(edge.verts[0], edge, [])
vert_list1 = crawl(edge.verts[1], edge, [])
vert_list = list(reversed(vert_list0)) + vert_list1[2:]
if len(vert_list) >= min_length: strings.append(vert_list)
return strings
def find_cycles(edges, max_loops=10):
# searches through edges to find loops
# first, break into connected components
# then, find all the junctions (verts with more than two connected edges)
# sequence of edges between junctions can be reduced to single edge
# find cycles in graph
if not edges: return []
vert_edges = {}
for edge in edges:
v0,v1 = edge.verts
vert_edges[v0] = vert_edges.get(v0, []) + [(edge,v1)]
vert_edges[v1] = vert_edges.get(v1, []) + [(edge,v0)]
touched_edges = set()
touched_verts = set()
cycles = []
cycle_hashes = set()
def crawl(v0, vert_list):
touched_verts.add(v0)
vert_list.append(v0)
for edge,v1 in vert_edges[v0]:
if edge in touched_edges: continue
touched_edges.add(edge)
if v1 in vert_list:
# found cycle!
cycle = list(reversed(vert_list))
while cycle[-1] != v1: cycle.pop()
h = hash_cycle(cycle)
if h not in cycle_hashes:
cycle_hashes.add(h)
cycles.append(cycle)
else:
crawl(v1, vert_list)
touched_edges.remove(edge)
if len(cycles) == max_loops: return
vert_list.pop()
for v in vert_edges.keys():
if v in touched_verts: continue
crawl(v, [])
if len(cycles) == max_loops: print('max loop count reached')
return cycles
def edges_of_loop(vert_loop):
edges = []
for v0,v1 in iter_pairs(vert_loop, True):
e0 = set(v0.link_edges)
e1 = set(v1.link_edges)
edges += list(e0 & e1)
return edges
def verts_of_loop(edge_loop):
verts = []
for e0,e1 in iter_pairs(edge_loop, False):
if not verts:
v0 = e0.shared_vert(e1)
verts += [e0.other_vert(v0), v0]
verts += [e1.other_vert(verts[-1])]
if len(verts) > 1 and verts[0] == verts[-1]: return verts[:-1]
return verts
def loop_plane(vert_loop):
# average co is pt on plane
# average cross product (point in same direction) is normal
if not vert_loop: return None
vert_loop = [to_point(v) for v in vert_loop]
pt = sum((Vector(vert) for vert in vert_loop), Vector()) / len(vert_loop)
n,cnt = None,0
for vert0,vert1 in zip(vert_loop[:-1], vert_loop[1:]):
c = Vec((vert0-pt).cross(vert1-pt)).normalize()
n = n+c if n else c
if not n: return Plane(pt, Normal())
return Plane(pt, Normal(n).normalize())
def loop_radius(vert_loop):
pt = sum((Vector(to_point(vert)) for vert in vert_loop), Vector()) / len(vert_loop)
rad = sum((to_point(vert) - pt).length for vert in vert_loop) / len(vert_loop)
return rad
def loop_length(vert_loop):
return sum((to_point(v0)-to_point(v1)).length for v0,v1 in zip(vert_loop, chain(vert_loop[1:], vert_loop[:1])))
def loops_connected(vert_loop0, vert_loop1):
if not vert_loop0 or not vert_loop1: return False
v0 = vert_loop0
v0_connected = { e.other_vert(v0) for e in v0.link_edges }
return any(v1 in v0_connected for v1 in vert_loop1)
def edges_between_loops(vert_loop0, vert_loop1):
loop1 = set(vert_loop1)
return [e for v0 in vert_loop0 for e in v0.link_edges if e.other_vert(v0) in loop1]
def faces_between_loops(vert_loop0, vert_loop1):
loop1 = set(vert_loop1)
return [f for v0 in vert_loop0 for f in v0.link_faces if any(fv in loop1 for fv in f.verts)]
def string_length(vert_loop):
return sum((to_point(v0)-to_point(v1)).length for v0,v1 in zip(vert_loop[:-1], vert_loop[1:]))
def project_loop_to_plane(vert_loop, plane):
return [plane.project(to_point(v)) for v in vert_loop]
class Contours_Loop:
def __init__(self, vert_loop, connected, offset=0):
self.connected = connected
self.set_vert_loop(vert_loop, offset)
def __repr__(self):
return '<Contours_Loop: %d,%s,%s>' % (len(self.verts), str(self.connected), str(self.verts))
# @profiler.function
def set_vert_loop(self, vert_loop, offset):
self.verts = vert_loop
self.offset = offset
self.pts = [to_point(bmv) for bmv in self.verts]
self.count = len(self.pts)
self.plane = loop_plane(self.pts)
if not self.connected:
self.plane.o = self.pts[0] + (self.pts[-1] - self.pts[0]) / 2
self.up_dir = Direction(self.pts[0] - self.plane.o)
self.frame = Frame.from_plane(self.plane, y=self.up_dir)
proj = self.plane.project
self.dists = [(proj(p0)-proj(p1)).length for p0,p1 in iter_pairs(self.pts, self.connected)]
self.proj_dists = [self.plane.signed_distance_to(p) for p in self.pts]
self.circumference = sum(self.dists)
self.radius = sum(self.w2l_point(pt).length for pt in self.pts) / self.count
def get_origin(self): return self.plane.o
def get_normal(self): return self.plane.n
def get_local_by_index(self, idx): return self.w2l_point(self.pts[idx])
def w2l_point(self, co): return self.frame.w2l_point(to_point(co))
def l2w_point(self, co): return self.frame.l2w_point(to_point(co))
def get_index_of_top(self, pts):
pts_local = [self.w2l_point(pt+self.frame.o) for pt in pts]
idx = max_index(pts_local, key=lambda pt:pt.y)
t = pts_local[idx]
#print(pts_local, idx, t)
offset = ((math.pi/2 - math.atan2(t.y, t.x)) * self.circumference / (math.pi*2)) % self.circumference
return (idx,offset)
def align_to(self, other):
n0, n1 = self.get_normal(), other.get_normal()
is_opposite = n0.dot(n1) < 0
vert_loop = list(reversed(self.verts)) if is_opposite else self.verts
if not self.connected:
self.set_vert_loop(vert_loop, 0)
return
if is_opposite: n0 = -n0
# issue #659
angle = 0 if n0.length_squared == 0 or n1.length_squared == 0 else n0.angle(n1)
q = Quaternion(n0.cross(n1), angle)
# rotate to align "topmost" vertex
rel_pos = [Vec(q @ (to_point(p) - self.frame.o)) for p in vert_loop]
rot_by,offset = other.get_index_of_top(rel_pos)
vert_loop = vert_loop[rot_by:] + vert_loop[:rot_by]
offset = (offset * self.circumference / other.circumference)
self.set_vert_loop(vert_loop, offset)
def get_closest_point(self, point):
point = to_point(point)
cp,cd = None,None
for p0,p1 in iter_pairs(self.pts, self.connected):
diff = p1 - p0
l = diff.length
d = diff / l
pp = p0 + d * max(0, min(l, (point - p0).dot(d)))
dist = (point - pp).length
if not cp or dist < cd: cp,cd = pp,dist
return cp
def get_points_relative_to(self, other):
scale = other.radius / self.radius
return [other.l2w_point(Vector(self.w2l_point(pt)) * scale) for pt in self.pts]
def iter_pts(self, repeat=False):
return iter_pairs(self.pts, self.connected, repeat=repeat)
def move_2D(self, xy_delta:Vec2D):
pass