forgekernel 0.1.0__tar.gz → 0.9.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 (47) hide show
  1. forgekernel-0.9.0/PKG-INFO +79 -0
  2. forgekernel-0.9.0/README.md +44 -0
  3. {forgekernel-0.1.0 → forgekernel-0.9.0}/pyproject.toml +3 -4
  4. {forgekernel-0.1.0 → forgekernel-0.9.0}/src/forgekernel/__init__.py +1 -1
  5. forgekernel-0.9.0/src/forgekernel/hlr.py +571 -0
  6. forgekernel-0.9.0/src/forgekernel.egg-info/PKG-INFO +79 -0
  7. {forgekernel-0.1.0 → forgekernel-0.9.0}/src/forgekernel.egg-info/SOURCES.txt +2 -0
  8. {forgekernel-0.1.0 → forgekernel-0.9.0}/src/forgekernel.egg-info/requires.txt +1 -1
  9. forgekernel-0.9.0/tests/test_hlr.py +142 -0
  10. forgekernel-0.1.0/PKG-INFO +0 -82
  11. forgekernel-0.1.0/README.md +0 -47
  12. forgekernel-0.1.0/src/forgekernel.egg-info/PKG-INFO +0 -82
  13. {forgekernel-0.1.0 → forgekernel-0.9.0}/LICENSE +0 -0
  14. {forgekernel-0.1.0 → forgekernel-0.9.0}/setup.cfg +0 -0
  15. {forgekernel-0.1.0 → forgekernel-0.9.0}/src/forgekernel/brep.py +0 -0
  16. {forgekernel-0.1.0 → forgekernel-0.9.0}/src/forgekernel/bsolid.py +0 -0
  17. {forgekernel-0.1.0 → forgekernel-0.9.0}/src/forgekernel/csg.py +0 -0
  18. {forgekernel-0.1.0 → forgekernel-0.9.0}/src/forgekernel/curve.py +0 -0
  19. {forgekernel-0.1.0 → forgekernel-0.9.0}/src/forgekernel/exact.py +0 -0
  20. {forgekernel-0.1.0 → forgekernel-0.9.0}/src/forgekernel/interval.py +0 -0
  21. {forgekernel-0.1.0 → forgekernel-0.9.0}/src/forgekernel/io.py +0 -0
  22. {forgekernel-0.1.0 → forgekernel-0.9.0}/src/forgekernel/kernel.py +0 -0
  23. {forgekernel-0.1.0 → forgekernel-0.9.0}/src/forgekernel/loft.py +0 -0
  24. {forgekernel-0.1.0 → forgekernel-0.9.0}/src/forgekernel/nurbs.py +0 -0
  25. {forgekernel-0.1.0 → forgekernel-0.9.0}/src/forgekernel/profile2d.py +0 -0
  26. {forgekernel-0.1.0 → forgekernel-0.9.0}/src/forgekernel/quadric.py +0 -0
  27. {forgekernel-0.1.0 → forgekernel-0.9.0}/src/forgekernel/ssi.py +0 -0
  28. {forgekernel-0.1.0 → forgekernel-0.9.0}/src/forgekernel/stepio.py +0 -0
  29. {forgekernel-0.1.0 → forgekernel-0.9.0}/src/forgekernel/surd.py +0 -0
  30. {forgekernel-0.1.0 → forgekernel-0.9.0}/src/forgekernel/surfacing.py +0 -0
  31. {forgekernel-0.1.0 → forgekernel-0.9.0}/src/forgekernel/tess.py +0 -0
  32. {forgekernel-0.1.0 → forgekernel-0.9.0}/src/forgekernel/trim.py +0 -0
  33. {forgekernel-0.1.0 → forgekernel-0.9.0}/src/forgekernel.egg-info/dependency_links.txt +0 -0
  34. {forgekernel-0.1.0 → forgekernel-0.9.0}/src/forgekernel.egg-info/top_level.txt +0 -0
  35. {forgekernel-0.1.0 → forgekernel-0.9.0}/tests/test_k1_exact.py +0 -0
  36. {forgekernel-0.1.0 → forgekernel-0.9.0}/tests/test_k37_loft.py +0 -0
  37. {forgekernel-0.1.0 → forgekernel-0.9.0}/tests/test_k38_profile.py +0 -0
  38. {forgekernel-0.1.0 → forgekernel-0.9.0}/tests/test_k3_curves.py +0 -0
  39. {forgekernel-0.1.0 → forgekernel-0.9.0}/tests/test_k3_ssi.py +0 -0
  40. {forgekernel-0.1.0 → forgekernel-0.9.0}/tests/test_k3_step.py +0 -0
  41. {forgekernel-0.1.0 → forgekernel-0.9.0}/tests/test_k6_surfacing.py +0 -0
  42. {forgekernel-0.1.0 → forgekernel-0.9.0}/tests/test_k7_bsolid.py +0 -0
  43. {forgekernel-0.1.0 → forgekernel-0.9.0}/tests/test_k7_stepexport.py +0 -0
  44. {forgekernel-0.1.0 → forgekernel-0.9.0}/tests/test_k7_trim.py +0 -0
  45. {forgekernel-0.1.0 → forgekernel-0.9.0}/tests/test_k7_trimflux.py +0 -0
  46. {forgekernel-0.1.0 → forgekernel-0.9.0}/tests/test_rust_oracle.py +0 -0
  47. {forgekernel-0.1.0 → forgekernel-0.9.0}/tests/test_scaffold.py +0 -0
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: forgekernel
3
+ Version: 0.9.0
4
+ Summary: Exact-arithmetic B-rep CAD kernel
5
+ Author-email: Dan Willis <danielcwillis@gmail.com>
6
+ Maintainer-email: Dan Willis <danielcwillis@gmail.com>
7
+ License: Apache-2.0
8
+ Project-URL: Homepage, https://github.com/gitcad-xyz/forge
9
+ Project-URL: Repository, https://github.com/gitcad-xyz/forge
10
+ Project-URL: Issues, https://github.com/gitcad-xyz/forge/issues
11
+ Project-URL: Design — ADR-0018, https://github.com/gitcad-xyz/gitcad/blob/main/docs/adr/0018-native-kernel.md
12
+ Project-URL: Benchmarks vs OCCT, https://github.com/gitcad-xyz/gitcad/blob/main/bench/RUST-vs-OCCT.md
13
+ Keywords: cad,b-rep,brep,geometry,kernel,nurbs,solid-modeling,computational-geometry,exact-arithmetic,rational-arithmetic,step
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: Manufacturing
17
+ Classifier: Intended Audience :: Science/Research
18
+ Classifier: License :: OSI Approved :: Apache Software License
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Scientific/Engineering
25
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
26
+ Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
27
+ Requires-Python: >=3.11
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Provides-Extra: rust
31
+ Requires-Dist: forgekernel_rs>=0.8.0; extra == "rust"
32
+ Provides-Extra: test
33
+ Requires-Dist: pytest>=7; extra == "test"
34
+ Dynamic: license-file
35
+
36
+ # forge
37
+
38
+ An exact-arithmetic boundary-representation (B-rep) CAD kernel. Geometric and
39
+ topological decisions use exact rational arithmetic (ℚ, ℚ[π], ℚ[√d]) or
40
+ certified intervals; no result depends on a floating-point tolerance.
41
+
42
+ It ships as two PyPI packages:
43
+
44
+ - `forgekernel` — the kernel, pure Python.
45
+ - `forgekernel_rs` — a native (Rust) build of the performance-critical routines,
46
+ installed automatically when a wheel is available and returning the same
47
+ results.
48
+
49
+ ## Capabilities
50
+
51
+ - Planar solids: rational linear algebra, plane-based B-rep, booleans,
52
+ divergence-theorem mass properties.
53
+ - Quadrics (cylinder, cone, sphere, torus) with closed-form intersections.
54
+ - NURBS curves and surfaces; surface–surface intersection with complete branch
55
+ detection and certified points.
56
+ - Procedural offsets and shells; edge blends (fillets); lofts.
57
+ - Exact volume, centroid, and inertia via the divergence theorem.
58
+ - STEP (AP203/AP214) import and export.
59
+
60
+ ## Design
61
+
62
+ A Python reference implementation defines the exact semantics. A Rust build
63
+ provides the same operations for production use; each ported routine is
64
+ cross-checked against the reference. Design notes and decisions are recorded in
65
+ the gitcad repository (ADR-0018).
66
+
67
+ ## Layout
68
+
69
+ - `src/forgekernel/` — the Python kernel.
70
+ - `rust/forge-core/` — the native build (`forgekernel_rs`).
71
+
72
+ ## Install
73
+
74
+ ```
75
+ pip install forgekernel # pure Python
76
+ pip install forgekernel[rust] # add the native build
77
+ ```
78
+
79
+ License: Apache-2.0. Source: https://github.com/gitcad-xyz/forge
@@ -0,0 +1,44 @@
1
+ # forge
2
+
3
+ An exact-arithmetic boundary-representation (B-rep) CAD kernel. Geometric and
4
+ topological decisions use exact rational arithmetic (ℚ, ℚ[π], ℚ[√d]) or
5
+ certified intervals; no result depends on a floating-point tolerance.
6
+
7
+ It ships as two PyPI packages:
8
+
9
+ - `forgekernel` — the kernel, pure Python.
10
+ - `forgekernel_rs` — a native (Rust) build of the performance-critical routines,
11
+ installed automatically when a wheel is available and returning the same
12
+ results.
13
+
14
+ ## Capabilities
15
+
16
+ - Planar solids: rational linear algebra, plane-based B-rep, booleans,
17
+ divergence-theorem mass properties.
18
+ - Quadrics (cylinder, cone, sphere, torus) with closed-form intersections.
19
+ - NURBS curves and surfaces; surface–surface intersection with complete branch
20
+ detection and certified points.
21
+ - Procedural offsets and shells; edge blends (fillets); lofts.
22
+ - Exact volume, centroid, and inertia via the divergence theorem.
23
+ - STEP (AP203/AP214) import and export.
24
+
25
+ ## Design
26
+
27
+ A Python reference implementation defines the exact semantics. A Rust build
28
+ provides the same operations for production use; each ported routine is
29
+ cross-checked against the reference. Design notes and decisions are recorded in
30
+ the gitcad repository (ADR-0018).
31
+
32
+ ## Layout
33
+
34
+ - `src/forgekernel/` — the Python kernel.
35
+ - `rust/forge-core/` — the native build (`forgekernel_rs`).
36
+
37
+ ## Install
38
+
39
+ ```
40
+ pip install forgekernel # pure Python
41
+ pip install forgekernel[rust] # add the native build
42
+ ```
43
+
44
+ License: Apache-2.0. Source: https://github.com/gitcad-xyz/forge
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
  [project]
6
6
  name = "forgekernel"
7
7
  dynamic = ["version"]
8
- description = "Exact-arithmetic B-rep CAD kernel: topology decided in ℚ, never by floating-point epsilon"
8
+ description = "Exact-arithmetic B-rep CAD kernel"
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}
11
11
  requires-python = ">=3.11"
@@ -13,8 +13,7 @@ authors = [{name = "Dan Willis", email = "danielcwillis@gmail.com"}]
13
13
  maintainers = [{name = "Dan Willis", email = "danielcwillis@gmail.com"}]
14
14
  keywords = [
15
15
  "cad", "b-rep", "brep", "geometry", "kernel", "nurbs", "solid-modeling",
16
- "computational-geometry", "exact-arithmetic", "rational-arithmetic",
17
- "step", "occt", "opencascade",
16
+ "computational-geometry", "exact-arithmetic", "rational-arithmetic", "step",
18
17
  ]
19
18
  classifiers = [
20
19
  "Development Status :: 3 - Alpha",
@@ -44,7 +43,7 @@ Issues = "https://github.com/gitcad-xyz/forge/issues"
44
43
  [project.optional-dependencies]
45
44
  # Optional native accelerator (separate compiled wheels). forgekernel runs
46
45
  # pure-Python and exact WITHOUT it; forgekernel_rs only speeds the hot paths.
47
- rust = ["forgekernel_rs>=0.1.0"]
46
+ rust = ["forgekernel_rs>=0.8.0"]
48
47
  test = ["pytest>=7"]
49
48
 
50
49
  [tool.setuptools.dynamic]
@@ -4,4 +4,4 @@ Exact rational arithmetic for every topological decision; approximation
4
4
  only with tracked error bounds. See README for the three-oracle chain.
5
5
  """
6
6
 
7
- __version__ = "0.1.0"
7
+ __version__ = "0.9.0"
@@ -0,0 +1,571 @@
1
+ """Native hidden-line removal for 2D orthographic drawings (ADR-0020).
2
+
3
+ Visibility is a *display* property, not a topological decision, so — under the
4
+ exactness charter (ADR-0019) — this layer may use floats; the geometry it draws
5
+ is the exact solid. For a view direction it returns two lists of 2D polylines,
6
+ ``visible`` and ``hidden``, in the sheet frame the drawing engine consumes:
7
+ the x-axis is ``xdir``, the y-axis is ``direction × xdir``, the viewer looks
8
+ along +``direction`` (so smaller ``dot(P, direction)`` is closer to the viewer).
9
+
10
+ Approach: extract the solid's real edges (exact straight edges for planar
11
+ solids; sampled circles + silhouettes for cylinders; sharp + silhouette edges
12
+ from the tessellation for everything else), project them, then split each into
13
+ visible/hidden spans by asking, at sample midpoints, whether a hair toward the
14
+ viewer lands *inside* the solid (an inside classifier that respects holes).
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import math
20
+
21
+ _EPS = 1e-7
22
+ # a fixed, non-axis-aligned ray for inside/parity tests — dodges the coplanar
23
+ # and edge-grazing degeneracies that axis-aligned rays hit on boxy models.
24
+ _RAY = (0.41931, 0.77913, 0.46671)
25
+
26
+
27
+ def _f3(v):
28
+ return (float(v[0]), float(v[1]), float(v[2]))
29
+
30
+
31
+ def _sub(a, b):
32
+ return (a[0] - b[0], a[1] - b[1], a[2] - b[2])
33
+
34
+
35
+ def _add(a, b):
36
+ return (a[0] + b[0], a[1] + b[1], a[2] + b[2])
37
+
38
+
39
+ def _mul(a, s):
40
+ return (a[0] * s, a[1] * s, a[2] * s)
41
+
42
+
43
+ def _dot(a, b):
44
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
45
+
46
+
47
+ def _cross(a, b):
48
+ return (a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2],
49
+ a[0] * b[1] - a[1] * b[0])
50
+
51
+
52
+ def _norm(v):
53
+ m = math.sqrt(_dot(v, v)) or 1.0
54
+ return (v[0] / m, v[1] / m, v[2] / m)
55
+
56
+
57
+ class _View:
58
+ def __init__(self, direction, xdir):
59
+ self.look = _norm(_f3(direction))
60
+ self.right = _norm(_f3(xdir))
61
+ # sheet-up = direction × xdir (the drawing engine's convention)
62
+ self.up = _norm(_cross(self.look, self.right))
63
+
64
+ def xy(self, p):
65
+ return (_dot(p, self.right), _dot(p, self.up))
66
+
67
+
68
+ # -- inside/occlusion classifiers ---------------------------------------------
69
+
70
+ def _ray_hits_convex(orig, dirn, poly, n):
71
+ """t>0 where the ray orig+t·dirn crosses the convex polygon (with plane
72
+ normal n), or None. Point-in-polygon via consistent edge cross-signs."""
73
+ denom = _dot(dirn, n)
74
+ if abs(denom) < 1e-12:
75
+ return None
76
+ t = _dot(_sub(poly[0], orig), n) / denom
77
+ if t <= _EPS:
78
+ return None
79
+ h = _add(orig, _mul(dirn, t))
80
+ m = len(poly)
81
+ sign = 0
82
+ for i in range(m):
83
+ a, b = poly[i], poly[(i + 1) % m]
84
+ c = _dot(_cross(_sub(b, a), _sub(h, a)), n)
85
+ if c > 1e-12:
86
+ s = 1
87
+ elif c < -1e-12:
88
+ s = -1
89
+ else:
90
+ continue
91
+ if sign == 0:
92
+ sign = s
93
+ elif s != sign:
94
+ return None
95
+ return t
96
+
97
+
98
+ def _parity_inside(polys_n, q):
99
+ """Odd crossings of the fixed ray ⇒ q is inside a closed polygon shell."""
100
+ c = 0
101
+ for poly, n in polys_n:
102
+ if _ray_hits_convex(q, _RAY, poly, n) is not None:
103
+ c += 1
104
+ return c % 2 == 1
105
+
106
+
107
+ def _solid_polys_n(solid):
108
+ out = []
109
+ for p in solid.polys:
110
+ v = [_f3(x) for x in p.verts]
111
+ out.append((v, _cross(_sub(v[1], v[0]), _sub(v[2], v[0]))))
112
+ return out
113
+
114
+
115
+ def _tri_polys_n(mesh):
116
+ verts = [tuple(float(c) for c in v) for v in mesh["vertices"]]
117
+ out = []
118
+ for i, j, k in mesh["triangles"]:
119
+ tri = [verts[i], verts[j], verts[k]]
120
+ out.append((tri, _cross(_sub(tri[1], tri[0]), _sub(tri[2], tri[0]))))
121
+ return out
122
+
123
+
124
+ def _cyl_tuple(c):
125
+ return (float(c.cx), float(c.cy), float(c.r), float(c.z0), float(c.z1))
126
+
127
+
128
+ def _inside_fn(solid):
129
+ """A callable q(float3) -> bool: is q strictly inside the solid? Composed so
130
+ holes are respected without triangulating them away."""
131
+ name = type(solid).__name__
132
+ if name == "Solid":
133
+ pn = _solid_polys_n(solid)
134
+ return lambda q: _parity_inside(pn, q)
135
+ if name == "DrilledSolid":
136
+ base_in = _inside_fn(solid.base)
137
+ bores = [_cyl_tuple(c) for c in solid.bores]
138
+
139
+ def inside(q):
140
+ if not base_in(q):
141
+ return False
142
+ x, y, z = q
143
+ for cx, cy, r, z0, z1 in bores:
144
+ if z0 - _EPS <= z <= z1 + _EPS and \
145
+ (x - cx) ** 2 + (y - cy) ** 2 <= r * r + _EPS:
146
+ return False # in a bore = removed
147
+ return True
148
+ return inside
149
+ if name == "Cyl":
150
+ cx, cy, r, z0, z1 = _cyl_tuple(solid)
151
+ return lambda q: (z0 - _EPS <= q[2] <= z1 + _EPS
152
+ and (q[0] - cx) ** 2 + (q[1] - cy) ** 2 <= r * r + _EPS)
153
+ # general: parity against the tessellation
154
+ try:
155
+ pn = _tri_polys_n(solid.tessellate())
156
+ return lambda q: _parity_inside(pn, q)
157
+ except Exception: # noqa: BLE001 - no mesh ⇒ never occlude
158
+ return lambda q: False
159
+
160
+
161
+ # -- edges to draw -------------------------------------------------------------
162
+
163
+ def _solid_edges(solid):
164
+ """Exact straight edges of a planar solid, as 3D float segments."""
165
+ from forgekernel.brep import logical_edges
166
+
167
+ out = []
168
+ for e in logical_edges(solid):
169
+ d = _f3(e["dir"])
170
+ pt = _f3(e["point"])
171
+ dd = _dot(d, d) or 1.0
172
+ tp = _dot(pt, d)
173
+ tmin, tmax = float(e["tmin"]), float(e["tmax"])
174
+ p0 = _add(pt, _mul(d, (tmin - tp) / dd))
175
+ p1 = _add(pt, _mul(d, (tmax - tp) / dd))
176
+ out.append([p0, p1])
177
+ return out
178
+
179
+
180
+ def _circle(cx, cy, r, z, segs):
181
+ return [(cx + r * math.cos(2 * math.pi * k / segs),
182
+ cy + r * math.sin(2 * math.pi * k / segs), z)
183
+ for k in range(segs + 1)]
184
+
185
+
186
+ def _circle_segs(r, deflection):
187
+ """Facet count for a circle of radius ``r`` at chord error ``deflection``."""
188
+ if r <= deflection:
189
+ return 24
190
+ return max(24, int(math.ceil(math.pi / math.acos(max(-1.0, 1.0 - deflection / r)))))
191
+
192
+
193
+ def _cyl_edges(c, view, deflection):
194
+ """A z-axis cylinder's drawing edges: the two rim circles plus, when the
195
+ view is not down the axis, the two silhouette generators."""
196
+ cx, cy, r, z0, z1 = _cyl_tuple(c)
197
+ segs = _circle_segs(r, deflection)
198
+ out = [_circle(cx, cy, r, z0, segs), _circle(cx, cy, r, z1, segs)]
199
+ lx, ly = view.look[0], view.look[1] # axis is +z; silhouette needs
200
+ m = math.hypot(lx, ly) # the in-plane look component
201
+ if m > 1e-6:
202
+ px, py = -ly / m, lx / m # perpendicular to look, in xy
203
+ for s in (1.0, -1.0):
204
+ x, y = cx + s * r * px, cy + s * r * py
205
+ out.append([(x, y, z0), (x, y, z1)])
206
+ return out
207
+
208
+
209
+ def _mesh_edges(solid, view):
210
+ """Sharp feature edges + view-dependent silhouette edges from a mesh."""
211
+ try:
212
+ mesh = solid.tessellate()
213
+ except Exception: # noqa: BLE001
214
+ return []
215
+ verts = [tuple(float(c) for c in v) for v in mesh["vertices"]]
216
+ from collections import defaultdict
217
+ faces = defaultdict(list) # undirected edge -> [triangle normals]
218
+ tri_of = defaultdict(list) # undirected edge -> [(a,b) as given]
219
+ for i, j, k in mesh["triangles"]:
220
+ n = _norm(_cross(_sub(verts[j], verts[i]), _sub(verts[k], verts[i])))
221
+ for a, b in ((i, j), (j, k), (k, i)):
222
+ e = (min(a, b), max(a, b))
223
+ faces[e].append(n)
224
+ tri_of[e].append((verts[a], verts[b]))
225
+ out = []
226
+ for e, normals in faces.items():
227
+ seg = [verts[e[0]], verts[e[1]]]
228
+ if len(normals) == 1:
229
+ out.append(seg) # boundary edge
230
+ continue
231
+ n0, n1 = normals[0], normals[1]
232
+ if _dot(n0, n1) < 0.985: # sharp feature edge (~10°)
233
+ out.append(seg)
234
+ continue
235
+ # silhouette: the two faces face opposite ways relative to the viewer
236
+ f0 = _dot(n0, view.look) < 0
237
+ f1 = _dot(n1, view.look) < 0
238
+ if f0 != f1:
239
+ out.append(seg)
240
+ return out
241
+
242
+
243
+ def _draw_edges(solid, view, deflection):
244
+ name = type(solid).__name__
245
+ if name == "Solid":
246
+ return _solid_edges(solid)
247
+ if name == "DrilledSolid":
248
+ segs = _solid_edges(solid.base)
249
+ for c in solid.bores:
250
+ segs += _cyl_edges(c, view, deflection)
251
+ return segs
252
+ if name == "Cyl":
253
+ return _cyl_edges(solid, view, deflection)
254
+ if name == "DisjointUnion":
255
+ segs = []
256
+ for m in solid.members:
257
+ segs += _draw_edges(m, view, deflection)
258
+ return segs
259
+ return _mesh_edges(solid, view)
260
+
261
+
262
+ # -- main ----------------------------------------------------------------------
263
+
264
+ def _split_polyline(pts3, view, inside, deflection):
265
+ """Project a 3D polyline and split it into visible/hidden runs by testing,
266
+ at each segment's samples, whether a hair toward the viewer is occluded."""
267
+ toward = _mul(view.look, -1.0) # from a surface point to viewer
268
+ vis, hid = [], []
269
+ for a, b in zip(pts3[:-1], pts3[1:]):
270
+ seg = _sub(b, a)
271
+ length = math.sqrt(_dot(seg, seg))
272
+ nseg = max(1, int(math.ceil(length / max(deflection, 1e-6))))
273
+ prev_hidden = None
274
+ run = []
275
+ for s in range(nseg + 1):
276
+ t = s / nseg
277
+ p = _add(a, _mul(seg, t))
278
+ xy = view.xy(p)
279
+ if s < nseg: # visibility of the sub-segment
280
+ mid = _add(a, _mul(seg, (s + 0.5) / nseg))
281
+ h = inside(_add(mid, _mul(toward, 1e-4 * max(1.0, length))))
282
+ else:
283
+ h = prev_hidden
284
+ if prev_hidden is None or h == prev_hidden:
285
+ run.append(xy)
286
+ else:
287
+ (hid if prev_hidden else vis).append(run)
288
+ run = [run[-1], xy]
289
+ prev_hidden = h
290
+ if len(run) >= 2:
291
+ (hid if prev_hidden else vis).append(run)
292
+ return vis, hid
293
+
294
+
295
+ def hidden_line(solid, direction, xdir, *, deflection=0.05):
296
+ """Return ``{"visible": [polyline…], "hidden": [polyline…]}`` for the view,
297
+ each polyline a list of ``(x, y)`` floats in the sheet frame."""
298
+ view = _View(direction, xdir)
299
+ inside = _inside_fn(solid)
300
+ visible, hidden = [], []
301
+ for edge in _draw_edges(solid, view, deflection):
302
+ v, h = _split_polyline(edge, view, inside, deflection)
303
+ visible += [p for p in v if len(p) >= 2]
304
+ hidden += [p for p in h if len(p) >= 2]
305
+ return {"visible": visible, "hidden": hidden}
306
+
307
+
308
+ # -- section curves (plane ∩ solid boundary) ----------------------------------
309
+ #
310
+ # A section view cuts the solid with a plane and draws the intersection of that
311
+ # plane with the solid's *surface* — the closed loops the section engine chains
312
+ # and hatches. Like HLR this is a display computation (floats legal); the plane
313
+ # and the solid it cuts are exact. The plane is ``{P : dot(P, direction)=offset}``
314
+ # (``direction`` is the view/look axis, so the section lies flat in the sheet).
315
+
316
+ def _plane_from(direction, offset):
317
+ """Cut plane as a unit normal + signed distance ``dot(P, n) = d``."""
318
+ raw = _f3(direction)
319
+ scale = math.sqrt(_dot(raw, raw)) or 1.0
320
+ return _norm(raw), float(offset) / scale
321
+
322
+
323
+ def _det3(m):
324
+ return (m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1])
325
+ - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0])
326
+ + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]))
327
+
328
+
329
+ def _two_plane_point(n1, d1, n2, d2, u):
330
+ """A point on the line where planes (n1,d1) and (n2,d2) meet; ``u=n1×n2``."""
331
+ m = [list(n1), list(n2), list(u)]
332
+ det = _det3(m)
333
+ if abs(det) < 1e-16:
334
+ return (0.0, 0.0, 0.0)
335
+ rhs = (d1, d2, 0.0)
336
+ out = []
337
+ for j in range(3):
338
+ mj = [row[:] for row in m]
339
+ for i in range(3):
340
+ mj[i][j] = rhs[i]
341
+ out.append(_det3(mj) / det)
342
+ return tuple(out)
343
+
344
+
345
+ def _line_in_convex(p0, u, poly, fn):
346
+ """λ-interval of ``p0+λu`` inside a convex planar polygon (normal ``fn``),
347
+ found by the two boundary crossings — winding-agnostic."""
348
+ lambdas = []
349
+ m = len(poly)
350
+ for i in range(m):
351
+ a, b = poly[i], poly[(i + 1) % m]
352
+ e = _sub(b, a)
353
+ en = _cross(fn, e)
354
+ denom = _dot(u, en)
355
+ if abs(denom) < 1e-15:
356
+ continue
357
+ lam = _dot(_sub(a, p0), en) / denom
358
+ pt = _add(p0, _mul(u, lam))
359
+ ee = _dot(e, e) or 1.0
360
+ s = _dot(_sub(pt, a), e) / ee
361
+ if -1e-9 <= s <= 1 + 1e-9:
362
+ lambdas.append(lam)
363
+ if len(lambdas) < 2:
364
+ return None
365
+ return (min(lambdas), max(lambdas))
366
+
367
+
368
+ def _circle_lambdas(p0, u, c3, r):
369
+ """λ-interval where ``p0+λu`` lies inside the circle (centre ``c3``, radius
370
+ ``r``, both in the line's plane), or None."""
371
+ w = _sub(p0, c3)
372
+ a = _dot(u, u)
373
+ b = 2.0 * _dot(w, u)
374
+ c = _dot(w, w) - r * r
375
+ disc = b * b - 4 * a * c
376
+ if disc <= 1e-12 or a < 1e-16:
377
+ return None
378
+ sq = math.sqrt(disc)
379
+ return ((-b - sq) / (2 * a), (-b + sq) / (2 * a))
380
+
381
+
382
+ def _subtract(intervals, lo, hi):
383
+ out = []
384
+ for a, b in intervals:
385
+ if hi <= a or lo >= b:
386
+ out.append((a, b))
387
+ continue
388
+ if a < lo:
389
+ out.append((a, lo))
390
+ if hi < b:
391
+ out.append((hi, b))
392
+ return out
393
+
394
+
395
+ def _planar_face_section(poly, fn, nc, dc, bores):
396
+ """Segments of the cut plane across one planar face, with any bore disks
397
+ that open onto the face subtracted (so through-holes stay clear)."""
398
+ u = _cross(nc, fn)
399
+ if _dot(u, u) < 1e-16:
400
+ return [] # face parallel to the cut plane
401
+ df = _dot(fn, poly[0])
402
+ p0 = _two_plane_point(nc, dc, fn, df, u)
403
+ span = _line_in_convex(p0, u, poly, fn)
404
+ if span is None:
405
+ return []
406
+ intervals = [span]
407
+ nfu = _norm(fn)
408
+ if abs(nfu[2]) > 0.99: # a z-cap face — bores open onto it
409
+ zf = float(poly[0][2])
410
+ for cx, cy, r, z0, z1 in bores:
411
+ if abs(zf - z0) < 1e-6 or abs(zf - z1) < 1e-6:
412
+ hit = _circle_lambdas(p0, u, (cx, cy, zf), r)
413
+ if hit:
414
+ intervals = _subtract(intervals, hit[0], hit[1])
415
+ segs = []
416
+ for a, b in intervals:
417
+ if b - a > 1e-9:
418
+ segs.append((_add(p0, _mul(u, a)), _add(p0, _mul(u, b))))
419
+ return segs
420
+
421
+
422
+ def _wall_xy(cx, cy, r, nc, dc):
423
+ """The up-to-two (x, y) where a z-cylinder wall (radius r) meets a cut plane
424
+ parallel to the axis (nz≈0), or [] if the plane misses it."""
425
+ nx, ny, _ = nc
426
+ mag = r * math.hypot(nx, ny)
427
+ if mag < 1e-12:
428
+ return []
429
+ cth = (dc - (nx * cx + ny * cy)) / mag
430
+ if cth < -1 - 1e-9 or cth > 1 + 1e-9:
431
+ return []
432
+ phi = math.atan2(ny, nx)
433
+ ac = math.acos(max(-1.0, min(1.0, cth)))
434
+ return [(cx + r * math.cos(t), cy + r * math.sin(t)) for t in {phi + ac, phi - ac}]
435
+
436
+
437
+ def _cyl_wall_seg(cx, cy, r, z0, z1, nc, dc, deflection):
438
+ """Segments where the cut plane crosses a z-cylinder wall over [z0, z1].
439
+ Two vertical generators when the plane is parallel to the axis; a sampled
440
+ ellipse/circle otherwise."""
441
+ nx, ny, nz = nc
442
+ if abs(nz) < 1e-9: # plane parallel to the axis
443
+ return [((x, y, z0), (x, y, z1)) for (x, y) in _wall_xy(cx, cy, r, nc, dc)]
444
+ segs = _circle_segs(r, deflection) # plane crosses the axis: sample θ
445
+ prev = None
446
+ out = []
447
+ for k in range(segs + 1):
448
+ th = 2 * math.pi * k / segs
449
+ x, y = cx + r * math.cos(th), cy + r * math.sin(th)
450
+ z = (dc - nx * x - ny * y) / nz
451
+ cur = (x, y, z) if z0 - 1e-9 <= z <= z1 + 1e-9 else None
452
+ if prev is not None and cur is not None:
453
+ out.append((prev, cur))
454
+ prev = cur
455
+ return out
456
+
457
+
458
+ def _cyl_wall_section(c, nc, dc, deflection):
459
+ cx, cy, r, z0, z1 = _cyl_tuple(c)
460
+ return _cyl_wall_seg(cx, cy, r, z0, z1, nc, dc, deflection)
461
+
462
+
463
+ def _bore_bands(group):
464
+ """Partition a coaxial bore group into z-bands, each tagged with the
465
+ OUTERMOST radius covering it — the profile of the bores' union. ``group`` is
466
+ a list of (cx, cy, r, z0, z1) tuples sharing (cx, cy)."""
467
+ zs = sorted({z for (_, _, _, z0, z1) in group for z in (z0, z1)})
468
+ bands = []
469
+ for za, zb in zip(zs, zs[1:]):
470
+ zmid = (za + zb) / 2
471
+ rs = [r for (_, _, r, z0, z1) in group if z0 - 1e-9 <= zmid <= z1 + 1e-9]
472
+ if rs:
473
+ bands.append((za, zb, max(rs)))
474
+ return bands
475
+
476
+
477
+ def _section_bore_group(group, nc, dc, deflection):
478
+ """Section a coaxial bore stack (e.g. a counterbore) as ONE stepped-cylinder
479
+ profile: walls at the outermost radius per z-band, plus shoulder rings where
480
+ the radius steps (only visible when the plane is parallel to the axis).
481
+ Sectioning each bore independently would draw the inner wall straight through
482
+ the empty counterbore cavity and omit the shoulder."""
483
+ cx, cy = float(group[0][0]), float(group[0][1])
484
+ bands = _bore_bands(group)
485
+ out = []
486
+ for za, zb, r in bands:
487
+ out += _cyl_wall_seg(cx, cy, r, za, zb, nc, dc, deflection)
488
+ if abs(nc[2]) < 1e-9: # shoulders only cut when plane ∥ axis
489
+ for (za, zb, r0), (zb2, zc, r1) in zip(bands, bands[1:]):
490
+ if abs(r0 - r1) < 1e-12:
491
+ continue
492
+ lo = _wall_xy(cx, cy, min(r0, r1), nc, dc)
493
+ hi = _wall_xy(cx, cy, max(r0, r1), nc, dc)
494
+ for h in hi: # connect each outer point to its side's inner
495
+ if not lo:
496
+ continue
497
+ l = min(lo, key=lambda p: (p[0] - h[0]) ** 2 + (p[1] - h[1]) ** 2)
498
+ out.append(((l[0], l[1], zb), (h[0], h[1], zb)))
499
+ return out
500
+
501
+
502
+ def _cyl_cap_sections(c, nc, dc):
503
+ cx, cy, r, z0, z1 = _cyl_tuple(c)
504
+ out = []
505
+ for zc in (z0, z1):
506
+ fn = (0.0, 0.0, 1.0)
507
+ u = _cross(nc, fn)
508
+ if _dot(u, u) < 1e-16:
509
+ continue
510
+ p0 = _two_plane_point(nc, dc, fn, zc, u)
511
+ hit = _circle_lambdas(p0, u, (cx, cy, zc), r)
512
+ if hit:
513
+ out.append((_add(p0, _mul(u, hit[0])), _add(p0, _mul(u, hit[1]))))
514
+ return out
515
+
516
+
517
+ def _mesh_section(solid, nc, dc):
518
+ try:
519
+ mesh = solid.tessellate()
520
+ except Exception: # noqa: BLE001
521
+ return []
522
+ verts = [tuple(float(x) for x in v) for v in mesh["vertices"]]
523
+ out = []
524
+ for tri in mesh["triangles"]:
525
+ pts = [verts[i] for i in tri]
526
+ hits = []
527
+ for a, b in ((0, 1), (1, 2), (2, 0)):
528
+ pa, pb = pts[a], pts[b]
529
+ da, db = _dot(pa, nc) - dc, _dot(pb, nc) - dc
530
+ if (da <= 0 < db) or (db <= 0 < da):
531
+ t = da / (da - db)
532
+ hits.append(_add(pa, _mul(_sub(pb, pa), t)))
533
+ if len(hits) == 2:
534
+ out.append((hits[0], hits[1]))
535
+ return out
536
+
537
+
538
+ def _section_segments(solid, nc, dc, deflection):
539
+ name = type(solid).__name__
540
+ if name == "Solid":
541
+ return [s for poly, fn in _solid_polys_n(solid)
542
+ for s in _planar_face_section(poly, fn, nc, dc, [])]
543
+ if name == "DrilledSolid":
544
+ bores = [_cyl_tuple(x) for x in solid.bores]
545
+ out = [s for poly, fn in _solid_polys_n(solid.base)
546
+ for s in _planar_face_section(poly, fn, nc, dc, bores)]
547
+ groups = {} # coaxial bores section as one profile
548
+ for b in bores:
549
+ groups.setdefault((round(b[0], 9), round(b[1], 9)), []).append(b)
550
+ for g in groups.values():
551
+ out += _section_bore_group(g, nc, dc, deflection)
552
+ return out
553
+ if name == "Cyl":
554
+ return (_cyl_wall_section(solid, nc, dc, deflection)
555
+ + _cyl_cap_sections(solid, nc, dc))
556
+ if name == "DisjointUnion":
557
+ out = []
558
+ for m in solid.members:
559
+ out += _section_segments(m, nc, dc, deflection)
560
+ return out
561
+ return _mesh_section(solid, nc, dc)
562
+
563
+
564
+ def section_polys(solid, direction, xdir, offset, *, deflection=0.05):
565
+ """Cut ``solid`` with the plane ``dot(P, direction)=offset`` and return the
566
+ section curves as 2D segments ``[[(x,y),(x,y)], …]`` in the same sheet frame
567
+ ``hidden_line`` uses, so a section view overlays its projection exactly."""
568
+ view = _View(direction, xdir)
569
+ nc, dc = _plane_from(direction, offset)
570
+ return [[view.xy(a), view.xy(b)]
571
+ for a, b in _section_segments(solid, nc, dc, deflection)]
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: forgekernel
3
+ Version: 0.9.0
4
+ Summary: Exact-arithmetic B-rep CAD kernel
5
+ Author-email: Dan Willis <danielcwillis@gmail.com>
6
+ Maintainer-email: Dan Willis <danielcwillis@gmail.com>
7
+ License: Apache-2.0
8
+ Project-URL: Homepage, https://github.com/gitcad-xyz/forge
9
+ Project-URL: Repository, https://github.com/gitcad-xyz/forge
10
+ Project-URL: Issues, https://github.com/gitcad-xyz/forge/issues
11
+ Project-URL: Design — ADR-0018, https://github.com/gitcad-xyz/gitcad/blob/main/docs/adr/0018-native-kernel.md
12
+ Project-URL: Benchmarks vs OCCT, https://github.com/gitcad-xyz/gitcad/blob/main/bench/RUST-vs-OCCT.md
13
+ Keywords: cad,b-rep,brep,geometry,kernel,nurbs,solid-modeling,computational-geometry,exact-arithmetic,rational-arithmetic,step
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: Manufacturing
17
+ Classifier: Intended Audience :: Science/Research
18
+ Classifier: License :: OSI Approved :: Apache Software License
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Scientific/Engineering
25
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
26
+ Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
27
+ Requires-Python: >=3.11
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Provides-Extra: rust
31
+ Requires-Dist: forgekernel_rs>=0.8.0; extra == "rust"
32
+ Provides-Extra: test
33
+ Requires-Dist: pytest>=7; extra == "test"
34
+ Dynamic: license-file
35
+
36
+ # forge
37
+
38
+ An exact-arithmetic boundary-representation (B-rep) CAD kernel. Geometric and
39
+ topological decisions use exact rational arithmetic (ℚ, ℚ[π], ℚ[√d]) or
40
+ certified intervals; no result depends on a floating-point tolerance.
41
+
42
+ It ships as two PyPI packages:
43
+
44
+ - `forgekernel` — the kernel, pure Python.
45
+ - `forgekernel_rs` — a native (Rust) build of the performance-critical routines,
46
+ installed automatically when a wheel is available and returning the same
47
+ results.
48
+
49
+ ## Capabilities
50
+
51
+ - Planar solids: rational linear algebra, plane-based B-rep, booleans,
52
+ divergence-theorem mass properties.
53
+ - Quadrics (cylinder, cone, sphere, torus) with closed-form intersections.
54
+ - NURBS curves and surfaces; surface–surface intersection with complete branch
55
+ detection and certified points.
56
+ - Procedural offsets and shells; edge blends (fillets); lofts.
57
+ - Exact volume, centroid, and inertia via the divergence theorem.
58
+ - STEP (AP203/AP214) import and export.
59
+
60
+ ## Design
61
+
62
+ A Python reference implementation defines the exact semantics. A Rust build
63
+ provides the same operations for production use; each ported routine is
64
+ cross-checked against the reference. Design notes and decisions are recorded in
65
+ the gitcad repository (ADR-0018).
66
+
67
+ ## Layout
68
+
69
+ - `src/forgekernel/` — the Python kernel.
70
+ - `rust/forge-core/` — the native build (`forgekernel_rs`).
71
+
72
+ ## Install
73
+
74
+ ```
75
+ pip install forgekernel # pure Python
76
+ pip install forgekernel[rust] # add the native build
77
+ ```
78
+
79
+ License: Apache-2.0. Source: https://github.com/gitcad-xyz/forge
@@ -7,6 +7,7 @@ src/forgekernel/bsolid.py
7
7
  src/forgekernel/csg.py
8
8
  src/forgekernel/curve.py
9
9
  src/forgekernel/exact.py
10
+ src/forgekernel/hlr.py
10
11
  src/forgekernel/interval.py
11
12
  src/forgekernel/io.py
12
13
  src/forgekernel/kernel.py
@@ -25,6 +26,7 @@ src/forgekernel.egg-info/SOURCES.txt
25
26
  src/forgekernel.egg-info/dependency_links.txt
26
27
  src/forgekernel.egg-info/requires.txt
27
28
  src/forgekernel.egg-info/top_level.txt
29
+ tests/test_hlr.py
28
30
  tests/test_k1_exact.py
29
31
  tests/test_k37_loft.py
30
32
  tests/test_k38_profile.py
@@ -1,6 +1,6 @@
1
1
 
2
2
  [rust]
3
- forgekernel_rs>=0.1.0
3
+ forgekernel_rs>=0.8.0
4
4
 
5
5
  [test]
6
6
  pytest>=7
@@ -0,0 +1,142 @@
1
+ """Native hidden-line removal (ADR-0020) — visible/hidden 2D polylines per view.
2
+
3
+ Visibility is a display property (floats legal); these check the projected
4
+ geometry lands where it must and holes/occlusion behave."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import math
9
+
10
+ from forgekernel.brep import Solid
11
+ from forgekernel.hlr import hidden_line, section_polys
12
+ from forgekernel.quadric import Cyl, DrilledSolid
13
+
14
+
15
+ def _chain(segs, tol=1e-4):
16
+ """Chain 2D segments into closed loops (mirrors the drawing engine)."""
17
+ def key(p):
18
+ return (round(p[0] / tol), round(p[1] / tol))
19
+ rem = [list(s) for s in segs if len(s) >= 2]
20
+ loops = []
21
+ while rem:
22
+ loop = rem.pop()
23
+ while key(loop[0]) != key(loop[-1]):
24
+ for i, c in enumerate(rem):
25
+ if key(c[0]) == key(loop[-1]):
26
+ loop += c[1:]
27
+ rem.pop(i)
28
+ break
29
+ if key(c[-1]) == key(loop[-1]):
30
+ loop += list(reversed(c))[1:]
31
+ rem.pop(i)
32
+ break
33
+ else:
34
+ break
35
+ if key(loop[0]) == key(loop[-1]) and len(loop) >= 4:
36
+ loops.append(loop)
37
+ return loops
38
+
39
+
40
+ def _bbox2d(polys):
41
+ xs = [p[0] for pl in polys for p in pl]
42
+ ys = [p[1] for pl in polys for p in pl]
43
+ return min(xs), max(xs), min(ys), max(ys)
44
+
45
+
46
+ def test_box_front_and_top_project_to_the_right_rectangle() -> None:
47
+ box = Solid.box(20, 10, 5)
48
+ front = hidden_line(box, (0, -1, 0), (1, 0, 0)) # sheet x=X, y=Z
49
+ x0, x1, y0, y1 = _bbox2d(front["visible"])
50
+ assert (round(x0), round(x1), round(y0), round(y1)) == (0, 20, 0, 5)
51
+ top = hidden_line(box, (0, 0, 1), (1, 0, 0)) # sheet x=X, y=Y
52
+ x0, x1, y0, y1 = _bbox2d(top["visible"])
53
+ assert (round(x0), round(x1), round(y0), round(y1)) == (0, 20, 0, 10)
54
+
55
+
56
+ def test_box_back_edges_are_hidden_not_visible() -> None:
57
+ # a solid box has no hidden geometry that isn't coincident with an outline;
58
+ # but every projected outline is present and closed (>= 4 visible runs)
59
+ box = Solid.box(8, 8, 8)
60
+ r = hidden_line(box, (0, -1, 0), (1, 0, 0))
61
+ assert len(r["visible"]) >= 4
62
+
63
+
64
+ def test_drilled_plate_shows_the_hole_and_hides_the_bore_wall() -> None:
65
+ # 40x20x5 plate, Ø8 through-hole at (20,10)
66
+ plate = DrilledSolid(Solid.box(40, 20, 5),
67
+ [Cyl(20, 10, 4, 0, 5)])
68
+ top = hidden_line(plate, (0, 0, 1), (1, 0, 0)) # looking down the bore
69
+ # the rim circle appears near (20,10) with radius ~4
70
+ pts = [p for pl in top["visible"] for p in pl]
71
+ near = [p for p in pts if abs(math.hypot(p[0] - 20, p[1] - 10) - 4) < 0.6]
72
+ assert near, "hole rim circle should be visible in the top view"
73
+ # front view: the bore is behind the front face -> its wall runs are hidden
74
+ front = hidden_line(plate, (0, -1, 0), (1, 0, 0))
75
+ assert front["hidden"], "bore walls should be hidden in the front view"
76
+
77
+
78
+ def test_deflection_controls_circle_fidelity() -> None:
79
+ plate = DrilledSolid(Solid.box(30, 30, 4), [Cyl(15, 15, 6, 0, 4)])
80
+ coarse = hidden_line(plate, (0, 0, 1), (1, 0, 0), deflection=0.5)
81
+ fine = hidden_line(plate, (0, 0, 1), (1, 0, 0), deflection=0.05)
82
+ n_coarse = sum(len(pl) for pl in coarse["visible"])
83
+ n_fine = sum(len(pl) for pl in fine["visible"])
84
+ assert n_fine > n_coarse
85
+
86
+
87
+ # -- section curves -----------------------------------------------------------
88
+
89
+ def test_section_of_box_is_the_cut_rectangle() -> None:
90
+ box = Solid.box(20, 10, 10)
91
+ segs = section_polys(box, (1, 0, 0), (0, 1, 0), 10.0) # plane x=10
92
+ loops = _chain(segs)
93
+ assert len(loops) == 1
94
+ xs = [p[0] for lp in loops for p in lp]
95
+ ys = [p[1] for lp in loops for p in lp]
96
+ assert (round(min(xs)), round(max(xs))) == (0, 10) # sheet x = Y
97
+ assert (round(min(ys)), round(max(ys))) == (0, 10) # sheet y = Z
98
+
99
+
100
+ def test_section_through_drilled_hole_splits_into_two_loops() -> None:
101
+ # box 20x10x10, Ø4 through-hole at (10,5); section on the hole axis (x=10)
102
+ # is the material on either side of the slot: exactly two closed loops.
103
+ plate = DrilledSolid(Solid.box(20, 10, 10), [Cyl(10, 5, 2, 0, 10)])
104
+ segs = section_polys(plate, (1, 0, 0), (0, 1, 0), 10.0)
105
+ loops = _chain(segs)
106
+ assert len(loops) == 2
107
+ spans = sorted((round(min(p[0] for p in lp)), round(max(p[0] for p in lp)))
108
+ for lp in loops)
109
+ assert spans == [(0, 3), (7, 10)] # slot clears Y∈(3,7)
110
+
111
+
112
+ def test_counterbore_sections_as_one_stepped_profile() -> None:
113
+ # coaxial bores (r2 through + r4 counterbore over z7..10) must section as a
114
+ # single stepped profile: two closed loops with the shoulder step, NOT the
115
+ # inner wall drawn straight through the empty counterbore cavity.
116
+ cbore = DrilledSolid(Solid.box(20, 20, 10),
117
+ [Cyl(10, 10, 2, 0, 10), Cyl(10, 10, 4, 7, 10)])
118
+ side = _chain(section_polys(cbore, (0, 1, 0), (1, 0, 0), 10.0))
119
+ assert len(side) == 2 # material either side of the bore
120
+ # each loop steps in at the shoulder: both the r2 wall (x=8/12) and the r4
121
+ # wall (x=6/14) appear, so the x-extent per loop is 8 (0..8), not 6.
122
+ left = min(side, key=lambda lp: min(p[0] for p in lp))
123
+ xs = sorted({round(p[0], 3) for p in left})
124
+ assert 6.0 in xs and 8.0 in xs # both radii present -> stepped
125
+ # a plane normal to the bore, inside the counterbore band, shows ONLY the
126
+ # outer (r4) circle — no phantom inner r2 circle.
127
+ top = section_polys(cbore, (0, 0, 1), (1, 0, 0), 8.5)
128
+ radii = [round(math.hypot(p[0] - 10, p[1] - 10), 2)
129
+ for pl in top for p in pl if math.hypot(p[0] - 10, p[1] - 10) < 6]
130
+ assert radii and all(abs(r - 4.0) < 0.3 for r in radii)
131
+
132
+
133
+ def test_section_normal_to_bore_shows_the_hole_circle() -> None:
134
+ # a plane perpendicular to the bore axis cuts the wall as a full circle:
135
+ # outer rectangle + inner circle = two loops (the hole stays clear).
136
+ plate = DrilledSolid(Solid.box(20, 20, 10), [Cyl(10, 10, 3, 0, 10)])
137
+ segs = section_polys(plate, (0, 0, 1), (1, 0, 0), 5.0) # plane z=5
138
+ loops = _chain(segs)
139
+ assert len(loops) == 2
140
+ inner = min(loops, key=lambda lp: max(p[0] for p in lp) - min(p[0] for p in lp))
141
+ r = (max(p[0] for p in inner) - min(p[0] for p in inner)) / 2
142
+ assert abs(r - 3.0) < 0.2 # the bore radius, within faceting
@@ -1,82 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: forgekernel
3
- Version: 0.1.0
4
- Summary: Exact-arithmetic B-rep CAD kernel: topology decided in ℚ, never by floating-point epsilon
5
- Author-email: Dan Willis <danielcwillis@gmail.com>
6
- Maintainer-email: Dan Willis <danielcwillis@gmail.com>
7
- License: Apache-2.0
8
- Project-URL: Homepage, https://github.com/gitcad-xyz/forge
9
- Project-URL: Repository, https://github.com/gitcad-xyz/forge
10
- Project-URL: Issues, https://github.com/gitcad-xyz/forge/issues
11
- Project-URL: Design — ADR-0018, https://github.com/gitcad-xyz/gitcad/blob/main/docs/adr/0018-native-kernel.md
12
- Project-URL: Benchmarks vs OCCT, https://github.com/gitcad-xyz/gitcad/blob/main/bench/RUST-vs-OCCT.md
13
- Keywords: cad,b-rep,brep,geometry,kernel,nurbs,solid-modeling,computational-geometry,exact-arithmetic,rational-arithmetic,step,occt,opencascade
14
- Classifier: Development Status :: 3 - Alpha
15
- Classifier: Intended Audience :: Developers
16
- Classifier: Intended Audience :: Manufacturing
17
- Classifier: Intended Audience :: Science/Research
18
- Classifier: License :: OSI Approved :: Apache Software License
19
- Classifier: Operating System :: OS Independent
20
- Classifier: Programming Language :: Python :: 3
21
- Classifier: Programming Language :: Python :: 3.11
22
- Classifier: Programming Language :: Python :: 3.12
23
- Classifier: Programming Language :: Python :: 3.13
24
- Classifier: Topic :: Scientific/Engineering
25
- Classifier: Topic :: Scientific/Engineering :: Mathematics
26
- Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
27
- Requires-Python: >=3.11
28
- Description-Content-Type: text/markdown
29
- License-File: LICENSE
30
- Provides-Extra: rust
31
- Requires-Dist: forgekernel_rs>=0.1.0; extra == "rust"
32
- Provides-Extra: test
33
- Requires-Dist: pytest>=7; extra == "test"
34
- Dynamic: license-file
35
-
36
- # forge — the gitcad-native B-rep kernel
37
-
38
- From-scratch, unencumbered (Apache-2.0), built to pass the bar OCCT
39
- sets and then raise it — the plan and the decision record live in
40
- gitcad ([ADR-0018](https://github.com/gitcad-xyz/gitcad/blob/main/docs/adr/0018-native-kernel.md),
41
- [coverage plan](https://github.com/gitcad-xyz/gitcad/blob/main/docs/research/kernel-coverage-plan.md)).
42
-
43
- ## The three-oracle chain
44
-
45
- ```
46
- forgekernel.ref Python + exact rational arithmetic — the executable
47
- specification; topological decisions are EXACT, never
48
- epsilon-guarded
49
- ⇅ differential
50
- OCCT the 30-year-hardened independent oracle, driven from
51
- the gitcad benchmark corpus through the Kernel seam
52
- ⇅ oracle
53
- forge (Rust) the production port, added operator class by operator
54
- class once ref has proven the semantics
55
- ```
56
-
57
- ## How progress is measured
58
-
59
- Not vibes: the [benchmark trend](https://github.com/gitcad-xyz/gitcad/blob/main/bench/TREND.md)
60
- in the gitcad repo scores every backend on the shared corpus —
61
- capability %, torture-case pass rate, correctness deltas, wall time.
62
- Day-one baseline: OCCT scores 93.8%, failing `swept_channel`
63
- (sharp-cornered sweep → invalid geometry). Beating that number with
64
- exact arithmetic is the first milestone.
65
-
66
- ## Roadmap (gates, not dates)
67
-
68
- - **K1** exact planar core: rational linear algebra, plane-based
69
- polyhedral B-rep, epsilon-free booleans, divergence-theorem mass
70
- properties, native lineage. Gate: planar corpus green vs OCCT.
71
- - **K2** quadrics + torus with closed-form intersectors — most of
72
- real mech. Gate: ≥80% corpus green.
73
- - **K3** NURBS + branch-complete surface–surface intersection (the
74
- crown jewel). **K4** procedural offsets/shell. **K5** G2 blends.
75
- **K6** the surfacing suite — the SolidWorks-class end goal.
76
-
77
- ## Layout
78
-
79
- - `src/forgekernel/` — the Python reference kernel (`ref`)
80
- - `rust/` — the forge port (arrives with K1 stability)
81
- - gitcad consumes both through its `Kernel` seam; nothing here depends
82
- on gitcad
@@ -1,47 +0,0 @@
1
- # forge — the gitcad-native B-rep kernel
2
-
3
- From-scratch, unencumbered (Apache-2.0), built to pass the bar OCCT
4
- sets and then raise it — the plan and the decision record live in
5
- gitcad ([ADR-0018](https://github.com/gitcad-xyz/gitcad/blob/main/docs/adr/0018-native-kernel.md),
6
- [coverage plan](https://github.com/gitcad-xyz/gitcad/blob/main/docs/research/kernel-coverage-plan.md)).
7
-
8
- ## The three-oracle chain
9
-
10
- ```
11
- forgekernel.ref Python + exact rational arithmetic — the executable
12
- specification; topological decisions are EXACT, never
13
- epsilon-guarded
14
- ⇅ differential
15
- OCCT the 30-year-hardened independent oracle, driven from
16
- the gitcad benchmark corpus through the Kernel seam
17
- ⇅ oracle
18
- forge (Rust) the production port, added operator class by operator
19
- class once ref has proven the semantics
20
- ```
21
-
22
- ## How progress is measured
23
-
24
- Not vibes: the [benchmark trend](https://github.com/gitcad-xyz/gitcad/blob/main/bench/TREND.md)
25
- in the gitcad repo scores every backend on the shared corpus —
26
- capability %, torture-case pass rate, correctness deltas, wall time.
27
- Day-one baseline: OCCT scores 93.8%, failing `swept_channel`
28
- (sharp-cornered sweep → invalid geometry). Beating that number with
29
- exact arithmetic is the first milestone.
30
-
31
- ## Roadmap (gates, not dates)
32
-
33
- - **K1** exact planar core: rational linear algebra, plane-based
34
- polyhedral B-rep, epsilon-free booleans, divergence-theorem mass
35
- properties, native lineage. Gate: planar corpus green vs OCCT.
36
- - **K2** quadrics + torus with closed-form intersectors — most of
37
- real mech. Gate: ≥80% corpus green.
38
- - **K3** NURBS + branch-complete surface–surface intersection (the
39
- crown jewel). **K4** procedural offsets/shell. **K5** G2 blends.
40
- **K6** the surfacing suite — the SolidWorks-class end goal.
41
-
42
- ## Layout
43
-
44
- - `src/forgekernel/` — the Python reference kernel (`ref`)
45
- - `rust/` — the forge port (arrives with K1 stability)
46
- - gitcad consumes both through its `Kernel` seam; nothing here depends
47
- on gitcad
@@ -1,82 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: forgekernel
3
- Version: 0.1.0
4
- Summary: Exact-arithmetic B-rep CAD kernel: topology decided in ℚ, never by floating-point epsilon
5
- Author-email: Dan Willis <danielcwillis@gmail.com>
6
- Maintainer-email: Dan Willis <danielcwillis@gmail.com>
7
- License: Apache-2.0
8
- Project-URL: Homepage, https://github.com/gitcad-xyz/forge
9
- Project-URL: Repository, https://github.com/gitcad-xyz/forge
10
- Project-URL: Issues, https://github.com/gitcad-xyz/forge/issues
11
- Project-URL: Design — ADR-0018, https://github.com/gitcad-xyz/gitcad/blob/main/docs/adr/0018-native-kernel.md
12
- Project-URL: Benchmarks vs OCCT, https://github.com/gitcad-xyz/gitcad/blob/main/bench/RUST-vs-OCCT.md
13
- Keywords: cad,b-rep,brep,geometry,kernel,nurbs,solid-modeling,computational-geometry,exact-arithmetic,rational-arithmetic,step,occt,opencascade
14
- Classifier: Development Status :: 3 - Alpha
15
- Classifier: Intended Audience :: Developers
16
- Classifier: Intended Audience :: Manufacturing
17
- Classifier: Intended Audience :: Science/Research
18
- Classifier: License :: OSI Approved :: Apache Software License
19
- Classifier: Operating System :: OS Independent
20
- Classifier: Programming Language :: Python :: 3
21
- Classifier: Programming Language :: Python :: 3.11
22
- Classifier: Programming Language :: Python :: 3.12
23
- Classifier: Programming Language :: Python :: 3.13
24
- Classifier: Topic :: Scientific/Engineering
25
- Classifier: Topic :: Scientific/Engineering :: Mathematics
26
- Classifier: Topic :: Multimedia :: Graphics :: 3D Modeling
27
- Requires-Python: >=3.11
28
- Description-Content-Type: text/markdown
29
- License-File: LICENSE
30
- Provides-Extra: rust
31
- Requires-Dist: forgekernel_rs>=0.1.0; extra == "rust"
32
- Provides-Extra: test
33
- Requires-Dist: pytest>=7; extra == "test"
34
- Dynamic: license-file
35
-
36
- # forge — the gitcad-native B-rep kernel
37
-
38
- From-scratch, unencumbered (Apache-2.0), built to pass the bar OCCT
39
- sets and then raise it — the plan and the decision record live in
40
- gitcad ([ADR-0018](https://github.com/gitcad-xyz/gitcad/blob/main/docs/adr/0018-native-kernel.md),
41
- [coverage plan](https://github.com/gitcad-xyz/gitcad/blob/main/docs/research/kernel-coverage-plan.md)).
42
-
43
- ## The three-oracle chain
44
-
45
- ```
46
- forgekernel.ref Python + exact rational arithmetic — the executable
47
- specification; topological decisions are EXACT, never
48
- epsilon-guarded
49
- ⇅ differential
50
- OCCT the 30-year-hardened independent oracle, driven from
51
- the gitcad benchmark corpus through the Kernel seam
52
- ⇅ oracle
53
- forge (Rust) the production port, added operator class by operator
54
- class once ref has proven the semantics
55
- ```
56
-
57
- ## How progress is measured
58
-
59
- Not vibes: the [benchmark trend](https://github.com/gitcad-xyz/gitcad/blob/main/bench/TREND.md)
60
- in the gitcad repo scores every backend on the shared corpus —
61
- capability %, torture-case pass rate, correctness deltas, wall time.
62
- Day-one baseline: OCCT scores 93.8%, failing `swept_channel`
63
- (sharp-cornered sweep → invalid geometry). Beating that number with
64
- exact arithmetic is the first milestone.
65
-
66
- ## Roadmap (gates, not dates)
67
-
68
- - **K1** exact planar core: rational linear algebra, plane-based
69
- polyhedral B-rep, epsilon-free booleans, divergence-theorem mass
70
- properties, native lineage. Gate: planar corpus green vs OCCT.
71
- - **K2** quadrics + torus with closed-form intersectors — most of
72
- real mech. Gate: ≥80% corpus green.
73
- - **K3** NURBS + branch-complete surface–surface intersection (the
74
- crown jewel). **K4** procedural offsets/shell. **K5** G2 blends.
75
- **K6** the surfacing suite — the SolidWorks-class end goal.
76
-
77
- ## Layout
78
-
79
- - `src/forgekernel/` — the Python reference kernel (`ref`)
80
- - `rust/` — the forge port (arrives with K1 stability)
81
- - gitcad consumes both through its `Kernel` seam; nothing here depends
82
- on gitcad
File without changes
File without changes