geometrykit 0.5.1__tar.gz → 0.6.0__tar.gz
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.
- {geometrykit-0.5.1/python/geometrykit.egg-info → geometrykit-0.6.0}/PKG-INFO +1 -1
- {geometrykit-0.5.1 → geometrykit-0.6.0}/pyproject.toml +1 -1
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/__init__.py +1 -1
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/geometry/__init__.py +9 -2
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/geometry/curve.py +6 -2
- geometrykit-0.6.0/python/geometrykit/geometry/panel.py +140 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/geometry/surface.py +41 -16
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/runtimes/_npu/curve_np.py +80 -0
- geometrykit-0.6.0/python/geometrykit/runtimes/_npu/tessellate_np.py +299 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/runtimes/dispatch.py +58 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0/python/geometrykit.egg-info}/PKG-INFO +1 -1
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit.egg-info/SOURCES.txt +2 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/tests/test_engine_parity.py +1 -1
- {geometrykit-0.5.1 → geometrykit-0.6.0}/COPYING +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/COPYING.LESSER +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/README.md +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/cage.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/contract.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/engine.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/export.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/geometry/_bridge.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/geometry/biarc.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/geometry/field.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/geometry/frames.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/geometry/intersection.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/geometry/path.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/geometry/patterns.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/geometry/primitives.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/geometry/spiral.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/geometry/subdivision.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/geometry/transform.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/runtimes/__init__.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/runtimes/_npu/__init__.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/runtimes/_npu/fit_np.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/runtimes/_npu/linalg_np.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/runtimes/_npu/patch_g1.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/runtimes/_npu/patch_np.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/runtimes/_npu/section_np.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit/runtimes/_npu/surface_np.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit.egg-info/dependency_links.txt +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit.egg-info/requires.txt +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/python/geometrykit.egg-info/top_level.txt +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/setup.cfg +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/tests/test_cage.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/tests/test_contract.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/tests/test_engine_probe.py +0 -0
- {geometrykit-0.5.1 → geometrykit-0.6.0}/tests/test_repo_boundary.py +0 -0
|
@@ -12,7 +12,7 @@ build-backend = "setuptools.build_meta"
|
|
|
12
12
|
|
|
13
13
|
[project]
|
|
14
14
|
name = "geometrykit"
|
|
15
|
-
version = "0.
|
|
15
|
+
version = "0.6.0"
|
|
16
16
|
description = "Geometry primitives and NURBS data model — the geometry substrate for the ifckit family"
|
|
17
17
|
readme = "README.md"
|
|
18
18
|
requires-python = ">=3.9"
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# This file was generated with the assistance of an AI coding tool.
|
|
2
2
|
# Ported from ifckit/ifckit/geometry/__init__.py (LGPL-3.0-or-later heritage;
|
|
3
|
-
# ifckit is authored by the same copyright holder).
|
|
4
|
-
#
|
|
3
|
+
# ifckit is authored by the same copyright holder). The IFC
|
|
4
|
+
# serialisation layer (ifc_geom) stays in ifckit by design; the panel
|
|
5
|
+
# metrics (panel) were promoted here per the geometry-ownership plan.
|
|
5
6
|
|
|
6
7
|
"""
|
|
7
8
|
geometrykit.geometry
|
|
@@ -19,6 +20,7 @@ curve — Curve
|
|
|
19
20
|
surface — Surface (eval/normal/tessellate kernels; OCC oracle-compare only)
|
|
20
21
|
field — Field, FieldPoint, trim_grid, crossing_grid, divide_trim_edge
|
|
21
22
|
patterns — quad/diamond/triangle/wave/anglebox/brick/truchet/hex/hankin cells
|
|
23
|
+
panel — FieldReport, cells_report, field_report (panel metrics)
|
|
22
24
|
intersection — Intersection
|
|
23
25
|
subdivision — catmull_clark, extract_patches, write_obj
|
|
24
26
|
frames — FrameField, transport_frames, fixed_ref_frames, upvector_frames
|
|
@@ -42,6 +44,7 @@ from geometrykit.geometry.frames import (
|
|
|
42
44
|
upvector_frames,
|
|
43
45
|
)
|
|
44
46
|
from geometrykit.geometry.intersection import Intersection
|
|
47
|
+
from geometrykit.geometry.panel import FieldReport, cells_report, field_report
|
|
45
48
|
from geometrykit.geometry.path import Path, PathReport, assemble_path
|
|
46
49
|
from geometrykit.geometry.patterns import (
|
|
47
50
|
DiamondPattern,
|
|
@@ -122,6 +125,10 @@ __all__ = [
|
|
|
122
125
|
"truchet_cells",
|
|
123
126
|
"hankin_cells",
|
|
124
127
|
"triangle_cells",
|
|
128
|
+
# panel
|
|
129
|
+
"FieldReport",
|
|
130
|
+
"cells_report",
|
|
131
|
+
"field_report",
|
|
125
132
|
# subdivision
|
|
126
133
|
"catmull_clark",
|
|
127
134
|
"extract_patches",
|
|
@@ -451,12 +451,16 @@ class Curve:
|
|
|
451
451
|
except ImportError:
|
|
452
452
|
pass
|
|
453
453
|
|
|
454
|
-
|
|
454
|
+
# Kernel arc-length division (no OCC), engine-dispatched
|
|
455
|
+
# (mirror or the `_rust` accelerator, bit-identical).
|
|
455
456
|
from geometrykit.geometry.path import PathPoint
|
|
457
|
+
from geometrykit.runtimes import dispatch
|
|
456
458
|
|
|
457
459
|
return [
|
|
458
460
|
PathPoint(t=t, point=self.point_at(t), tangent=self.tangent_at(t))
|
|
459
|
-
for t in (
|
|
461
|
+
for t in dispatch.arclength_divide(
|
|
462
|
+
self, total, num=num, dist=dist, even=even
|
|
463
|
+
)
|
|
460
464
|
]
|
|
461
465
|
|
|
462
466
|
@property
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# This file was generated with the assistance of an AI coding tool.
|
|
2
|
+
# Promoted from ifckit/ifckit/geometry/panel_report.py (LGPL-3.0-or-later
|
|
3
|
+
# heritage; ifckit is authored by the same copyright holder as
|
|
4
|
+
# geometrykit).
|
|
5
|
+
"""Buildability metrics for panel populations (Eigensatz 2010 vocabulary).
|
|
6
|
+
|
|
7
|
+
``field_report`` measures a cell population: edge-length spread
|
|
8
|
+
(limited size bandwidth), right-angle deviation and planarity per
|
|
9
|
+
cell. Seam divergence/kink tracking is a follow-up once populations
|
|
10
|
+
map to IFC products — the per-cell metrics here are its input. Pure
|
|
11
|
+
Python.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import math
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from dataclasses import field as _dc_field
|
|
19
|
+
from typing import Any, Dict, List, Optional
|
|
20
|
+
|
|
21
|
+
from geometrykit.geometry.field import Field
|
|
22
|
+
from geometrykit.geometry.primitives import Vec, _fmt_num, _new_id, _short_id
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class FieldReport:
|
|
27
|
+
"""Immutable metric snapshot of a panel population.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
n_cells: Number of measured cells.
|
|
31
|
+
edge_min: Smallest edge length.
|
|
32
|
+
edge_mean: Mean edge length.
|
|
33
|
+
edge_max: Largest edge length.
|
|
34
|
+
angle_max_dev: Largest absolute deviation from the regular-polygon
|
|
35
|
+
interior angle (90° for quads, 60° for triangles), in degrees.
|
|
36
|
+
planarity_max: Largest distance of a corner to the plane of the
|
|
37
|
+
first three (model units; always 0 for triangles).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
type: str = "field_report"
|
|
41
|
+
id: Optional[str] = None
|
|
42
|
+
meta: Dict[str, Any] = _dc_field(default_factory=dict)
|
|
43
|
+
n_cells: int = 0
|
|
44
|
+
edge_min: float = 0.0
|
|
45
|
+
edge_mean: float = 0.0
|
|
46
|
+
edge_max: float = 0.0
|
|
47
|
+
angle_max_dev: float = 0.0
|
|
48
|
+
planarity_max: float = 0.0
|
|
49
|
+
|
|
50
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
51
|
+
"""Serialise to a plain (JSON-safe) dict."""
|
|
52
|
+
return {
|
|
53
|
+
"type": self.type,
|
|
54
|
+
"id": self.id,
|
|
55
|
+
"meta": dict(self.meta),
|
|
56
|
+
"n_cells": self.n_cells,
|
|
57
|
+
"edge_min": self.edge_min,
|
|
58
|
+
"edge_mean": self.edge_mean,
|
|
59
|
+
"edge_max": self.edge_max,
|
|
60
|
+
"angle_max_dev": self.angle_max_dev,
|
|
61
|
+
"planarity_max": self.planarity_max,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
def __str__(self) -> str:
|
|
65
|
+
return (
|
|
66
|
+
f"FieldReport(id={_short_id(self.id)}, n={self.n_cells}, "
|
|
67
|
+
f"edge=[{_fmt_num(self.edge_min)}..{_fmt_num(self.edge_max)}], "
|
|
68
|
+
f"angle_dev={_fmt_num(self.angle_max_dev)}°, "
|
|
69
|
+
f"planar={_fmt_num(self.planarity_max)})"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def cells_report(cells: List[List[Vec]]) -> FieldReport:
|
|
74
|
+
"""Measure a population of polygonal cells (any corner count >= 3).
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
cells: Corner lists (``[Vec × n]``), e.g. from ``patterns``.
|
|
78
|
+
Mixed populations are measured jointly against their own
|
|
79
|
+
regular-polygon angles.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
``FieldReport`` with zeros when *cells* is empty.
|
|
83
|
+
"""
|
|
84
|
+
edges: List[float] = []
|
|
85
|
+
max_dev = 0.0
|
|
86
|
+
max_flat = 0.0
|
|
87
|
+
for poly in cells:
|
|
88
|
+
n = len(poly)
|
|
89
|
+
if n < 3:
|
|
90
|
+
raise ValueError(f"Expected polygonal cells, got {n} corners")
|
|
91
|
+
ref_angle = (n - 2) * 180.0 / n
|
|
92
|
+
for k in range(n):
|
|
93
|
+
p, q = poly[k], poly[(k + 1) % n]
|
|
94
|
+
edges.append((q - p).length())
|
|
95
|
+
for k in range(n):
|
|
96
|
+
u, v = poly[(k - 1) % n] - poly[k], poly[(k + 1) % n] - poly[k]
|
|
97
|
+
denom = u.length() * v.length()
|
|
98
|
+
if denom < 1e-18:
|
|
99
|
+
continue
|
|
100
|
+
cos_a = max(-1.0, min(1.0, u.dot(v) / denom))
|
|
101
|
+
max_dev = max(max_dev, abs(ref_angle - math.degrees(math.acos(cos_a))))
|
|
102
|
+
a, b, c = poly[0], poly[1], poly[2]
|
|
103
|
+
nrm = (b - a) ** (c - a)
|
|
104
|
+
nl = nrm.length()
|
|
105
|
+
if nl > 1e-18:
|
|
106
|
+
for p in poly[3:]:
|
|
107
|
+
max_flat = max(max_flat, abs((p - a).dot(nrm / nl)))
|
|
108
|
+
if not edges:
|
|
109
|
+
return FieldReport(id=_new_id())
|
|
110
|
+
return FieldReport(
|
|
111
|
+
id=_new_id(),
|
|
112
|
+
n_cells=len(cells),
|
|
113
|
+
edge_min=min(edges),
|
|
114
|
+
edge_mean=sum(edges) / len(edges),
|
|
115
|
+
edge_max=max(edges),
|
|
116
|
+
angle_max_dev=max_dev,
|
|
117
|
+
planarity_max=max_flat,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def field_report(field: Field) -> FieldReport:
|
|
122
|
+
"""Measure the quad cells of a built field.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
field: Built field (see ``geometrykit.geometry.field``).
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
``FieldReport`` over ``field.cell_points()``.
|
|
129
|
+
"""
|
|
130
|
+
rep = cells_report(field.cell_points())
|
|
131
|
+
return FieldReport(
|
|
132
|
+
id=_new_id(),
|
|
133
|
+
meta={"field_id": field.id},
|
|
134
|
+
n_cells=rep.n_cells,
|
|
135
|
+
edge_min=rep.edge_min,
|
|
136
|
+
edge_mean=rep.edge_mean,
|
|
137
|
+
edge_max=rep.edge_max,
|
|
138
|
+
angle_max_dev=rep.angle_max_dev,
|
|
139
|
+
planarity_max=rep.planarity_max,
|
|
140
|
+
)
|
|
@@ -276,33 +276,54 @@ class Surface:
|
|
|
276
276
|
material: "dict | None" = None,
|
|
277
277
|
deflection: "float | object" = 0.01,
|
|
278
278
|
y_up: bool = True,
|
|
279
|
+
method: str = "occ",
|
|
279
280
|
) -> dict:
|
|
280
|
-
"""Triangulate the surface
|
|
281
|
-
|
|
282
|
-
Uses OCC ``BRepMesh_IncrementalMesh`` (requires ``pythonocc-core``).
|
|
281
|
+
"""Triangulate the surface and return a dict for 3D viewer.
|
|
283
282
|
|
|
284
283
|
Args:
|
|
285
|
-
nu: Ignored (
|
|
286
|
-
nv: Ignored (
|
|
284
|
+
nu: Ignored (both methods determine density themselves).
|
|
285
|
+
nv: Ignored (both methods determine density themselves).
|
|
287
286
|
label: Display name.
|
|
288
287
|
material: Visual properties (color, opacity, …).
|
|
289
288
|
deflection: Mesh deflection / ``TessellationDetail``.
|
|
290
289
|
y_up: If True (default), convert coordinates to
|
|
291
290
|
Three.js/glTF Y-up: ``(x, z, -y)``.
|
|
291
|
+
method: ``"occ"`` (default, unchanged): OCC
|
|
292
|
+
``BRepMesh_IncrementalMesh`` (requires
|
|
293
|
+
``pythonocc-core``). ``"adaptive"``: the
|
|
294
|
+
kernel tessellator (no OCC needed; watertight,
|
|
295
|
+
deflection-bounded, deterministic).
|
|
292
296
|
|
|
293
297
|
Returns:
|
|
294
298
|
A dict with ``primitive="triangles"``, ``positions``,
|
|
295
299
|
``indices``, and optional ``label``, ``material``.
|
|
296
300
|
"""
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
301
|
+
if method not in ("occ", "adaptive"):
|
|
302
|
+
raise ValueError(f"unknown mesh method {method!r}: use 'occ' or 'adaptive'")
|
|
303
|
+
if method == "adaptive":
|
|
304
|
+
from geometrykit.runtimes._npu import tessellate_np
|
|
305
|
+
|
|
306
|
+
tolerance = deflection if isinstance(deflection, float) else 0.01
|
|
307
|
+
verts, _normals, tris, _uvs = tessellate_np.adaptive_mesh(
|
|
308
|
+
self, deflection=tolerance
|
|
309
|
+
)
|
|
310
|
+
if y_up:
|
|
311
|
+
positions = []
|
|
312
|
+
for k in range(0, len(verts), 3):
|
|
313
|
+
positions.extend((verts[k], verts[k + 2], -verts[k + 1]))
|
|
314
|
+
else:
|
|
315
|
+
positions = list(verts)
|
|
316
|
+
indices = [i for tri in tris for i in tri]
|
|
303
317
|
else:
|
|
304
|
-
|
|
305
|
-
|
|
318
|
+
from geometrykit.geometry.surface import occ_tessellate, require_occ
|
|
319
|
+
|
|
320
|
+
require_occ()
|
|
321
|
+
verts, tris = occ_tessellate(self, deflection)
|
|
322
|
+
if y_up:
|
|
323
|
+
positions = [c for v in verts for c in (v[0], v[2], -v[1])]
|
|
324
|
+
else:
|
|
325
|
+
positions = [c for v in verts for c in v]
|
|
326
|
+
indices = [i - 1 for tri in tris for i in tri]
|
|
306
327
|
|
|
307
328
|
d: dict = {
|
|
308
329
|
"primitive": "triangles",
|
|
@@ -320,11 +341,13 @@ class Surface:
|
|
|
320
341
|
material: "dict | None" = None,
|
|
321
342
|
deflection: "float | object" = 0.01,
|
|
322
343
|
y_up: bool = True,
|
|
344
|
+
method: str = "occ",
|
|
323
345
|
) -> dict:
|
|
324
346
|
"""Return a ``__type__: "mesh"`` dict ready for the viewer pipeline.
|
|
325
347
|
|
|
326
|
-
|
|
327
|
-
|
|
348
|
+
``method`` selects the tessellator: ``"occ"`` (default,
|
|
349
|
+
unchanged) or ``"adaptive"`` (kernel, no OCC needed). See
|
|
350
|
+
``Path.preview()`` for the full material documentation.
|
|
328
351
|
|
|
329
352
|
Args:
|
|
330
353
|
label: Display name.
|
|
@@ -332,13 +355,15 @@ class Surface:
|
|
|
332
355
|
deflection: Mesh deflection / ``TessellationDetail`` (see ifckit).
|
|
333
356
|
y_up: If True (default), convert coordinates to
|
|
334
357
|
Three.js/glTF Y-up: ``(x, z, -y)``.
|
|
358
|
+
method: ``"occ"`` or ``"adaptive"`` (see ``to_mesh_dict``).
|
|
335
359
|
|
|
336
360
|
Returns:
|
|
337
361
|
``{"__type__": "mesh", "primitive": "triangles", …}``
|
|
338
362
|
"""
|
|
339
363
|
return {
|
|
340
364
|
"__type__": "mesh",
|
|
341
|
-
**self.to_mesh_dict(label=label, material=material, deflection=deflection, y_up=y_up
|
|
365
|
+
**self.to_mesh_dict(label=label, material=material, deflection=deflection, y_up=y_up,
|
|
366
|
+
method=method),
|
|
342
367
|
}
|
|
343
368
|
|
|
344
369
|
# ── IFC serialisation ──────────────────────────────────────────
|
|
@@ -349,3 +349,83 @@ def group_and_orient(curves: Sequence, tol: float = 1e-6) -> list:
|
|
|
349
349
|
|
|
350
350
|
groups.append(oriented)
|
|
351
351
|
return groups
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def arclength_divide(
|
|
355
|
+
point_at,
|
|
356
|
+
total: float,
|
|
357
|
+
num: "int | None" = None,
|
|
358
|
+
dist: "float | None" = None,
|
|
359
|
+
even: bool = True,
|
|
360
|
+
) -> list:
|
|
361
|
+
"""Kernel arc-length division: normalized parameters ``∈ [0, 1]``.
|
|
362
|
+
|
|
363
|
+
A dense chord table over ``point_at`` is inverted per target
|
|
364
|
+
distance (binary search + linear interpolation). Arc-length
|
|
365
|
+
uniformity holds within a few percent on fair curves (the old
|
|
366
|
+
parameter-uniform fallback measured 2.7x gap spread on a Hermite
|
|
367
|
+
wiggle); straight chords between outputs vary more where the
|
|
368
|
+
curve bends, which is geometric, not error.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
point_at: callable ``t → xyz`` (t in [0, 1]).
|
|
372
|
+
total: curve length in the same units as ``dist``.
|
|
373
|
+
num: point count (endpoints included).
|
|
374
|
+
dist: target spacing; ``even`` distributes evenly with the
|
|
375
|
+
endpoints included, otherwise fixed steps from the
|
|
376
|
+
start plus the exact endpoint.
|
|
377
|
+
even: spacing rule for *dist* (see above).
|
|
378
|
+
|
|
379
|
+
Returns:
|
|
380
|
+
Ascending parameter list (first 0.0, last 1.0).
|
|
381
|
+
"""
|
|
382
|
+
import math
|
|
383
|
+
|
|
384
|
+
if total < _EPS:
|
|
385
|
+
raise ValueError("Cannot divide a zero-length curve")
|
|
386
|
+
table_n = max(64, 8 * (num if num is not None else 16))
|
|
387
|
+
samples = [point_at(i / table_n) for i in range(table_n + 1)]
|
|
388
|
+
cumulative = [0.0]
|
|
389
|
+
for a, b in zip(samples, samples[1:]):
|
|
390
|
+
cumulative.append(
|
|
391
|
+
cumulative[-1]
|
|
392
|
+
+ math.sqrt(
|
|
393
|
+
(b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2 + (b[2] - a[2]) ** 2
|
|
394
|
+
)
|
|
395
|
+
)
|
|
396
|
+
table_total = cumulative[-1]
|
|
397
|
+
if table_total < _EPS:
|
|
398
|
+
raise ValueError("Cannot divide a zero-length curve")
|
|
399
|
+
if num is not None:
|
|
400
|
+
targets = [table_total * k / (num - 1) for k in range(num)]
|
|
401
|
+
elif even:
|
|
402
|
+
segs = max(1, math.ceil(table_total / dist))
|
|
403
|
+
targets = [table_total * k / segs for k in range(segs + 1)]
|
|
404
|
+
else:
|
|
405
|
+
targets = []
|
|
406
|
+
d = 0.0
|
|
407
|
+
while d < table_total:
|
|
408
|
+
targets.append(d)
|
|
409
|
+
d += dist
|
|
410
|
+
targets.append(table_total)
|
|
411
|
+
|
|
412
|
+
out = []
|
|
413
|
+
for target in targets:
|
|
414
|
+
lo, hi = 0, table_n
|
|
415
|
+
while lo < hi:
|
|
416
|
+
mid = (lo + hi) // 2
|
|
417
|
+
if cumulative[mid] < target:
|
|
418
|
+
lo = mid + 1
|
|
419
|
+
else:
|
|
420
|
+
hi = mid
|
|
421
|
+
if lo <= 0:
|
|
422
|
+
out.append(0.0)
|
|
423
|
+
elif lo > table_n:
|
|
424
|
+
out.append(1.0)
|
|
425
|
+
else:
|
|
426
|
+
span = cumulative[lo] - cumulative[lo - 1]
|
|
427
|
+
if span <= 0.0:
|
|
428
|
+
out.append(lo / table_n)
|
|
429
|
+
else:
|
|
430
|
+
out.append((lo - 1 + (target - cumulative[lo - 1]) / span) / table_n)
|
|
431
|
+
return out
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# This file was generated with the assistance of an AI coding tool.
|
|
2
|
+
"""Adaptive kernel tessellation: OCC-free surface meshing.
|
|
3
|
+
|
|
4
|
+
A restricted quadtree over the parameter domain: quads split while
|
|
5
|
+
their geometric error exceeds the tolerances, neighbours are forced
|
|
6
|
+
to differ by at most one level, and split edges close with a fan from
|
|
7
|
+
the evaluated centroid (green closure). Every vertex is an evaluated
|
|
8
|
+
surface point and every edge is shared exactly, so the mesh is
|
|
9
|
+
watertight by construction; the only numerical content is the kernel
|
|
10
|
+
evaluation itself.
|
|
11
|
+
|
|
12
|
+
Error metric per quad: maximum edge-midpoint deviation (surface point
|
|
13
|
+
at the edge midpoint vs. the 3D edge midpoint) against ``deflection``,
|
|
14
|
+
and maximum corner-normal deviation against the quad normal versus
|
|
15
|
+
``angle_tol`` (radians). Stations are dyadic integer lattice points
|
|
16
|
+
``(iu, iv, level)`` with ``u = u0 + (u1-u0)*iu/2**level``, so shared
|
|
17
|
+
edges compute bit-identical vertices on both sides.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import math
|
|
23
|
+
|
|
24
|
+
from geometrykit.runtimes._npu.surface_np import (
|
|
25
|
+
_data_of,
|
|
26
|
+
_eval_all_data,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _station_uv(u0: float, u1: float, v0: float, v1: float,
|
|
31
|
+
iu: int, iv: int, level: int) -> tuple:
|
|
32
|
+
"""Domain point of a dyadic lattice station."""
|
|
33
|
+
scale = 1.0 / (1 << level)
|
|
34
|
+
return (
|
|
35
|
+
u0 + (u1 - u0) * iu * scale,
|
|
36
|
+
v0 + (v1 - v0) * iv * scale,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def adaptive_mesh(surf, deflection: float = 0.01, angle_tol: float = 0.5,
|
|
41
|
+
max_depth: int = 8, base_level: int = 2) -> tuple:
|
|
42
|
+
"""Triangulate *surf* adaptively; ``(verts, normals, tris)``.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
surf: kernel Surface.
|
|
46
|
+
deflection: maximum distance of the surface from any triangle.
|
|
47
|
+
angle_tol: maximum normal deviation within a quad (radians).
|
|
48
|
+
max_depth: deepest quad level (base_level + refinements).
|
|
49
|
+
base_level: starting grid is ``2**base_level`` quads per side.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
``(verts, normals, tris, uvs)`` — ``verts`` flat ``[x, y, z]``,
|
|
53
|
+
``normals`` flat unit normals per vertex, ``tris`` index
|
|
54
|
+
triples, ``uvs`` flat ``[u, v]`` per vertex (the parameter
|
|
55
|
+
track, like section chains carry). Every edge is shared by
|
|
56
|
+
exactly two triangles (watertight) on non-degenerate
|
|
57
|
+
surfaces.
|
|
58
|
+
"""
|
|
59
|
+
if deflection <= 0.0:
|
|
60
|
+
raise ValueError(f"deflection must be positive, got {deflection}")
|
|
61
|
+
if angle_tol <= 0.0:
|
|
62
|
+
raise ValueError(f"angle_tol must be positive, got {angle_tol}")
|
|
63
|
+
if max_depth < base_level:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"max_depth {max_depth} below base_level {base_level}"
|
|
66
|
+
)
|
|
67
|
+
d = _data_of(surf)
|
|
68
|
+
u0, u1 = d.uflat[d.pu], d.uflat[-d.pu - 1]
|
|
69
|
+
v0, v1 = d.vflat[d.pv], d.vflat[-d.pv - 1]
|
|
70
|
+
if not (u1 > u0 and v1 > v0):
|
|
71
|
+
raise ValueError("degenerate parameter domain")
|
|
72
|
+
|
|
73
|
+
# eval cache: (iu, iv, level) -> (point, normal), one basis build
|
|
74
|
+
cache: dict = {}
|
|
75
|
+
|
|
76
|
+
def _eval(iu: int, iv: int, level: int) -> tuple:
|
|
77
|
+
key = (iu, iv, level)
|
|
78
|
+
hit = cache.get(key)
|
|
79
|
+
if hit is None:
|
|
80
|
+
u, v = _station_uv(u0, u1, v0, v1, iu, iv, level)
|
|
81
|
+
pt, su, sv = _eval_all_data(d, u, v)
|
|
82
|
+
nx = su.y * sv.z - su.z * sv.y
|
|
83
|
+
ny = su.z * sv.x - su.x * sv.z
|
|
84
|
+
nz = su.x * sv.y - su.y * sv.x
|
|
85
|
+
n = math.sqrt(nx * nx + ny * ny + nz * nz)
|
|
86
|
+
if n > 0.0:
|
|
87
|
+
nx, ny, nz = nx / n, ny / n, nz / n
|
|
88
|
+
hit = ((pt.x, pt.y, pt.z), (nx, ny, nz))
|
|
89
|
+
cache[key] = hit
|
|
90
|
+
return hit
|
|
91
|
+
|
|
92
|
+
def _corners(i: int, j: int, level: int) -> list:
|
|
93
|
+
return [(i, j, level), (i + 1, j, level),
|
|
94
|
+
(i + 1, j + 1, level), (i, j + 1, level)]
|
|
95
|
+
|
|
96
|
+
def _mid(a: tuple, b: tuple) -> tuple:
|
|
97
|
+
(aiu, aiv, al), (biu, biv, bl) = a, b
|
|
98
|
+
assert al == bl, "midpoint of mixed-level stations"
|
|
99
|
+
return (aiu + biu, aiv + biv, al + 1)
|
|
100
|
+
|
|
101
|
+
def _quad_error(i: int, j: int, level: int) -> tuple:
|
|
102
|
+
"""(edge deviation, normal deviation) of one quad.
|
|
103
|
+
|
|
104
|
+
Edge deviation covers quad edges, both diagonals and the
|
|
105
|
+
centroid spoke deviations, so every emitted triangle edge is
|
|
106
|
+
bounded (green-closure fans introduce diagonals and spokes
|
|
107
|
+
that edge-only checks would miss).
|
|
108
|
+
"""
|
|
109
|
+
corners = _corners(i, j, level)
|
|
110
|
+
pts = [_eval(*c)[0] for c in corners]
|
|
111
|
+
norms = [_eval(*c)[1] for c in corners]
|
|
112
|
+
edge_dev = 0.0
|
|
113
|
+
for k in range(4):
|
|
114
|
+
a, b = corners[k], corners[(k + 1) % 4]
|
|
115
|
+
mid = _eval(*_mid(a, b))[0]
|
|
116
|
+
pa, pb = pts[k], pts[(k + 1) % 4]
|
|
117
|
+
edge_dev = max(edge_dev, abs(mid[0] - 0.5 * (pa[0] + pb[0]))
|
|
118
|
+
+ abs(mid[1] - 0.5 * (pa[1] + pb[1]))
|
|
119
|
+
+ abs(mid[2] - 0.5 * (pa[2] + pb[2])))
|
|
120
|
+
# diagonals and centroid (fan spokes land here)
|
|
121
|
+
cu = (corners[0][0] + corners[2][0], corners[0][1] + corners[2][1],
|
|
122
|
+
corners[0][2] + 1)
|
|
123
|
+
cc = _eval(*cu)[0]
|
|
124
|
+
for pa, pb in ((pts[0], pts[2]), (pts[1], pts[3])):
|
|
125
|
+
edge_dev = max(edge_dev, abs(cc[0] - 0.5 * (pa[0] + pb[0]))
|
|
126
|
+
+ abs(cc[1] - 0.5 * (pa[1] + pb[1]))
|
|
127
|
+
+ abs(cc[2] - 0.5 * (pa[2] + pb[2])))
|
|
128
|
+
bil = tuple(sum(p[k] for p in pts) / 4.0 for k in range(3))
|
|
129
|
+
edge_dev = max(edge_dev, abs(cc[0] - bil[0])
|
|
130
|
+
+ abs(cc[1] - bil[1]) + abs(cc[2] - bil[2]))
|
|
131
|
+
# fan spokes: the green-closure fan connects the centroid to
|
|
132
|
+
# every boundary station, so bound every spoke midpoint (covers
|
|
133
|
+
# all split patterns; edge midpoints already evaluated above)
|
|
134
|
+
cstat = (corners[0][0] + corners[2][0], corners[0][1] + corners[2][1],
|
|
135
|
+
corners[0][2] + 1)
|
|
136
|
+
boundary = list(corners)
|
|
137
|
+
for k in range(4):
|
|
138
|
+
a, b = corners[k], corners[(k + 1) % 4]
|
|
139
|
+
boundary.append(_mid(a, b))
|
|
140
|
+
for (biu, biv, bl) in boundary:
|
|
141
|
+
# centroid lives at level L+1, boundary at L or L+1:
|
|
142
|
+
# lift the boundary numerator before halving
|
|
143
|
+
lift = 1 << (cstat[2] - bl)
|
|
144
|
+
mstat = (cstat[0] + lift * biu, cstat[1] + lift * biv,
|
|
145
|
+
cstat[2] + 1)
|
|
146
|
+
sm = _eval(*mstat)[0]
|
|
147
|
+
bp = _eval(biu, biv, bl)[0]
|
|
148
|
+
edge_dev = max(edge_dev, abs(sm[0] - 0.5 * (cc[0] + bp[0]))
|
|
149
|
+
+ abs(sm[1] - 0.5 * (cc[1] + bp[1]))
|
|
150
|
+
+ abs(sm[2] - 0.5 * (cc[2] + bp[2])))
|
|
151
|
+
# quad normal from the diagonals
|
|
152
|
+
d0 = (pts[2][0] - pts[0][0], pts[2][1] - pts[0][1], pts[2][2] - pts[0][2])
|
|
153
|
+
d1 = (pts[3][0] - pts[1][0], pts[3][1] - pts[1][1], pts[3][2] - pts[1][2])
|
|
154
|
+
qn = (d0[1] * d1[2] - d0[2] * d1[1],
|
|
155
|
+
d0[2] * d1[0] - d0[0] * d1[2],
|
|
156
|
+
d0[0] * d1[1] - d0[1] * d1[0])
|
|
157
|
+
ql = math.sqrt(qn[0] ** 2 + qn[1] ** 2 + qn[2] ** 2)
|
|
158
|
+
normal_dev = 0.0
|
|
159
|
+
if ql > 0.0:
|
|
160
|
+
qn = (qn[0] / ql, qn[1] / ql, qn[2] / ql)
|
|
161
|
+
for n in norms:
|
|
162
|
+
cos_a = max(-1.0, min(1.0, n[0] * qn[0] + n[1] * qn[1] + n[2] * qn[2]))
|
|
163
|
+
normal_dev = max(normal_dev, math.acos(cos_a))
|
|
164
|
+
return edge_dev, normal_dev
|
|
165
|
+
|
|
166
|
+
# leaves: (i, j, level) quads, base grid first
|
|
167
|
+
n_base = 1 << base_level
|
|
168
|
+
leaves: dict = {
|
|
169
|
+
(i, j, base_level): None
|
|
170
|
+
for i in range(n_base) for j in range(n_base)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
def _neighbors(i: int, j: int, level: int) -> list:
|
|
174
|
+
"""Candidate edge-neighbours at levels level-1..level+1."""
|
|
175
|
+
found = []
|
|
176
|
+
for key in ((i - 1, j, level), (i + 1, j, level),
|
|
177
|
+
(i, j - 1, level), (i, j + 1, level),
|
|
178
|
+
(i >> 1, j >> 1, level - 1)
|
|
179
|
+
if level > 0 else None):
|
|
180
|
+
if key is not None and key in leaves and key not in found:
|
|
181
|
+
found.append(key)
|
|
182
|
+
for key in ((2 * i - 1, 2 * j, level + 1),
|
|
183
|
+
(2 * i + 2, 2 * j, level + 1),
|
|
184
|
+
(2 * i - 1, 2 * j + 1, level + 1),
|
|
185
|
+
(2 * i + 2, 2 * j + 1, level + 1),
|
|
186
|
+
(2 * i, 2 * j - 1, level + 1),
|
|
187
|
+
(2 * i + 1, 2 * j - 1, level + 1),
|
|
188
|
+
(2 * i, 2 * j + 2, level + 1),
|
|
189
|
+
(2 * i + 1, 2 * j + 2, level + 1)):
|
|
190
|
+
if key in leaves and key not in found:
|
|
191
|
+
found.append(key)
|
|
192
|
+
return found
|
|
193
|
+
|
|
194
|
+
# refine to fixpoint: error splits, then balance (2:1 at most)
|
|
195
|
+
while True:
|
|
196
|
+
marked = set()
|
|
197
|
+
for key in sorted(leaves):
|
|
198
|
+
i, j, level = key
|
|
199
|
+
if level >= max_depth:
|
|
200
|
+
continue
|
|
201
|
+
edge_dev, normal_dev = _quad_error(i, j, level)
|
|
202
|
+
if edge_dev > deflection or normal_dev > angle_tol:
|
|
203
|
+
marked.add(key)
|
|
204
|
+
if not marked:
|
|
205
|
+
break
|
|
206
|
+
# balance: no edge-neighbour more than one level coarser
|
|
207
|
+
while True:
|
|
208
|
+
extra = set()
|
|
209
|
+
for key in sorted(leaves):
|
|
210
|
+
if key in marked:
|
|
211
|
+
continue
|
|
212
|
+
i, j, level = key
|
|
213
|
+
for nb in _neighbors(i, j, level):
|
|
214
|
+
if nb in marked:
|
|
215
|
+
continue
|
|
216
|
+
if nb[2] < level - 1 and nb[2] < max_depth:
|
|
217
|
+
# a marked finer quad touches this one: split up
|
|
218
|
+
if any(m[2] > nb[2] + 1 for m in marked
|
|
219
|
+
if _quads_touch(nb, m)):
|
|
220
|
+
extra.add(nb)
|
|
221
|
+
if not extra:
|
|
222
|
+
break
|
|
223
|
+
marked |= extra
|
|
224
|
+
for key in marked:
|
|
225
|
+
i, j, level = key
|
|
226
|
+
del leaves[key]
|
|
227
|
+
leaves[(2 * i, 2 * j, level + 1)] = None
|
|
228
|
+
leaves[(2 * i + 1, 2 * j, level + 1)] = None
|
|
229
|
+
leaves[(2 * i, 2 * j + 1, level + 1)] = None
|
|
230
|
+
leaves[(2 * i + 1, 2 * j + 1, level + 1)] = None
|
|
231
|
+
|
|
232
|
+
# triangulate with green closure: split edges fan from the
|
|
233
|
+
# evaluated centroid; every vertex is on the surface
|
|
234
|
+
verts: list = []
|
|
235
|
+
normals: list = []
|
|
236
|
+
uvs: list = []
|
|
237
|
+
index_of: dict = {}
|
|
238
|
+
|
|
239
|
+
def _vertex(station: tuple) -> int:
|
|
240
|
+
idx = index_of.get(station)
|
|
241
|
+
if idx is None:
|
|
242
|
+
(px, py, pz), (nx, ny, nz) = _eval(*station)
|
|
243
|
+
idx = len(verts) // 3
|
|
244
|
+
verts.extend((px, py, pz))
|
|
245
|
+
normals.extend((nx, ny, nz))
|
|
246
|
+
u, v = _station_uv(u0, u1, v0, v1, *station)
|
|
247
|
+
uvs.extend((u, v))
|
|
248
|
+
index_of[station] = idx
|
|
249
|
+
return idx
|
|
250
|
+
|
|
251
|
+
def _covers(candidate: tuple, a: tuple, b: tuple, level: int) -> bool:
|
|
252
|
+
"""True when the level-(level+1) quad covers part of edge a-b."""
|
|
253
|
+
(ni, nj, _nl) = candidate
|
|
254
|
+
(aiu, aiv, _al), (biu, biv, _bl) = a, b
|
|
255
|
+
if aiu == biu: # vertical edge
|
|
256
|
+
eu = 2 * aiu
|
|
257
|
+
lo, hi = sorted((2 * aiv, 2 * biv))
|
|
258
|
+
return (ni == eu or ni + 1 == eu) and nj < hi and nj + 1 > lo
|
|
259
|
+
# horizontal edge
|
|
260
|
+
ev = 2 * aiv
|
|
261
|
+
lo, hi = sorted((2 * aiu, 2 * biu))
|
|
262
|
+
return (nj == ev or nj + 1 == ev) and ni < hi and ni + 1 > lo
|
|
263
|
+
|
|
264
|
+
tris: list = []
|
|
265
|
+
for key in sorted(leaves):
|
|
266
|
+
i, j, level = key
|
|
267
|
+
corners = _corners(i, j, level)
|
|
268
|
+
finer = [nb for nb in _neighbors(i, j, level) if nb[2] == level + 1]
|
|
269
|
+
loop = [corners[0]]
|
|
270
|
+
for k in range(4):
|
|
271
|
+
a, b = corners[k], corners[(k + 1) % 4]
|
|
272
|
+
if any(_covers(nb, a, b, level) for nb in finer):
|
|
273
|
+
loop.append(_mid(a, b))
|
|
274
|
+
if k < 3:
|
|
275
|
+
loop.append(b)
|
|
276
|
+
if len(loop) == 4:
|
|
277
|
+
a, b, c, e = (_vertex(p) for p in loop)
|
|
278
|
+
tris.extend(((a, b, c), (a, c, e)))
|
|
279
|
+
else:
|
|
280
|
+
center = (corners[0][0] + corners[2][0],
|
|
281
|
+
corners[0][1] + corners[2][1], corners[0][2] + 1)
|
|
282
|
+
c = _vertex(center)
|
|
283
|
+
ring = [_vertex(p) for p in loop]
|
|
284
|
+
for k in range(len(ring)):
|
|
285
|
+
tris.append((c, ring[k], ring[(k + 1) % len(ring)]))
|
|
286
|
+
return verts, normals, tris, uvs
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _quads_touch(a: tuple, b: tuple) -> bool:
|
|
290
|
+
"""True when quads a and b share an edge segment."""
|
|
291
|
+
(ai, aj, al), (bi, bj, bl) = a, b
|
|
292
|
+
scale = 1 << max(al, bl)
|
|
293
|
+
au0, au1 = ai * (scale >> al), (ai + 1) * (scale >> al)
|
|
294
|
+
av0, av1 = aj * (scale >> al), (aj + 1) * (scale >> al)
|
|
295
|
+
bu0, bu1 = bi * (scale >> bl), (bi + 1) * (scale >> bl)
|
|
296
|
+
bv0, bv1 = bj * (scale >> bl), (bj + 1) * (scale >> bl)
|
|
297
|
+
share_v = (au1 == bu0 or bu1 == au0) and av0 < bv1 and bv0 < av1
|
|
298
|
+
share_h = (av1 == bv0 or bv1 == av0) and au0 < bu1 and bu0 < au1
|
|
299
|
+
return share_v or share_h
|
|
@@ -51,6 +51,64 @@ def basis_row_list(degree: int, uflat, stations):
|
|
|
51
51
|
]
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
def tessellate(surf, deflection: float = 0.01, angle_tol: float = 0.5,
|
|
55
|
+
max_depth: int = 8, base_level: int = 2):
|
|
56
|
+
"""Mirror `tessellate_np.adaptive_mesh` under the active engine.
|
|
57
|
+
|
|
58
|
+
Returns `(verts, normals, tris, uvs)` — flat xyz vertices, flat
|
|
59
|
+
unit normals, flat index triples, flat uv tracks. Bit-identical
|
|
60
|
+
across engines by harness.
|
|
61
|
+
"""
|
|
62
|
+
from geometrykit.runtimes._npu import tessellate_np
|
|
63
|
+
|
|
64
|
+
accel = _rust()
|
|
65
|
+
if accel is None:
|
|
66
|
+
return tessellate_np.adaptive_mesh(
|
|
67
|
+
surf, deflection=deflection, angle_tol=angle_tol,
|
|
68
|
+
max_depth=max_depth, base_level=base_level,
|
|
69
|
+
)
|
|
70
|
+
from geometrykit import _rust as _ext
|
|
71
|
+
|
|
72
|
+
verts, normals, flat_tris, uvs = _ext.tessellate_grid(
|
|
73
|
+
surf, float(deflection), float(angle_tol), int(max_depth),
|
|
74
|
+
int(base_level),
|
|
75
|
+
)
|
|
76
|
+
tris = [
|
|
77
|
+
(flat_tris[k], flat_tris[k + 1], flat_tris[k + 2])
|
|
78
|
+
for k in range(0, len(flat_tris), 3)
|
|
79
|
+
]
|
|
80
|
+
return verts, normals, tris, uvs
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def arclength_divide(curve, total: float, num=None, dist=None, even: bool = True):
|
|
84
|
+
"""Mirror `curve_np.arclength_divide` under the active engine.
|
|
85
|
+
|
|
86
|
+
Returns ascending normalized parameters (first 0.0, last 1.0),
|
|
87
|
+
bit-identical across engines by harness.
|
|
88
|
+
"""
|
|
89
|
+
from geometrykit.runtimes._npu import curve_np
|
|
90
|
+
|
|
91
|
+
def _sample(t: float):
|
|
92
|
+
p = curve.point_at(t)
|
|
93
|
+
return (p.x, p.y, p.z)
|
|
94
|
+
|
|
95
|
+
accel = _rust()
|
|
96
|
+
if accel is None:
|
|
97
|
+
return curve_np.arclength_divide(_sample, total, num=num, dist=dist, even=even)
|
|
98
|
+
from geometrykit import _rust as _ext
|
|
99
|
+
|
|
100
|
+
flat = []
|
|
101
|
+
for p in curve.points:
|
|
102
|
+
flat.extend((p.x, p.y, p.z))
|
|
103
|
+
weights = (
|
|
104
|
+
list(curve._weights) if curve._weights is not None else []
|
|
105
|
+
)
|
|
106
|
+
return _ext.curve_divide_grid(
|
|
107
|
+
flat, weights, int(curve.degree), list(curve.knots),
|
|
108
|
+
list(curve.multiplicities), float(total), num, dist, bool(even),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
54
112
|
def plane_section_chains(surf, origin, normal, offsets, nu: int, nv: int):
|
|
55
113
|
"""Mirror `section_np.section_levels` under the active engine.
|
|
56
114
|
|
|
@@ -19,6 +19,7 @@ python/geometrykit/geometry/curve.py
|
|
|
19
19
|
python/geometrykit/geometry/field.py
|
|
20
20
|
python/geometrykit/geometry/frames.py
|
|
21
21
|
python/geometrykit/geometry/intersection.py
|
|
22
|
+
python/geometrykit/geometry/panel.py
|
|
22
23
|
python/geometrykit/geometry/path.py
|
|
23
24
|
python/geometrykit/geometry/patterns.py
|
|
24
25
|
python/geometrykit/geometry/primitives.py
|
|
@@ -36,6 +37,7 @@ python/geometrykit/runtimes/_npu/patch_g1.py
|
|
|
36
37
|
python/geometrykit/runtimes/_npu/patch_np.py
|
|
37
38
|
python/geometrykit/runtimes/_npu/section_np.py
|
|
38
39
|
python/geometrykit/runtimes/_npu/surface_np.py
|
|
40
|
+
python/geometrykit/runtimes/_npu/tessellate_np.py
|
|
39
41
|
tests/test_cage.py
|
|
40
42
|
tests/test_contract.py
|
|
41
43
|
tests/test_engine_parity.py
|
|
@@ -112,7 +112,7 @@ class TestAcceleratorParity:
|
|
|
112
112
|
def test_core_version_stamp(self):
|
|
113
113
|
from geometrykit import _rust
|
|
114
114
|
|
|
115
|
-
assert _rust.core_version() == "geometrykit-core 0.
|
|
115
|
+
assert _rust.core_version() == "geometrykit-core 0.2.0"
|
|
116
116
|
|
|
117
117
|
def test_bits_hex_agrees_with_contract(self):
|
|
118
118
|
from geometrykit import _rust
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|