forgekernel 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- forgekernel/__init__.py +7 -0
- forgekernel/brep.py +624 -0
- forgekernel/bsolid.py +295 -0
- forgekernel/csg.py +158 -0
- forgekernel/curve.py +166 -0
- forgekernel/exact.py +121 -0
- forgekernel/interval.py +141 -0
- forgekernel/io.py +56 -0
- forgekernel/kernel.py +93 -0
- forgekernel/loft.py +185 -0
- forgekernel/nurbs.py +481 -0
- forgekernel/profile2d.py +228 -0
- forgekernel/quadric.py +979 -0
- forgekernel/ssi.py +524 -0
- forgekernel/stepio.py +460 -0
- forgekernel/surd.py +104 -0
- forgekernel/surfacing.py +341 -0
- forgekernel/tess.py +74 -0
- forgekernel/trim.py +193 -0
- forgekernel-0.1.0.dist-info/METADATA +82 -0
- forgekernel-0.1.0.dist-info/RECORD +24 -0
- forgekernel-0.1.0.dist-info/WHEEL +5 -0
- forgekernel-0.1.0.dist-info/licenses/LICENSE +201 -0
- forgekernel-0.1.0.dist-info/top_level.txt +1 -0
forgekernel/profile2d.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""K3.8 — spline sketch profiles with EXACT area (Green's theorem).
|
|
2
|
+
|
|
3
|
+
A closed 2D profile of line and polynomial-Bézier segments encloses an
|
|
4
|
+
area that is *exactly rational*:
|
|
5
|
+
|
|
6
|
+
A = ½ ∮ (x dy − y dx)
|
|
7
|
+
|
|
8
|
+
Over a line segment this is ½(x0 y1 − x1 y0); over a Bézier segment
|
|
9
|
+
x(t), y(t) are polynomials, so ½(x y' − y x') is a polynomial and its
|
|
10
|
+
integral over [0,1] is exact. Extruding such a profile therefore has an
|
|
11
|
+
exactly rational volume A·h — a curved-boundary solid OCCT can only
|
|
12
|
+
Gauss-quadrature.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from fractions import Fraction
|
|
18
|
+
|
|
19
|
+
from forgekernel.bsolid import _lagrange_weights, _nodes
|
|
20
|
+
|
|
21
|
+
F = Fraction
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _bezier2(pts, t):
|
|
25
|
+
"""De Casteljau on 2D control points at parameter t (exact)."""
|
|
26
|
+
p = [(F(a), F(b)) for a, b in pts]
|
|
27
|
+
n = len(p)
|
|
28
|
+
for r in range(1, n):
|
|
29
|
+
p = [((1 - t) * p[i][0] + t * p[i + 1][0],
|
|
30
|
+
(1 - t) * p[i][1] + t * p[i + 1][1]) for i in range(n - r)]
|
|
31
|
+
return p[0]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _bezier2_d(pts):
|
|
35
|
+
"""Control points of the derivative curve (degree p-1), exact."""
|
|
36
|
+
p = len(pts) - 1
|
|
37
|
+
return [(F(p) * (F(pts[i + 1][0]) - F(pts[i][0])),
|
|
38
|
+
F(p) * (F(pts[i + 1][1]) - F(pts[i][1]))) for i in range(p)]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def segments_to_beziers(start, segments):
|
|
42
|
+
"""Normalize a profile into a list of Bézier control-point lists.
|
|
43
|
+
Lines → degree-1 Béziers; arcs are rejected here (they belong to the
|
|
44
|
+
ℚ[π] path). ``spline`` segments carry explicit control points."""
|
|
45
|
+
beziers = []
|
|
46
|
+
cur = (F(start[0]), F(start[1]))
|
|
47
|
+
for seg in segments:
|
|
48
|
+
kind = seg["kind"]
|
|
49
|
+
to = (F(seg["to"][0]), F(seg["to"][1]))
|
|
50
|
+
if kind == "line":
|
|
51
|
+
beziers.append([cur, to])
|
|
52
|
+
elif kind == "spline":
|
|
53
|
+
# Bézier control points between cur and to (exclusive endpoints
|
|
54
|
+
# given in "ctrl"); a cubic if two ctrl points, etc.
|
|
55
|
+
ctrl = [(F(a), F(b)) for a, b in seg.get("ctrl", [])]
|
|
56
|
+
beziers.append([cur, *ctrl, to])
|
|
57
|
+
else:
|
|
58
|
+
raise ValueError(f"profile2d: segment kind {kind!r} not exact "
|
|
59
|
+
f"(arcs → ℚ[π] path)")
|
|
60
|
+
cur = to
|
|
61
|
+
# close the loop back to the start if the last point doesn't already —
|
|
62
|
+
# Green's theorem needs a CLOSED contour or the area is meaningless
|
|
63
|
+
# (the line-extrude path auto-closes; the spline path must too).
|
|
64
|
+
s0 = (F(start[0]), F(start[1]))
|
|
65
|
+
if cur != s0:
|
|
66
|
+
beziers.append([cur, s0])
|
|
67
|
+
return beziers
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def exact_signed_area(start, segments) -> Fraction:
|
|
71
|
+
"""Signed area of the closed profile via Green's theorem — exact ℚ.
|
|
72
|
+
CCW positive. Keep the sign when pairing with the area moments so the
|
|
73
|
+
orientation cancels in the centroid ratio."""
|
|
74
|
+
beziers = segments_to_beziers(start, segments)
|
|
75
|
+
total = F(0)
|
|
76
|
+
for bez in beziers:
|
|
77
|
+
if len(bez) == 2: # line: ½(x0 y1 − x1 y0)
|
|
78
|
+
(x0, y0), (x1, y1) = bez
|
|
79
|
+
total += (x0 * y1 - x1 * y0) / 2
|
|
80
|
+
else: # Bézier: ∫ ½(x y' − y x') dt
|
|
81
|
+
dctrl = _bezier2_d(bez)
|
|
82
|
+
p = len(bez) - 1
|
|
83
|
+
# integrand degree ≤ 2p-1 → 2p nodes exact
|
|
84
|
+
nn = _nodes(2 * p)
|
|
85
|
+
ww = _lagrange_weights(nn)
|
|
86
|
+
seg_int = F(0)
|
|
87
|
+
for w, t in zip(ww, nn):
|
|
88
|
+
x, y = _bezier2(bez, t)
|
|
89
|
+
dx, dy = _bezier2(dctrl, t)
|
|
90
|
+
seg_int += w * (x * dy - y * dx) / 2
|
|
91
|
+
total += seg_int
|
|
92
|
+
return total
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def exact_area(start, segments) -> Fraction:
|
|
96
|
+
"""Absolute enclosed area (a positive Fraction), exact ℚ."""
|
|
97
|
+
return abs(exact_signed_area(start, segments))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def exact_moments(start, segments):
|
|
101
|
+
"""First area moments (Qx = ∫∫ x dA, Qy = ∫∫ y dA) of the closed profile
|
|
102
|
+
via Green's theorem — exact ℚ. Qx = ∮ ½x² dy and Qy = −∮ ½y² dx; the
|
|
103
|
+
integrand ``x²·y'`` has degree ≤ 3p−1 over a degree-p Bézier, so 3p
|
|
104
|
+
interpolatory nodes integrate it exactly. Signs follow the loop
|
|
105
|
+
orientation (pair with :func:`exact_signed_area`)."""
|
|
106
|
+
beziers = segments_to_beziers(start, segments)
|
|
107
|
+
qx = qy = F(0)
|
|
108
|
+
for bez in beziers:
|
|
109
|
+
dctrl = _bezier2_d(bez)
|
|
110
|
+
p = len(bez) - 1
|
|
111
|
+
nn = _nodes(3 * p)
|
|
112
|
+
ww = _lagrange_weights(nn)
|
|
113
|
+
for w, t in zip(ww, nn):
|
|
114
|
+
x, y = _bezier2(bez, t)
|
|
115
|
+
dx, dy = _bezier2(dctrl, t)
|
|
116
|
+
qx += w * (x * x) / 2 * dy
|
|
117
|
+
qy += w * (-(y * y)) / 2 * dx
|
|
118
|
+
return qx, qy
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _boundary_is_simple(start, segments, samples: int = 16) -> bool:
|
|
122
|
+
"""Flatten the line/Bézier boundary to a polyline and test for a proper
|
|
123
|
+
self-intersection of non-adjacent edges (a simple-loop guard)."""
|
|
124
|
+
ring = []
|
|
125
|
+
for bez in segments_to_beziers(start, segments):
|
|
126
|
+
steps = 1 if len(bez) == 2 else samples
|
|
127
|
+
for k in range(steps):
|
|
128
|
+
t = F(k, steps)
|
|
129
|
+
ring.append(_bezier2(bez, t))
|
|
130
|
+
n = len(ring)
|
|
131
|
+
if n < 3:
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
def seg_cross(a, b, c, d):
|
|
135
|
+
def orient(p, q, r):
|
|
136
|
+
return ((q[0] - p[0]) * (r[1] - p[1])
|
|
137
|
+
- (q[1] - p[1]) * (r[0] - p[0]))
|
|
138
|
+
o1, o2 = orient(a, b, c), orient(a, b, d)
|
|
139
|
+
o3, o4 = orient(c, d, a), orient(c, d, b)
|
|
140
|
+
return (o1 > 0) != (o2 > 0) and (o3 > 0) != (o4 > 0)
|
|
141
|
+
|
|
142
|
+
for i in range(n):
|
|
143
|
+
a, b = ring[i], ring[(i + 1) % n]
|
|
144
|
+
for j in range(i + 1, n):
|
|
145
|
+
if j == i or (j + 1) % n == i or j == (i + 1) % n:
|
|
146
|
+
continue # skip shared-vertex neighbours
|
|
147
|
+
c, d = ring[j], ring[(j + 1) % n]
|
|
148
|
+
if seg_cross(a, b, c, d):
|
|
149
|
+
return False
|
|
150
|
+
return True
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class SplinePrism:
|
|
154
|
+
"""Extrusion of a closed line/Bézier profile — exact rational volume
|
|
155
|
+
A·h (Green's-theorem area). Curved boundary; planar top/bottom caps."""
|
|
156
|
+
|
|
157
|
+
provenance = "exact"
|
|
158
|
+
|
|
159
|
+
def __init__(self, start, segments, height, base_z=0) -> None:
|
|
160
|
+
self.start = (F(start[0]), F(start[1]))
|
|
161
|
+
self.segments = segments
|
|
162
|
+
self.h = F(height)
|
|
163
|
+
self.z0 = F(base_z)
|
|
164
|
+
if self.h == 0:
|
|
165
|
+
raise ValueError("spline prism has zero height")
|
|
166
|
+
# a self-intersecting boundary makes the signed Green's area
|
|
167
|
+
# meaningless (opposite lobes cancel) — catch it explicitly rather
|
|
168
|
+
# than mis-diagnosing it as "zero area".
|
|
169
|
+
if not _boundary_is_simple(start, segments):
|
|
170
|
+
raise ValueError("profile boundary self-intersects (not a simple loop)")
|
|
171
|
+
self._area = exact_area(start, segments)
|
|
172
|
+
if self._area == 0:
|
|
173
|
+
raise ValueError("spline prism has zero enclosed area")
|
|
174
|
+
|
|
175
|
+
def volume(self) -> Fraction:
|
|
176
|
+
return self._area * abs(self.h)
|
|
177
|
+
|
|
178
|
+
def area(self) -> Fraction:
|
|
179
|
+
return self._area
|
|
180
|
+
|
|
181
|
+
def bbox_f(self):
|
|
182
|
+
beziers = segments_to_beziers(self.start, self.segments)
|
|
183
|
+
xs = [float(c[0]) for b in beziers for c in b]
|
|
184
|
+
ys = [float(c[1]) for b in beziers for c in b]
|
|
185
|
+
# control-net hull bounds the curve; sample for a tight-ish box
|
|
186
|
+
for b in beziers:
|
|
187
|
+
if len(b) > 2:
|
|
188
|
+
for k in range(9):
|
|
189
|
+
x, y = _bezier2(b, F(k, 8))
|
|
190
|
+
xs.append(float(x)); ys.append(float(y))
|
|
191
|
+
z0, z1 = float(self.z0), float(self.z0 + self.h)
|
|
192
|
+
return ((min(xs), min(ys), min(z0, z1)),
|
|
193
|
+
(max(xs), max(ys), max(z0, z1)))
|
|
194
|
+
|
|
195
|
+
def centroid(self):
|
|
196
|
+
"""Exact centroid in ℚ³. The profile's area centroid (Green's-theorem
|
|
197
|
+
first moments ÷ signed area) in x, y; the extrusion mid-height in z.
|
|
198
|
+
Orientation cancels because moments and area share the signed factor."""
|
|
199
|
+
a = exact_signed_area(self.start, self.segments)
|
|
200
|
+
qx, qy = exact_moments(self.start, self.segments)
|
|
201
|
+
return (qx / a, qy / a, self.z0 + self.h / 2)
|
|
202
|
+
|
|
203
|
+
def centroid_f(self):
|
|
204
|
+
"""Float centroid derived from the exact :meth:`centroid` — the
|
|
205
|
+
profile's true area centroid, not the bbox centre (which is wrong
|
|
206
|
+
for an asymmetric profile)."""
|
|
207
|
+
return tuple(float(c) for c in self.centroid())
|
|
208
|
+
|
|
209
|
+
def tessellate(self, deflection: float = 0.2) -> dict:
|
|
210
|
+
beziers = segments_to_beziers(self.start, self.segments)
|
|
211
|
+
ring = []
|
|
212
|
+
for b in beziers:
|
|
213
|
+
steps = 1 if len(b) == 2 else 12
|
|
214
|
+
for k in range(steps):
|
|
215
|
+
x, y = _bezier2(b, F(k, steps))
|
|
216
|
+
ring.append((float(x), float(y)))
|
|
217
|
+
z0, z1 = float(self.z0), float(self.z0 + self.h)
|
|
218
|
+
n = len(ring)
|
|
219
|
+
verts = [[x, y, z0] for x, y in ring] + [[x, y, z1] for x, y in ring]
|
|
220
|
+
tris = []
|
|
221
|
+
for i in range(n):
|
|
222
|
+
j = (i + 1) % n
|
|
223
|
+
tris += [[i, j, n + i], [j, n + j, n + i]]
|
|
224
|
+
# fan caps (approximate; render only)
|
|
225
|
+
for i in range(1, n - 1):
|
|
226
|
+
tris.append([0, i, i + 1])
|
|
227
|
+
tris.append([n, n + i + 1, n + i])
|
|
228
|
+
return {"vertices": verts, "triangles": tris}
|