@code3d/core 0.0.1-alpha.0 → 0.0.1-alpha.11

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 (154) hide show
  1. package/README.md +43 -807
  2. package/THIRD_PARTY.md +40 -0
  3. package/bld/THIRD_PARTY_NOTICES.txt +182 -0
  4. package/bld/chunks/chunk-7FIFPADJ.js +1 -0
  5. package/bld/chunks/chunk-7FIFPADJ.js.map +7 -0
  6. package/bld/chunks/chunk-CL3E2DE4.js +19 -0
  7. package/bld/chunks/chunk-CL3E2DE4.js.map +7 -0
  8. package/bld/chunks/chunk-HZRQUHM2.js +30 -0
  9. package/bld/chunks/chunk-HZRQUHM2.js.map +7 -0
  10. package/bld/chunks/chunk-JBVIAMJ6.js +26474 -0
  11. package/bld/chunks/chunk-JBVIAMJ6.js.map +7 -0
  12. package/bld/chunks/chunk-QK6ZET47.js +31 -0
  13. package/bld/chunks/chunk-QK6ZET47.js.map +7 -0
  14. package/bld/library/bound-solver.d.ts +29 -5
  15. package/bld/library/bound-solver.d.ts.map +1 -1
  16. package/bld/library/cached.d.ts +35 -0
  17. package/bld/library/cached.d.ts.map +1 -0
  18. package/bld/library/font.d.ts +39 -0
  19. package/bld/library/font.d.ts.map +1 -0
  20. package/bld/library/google-font.d.ts +14 -0
  21. package/bld/library/google-font.d.ts.map +1 -0
  22. package/bld/library/index.d.ts +10 -2
  23. package/bld/library/index.d.ts.map +1 -1
  24. package/bld/library/index.js +94 -3
  25. package/bld/library/index.js.map +7 -1
  26. package/bld/library/inspect.d.ts +100 -0
  27. package/bld/library/inspect.d.ts.map +1 -0
  28. package/bld/library/kernel-artifact-codec.d.ts.map +1 -1
  29. package/bld/library/kernel-cache.d.ts +28 -7
  30. package/bld/library/kernel-cache.d.ts.map +1 -1
  31. package/bld/library/loft.d.ts.map +1 -1
  32. package/bld/library/material.d.ts +1 -1
  33. package/bld/library/material.d.ts.map +1 -1
  34. package/bld/library/open-cascade.d.ts +4 -0
  35. package/bld/library/open-cascade.d.ts.map +1 -0
  36. package/bld/library/relation-solver.d.ts +19 -8
  37. package/bld/library/relation-solver.d.ts.map +1 -1
  38. package/bld/library/replicad.d.ts +3 -2
  39. package/bld/library/replicad.d.ts.map +1 -1
  40. package/bld/library/replicad.js +10 -24
  41. package/bld/library/replicad.js.map +7 -1
  42. package/bld/library/retained-memory.d.ts.map +1 -1
  43. package/bld/library/runtime.d.ts +518 -155
  44. package/bld/library/runtime.d.ts.map +1 -1
  45. package/bld/library/sketch-solver.d.ts +1 -1
  46. package/bld/library/sketch-solver.d.ts.map +1 -1
  47. package/bld/library/sketch.d.ts +21 -1
  48. package/bld/library/sketch.d.ts.map +1 -1
  49. package/bld/library/text.d.ts +53 -0
  50. package/bld/library/text.d.ts.map +1 -0
  51. package/bld/library/three.js +3 -3
  52. package/bld/library/three.js.map +7 -1
  53. package/bld/library/topology.d.ts +6 -2
  54. package/bld/library/topology.d.ts.map +1 -1
  55. package/bld/node/index.d.ts.map +1 -1
  56. package/bld/node/index.js +96 -14
  57. package/bld/node/index.js.map +7 -1
  58. package/bld/node/replicad.js +13 -3
  59. package/bld/node/replicad.js.map +7 -1
  60. package/bld/tooling/index.d.ts +18 -15
  61. package/bld/tooling/index.d.ts.map +1 -1
  62. package/bld/tooling/index.js +165 -19
  63. package/bld/tooling/index.js.map +7 -1
  64. package/docs/api.md +764 -0
  65. package/docs/custom-primitives.mdx +58 -0
  66. package/docs/local-coordinates.md +187 -0
  67. package/docs/origins-and-rotation.mdx +105 -0
  68. package/docs/relations.mdx +450 -0
  69. package/docs/runtime.md +297 -0
  70. package/docs/shells.mdx +70 -0
  71. package/docs/sketches.md +83 -0
  72. package/docs/text.md +23 -0
  73. package/docs/topology.md +152 -0
  74. package/docs/values.md +119 -0
  75. package/package.json +20 -7
  76. package/src/library/bound-solver.ts +103 -73
  77. package/src/library/cached.ts +153 -0
  78. package/src/library/font.ts +186 -0
  79. package/src/library/google-font.ts +97 -0
  80. package/src/library/index.ts +53 -5
  81. package/src/library/inspect.ts +178 -0
  82. package/src/library/kernel-artifact-codec.ts +129 -20
  83. package/src/library/kernel-cache.ts +175 -39
  84. package/src/library/loft.ts +9 -1
  85. package/src/library/material.ts +3 -4
  86. package/src/library/open-cascade.ts +13 -0
  87. package/src/library/relation-solver.ts +230 -113
  88. package/src/library/replicad.ts +5 -5
  89. package/src/library/retained-memory.ts +6 -1
  90. package/src/library/runtime.ts +3769 -1178
  91. package/src/library/shell.ts +3 -3
  92. package/src/library/sketch-solver.ts +3 -1
  93. package/src/library/sketch.ts +112 -13
  94. package/src/library/text.ts +477 -0
  95. package/src/library/topology.ts +61 -17
  96. package/src/library/tsconfig.json +2 -1
  97. package/src/node/index.ts +14 -3
  98. package/src/node/tsconfig.json +2 -1
  99. package/src/tooling/index.ts +64 -51
  100. package/src/tooling/tsconfig.json +2 -1
  101. package/bld/library/alignment-geometry.js +0 -322
  102. package/bld/library/alignment-geometry.js.map +0 -1
  103. package/bld/library/bound-solver.js +0 -252
  104. package/bld/library/bound-solver.js.map +0 -1
  105. package/bld/library/extrude.js +0 -28
  106. package/bld/library/extrude.js.map +0 -1
  107. package/bld/library/kernel-artifact-codec.js +0 -148
  108. package/bld/library/kernel-artifact-codec.js.map +0 -1
  109. package/bld/library/kernel-cache.js +0 -245
  110. package/bld/library/kernel-cache.js.map +0 -1
  111. package/bld/library/kernel-shapes.js +0 -108
  112. package/bld/library/kernel-shapes.js.map +0 -1
  113. package/bld/library/loft.js +0 -161
  114. package/bld/library/loft.js.map +0 -1
  115. package/bld/library/material.js +0 -176
  116. package/bld/library/material.js.map +0 -1
  117. package/bld/library/model-color.js +0 -36
  118. package/bld/library/model-color.js.map +0 -1
  119. package/bld/library/open-cascade-error.js +0 -22
  120. package/bld/library/open-cascade-error.js.map +0 -1
  121. package/bld/library/relation-solver.js +0 -258
  122. package/bld/library/relation-solver.js.map +0 -1
  123. package/bld/library/retained-memory.js +0 -27
  124. package/bld/library/retained-memory.js.map +0 -1
  125. package/bld/library/runtime.js +0 -2954
  126. package/bld/library/runtime.js.map +0 -1
  127. package/bld/library/shell.js +0 -221
  128. package/bld/library/shell.js.map +0 -1
  129. package/bld/library/sketch-curve-intersections.js +0 -117
  130. package/bld/library/sketch-curve-intersections.js.map +0 -1
  131. package/bld/library/sketch-curves.js +0 -89
  132. package/bld/library/sketch-curves.js.map +0 -1
  133. package/bld/library/sketch-drag-rules.js +0 -522
  134. package/bld/library/sketch-drag-rules.js.map +0 -1
  135. package/bld/library/sketch-face.js +0 -72
  136. package/bld/library/sketch-face.js.map +0 -1
  137. package/bld/library/sketch-incidence.js +0 -114
  138. package/bld/library/sketch-incidence.js.map +0 -1
  139. package/bld/library/sketch-precision.js +0 -55
  140. package/bld/library/sketch-precision.js.map +0 -1
  141. package/bld/library/sketch-regions.js +0 -193
  142. package/bld/library/sketch-regions.js.map +0 -1
  143. package/bld/library/sketch-solver.js +0 -578
  144. package/bld/library/sketch-solver.js.map +0 -1
  145. package/bld/library/sketch.js +0 -650
  146. package/bld/library/sketch.js.map +0 -1
  147. package/bld/library/spatial.js +0 -195
  148. package/bld/library/spatial.js.map +0 -1
  149. package/bld/library/topology-id.js +0 -67
  150. package/bld/library/topology-id.js.map +0 -1
  151. package/bld/library/topology-inspection.js +0 -267
  152. package/bld/library/topology-inspection.js.map +0 -1
  153. package/bld/library/topology.js +0 -455
  154. package/bld/library/topology.js.map +0 -1
package/docs/api.md ADDED
@@ -0,0 +1,764 @@
1
+ ---
2
+ title: Modeling API
3
+ description: A curated map of the public core modeling operations.
4
+ sidebar:
5
+ order: 1
6
+ ---
7
+
8
+ Import these functions from `@code3d/core`. The editor's TypeScript signatures
9
+ provide exact overloads and inferred model interfaces.
10
+
11
+ Types used by the authoring API are also exported, including generic constraints,
12
+ named-element result types, and capability interfaces. Use `import type` from
13
+ `@code3d/core` for types such as `ElementKind`, `ModelKind`, `ModelForKind`, `TopologyKind`,
14
+ `NamedElements`, `ExposedElements`, `Bound`, and `TopologyId`. Replicad builder types such as `Shape3D`
15
+ are available from `@code3d/core/replicad` alongside `definePrimitive`.
16
+
17
+ ## Solid primitives
18
+
19
+ | Function | Meaning |
20
+ | -------------------------------------------- | --------------------------------- |
21
+ | `box(x, y, z)` | Box dimensions along X, Y, and Z |
22
+ | `cylinder(radius, y)` | Cylinder with its axis along Y |
23
+ | `sphere(radius)` | Sphere of the given radius |
24
+ | `frustum(bottomRadius, topRadius, y)` | Truncated cone |
25
+ | `regularPrism(radius, y, sides, rotation?)` | Regular polygonal prism |
26
+ | `tube(outerRadius, innerRadius, y)` | Straight tube with a through bore |
27
+ | `coil(coilRadius, wireRadius, pitch, turns)` | Circular-wire coil along Y |
28
+
29
+ Tubes are centered on Y; the inner radius must be smaller than the outer
30
+ radius. For coils, `coilRadius` is measured to the wire centerline and
31
+ `pitch` is the advance per turn. Fractional turns are supported; the wire
32
+ must fit inside the coil radius and neighboring turns must remain separated.
33
+ Use [`@code3d/screws`](../../screws/docs/assembly.mdx) for standard fasteners and matching hole tools.
34
+
35
+ To build a solid beyond these primitives, import `definePrimitive` and
36
+ `replicad` from `@code3d/core/replicad`. See
37
+ [custom primitives](custom-primitives.mdx) for a complete example.
38
+
39
+ ## Profiles and curves
40
+
41
+ Planar profiles lie in the local XZ plane with a +Y normal.
42
+
43
+ | Function | Meaning |
44
+ | ------------------------------------------ | -------------------------------------------- |
45
+ | `circle(radius)` | Circular face |
46
+ | `ellipse(xRadius, zRadius)` | Elliptical face |
47
+ | `rectangle(x, z)` | Rectangular face |
48
+ | `regularPolygon(radius, sides, rotation?)` | Regular polygonal face |
49
+ | `point()` or `point([x, y, z])` | Vertex model |
50
+ | `line([x, y, z])` or `line(start, end)` | Straight edge |
51
+ | `arc(start, middle, end)` | Arc through three points |
52
+ | `bezier(points)` | Bézier curve |
53
+ | `spline(points)` | Interpolating spline |
54
+ | `loft(sections, options?)` | Solid through sections; optional curve spine |
55
+ | `extrude(faceOrFaces, distance)` | Solid extruded along one face's local normal |
56
+
57
+ See [local coordinates and placement](local-coordinates.md) for
58
+ the coordinate frame of a model, reference, or composition.
59
+
60
+ Position coordinates use arrays; dimensions, offsets and angles use scalar
61
+ arguments. `point([x, y, z])` equals `point().originOffset(-x, -y, -z)`.
62
+ `line([x, y, z])` starts at zero; the two-array form uses both supplied local
63
+ endpoints. Curve tangents do not redefine the model's XYZ axes.
64
+
65
+ Profiles and curves are model values that can be inspected and related to
66
+ other models.
67
+
68
+ Face models also support `face.extrude(distance)`. Both forms accept a finite,
69
+ non-zero signed distance and preserve the starting face's coordinates. For an
70
+ unrotated profile, positive distance extends along +Y; negative distance extends
71
+ along −Y. Rotating the face rotates its extrusion direction; changing its origin
72
+ does not recenter the result. The returned solid supports Boolean operations,
73
+ fillets, chamfers, and shells. `extrude(faces, distance)` accepts a readonly array
74
+ of profiles and returns a readonly array of solids in the same order, preserving
75
+ each face's placement. An empty input returns `[]`; a single face still returns
76
+ a single solid. Both overloads retain required TypeScript distances and use the
77
+ same runtime default of 10 while editing.
78
+
79
+ ```ts
80
+ import {circle, extrude, rectangle} from '@code3d/core';
81
+
82
+ export const plate = rectangle(30, 20).extrude(3).fillet(0.5);
83
+ export const pin = extrude(circle(2), -10);
84
+ ```
85
+
86
+ ## Measurements
87
+
88
+ `distance(a, b, axis?)` returns a non-negative `number` from the models and
89
+ relations available at the call. It accepts vertex, edge, face and solid models,
90
+ non-empty groups, finite topology references, directional bounds, and point
91
+ anchors such as `center`, `start` and exposed mounting points.
92
+
93
+ | Axis | Result |
94
+ | ------------------------------- | ------------------------------------------------------------------------------------------- |
95
+ | Omitted | Shortest distance between the actual finite geometries |
96
+ | `'x'`, `'y'`, `'z'` | Gap between the geometries' projection intervals along a fixed solve-frame axis |
97
+ | `[x, y, z]` | The same projection gap along a finite, non-zero direction vector, normalized automatically |
98
+ | Straight edge or axis reference | Projection gap along that reference's solved direction |
99
+
100
+ Intersecting or touching geometries have zero shortest distance, including a
101
+ point inside a solid. A face measures its trimmed surface, including holes;
102
+ it does not represent the volume enclosed by its parent solid. Projected intervals
103
+ that overlap have zero gap, even if the geometries do not touch in space.
104
+ Groups measure their actual members; their projected interval spans the full
105
+ group, including spaces between disconnected members. Exchanging operands or
106
+ reversing an axis does not change the non-negative result. An axis's position
107
+ does not affect the measurement.
108
+
109
+ Queries solve the inputs' existing relationship closure without requiring
110
+ `group()`. Unrelated models use coincident origins and matching axes. String
111
+ and vector axes belong to that common solve frame, not the camera or implicitly
112
+ the first operand's local frame. A referenced axis carries its owning model's
113
+ solved orientation. For a specific assembled occurrence, expose its geometry
114
+ through the containing group and measure those references.
115
+
116
+ ```ts
117
+ import {box, distance, group, offset} from '@code3d/core';
118
+
119
+ const left = box(8, 30, 32);
120
+ const right = box(8, 30, 32).relate(self => [
121
+ self.on(left.right),
122
+ offset(60, 0, 0),
123
+ ]);
124
+ const length = distance(left.right, right.left, 'x');
125
+ const beam = box(length, 10, 24).relate(self => self.on(left.right));
126
+ export default group([left, right, beam]);
127
+ ```
128
+
129
+ The result is an ordinary number. Later relations and derived model values do
130
+ not update an earlier measurement, and no reverse dependency is solved. Arrange
131
+ measurement and construction in source order; re-running the source computes
132
+ fresh values. Relations returned by a `relate` callback are attached only after
133
+ that callback returns. The query does not see constraints still being built in it.
134
+
135
+ Infinite reference planes and axes cannot be distance operands: select a finite
136
+ face or edge instead. A straight infinite axis is supported as the third argument.
137
+ An empty group, zero direction or curved axis reports an error. Geometric query
138
+ results reuse the shared computation cache; point-to-point measurements use
139
+ ordinary arithmetic.
140
+
141
+ Try the [fitted beam example](/examples/distance/) and
142
+ [measurement workflow](relations.mdx#measure-before-building-a-part).
143
+
144
+ ## Independent placement transformations
145
+
146
+ `relate` callbacks return a `Constraint`, a `Transformation`, or a readonly array of both.
147
+ Independent constructors are `offset(x, y, z)`, `rotate(x, y, z)`,
148
+ `pivot([x, y, z]).rotate(x, y, z)`, `pivotVertex(id).rotate(x, y, z)`,
149
+ `pivotPoint(pointRef).rotate(x, y, z)`, `axisEdge(id).rotate(angle)`, and
150
+ `axisLine(lineRef).rotate(angle)`. Values can be built in helper functions and reused.
151
+
152
+ | Selector | Reference |
153
+ | ---------------------- | --------------------------------------- |
154
+ | `pivot([x, y, z])` | Coordinates in self |
155
+ | `pivotVertex(id)` | A vertex of self |
156
+ | `pivotPoint(pointRef)` | A local or external point reference |
157
+ | `axisEdge(id)` | A straight edge of self |
158
+ | `axisLine(lineRef)` | A local or external line/axis reference |
159
+
160
+ Point references change the rotation center while retaining self's XYZ axes.
161
+ External references use their owning model's solved placement in the composition.
162
+ Curved edges do not define a rotation axis.
163
+
164
+ Each independent transformation is one completed operation, with no further
165
+ chaining methods. Use an array to combine steps: `[offset(0, 8, 0), rotate(0, 25, 0)]`.
166
+ These five reference selectors return an unfinished selection with a
167
+ `rotate` method; its result is again a completed transformation.
168
+ Point selectors additionally accept one `pivotOffset(dx, dy, dz)`; axis selectors
169
+ accept one `axisOffset(dx, dy, dz)`. The resulting selector only exposes `rotate`.
170
+ Point offsets use self local axes. Axis offsets use the selected axis reference
171
+ frame, retaining its direction. Both retain the original point/axis reference.
172
+
173
+ Consecutive constraints form a joint solve segment. Transformations act after
174
+ its result; a subsequent constraint starts another segment and inherits the
175
+ previous pose in its free directions. Independent offset uses fixed composition
176
+ axes; rotation defaults to the current self origin and local XYZ axes. See
177
+ [the complete placement rules](relations.mdx#transform-a-joint-result).
178
+
179
+ ## Runtime defaults while editing
180
+
181
+ The dimension-based primitives and numeric methods below keep their required TypeScript parameters,
182
+ but their implementations supply defaults for omitted or `undefined` arguments.
183
+ For example, `box()` previews a 10 × 10 × 10 box, while the editor still reports
184
+ the missing arguments; `box(20)` previews 20 × 10 × 10. Finish the arguments to
185
+ make the source type-correct. These defaults also apply in ordinary JavaScript
186
+ execution and do not depend on the App.
187
+
188
+ | Function | Runtime defaults, in parameter order |
189
+ | ---------------- | ------------------------------------ |
190
+ | `box` | `10, 10, 10` |
191
+ | `cylinder` | `5, 10` |
192
+ | `sphere` | `5` |
193
+ | `frustum` | `5, 3, 10` |
194
+ | `regularPrism` | `5, 10, 6, 0` |
195
+ | `tube` | `5, 3, 10` |
196
+ | `coil` | `5, 1, 3, 3` |
197
+ | `circle` | `5` |
198
+ | `ellipse` | `5, 3` |
199
+ | `rectangle` | `10, 10` |
200
+ | `regularPolygon` | `5, 6, 0` |
201
+
202
+ | Method or utility parameter | Runtime defaults |
203
+ | --------------------------------------------------- | ---------------- |
204
+ | Model/group `rotate` and independent/pivot `rotate` | `0, 0, 0` |
205
+ | Model/group `originOffset` and relation `offset` | `0, 0, 0` |
206
+ | Relation `pivot` | `[0, 0, 0]` |
207
+ | Selector `pivotOffset` and `axisOffset` | `0, 0, 0` |
208
+ | `axisLine(axis).rotate` | `0` |
209
+ | Geometric model `scaled` | `1` |
210
+ | Face `extrude` and the `extrude` utility's distance | `10` |
211
+ | Solid `fillet`, `chamfer` and `shell` | `1` |
212
+
213
+ For example, `box(20, 30, 40).rotate()` previews the unchanged body, and
214
+ `.rotate(30)` previews a 30-degree X rotation. Their missing-angle diagnostics
215
+ remain until all three arguments are supplied. Relation `offset()` translates
216
+ self from the relation's solution in the target reference axes. Like explicit
217
+ `offset(0, 0, 0)`, omitting its arguments preserves that solution and adds no
218
+ tangential constraints. `pivot()` selects self's local origin. Geometry IDs, reference axes and input
219
+ models still need explicit values.
220
+
221
+ Explicit arguments remain subject to their normal validation: `box(0)`, for
222
+ example, still reports an error. The parameter panel shows omitted defaults as
223
+ placeholders and only writes arguments when you edit them. See
224
+ [parameter defaults](../../web/src/content/docs/docs/guides/model-tools.mdx#describe-an-omitted-arguments-default).
225
+
226
+ Spatial controls use the rendered operation's position and frame, so omitted
227
+ arguments do not hide its translation arrows or rotation rings. Committing a
228
+ drag fills all remaining omitted defaults in that call: dragging the X ring of
229
+ `rotate()` writes `rotate(angle, 0, 0)`, and dragging `pivot()` writes all three
230
+ coordinates. This also applies when editing an existing or upstream parameter.
231
+ The parameter change and default completion form one undo step. Merely selecting a
232
+ tool, cancelling a drag or returning to its starting value leaves the source
233
+ unchanged. Existing editable expressions retain their normal editing behavior;
234
+ opaque inputs such as `pivot(coords)` or `rotate(...angles)` are replaced with
235
+ the current evaluated coordinates or angles when you commit the drag. Undo
236
+ restores the original expression.
237
+
238
+ ## Editable sketch regions
239
+
240
+ Select a `sketch([...])` expression or variable to preview its points and curves
241
+ in 3D. Choose **Edit sketch** to open its 2D editor and **Finish sketch** to return
242
+ to the shared 3D scene.
243
+ Points, lines, circles and arcs use explicit layer-local entity IDs. `face()`
244
+ requires exactly one closed region, including holes; `faces()` returns all regions
245
+ as an ordinary readonly array. Use `map` for independent modeling operations:
246
+
247
+ ```ts
248
+ import {sketch} from '@code3d/core';
249
+
250
+ const profile = sketch([
251
+ ['point', 1, [0, 0]],
252
+ ['circle', 2, [1, 12]],
253
+ ['circle', 3, [1, 8]],
254
+ ]);
255
+ const sleeve = profile.face().extrude(20);
256
+ const parts = profile.faces().map(face => face.extrude(10));
257
+ ```
258
+
259
+ Geometry tuples store current data; the second argument's `constraints` array
260
+ specifies relations that must remain true. Constraints have no IDs and use
261
+ `['kind', target, value?]`. For local lines:
262
+
263
+ - `['horizontal', line]` and `['vertical', line]` set an axis direction;
264
+ `['length', line, distance]` sets a positive length.
265
+ - `['angle', line, degrees]` sets Orientation relative to +X.
266
+ - `['parallel', [line1, line2]]` and `['perpendicular', [line1, line2]]`
267
+ relate two lines without requiring their finite segments to intersect.
268
+ - `['angle', [line1, line2], degrees]` sets Angle between lines: the signed
269
+ rotation from the first line's authored start-to-end direction to the second,
270
+ positive counterclockwise and equivalent modulo 360.
271
+
272
+ In Select, an ordinary click or box selection replaces the selection, Ctrl
273
+ toggles elements, and Shift only adds them. Drag a box left-to-right for fully
274
+ enclosed geometry or right-to-left for intersecting geometry. A multi-selection
275
+ can remove any editable local constraint on its elements, while adding one
276
+ requires the entire selection to satisfy the tool's prerequisites. Parallel
277
+ accepts two or more local lines and creates pairwise relations; Perpendicular
278
+ and Angle between lines require exactly two. Rectangle tools still create
279
+ horizontal and vertical constraints by default.
280
+
281
+ Drag an arc endpoint to reshape it while preferring to keep its center in place.
282
+ Drag a circle or arc center to move it while preferring to keep its radius
283
+ unchanged. Hard constraints, expression-controlled values and read-only upstream
284
+ geometry take precedence; these preferences can keep the dragged point from
285
+ reaching the pointer. They apply only during the gesture and do not add persistent
286
+ fixed or radius constraints. To change an editable radius, drag the curve itself
287
+ or edit its source or dimension.
288
+ After the gesture-specific preferences, all other points prefer staying near
289
+ their gesture-start positions. This lowest-priority step only resolves remaining
290
+ freedom: it does not pull back a translated shape, weaken hard constraints or
291
+ add fixed-point constraints to the source.
292
+
293
+ Derived sketches include their read-only upstream boundaries. Separate contours
294
+ produce separate faces; nested contours alternate material, holes and islands.
295
+ Open, crossing, touching, overlapping and branched boundaries must be trimmed into
296
+ valid closed contours before creating faces. The editor leaves unfinished sketches
297
+ editable and previews valid regions without changing entity IDs.
298
+
299
+ Sketch `[x, y]` maps to model `[x, 0, -y]`, without recentering. `.extrude(distance)`
300
+ and `extrude(face, distance)` are equivalent single-face operations. Distance must
301
+ be finite and nonzero; positive follows the plane normal, negative reverses it.
302
+ Rotating the face rotates its extrusion direction too.
303
+
304
+ `loft` takes one face per section and preserves a single corresponding hole, with
305
+ or without a spine. Different hole counts or multiple unpaired holes report an
306
+ error rather than silently filling holes. Persistent region IDs and general
307
+ multi-hole correspondence are not available yet. Try `examples/sketches/regions.ts`
308
+ in the App for a plate, multiple cutting tools and a hollow loft.
309
+
310
+ ### Sketch placement and model context
311
+
312
+ `sketch.relate(self => self.plane.align(target))` creates an immutable spatial
313
+ copy of the same local 2D definition. Empty and open sketches can relate before
314
+ `face()` is available. Targets include named model planes and planar
315
+ `model.surface(id)` references.
316
+
317
+ ```ts
318
+ const host = box(40, 20, 30).rotate(0, 0, 25);
319
+ const profile = sketch([
320
+ ['point', 1, [0, 0]],
321
+ ['circle', 2, [1, 4]],
322
+ ]);
323
+ const opening = profile.relate(s => s.plane.align(host.surface(4)));
324
+ const result = host.cut([opening.face().extrude(-20)]);
325
+ ```
326
+
327
+ Derived layers, faces and extrusion inherit these relations. Plane alignment does
328
+ not center on a trimmed face or rewrite sketch coordinates. The relation binds
329
+ the referenced immutable host value; later creating another transformed host does
330
+ not redirect it. Use `align()`, not finite-bound `on()`, for the sketch's unbounded
331
+ reference plane.
332
+
333
+ Select `opening` and choose **Edit sketch** to edit with read-only model outlines in the sketch's
334
+ local plane, or `profile` for the original local view. Both write the same geometry
335
+ array. The outlines do not become snapping targets or external geometry constraints.
336
+ See `examples/sketches/mounting-plate.ts` for a slotted plate. Create the relation
337
+ in code; selecting a face does not automatically generate a related sketch.
338
+
339
+ ## Composition and boolean operations
340
+
341
+ | Function | Result |
342
+ | ---------------------- | --------------------------------------------- |
343
+ | `group(models, name?)` | Composition that preserves its separate parts |
344
+ | `union(solids)` | Fused solid |
345
+ | `cut(stock, tools)` | Stock with the tool volumes removed |
346
+ | `intersect(solids)` | Shared solid volume |
347
+
348
+ `group()` accepts a `readonly Model[]`, including ordinary groups, empty groups,
349
+ and any depth of nested groups mixed with solids, faces, curves, or points. Each
350
+ nested group keeps its hierarchy. No type assertion or `expose()` call is needed
351
+ to compose it; use `expose()` when callers need named member references. Generic
352
+ helpers can use `ModelCapabilities<Elements, Kind>` and `ModelForKind<Elements, Kind>`
353
+ to preserve the concrete model kind and exposed members through chained calls.
354
+
355
+ Relations are resolved at composition and geometry evaluation boundaries.
356
+ `stock.cut(tools)` is equivalent to `cut(stock, tools)`. Arrays in booleans and
357
+ loft describe the inputs of one operation; they do not automatically map it.
358
+ `intersect()` requires a common solid volume across all inputs. Disjoint inputs
359
+ or inputs that only touch produce a diagnostic rather than an empty solid.
360
+
361
+ ## Model operations
362
+
363
+ Available operations depend on the kind of geometry. TypeScript completion
364
+ shows which operations are supported by the value you hold.
365
+
366
+ - `.fillet(radius, edgeIds?)`: round selected edges, or all edges.
367
+ - `.chamfer(distance, edgeIds?)`: bevel selected edges, or all edges.
368
+ - `.shell(thickness, removedSurfaceIds?)`: hollow one connected solid. Positive
369
+ thickness offsets inward; negative thickness offsets outward. Selected surfaces
370
+ become openings; omission or `[]` creates an enclosed cavity. See
371
+ [making hollow parts](shells.mdx).
372
+ - `.scaled(factor)`: uniformly scale a geometric model about local coordinate zero.
373
+ - `.material(value)`: replace the complete material with a native Three.js material
374
+ or a CSS color shorthand; a group overrides every descendant's material.
375
+ - `.relate(self => constraint)` or `.relate(self => [first, second])`: attach
376
+ one or more relations for placement in a composition.
377
+ - `.expose({name: element})`: publish a typed named-element interface.
378
+
379
+ ## Materials
380
+
381
+ Use [`@code3d/materials`](../../materials/docs/presets.md) for common plastic, metal, glass,
382
+ ceramic and paint presets, such as `.material(aluminum({finish: 'polished'}))`.
383
+ Each preset returns a native Three.js material and follows the same rules below.
384
+
385
+ ```ts
386
+ import {box} from '@code3d/core';
387
+ import {MeshPhysicalMaterial} from '@code3d/core/three';
388
+
389
+ const part = box(20, 10, 12).material(
390
+ new MeshPhysicalMaterial({
391
+ color: '#eb633e',
392
+ roughness: 0.25,
393
+ clearcoat: 1,
394
+ }),
395
+ );
396
+ ```
397
+
398
+ `@code3d/core/three` directly re-exports Core's native Three.js classes and types.
399
+ Use this entry in the model and its reusable packages to share the same instance.
400
+ Named imports and `import * as THREE from "@code3d/core/three"` both work in the
401
+ App and Node. Each `.material(value)` call captures a
402
+ complete material and its loaded texture pixels. It returns a new model;
403
+ subsequent changes to the original Three.js instance do not change that model.
404
+ Calling it again replaces everything, without merging fields. An outer group
405
+ replaces the material throughout its subtree; original parts used elsewhere
406
+ remain unchanged.
407
+
408
+ Use mesh materials for solids and surfaces, line materials for curves, and
409
+ `PointsMaterial` for vertices. Modeling emphasis uses preview copies; Render
410
+ mode and PNG images use the authored material. Loaded image, canvas, ImageBitmap,
411
+ data and cube textures are supported. Load images with `ImageBitmapLoader` in
412
+ the worker before assignment. Native face UVs are normalized to 0–1 per face;
413
+ texture `repeat`, `offset` and `rotation` control mapping. See
414
+ `/examples/materials.ts` in the App.
415
+
416
+ The transferable value follows Three.js's `toJSON()` / `MaterialLoader`
417
+ representation. Custom classes, callbacks such as `onBeforeCompile`, live
418
+ video/render-target textures, compressed/layered textures and manual mipmaps
419
+ are rejected. Material-local clipping, shadow-side and precision overrides are
420
+ not serialized by Three.js and are also rejected. Shader uniforms must be supported by Three.js JSON.
421
+
422
+ A CSS string replaces the whole material with the geometry's default material.
423
+ It accepts names, `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`, `rgb(...)` and
424
+ `rgba(...)`. `.material('#f008')` equals `.material('#ff000088')`;
425
+ `.material('rgba(255, 0, 0, 0.5)')` and `.material('rgb(100% 0% 0% / 50%)')`
426
+ produce half-opaque red. Native materials use Three.js's `opacity` and
427
+ `transparent` settings. STEP and 3MF preserve base color and opacity, while
428
+ shaders and textures are rendered in PNG; STL contains geometry only.
429
+
430
+ ## Scaling
431
+
432
+ Solids, faces, curves, and points support `.scaled(factor)`. The factor must be
433
+ positive and finite. For example, `box(20, 8, 12).scaled(0.5)` returns a new box
434
+ with dimensions 10, 4, and 6, leaving the original model unchanged.
435
+
436
+ Scaling uses local coordinate zero even after an origin edit. Geometry, named
437
+ anchors and the `center` anchor scale together; the model origin stays zero;
438
+ topology IDs are preserved. Groups do not provide `.scaled()`; scale their
439
+ geometric parts before composing them. To change only an exported file's unit
440
+ conversion, use the [export scale](../../web/src/content/docs/docs/guides/exporting.md#scale-and-orientation).
441
+
442
+ ## Origins and rotation
443
+
444
+ Every model, including groups, has a `frame: FrameAnchor` coordinate reference.
445
+ `frame.origin: PointAnchor` references its zero point; `model.origin` returns
446
+ that same reference. These references have no geometry and cannot be added as
447
+ models to a group. A point model's geometry may be away from its own origin.
448
+
449
+ `self.origin.align(other.origin)` constrains only position.
450
+ `self.frame.align(other.frame)` constrains position and all three axis directions.
451
+ Use `.frame` explicitly: aligning curves or surfaces still refers to their
452
+ underlying geometry. Frames support `expose`, including `.origin` on the exposed
453
+ frame, and retain their occurrence and transform through composition.
454
+
455
+ All models provide `originPoint()`, `originOffset()` and `rotate()`. Solids, faces,
456
+ curves and points additionally provide vertex/center selection:
457
+
458
+ | Method | Behavior |
459
+ | --------------------------- | ------------------------------------------------------------- |
460
+ | `.originPoint(pointRef)` | Set the origin to a point reference, including a group member |
461
+ | `.originVertex(id)` | Set the origin to an input-model vertex |
462
+ | `.originCenter()` | Set the origin to the model's center anchor |
463
+ | `.originOffset(dx, dy, dz)` | Add a local-coordinate offset to the current origin |
464
+ | `.rotate(x, y, z)` | Rotate about the origin, in degrees, fixed X then Y then Z |
465
+
466
+ The origin is always zero in model coordinates. `originOffset(dx, dy, dz)`
467
+ re-expresses every local point as `p - [dx, dy, dz]`; offsets accumulate and can
468
+ cancel. `originVertex` and `originCenter` make the selected point local zero.
469
+ Geometry, named anchors and topology positions use the resulting coordinates;
470
+ directions and topology IDs are preserved. Old model values remain unchanged.
471
+ Rotation and scaling act about current local zero.
472
+
473
+ Every geometric model exposes `center`: its initial local bounding-box center,
474
+ carried along by subsequent transforms. Rotation does not recalculate it from
475
+ the rotated shape's axis-aligned bounds. Origin edits change its coordinates;
476
+ `.originCenter().originOffset(1, 0, 0)` leaves it at `[-1, 0, 0]`.
477
+ A group inherits the first member's solved local coordinate frame, including
478
+ its origin and axes, while preserving relative member placement. Nested groups
479
+ keep their own frames; an empty group uses the default origin and axes. Member
480
+ order can change the group's frame. Group origin
481
+ edits move the entire assembly's local coordinates together; they preserve its
482
+ internal relations. `rotate(x, y, z)` turns the solved assembly about its current
483
+ origin, including nested instances. `originPoint(part.center)` resolves the member's actual
484
+ placement; repeated sources need a specific instance reference. Groups do not
485
+ have aggregate vertex IDs, a geometric center or scaling.
486
+ See [group coordinates](local-coordinates.md#group-origins).
487
+
488
+ For a runnable example and
489
+ the vertex picker, origin arrows, and rotation rings, see
490
+ [choosing an origin and rotating a part](origins-and-rotation.mdx).
491
+
492
+ ## Anchors and relations
493
+
494
+ Solid primitives expose `center` and `axis`; every model provides directional
495
+ bounds: `up` (+Y), `down` (−Y), `right` (+X), `left` (−X), `front` (+Z),
496
+ and `back` (−Z), in that model's local frame.
497
+
498
+ `geometry.on(target.up)` only translates. The source may be a model, point,
499
+ edge, or surface; its own finite extent is measured along the target direction.
500
+ Tangential position and orientation are preserved. Targets must be directional
501
+ bounds. Infinite reference lines and planes cannot supply a finite source extent.
502
+
503
+ Return an array from `relate()` to combine positional conditions. Inconsistent
504
+ positions report a conflict. `offset(x, y, z)` translates self from the original
505
+ solution in the target reference frame. Explicit zero changes nothing; use
506
+ point or axis alignment for centering. `bound.flip()` reverses contact
507
+ facing without changing geometry or reference axes.
508
+
509
+ `relate` returns a new model, represented by its callback parameter `self`.
510
+ Every returned constraint must involve that value, as in `self.on(base.up)`
511
+ or `base.on(self.up)`. External variables keep their original identity, including
512
+ the receiver of `relate`: `part.relate(self => self.on(part.right))` places a
513
+ new part against the original. Select the new part's topology and rotation
514
+ references through `self`; references selected from `part` belong to the original.
515
+
516
+ `pointOrCurveOrSurface.align(target)` solves geometric position and orientation.
517
+ Same-dimensional references coincide; a lower-dimensional reference lies on the
518
+ whole supporting geometry of the other. Edges use their underlying curves and
519
+ faces their underlying surfaces, ignoring trims and parameter origins. Supported
520
+ types are points, straight lines, circles, ellipses, planes, cylinders, and
521
+ spheres. Select a solid's center, axis, vertex, edge, or surface first.
522
+
523
+ Curve–curve alignment is directed; `lineReference.reverse()` selects the opposite
524
+ direction. Surface–surface alignment matches normal sense; `faceReference.flip()`
525
+ selects the opposite facing. Neither changes the reference axes or geometry.
526
+ Point membership ignores direction. Constraints expose no transformation methods.
527
+ Place independent transformations after the constraints in the `relate` array:
528
+
529
+ - `offset(x, y, z)`: move self along fixed composition axes.
530
+ - `rotate(x, y, z)`: rotate around self's origin and local XYZ axes.
531
+ - `pivot([x, y, z]).rotate(x, y, z)`: a pivot in self's local frame.
532
+ - `pivotVertex(id).rotate(x, y, z)`: a vertex belonging to self.
533
+ - `pivotPoint(pointRef).rotate(x, y, z)`: a local or external point.
534
+ - `axisEdge(id).rotate(angle)`: a straight edge belonging to self.
535
+ - `axisLine(lineRef).rotate(angle)`: a positioned local or external axis.
536
+
537
+ Angles are degrees; XYZ rotations apply X, then Y, then Z. Pivot/axis selections
538
+ must be completed with `rotate`. Consecutive constraints solve jointly, followed
539
+ by transformations in array order. A later constraint starts a new segment from
540
+ the preceding pose. Groups move their assembled children as rigid bodies.
541
+ Standalone geometry is unchanged.
542
+
543
+ ## Topology
544
+
545
+ - `.vertex(id)`, `.edge(id)`, `.surface(id)`: one point, line, or face anchor.
546
+ - `.vertices(ids?)`, `.edges(ids?)`, `.surfaces(ids?)`: arrays of anchors.
547
+
548
+ These topology references expose readonly `kind` (`vertex`, `edge`, or
549
+ `surface`) and `id` properties. Use `model.edges().map(edge => edge.id)` to
550
+ collect edge IDs for an operation on that model. Plain named anchors such as
551
+ `model.up` do not have these topology properties.
552
+
553
+ IDs are model-local. See [topology selection](topology.md) for
554
+ selection behavior and derived-model identity.
555
+
556
+ `TopologyId` (also used by `VertexId`, `EdgeId`, and `SurfaceId`) is a
557
+ positive integer or a flat numeric source path. A loft cap can be selected with
558
+ `body.surface([1, 1])`; a mixed selection uses an outer list, such as
559
+ `body.surfaces([1, [1, 1], [2, 1]])`. Loft, extrusion and Boolean operations prefix
560
+ one-to-one inherited IDs with the one-based input index. Local edits (`fillet`,
561
+ `chamfer`, `shell`) preserve inherited IDs, including existing paths; their new
562
+ or ambiguous elements receive fresh numbers without reusing retired IDs.
563
+ Transforms preserve complete IDs.
564
+
565
+ A surface can query its edges and vertices; an edge can query its vertices.
566
+ These queries retain the source model's IDs and validate membership.
567
+ `.center` is a transformed local bounding-box center; edges also provide
568
+ `.start`, `.midpoint`, and `.end` at curve parameters 0, 0.5, and 1.
569
+ Calculated points are anchors, not topology vertices.
570
+
571
+ Model dimensions use a consistent coordinate scale. When
572
+ [exporting](../../web/src/content/docs/docs/guides/exporting.md#scale-and-orientation), choose how many
573
+ millimeters each model unit represents. This scales the output without changing
574
+ the source model.
575
+
576
+ ## Cached computations
577
+
578
+ ```ts
579
+ import {cache} from '@code3d/core';
580
+
581
+ function buildProfile(radius: number, sides: number) {
582
+ return Array.from({length: sides}, (_, index) => {
583
+ const angle = (index * 2 * Math.PI) / sides;
584
+ return [radius * Math.cos(angle), radius * Math.sin(angle)];
585
+ });
586
+ }
587
+ const profile = cache(buildProfile);
588
+ const points = cache(buildProfile, [10, 6]);
589
+ ```
590
+
591
+ `cache(fn)` returns a memoized function; `cache(fn, args)` immediately returns
592
+ its result for the supplied argument tuple. Both forms preserve synchronous
593
+ parameter/result types and use the same definition and argument cache keys.
594
+ In the example, `profile(10, 6)` reuses the same cached result as `points`. An empty tuple `[]` immediately invokes
595
+ a computation with no arguments. The argument tuple is not part of the
596
+ compiler's function fingerprint: changing inputs selects another cache entry.
597
+
598
+ The API caches
599
+ ordinary data; use `definePrimitive()` for Replicad geometry so Core also owns
600
+ native resources and creates fresh model metadata. Treat cached results as
601
+ immutable. A memory hit returns the retained computed or decoded value directly,
602
+ without decoding, copying or freezing it.
603
+
604
+ The default persistent codec supports plain objects, arrays, scalar values
605
+ (including `undefined`, nonfinite numbers and bigint), Date, Map, Set, ArrayBuffer,
606
+ standard TypedArrays and DataView. Shared references, cycles, sparse arrays and
607
+ shared buffer views survive restoration. Arguments use the same data encoding;
608
+ changing dynamic state must be supplied as arguments. Functions, native handles
609
+ and application class instances are not ordinary data arguments.
610
+
611
+ For custom result types, supply both functions as
612
+ `cache(fn, undefined, {encoder: value => bytes, decoder: bytes => value})`.
613
+ For immediate evaluation, use `cache(fn, args, options)` with the same codec options.
614
+ The encoder runs when saving to disk; the decoder runs once when restoring an
615
+ entry into memory. A subsequent memory hit never calls either codec. Async
616
+ computations are excluded: incomplete work is not admitted to the cache.
617
+
618
+ New results are written to disk only when their computation reaches the configured
619
+ threshold, 1 ms by default. In the App, change **Disk cache threshold (ms)** under
620
+ **Settings → Cache**; fractional values are supported and 0 removes the time
621
+ threshold. Changes apply to new computations from the next model execution;
622
+ existing entries retain their disk eligibility.
623
+ Faster results still use the memory cache, and later memory hits do not promote
624
+ them to disk. The computation timer excludes the surrounding cache lookup,
625
+ argument hashing and persistence encoding. Batched snapshot queries use their
626
+ local or Worker computation time, excluding input restoration and transport.
627
+ Existing disk records remain readable; restoring a record preserves its disk
628
+ eligibility.
629
+
630
+ The model engine fingerprints static function definitions, their referenced
631
+ local declarations, imported implementation graphs and codec definitions. Aliases
632
+ and re-exports of Core cache factories are supported. Editing an unrelated local
633
+ binding, moving a definition or adding/removing `export` preserves its identity;
634
+ changing a referenced helper or dependency invalidates it. Functions supplied as parameters, dynamic factory results and closures capturing
635
+ enclosing function/loop bindings use memory-only object identity.
636
+ Outside the model engine, ordinary Node calls also use function object identity
637
+ and share the process-wide memory LRU. Authors do not provide cache IDs or versions.
638
+
639
+ Public cached computations, primitives, Core geometry, font parsing, glyph contours
640
+ and snapshot queries share one cache. The memory budget remains 2 GiB; browser
641
+ persistence shares the OPFS disk budget configured in App settings (2 GiB by default).
642
+ Cancellation and exceptions retain completed entries and editing history.
643
+
644
+ ## Text
645
+
646
+ ```ts
647
+ import {font, text, extrude, group} from '@code3d/core';
648
+
649
+ const sans = font(new URL('./fonts/DejaVuSans.ttf', import.meta.url));
650
+ const profiles = text('B8i', sans, 10);
651
+ export const lettering = group(extrude(profiles, 1));
652
+ ```
653
+
654
+ `font()` synchronously returns an immutable font resource. The App prepares literal
655
+ `new URL('./font.ttf', import.meta.url)` assets before evaluating model code, including
656
+ assets in imported modules. Changing the font file invalidates the resource; equal
657
+ file contents reuse parsed fonts and geometry. Node reads file URLs directly.
658
+ `font()` also accepts `ArrayBuffer` or `Uint8Array` bytes, captured at the call.
659
+ The engine also prepares static HTTP(S) font URLs before model execution:
660
+
661
+ ```ts
662
+ const remote = font(new URL('https://example.com/fonts/SomeFont.ttf'));
663
+ const label = text('AV', remote, 10, {letterSpacing: 0.5, kerning: true});
664
+ ```
665
+
666
+ Use a direct font-file URL whose server permits CORS access from the App. The URL
667
+ must be a literal in `new URL(...)`, including when declared in an imported module;
668
+ no `await` is needed in model code. The engine decodes remote WOFF2 files to SFNT
669
+ before synchronous font parsing.
670
+
671
+ Google Fonts can instead be selected by name:
672
+
673
+ ```ts
674
+ import {googleFont, text, extrude, group} from '@code3d/core';
675
+
676
+ const play = googleFont('Play');
677
+ const medium = googleFont('Roboto', {weight: 450, italic: true});
678
+ export default group(extrude(text('Hello', play, 10), 1));
679
+ ```
680
+
681
+ `googleFont(family, options?)` synchronously returns a `Font`. Both `weight` and
682
+ `italic` are optional. Omitted axes are omitted from the Google request, leaving
683
+ the defaults to Google; explicit weights apply to variable fonts as well as static
684
+ faces. The App prepares the CSS and all of its Unicode subsets before execution,
685
+ then selects the appropriate subset for each character. No stylesheet is installed.
686
+ The family and options must be literals or static `const` values, including imports,
687
+ aliases, object properties and spreads. Computed calls are reported at their source.
688
+ Large families such as Chinese fonts require downloading all returned subsets on
689
+ the first use; changing the text subsequently reuses those font resources.
690
+
691
+ Network resources use an engine-owned 64 MiB memory LRU and the shared OPFS disk
692
+ journal, then the network. CSS, compressed font bytes and content-addressed decoded
693
+ bytes are retained. The disk budget is the smaller of 1 GiB and 10% of the browser's
694
+ origin quota, including compaction space, shared with geometry. Fresh resources
695
+ need no request across edits or Worker/page restarts. Expired resources revalidate
696
+ through the browser HTTP cache; `no-store` resources are not retained. Concurrent
697
+ requests share one download. Failed or cancelled builds preserve completed resources;
698
+ partial downloads are discarded and can retry. Without OPFS, memory caching remains.
699
+ The active build's resource references are outside the historical memory limit.
700
+
701
+ Parsed fonts are memory-only entries in the existing 2 GiB kernel cache budget.
702
+ CSS interpretation, normalized glyph contours, B-Rep, bounds and meshes reuse the
703
+ existing memory/disk artifact cache. Font contents and requested variations identify
704
+ these artifacts; changing text position or spacing can reuse unchanged glyphs.
705
+ HTTP resource records remain reusable when the geometry runtime changes.
706
+
707
+ For computed URLs or Node execution outside the App engine, download TTF/OTF bytes
708
+ first (decode WOFF2 before passing its bytes):
709
+
710
+ ```ts
711
+ const response = await fetch(fontUrl);
712
+ if (!response.ok) throw new Error(`Font download failed: ${response.status}`);
713
+ const remote = font(await response.arrayBuffer());
714
+ ```
715
+
716
+ TTF and OTF fonts are supported, including variable fonts and Chinese characters
717
+ when present in the font. HarfBuzz supplies glyph outlines, advances, kerning and
718
+ ligatures. Quadratic/cubic curves are preserved, and overlapping contours within
719
+ a glyph use the non-zero fill rule. Font collections (TTC), color glyph rendering,
720
+ full bidirectional/multiscript paragraph layout and multiline text are outside this
721
+ API. Missing glyphs report an error. Empty text and spaces create no faces; spaces
722
+ still advance subsequent characters.
723
+
724
+ `text(content, font, size, options?)` requires the first three arguments and returns connected planar
725
+ regions as ordinary readonly `FaceModel[]`: `B` has one face with two holes; `i` has
726
+ two faces. Size is the font em in model units, not the cap height. Coordinates are
727
+ +X right, -Z up, normal +Y, with all faces retaining the same baseline origin.
728
+ Faces are never individually centered,
729
+ so `group(extrude(...))`, origin operations and boolean tools preserve the layout.
730
+ Use positive/negative extrusion and `union`/`cut` for raised or engraved lettering.
731
+ Text is currently code-defined geometry rather than an editable sketch entity.
732
+
733
+ `options.letterSpacing` defaults to `0` and adds a finite distance in model units
734
+ between laid-out glyphs, including spaces. Negative values tighten the text. The
735
+ distance stays constant when size changes, and disconnected parts of one glyph move
736
+ together. `options.kerning` defaults to `true`; set it to `false` to disable the
737
+ font's pair adjustments. Extra letter spacing is added after kerning. Supported
738
+ ligatures remain single glyphs for spacing purposes.
739
+
740
+ ## Geometry measurements
741
+
742
+ `model.bounds(relativeTo?)` returns readonly `minimum`, `maximum` and `size`
743
+ XYZ vectors for tight finite geometry bounds. By default it uses the model's
744
+ own local frame. An explicit reference includes solved placement and nested
745
+ member occurrences in that reference's frame. Empty groups have no finite
746
+ bounds; a source occurring more than once in the reference is ambiguous.
747
+
748
+ `model.position(relativeTo)` returns the model origin in the explicit reference's
749
+ local frame. A model's origin in its own frame is always `[0, 0, 0]`, including
750
+ point models whose geometry may be offset from that origin. Neither query
751
+ changes the model or its placement. These methods are available on every model
752
+ kind, including groups. As model members, `bounds` and `position` are reserved
753
+ names and cannot be used as exposed element names.
754
+
755
+ ```ts
756
+ import {box, group, offset} from '@code3d/core';
757
+
758
+ const base = box(20, 4, 20);
759
+ const part = box(8, 12, 4).relate(self => [self.on(base.up), offset(20, 0, 0)]);
760
+ const size = part.bounds().size; // [8, 12, 4]
761
+ const origin = part.position(base); // [20, 8, 0]
762
+ const minimum = part.bounds(base).minimum; // [16, 2, -2]
763
+ export default group([base, part]);
764
+ ```