380 lines
14 KiB
Python
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
|