geomcore 0.32.1

A pure, standalone geometric kernel for CAD-grade curves and surfaces: parametric evaluation, transformations, and curve-on-surface parametrization.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
# Elementary curves & surfaces

The ten geometries a kernel knows in closed form: a line, four conics,
and five surfaces. Each is a short formula placed in a frame — a
**canonical shape** plus a **placement** — with a domain, an inverse
map, and known defects at the seams. This page is the reference; the
full story, with measured checks against a production kernel, is
[The mathematics behind geometry](https://avenirgeometry.com/articles/elementary-geometry/),
from which the formulas and figures here are adapted.

## Foundations

**Point, vector, direction** are three separate types. A point has
position and nothing else; a vector has length and orientation but no
position, so translations do not apply to it; a direction is a vector
constrained to length 1. `geomcore` enforces this in its type system
(`Point3D`, `Vector3D`), because mixing them produces bugs that look
like geometry errors — the classic one is translating a normal.

**Canonical shape plus placement.** Nothing is stored as a general
shape in space. A circle is a radius plus an origin and a frame; every
geometry below follows the same division. Placements come in three
sizes: a point and one direction (an axis — lines, surfaces of
revolution), a point and two directions (a plane frame — the conics are
planar curves), a point and three directions (a full frame — the
surfaces). Frames are right-handed: flip the third direction and every
normal flips with it.

**A curve is a map** $C: I \subset \mathbb{R} \to \mathbb{R}^3$ **from an
interval; a surface is a map** $S: D \subset \mathbb{R}^2 \to
\mathbb{R}^3$ **from a rectangle.** The domain is part of the geometry:
the circle formula on a quarter turn is an arc, and a closed domain
$[0, 2\pi]$ whose ends evaluate to the same point is what *periodic*
means. Forward evaluation is cheap; the way back — point to parameter —
is a separate algorithm, closed-form for all ten below, which is
largely why they are kept as special cases.

## The five curves

### Line

The only elementary curve with no frame: a point and a direction, and
the parameter is arc length — equal steps in $u$ are equal steps in
space.

<figure class="geom-fig">
<img src="../../assets/figures/elementary-geometry/06-a-straight-line-with-equally-spaced.svg" alt="A line with equally spaced parameter marks">
<figcaption>A line. u is the signed distance from the origin along the direction.</figcaption>
</figure>

$$
C(u) = O + u\,\vec{D}, \qquad u \in (-\infty, +\infty)
$$

The way back: $u = (P - O) \cdot \vec{D}$.

=== "Python"

    ```python
    from geomcore import Point3D, Vector3D
    from geomcore.curves import Line3D

    line = Line3D(Point3D.origin(), Vector3D.z())
    p = line.eval_point(2.0)
    ```

=== "Rust"

    ```rust
    use geomcore::{Point3D, Vector3D};
    use geomcore::curves::Line3D;

    let line = Line3D::new(Point3D::ORIGIN, Vector3D::Z).unwrap();
    let p = line.eval_point(2.0);
    ```

### Circle

The parameter is the angle from X, and arc length is $R\,u$ — the only
conic whose parameter is proportional to distance travelled.

<figure class="geom-fig">
<img src="../../assets/figures/elementary-geometry/07-a-circle-with-a-point-resolved.svg" alt="A circle with a point resolved into its coordinates">
<figcaption>A circle. Equal steps in u are equal steps along the curve.</figcaption>
</figure>

$$
C(u) = O + R\cos u\,\vec{X} + R\sin u\,\vec{Y}, \qquad u \in [0, 2\pi]
$$

The way back: $u = \mathrm{atan2}(y, x)$, with $x, y$ the point
resolved in the placement frame. Periodic: $C(u + 2\pi) = C(u)$.

=== "Python"

    ```python
    from geomcore.curves import Circle3D

    circle = Circle3D(Point3D.origin(), Vector3D.z(), 2.0)
    assert abs(circle.circumference() - 4.0 * 3.141592653589793) < 1e-12
    ```

=== "Rust"

    ```rust
    use geomcore::curves::Circle3D;

    let circle = Circle3D::new(Point3D::ORIGIN, Vector3D::Z, 2.0).unwrap();
    ```

### Ellipse

Reads like the circle with two radii, but the parameter is the
**eccentric** angle — taken on the circle of radius $a$, projected onto
the curve — not the polar angle from the centre. It is not arc length
either: speed runs from $b$ to $a$, so uniform steps bunch at the ends
of the major axis.

<figure class="geom-fig">
<img src="../../assets/figures/elementary-geometry/08-an-ellipse-with-a-point-resolved.svg" alt="An ellipse with a point resolved into its coordinates">
<figcaption>An ellipse. u is the eccentric angle, not the polar angle.</figcaption>
</figure>

$$
C(u) = O + a\cos u\,\vec{X} + b\sin u\,\vec{Y}, \qquad u \in [0, 2\pi]
$$

The way back: $u = \mathrm{atan2}(y/b,\; x/a)$ — divide out the radii
and the ellipse becomes the circle whose angle is $u$.

=== "Python"

    ```python
    from geomcore.curves import Ellipse3D

    ellipse = Ellipse3D(Point3D.origin(), Vector3D.z(), Vector3D.x(), 3.0, 1.0)
    ```

=== "Rust"

    ```rust
    use geomcore::curves::Ellipse3D;

    let ellipse = Ellipse3D::new(Point3D::ORIGIN, Vector3D::Z, Vector3D::X, 3.0, 1.0).unwrap();
    ```

### Hyperbola

$\cosh$ is never below 1, so this map reaches **one branch** only; the
mirror branch is a second curve no value of $u$ reaches.

<figure class="geom-fig">
<img src="../../assets/figures/elementary-geometry/09-one-branch-of-a-hyperbola-with.svg" alt="One branch of a hyperbola">
<figcaption>A hyperbola. One branch only — the opposite branch is a second curve.</figcaption>
</figure>

$$
C(u) = O + a\cosh u\,\vec{X} + b\sinh u\,\vec{Y}, \qquad u \in (-\infty, +\infty)
$$

The way back: $u = \mathrm{arsinh}(y/b)$. Implicit form:
$\frac{x^2}{a^2} - \frac{y^2}{b^2} = 1$.

=== "Python"

    ```python
    from geomcore.curves import Hyperbola3D

    hyp = Hyperbola3D(Point3D.origin(), Vector3D.z(), Vector3D.x(), 2.0, 1.0)
    ```

=== "Rust"

    ```rust
    use geomcore::curves::Hyperbola3D;

    let hyp = Hyperbola3D::new(Point3D::ORIGIN, Vector3D::Z, Vector3D::X, 2.0, 1.0).unwrap();
    ```

### Parabola

The transpose of the schoolbook $y = x^2$: the parameter runs along
$Y$, the curve opens along $X$. $f$ is the focal distance — focus at
$(f, 0)$, directrix $x = -f$.

<figure class="geom-fig">
<img src="../../assets/figures/elementary-geometry/10-a-parabola-with-focus-directrix-and.svg" alt="A parabola with focus and directrix">
<figcaption>A parabola. The parameter is the offset along Y; the axis is X.</figcaption>
</figure>

$$
C(u) = O + \frac{u^2}{4f}\,\vec{X} + u\,\vec{Y}, \qquad u \in (-\infty, +\infty)
$$

The way back: $u = y$.

=== "Python"

    ```python
    from geomcore.curves import Parabola3D

    parab = Parabola3D(Point3D.origin(), Vector3D.z(), Vector3D.x(), 1.5)
    ```

=== "Rust"

    ```rust
    use geomcore::curves::Parabola3D;

    let parab = Parabola3D::new(Point3D::ORIGIN, Vector3D::Z, Vector3D::X, 1.5).unwrap();
    ```

## The five surfaces

Four are surfaces of revolution — a profile curve turned about an
axis — so the two horizontal components are always some radius times
$\cos u$ and $\sin u$. They differ only in that radius and in the third
component. With $\rho = \sqrt{x^2 + y^2}$ the distance from the axis,
four of the five inverses start with the same $u = \mathrm{atan2}(y, x)$.

### Plane

Both parameters are lengths; the only surface with no angles, no
periodicity, no degenerate point.

<figure class="geom-fig">
<img src="../../assets/figures/elementary-geometry/11-a-plane-with-a-point-resolved.svg" alt="A plane with a point resolved into its coordinates">
<figcaption>A plane. u and v are lengths along the frame directions.</figcaption>
</figure>

$$
S(u, v) = O + u\,\vec{X} + v\,\vec{Y}, \qquad (u, v) \in (-\infty, +\infty)^2
$$

The way back: $u = x,\; v = y$.

=== "Python"

    ```python
    from geomcore.surfaces import Plane

    plane = Plane(Point3D.origin(), Vector3D.z())
    ```

=== "Rust"

    ```rust
    use geomcore::surfaces::Plane;

    let plane = Plane::new(Point3D::ORIGIN, Vector3D::Z).unwrap();
    ```

### Cylinder

$u$ is an angle, $v$ a length — the only surface whose parameters are
not the same kind of quantity.

<figure class="geom-fig">
<img src="../../assets/figures/elementary-geometry/12-a-cylinder-with-a-point-resolved.svg" alt="A cylinder with a point resolved into its coordinates">
<figcaption>A cylinder. u goes round the axis, v along it.</figcaption>
</figure>

$$
S(u, v) = O + R\cos u\,\vec{X} + R\sin u\,\vec{Y} + v\,\vec{Z}, \qquad (u, v) \in [0, 2\pi] \times (-\infty, +\infty)
$$

The way back: $u = \mathrm{atan2}(y, x),\; v = z$.

=== "Python"

    ```python
    from geomcore.surfaces import Cylinder

    cyl = Cylinder(Point3D.origin(), Vector3D.z(), 2.0)
    ```

=== "Rust"

    ```rust
    use geomcore::surfaces::Cylinder;

    let cyl = Cylinder::new(Point3D::ORIGIN, Vector3D::Z, 2.0).unwrap();
    ```

### Cone

The cylinder's formula with a radius that grows along the axis:
$\rho = R + v\sin\alpha$. `geomcore` measures $v$ **along the ruling**,
so $\lVert \partial S/\partial v \rVert = 1$ exactly — a step of 1 in
$v$ is a step of 1 across the surface. The alternative height
convention ($v = z$, $\rho = R + v\tan\alpha$) shares the cylinder's
parameter plane instead; the article compares both.

<figure class="geom-fig">
<img src="../../assets/figures/elementary-geometry/13-a-cone-with-a-point-resolved.svg" alt="A cone with a point resolved into its coordinates">
<figcaption>A cone. The radius grows along the ruling and reaches zero at the apex.</figcaption>
</figure>

$$
S(u, v) = O + \rho\,(\cos u\,\vec{X} + \sin u\,\vec{Y}) + v\cos\alpha\,\vec{Z}, \qquad (u, v) \in [0, 2\pi] \times (-\infty, +\infty)
$$

The way back: $u = \mathrm{atan2}(y, x),\; v = \sin\alpha\,(\rho - R) + \cos\alpha\,z$.

At $v_{\mathrm{apex}} = -R / \sin\alpha$ the radius reaches zero: a
whole edge of the rectangle is one point, the normal vanishes, and past
it $\rho$ goes negative into the opposite nappe. The frame sits on the
reference circle ($v = 0$), not at the apex — the only point with no
normal is a bad datum, it is often hundreds of radii from the part, and
anchoring there would break the cone→cylinder limit as $\alpha \to 0$.

=== "Python"

    ```python
    from geomcore.surfaces import Cone
    import math

    cone = Cone(Point3D.origin(), Vector3D.z(), math.radians(28), 4.0)
    ```

=== "Rust"

    ```rust
    use geomcore::surfaces::Cone;

    let cone = Cone::new(Point3D::ORIGIN, Vector3D::Z, 0.4887, 4.0).unwrap();
    ```

### Sphere

Both parameters are angles: longitude $u$, latitude $v$ from the
equator. Both ends of $v$ are degenerate — at the poles the whole
circle of longitudes collapses onto the axis.

<figure class="geom-fig">
<img src="../../assets/figures/elementary-geometry/15-a-sphere-with-a-point-resolved.svg" alt="A sphere with a point resolved into its coordinates">
<figcaption>A sphere. The red dots are the poles, where a whole circle of u is one point.</figcaption>
</figure>

$$
S(u, v) = O + \rho\,(\cos u\,\vec{X} + \sin u\,\vec{Y}) + R\sin v\,\vec{Z}, \quad \rho = R\cos v, \quad (u, v) \in [0, 2\pi] \times [-\tfrac{\pi}{2}, \tfrac{\pi}{2}]
$$

The way back: $u = \mathrm{atan2}(y, x),\; v = \mathrm{atan2}(z, \rho)$.

=== "Python"

    ```python
    from geomcore.surfaces import Sphere

    sphere = Sphere(Point3D.origin(), 2.0)
    assert abs(sphere.volume() - 4.0 / 3.0 * 3.141592653589793 * 8.0) < 1e-9
    ```

=== "Rust"

    ```rust
    use geomcore::surfaces::Sphere;

    let sphere = Sphere::new(Point3D::ORIGIN, 2.0).unwrap();
    ```

### Torus

The cone's shape with $v$ turning the radius instead of growing it:
$\rho = R + r\cos v$. Both parameters close, so the domain — both
pairs of edges glued — is itself a torus, the most faithful
rectangle of the five.

<figure class="geom-fig">
<img src="../../assets/figures/elementary-geometry/16-a-torus-with-a-point-resolved.svg" alt="A torus with a point resolved into its coordinates">
<figcaption>A torus. v rounds the tube, changing the radius that u sweeps about the axis.</figcaption>
</figure>

$$
S(u, v) = O + \rho\,(\cos u\,\vec{X} + \sin u\,\vec{Y}) + r\sin v\,\vec{Z}, \quad \rho = R + r\cos v, \quad (u, v) \in [0, 2\pi]^2
$$

The way back: $u = \mathrm{atan2}(y, x),\; v = \mathrm{atan2}(z, \rho - R)$.
If $r > R$, $\rho$ has two zeros and the spindle self-intersects —
the same five numbers describe a ring or a spindle, and only $R > r$
says which.

=== "Python"

    ```python
    from geomcore.surfaces import Torus

    torus = Torus(Point3D.origin(), Vector3D.z(), 5.0, 1.5)
    ```

=== "Rust"

    ```rust
    use geomcore::surfaces::Torus;

    let torus = Torus::new(Point3D::ORIGIN, Vector3D::Z, 5.0, 1.5).unwrap();
    ```

## What the parameters measure

The derivative magnitude $\lVert \partial S/\partial u \rVert$ is the
unit conversion of a parameter: where it is 1 the parameter is arc
length; where it is a radius the parameter is an angle.

| Surface | $\lVert \partial S/\partial u \rVert$ | $u$ is | $\lVert \partial S/\partial v \rVert$ | $v$ is |
|---|---|---|---|---|
| Plane | 1 | a length | 1 | a length |
| Cylinder | $R$ | an angle | 1 | a length |
| Cone | $\rho = R + v\sin\alpha$ | an angle | 1 | a length along the ruling |
| Sphere | $R\cos v$ | an angle | $R$ | an angle |
| Torus | $\rho = R + r\cos v$ | an angle | $r$ | an angle |

Where the $u$-speed depends on $v$, a uniform $(u, v)$ grid is not
uniform on the surface — triangles crowd at the poles. Every mesher
corrects for exactly this table.

## Seams and degenerate edges

Three things go wrong, and all the special cases in surface code live
at these three places:

- **Seam** — two parameters, one point. On a cylinder, $u = 0$ and $u =
  2\pi$ evaluate $9.8\times10^{-16}$ apart. The surface is fine; the
  inverse must *choose*, and the geometry does not determine the
  choice. `geomcore` reports seam parameters inside `bounds()` and
  normalises hits there to 0.
- **Degenerate edge** — a whole edge that is one point: the cone apex,
  the sphere poles. $\lVert \partial S/\partial u \rVert$ measures
  $1.5\times10^{-15}$ at a pole, the normal cross product is the zero
  vector, and there is no normal to ask for.
- **Self-intersection** — the spindle torus above.

<figure class="geom-fig">
<img src="../../assets/figures/elementary-geometry/17-the-parameter-rectangles-of-the-plane.svg" alt="The five parameter rectangles with seams and degenerate edges">
<figcaption>The five domains. Dashed border: unbounded. Shaded edge: seam, two edges one curve. Red edge: a whole edge that is a single point.</figcaption>
</figure>

## Why ten special cases

Everything above can be written as NURBS — conics exactly, as rational
quadratics — so a kernel could keep one representation. Keeping the ten
buys four things: exactness that survives editing (a radius is stored,
not derived), closed-form inversion (`atan2` and a dot product instead
of Newton), closed-form intersections (plane∩cylinder is a conic, not a
march), and recognisability (STEP names these entities; a hole stays a
cylinder through a round trip). The cost is the pairing table in the
intersector — see [Intersection](../guides/intersection.md).