@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/values.md ADDED
@@ -0,0 +1,119 @@
1
+ ---
2
+ title: Model values and measurements
3
+ description: Understand immutable models, geometry measurements and composition.
4
+ sidebar:
5
+ order: 2
6
+ ---
7
+
8
+ ## Model values and coordinates
9
+
10
+ Operations produce new model values. Building another result must not change an
11
+ already-observable model's geometry, material, topology, or relations.
12
+
13
+ In `part.relate(self => ...)`, the callback parameter represents the new value.
14
+ `part` and any aliases or element references selected from it still refer to the
15
+ original value. Each constraint must involve the callback value.
16
+
17
+ A model's local geometry and its placement in a composition are separate.
18
+ `relate()` records how a part is placed when composed with other parts; observing
19
+ that part alone shows its local geometry. `originOffset()` changes geometry
20
+ coordinates without changing the shape. Position arrays use `[x, y, z]`; scalar
21
+ angles use degrees. Read [local coordinates](local-coordinates.md)
22
+ and [relations](relations.mdx) before mixing
23
+ origin changes, alignment, and rotation.
24
+
25
+ Constraints express `on` and `align` only. Put relative `offset` and `rotate`
26
+ transformations in the `relate` array after the constraints. Zero values add no
27
+ centering or orientation condition; use point or axis alignment to center a part.
28
+ The App's gizmos edit or insert independent array entries.
29
+
30
+ Choose a
31
+ center with `pivot([x,y,z])`, self topology with `pivotVertex(id)`/`axisEdge(id)`,
32
+ or references with `pivotPoint(pointRef)`/`axisLine(lineRef)`; finish each selector
33
+ with `rotate`. Point rotations retain self XYZ axes, including external centers. Consecutive constraints solve jointly; transformations
34
+ then act on that result in order. A later constraint starts a new segment using
35
+ the preceding pose. Independent offsets use fixed composition axes, and rotations
36
+ default to self's current origin. Each completed transformation is one array item,
37
+ for example `[offset(0, 8, 0), rotate(0, 25, 0)]`. Only pivot/axis selections
38
+ chain into `rotate`; completed transformations cannot chain into another operation.
39
+ Keep a selected reference while moving it with
40
+ `pivotVertex(id).pivotOffset(dx, dy, dz).rotate(x, y, z)` or
41
+ `axisLine(axis).axisOffset(dx, dy, dz).rotate(angle)`. Point offsets use self local
42
+ axes; axis offsets use the selected axis frame and preserve its direction.
43
+ Each selector accepts one matching offset, followed by `rotate`.
44
+ See the [transformation example](../../app/examples/constraints/transformations.ts)
45
+ and [placement guide](relations.mdx#transform-a-joint-result).
46
+
47
+ Build readable models from named intermediate values and public operations. A
48
+ profile followed by extrusion, or solids combined with Boolean operations,
49
+ keeps the construction understandable and editable by both people and agents.
50
+
51
+ ## Measure geometry for another part
52
+
53
+ `distance(a, b, axis?)` measures models, finite topology, bounds or point anchors
54
+ in their solved placement and returns a normal non-negative number. Omit the axis
55
+ for shortest geometric distance; supply `x`, `y`, `z`, a direction vector or a
56
+ straight edge/axis reference for the gap between projected intervals. Overlap
57
+ returns zero. Existing relations are resolved on demand, before any group is
58
+ needed; subsequent relations do not update the number.
59
+
60
+ See the [measurement reference](api.md#measurements)
61
+ and the [fitted beam example](../../app/examples/operations/distance.ts). They cover
62
+ finite geometry, nested occurrences, axis frames and source-order dependencies.
63
+
64
+ ## Geometry measurements
65
+
66
+ `model.bounds(relativeTo?)` returns readonly `minimum`, `maximum` and `size`
67
+ XYZ vectors for tight finite geometry bounds. By default it uses the model's
68
+ own local frame. An explicit reference includes solved placement and nested
69
+ member occurrences in that reference's frame. Empty groups have no finite
70
+ bounds; a source occurring more than once in the reference is ambiguous.
71
+
72
+ `model.position(relativeTo)` returns the model origin in the explicit reference's
73
+ local frame. A model's origin in its own frame is always `[0, 0, 0]`, including
74
+ point models whose geometry may be offset from that origin. Neither query
75
+ changes the model or its placement. These methods are available on every model
76
+ kind, including groups. As model members, `bounds` and `position` are reserved
77
+ names and cannot be used as exposed element names.
78
+
79
+ ```ts
80
+ import {box, group, offset} from '@code3d/core';
81
+
82
+ const base = box(20, 4, 20);
83
+ const part = box(8, 12, 4).relate(self => [self.on(base.up), offset(20, 0, 0)]);
84
+ const size = part.bounds().size; // [8, 12, 4]
85
+ const origin = part.position(base); // [20, 8, 0]
86
+ const minimum = part.bounds(base).minimum; // [16, 2, -2]
87
+ export default group([base, part]);
88
+ ```
89
+
90
+ Dimension-based primitives and numeric modeling methods retain required TypeScript
91
+ signatures while providing runtime defaults for omitted or `undefined` values.
92
+ Rotations and displacements default to zero, scaling to one, extrusion distance
93
+ to ten, and fillet radius, chamfer distance and shell thickness to one. Relation
94
+ rotation selectors use the same angle defaults; `pivot()` defaults to local zero.
95
+ Explicit invalid values retain their normal errors. These defaults work in
96
+ ordinary JavaScript execution as well as App previews.
97
+
98
+ The App displays defaults as placeholders without inserting source arguments.
99
+ Committing a spatial drag fills all remaining omitted defaults in that operation;
100
+ for example, dragging the X ring of `rotate()` writes `rotate(angle, 0, 0)`. The
101
+ edit and completion share one undo step. Use explicit dimensions in finished
102
+ models; the [reference](api.md#runtime-defaults-while-editing)
103
+ lists the actual defaults.
104
+
105
+ Topology capabilities follow dimension: vertices expose vertex selection, edges
106
+ add edge selection, and faces and solids add surface selection. Only solids
107
+ provide `fillet`, `chamfer`, and `shell`. Groups compose values and support
108
+ relations, exposed elements, and materials without pretending to be geometry.
109
+
110
+ Groups are model values and can be nested directly with `group([inner, other])`,
111
+ including in mixed `Model[]` collections. Nesting preserves each group's hierarchy;
112
+ `expose()` adds named references when callers need to address members.
113
+
114
+ A topology ID belongs to its owning model and element kind. It is a number or a
115
+ flat numeric path, such as `.edge([1, 3])`. Local edits (`fillet`, `chamfer`, `shell`)
116
+ preserve one-to-one IDs and allocate fresh IDs for new elements without reusing
117
+ retired numbers. Loft, extrusion and Booleans prefix inherited IDs by input;
118
+ transforms preserve complete paths. Inspect the result after topology changes
119
+ instead of assuming IDs from a different model still apply.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code3d/core",
3
- "version": "0.0.1-alpha.0",
3
+ "version": "0.0.1-alpha.11",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -8,10 +8,11 @@
8
8
  "author": "vilicvane",
9
9
  "type": "module",
10
10
  "scripts": {
11
- "prepack": "node ../../scripts/package-license.mjs",
12
- "test": "node ../../node_modules/typescript/bin/tsc --build src/library/tsconfig.json src/node/tsconfig.json src/tooling/tsconfig.json && npm run test:types && npm run test:run",
13
- "test:run": "node --test \"test/*.test.{mjs,ts}\"",
14
- "test:types": "node ../../node_modules/typescript/bin/tsc --noEmit -p test/tsconfig.json"
11
+ "prepack": "npm run build",
12
+ "test": "npm run build && npm run test:types && npm run test:run",
13
+ "test:run": "node --import ../../test/source-loader.mjs --test --test-concurrency=1 \"test/*.test.{mjs,ts}\"",
14
+ "test:types": "node ../../node_modules/typescript/bin/tsc --noEmit -p test/tsconfig.json",
15
+ "build": "node ../../scripts/build-packages.mjs @code3d/core"
15
16
  },
16
17
  "exports": {
17
18
  ".": {
@@ -39,15 +40,27 @@
39
40
  "bld",
40
41
  "THIRD_PARTY.md",
41
42
  "README.md",
42
- "src"
43
+ "src",
44
+ "docs"
43
45
  ],
44
46
  "dependencies": {
45
47
  "@code3d/opencascade": "0.0.1-alpha.0",
46
- "@ctrl/tinycolor": "^4.2.0",
47
48
  "@salusoft89/planegcs": "1.2.0",
49
+ "@types/emscripten": "1.41.5",
48
50
  "@types/three": "^0.185.4",
51
+ "harfbuzzjs": "1.6.1",
52
+ "manifold-3d": "3.0.1",
49
53
  "replicad": "^1.0.1",
50
54
  "replicad-opencascadejs": "1.0.0",
51
55
  "three": "^0.185.1"
56
+ },
57
+ "devDependencies": {
58
+ "@ctrl/tinycolor": "^4.2.0",
59
+ "flo-boolean": "5.0.8"
60
+ },
61
+ "repository": {
62
+ "type": "git",
63
+ "url": "git+https://github.com/vilicvane/code3d.git",
64
+ "directory": "packages/core"
52
65
  }
53
66
  }
@@ -7,17 +7,56 @@ import {
7
7
  origin,
8
8
  rotateVector,
9
9
  rotation,
10
- transformsAreEquivalent,
11
10
  type Quaternion,
12
11
  type RigidTransform,
13
12
  type Vec3,
14
13
  } from './spatial.js';
15
14
 
16
15
  export type Bounds = readonly [minimum: Vec3, maximum: Vec3];
17
- export type BodyRotation =
18
- | Readonly<{local: RigidTransform}>
19
- | Readonly<{axis: RigidTransform; body: number; angle: number}>;
16
+ export type ExternalRotation =
17
+ | Readonly<{axis: RigidTransform; body: number; angle: number}>
18
+ | Readonly<{
19
+ point: Vec3;
20
+ body: number;
21
+ rotation: Quaternion;
22
+ displacement: Vec3;
23
+ }>;
24
+ export type BodyRotation = Readonly<{local: RigidTransform}> | ExternalRotation;
20
25
 
26
+ /** External center relative to its owning body; point references retain self's axes. */
27
+ export function externalRotationFrame(
28
+ action: ExternalRotation,
29
+ self: Quaternion,
30
+ reference: Quaternion,
31
+ inverse = false,
32
+ ): {center: Vec3; quaternion: Quaternion} {
33
+ if ('axis' in action) {
34
+ const axis = composeTransforms(rotation(reference), action.axis);
35
+ return {
36
+ center: axis.position,
37
+ quaternion: axisRotation(axis, inverse ? -action.angle : action.angle)
38
+ .quaternion,
39
+ };
40
+ }
41
+ const local = inverse
42
+ ? invertTransform(rotation(action.rotation))
43
+ : rotation(action.rotation);
44
+ const before = inverse
45
+ ? composeTransforms(rotation(self), local)
46
+ : rotation(self);
47
+ return {
48
+ center: addVectors(
49
+ rotateVector(action.point, reference),
50
+ rotateVector(action.displacement, before.quaternion),
51
+ ),
52
+ quaternion: composeTransforms(
53
+ composeTransforms(before, local),
54
+ invertTransform(before),
55
+ ).quaternion,
56
+ };
57
+ }
58
+
59
+ export type BodyAction = BodyRotation | Readonly<{offset: Vec3}>;
21
60
  export type BodyRelation = Readonly<{
22
61
  id: string;
23
62
  source: Readonly<{
@@ -26,11 +65,16 @@ export type BodyRelation = Readonly<{
26
65
  bounds: (orientation: Quaternion) => Bounds;
27
66
  }>;
28
67
  target: Readonly<{body: number; transform: RigidTransform; facing: 1 | -1}>;
29
- offset: Vec3 | undefined;
30
- rotations: readonly BodyRotation[];
31
68
  }>;
32
69
 
33
- export type Body = Readonly<{name: string; relations: readonly BodyRelation[]}>;
70
+ export type Body = Readonly<{
71
+ name: string;
72
+ relations: readonly BodyRelation[];
73
+ /** Pose inherited from the preceding placement segment. */
74
+ initial?: RigidTransform;
75
+ /** Independent actions apply after the joint constraint solution. */
76
+ transformations?: readonly BodyAction[];
77
+ }>;
34
78
  type AffinePoint = {constant: Vec3; columns: Vec3[]};
35
79
  type AffinePose = {position: AffinePoint; quaternion: Quaternion};
36
80
  type Equation = {id: string; coefficients: number[]; value: number};
@@ -74,37 +118,21 @@ export function solveBodies(
74
118
  'Cyclic external rotation axes cannot determine an authored orientation.',
75
119
  );
76
120
  visiting.add(index);
77
- const candidates = bodies[index].relations
78
- .filter(r => r.rotations.length)
79
- .map(relation => {
80
- let pose = identityRigidTransform;
81
- for (const action of relation.rotations) {
82
- pose =
83
- 'local' in action
84
- ? composeTransforms(pose, action.local)
85
- : composeTransforms(
86
- axisRotation(
87
- composeTransforms(seed(action.body), action.axis),
88
- action.angle,
89
- ),
90
- pose,
91
- );
92
- }
93
- return pose;
94
- });
95
- const pose = candidates[0] ?? identityRigidTransform;
96
- if (
97
- candidates.some(
98
- candidate =>
99
- !transformsAreEquivalent(
100
- rotation(candidate.quaternion),
101
- rotation(pose.quaternion),
102
- ),
103
- )
104
- ) {
105
- throw new Error(
106
- `Conflicting explicit rotations for ${bodies[index].name}.`,
107
- );
121
+ let pose = bodies[index].initial ?? identityRigidTransform;
122
+ for (const action of bodies[index].transformations ?? []) {
123
+ if ('offset' in action) continue;
124
+ pose =
125
+ 'local' in action
126
+ ? composeTransforms(pose, action.local)
127
+ : 'point' in action
128
+ ? composeTransforms(pose, rotation(action.rotation))
129
+ : composeTransforms(
130
+ axisRotation(
131
+ composeTransforms(seed(action.body), action.axis),
132
+ action.angle,
133
+ ),
134
+ pose,
135
+ );
108
136
  }
109
137
  visiting.delete(index);
110
138
  seeds.set(index, pose);
@@ -120,19 +148,18 @@ export function solveBodies(
120
148
  const known = positioned.get(index);
121
149
  if (known) return known;
122
150
  let pose: AffinePose = {
123
- quaternion: identityRigidTransform.quaternion,
151
+ quaternion: (bodies[index].initial ?? identityRigidTransform).quaternion,
124
152
  position: {
125
- constant: origin,
153
+ constant: (bodies[index].initial ?? identityRigidTransform).position,
126
154
  columns: variables.flatMap(body =>
127
155
  axes.map(axis => (body === index ? axis : origin)),
128
156
  ),
129
157
  },
130
158
  };
131
- const actions =
132
- bodies[index].relations.find(relation => relation.rotations.length)
133
- ?.rotations ?? [];
134
- for (const action of actions) {
135
- if ('local' in action) {
159
+ for (const action of bodies[index].transformations ?? []) {
160
+ if ('offset' in action) {
161
+ pose = {...pose, position: shiftPoint(pose.position, action.offset)};
162
+ } else if ('local' in action) {
136
163
  pose = {
137
164
  position: shiftPoint(
138
165
  pose.position,
@@ -145,12 +172,12 @@ export function solveBodies(
145
172
  };
146
173
  } else {
147
174
  const axisPose = positionBody(action.body);
148
- const axis = composeTransforms(
149
- rotation(axisPose.quaternion),
150
- action.axis,
175
+ const {center, quaternion: q} = externalRotationFrame(
176
+ action,
177
+ pose.quaternion,
178
+ axisPose.quaternion,
151
179
  );
152
- const q = axisRotation(axis, action.angle).quaternion;
153
- const axisPoint = shiftPoint(axisPose.position, axis.position);
180
+ const axisPoint = shiftPoint(axisPose.position, center);
154
181
  const rotatedAxis = rotatePoint(axisPoint, q);
155
182
  pose = {
156
183
  position: addPoints(
@@ -173,16 +200,19 @@ export function solveBodies(
173
200
  const preferences = new Map<string, Equation>();
174
201
  bodies.forEach((body, owner) =>
175
202
  body.relations.forEach(relation => {
176
- // A chain describes contact followed by its explicit rotations. Invert only
177
- // this chain; every other relation still sees and constrains the final pose.
178
- const baseline = undoRotations(poses[owner], relation.rotations, poses);
179
- // All authored contact stages contribute equally to the free-position
180
- // choice. Deduplicate identical stages so repeated conditions add no bias.
203
+ // Every contact constrains the joint result before independent transforms.
204
+ const baseline = undoActions(
205
+ poses[owner],
206
+ body.transformations ?? [],
207
+ poses,
208
+ );
181
209
  for (let axis = 0; axis < 3; axis++) {
182
210
  const preference = {
183
211
  id: relation.id,
184
212
  coefficients: baseline.position.columns.map(column => column[axis]),
185
- value: -baseline.position.constant[axis],
213
+ value:
214
+ (body.initial?.position[axis] ?? 0) -
215
+ baseline.position.constant[axis],
186
216
  };
187
217
  preferences.set(
188
218
  JSON.stringify([preference.coefficients, preference.value]),
@@ -212,14 +242,8 @@ export function solveBodies(
212
242
  source.position,
213
243
  rotateVector(center, targetFrame.quaternion),
214
244
  );
215
- const targetPoint = shiftPoint(
216
- target.position,
217
- addVectors(
218
- targetFrame.position,
219
- rotateVector(relation.offset ?? origin, targetFrame.quaternion),
220
- ),
221
- );
222
- for (const localAxis of relation.offset ? axes : [axes[1]]) {
245
+ const targetPoint = shiftPoint(target.position, targetFrame.position);
246
+ for (const localAxis of [axes[1]]) {
223
247
  const normal = rotateVector(localAxis, targetFrame.quaternion);
224
248
  equations.push({
225
249
  id: relation.id,
@@ -272,13 +296,18 @@ function addPoints(left: AffinePoint, right: AffinePoint): AffinePoint {
272
296
  };
273
297
  }
274
298
 
275
- function undoRotations(
299
+ function undoActions(
276
300
  pose: AffinePose,
277
- actions: readonly BodyRotation[],
301
+ actions: readonly BodyAction[],
278
302
  poses: readonly AffinePose[],
279
303
  ): AffinePose {
280
304
  for (const action of [...actions].reverse()) {
281
- if ('local' in action) {
305
+ if ('offset' in action) {
306
+ pose = {
307
+ ...pose,
308
+ position: shiftPoint(pose.position, negateVector(action.offset)),
309
+ };
310
+ } else if ('local' in action) {
282
311
  const inverse = invertTransform(action.local);
283
312
  pose = {
284
313
  position: shiftPoint(
@@ -292,12 +321,13 @@ function undoRotations(
292
321
  };
293
322
  } else {
294
323
  const axisPose = poses[action.body];
295
- const axis = composeTransforms(
296
- rotation(axisPose.quaternion),
297
- action.axis,
324
+ const {center, quaternion: q} = externalRotationFrame(
325
+ action,
326
+ pose.quaternion,
327
+ axisPose.quaternion,
328
+ true,
298
329
  );
299
- const q = axisRotation(axis, -action.angle).quaternion;
300
- const axisPoint = shiftPoint(axisPose.position, axis.position);
330
+ const axisPoint = shiftPoint(axisPose.position, center);
301
331
  const rotatedAxis = rotatePoint(axisPoint, q);
302
332
  pose = {
303
333
  position: addPoints(
@@ -316,7 +346,7 @@ function undoRotations(
316
346
  }
317
347
 
318
348
  /** Solve Ax=b, then minimize contact-stage displacement in its nullspace. */
319
- function solveTranslations(
349
+ export function solveTranslations(
320
350
  equations: readonly Equation[],
321
351
  preferences: readonly Equation[],
322
352
  dimension: number,
@@ -0,0 +1,153 @@
1
+ import {encodeKernelArtifact} from './kernel-artifact-codec.js';
2
+ import {
3
+ acceptKernelOperation,
4
+ evaluateCachedArtifact,
5
+ findKernelOperation,
6
+ findKernelOperations,
7
+ kernelContentId,
8
+ kernelOperationKey,
9
+ type CacheCodec,
10
+ type KernelArtifact,
11
+ type KernelOperationKey,
12
+ type KernelValueLifecycle,
13
+ } from './kernel-cache.js';
14
+ import {estimateRetainedBytes} from './retained-memory.js';
15
+
16
+ export type CacheOptions<Value> = CacheCodec<Value>;
17
+
18
+ const identities = new WeakMap<Function, string>();
19
+ const sessionIdentities = new WeakMap<Function, string>();
20
+ const session = crypto.randomUUID();
21
+ let nextIdentity = 0;
22
+
23
+ /** The compiler supplies the definition and its static dependency fingerprint. */
24
+ export function identifyCachedFunction<Fn extends Function>(
25
+ fn: Fn,
26
+ identity: string,
27
+ ): Fn {
28
+ // Identity belongs to this cache definition (including its codec), not to
29
+ // every other use of the author's original function object.
30
+ const identified = function (this: unknown, ...args: unknown[]) {
31
+ return Reflect.apply(fn, this, args);
32
+ } as unknown as Fn;
33
+ identities.set(identified, identity);
34
+ return identified;
35
+ }
36
+
37
+ function functionIdentity(fn: Function): {id: string; persistent: boolean} {
38
+ const identity = identities.get(fn);
39
+ if (identity) return {id: identity, persistent: true};
40
+ let id = sessionIdentities.get(fn);
41
+ if (!id) {
42
+ id = `${session}:${nextIdentity++}`;
43
+ sessionIdentities.set(fn, id);
44
+ }
45
+ // Without compiler fingerprints, object identity is the only sound identity
46
+ // for a JavaScript closure. It still shares the process-wide memory budget.
47
+ return {id, persistent: false};
48
+ }
49
+
50
+ const dataLifecycle: KernelValueLifecycle<unknown> = {
51
+ estimateBytes: estimateRetainedBytes,
52
+ retain: value => value,
53
+ instantiate: value => value,
54
+ release() {},
55
+ };
56
+
57
+ /**
58
+ * Memoizes a synchronous, deterministic computation in the shared cache.
59
+ * Omit args to create a function, or pass an argument tuple to return its value.
60
+ * Pass changing captured state as arguments and treat returned data as immutable.
61
+ * The engine fingerprints definitions and dependencies for persistent reuse.
62
+ * Outside the engine, function identity provides process-local memory reuse.
63
+ * Custom encoder/decoder pairs run only when writing/restoring persistent data;
64
+ * memory hits return the retained value without decoding or copying it.
65
+ */
66
+ export function cache<Args extends unknown[], Value>(
67
+ compute: (
68
+ ...args: Args
69
+ ) => Value & (Value extends PromiseLike<unknown> ? never : unknown),
70
+ args: Readonly<NoInfer<Args>>,
71
+ options?: CacheOptions<NoInfer<Value>>,
72
+ ): Value;
73
+ export function cache<Args extends unknown[], Value>(
74
+ compute: (
75
+ ...args: Args
76
+ ) => Value & (Value extends PromiseLike<unknown> ? never : unknown),
77
+ args?: undefined,
78
+ options?: CacheOptions<NoInfer<Value>>,
79
+ ): (...args: Args) => Value;
80
+ export function cache<Args extends unknown[], Value>(
81
+ compute: (
82
+ ...args: Args
83
+ ) => Value & (Value extends PromiseLike<unknown> ? never : unknown),
84
+ args?: Readonly<Args>,
85
+ options?: CacheOptions<Value>,
86
+ ): Value | ((...args: Args) => Value) {
87
+ const operation = cachedArtifact(
88
+ (...args: Args): Value => {
89
+ const value = compute(...args);
90
+ if (
91
+ value &&
92
+ typeof value === 'object' &&
93
+ 'then' in value &&
94
+ typeof value.then === 'function'
95
+ )
96
+ throw new Error('cache() requires a synchronous computation.');
97
+ return value;
98
+ },
99
+ {identity: compute, codec: options},
100
+ );
101
+ return args === undefined
102
+ ? (...args: Args) => operation(...args).value
103
+ : operation(...args).value;
104
+ }
105
+
106
+ /** Internal artifact form also supports native ownership and remote admission. */
107
+ export function cachedArtifact<Args extends unknown[], Value>(
108
+ compute: (...args: Args) => Value,
109
+ {
110
+ key,
111
+ lifecycle = dataLifecycle as KernelValueLifecycle<Value>,
112
+ codec,
113
+ identity = compute,
114
+ namespace = 'cached',
115
+ }: {
116
+ key?: (...args: Args) => KernelOperationKey;
117
+ lifecycle?: KernelValueLifecycle<Value>;
118
+ codec?: CacheCodec<Value> | false;
119
+ identity?: Function;
120
+ namespace?: string;
121
+ } = {},
122
+ ) {
123
+ const definition = functionIdentity(identity);
124
+ const persistence = key || definition.persistent ? codec : false;
125
+ const operationKey =
126
+ key ??
127
+ ((...args: Args) =>
128
+ kernelOperationKey(
129
+ namespace,
130
+ [definition.id, kernelContentId(encodeKernelArtifact('', args))],
131
+ [],
132
+ ));
133
+ const find = (key: KernelOperationKey) =>
134
+ findKernelOperation(key, lifecycle, persistence);
135
+ const findMany = (keys: readonly KernelOperationKey[]) =>
136
+ findKernelOperations(keys, lifecycle, persistence);
137
+ const accept = (
138
+ key: KernelOperationKey,
139
+ value: Value,
140
+ milliseconds: number,
141
+ ) => acceptKernelOperation(key, lifecycle, value, milliseconds, persistence);
142
+ return Object.assign(
143
+ (...args: Args): KernelArtifact<Value> => {
144
+ return evaluateCachedArtifact(
145
+ operationKey(...args),
146
+ lifecycle,
147
+ () => compute(...args),
148
+ persistence,
149
+ );
150
+ },
151
+ {key: operationKey, find, findMany, accept},
152
+ );
153
+ }