geometrykit 0.5.0__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.
Files changed (50) hide show
  1. geometrykit-0.6.0/PKG-INFO +107 -0
  2. geometrykit-0.6.0/README.md +78 -0
  3. {geometrykit-0.5.0 → geometrykit-0.6.0}/pyproject.toml +1 -1
  4. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/__init__.py +1 -1
  5. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/__init__.py +9 -2
  6. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/curve.py +6 -2
  7. geometrykit-0.6.0/python/geometrykit/geometry/panel.py +140 -0
  8. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/surface.py +41 -16
  9. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/runtimes/_npu/curve_np.py +80 -0
  10. geometrykit-0.6.0/python/geometrykit/runtimes/_npu/tessellate_np.py +299 -0
  11. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/runtimes/dispatch.py +58 -0
  12. geometrykit-0.6.0/python/geometrykit.egg-info/PKG-INFO +107 -0
  13. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit.egg-info/SOURCES.txt +2 -0
  14. {geometrykit-0.5.0 → geometrykit-0.6.0}/tests/test_engine_parity.py +1 -1
  15. geometrykit-0.5.0/PKG-INFO +0 -94
  16. geometrykit-0.5.0/README.md +0 -65
  17. geometrykit-0.5.0/python/geometrykit.egg-info/PKG-INFO +0 -94
  18. {geometrykit-0.5.0 → geometrykit-0.6.0}/COPYING +0 -0
  19. {geometrykit-0.5.0 → geometrykit-0.6.0}/COPYING.LESSER +0 -0
  20. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/cage.py +0 -0
  21. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/contract.py +0 -0
  22. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/engine.py +0 -0
  23. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/export.py +0 -0
  24. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/_bridge.py +0 -0
  25. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/biarc.py +0 -0
  26. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/field.py +0 -0
  27. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/frames.py +0 -0
  28. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/intersection.py +0 -0
  29. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/path.py +0 -0
  30. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/patterns.py +0 -0
  31. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/primitives.py +0 -0
  32. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/spiral.py +0 -0
  33. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/subdivision.py +0 -0
  34. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/transform.py +0 -0
  35. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/runtimes/__init__.py +0 -0
  36. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/runtimes/_npu/__init__.py +0 -0
  37. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/runtimes/_npu/fit_np.py +0 -0
  38. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/runtimes/_npu/linalg_np.py +0 -0
  39. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/runtimes/_npu/patch_g1.py +0 -0
  40. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/runtimes/_npu/patch_np.py +0 -0
  41. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/runtimes/_npu/section_np.py +0 -0
  42. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/runtimes/_npu/surface_np.py +0 -0
  43. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit.egg-info/dependency_links.txt +0 -0
  44. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit.egg-info/requires.txt +0 -0
  45. {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit.egg-info/top_level.txt +0 -0
  46. {geometrykit-0.5.0 → geometrykit-0.6.0}/setup.cfg +0 -0
  47. {geometrykit-0.5.0 → geometrykit-0.6.0}/tests/test_cage.py +0 -0
  48. {geometrykit-0.5.0 → geometrykit-0.6.0}/tests/test_contract.py +0 -0
  49. {geometrykit-0.5.0 → geometrykit-0.6.0}/tests/test_engine_probe.py +0 -0
  50. {geometrykit-0.5.0 → geometrykit-0.6.0}/tests/test_repo_boundary.py +0 -0
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.4
2
+ Name: geometrykit
3
+ Version: 0.6.0
4
+ Summary: Geometry primitives and NURBS data model — the geometry substrate for the ifckit family
5
+ Author: Sander Boer
6
+ License-Expression: LGPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/maucworks/geometrykit
8
+ Project-URL: Changelog, https://github.com/maucworks/geometrykit/blob/master/CHANGELOG.md
9
+ Keywords: geometry,nurbs,vector,curve,surface,splines
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
19
+ Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: COPYING.LESSER
23
+ License-File: COPYING
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest<10,>=7; extra == "dev"
26
+ Requires-Dist: pytest-cov<8,>=7; extra == "dev"
27
+ Requires-Dist: hypothesis<8,>=6; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # geometrykit
31
+
32
+ OCC-free NURBS geometry for the ifckit family: value types (`Vec`,
33
+ `Plane`, `Line`, `Arc`, `Polyline`, `Path`, `Curve`, `Surface`), NURBS
34
+ evaluation / tangents / normals / projection, energy-minimising patch
35
+ fills with G1/G2 continuity options, plane × surface sections with
36
+ pcurve tracks, and STEP/OBJ/STL writers. No OCCT, no compiled
37
+ toolchain required to install.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install geometrykit # Python >= 3.9, incl. Rhino 8's CPython 3.9
43
+ ```
44
+
45
+ Binary wheels (`cp39-abi3`) carry the compiled Rust core
46
+ (`geometrykit._rust`) where a platform build exists, and the public
47
+ API routes to it automatically. Platforms without a matching wheel get
48
+ the pure-Python/numpy mirror instead: same API, same results, slower.
49
+ `GEOMETRYKIT_ENGINE=npu` forces the mirror; the default prefers the
50
+ accelerator when importable.
51
+
52
+ ## Quickstart
53
+
54
+ ```python
55
+ from geometrykit import engine
56
+ from geometrykit.geometry import Curve, Surface
57
+ from geometrykit.geometry.primitives import Vec
58
+
59
+ curves = [
60
+ Curve.from_tangents(start=Vec(0, 0, 2.4), tan_start=Vec(1, 0, 0),
61
+ end=Vec(8, 0, 2.8), tan_end=Vec(1, 0, 0), scale=3.0),
62
+ Curve.from_tangents(start=Vec(8, 0, 2.8), tan_start=Vec(0, 1, 0),
63
+ end=Vec(8, 6, 2.9), tan_end=Vec(0, 1, 0), scale=3.0),
64
+ Curve.from_tangents(start=Vec(0, 6, 3.2), tan_start=Vec(1, 0, 0),
65
+ end=Vec(8, 6, 2.9), tan_end=Vec(1, 0, 0), scale=3.0),
66
+ Curve.from_tangents(start=Vec(0, 6, 3.2), tan_start=Vec(0, -1, 0),
67
+ end=Vec(0, 0, 2.4), tan_end=Vec(0, -1, 0), scale=3.0),
68
+ ]
69
+ surface = Surface.patch(curves, fill="energy", nu=16, nv=16, tangency=0.75)
70
+ proj, (u, v) = surface.project(Vec(-1.0, -1.0, 3.0))
71
+ print(engine.engine(), proj, (u, v))
72
+ ```
73
+
74
+ ## What is in here
75
+
76
+ - NURBS evaluation, analytic tangents and normals, closest-point
77
+ projection (kernel Newton, no OCC).
78
+ - Energy-minimising patch fill over a boundary ring
79
+ (`Surface.patch(..., fill="energy")`), with tangency strength and
80
+ optional G2 and adaptive re-fit.
81
+ - Plane × surface sections with parameter tracks, batch level sets,
82
+ and node-exact diagrid weaving.
83
+ - Writers: STEP (AP214 B-rep), OBJ, STL.
84
+ - `geometrykit-core` on crates.io: the same kernels as a pure-Rust
85
+ dependency (no bindings, no unsafe code).
86
+
87
+ Out of scope by design: B-rep booleans and tolerant topology (volumes
88
+ are OCCT's domain); IFC serialisation lives in ifckit.
89
+
90
+ ## Development
91
+
92
+ ```bash
93
+ python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
94
+ .venv/bin/python -m pytest tests/ -q
95
+ GEOMETRYKIT_ENGINE=npu .venv/bin/python -m pytest tests/ -q
96
+ GEOMETRYKIT_ENGINE=rust .venv/bin/python -m pytest tests/ -q
97
+ ```
98
+
99
+ The oracle tier checks the kernel against OCCT and stays local
100
+ (`pythonocc-core` is conda-only; see AGENTS.md). Every suite train
101
+ runs in both engine configurations before a commit lands.
102
+
103
+ ## Licensing
104
+
105
+ LGPL-3.0-or-later (`COPYING.LESSER`, `COPYING`). Kernel code promoted
106
+ from ifckit's geometry package carries a provenance header recording
107
+ its origin.
@@ -0,0 +1,78 @@
1
+ # geometrykit
2
+
3
+ OCC-free NURBS geometry for the ifckit family: value types (`Vec`,
4
+ `Plane`, `Line`, `Arc`, `Polyline`, `Path`, `Curve`, `Surface`), NURBS
5
+ evaluation / tangents / normals / projection, energy-minimising patch
6
+ fills with G1/G2 continuity options, plane × surface sections with
7
+ pcurve tracks, and STEP/OBJ/STL writers. No OCCT, no compiled
8
+ toolchain required to install.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install geometrykit # Python >= 3.9, incl. Rhino 8's CPython 3.9
14
+ ```
15
+
16
+ Binary wheels (`cp39-abi3`) carry the compiled Rust core
17
+ (`geometrykit._rust`) where a platform build exists, and the public
18
+ API routes to it automatically. Platforms without a matching wheel get
19
+ the pure-Python/numpy mirror instead: same API, same results, slower.
20
+ `GEOMETRYKIT_ENGINE=npu` forces the mirror; the default prefers the
21
+ accelerator when importable.
22
+
23
+ ## Quickstart
24
+
25
+ ```python
26
+ from geometrykit import engine
27
+ from geometrykit.geometry import Curve, Surface
28
+ from geometrykit.geometry.primitives import Vec
29
+
30
+ curves = [
31
+ Curve.from_tangents(start=Vec(0, 0, 2.4), tan_start=Vec(1, 0, 0),
32
+ end=Vec(8, 0, 2.8), tan_end=Vec(1, 0, 0), scale=3.0),
33
+ Curve.from_tangents(start=Vec(8, 0, 2.8), tan_start=Vec(0, 1, 0),
34
+ end=Vec(8, 6, 2.9), tan_end=Vec(0, 1, 0), scale=3.0),
35
+ Curve.from_tangents(start=Vec(0, 6, 3.2), tan_start=Vec(1, 0, 0),
36
+ end=Vec(8, 6, 2.9), tan_end=Vec(1, 0, 0), scale=3.0),
37
+ Curve.from_tangents(start=Vec(0, 6, 3.2), tan_start=Vec(0, -1, 0),
38
+ end=Vec(0, 0, 2.4), tan_end=Vec(0, -1, 0), scale=3.0),
39
+ ]
40
+ surface = Surface.patch(curves, fill="energy", nu=16, nv=16, tangency=0.75)
41
+ proj, (u, v) = surface.project(Vec(-1.0, -1.0, 3.0))
42
+ print(engine.engine(), proj, (u, v))
43
+ ```
44
+
45
+ ## What is in here
46
+
47
+ - NURBS evaluation, analytic tangents and normals, closest-point
48
+ projection (kernel Newton, no OCC).
49
+ - Energy-minimising patch fill over a boundary ring
50
+ (`Surface.patch(..., fill="energy")`), with tangency strength and
51
+ optional G2 and adaptive re-fit.
52
+ - Plane × surface sections with parameter tracks, batch level sets,
53
+ and node-exact diagrid weaving.
54
+ - Writers: STEP (AP214 B-rep), OBJ, STL.
55
+ - `geometrykit-core` on crates.io: the same kernels as a pure-Rust
56
+ dependency (no bindings, no unsafe code).
57
+
58
+ Out of scope by design: B-rep booleans and tolerant topology (volumes
59
+ are OCCT's domain); IFC serialisation lives in ifckit.
60
+
61
+ ## Development
62
+
63
+ ```bash
64
+ python3 -m venv .venv && .venv/bin/pip install -e ".[dev]"
65
+ .venv/bin/python -m pytest tests/ -q
66
+ GEOMETRYKIT_ENGINE=npu .venv/bin/python -m pytest tests/ -q
67
+ GEOMETRYKIT_ENGINE=rust .venv/bin/python -m pytest tests/ -q
68
+ ```
69
+
70
+ The oracle tier checks the kernel against OCCT and stays local
71
+ (`pythonocc-core` is conda-only; see AGENTS.md). Every suite train
72
+ runs in both engine configurations before a commit lands.
73
+
74
+ ## Licensing
75
+
76
+ LGPL-3.0-or-later (`COPYING.LESSER`, `COPYING`). Kernel code promoted
77
+ from ifckit's geometry package carries a provenance header recording
78
+ its origin.
@@ -12,7 +12,7 @@ build-backend = "setuptools.build_meta"
12
12
 
13
13
  [project]
14
14
  name = "geometrykit"
15
- version = "0.5.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"
@@ -21,7 +21,7 @@ from geometrykit.geometry import (
21
21
  Vec,
22
22
  )
23
23
 
24
- __version__ = "0.5.0"
24
+ __version__ = "0.6.0"
25
25
 
26
26
  __all__ = [
27
27
  "__version__",
@@ -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). panel_report and the
4
- # IFC serialisation layer (ifc_geom) stay in ifckit by design.
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
- n = num if num is not None else int(total / step) + 1
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 (i / (n - 1) for i in range(n))
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 via OCC and return a dict for 3D viewer.
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 (OCC adaptive mesh).
286
- nv: Ignored (OCC adaptive mesh).
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
- from geometrykit.geometry.surface import occ_tessellate, require_occ
298
-
299
- require_occ()
300
- verts, tris = occ_tessellate(self, deflection)
301
- if y_up:
302
- positions = [c for v in verts for c in (v[0], v[2], -v[1])]
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
- positions = [c for v in verts for c in v]
305
- indices = [i - 1 for tri in tris for i in tri]
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
- Uses OCC adaptive triangulation. See ``Path.preview()`` for
327
- the full material documentation.
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