loadpath 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- loadpath/README.md +294 -0
- loadpath/SKILL.md +584 -0
- loadpath/__init__.py +17 -0
- loadpath/__main__.py +1202 -0
- loadpath/am.py +316 -0
- loadpath/amgfast.py +326 -0
- loadpath/assembly.py +985 -0
- loadpath/benchmarks/__init__.py +0 -0
- loadpath/benchmarks/beams.py +266 -0
- loadpath/benchmarks/cases/ge-bracket.json +380 -0
- loadpath/benchmarks/evaluate.py +172 -0
- loadpath/benchmarks/mechanisms.py +416 -0
- loadpath/benchmarks/problems.py +140 -0
- loadpath/benchmarks/reference/bridge-3d.top3d.npz +0 -0
- loadpath/benchmarks/reference/cantilever-3d.top3d.npz +0 -0
- loadpath/benchmarks/reference/lbracket-2d.top88-ft2.npz +0 -0
- loadpath/benchmarks/reference/mbb-2d.top88-ft1.npz +0 -0
- loadpath/benchmarks/reference/mbb-2d.top88-ft2.npz +0 -0
- loadpath/benchmarks/reference.py +138 -0
- loadpath/benchmarks/run.py +301 -0
- loadpath/brep.py +1470 -0
- loadpath/bucklingopt.py +300 -0
- loadpath/buildsim.py +1490 -0
- loadpath/cadimport.py +904 -0
- loadpath/calibration.py +885 -0
- loadpath/cavities.py +211 -0
- loadpath/checks/__init__.py +0 -0
- loadpath/checks/dfm.py +354 -0
- loadpath/checks/exchanger.py +7 -0
- loadpath/checks/fea.py +212 -0
- loadpath/checks/fit.py +475 -0
- loadpath/checks/networks.py +260 -0
- loadpath/checks/opening.py +67 -0
- loadpath/checks/sdf_kit.py +7 -0
- loadpath/conjugate.py +267 -0
- loadpath/constrained.py +329 -0
- loadpath/contact.py +691 -0
- loadpath/coopt.py +342 -0
- loadpath/cores.py +163 -0
- loadpath/crease.py +1138 -0
- loadpath/critic.py +212 -0
- loadpath/crosscheck.py +1107 -0
- loadpath/cutcell.py +510 -0
- loadpath/data/lattice_buckling.json +439 -0
- loadpath/data/lattice_tensors.json +27449 -0
- loadpath/data/published_experiments.json +71 -0
- loadpath/decimate.py +177 -0
- loadpath/delivery.py +190 -0
- loadpath/directions.py +262 -0
- loadpath/domain.py +630 -0
- loadpath/drains.py +615 -0
- loadpath/durability.py +413 -0
- loadpath/editor.html +3135 -0
- loadpath/editor.py +610 -0
- loadpath/escape.py +354 -0
- loadpath/evals/__init__.py +0 -0
- loadpath/evals/baselines/coarse.json +1543 -0
- loadpath/evals/baselines/full.json +1497 -0
- loadpath/evals/brief_kinds/__init__.py +1 -0
- loadpath/evals/brief_kinds/heatx.py +387 -0
- loadpath/evals/brief_kinds/lattice.py +571 -0
- loadpath/evals/brief_kinds/multimat.py +593 -0
- loadpath/evals/briefs/assets/bracket.step +1201 -0
- loadpath/evals/briefs/assets/desk_arm.urdf +22 -0
- loadpath/evals/briefs/assets/elbow-gearbox.stl +0 -0
- loadpath/evals/briefs/assets/motor-body.stl +0 -0
- loadpath/evals/briefs/assets/sketch.png +0 -0
- loadpath/evals/briefs/base-adapter-fdm.json +492 -0
- loadpath/evals/briefs/bell-crank-lpbf.json +180 -0
- loadpath/evals/briefs/belt-tensioner-fdm.json +151 -0
- loadpath/evals/briefs/camera-boom-mjf.json +339 -0
- loadpath/evals/briefs/camera-mount-pla.json +203 -0
- loadpath/evals/briefs/coolant-isolator-petg.json +100 -0
- loadpath/evals/briefs/elbow-bearing-bracket-sls.json +300 -0
- loadpath/evals/briefs/elbow-gearbox-forearm-mjf.json +558 -0
- loadpath/evals/briefs/elbow-link-fatigue-fdm.json +251 -0
- loadpath/evals/briefs/elbow-stop-pad-tpu.json +39 -0
- loadpath/evals/briefs/forearm-fdm.json +146 -0
- loadpath/evals/briefs/forearm-servo-link-fdm.json +327 -0
- loadpath/evals/briefs/grip-pad-finger-mm.json +354 -0
- loadpath/evals/briefs/gripper-bracket-creep-fdm.json +340 -0
- loadpath/evals/briefs/gripper-finger-fdm.json +155 -0
- loadpath/evals/briefs/gripper-jaw-fatigue-mjf.json +328 -0
- loadpath/evals/briefs/gripper-palm-fdm.json +715 -0
- loadpath/evals/briefs/hot-stepper-mount-asa-fdm.json +471 -0
- loadpath/evals/briefs/hpu-oil-cooler-lpbf.json +121 -0
- loadpath/evals/briefs/isolated-camera-mount-mm.json +675 -0
- loadpath/evals/briefs/kart-charge-cooler-lpbf.json +93 -0
- loadpath/evals/briefs/lighten-step-bracket.json +220 -0
- loadpath/evals/briefs/nema17-mount-stl.json +429 -0
- loadpath/evals/briefs/pillow-block-sls.json +220 -0
- loadpath/evals/briefs/polyjet-fingertip-mm.json +305 -0
- loadpath/evals/briefs/rod-hook-pla.json +154 -0
- loadpath/evals/briefs/sensor-arm-resin.json +150 -0
- loadpath/evals/briefs/shelf-bracket-pla.json +158 -0
- loadpath/evals/briefs/shoulder-riser-mjf.json +487 -0
- loadpath/evals/briefs/sketch-clevis.json +229 -0
- loadpath/evals/briefs/spring-bracket-pacf.json +294 -0
- loadpath/evals/briefs/tool-flange-ti64.json +280 -0
- loadpath/evals/briefs/tray-arm-lattice-fdm.json +260 -0
- loadpath/evals/briefs/turret-fdm.json +281 -0
- loadpath/evals/briefs/upper-arm-sls.json +179 -0
- loadpath/evals/briefs/urdf-forearm.json +233 -0
- loadpath/evals/briefs/wrist-link-mjf.json +278 -0
- loadpath/evals/briefs/wrist-yoke-mjf.json +245 -0
- loadpath/evals/briefs.py +393 -0
- loadpath/evals/cases/arm-link-cast.json +157 -0
- loadpath/evals/cases/arm-link-driven.json +171 -0
- loadpath/evals/cases/arm-link-sandwich.json +315 -0
- loadpath/evals/cases/arm-link.json +111 -0
- loadpath/evals/cases/bell-crank.json +116 -0
- loadpath/evals/cases/grip-turret.json +479 -0
- loadpath/evals/cases/grip-upper-arm.json +253 -0
- loadpath/evals/cases/grip-wrist-link-mjf.json +929 -0
- loadpath/evals/cases/grip-wrist-yoke.json +831 -0
- loadpath/evals/cases/lpbf-intercooler-core.json +59 -0
- loadpath/evals/cases/mbb-beam.json +61 -0
- loadpath/evals/cases/motor-body.stl +0 -0
- loadpath/evals/cases/motor-mount.json +265 -0
- loadpath/evals/cases/printed-hook.json +77 -0
- loadpath/evals/cases/wall-bracket-fasteners.json +125 -0
- loadpath/evals/cases/wall-bracket.json +126 -0
- loadpath/evals/corpus.py +179 -0
- loadpath/evals/pareto.py +90 -0
- loadpath/evals/run.py +860 -0
- loadpath/examples/arm-link.json +25 -0
- loadpath/examples/bumpered-bracket-compression.json +225 -0
- loadpath/examples/bumpered-bracket-lattice.json +204 -0
- loadpath/examples/bumpered-bracket.json +34 -0
- loadpath/examples/gripper-finger-mm.json +185 -0
- loadpath/examples/heat-exchanger-counterflow.json +21 -0
- loadpath/examples/heat-exchanger-crossflow-tuned.json +48 -0
- loadpath/examples/heat-exchanger-crossflow.json +20 -0
- loadpath/examples/pedal-lattice.json +23 -0
- loadpath/examples/pressure-plenum.json +12 -0
- loadpath/examples/shelf-bracket.json +27 -0
- loadpath/exchanger.py +831 -0
- loadpath/fasteners.py +199 -0
- loadpath/fem.py +668 -0
- loadpath/femops.py +70 -0
- loadpath/fieldjit.py +981 -0
- loadpath/fieldops.py +119 -0
- loadpath/fields.py +3697 -0
- loadpath/fieldtorch.py +464 -0
- loadpath/flow.py +984 -0
- loadpath/flowtopo.py +300 -0
- loadpath/gpu.py +521 -0
- loadpath/heatx.py +1067 -0
- loadpath/heatx_thermal.py +432 -0
- loadpath/homogenized.py +886 -0
- loadpath/ibrep.py +552 -0
- loadpath/impact.py +693 -0
- loadpath/infill.py +376 -0
- loadpath/interface_library.py +340 -0
- loadpath/interlock.py +186 -0
- loadpath/isolator.py +149 -0
- loadpath/knn.py +204 -0
- loadpath/lattice.py +1065 -0
- loadpath/lattice3mf.py +760 -0
- loadpath/lattice_buckling.py +245 -0
- loadpath/lattice_grade.py +325 -0
- loadpath/lattice_orient.py +595 -0
- loadpath/lattice_print.py +159 -0
- loadpath/lattice_tensors.py +558 -0
- loadpath/latticestep.py +1210 -0
- loadpath/manufacture.py +446 -0
- loadpath/material_library.py +489 -0
- loadpath/materials.py +144 -0
- loadpath/mcp_server.py +786 -0
- loadpath/mechanism.py +218 -0
- loadpath/mechanism_nl.py +811 -0
- loadpath/meshops.py +452 -0
- loadpath/mg.py +853 -0
- loadpath/mma.py +214 -0
- loadpath/modal.py +210 -0
- loadpath/multimat.py +1788 -0
- loadpath/narrowband.py +418 -0
- loadpath/nlkernel.py +241 -0
- loadpath/nonlinear.py +1095 -0
- loadpath/orient.py +334 -0
- loadpath/porous.py +994 -0
- loadpath/pressure.py +171 -0
- loadpath/preview.py +335 -0
- loadpath/printcheck.py +263 -0
- loadpath/printsetup.py +207 -0
- loadpath/processcheck.py +376 -0
- loadpath/quick.py +113 -0
- loadpath/recall.py +406 -0
- loadpath/reconstruct.py +797 -0
- loadpath/render.py +436 -0
- loadpath/resolved.py +189 -0
- loadpath/schema.py +1701 -0
- loadpath/sdf_kit.py +843 -0
- loadpath/selfx.py +292 -0
- loadpath/shapes.py +584 -0
- loadpath/sharp.py +717 -0
- loadpath/simp.py +1170 -0
- loadpath/sizing.py +312 -0
- loadpath/slicer.py +321 -0
- loadpath/spec.py +1072 -0
- loadpath/step.py +806 -0
- loadpath/struts.py +639 -0
- loadpath/study.py +1937 -0
- loadpath/tasks.py +822 -0
- loadpath/thermal.py +1691 -0
- loadpath/threemf.py +487 -0
- loadpath/tune.py +437 -0
- loadpath/validation.py +145 -0
- loadpath/verify.py +1215 -0
- loadpath/viewer.html +694 -0
- loadpath/viewer.py +446 -0
- loadpath/viscoelastic.py +106 -0
- loadpath/voxelprint.py +582 -0
- loadpath/voxels.py +402 -0
- loadpath-0.1.1.dist-info/METADATA +319 -0
- loadpath-0.1.1.dist-info/RECORD +220 -0
- loadpath-0.1.1.dist-info/WHEEL +5 -0
- loadpath-0.1.1.dist-info/entry_points.txt +2 -0
- loadpath-0.1.1.dist-info/licenses/LICENSE +21 -0
- loadpath-0.1.1.dist-info/top_level.txt +1 -0
loadpath/ibrep.py
ADDED
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Smooth B-rep patches straight from an implicit field: one level set f = 0
|
|
3
|
+
(f < 0 inside) as a quad layout of bicubic Bezier patches that sit on the
|
|
4
|
+
true surface, ready to sew into a closed OpenCascade shell.
|
|
5
|
+
|
|
6
|
+
1. surface nets: the zero set on a grid of spacing h as a quad mesh (one
|
|
7
|
+
quad per grid edge the surface crosses), made manifold (one vertex per
|
|
8
|
+
surface sheet in a cell, ambiguous faces decided by their centre sample,
|
|
9
|
+
the grid shifted until no edge has more than two quads), vertices on the
|
|
10
|
+
surface (Newton along the gradient of the exact field);
|
|
11
|
+
2. patches: each quad edge a cubic Bezier through points draped onto the
|
|
12
|
+
surface, each quad a bicubic Bezier patch through its sides and four
|
|
13
|
+
inside points on the surface. Shared edges are computed once, so
|
|
14
|
+
neighbouring patches meet exactly and sew at 1e-4 mm.
|
|
15
|
+
|
|
16
|
+
Accuracy is to the field itself, not to a mesh of it: the patches sit within
|
|
17
|
+
microns of the level set where it is smooth at the scale of h (the report
|
|
18
|
+
samples them). Used by latticestep.py for a skinned TPMS part's cavities.
|
|
19
|
+
|
|
20
|
+
(A general builder for intersections of several families, trimming patches
|
|
21
|
+
along their creases, was tried first, 2026-11-02: 1-3% of trimmed faces
|
|
22
|
+
failed at grazing creases, so the cavity route that needs no trimming won.)
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from math import comb
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
|
|
30
|
+
__all__ = ["Family", "Sheet", "manifold_nets", "surface_nets"]
|
|
31
|
+
|
|
32
|
+
DEG = 3 # patch and edge degree
|
|
33
|
+
EPS = 1e-4 # mm: finite-difference step for gradients
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Family:
|
|
37
|
+
"""A smooth implicit surface: f(P) (n, 3) -> (n,) mm, negative inside,
|
|
38
|
+
meshed at grid spacing h (mm)."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, name, f, h):
|
|
41
|
+
self.name, self.f, self.h = name, f, float(h)
|
|
42
|
+
|
|
43
|
+
def vg(self, P):
|
|
44
|
+
P = np.asarray(P, float)
|
|
45
|
+
n = len(P)
|
|
46
|
+
Q = np.concatenate([P] + [P + d for d in np.eye(3) * EPS] + [P - d for d in np.eye(3) * EPS])
|
|
47
|
+
v = np.asarray(self.f(Q), float)
|
|
48
|
+
g = np.stack([(v[(1 + a) * n:(2 + a) * n] - v[(4 + a) * n:(5 + a) * n]) / (2 * EPS) for a in range(3)], 1)
|
|
49
|
+
return v[:n], g
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _project(fam, P, it=10, step=None):
|
|
53
|
+
"""Points onto fam = 0 (Newton along the gradient, steps capped)."""
|
|
54
|
+
P = np.array(P, float)
|
|
55
|
+
if not len(P):
|
|
56
|
+
return P
|
|
57
|
+
for _ in range(it):
|
|
58
|
+
v, g = fam.vg(P)
|
|
59
|
+
dx = (v / np.maximum((g * g).sum(1), 1e-12))[:, None] * g
|
|
60
|
+
if step is not None:
|
|
61
|
+
L = np.linalg.norm(dx, axis=1, keepdims=True)
|
|
62
|
+
dx *= np.minimum(1.0, step / np.maximum(L, 1e-300))
|
|
63
|
+
P -= dx
|
|
64
|
+
if np.abs(v).max() < 1e-7:
|
|
65
|
+
break
|
|
66
|
+
return P
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def surface_nets(fam, lo, hi):
|
|
70
|
+
"""Quad mesh of fam's zero set over the box lo..hi: (V (n, 3), Q (m, 4)),
|
|
71
|
+
quads counter-clockwise seen from outside (f > 0). Manifold: a cell the
|
|
72
|
+
surface passes through more than once (a gap thinner than the grid) gets
|
|
73
|
+
one vertex per sheet, the sheets told apart on each cube face as
|
|
74
|
+
marching cubes would (an ambiguous face by its centre's sign), so two
|
|
75
|
+
sheets never share a vertex."""
|
|
76
|
+
h = fam.h
|
|
77
|
+
lo = np.asarray(lo, float)
|
|
78
|
+
n = np.ceil((np.asarray(hi, float) - lo) / h).astype(int) + 1
|
|
79
|
+
ax = [lo[i] + np.arange(n[i]) * h for i in range(3)]
|
|
80
|
+
G = np.stack(np.meshgrid(*ax, indexing="ij"), -1).reshape(-1, 3)
|
|
81
|
+
f = np.empty(len(G))
|
|
82
|
+
for s0 in range(0, len(G), 400_000):
|
|
83
|
+
f[s0:s0 + 400_000] = fam.f(G[s0:s0 + 400_000])
|
|
84
|
+
f = f.reshape(tuple(n))
|
|
85
|
+
inside = f < 0
|
|
86
|
+
nc = n - 1
|
|
87
|
+
corners = [(i, j, k) for i in (0, 1) for j in (0, 1) for k in (0, 1)]
|
|
88
|
+
S = np.stack([inside[i:i + nc[0], j:j + nc[1], k:k + nc[2]] for i, j, k in corners], -1)
|
|
89
|
+
act = S.any(-1) & ~S.all(-1)
|
|
90
|
+
cells = np.argwhere(act)
|
|
91
|
+
cid = -np.ones(tuple(nc), np.int64)
|
|
92
|
+
cid[tuple(cells.T)] = np.arange(len(cells))
|
|
93
|
+
# the 12 cell edges: (axis, offset in the two other axes)
|
|
94
|
+
edges = []
|
|
95
|
+
for a in range(3):
|
|
96
|
+
b, c = [x for x in range(3) if x != a]
|
|
97
|
+
for o1 in (0, 1):
|
|
98
|
+
for o2 in (0, 1):
|
|
99
|
+
off = np.zeros(3, int)
|
|
100
|
+
off[b], off[c] = o1, o2
|
|
101
|
+
edges.append((a, off))
|
|
102
|
+
eix = {(a, tuple(off)): k for k, (a, off) in enumerate(edges)}
|
|
103
|
+
# crossings per cell and edge
|
|
104
|
+
cross = np.zeros((len(cells), 12), bool)
|
|
105
|
+
X = np.zeros((len(cells), 12, 3))
|
|
106
|
+
for k, (a, off) in enumerate(edges):
|
|
107
|
+
e = np.zeros(3, int)
|
|
108
|
+
e[a] = 1
|
|
109
|
+
p0 = cells + off
|
|
110
|
+
p1 = p0 + e
|
|
111
|
+
f0, f1 = f[tuple(p0.T)], f[tuple(p1.T)]
|
|
112
|
+
ch = (f0 < 0) != (f1 < 0)
|
|
113
|
+
t = np.where(ch, f0 / np.where(ch, f0 - f1, 1.0), 0.0)
|
|
114
|
+
cross[:, k] = ch
|
|
115
|
+
X[:, k] = lo + (p0 + t[:, None] * e) * h
|
|
116
|
+
# the 6 faces: normal axis, side; their 4 edges in order round the face
|
|
117
|
+
faces = []
|
|
118
|
+
for a in range(3):
|
|
119
|
+
b, c = [x for x in range(3) if x != a]
|
|
120
|
+
for sd in (0, 1):
|
|
121
|
+
def E(ax_, ob, oc, a=a, b=b, c=c, sd=sd):
|
|
122
|
+
off = np.zeros(3, int)
|
|
123
|
+
off[a], off[b], off[c] = sd, ob, oc
|
|
124
|
+
off[ax_] = 0
|
|
125
|
+
return eix[(ax_, tuple(off))]
|
|
126
|
+
# corners c00 (b=0,c=0), c10, c11, c01; edges bottom (c00-c10, along b, c=0), right (c10-c11, along c, b=1),
|
|
127
|
+
# top (c01-c11, along b, c=1), left (c00-c01, along c, b=0)
|
|
128
|
+
faces.append((a, sd, b, c, [E(b, 0, 0), E(c, 1, 0), E(b, 0, 1), E(c, 0, 0)]))
|
|
129
|
+
# union-find of crossed edges per cell, across its faces
|
|
130
|
+
parent = np.tile(np.arange(12), (len(cells), 1))
|
|
131
|
+
|
|
132
|
+
def find(r, x):
|
|
133
|
+
while parent[r, x] != x:
|
|
134
|
+
parent[r, x] = parent[r, parent[r, x]]
|
|
135
|
+
x = parent[r, x]
|
|
136
|
+
return x
|
|
137
|
+
|
|
138
|
+
def union(rows, x, y):
|
|
139
|
+
for r, a_, b_ in zip(rows, x, y):
|
|
140
|
+
ra, rb = find(r, a_), find(r, b_)
|
|
141
|
+
if ra != rb:
|
|
142
|
+
parent[r, ra] = rb
|
|
143
|
+
|
|
144
|
+
for a, sd, b, c, (eb, er, et, el) in faces:
|
|
145
|
+
cc = cross[:, [eb, er, et, el]]
|
|
146
|
+
cnt = cc.sum(1)
|
|
147
|
+
two = np.flatnonzero(cnt == 2)
|
|
148
|
+
if len(two):
|
|
149
|
+
idx = np.argsort(~cc[two], axis=1, kind="stable")[:, :2]
|
|
150
|
+
loc = np.array([eb, er, et, el])[idx]
|
|
151
|
+
union(two, loc[:, 0], loc[:, 1])
|
|
152
|
+
four = np.flatnonzero(cnt == 4)
|
|
153
|
+
if len(four):
|
|
154
|
+
off = np.zeros(3)
|
|
155
|
+
off[a] = sd
|
|
156
|
+
off[b] = off[c] = 0.5
|
|
157
|
+
Pc = lo + (cells[four] + off) * h
|
|
158
|
+
fc = np.asarray(fam.f(Pc), float)
|
|
159
|
+
p00 = cells[four].copy()
|
|
160
|
+
p00[:, a] += sd
|
|
161
|
+
c00 = inside[tuple(p00.T)]
|
|
162
|
+
joined = (fc < 0) == c00 # c00 and c11 joined through the centre
|
|
163
|
+
# joined: the curves cut off c10 (bottom-right) and c01 (top-left)
|
|
164
|
+
union(four[joined], np.full(joined.sum(), eb), np.full(joined.sum(), er))
|
|
165
|
+
union(four[joined], np.full(joined.sum(), et), np.full(joined.sum(), el))
|
|
166
|
+
union(four[~joined], np.full((~joined).sum(), eb), np.full((~joined).sum(), el))
|
|
167
|
+
union(four[~joined], np.full((~joined).sum(), et), np.full((~joined).sum(), er))
|
|
168
|
+
roots = np.array([[find(r, k) for k in range(12)] for r in range(len(cells))]) if len(cells) else np.zeros((0, 12), int)
|
|
169
|
+
# one vertex per (cell, component)
|
|
170
|
+
key = np.where(cross, np.arange(len(cells))[:, None] * 12 + roots, -1)
|
|
171
|
+
uk, inv = np.unique(key[cross], return_inverse=True)
|
|
172
|
+
vid = -np.ones((len(cells), 12), np.int64)
|
|
173
|
+
vid[cross] = inv
|
|
174
|
+
acc = np.zeros((len(uk), 3))
|
|
175
|
+
np.add.at(acc, inv, X[cross])
|
|
176
|
+
cnt = np.bincount(inv, minlength=len(uk))
|
|
177
|
+
V = acc / np.maximum(cnt, 1)[:, None]
|
|
178
|
+
V = _project(fam, V, step=0.5 * h)
|
|
179
|
+
quads = []
|
|
180
|
+
for a in range(3):
|
|
181
|
+
e = np.zeros(3, int)
|
|
182
|
+
e[a] = 1
|
|
183
|
+
s0 = inside[:n[0] - e[0], :n[1] - e[1], :n[2] - e[2]]
|
|
184
|
+
s1 = inside[e[0]:, e[1]:, e[2]:]
|
|
185
|
+
ch = np.argwhere(s0 != s1)
|
|
186
|
+
b, c = [(1, 2), (2, 0), (0, 1)][a]
|
|
187
|
+
ids, ok = [], np.ones(len(ch), bool)
|
|
188
|
+
for db, dc in ((0, 0), (-1, 0), (-1, -1), (0, -1)):
|
|
189
|
+
q = ch.copy()
|
|
190
|
+
q[:, b] += db
|
|
191
|
+
q[:, c] += dc
|
|
192
|
+
good = (q >= 0).all(1) & (q < nc).all(1)
|
|
193
|
+
ok &= good
|
|
194
|
+
qc = np.where(good[:, None], q, 0)
|
|
195
|
+
ci = cid[tuple(qc.T)]
|
|
196
|
+
off = np.zeros((len(ch), 3), int)
|
|
197
|
+
off[:, b], off[:, c] = -db, -dc # the grid edge's place in this cell
|
|
198
|
+
k = np.array([eix[(a, tuple(o))] for o in off]) if len(ch) else np.zeros(0, int)
|
|
199
|
+
ids.append(np.where(good & (ci >= 0), vid[np.maximum(ci, 0), k], -1))
|
|
200
|
+
ids = np.stack(ids, 1)[ok]
|
|
201
|
+
flip = s0[tuple(ch[ok].T)]
|
|
202
|
+
ids[~flip] = ids[~flip][:, ::-1]
|
|
203
|
+
quads.append(ids)
|
|
204
|
+
Q = np.concatenate(quads) if quads else np.zeros((0, 4), np.int64)
|
|
205
|
+
Q = Q[(Q >= 0).all(1)]
|
|
206
|
+
if len(Q):
|
|
207
|
+
# the grid's own orientation is consistent; check its sense once, on the majority
|
|
208
|
+
nrm = np.cross(V[Q[:, 2]] - V[Q[:, 0]], V[Q[:, 3]] - V[Q[:, 1]])
|
|
209
|
+
_, g = fam.vg(V[Q].mean(axis=1))
|
|
210
|
+
if (np.einsum("ij,ij->i", nrm, g) < 0).mean() > 0.5:
|
|
211
|
+
Q = Q[:, ::-1]
|
|
212
|
+
return V, Q
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def nonmanifold(Q):
|
|
216
|
+
"""Edges of a quad mesh not shared by exactly two quads."""
|
|
217
|
+
if not len(Q):
|
|
218
|
+
return 0
|
|
219
|
+
E = np.sort(np.concatenate([Q[:, [k, (k + 1) % 4]] for k in range(4)]), axis=1)
|
|
220
|
+
_, c = np.unique(E, axis=0, return_counts=True)
|
|
221
|
+
return int((c != 2).sum())
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _bad_edges(Q):
|
|
225
|
+
"""The vertex pairs of edges not shared by exactly two quads."""
|
|
226
|
+
if not len(Q):
|
|
227
|
+
return np.zeros((0, 2), np.int64)
|
|
228
|
+
E = np.sort(np.concatenate([Q[:, [k, (k + 1) % 4]] for k in range(4)]), axis=1)
|
|
229
|
+
u, c = np.unique(E, axis=0, return_counts=True)
|
|
230
|
+
return u[c != 2]
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _same_spot(V0, e, V, f, tol=1e-9):
|
|
234
|
+
"""Whether edge e of mesh V0 and edge f of V are the same edge (the
|
|
235
|
+
split moves copies, never the first vertex)."""
|
|
236
|
+
return bool(np.allclose(np.sort(V0[e], axis=0), np.sort(V[f], axis=0), atol=tol) or
|
|
237
|
+
(set(map(int, e)) == set(map(int, f))))
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def manifold_nets(fam, lo, hi, tries=16, split=True):
|
|
241
|
+
"""surface_nets on a grid shifted until the mesh is a closed manifold
|
|
242
|
+
(a cell the surface crosses in the ambiguous way marching cubes cannot
|
|
243
|
+
split moves off the grid), or the best of `tries` shifts; then (split)
|
|
244
|
+
each try's pinches (split_pinches) pulled apart first. Returns (V, Q,
|
|
245
|
+
non-manifold edges left)."""
|
|
246
|
+
best = None
|
|
247
|
+
rng = np.random.default_rng(7)
|
|
248
|
+
for k in range(tries):
|
|
249
|
+
shift = np.zeros(3) if k == 0 else rng.uniform(0, 1, 3) * fam.h
|
|
250
|
+
V, Q = surface_nets(fam, np.asarray(lo, float) - shift, np.asarray(hi, float) - shift + fam.h)
|
|
251
|
+
bad = nonmanifold(Q)
|
|
252
|
+
if bad and split:
|
|
253
|
+
V0, Q0 = V, Q
|
|
254
|
+
V, Q = split_pinches(fam, V0, Q0)
|
|
255
|
+
bad = nonmanifold(Q)
|
|
256
|
+
if bad:
|
|
257
|
+
# the first pairing at an edge can join the sheets' fans
|
|
258
|
+
# again at its ends (each vertex one fan, the edge still
|
|
259
|
+
# four quads): try the other pairing at the edges left
|
|
260
|
+
left = _bad_edges(Q)
|
|
261
|
+
alt = {(int(min(e)), int(max(e))) for e in _bad_edges(Q0)
|
|
262
|
+
if any(_same_spot(V0, e, V, f) for f in left)}
|
|
263
|
+
if alt:
|
|
264
|
+
V2, Q2 = split_pinches(fam, V0, Q0, alternate=alt)
|
|
265
|
+
if nonmanifold(Q2) < bad:
|
|
266
|
+
V, Q, bad = V2, Q2, nonmanifold(Q2)
|
|
267
|
+
if best is None or bad < best[0]:
|
|
268
|
+
best = (bad, V, Q)
|
|
269
|
+
if bad == 0:
|
|
270
|
+
break
|
|
271
|
+
bad, V, Q = best
|
|
272
|
+
return V, Q, bad
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def split_pinches(fam, V, Q, alternate=()):
|
|
276
|
+
"""Pull a quad mesh apart where two sheets of the surface pinch together
|
|
277
|
+
at a near-zero saddle (two struts' fillets that barely touch, a skin
|
|
278
|
+
kissed by a fillet): one cell meets the surface twice and its one vertex
|
|
279
|
+
joins both, so a quad edge has four quads, or a vertex two separate
|
|
280
|
+
fans. Each such edge's quads are paired (one each way along the edge,
|
|
281
|
+
neighbours round it) and every vertex gets one copy per fan of its
|
|
282
|
+
quads; a copy is moved a little towards its own fan and back onto the
|
|
283
|
+
surface, so the sheets part by the width of the saddle. Returns (V, Q):
|
|
284
|
+
a manifold mesh (every edge two quads, every vertex one fan)."""
|
|
285
|
+
Q = np.array(Q, np.int64)
|
|
286
|
+
if not len(Q):
|
|
287
|
+
return V, Q
|
|
288
|
+
# directed edges: quad q, side k runs Q[q, k] -> Q[q, k+1]
|
|
289
|
+
a = Q.reshape(-1)
|
|
290
|
+
b = np.roll(Q, -1, axis=1).reshape(-1)
|
|
291
|
+
qid = np.repeat(np.arange(len(Q)), 4)
|
|
292
|
+
lo_, hi_ = np.minimum(a, b), np.maximum(a, b)
|
|
293
|
+
key = lo_ * (len(V) + 1) + hi_
|
|
294
|
+
_, inv, cnt = np.unique(key, return_inverse=True, return_counts=True)
|
|
295
|
+
many = np.flatnonzero(cnt > 2)
|
|
296
|
+
# pairs: half-edge index -> partner half-edge index (normal edges: the other one)
|
|
297
|
+
order = np.argsort(inv, kind="stable")
|
|
298
|
+
starts = np.r_[0, np.cumsum(cnt)[:-1]]
|
|
299
|
+
partner = -np.ones(len(a), np.int64)
|
|
300
|
+
two = np.flatnonzero(cnt == 2)
|
|
301
|
+
h0, h1 = order[starts[two]], order[starts[two] + 1]
|
|
302
|
+
partner[h0], partner[h1] = h1, h0
|
|
303
|
+
C = V[Q].mean(axis=1)
|
|
304
|
+
for e in many:
|
|
305
|
+
hs = order[starts[e]:starts[e] + cnt[e]]
|
|
306
|
+
va, vb = V[lo_[hs[0]]], V[hi_[hs[0]]]
|
|
307
|
+
ax = vb - va
|
|
308
|
+
ax /= max(np.linalg.norm(ax), 1e-12)
|
|
309
|
+
ref = np.cross(ax, [1.0, 0, 0])
|
|
310
|
+
if np.linalg.norm(ref) < 0.1:
|
|
311
|
+
ref = np.cross(ax, [0, 1.0, 0])
|
|
312
|
+
ref /= np.linalg.norm(ref)
|
|
313
|
+
ref2 = np.cross(ax, ref)
|
|
314
|
+
d = C[qid[hs]] - va
|
|
315
|
+
ang = np.arctan2(d @ ref2, d @ ref)
|
|
316
|
+
hs = hs[np.argsort(ang)]
|
|
317
|
+
# neighbours round the edge, one each way: two choices of pairing
|
|
318
|
+
# (0-1, 2-3, ...) or (1-2, 3-0, ...); take the one whose pairs sit
|
|
319
|
+
# closer in angle (the same sheet)
|
|
320
|
+
m = len(hs)
|
|
321
|
+
cands = []
|
|
322
|
+
for sh in (0, 1):
|
|
323
|
+
pairs = [(hs[(i + sh) % m], hs[(i + sh + 1) % m]) for i in range(0, m, 2)]
|
|
324
|
+
if any((a[x] == lo_[x]) == (a[y] == lo_[y]) for x, y in pairs):
|
|
325
|
+
continue
|
|
326
|
+
cands.append((sum(np.linalg.norm(C[qid[x]] - C[qid[y]]) for x, y in pairs), pairs))
|
|
327
|
+
if m == 4:
|
|
328
|
+
# the angular order round a short edge (its ends projected close
|
|
329
|
+
# together) can interleave the sheets: any pairing of the four
|
|
330
|
+
# whose halves run opposite ways is a candidate
|
|
331
|
+
for pairs in (((hs[0], hs[2]), (hs[1], hs[3])),):
|
|
332
|
+
if not any((a[x] == lo_[x]) == (a[y] == lo_[y]) for x, y in pairs):
|
|
333
|
+
cands.append((sum(np.linalg.norm(C[qid[x]] - C[qid[y]]) for x, y in pairs), list(pairs)))
|
|
334
|
+
if not cands: # odd or unpaired: leave it
|
|
335
|
+
continue
|
|
336
|
+
cands.sort(key=lambda c: c[0])
|
|
337
|
+
# `alternate`: edges a first split left joined (manifold_nets asks
|
|
338
|
+
# again): the next candidate pairing
|
|
339
|
+
best = cands[1] if (int(lo_[hs[0]]), int(hi_[hs[0]])) in alternate and len(cands) > 1 else cands[0]
|
|
340
|
+
for x, y in best[1]:
|
|
341
|
+
partner[x], partner[y] = y, x
|
|
342
|
+
# fans: at each vertex, quads joined across the paired edges at it
|
|
343
|
+
import scipy.sparse as sp
|
|
344
|
+
from scipy.sparse.csgraph import connected_components
|
|
345
|
+
|
|
346
|
+
# a corner (q, k) is vertex Q[q, k]; its two sides are half-edges k
|
|
347
|
+
# (leaving it) and k-1 (arriving); join the corner to the corner of
|
|
348
|
+
# the partner quad at the same vertex
|
|
349
|
+
corner = np.arange(len(a))
|
|
350
|
+
side_out = corner # half-edge leaving corner
|
|
351
|
+
side_in = qid * 4 + (corner % 4 + 3) % 4 # half-edge arriving at corner
|
|
352
|
+
rows, cols = [], []
|
|
353
|
+
for hs_ in (side_out, side_in):
|
|
354
|
+
p = partner[hs_]
|
|
355
|
+
ok = p >= 0
|
|
356
|
+
# partner half-edge runs the other way: its corner at this vertex is
|
|
357
|
+
# its end (k+1) when hs_ leaves, its start when hs_ arrives
|
|
358
|
+
pq, pk = p[ok] // 4, p[ok] % 4
|
|
359
|
+
if hs_ is side_out:
|
|
360
|
+
pc = pq * 4 + (pk + 1) % 4
|
|
361
|
+
else:
|
|
362
|
+
pc = pq * 4 + pk
|
|
363
|
+
rows.append(corner[ok])
|
|
364
|
+
cols.append(pc)
|
|
365
|
+
rows, cols = np.concatenate(rows), np.concatenate(cols)
|
|
366
|
+
good = a[rows] == a[cols]
|
|
367
|
+
G = sp.coo_matrix((np.ones(good.sum()), (rows[good], cols[good])), shape=(len(a), len(a)))
|
|
368
|
+
_, lab = connected_components(G, directed=False)
|
|
369
|
+
# one vertex per (vertex, fan)
|
|
370
|
+
vk = a * (len(a) + 1) + lab
|
|
371
|
+
ufan, finv = np.unique(vk, return_inverse=True)
|
|
372
|
+
vert = ufan // (len(a) + 1)
|
|
373
|
+
first = np.zeros(len(V), bool)
|
|
374
|
+
newid = np.empty(len(ufan), np.int64)
|
|
375
|
+
extra = []
|
|
376
|
+
for i, v in enumerate(vert):
|
|
377
|
+
if not first[v]:
|
|
378
|
+
first[v] = True
|
|
379
|
+
newid[i] = v
|
|
380
|
+
else:
|
|
381
|
+
newid[i] = len(V) + len(extra)
|
|
382
|
+
extra.append(i)
|
|
383
|
+
Q2 = newid[finv].reshape(-1, 4)
|
|
384
|
+
if extra:
|
|
385
|
+
# each copy (and the vertex it came from) nudged towards its own fan
|
|
386
|
+
split_v = np.unique(vert[extra])
|
|
387
|
+
fans = np.flatnonzero(np.isin(vert, split_v))
|
|
388
|
+
cen = np.zeros((len(ufan), 3))
|
|
389
|
+
np.add.at(cen, finv, C[qid])
|
|
390
|
+
cen /= np.maximum(np.bincount(finv, minlength=len(ufan)), 1)[:, None]
|
|
391
|
+
P = V[vert[fans]] + 0.2 * (cen[fans] - V[vert[fans]])
|
|
392
|
+
P = _project(fam, P, step=0.1 * fam.h)
|
|
393
|
+
Vn = np.concatenate([V, np.zeros((len(extra), 3))])
|
|
394
|
+
Vn[newid[fans]] = P
|
|
395
|
+
V = Vn
|
|
396
|
+
return V, Q2
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
_B = np.array([[comb(DEG, j) * x ** j * (1 - x) ** (DEG - j) for j in range(DEG + 1)]
|
|
400
|
+
for x in np.linspace(0, 1, DEG + 1)])
|
|
401
|
+
_BI = np.linalg.inv(_B)
|
|
402
|
+
_BINT = np.linalg.inv(_B[1:-1, 1:-1])
|
|
403
|
+
_U = np.linspace(0, 1, DEG + 1)
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def bern(t):
|
|
407
|
+
t = np.atleast_1d(np.asarray(t, float))
|
|
408
|
+
return np.stack([comb(DEG, j) * t ** j * (1 - t) ** (DEG - j) for j in range(DEG + 1)], -1)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def dbern(t):
|
|
412
|
+
t = np.atleast_1d(np.asarray(t, float))
|
|
413
|
+
d = DEG
|
|
414
|
+
out = np.zeros(t.shape + (d + 1,))
|
|
415
|
+
for j in range(d + 1):
|
|
416
|
+
a = j * t ** (j - 1) * (1 - t) ** (d - j) if j > 0 else 0.0
|
|
417
|
+
b = (d - j) * t ** j * (1 - t) ** (d - j - 1) if j < d else 0.0
|
|
418
|
+
out[..., j] = comb(d, j) * (a - b)
|
|
419
|
+
return out
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _resample(P, n):
|
|
423
|
+
d = np.r_[0, np.cumsum(np.linalg.norm(np.diff(P, axis=0), axis=1))]
|
|
424
|
+
if d[-1] <= 0:
|
|
425
|
+
return np.repeat(P[:1], n, axis=0)
|
|
426
|
+
t = np.linspace(0, d[-1], n)
|
|
427
|
+
return np.stack([np.interp(t, d, P[:, k]) for k in range(3)], 1)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
class Sheet:
|
|
431
|
+
"""One family's patches: V, Q, edge poles {(a, b) a < b: (4, 3) from a
|
|
432
|
+
to b}, patch poles C (m, 4, 4, 3) (C[i, j]: i along u, j along v; u
|
|
433
|
+
from corner 0 to 1, v from corner 0 to 3)."""
|
|
434
|
+
|
|
435
|
+
def __init__(self, fam, V, Q):
|
|
436
|
+
self.fam, self.V, self.Q = fam, V, Q
|
|
437
|
+
E = np.sort(np.concatenate([Q[:, [k, (k + 1) % 4]] for k in range(4)]), axis=1)
|
|
438
|
+
self.E = np.unique(E, axis=0)
|
|
439
|
+
self.eidx = {(int(a), int(b)): i for i, (a, b) in enumerate(self.E)}
|
|
440
|
+
n = DEG + 1
|
|
441
|
+
A = self.E
|
|
442
|
+
# drape: onto the surface, evenly spaced by arc length
|
|
443
|
+
t = np.linspace(0, 1, 9)[1:-1][None, :, None]
|
|
444
|
+
P = V[A[:, 0]][:, None] * (1 - t) + V[A[:, 1]][:, None] * t
|
|
445
|
+
for r in range(4):
|
|
446
|
+
P = _project(fam, P.reshape(-1, 3), step=0.5 * fam.h).reshape(P.shape)
|
|
447
|
+
full = np.concatenate([V[A[:, 0]][:, None], P, V[A[:, 1]][:, None]], 1)
|
|
448
|
+
P = np.stack([_resample(x, 9 if r < 3 else n) for x in full])[:, 1:-1]
|
|
449
|
+
P = _project(fam, P.reshape(-1, 3), step=0.5 * fam.h).reshape(P.shape)
|
|
450
|
+
full = np.concatenate([V[A[:, 0]][:, None], P, V[A[:, 1]][:, None]], 1)
|
|
451
|
+
self.EP = np.einsum("ij,ejk->eik", _BI, full) # (e, 4, 3) poles a -> b
|
|
452
|
+
# an edge whose drape ran off (a pull onto another sheet, a fold)
|
|
453
|
+
# is the chord: wrong by its sag, never by a spike
|
|
454
|
+
S = np.einsum("ta,eax->etx", bern(np.linspace(0, 1, 9)), self.EP)
|
|
455
|
+
a0, a1 = V[A[:, 0]], V[A[:, 1]]
|
|
456
|
+
L = np.linalg.norm(a1 - a0, axis=1)
|
|
457
|
+
wild = np.linalg.norm(S - 0.5 * (a0 + a1)[:, None], axis=2).max(1) > 0.5 * L + 0.5 * fam.h
|
|
458
|
+
if wild.any():
|
|
459
|
+
t_ = _U[None, :, None]
|
|
460
|
+
self.EP[wild] = np.einsum("ij,ejk->eik", _BI, a0[wild][:, None] * (1 - t_) + a1[wild][:, None] * t_)
|
|
461
|
+
self.wild_edges = int(wild.sum())
|
|
462
|
+
sides = [self.curve(Q[:, 0], Q[:, 1]), self.curve(Q[:, 3], Q[:, 2]),
|
|
463
|
+
self.curve(Q[:, 0], Q[:, 3]), self.curve(Q[:, 1], Q[:, 2])]
|
|
464
|
+
sb, st, sl, sr = (np.einsum("ij,mjk->mik", _B, s) for s in sides)
|
|
465
|
+
Uu, W = np.meshgrid(_U, _U, indexing="ij")
|
|
466
|
+
X = ((1 - W)[None, ..., None] * sb[:, :, None] + W[None, ..., None] * st[:, :, None]
|
|
467
|
+
+ (1 - Uu)[None, ..., None] * sl[:, None] + Uu[None, ..., None] * sr[:, None]
|
|
468
|
+
- ((1 - Uu) * (1 - W))[None, ..., None] * sb[:, :1, None] - (Uu * (1 - W))[None, ..., None] * sb[:, -1:, None]
|
|
469
|
+
- ((1 - Uu) * W)[None, ..., None] * st[:, :1, None] - (Uu * W)[None, ..., None] * st[:, -1:, None])
|
|
470
|
+
X0 = X.copy()
|
|
471
|
+
inner = _project(fam, X[:, 1:-1, 1:-1].reshape(-1, 3), step=0.5 * fam.h)
|
|
472
|
+
X[:, 1:-1, 1:-1] = inner.reshape(len(Q), n - 2, n - 2, 3)
|
|
473
|
+
# an inside point pulled further than the patch is across went to
|
|
474
|
+
# another sheet: that patch keeps its Coons blend
|
|
475
|
+
span = np.linalg.norm(X0[:, 0, 0] - X0[:, -1, -1], axis=1) + np.linalg.norm(X0[:, -1, 0] - X0[:, 0, -1], axis=1)
|
|
476
|
+
moved = np.linalg.norm(X - X0, axis=3).max((1, 2))
|
|
477
|
+
off = moved > 0.35 * span
|
|
478
|
+
X[off] = X0[off]
|
|
479
|
+
self.wild_patches = int(off.sum())
|
|
480
|
+
C = np.zeros((len(Q), n, n, 3))
|
|
481
|
+
C[:, :, 0], C[:, :, -1], C[:, 0, :], C[:, -1, :] = sides[0], sides[1], sides[2], sides[3]
|
|
482
|
+
Cb = C.copy()
|
|
483
|
+
Cb[:, 1:-1, 1:-1] = 0
|
|
484
|
+
R = X[:, 1:-1, 1:-1] - np.einsum("ia,mabk,jb->mijk", _B[1:-1], Cb, _B[1:-1])
|
|
485
|
+
C[:, 1:-1, 1:-1] = np.einsum("ia,mabk,jb->mijk", _BINT, R, _BINT)
|
|
486
|
+
self.C = C
|
|
487
|
+
|
|
488
|
+
def curve(self, a, b):
|
|
489
|
+
"""Poles of edges a -> b (arrays of vertex ids): (m, 4, 3)."""
|
|
490
|
+
a, b = np.asarray(a), np.asarray(b)
|
|
491
|
+
lo, hi = np.minimum(a, b), np.maximum(a, b)
|
|
492
|
+
i = np.array([self.eidx[(int(x), int(y))] for x, y in zip(lo, hi)], np.int64)
|
|
493
|
+
P = self.EP[i]
|
|
494
|
+
rev = a > b
|
|
495
|
+
P[rev] = P[rev][:, ::-1]
|
|
496
|
+
return P
|
|
497
|
+
|
|
498
|
+
def at(self, qi, u, v):
|
|
499
|
+
"""Patch points: qi (k,), u, v (k,) or grids broadcast."""
|
|
500
|
+
return np.einsum("ka,kabx,kb->kx", bern(u), self.C[qi], bern(v))
|
|
501
|
+
|
|
502
|
+
def grid(self, qi, m):
|
|
503
|
+
"""(len(qi), m, m, 3) points at an m x m grid of (u, v)."""
|
|
504
|
+
B = bern(np.linspace(0, 1, m))
|
|
505
|
+
return np.einsum("ua,qabx,vb->quvx", B, self.C[qi], B)
|
|
506
|
+
|
|
507
|
+
def invert(self, q, x, m=9, it=12):
|
|
508
|
+
"""(u, v) of the point of patch q closest to x, not clamped: outside
|
|
509
|
+
[0, 1]^2 when x is past the patch's sides."""
|
|
510
|
+
G = self.grid(np.array([q]), m)[0]
|
|
511
|
+
k = int(np.argmin(np.linalg.norm(G.reshape(-1, 3) - x, axis=1)))
|
|
512
|
+
u, v = (k // m) / (m - 1), (k % m) / (m - 1)
|
|
513
|
+
C = self.C[q]
|
|
514
|
+
for _ in range(it):
|
|
515
|
+
bu, bv = bern(u)[0], bern(v)[0]
|
|
516
|
+
du, dv = dbern(u)[0], dbern(v)[0]
|
|
517
|
+
P = np.einsum("a,abx,b->x", bu, C, bv)
|
|
518
|
+
Pu = np.einsum("a,abx,b->x", du, C, bv)
|
|
519
|
+
Pv = np.einsum("a,abx,b->x", bu, C, dv)
|
|
520
|
+
J = np.stack([Pu, Pv], 1)
|
|
521
|
+
step = np.linalg.lstsq(J, x - P, rcond=None)[0]
|
|
522
|
+
u, v = u + float(step[0]), v + float(step[1])
|
|
523
|
+
if abs(step).max() < 1e-9:
|
|
524
|
+
break
|
|
525
|
+
return u, v
|
|
526
|
+
|
|
527
|
+
def folds(self, m=5):
|
|
528
|
+
"""Patches whose normal turns against the surface's somewhere (a
|
|
529
|
+
fold: the patch is too coarse for the surface there): (m,) bool."""
|
|
530
|
+
t = np.linspace(0.05, 0.95, m)
|
|
531
|
+
B, D = bern(t), dbern(t)
|
|
532
|
+
C = self.C
|
|
533
|
+
P = np.einsum("ua,qabx,vb->quvx", B, C, B)
|
|
534
|
+
Su = np.einsum("ua,qabx,vb->quvx", D, C, B)
|
|
535
|
+
Sv = np.einsum("ua,qabx,vb->quvx", B, C, D)
|
|
536
|
+
N = np.cross(Su, Sv)
|
|
537
|
+
_, g = self.fam.vg(P.reshape(-1, 3))
|
|
538
|
+
s = np.sign(np.einsum("ix,ix->i", N.reshape(-1, 3), g)).reshape(len(C), -1)
|
|
539
|
+
major = np.sign(s.sum()) or 1.0
|
|
540
|
+
return (s != major).any(1)
|
|
541
|
+
|
|
542
|
+
def deviation(self, m=7):
|
|
543
|
+
return self.deviation_of(np.arange(len(self.Q)), m)
|
|
544
|
+
|
|
545
|
+
def deviation_of(self, qi, m=7):
|
|
546
|
+
"""Distance (mm, first order) of patches qi from the level set at an
|
|
547
|
+
m x m grid each."""
|
|
548
|
+
P = self.grid(np.asarray(qi), m).reshape(-1, 3)
|
|
549
|
+
v, g = self.fam.vg(P)
|
|
550
|
+
return np.abs(v) / np.maximum(np.linalg.norm(g, axis=1), 1e-9)
|
|
551
|
+
|
|
552
|
+
|