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.
- geometrykit-0.6.0/PKG-INFO +107 -0
- geometrykit-0.6.0/README.md +78 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/pyproject.toml +1 -1
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/__init__.py +1 -1
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/__init__.py +9 -2
- {geometrykit-0.5.0 → 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.0 → geometrykit-0.6.0}/python/geometrykit/geometry/surface.py +41 -16
- {geometrykit-0.5.0 → 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.0 → geometrykit-0.6.0}/python/geometrykit/runtimes/dispatch.py +58 -0
- geometrykit-0.6.0/python/geometrykit.egg-info/PKG-INFO +107 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit.egg-info/SOURCES.txt +2 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/tests/test_engine_parity.py +1 -1
- geometrykit-0.5.0/PKG-INFO +0 -94
- geometrykit-0.5.0/README.md +0 -65
- geometrykit-0.5.0/python/geometrykit.egg-info/PKG-INFO +0 -94
- {geometrykit-0.5.0 → geometrykit-0.6.0}/COPYING +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/COPYING.LESSER +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/cage.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/contract.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/engine.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/export.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/_bridge.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/biarc.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/field.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/frames.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/intersection.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/path.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/patterns.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/primitives.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/spiral.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/subdivision.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/geometry/transform.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/runtimes/__init__.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/runtimes/_npu/__init__.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/runtimes/_npu/fit_np.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/runtimes/_npu/linalg_np.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/runtimes/_npu/patch_g1.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/runtimes/_npu/patch_np.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/runtimes/_npu/section_np.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit/runtimes/_npu/surface_np.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit.egg-info/dependency_links.txt +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit.egg-info/requires.txt +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/python/geometrykit.egg-info/top_level.txt +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/setup.cfg +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/tests/test_cage.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/tests/test_contract.py +0 -0
- {geometrykit-0.5.0 → geometrykit-0.6.0}/tests/test_engine_probe.py +0 -0
- {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.
|
|
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
|