@code3d/core 0.0.1-alpha.4 → 0.0.1-alpha.7

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 (43) hide show
  1. package/README.md +42 -5
  2. package/bld/chunks/{chunk-J77FMYFY.js → chunk-4WLVDYUJ.js} +992 -545
  3. package/bld/chunks/chunk-4WLVDYUJ.js.map +7 -0
  4. package/bld/chunks/{chunk-PAIFBNU4.js → chunk-C3AJ6WNB.js} +2 -2
  5. package/bld/chunks/{chunk-OQLJDJIF.js → chunk-TCMEEKWL.js} +2 -2
  6. package/bld/chunks/{chunk-ZMT6EFHL.js → chunk-VHSPHJ5F.js} +3 -3
  7. package/bld/library/bound-solver.d.ts +29 -5
  8. package/bld/library/bound-solver.d.ts.map +1 -1
  9. package/bld/library/cached.d.ts +5 -3
  10. package/bld/library/cached.d.ts.map +1 -1
  11. package/bld/library/index.d.ts +4 -4
  12. package/bld/library/index.d.ts.map +1 -1
  13. package/bld/library/index.js +17 -3
  14. package/bld/library/kernel-cache.d.ts +6 -3
  15. package/bld/library/kernel-cache.d.ts.map +1 -1
  16. package/bld/library/relation-solver.d.ts +12 -7
  17. package/bld/library/relation-solver.d.ts.map +1 -1
  18. package/bld/library/replicad.js +2 -2
  19. package/bld/library/runtime.d.ts +218 -82
  20. package/bld/library/runtime.d.ts.map +1 -1
  21. package/bld/library/sketch.d.ts +2 -2
  22. package/bld/library/sketch.d.ts.map +1 -1
  23. package/bld/library/topology.d.ts +2 -0
  24. package/bld/library/topology.d.ts.map +1 -1
  25. package/bld/node/index.js +19 -5
  26. package/bld/node/replicad.js +4 -4
  27. package/bld/tooling/index.d.ts +3 -3
  28. package/bld/tooling/index.d.ts.map +1 -1
  29. package/bld/tooling/index.js +18 -10
  30. package/package.json +1 -1
  31. package/src/library/bound-solver.ts +103 -73
  32. package/src/library/cached.ts +29 -8
  33. package/src/library/index.ts +15 -4
  34. package/src/library/kernel-cache.ts +37 -5
  35. package/src/library/relation-solver.ts +194 -110
  36. package/src/library/runtime.ts +1046 -442
  37. package/src/library/sketch.ts +3 -3
  38. package/src/library/topology.ts +13 -2
  39. package/src/tooling/index.ts +15 -7
  40. package/bld/chunks/chunk-J77FMYFY.js.map +0 -7
  41. /package/bld/chunks/{chunk-PAIFBNU4.js.map → chunk-C3AJ6WNB.js.map} +0 -0
  42. /package/bld/chunks/{chunk-OQLJDJIF.js.map → chunk-TCMEEKWL.js.map} +0 -0
  43. /package/bld/chunks/{chunk-ZMT6EFHL.js.map → chunk-VHSPHJ5F.js.map} +0 -0
@@ -32,7 +32,7 @@ import {
32
32
  transformGeometry,
33
33
  type AlignmentGeometry,
34
34
  } from './alignment-geometry.js';
35
- import {cached, cachedArtifact} from './cached.js';
35
+ import {cache, cachedArtifact} from './cached.js';
36
36
  import {extrudeWithTopology} from './extrude.js';
37
37
  import {font, googleFont, type Font} from './font.js';
38
38
  import {
@@ -56,7 +56,9 @@ import {
56
56
  axisRotation,
57
57
  solveBodies,
58
58
  type Body,
59
- type BodyRotation,
59
+ beforeTransformations,
60
+ afterTransformations,
61
+ type BodyAction,
60
62
  } from './relation-solver.js';
61
63
  import {estimateRetainedBytes} from './retained-memory.js';
62
64
  import {shellWithTopology} from './shell.js';
@@ -76,6 +78,7 @@ import {
76
78
  rotation,
77
79
  rotationAround,
78
80
  translation,
81
+ transformsAreEquivalent,
79
82
  type Quaternion,
80
83
  type RigidTransform,
81
84
  type Vec3,
@@ -89,6 +92,7 @@ import {
89
92
  type TopologyInspectionOptions,
90
93
  } from './topology-inspection.js';
91
94
  import {
95
+ assertTopologyId,
92
96
  booleanWithTopology,
93
97
  chamferEdges,
94
98
  filletEdges,
@@ -179,11 +183,6 @@ export type ConstraintSnapshot = Readonly<{
179
183
  target: ConstraintAnchorSnapshot;
180
184
  sourceElement: ElementSnapshot;
181
185
  targetElement: ElementSnapshot;
182
- offsetDirection: 1 | -1;
183
- offset: Vec3;
184
- offsetFrame: Transform;
185
- /** Latest authored rotation, expressed in the fully solved self frame. */
186
- rotation?: ModelSpatialOperation;
187
186
  sourceRefs: readonly SourceRef[];
188
187
  parameters: readonly ParameterUsage[];
189
188
  }> &
@@ -292,6 +291,26 @@ export type ModelParameterDimension = Readonly<{
292
291
  vector: Vec3;
293
292
  }>;
294
293
 
294
+ export type RotationReferenceSnapshot = Readonly<{
295
+ offset: Vec3;
296
+ offsetExplicit: boolean;
297
+ explicit: boolean;
298
+ /** Rotation in the displacement frame, including reversed axes. */
299
+ rotation: Vec3;
300
+ /** Reference displacement axes, in the result model's local coordinates. */
301
+ frame: RigidTransform;
302
+ }> &
303
+ (
304
+ | Readonly<{kind: 'pivot'; point: Vec3}>
305
+ | Readonly<{kind: 'pivotVertex'; id: VertexId}>
306
+ | Readonly<{kind: 'axisEdge'; id: EdgeId}>
307
+ | Readonly<{
308
+ kind: 'pivotPoint' | 'axisLine';
309
+ nodeId: string;
310
+ name: string;
311
+ }>
312
+ );
313
+
295
314
  /** Local geometry coordinates; vector is the authored coordinates, offset, or angles. */
296
315
  export type ModelSpatialOperation = Readonly<{
297
316
  origin: Vec3;
@@ -300,20 +319,34 @@ export type ModelSpatialOperation = Readonly<{
300
319
  frame?: RigidTransform;
301
320
  rotation?: Vec3;
302
321
  axisOnly?: boolean;
322
+ reference?: RotationReferenceSnapshot;
303
323
  }>;
304
324
 
305
- export type ConstraintSpatialReference = Readonly<{
325
+ export type RelationSpatialReference = Readonly<{
306
326
  nodeId: string;
307
- kind: 'pivot' | 'pivotVertex' | 'around' | 'rotate';
327
+ kind:
328
+ | 'pivot'
329
+ | 'pivotVertex'
330
+ | 'pivotPoint'
331
+ | 'axisEdge'
332
+ | 'pivotOffset'
333
+ | 'axisLine'
334
+ | 'axisOffset'
335
+ | 'rotate'
336
+ | 'offset';
308
337
  spatial: ModelSpatialOperation;
309
338
  }>;
310
339
 
311
- export type ConstraintPreview = Readonly<{
340
+ export type RelationPreview = Readonly<{
312
341
  object: Pick<
313
342
  ModelSnapshotObject,
314
- 'nodeId' | 'compositionTransform' | 'constraints'
343
+ | 'nodeId'
344
+ | 'compositionTransform'
345
+ | 'constraints'
346
+ | 'transformations'
347
+ | 'relationStages'
315
348
  >;
316
- spatial?: ConstraintSpatialReference;
349
+ spatial?: RelationSpatialReference;
317
350
  }>;
318
351
 
319
352
  export type RenderMesh = Readonly<{
@@ -336,6 +369,8 @@ export type RenderMesh = Readonly<{
336
369
  start: number;
337
370
  count: number;
338
371
  edgeId: EdgeId;
372
+ /** True for native straight edges, suitable as rotation references. */
373
+ linear?: boolean;
339
374
  }>[];
340
375
  }>;
341
376
 
@@ -350,6 +385,8 @@ export type ModelSnapshotObject = Readonly<{
350
385
  /** Placement in this snapshot tree; a root value uses model-local coordinates. */
351
386
  transform: Transform;
352
387
  constraints: readonly ConstraintSnapshot[];
388
+ transformations?: readonly TransformationSnapshot[];
389
+ relationStages?: readonly RelationStageSnapshot[];
353
390
  elements: readonly ElementSnapshot[];
354
391
  origin: Vec3;
355
392
  sourceRefs: readonly SourceRef[];
@@ -403,12 +440,21 @@ export type ModelTopologyReference = Readonly<{
403
440
  TopologySelection;
404
441
 
405
442
  export type ConstraintTraceReference = Readonly<{
443
+ kind: 'constraint';
406
444
  constraintId: string;
407
445
  source: RelationObject;
408
446
  target: RelationObject;
409
447
  self?: RelationObject;
410
448
  }>;
411
449
 
450
+ export type RelationTraceReference =
451
+ | ConstraintTraceReference
452
+ | Readonly<{
453
+ kind: 'transformation';
454
+ transformationId: string;
455
+ self?: RelationObject;
456
+ }>;
457
+
412
458
  export type ModelOperationInstrumentation = Readonly<{
413
459
  siteId: string;
414
460
  execution: number;
@@ -425,27 +471,93 @@ export type ModelObjectRuntimeInfo = Readonly<{
425
471
  sourceRefs: readonly SourceRef[];
426
472
  }>;
427
473
 
474
+ type RelationDefinition =
475
+ | Readonly<{
476
+ kind: 'on' | 'align';
477
+ source: AnchorReference;
478
+ target: AnchorReference;
479
+ }>
480
+ | Readonly<{
481
+ kind: 'transformation';
482
+ actions: readonly TransformationAction[];
483
+ }>;
484
+ type StoredTransformation = Readonly<{
485
+ id: string;
486
+ kind: 'transformation';
487
+ actions: readonly TransformationAction[];
488
+ }>;
489
+ export type RelationStageSnapshot = Readonly<{
490
+ constraintIds: readonly string[];
491
+ transformationIds: readonly string[];
492
+ compositionTransform: Transform;
493
+ offsetFrame: Transform;
494
+ }>;
495
+ export type TransformationSnapshot = Readonly<{
496
+ id: string;
497
+ sourceRefs: readonly SourceRef[];
498
+ parameters: readonly ParameterUsage[];
499
+ offsets: readonly Readonly<{
500
+ value: Vec3;
501
+ frame: Transform;
502
+ sourceRefs: readonly SourceRef[];
503
+ }>[];
504
+ rotations: readonly Readonly<{
505
+ spatial: ModelSpatialOperation;
506
+ sourceRefs: readonly SourceRef[];
507
+ }>[];
508
+ offsetFrame: Transform;
509
+ }>;
510
+ type StoredPlacement = StoredConstraint | StoredTransformation;
511
+
428
512
  type StoredConstraint = Readonly<{
429
513
  id: string;
430
514
  kind: 'on' | 'align';
431
515
  source: RelationReference;
432
516
  target: RelationReference;
433
- rotations: readonly ConstraintRotation[];
434
- offset: Vec3 | undefined;
435
517
  }>;
436
518
 
437
519
  // An omitted model refers to relate's self, including after immutable copies.
438
520
  type RelationReference = StoredAnchor & Readonly<{model?: RelationObject}>;
439
- type PivotSelection =
521
+ type PivotSelection = (
440
522
  | Readonly<{kind: 'pivot'; point: Vec3}>
441
523
  | Readonly<{kind: 'pivotVertex'; id: VertexId}>
442
- | Readonly<{kind: 'around'; axis: RelationReference}>;
443
- type ConstraintRotation = Readonly<{
524
+ | Readonly<{kind: 'pivotPoint'; point: RelationReference}>
525
+ | Readonly<{kind: 'axisEdge'; id: EdgeId}>
526
+ | Readonly<{kind: 'axisLine'; axis: RelationReference}>
527
+ ) &
528
+ Readonly<{offset?: Vec3; implicit?: true}>;
529
+ type AxisSelection = Extract<PivotSelection, {kind: 'axisLine' | 'axisEdge'}>;
530
+ type PointSelection = Exclude<PivotSelection, AxisSelection>;
531
+ function isAxisSelection(pivot: PivotSelection): pivot is AxisSelection {
532
+ return pivot.kind === 'axisLine' || pivot.kind === 'axisEdge';
533
+ }
534
+ function selectionReference(
535
+ pivot: PivotSelection,
536
+ ): RelationReference | undefined {
537
+ return pivot.kind === 'axisLine'
538
+ ? pivot.axis
539
+ : pivot.kind === 'pivotPoint'
540
+ ? pivot.point
541
+ : undefined;
542
+ }
543
+ function mapSelectionReference(
544
+ pivot: PivotSelection,
545
+ map: (reference: RelationReference) => RelationReference,
546
+ ): PivotSelection {
547
+ return pivot.kind === 'axisLine'
548
+ ? {...pivot, axis: map(pivot.axis)}
549
+ : pivot.kind === 'pivotPoint'
550
+ ? {...pivot, point: map(pivot.point)}
551
+ : pivot;
552
+ }
553
+ type TransformationRotationAction = Readonly<{
444
554
  pivot: PivotSelection;
445
555
  angles: Vec3 | number;
446
556
  }>;
447
- type ConstraintSpatialSelection = Readonly<{
448
- kind: ConstraintSpatialReference['kind'];
557
+ type TransformationAction =
558
+ TransformationRotationAction | Readonly<{offset: Vec3}>;
559
+ type TransformationSpatialSelection = Readonly<{
560
+ kind: RelationSpatialReference['kind'];
449
561
  pivot: PivotSelection;
450
562
  }>;
451
563
  type RelateContext = Readonly<{self: RelationObject; original: RelationObject}>;
@@ -529,7 +641,7 @@ type ModelObjectInit<Kind extends ModelKind = ModelKind> = Readonly<{
529
641
  material?: ModelMaterialSnapshot;
530
642
  children?: readonly ModelObject[];
531
643
  assembly?: SolveContext;
532
- constraints?: readonly StoredConstraint[];
644
+ placements?: readonly StoredPlacement[];
533
645
  elements?: StoredElements;
534
646
  sourceRefs?: readonly SourceRef[];
535
647
  parameters?: readonly ParameterUsage[];
@@ -560,6 +672,7 @@ type SolidGeometry = KernelArtifact<
560
672
 
561
673
  type SolveContext = {
562
674
  poses: ReadonlyMap<RelationObject, RigidTransform>;
675
+ frame: RigidTransform;
563
676
  };
564
677
 
565
678
  function transformSolveContext(
@@ -567,6 +680,7 @@ function transformSolveContext(
567
680
  transform: RigidTransform,
568
681
  ): SolveContext {
569
682
  return {
683
+ frame: composeTransforms(transform, context.frame),
570
684
  poses: new Map(
571
685
  [...context.poses].map(([model, pose]) => [
572
686
  model,
@@ -740,7 +854,7 @@ export interface ModelCapabilities<
740
854
  relate(
741
855
  build: (
742
856
  self: ModelForKind<Elements, Kind>,
743
- ) => Constraint | readonly Constraint[],
857
+ ) => Relation | readonly Relation[],
744
858
  ): ModelForKind<Elements, Kind>;
745
859
  expose<const Sources extends ElementSources>(
746
860
  sources: Sources,
@@ -1127,7 +1241,7 @@ export function modelTopologyIds(
1127
1241
  : undefined;
1128
1242
  const source = isModelObject(value)
1129
1243
  ? value
1130
- : value instanceof ConstraintExpression
1244
+ : value instanceof RelationExpression
1131
1245
  ? value.self()
1132
1246
  : topology?.source;
1133
1247
  const geometry = isModelObject(source)
@@ -1142,24 +1256,14 @@ export function modelTopologyIds(
1142
1256
  );
1143
1257
  }
1144
1258
 
1145
- /** Traceable relation values, including unfinished pivot and axis selections. */
1146
- export abstract class ConstraintExpression {
1147
- private get sourceRefs(): SourceRef[] {
1148
- return valueTrace(this).sourceRefs;
1149
- }
1150
- private get parameters(): ParameterUsage[] {
1151
- return valueTrace(this).parameters;
1152
- }
1153
- protected spatialOperation?: ConstraintSpatialSelection;
1259
+ /** Traceable placement values, including unfinished pivot and axis selections. */
1260
+ export abstract class RelationExpression {
1154
1261
  protected constructor(
1155
- protected readonly kind: StoredConstraint['kind'],
1156
- protected readonly source: AnchorReference,
1157
- protected readonly target: AnchorReference,
1158
- protected readonly displacement: Vec3 | undefined,
1159
- protected readonly rotations: readonly ConstraintRotation[],
1262
+ protected readonly definition: RelationDefinition,
1160
1263
  protected readonly context: RelateContext | undefined,
1161
- protected readonly constraintId: string,
1162
- previous?: ConstraintExpression,
1264
+ protected readonly relationId: string,
1265
+ previous?: RelationExpression,
1266
+ protected readonly spatialOperation?: TransformationSpatialSelection,
1163
1267
  ) {
1164
1268
  valueTraces.set(this, {
1165
1269
  sourceRefs: [...(previous ? valueTrace(previous).sourceRefs : [])],
@@ -1168,7 +1272,7 @@ export abstract class ConstraintExpression {
1168
1272
  }
1169
1273
  /** @internal */
1170
1274
  attachSource(sourceRef: SourceRef): void {
1171
- const refs = this.sourceRefs;
1275
+ const refs = valueTrace(this).sourceRefs;
1172
1276
  if (
1173
1277
  !refs.some(
1174
1278
  ref =>
@@ -1181,107 +1285,125 @@ export abstract class ConstraintExpression {
1181
1285
  }
1182
1286
  /** @internal */
1183
1287
  attachParameters(parameters: readonly ParameterUsage[]): void {
1184
- appendUniqueParameters(this.parameters, parameters);
1288
+ appendUniqueParameters(valueTrace(this).parameters, parameters);
1185
1289
  }
1186
1290
  /** @internal */
1187
1291
  self(): RelationObject | undefined {
1188
1292
  return this.context?.self;
1189
1293
  }
1190
1294
  /** @internal */
1191
- traceReference(): ConstraintTraceReference {
1295
+ traceReference(): RelationTraceReference {
1296
+ if (this.definition.kind === 'transformation')
1297
+ return {
1298
+ kind: 'transformation',
1299
+ transformationId: this.relationId,
1300
+ self: this.context?.self,
1301
+ };
1192
1302
  const rebind = (model: RelationObject) =>
1193
1303
  model === this.context?.original ? this.context.self : model;
1194
1304
  return {
1195
- constraintId: this.constraintId,
1196
- source: rebind(this.source.model),
1197
- target: rebind(this.target.model),
1305
+ kind: 'constraint',
1306
+ constraintId: this.relationId,
1307
+ source: rebind(this.definition.source.model),
1308
+ target: rebind(this.definition.target.model),
1198
1309
  self: this.context?.self,
1199
1310
  };
1200
1311
  }
1201
1312
  /** @internal */
1202
- storeFor(model: RelationObject, original: RelationObject): StoredConstraint {
1313
+ storeFor(model: RelationObject, original: RelationObject): StoredPlacement {
1203
1314
  const isSelf = (candidate: RelationObject | undefined) =>
1204
1315
  candidate === model || candidate === original;
1205
- if (!isSelf(this.source.model) && !isSelf(this.target.model))
1206
- throw new Error(
1207
- 'The constraint returned by relate() must involve self or the original receiver.',
1208
- );
1209
1316
  const bind = (reference: RelationReference): RelationReference => ({
1210
1317
  ...reference,
1211
1318
  model: isSelf(reference.model) ? undefined : reference.model,
1212
1319
  });
1213
- const stored: StoredConstraint = {
1214
- id: this.constraintId,
1215
- kind: this.kind,
1216
- source: bind(this.source),
1217
- target: bind(this.target),
1218
- offset: this.displacement,
1219
- rotations: this.rotations.map(action => ({
1220
- ...action,
1221
- pivot:
1222
- action.pivot.kind === 'around'
1223
- ? {...action.pivot, axis: bind(action.pivot.axis)}
1224
- : action.pivot,
1225
- })),
1226
- };
1320
+ const definition = this.definition;
1321
+ if (
1322
+ definition.kind !== 'transformation' &&
1323
+ !isSelf(definition.source.model) &&
1324
+ !isSelf(definition.target.model)
1325
+ )
1326
+ throw new Error(
1327
+ 'The constraint returned by relate() must involve self or the original receiver.',
1328
+ );
1329
+ const stored: StoredPlacement =
1330
+ definition.kind === 'transformation'
1331
+ ? {
1332
+ kind: 'transformation',
1333
+ id: this.relationId,
1334
+ actions: definition.actions.map(action => {
1335
+ const storedAction = {
1336
+ ...action,
1337
+ ...('pivot' in action
1338
+ ? {
1339
+ pivot: mapSelectionReference(action.pivot, bind),
1340
+ }
1341
+ : {}),
1342
+ };
1343
+ valueTraces.set(storedAction, {
1344
+ sourceRefs: [...valueTrace(action).sourceRefs],
1345
+ parameters: [...valueTrace(action).parameters],
1346
+ });
1347
+ return storedAction;
1348
+ }),
1349
+ }
1350
+ : {
1351
+ kind: definition.kind,
1352
+ id: this.relationId,
1353
+ source: bind(definition.source),
1354
+ target: bind(definition.target),
1355
+ };
1227
1356
  valueTraces.set(stored, {
1228
- sourceRefs: [...this.sourceRefs],
1229
- parameters: [...this.parameters],
1357
+ sourceRefs: [...valueTrace(this).sourceRefs],
1358
+ parameters: [...valueTrace(this).parameters],
1230
1359
  });
1231
1360
  return stored;
1232
1361
  }
1233
1362
  /** @internal */
1234
- preview(): ConstraintPreview | undefined {
1363
+ preview(
1364
+ preceding: readonly RelationExpression[] = [],
1365
+ ): RelationPreview | undefined {
1235
1366
  if (!this.context) return undefined;
1236
1367
  const pivot = this.spatialOperation?.pivot;
1237
- const selection =
1238
- pivot?.kind === 'around'
1239
- ? {
1240
- ...this.spatialOperation!,
1241
- pivot: {
1242
- ...pivot,
1243
- axis: {
1244
- ...pivot.axis,
1245
- model:
1246
- pivot.axis.model === this.context.self ||
1247
- pivot.axis.model === this.context.original
1248
- ? undefined
1249
- : pivot.axis.model,
1250
- },
1251
- },
1252
- }
1253
- : this.spatialOperation;
1254
- return this.context.original[relationPreview](
1255
- this.context.self.nodeId,
1368
+ const selection = pivot
1369
+ ? {
1370
+ ...this.spatialOperation!,
1371
+ pivot: mapSelectionReference(pivot, reference => ({
1372
+ ...reference,
1373
+ model:
1374
+ reference.model === this.context!.self ||
1375
+ reference.model === this.context!.original
1376
+ ? undefined
1377
+ : reference.model,
1378
+ })),
1379
+ }
1380
+ : this.spatialOperation;
1381
+ return this.context.self[previewRelation](
1256
1382
  this.storeFor(this.context.self, this.context.original),
1257
1383
  selection,
1384
+ preceding,
1258
1385
  );
1259
1386
  }
1387
+ /** @internal */
1388
+ continuationArguments(): [
1389
+ RelationDefinition,
1390
+ RelateContext | undefined,
1391
+ string,
1392
+ RelationExpression,
1393
+ ] {
1394
+ return [this.definition, this.context, this.relationId, this];
1395
+ }
1260
1396
  }
1261
1397
 
1262
- export class Constraint extends ConstraintExpression {
1398
+ export class Constraint extends RelationExpression {
1399
+ declare protected readonly definition: Exclude<
1400
+ RelationDefinition,
1401
+ {kind: 'transformation'}
1402
+ >;
1263
1403
  private constructor(
1264
- kind: StoredConstraint['kind'],
1265
- source: AnchorReference,
1266
- target: AnchorReference,
1267
- displacement: Vec3 | undefined = undefined,
1268
- rotations: readonly ConstraintRotation[] = [],
1269
- context = activeRelate,
1270
- constraintId = `constraint-${nextConstraintId++}`,
1271
- previous?: ConstraintExpression,
1272
- spatialOperation?: ConstraintSpatialSelection,
1404
+ definition: Exclude<RelationDefinition, {kind: 'transformation'}>,
1273
1405
  ) {
1274
- super(
1275
- kind,
1276
- source,
1277
- target,
1278
- displacement,
1279
- rotations,
1280
- context,
1281
- constraintId,
1282
- previous,
1283
- );
1284
- this.spatialOperation = spatialOperation;
1406
+ super(definition, activeRelate, `constraint-${nextConstraintId++}`);
1285
1407
  }
1286
1408
  /** @internal */
1287
1409
  static create(
@@ -1295,171 +1417,254 @@ export class Constraint extends ConstraintExpression {
1295
1417
  throw new Error(
1296
1418
  'align() requires points, curves, or surfaces; select geometry on a solid or group.',
1297
1419
  );
1298
- return new Constraint(kind, source, target);
1420
+ return new Constraint({kind, source, target});
1299
1421
  }
1300
- /**
1301
- * In the target reference axes, on() pins matching bound centers; align()
1302
- * translates self after alignment, retaining the relation's free modes.
1303
- * Explicit zero pins tangential coordinates for on() only.
1304
- * @code3d.param x {kind: 'length', default: 0, label: 'ΔX'}
1305
- * @code3d.param y {kind: 'length', default: 0, label: 'ΔY'}
1306
- * @code3d.param z {kind: 'length', default: 0, label: 'ΔZ'}
1307
- */
1308
- offset(x: number, y: number, z: number): Constraint;
1309
- offset(x = 0, y = 0, z = 0): Constraint {
1310
- assertFiniteVector('offset', [x, y, z]);
1311
- return new Constraint(
1312
- this.kind,
1313
- this.source,
1314
- this.target,
1315
- addVectors(this.displacement ?? origin, [x, y, z]),
1316
- this.rotations,
1317
- this.context,
1318
- this.constraintId,
1319
- this,
1422
+ }
1423
+
1424
+ /** One completed relative placement step. Array order composes transformations. */
1425
+ export class Transformation extends RelationExpression {
1426
+ declare protected readonly definition: Extract<
1427
+ RelationDefinition,
1428
+ {kind: 'transformation'}
1429
+ >;
1430
+ /** @internal */
1431
+ constructor(action: TransformationAction, previous?: RelationExpression) {
1432
+ const args = previous?.continuationArguments();
1433
+ super(
1434
+ {kind: 'transformation', actions: [action]},
1435
+ args ? args[1] : activeRelate,
1436
+ args?.[2] ?? `transformation-${nextConstraintId++}`,
1437
+ previous,
1438
+ 'pivot' in action ? {kind: 'rotate', pivot: action.pivot} : undefined,
1320
1439
  );
1440
+ valueTraces.set(action, valueTrace(this));
1321
1441
  }
1322
- /**
1323
- * Select this rotation's pivot in self's local coordinates.
1324
- * @code3d.param x {kind: 'length', default: 0, label: 'Pivot X'}
1325
- * @code3d.param y {kind: 'length', default: 0, label: 'Pivot Y'}
1326
- * @code3d.param z {kind: 'length', default: 0, label: 'Pivot Z'}
1327
- */
1328
- pivot([x, y, z]: Vec3): ConstraintPivotChain;
1329
- pivot([x = 0, y = 0, z = 0]: Vec3 = [0, 0, 0]): ConstraintPivotChain {
1330
- assertFiniteVector('pivot', [x, y, z]);
1331
- return new ConstraintPivotChain(this, {kind: 'pivot', point: [x, y, z]});
1332
- }
1333
- /** @code3d.param id {kind: 'vertex', label: 'Pivot vertex'} */
1334
- pivotVertex(id: VertexId): ConstraintPivotChain {
1335
- return new ConstraintPivotChain(this, {kind: 'pivotVertex', id});
1336
- }
1337
- /** Select a positioned axis in the composition. */
1338
- around(axis: LineAnchor): ConstraintAroundChain {
1339
- const reference = anchorReference(axis);
1340
- if (reference.kind !== 'line')
1341
- throw new Error('around() requires an axis or straight edge.');
1342
- const geometry =
1343
- reference.topology?.source[modelGeometry]()?.value ??
1344
- (reference.whole && isModelObject(reference.model)
1345
- ? reference.model[modelGeometry]()?.value
1346
- : undefined);
1347
- if (geometry) {
1348
- const straight = reference.topology
1349
- ? withTopologyShape(
1350
- geometry.shape,
1351
- geometry.topology,
1352
- reference.topology.selection,
1353
- shape => (shape as ReplicadEdge).geomType === 'LINE',
1354
- )
1355
- : (geometry.shape as ReplicadEdge).geomType === 'LINE';
1356
- if (!straight)
1357
- throw new Error(
1358
- 'around() requires a straight axis; curved edges do not define one rotation axis.',
1359
- );
1360
- }
1361
- return new ConstraintAroundChain(this, {kind: 'around', axis: reference});
1362
- }
1363
- /**
1364
- * Rotate about self's origin and local X, Y, then Z axes, in degrees.
1365
- * @code3d.param x {kind: 'angle', default: 0, label: 'Rotate X'}
1366
- * @code3d.param y {kind: 'angle', default: 0, label: 'Rotate Y'}
1367
- * @code3d.param z {kind: 'angle', default: 0, label: 'Rotate Z'}
1368
- */
1369
- rotate(x: number, y: number, z: number): Constraint;
1370
- rotate(x = 0, y = 0, z = 0): Constraint {
1371
- return this.withRotation({kind: 'pivot', point: origin}, [x, y, z], this);
1442
+ }
1443
+
1444
+ /** Carries the reference selection until its independent rotation is complete. */
1445
+ class TransformationRotation extends RelationExpression {
1446
+ constructor() {
1447
+ super(
1448
+ {kind: 'transformation', actions: []},
1449
+ activeRelate,
1450
+ `transformation-${nextConstraintId++}`,
1451
+ );
1372
1452
  }
1373
- /** @internal */
1374
1453
  withRotation(
1375
1454
  pivot: PivotSelection,
1376
1455
  angles: Vec3 | number,
1377
- previous: ConstraintExpression,
1378
- ): Constraint {
1456
+ previous: RelationExpression,
1457
+ ): Transformation {
1379
1458
  assertFiniteVector(
1380
1459
  'rotate',
1381
1460
  typeof angles === 'number' ? [angles, 0, 0] : angles,
1382
1461
  );
1383
- return new Constraint(
1384
- this.kind,
1385
- this.source,
1386
- this.target,
1387
- this.displacement,
1388
- [...this.rotations, {pivot, angles}],
1389
- this.context,
1390
- this.constraintId,
1391
- previous,
1392
- {kind: 'rotate', pivot},
1393
- );
1462
+ return new Transformation({pivot, angles}, previous);
1394
1463
  }
1395
- /** @internal */
1396
- chainArguments(): [
1397
- StoredConstraint['kind'],
1398
- AnchorReference,
1399
- AnchorReference,
1400
- Vec3 | undefined,
1401
- readonly ConstraintRotation[],
1402
- RelateContext | undefined,
1403
- string,
1404
- ConstraintExpression,
1405
- ] {
1406
- return [
1407
- this.kind,
1408
- this.source,
1409
- this.target,
1410
- this.displacement,
1411
- this.rotations,
1412
- this.context,
1413
- this.constraintId,
1414
- this,
1415
- ];
1464
+ }
1465
+
1466
+ function rotationPointReference(point: PointAnchor): AnchorReference {
1467
+ const reference = anchorReference(point);
1468
+ if (reference.kind !== 'point')
1469
+ throw new Error('pivotPoint() requires a point reference.');
1470
+ return reference;
1471
+ }
1472
+
1473
+ function rotationAxisReference(axis: LineAnchor): AnchorReference {
1474
+ const reference = anchorReference(axis);
1475
+ if (reference.kind !== 'line')
1476
+ throw new Error('axisLine() requires an axis or straight edge.');
1477
+ const geometry =
1478
+ reference.topology?.source[modelGeometry]()?.value ??
1479
+ (reference.whole && isModelObject(reference.model)
1480
+ ? reference.model[modelGeometry]()?.value
1481
+ : undefined);
1482
+ if (geometry) {
1483
+ const straight = reference.topology
1484
+ ? withTopologyShape(
1485
+ geometry.shape,
1486
+ geometry.topology,
1487
+ reference.topology.selection,
1488
+ shape => (shape as ReplicadEdge).geomType === 'LINE',
1489
+ )
1490
+ : (geometry.shape as ReplicadEdge).geomType === 'LINE';
1491
+ if (!straight)
1492
+ throw new Error(
1493
+ 'axisLine() requires a straight axis; curved edges do not define one rotation axis.',
1494
+ );
1416
1495
  }
1496
+ return reference;
1417
1497
  }
1418
1498
 
1419
- export class ConstraintPivotChain extends ConstraintExpression {
1499
+ export type Relation = Constraint | Transformation;
1500
+
1501
+ export class PivotRotation extends RelationExpression {
1420
1502
  /** @internal */
1421
1503
  constructor(
1422
- private readonly constraint: Constraint,
1423
- private readonly selection: Exclude<PivotSelection, {kind: 'around'}>,
1504
+ protected readonly chain: TransformationRotation,
1505
+ protected readonly selection: PointSelection,
1506
+ previous?: RelationExpression,
1424
1507
  ) {
1425
- super(...constraint.chainArguments());
1426
- this.spatialOperation = {kind: selection.kind, pivot: selection};
1508
+ super(...(previous ?? chain).continuationArguments(), {
1509
+ kind: selection.offset ? 'pivotOffset' : selection.kind,
1510
+ pivot: selection,
1511
+ });
1427
1512
  }
1428
1513
  /**
1429
1514
  * @code3d.param x {kind: 'angle', default: 0, label: 'Rotate X'}
1430
1515
  * @code3d.param y {kind: 'angle', default: 0, label: 'Rotate Y'}
1431
1516
  * @code3d.param z {kind: 'angle', default: 0, label: 'Rotate Z'}
1432
1517
  */
1433
- rotate(x: number, y: number, z: number): Constraint;
1434
- rotate(x = 0, y = 0, z = 0): Constraint {
1435
- return this.constraint.withRotation(this.selection, [x, y, z], this);
1518
+ rotate(x: number, y: number, z: number): Transformation;
1519
+ rotate(x = 0, y = 0, z = 0): Transformation {
1520
+ return this.chain.withRotation(this.selection, [x, y, z], this);
1521
+ }
1522
+ }
1523
+
1524
+ export class PivotChain extends PivotRotation {
1525
+ /**
1526
+ * Offset the selected pivot along self's local axes, retaining its reference.
1527
+ * @code3d.param x {kind: 'length', default: 0, label: 'Pivot ΔX'}
1528
+ * @code3d.param y {kind: 'length', default: 0, label: 'Pivot ΔY'}
1529
+ * @code3d.param z {kind: 'length', default: 0, label: 'Pivot ΔZ'}
1530
+ */
1531
+ pivotOffset(x: number, y: number, z: number): PivotRotation;
1532
+ pivotOffset(x = 0, y = 0, z = 0): PivotRotation {
1533
+ assertFiniteVector('pivotOffset', [x, y, z]);
1534
+ return new PivotRotation(
1535
+ this.chain,
1536
+ {...this.selection, offset: [x, y, z]},
1537
+ this,
1538
+ );
1436
1539
  }
1437
1540
  }
1438
- export class ConstraintAroundChain extends ConstraintExpression {
1541
+
1542
+ export class AxisRotation extends RelationExpression {
1439
1543
  /** @internal */
1440
1544
  constructor(
1441
- private readonly constraint: Constraint,
1442
- private readonly selection: Extract<PivotSelection, {kind: 'around'}>,
1545
+ protected readonly chain: TransformationRotation,
1546
+ protected readonly selection: AxisSelection,
1547
+ previous?: RelationExpression,
1443
1548
  ) {
1444
- super(...constraint.chainArguments());
1445
- this.spatialOperation = {kind: selection.kind, pivot: selection};
1549
+ super(...(previous ?? chain).continuationArguments(), {
1550
+ kind: selection.offset ? 'axisOffset' : selection.kind,
1551
+ pivot: selection,
1552
+ });
1446
1553
  }
1447
1554
  /** @code3d.param angle {kind: 'angle', default: 0, label: 'Rotate'} */
1448
- rotate(angle: number): Constraint;
1449
- rotate(angle = 0): Constraint {
1450
- return this.constraint.withRotation(this.selection, angle, this);
1555
+ rotate(angle: number): Transformation;
1556
+ rotate(angle = 0): Transformation {
1557
+ return this.chain.withRotation(this.selection, angle, this);
1558
+ }
1559
+ }
1560
+
1561
+ export class AxisChain extends AxisRotation {
1562
+ /**
1563
+ * Offset the selected axis in its reference frame, retaining its direction.
1564
+ * @code3d.param x {kind: 'length', default: 0, label: 'Axis ΔX'}
1565
+ * @code3d.param y {kind: 'length', default: 0, label: 'Axis ΔY'}
1566
+ * @code3d.param z {kind: 'length', default: 0, label: 'Axis ΔZ'}
1567
+ */
1568
+ axisOffset(x: number, y: number, z: number): AxisRotation;
1569
+ axisOffset(x = 0, y = 0, z = 0): AxisRotation {
1570
+ assertFiniteVector('axisOffset', [x, y, z]);
1571
+ return new AxisRotation(
1572
+ this.chain,
1573
+ {...this.selection, offset: [x, y, z]},
1574
+ this,
1575
+ );
1451
1576
  }
1452
1577
  }
1453
1578
 
1579
+ function selectedAxis(
1580
+ axis: RelationReference,
1581
+ offset: Vec3 | undefined,
1582
+ ): RigidTransform {
1583
+ return composeTransforms(axis.transform, translation(offset ?? origin));
1584
+ }
1585
+
1586
+ /**
1587
+ * Move the joint result along the fixed axes of its composition.
1588
+ * @code3d.param x {kind: 'length', default: 0, label: 'ΔX'}
1589
+ * @code3d.param y {kind: 'length', default: 0, label: 'ΔY'}
1590
+ * @code3d.param z {kind: 'length', default: 0, label: 'ΔZ'}
1591
+ */
1592
+ export function offset(x: number, y: number, z: number): Transformation;
1593
+ export function offset(x = 0, y = 0, z = 0): Transformation {
1594
+ assertFiniteVector('offset', [x, y, z]);
1595
+ return new Transformation({offset: [x, y, z]});
1596
+ }
1597
+ /**
1598
+ * Rotate about self's current origin and local X, Y, then Z axes.
1599
+ * @code3d.param x {kind: 'angle', default: 0, label: 'Rotate X'}
1600
+ * @code3d.param y {kind: 'angle', default: 0, label: 'Rotate Y'}
1601
+ * @code3d.param z {kind: 'angle', default: 0, label: 'Rotate Z'}
1602
+ */
1603
+ export function rotate(x: number, y: number, z: number): Transformation;
1604
+ export function rotate(x = 0, y = 0, z = 0): Transformation {
1605
+ assertFiniteVector('rotate', [x, y, z]);
1606
+ return new Transformation({
1607
+ pivot: {kind: 'pivot', point: origin, implicit: true},
1608
+ angles: [x, y, z],
1609
+ });
1610
+ }
1611
+ /**
1612
+ * Select a pivot in self's local coordinates for the next rotation.
1613
+ * @code3d.param x {kind: 'length', default: 0, label: 'Pivot X'}
1614
+ * @code3d.param y {kind: 'length', default: 0, label: 'Pivot Y'}
1615
+ * @code3d.param z {kind: 'length', default: 0, label: 'Pivot Z'}
1616
+ */
1617
+ export function pivot([x, y, z]: Vec3): PivotChain;
1618
+ export function pivot([x = 0, y = 0, z = 0]: Vec3 = origin): PivotChain {
1619
+ assertFiniteVector('pivot', [x, y, z]);
1620
+ return new PivotChain(new TransformationRotation(), {
1621
+ kind: 'pivot',
1622
+ point: [x, y, z],
1623
+ });
1624
+ }
1625
+ /** @code3d.param id {kind: 'vertex', label: 'Pivot vertex'} */
1626
+ export function pivotVertex(id: VertexId): PivotChain {
1627
+ assertTopologyId('vertex', id);
1628
+ return new PivotChain(new TransformationRotation(), {
1629
+ kind: 'pivotVertex',
1630
+ id,
1631
+ });
1632
+ }
1633
+ /** Select a point reference as the center; rotation axes remain self local. @code3d.tool */
1634
+ export function pivotPoint(point: PointAnchor): PivotChain {
1635
+ return new PivotChain(new TransformationRotation(), {
1636
+ kind: 'pivotPoint',
1637
+ point: rotationPointReference(point),
1638
+ });
1639
+ }
1640
+ /** @code3d.param id {kind: 'edge', label: 'Rotation edge'} */
1641
+ export function axisEdge(id: EdgeId): AxisChain {
1642
+ assertTopologyId('edge', id);
1643
+ return new AxisChain(new TransformationRotation(), {
1644
+ kind: 'axisEdge',
1645
+ id,
1646
+ });
1647
+ }
1648
+ /**
1649
+ * Select a positioned axis in the composition for the next rotation.
1650
+ * @code3d.tool
1651
+ */
1652
+ export function axisLine(axis: LineAnchor): AxisChain {
1653
+ return new AxisChain(new TransformationRotation(), {
1654
+ kind: 'axisLine',
1655
+ axis: rotationAxisReference(axis),
1656
+ });
1657
+ }
1658
+
1454
1659
  const modelGeometry = Symbol('modelGeometry');
1455
1660
  const referenceBounds = Symbol('referenceBounds');
1456
1661
  const referenceBoundsParts = Symbol('referenceBoundsParts');
1457
1662
  const modelSnapshotQueries = Symbol('modelSnapshotQueries');
1458
- const relationPreview = Symbol('relationPreview');
1663
+ const previewRelation = Symbol('previewRelation');
1459
1664
 
1460
1665
  type RelationObjectInit = Readonly<{
1461
1666
  nodeId?: string;
1462
- constraints?: readonly StoredConstraint[];
1667
+ placements?: readonly StoredPlacement[];
1463
1668
  sourceRefs?: readonly SourceRef[];
1464
1669
  parameters?: readonly ParameterUsage[];
1465
1670
  }>;
@@ -1469,11 +1674,17 @@ export abstract class RelationObject {
1469
1674
  readonly nodeId: string;
1470
1675
  /** @internal */
1471
1676
  abstract readonly name: string;
1472
- protected constraints: StoredConstraint[];
1677
+ protected placements: StoredPlacement[];
1678
+
1679
+ protected get constraints(): StoredConstraint[] {
1680
+ return this.placements.filter(
1681
+ (value): value is StoredConstraint => value.kind !== 'transformation',
1682
+ );
1683
+ }
1473
1684
 
1474
1685
  protected constructor(init: RelationObjectInit = {}) {
1475
1686
  this.nodeId = init.nodeId ?? `node-${nextNodeId++}`;
1476
- this.constraints = [...(init.constraints ?? [])];
1687
+ this.placements = [...(init.placements ?? [])];
1477
1688
  valueTraces.set(this, {
1478
1689
  sourceRefs: [...(init.sourceRefs ?? [])],
1479
1690
  parameters: [...(init.parameters ?? [])],
@@ -1490,6 +1701,24 @@ export abstract class RelationObject {
1490
1701
 
1491
1702
  protected abstract copyRelations(init: RelationObjectInit): RelationObject;
1492
1703
 
1704
+ protected edgeReference(_id: EdgeId): RelationReference {
1705
+ throw new Error(
1706
+ 'axisEdge() requires model topology. Use axisLine(lineRef) for a line reference.',
1707
+ );
1708
+ }
1709
+ private rotationAxis(pivot: AxisSelection): RelationReference {
1710
+ return pivot.kind === 'axisLine'
1711
+ ? pivot.axis
1712
+ : this.edgeReference(pivot.id);
1713
+ }
1714
+ private rotationPoint(pivot: PointSelection): Vec3 {
1715
+ return pivot.kind === 'pivot'
1716
+ ? pivot.point
1717
+ : pivot.kind === 'pivotVertex'
1718
+ ? this.vertexPosition(pivot.id)
1719
+ : pivot.point.transform.position;
1720
+ }
1721
+
1493
1722
  protected vertexPosition(_id: VertexId): Vec3 {
1494
1723
  throw new Error(
1495
1724
  'pivotVertex() requires model topology, not a sketch reference frame. Use pivot([x, y, z]) instead.',
@@ -1514,27 +1743,30 @@ export abstract class RelationObject {
1514
1743
  /** Store relations only after the complete callback has returned. */
1515
1744
  protected addRelations(
1516
1745
  original: RelationObject,
1517
- build: () => Constraint | readonly Constraint[],
1746
+ build: () => Relation | readonly Relation[],
1518
1747
  ): readonly RelationObject[] {
1519
1748
  const previous = activeRelate;
1520
1749
  activeRelate = {self: this, original};
1521
- let built: Constraint | readonly Constraint[];
1750
+ let built: Relation | readonly Relation[];
1522
1751
  try {
1523
1752
  built = build();
1524
1753
  } finally {
1525
1754
  activeRelate = previous;
1526
1755
  }
1527
- const constraints: readonly Constraint[] = Array.isArray(built)
1756
+ const relations: readonly Relation[] = Array.isArray(built)
1528
1757
  ? built
1529
- : [built as Constraint];
1530
- const stored = constraints.map(constraint => {
1531
- if (!(constraint instanceof Constraint))
1758
+ : [built as Relation];
1759
+ const stored = relations.map(constraint => {
1760
+ if (
1761
+ !(constraint instanceof Constraint) &&
1762
+ !(constraint instanceof Transformation)
1763
+ )
1532
1764
  throw new Error(
1533
- 'relate() requires a completed Constraint; finish pivot/around with rotate().',
1765
+ 'relate() requires a completed Constraint or Transformation; finish the pivot or axis selector with rotate().',
1534
1766
  );
1535
1767
  return constraint.storeFor(this, original);
1536
1768
  });
1537
- this.constraints.push(...stored);
1769
+ this.placements.push(...stored);
1538
1770
  return uniqueModels(stored.flatMap(constraintReferences));
1539
1771
  }
1540
1772
 
@@ -1643,27 +1875,37 @@ export abstract class RelationObject {
1643
1875
  };
1644
1876
  }
1645
1877
 
1646
- private bodyRotations(
1647
- constraint: StoredConstraint,
1878
+ private bodyActions(
1879
+ constraint: StoredTransformation,
1648
1880
  indices: Map<RelationObject, number>,
1649
- ): BodyRotation[] {
1650
- return constraint.rotations.map(({pivot, angles}) => {
1651
- if (pivot.kind === 'around') {
1652
- return pivot.axis.model
1881
+ ): BodyAction[] {
1882
+ return constraint.actions.map(action => {
1883
+ if ('offset' in action) return {offset: action.offset};
1884
+ const {pivot, angles} = action;
1885
+ if (isAxisSelection(pivot)) {
1886
+ const axis = this.rotationAxis(pivot);
1887
+ return axis.model
1653
1888
  ? {
1654
- body: indices.get(pivot.axis.model)!,
1655
- axis: pivot.axis.transform,
1656
- angle: (angles as number) * (pivot.axis.direction ?? 1),
1889
+ body: indices.get(axis.model)!,
1890
+ axis: selectedAxis(axis, pivot.offset),
1891
+ angle: (angles as number) * (axis.direction ?? 1),
1657
1892
  }
1658
1893
  : {
1659
1894
  local: axisRotation(
1660
- pivot.axis.transform,
1661
- (angles as number) * (pivot.axis.direction ?? 1),
1895
+ selectedAxis(axis, pivot.offset),
1896
+ (angles as number) * (axis.direction ?? 1),
1662
1897
  ),
1663
1898
  };
1664
1899
  }
1900
+ if (pivot.kind === 'pivotPoint' && pivot.point.model)
1901
+ return {
1902
+ body: indices.get(pivot.point.model)!,
1903
+ point: pivot.point.transform.position,
1904
+ rotation: rotationAround(origin, angles as Vec3).quaternion,
1905
+ displacement: pivot.offset ?? origin,
1906
+ };
1665
1907
  const frame = translation(
1666
- pivot.kind === 'pivot' ? pivot.point : this.vertexPosition(pivot.id),
1908
+ addVectors(this.rotationPoint(pivot), pivot.offset ?? origin),
1667
1909
  );
1668
1910
  return {
1669
1911
  local: composeTransforms(
@@ -1675,84 +1917,167 @@ export abstract class RelationObject {
1675
1917
  }
1676
1918
 
1677
1919
  /** @internal */
1678
- [relationPreview](
1679
- nodeId: string,
1680
- constraint: StoredConstraint,
1681
- selection: ConstraintSpatialSelection | undefined,
1682
- ): ConstraintPreview {
1683
- // This expression starts from the relate receiver, before sibling constraints
1684
- // are returned and committed to self. Geometry and node identity stay shared.
1920
+ [previewRelation](
1921
+ constraint: StoredPlacement | undefined,
1922
+ selection: TransformationSpatialSelection | undefined,
1923
+ preceding: readonly RelationExpression[] = [],
1924
+ original?: RelationObject,
1925
+ ): RelationPreview {
1926
+ // Keep inherited and sibling relations in the solve. The selected step
1927
+ // limits the placement prefix; geometry and node identity stay shared.
1928
+ let placements = [...(original ?? this).placements];
1929
+ const index = placements.findIndex(value => value.id === constraint?.id);
1930
+ if (index < 0) {
1931
+ placements.push(
1932
+ ...preceding.map(value =>
1933
+ value.storeFor(this, value.continuationArguments()[1]!.original),
1934
+ ),
1935
+ );
1936
+ if (constraint) placements.push(constraint);
1937
+ } else if (constraint) {
1938
+ placements[index] = constraint;
1939
+ let end = index + 1;
1940
+ if (constraint.kind !== 'transformation')
1941
+ while (
1942
+ end < placements.length &&
1943
+ placements[end].kind !== 'transformation'
1944
+ )
1945
+ end++;
1946
+ placements = placements.slice(0, end);
1947
+ }
1685
1948
  const owner = this.copyRelations({
1686
- nodeId,
1687
- constraints: [...this.constraints, constraint],
1949
+ nodeId: this.nodeId,
1950
+ placements,
1688
1951
  });
1689
1952
  const context = RelationObject.createSolveContext([
1690
1953
  owner,
1691
- ...(selection?.pivot.kind === 'around' && selection.pivot.axis.model
1692
- ? [selection.pivot.axis.model]
1954
+ ...(selection && selectionReference(selection.pivot)?.model
1955
+ ? [selectionReference(selection.pivot)!.model!]
1693
1956
  : []),
1694
1957
  ]);
1695
1958
  return {
1696
1959
  object: {
1697
- nodeId,
1960
+ nodeId: this.nodeId,
1698
1961
  compositionTransform: toTransform(owner.solvePose(context)),
1699
1962
  constraints: owner.constraints.map(value =>
1700
1963
  owner.constraintSnapshot(value, context),
1701
1964
  ),
1965
+ transformations: owner.transformationSnapshots(context),
1966
+ relationStages: owner.relationStageSnapshots(context),
1702
1967
  },
1703
- spatial: selection
1704
- ? owner.constraintSpatial(constraint, selection, context)
1705
- : undefined,
1968
+ spatial:
1969
+ selection && constraint?.kind === 'transformation'
1970
+ ? owner.relationSpatial(constraint, selection, context)
1971
+ : constraint?.kind === 'transformation' &&
1972
+ constraint.actions.at(-1) &&
1973
+ 'offset' in constraint.actions.at(-1)!
1974
+ ? owner.transformationOffsetSpatial(constraint, context)
1975
+ : undefined,
1706
1976
  };
1707
1977
  }
1708
1978
 
1709
- private constraintSpatial(
1710
- constraint: StoredConstraint,
1711
- selection: ConstraintSpatialSelection,
1979
+ private relationSpatial(
1980
+ constraint: StoredTransformation,
1981
+ selection: TransformationSpatialSelection,
1712
1982
  context: SolveContext,
1713
- ): ConstraintSpatialReference {
1983
+ actionIndex?: number,
1984
+ ): RelationSpatialReference {
1985
+ context = this.placementStageContext(constraint, context);
1714
1986
  const models = [...context.poses.keys()];
1715
- const actions = this.bodyRotations(
1716
- constraint,
1717
- new Map(models.map((model, i) => [model, i])),
1718
- );
1719
- const stage = actions.length - (selection.kind === 'rotate' ? 1 : 0);
1987
+ const actions = this.placementActions(constraint, context);
1988
+ const stage =
1989
+ actionIndex ??
1990
+ (selection.kind === 'rotate'
1991
+ ? constraint.actions.length -
1992
+ 1 -
1993
+ [...constraint.actions]
1994
+ .reverse()
1995
+ .findIndex(action => 'angles' in action)
1996
+ : actions.length);
1720
1997
  const finalPose = this.solvePose(context);
1721
- let before = finalPose;
1722
- for (let i = actions.length - 1; i >= stage; i--) {
1723
- const action = actions[i];
1724
- before =
1725
- 'local' in action
1726
- ? composeTransforms(before, invertTransform(action.local))
1727
- : composeTransforms(
1728
- axisRotation(
1729
- composeTransforms(
1730
- models[action.body].solvePose(context),
1731
- action.axis,
1732
- ),
1733
- -action.angle,
1734
- ),
1735
- before,
1736
- );
1737
- }
1998
+ const following = this.followingActions(constraint, context);
1999
+ const before = beforeTransformations(
2000
+ beforeTransformations(
2001
+ finalPose,
2002
+ {actions: following},
2003
+ models.map(model => model.solvePose(context)),
2004
+ ),
2005
+ {
2006
+ actions: actions.slice(stage),
2007
+ },
2008
+ models.map(model => model.solvePose(context)),
2009
+ );
1738
2010
  const pivot = selection.pivot;
2011
+ const axis = isAxisSelection(pivot) ? this.rotationAxis(pivot) : undefined;
1739
2012
  let frame: RigidTransform;
1740
- if (pivot.kind === 'around') {
2013
+ if (axis) {
1741
2014
  frame = composeTransforms(
1742
- pivot.axis.model ? pivot.axis.model.solvePose(context) : before,
1743
- (pivot.axis.direction ?? 1) === 1
1744
- ? pivot.axis.transform
1745
- : composeTransforms(pivot.axis.transform, rotation([1, 0, 0, 0])),
2015
+ axis.model ? axis.model.solvePose(context) : before,
2016
+ (axis.direction ?? 1) === 1
2017
+ ? selectedAxis(axis, pivot.offset)
2018
+ : composeTransforms(
2019
+ selectedAxis(axis, pivot.offset),
2020
+ rotation([1, 0, 0, 0]),
2021
+ ),
1746
2022
  );
2023
+ } else if (pivot.kind === 'pivotPoint' && pivot.point.model) {
2024
+ const center = composeTransforms(
2025
+ pivot.point.model.solvePose(context),
2026
+ pivot.point.transform,
2027
+ ).position;
2028
+ frame = {
2029
+ position: addVectors(
2030
+ center,
2031
+ rotateVector(pivot.offset ?? origin, before.quaternion),
2032
+ ),
2033
+ quaternion: before.quaternion,
2034
+ };
1747
2035
  } else {
1748
2036
  const local = translation(
1749
- pivot.kind === 'pivot' ? pivot.point : this.vertexPosition(pivot.id),
2037
+ addVectors(
2038
+ this.rotationPoint(pivot as PointSelection),
2039
+ pivot.offset ?? origin,
2040
+ ),
1750
2041
  );
1751
2042
  frame = composeTransforms(before, local);
1752
2043
  }
1753
- const angles = constraint.rotations[stage]?.angles ?? origin;
2044
+ const selected = constraint.actions[stage];
2045
+ const angles = selected && 'angles' in selected ? selected.angles : origin;
1754
2046
  const rotationVector: Vec3 =
1755
2047
  typeof angles === 'number' ? [0, angles, 0] : angles;
2048
+ // Later world-space actions move the earlier rotation's effective frame.
2049
+ // Later self-local actions already follow that rotation.
2050
+ const poses = models.map(model => model.solvePose(context));
2051
+ let actionPose = afterTransformations(
2052
+ before,
2053
+ {actions: actions.slice(stage, stage + 1)},
2054
+ poses,
2055
+ );
2056
+ const remainder = [...actions.slice(stage + 1), ...following];
2057
+ for (const action of remainder) {
2058
+ const next = afterTransformations(
2059
+ actionPose,
2060
+ {
2061
+ actions: [action],
2062
+ },
2063
+ poses,
2064
+ );
2065
+ if ('body' in action)
2066
+ frame = composeTransforms(
2067
+ composeTransforms(next, invertTransform(actionPose)),
2068
+ frame,
2069
+ );
2070
+ else if ('offset' in action) {
2071
+ frame = {
2072
+ ...frame,
2073
+ position: addVectors(
2074
+ frame.position,
2075
+ addVectors(next.position, negateVector(actionPose.position)),
2076
+ ),
2077
+ };
2078
+ }
2079
+ actionPose = next;
2080
+ }
1756
2081
  const localFrame = relativeTransform(frame, finalPose);
1757
2082
  return {
1758
2083
  nodeId: this.nodeId,
@@ -1762,30 +2087,289 @@ export abstract class RelationObject {
1762
2087
  vector:
1763
2088
  selection.kind === 'rotate'
1764
2089
  ? rotationVector
1765
- : pivot.kind === 'pivot'
1766
- ? pivot.point
1767
- : origin,
2090
+ : selection.kind === 'pivotOffset' ||
2091
+ selection.kind === 'axisOffset'
2092
+ ? (pivot.offset ?? origin)
2093
+ : pivot.kind === 'pivot'
2094
+ ? pivot.point
2095
+ : origin,
1768
2096
  frame: localFrame,
1769
2097
  rotation: rotationVector,
1770
- axisOnly: pivot.kind === 'around',
2098
+ axisOnly: isAxisSelection(pivot),
2099
+ reference: {
2100
+ ...(pivot.kind === 'axisEdge' || pivot.kind === 'pivotVertex'
2101
+ ? {kind: pivot.kind, id: pivot.id}
2102
+ : pivot.kind === 'axisLine' || pivot.kind === 'pivotPoint'
2103
+ ? {
2104
+ kind: pivot.kind,
2105
+ nodeId:
2106
+ selectionReference(pivot)!.model?.nodeId ?? this.nodeId,
2107
+ name: selectionReference(pivot)!.name,
2108
+ }
2109
+ : {kind: pivot.kind, point: pivot.point}),
2110
+ offset: pivot.offset ?? origin,
2111
+ offsetExplicit: pivot.offset !== undefined,
2112
+ explicit: !pivot.implicit,
2113
+ rotation:
2114
+ axis && (axis.direction ?? 1) !== 1
2115
+ ? [0, -rotationVector[1], 0]
2116
+ : rotationVector,
2117
+ frame:
2118
+ axis && (axis.direction ?? 1) !== 1
2119
+ ? composeTransforms(localFrame, rotation([1, 0, 0, 0]))
2120
+ : localFrame,
2121
+ },
1771
2122
  },
1772
2123
  };
1773
2124
  }
1774
2125
 
1775
- static createSolveContext(roots: readonly RelationObject[]): SolveContext {
2126
+ private placementActions(
2127
+ value: StoredTransformation,
2128
+ context: SolveContext,
2129
+ ): BodyAction[] {
2130
+ const indices = new Map(
2131
+ [...context.poses.keys()].map((model, i) => [model, i]),
2132
+ );
2133
+ return this.bodyActions(value, indices).map(action =>
2134
+ 'offset' in action
2135
+ ? {offset: rotateVector(action.offset, context.frame.quaternion)}
2136
+ : action,
2137
+ );
2138
+ }
2139
+
2140
+ /** Pose at the end of this segment; later constraints have their own input pose. */
2141
+ private placementStageContext(
2142
+ placement: StoredPlacement,
2143
+ context: SolveContext,
2144
+ ): SolveContext {
2145
+ const index = this.placements.indexOf(placement);
2146
+ let end = index + 1;
2147
+ if (placement.kind !== 'transformation')
2148
+ while (
2149
+ end < this.placements.length &&
2150
+ this.placements[end].kind !== 'transformation'
2151
+ )
2152
+ end++;
2153
+ while (
2154
+ end < this.placements.length &&
2155
+ this.placements[end].kind === 'transformation'
2156
+ )
2157
+ end++;
2158
+ if (end >= this.placements.length) return context;
2159
+ return transformSolveContext(
2160
+ RelationObject.createSolveContext(
2161
+ [this],
2162
+ new Map([[this, this.placements.slice(0, end)]]),
2163
+ ),
2164
+ context.frame,
2165
+ );
2166
+ }
2167
+
2168
+ private followingActions(
2169
+ placement: StoredPlacement,
2170
+ context: SolveContext,
2171
+ ): BodyAction[] {
2172
+ let start = this.placements.indexOf(placement) + 1;
2173
+ if (placement.kind !== 'transformation')
2174
+ while (
2175
+ start < this.placements.length &&
2176
+ this.placements[start].kind !== 'transformation'
2177
+ )
2178
+ start++;
2179
+ const values: StoredTransformation[] = [];
2180
+ while (
2181
+ start < this.placements.length &&
2182
+ this.placements[start].kind === 'transformation'
2183
+ )
2184
+ values.push(this.placements[start++] as StoredTransformation);
2185
+ return values.flatMap(value => this.placementActions(value, context));
2186
+ }
2187
+
2188
+ protected relationStageSnapshots(
2189
+ context: SolveContext,
2190
+ ): readonly RelationStageSnapshot[] | undefined {
2191
+ if (!this.placements.length) return undefined;
2192
+ const stages: RelationStageSnapshot[] = [];
2193
+ let start = 0;
2194
+ while (start < this.placements.length) {
2195
+ let end = start;
2196
+ while (
2197
+ end < this.placements.length &&
2198
+ this.placements[end].kind !== 'transformation'
2199
+ )
2200
+ end++;
2201
+ while (
2202
+ end < this.placements.length &&
2203
+ this.placements[end].kind === 'transformation'
2204
+ )
2205
+ end++;
2206
+ const values = this.placements.slice(start, end);
2207
+ const stage = this.placementStageContext(values[0], context);
2208
+ stages.push({
2209
+ constraintIds: values
2210
+ .filter(value => value.kind !== 'transformation')
2211
+ .map(value => value.id),
2212
+ transformationIds: values
2213
+ .filter(value => value.kind === 'transformation')
2214
+ .map(value => value.id),
2215
+ compositionTransform: toTransform(this.solvePose(stage)),
2216
+ offsetFrame: toTransform(stage.frame),
2217
+ });
2218
+ start = end;
2219
+ }
2220
+ return stages;
2221
+ }
2222
+
2223
+ protected transformationSnapshots(
2224
+ context: SolveContext,
2225
+ ): readonly TransformationSnapshot[] | undefined {
2226
+ const values = this.placements.filter(
2227
+ (value): value is StoredTransformation => value.kind === 'transformation',
2228
+ );
2229
+ if (!values.length) return undefined;
2230
+ return values.map(value =>
2231
+ this.transformationSnapshot(
2232
+ value,
2233
+ this.placementStageContext(value, context),
2234
+ ),
2235
+ );
2236
+ }
2237
+
2238
+ private followingReferenceFrame(
2239
+ frame: RigidTransform,
2240
+ actions: readonly BodyAction[],
2241
+ context: SolveContext,
2242
+ ): RigidTransform {
2243
+ const models = [...context.poses.keys()];
2244
+ const poses = models.map(model => model.solvePose(context));
2245
+ let pose = beforeTransformations(this.solvePose(context), {actions}, poses);
2246
+ for (const action of actions) {
2247
+ const next = afterTransformations(pose, {actions: [action]}, poses);
2248
+ if ('body' in action)
2249
+ frame = composeTransforms(
2250
+ composeTransforms(next, invertTransform(pose)),
2251
+ frame,
2252
+ );
2253
+ pose = next;
2254
+ }
2255
+ return frame;
2256
+ }
2257
+
2258
+ private transformationSnapshot(
2259
+ value: StoredTransformation,
2260
+ context: SolveContext,
2261
+ ): TransformationSnapshot {
2262
+ const models = [...context.poses.keys()];
2263
+ const actions = this.placementActions(value, context);
2264
+ const following = this.followingActions(value, context);
2265
+ const offsets = value.actions.flatMap((action, index) => {
2266
+ if (!('offset' in action)) return [];
2267
+ const frame = this.followingReferenceFrame(
2268
+ context.frame,
2269
+ [...actions.slice(index + 1), ...following],
2270
+ context,
2271
+ );
2272
+ return [
2273
+ {
2274
+ value: action.offset,
2275
+ frame: toTransform(frame),
2276
+ sourceRefs: valueTrace(action).sourceRefs,
2277
+ },
2278
+ ];
2279
+ });
2280
+ const rotations = value.actions.flatMap((action, index) =>
2281
+ 'pivot' in action
2282
+ ? [
2283
+ {
2284
+ spatial: this.relationSpatial(
2285
+ value,
2286
+ {kind: 'rotate', pivot: action.pivot},
2287
+ context,
2288
+ index,
2289
+ ).spatial,
2290
+ sourceRefs: valueTrace(action).sourceRefs,
2291
+ },
2292
+ ]
2293
+ : [],
2294
+ );
2295
+ return {
2296
+ id: value.id,
2297
+ offsets,
2298
+ rotations,
2299
+ offsetFrame: toTransform(context.frame),
2300
+ sourceRefs: [...valueTrace(value).sourceRefs],
2301
+ parameters: [...valueTrace(value).parameters],
2302
+ };
2303
+ }
2304
+
2305
+ private transformationOffsetSpatial(
2306
+ value: StoredTransformation,
2307
+ context: SolveContext,
2308
+ ): RelationSpatialReference {
2309
+ const offset = this.transformationSnapshot(value, context).offsets.at(-1)!;
2310
+ const local = relativeTransform(
2311
+ {...offset.frame, position: this.solvePose(context).position},
2312
+ this.solvePose(context),
2313
+ );
2314
+ return {
2315
+ nodeId: this.nodeId,
2316
+ kind: 'offset',
2317
+ spatial: {origin, vector: offset.value, frame: local},
2318
+ };
2319
+ }
2320
+
2321
+ static createSolveContext(
2322
+ roots: readonly RelationObject[],
2323
+ overrides = new Map<RelationObject, readonly StoredPlacement[]>(),
2324
+ ): SolveContext {
1776
2325
  const models: RelationObject[] = [];
1777
2326
  const indices = new Map<RelationObject, number>();
1778
2327
  const collect = (model: RelationObject): void => {
1779
2328
  if (indices.has(model)) return;
1780
2329
  indices.set(model, models.length);
1781
2330
  models.push(model);
1782
- model.constraints.flatMap(constraintReferences).forEach(collect);
2331
+ (overrides.get(model) ?? model.placements)
2332
+ .flatMap(constraintReferences)
2333
+ .forEach(collect);
1783
2334
  };
1784
2335
  roots.forEach(collect);
2336
+ const programs = models.map(model => {
2337
+ const placements = overrides.get(model) ?? model.placements;
2338
+ let last = placements.length - 1;
2339
+ while (last >= 0 && placements[last].kind === 'transformation') last--;
2340
+ let start = last;
2341
+ while (start > 0 && placements[start - 1].kind !== 'transformation')
2342
+ start--;
2343
+ const initial =
2344
+ start > 0
2345
+ ? RelationObject.createSolveContext(
2346
+ [model],
2347
+ new Map(overrides).set(model, placements.slice(0, start)),
2348
+ ).poses.get(model)!
2349
+ : identityRigidTransform;
2350
+ return {
2351
+ initial,
2352
+ constraints: placements
2353
+ .slice(Math.max(0, start), last + 1)
2354
+ .filter(
2355
+ (value): value is StoredConstraint =>
2356
+ value.kind !== 'transformation',
2357
+ ),
2358
+ transformations: placements
2359
+ .slice(last + 1)
2360
+ .filter(
2361
+ (value): value is StoredTransformation =>
2362
+ value.kind === 'transformation',
2363
+ )
2364
+ .flatMap(value => model.bodyActions(value, indices)),
2365
+ };
2366
+ });
1785
2367
  const poses = solveBodies(
1786
- models.map((model): Body => ({
2368
+ models.map((model, index): Body => ({
2369
+ initial: programs[index].initial,
2370
+ transformations: programs[index].transformations,
1787
2371
  name: model.name,
1788
- relations: model.constraints.map(constraint =>
2372
+ relations: programs[index].constraints.map(constraint =>
1789
2373
  constraint.kind === 'align'
1790
2374
  ? {
1791
2375
  kind: 'align',
@@ -1804,8 +2388,6 @@ export abstract class RelationObject {
1804
2388
  ).alignmentGeometry(constraint.target),
1805
2389
  transform: constraint.target.transform,
1806
2390
  },
1807
- offset: constraint.offset,
1808
- rotations: model.bodyRotations(constraint, indices),
1809
2391
  }
1810
2392
  : {
1811
2393
  kind: 'on',
@@ -1824,14 +2406,13 @@ export abstract class RelationObject {
1824
2406
  transform: constraint.target.transform,
1825
2407
  facing: constraint.target.bound!.facing,
1826
2408
  },
1827
- offset: constraint.offset,
1828
- rotations: model.bodyRotations(constraint, indices),
1829
2409
  },
1830
2410
  ),
1831
2411
  })),
1832
2412
  );
1833
2413
  return {
1834
2414
  poses: new Map(models.map((model, index) => [model, poses[index]])),
2415
+ frame: identityRigidTransform,
1835
2416
  };
1836
2417
  }
1837
2418
 
@@ -1839,56 +2420,14 @@ export abstract class RelationObject {
1839
2420
  constraint: StoredConstraint,
1840
2421
  context: SolveContext,
1841
2422
  ): ConstraintSnapshot {
1842
- const lastRotation = constraint.rotations.at(-1);
1843
- const rotationSpatial = lastRotation
1844
- ? this.constraintSpatial(
1845
- constraint,
1846
- {kind: 'rotate', pivot: lastRotation.pivot},
1847
- context,
1848
- ).spatial
1849
- : undefined;
1850
2423
  const source = constraint.source.model ?? this;
1851
2424
  const target = constraint.target.model ?? this;
1852
- const models = [...context.poses.keys()];
1853
- const actions = this.bodyRotations(
1854
- constraint,
1855
- new Map(models.map((model, i) => [model, i])),
1856
- );
1857
- let before = this.solvePose(context);
1858
- for (const action of [...actions].reverse()) {
1859
- before =
1860
- 'local' in action
1861
- ? composeTransforms(before, invertTransform(action.local))
1862
- : composeTransforms(
1863
- axisRotation(
1864
- composeTransforms(
1865
- models[action.body].solvePose(context),
1866
- action.axis,
1867
- ),
1868
- -action.angle,
1869
- ),
1870
- before,
1871
- );
1872
- }
1873
- const sourcePose = source === this ? before : source.solvePose(context);
1874
- const targetPose = target === this ? before : target.solvePose(context);
1875
- let offsetFrame = composeTransforms(
1876
- targetPose,
2425
+ const sourcePose = source.solvePose(context);
2426
+ const contactFrame = composeTransforms(
2427
+ target.solvePose(context),
1877
2428
  constraint.target.transform,
1878
2429
  );
1879
2430
  if (constraint.kind === 'align') {
1880
- for (const action of actions)
1881
- if ('body' in action)
1882
- offsetFrame = composeTransforms(
1883
- axisRotation(
1884
- composeTransforms(
1885
- models[action.body].solvePose(context),
1886
- action.axis,
1887
- ),
1888
- action.angle,
1889
- ),
1890
- offsetFrame,
1891
- );
1892
2431
  return {
1893
2432
  id: constraint.id,
1894
2433
  kind: 'align',
@@ -1896,16 +2435,12 @@ export abstract class RelationObject {
1896
2435
  target: anchorSnapshot(target, constraint.target),
1897
2436
  sourceElement: source.relationElement(constraint.source),
1898
2437
  targetElement: target.relationElement(constraint.target),
1899
- offsetDirection: 1,
1900
- offset: constraint.offset ?? origin,
1901
- offsetFrame: toTransform(offsetFrame),
1902
- rotation: rotationSpatial,
1903
2438
  sourceRefs: [...valueTrace(constraint).sourceRefs],
1904
2439
  parameters: [...valueTrace(constraint).parameters],
1905
2440
  };
1906
2441
  }
1907
2442
  const orientation = composeTransforms(
1908
- invertTransform(rotation(offsetFrame.quaternion)),
2443
+ invertTransform(rotation(contactFrame.quaternion)),
1909
2444
  rotation(sourcePose.quaternion),
1910
2445
  ).quaternion;
1911
2446
  const bounds = source[referenceBounds](
@@ -1921,9 +2456,9 @@ export abstract class RelationObject {
1921
2456
  {
1922
2457
  position: addVectors(
1923
2458
  sourcePose.position,
1924
- rotateVector(center, offsetFrame.quaternion),
2459
+ rotateVector(center, contactFrame.quaternion),
1925
2460
  ),
1926
- quaternion: offsetFrame.quaternion,
2461
+ quaternion: contactFrame.quaternion,
1927
2462
  },
1928
2463
  sourcePose,
1929
2464
  );
@@ -1932,19 +2467,6 @@ export abstract class RelationObject {
1932
2467
  bounds[1][1] - bounds[0][1],
1933
2468
  bounds[1][2] - bounds[0][2],
1934
2469
  ];
1935
- for (const action of actions) {
1936
- if ('body' in action)
1937
- offsetFrame = composeTransforms(
1938
- axisRotation(
1939
- composeTransforms(
1940
- models[action.body].solvePose(context),
1941
- action.axis,
1942
- ),
1943
- action.angle,
1944
- ),
1945
- offsetFrame,
1946
- );
1947
- }
1948
2470
  return {
1949
2471
  id: constraint.id,
1950
2472
  kind: constraint.kind,
@@ -1978,10 +2500,6 @@ export abstract class RelationObject {
1978
2500
  transform: toTransform(constraint.target.transform),
1979
2501
  bound: constraint.target.bound,
1980
2502
  },
1981
- offsetDirection: source === this ? 1 : -1,
1982
- offset: constraint.offset ?? origin,
1983
- offsetFrame: toTransform(offsetFrame),
1984
- rotation: rotationSpatial,
1985
2503
  sourceRefs: [...valueTrace(constraint).sourceRefs],
1986
2504
  parameters: [...valueTrace(constraint).parameters],
1987
2505
  };
@@ -1997,7 +2515,7 @@ export class SketchFrame extends RelationObject {
1997
2515
 
1998
2516
  constructor(source?: SketchFrame, init: RelationObjectInit = {}) {
1999
2517
  super({
2000
- constraints: source?.constraints,
2518
+ placements: source?.placements,
2001
2519
  sourceRefs: source?.sourceRefs,
2002
2520
  parameters: source?.parameters,
2003
2521
  ...init,
@@ -2016,7 +2534,7 @@ export class SketchFrame extends RelationObject {
2016
2534
  relatedObjects(): readonly RelationObject[] {
2017
2535
  return [
2018
2536
  ...(this.source ? [this.source] : []),
2019
- ...this.constraints.flatMap(constraintReferences),
2537
+ ...this.placements.flatMap(constraintReferences),
2020
2538
  ];
2021
2539
  }
2022
2540
 
@@ -2072,7 +2590,7 @@ export class SketchFrame extends RelationObject {
2072
2590
  }
2073
2591
 
2074
2592
  relate(
2075
- build: (frame: SketchFrame) => Constraint | readonly Constraint[],
2593
+ build: (frame: SketchFrame) => Relation | readonly Relation[],
2076
2594
  ): SketchFrame {
2077
2595
  const related = new SketchFrame(this);
2078
2596
  related.addRelations(this, () => build(related));
@@ -2080,10 +2598,10 @@ export class SketchFrame extends RelationObject {
2080
2598
  }
2081
2599
 
2082
2600
  face(region: SketchRegion): FaceModel {
2083
- return sketchFaceModel(region, this.constraints, this);
2601
+ return sketchFaceModel(region, this.placements, this);
2084
2602
  }
2085
2603
 
2086
- snapshot(): ConstraintPreview['object'] {
2604
+ snapshot(): RelationPreview['object'] {
2087
2605
  const context = RelationObject.createSolveContext([this]);
2088
2606
  return {
2089
2607
  nodeId: this.nodeId,
@@ -2091,6 +2609,8 @@ export class SketchFrame extends RelationObject {
2091
2609
  constraints: this.constraints.map(constraint =>
2092
2610
  this.constraintSnapshot(constraint, context),
2093
2611
  ),
2612
+ transformations: this.transformationSnapshots(context),
2613
+ relationStages: this.relationStageSnapshots(context),
2094
2614
  };
2095
2615
  }
2096
2616
 
@@ -2254,7 +2774,7 @@ export class ModelObject<
2254
2774
  relate(
2255
2775
  build: (
2256
2776
  self: RuntimeModel<Elements, Kind>,
2257
- ) => Constraint | readonly Constraint[],
2777
+ ) => Relation | readonly Relation[],
2258
2778
  ): RuntimeModel<Elements, Kind> {
2259
2779
  const operation = storedOperation('relate', [
2260
2780
  {model: this, role: 'source', index: 0},
@@ -2518,7 +3038,7 @@ export class ModelObject<
2518
3038
  transformElement(element, transform),
2519
3039
  ]),
2520
3040
  ),
2521
- constraints: this.mapConstraintGeometry(
3041
+ placements: this.mapConstraintGeometry(
2522
3042
  element => transformElement(element, transform),
2523
3043
  point => composeTransforms(transform, translation(point)).position,
2524
3044
  ),
@@ -2556,23 +3076,52 @@ export class ModelObject<
2556
3076
  private mapConstraintGeometry(
2557
3077
  mapElement: (element: StoredElement) => StoredElement,
2558
3078
  mapPoint: (point: Vec3) => Vec3,
2559
- ): StoredConstraint[] {
3079
+ ): StoredPlacement[] {
2560
3080
  const mapReference = (reference: RelationReference): RelationReference =>
2561
3081
  reference.model ? reference : {...reference, ...mapElement(reference)};
2562
- return this.constraints.map(constraint => {
2563
- const mapped: StoredConstraint = {
3082
+ const zero = mapPoint(origin);
3083
+ const mapVector = (vector: Vec3): Vec3 =>
3084
+ mapPoint(vector).map(
3085
+ (value, index) => value - zero[index],
3086
+ ) as unknown as Vec3;
3087
+ const scale = Math.hypot(...mapVector([1, 0, 0]));
3088
+ return this.placements.map(constraint => {
3089
+ const mapped: StoredPlacement = {
2564
3090
  ...constraint,
2565
- source: mapReference(constraint.source),
2566
- target: mapReference(constraint.target),
2567
- rotations: constraint.rotations.map(action => ({
2568
- ...action,
2569
- pivot:
2570
- action.pivot.kind === 'around'
2571
- ? {...action.pivot, axis: mapReference(action.pivot.axis)}
2572
- : action.pivot.kind === 'pivot'
2573
- ? {...action.pivot, point: mapPoint(action.pivot.point)}
2574
- : action.pivot,
2575
- })),
3091
+ ...(constraint.kind === 'transformation'
3092
+ ? {}
3093
+ : {
3094
+ source: mapReference(constraint.source),
3095
+ target: mapReference(constraint.target),
3096
+ }),
3097
+ ...(constraint.kind === 'transformation'
3098
+ ? {
3099
+ actions: constraint.actions.map(action => {
3100
+ if ('offset' in action) return action;
3101
+ const mappedAction = {
3102
+ ...action,
3103
+ pivot:
3104
+ action.pivot.kind === 'pivot'
3105
+ ? {...action.pivot, point: mapPoint(action.pivot.point)}
3106
+ : mapSelectionReference(action.pivot, mapReference),
3107
+ };
3108
+ if (action.pivot.offset) {
3109
+ mappedAction.pivot = {
3110
+ ...mappedAction.pivot,
3111
+ offset: isAxisSelection(action.pivot)
3112
+ ? selectionReference(action.pivot)?.model
3113
+ ? action.pivot.offset
3114
+ : (action.pivot.offset.map(
3115
+ value => value * scale,
3116
+ ) as unknown as Vec3)
3117
+ : mapVector(action.pivot.offset),
3118
+ };
3119
+ }
3120
+ valueTraces.set(mappedAction, valueTrace(action));
3121
+ return mappedAction;
3122
+ }),
3123
+ }
3124
+ : {}),
2576
3125
  };
2577
3126
  valueTraces.set(mapped, valueTrace(constraint));
2578
3127
  return mapped;
@@ -2605,7 +3154,7 @@ export class ModelObject<
2605
3154
  {
2606
3155
  geometryAnchor: scaleElement(this.geometryAnchor, factor),
2607
3156
  elements: scaleElements(this.elements, factor),
2608
- constraints: this.mapConstraintGeometry(
3157
+ placements: this.mapConstraintGeometry(
2609
3158
  element => scaleElement(element, factor),
2610
3159
  point => scaleFrame(translation(point), factor).position,
2611
3160
  ),
@@ -2644,7 +3193,7 @@ export class ModelObject<
2644
3193
  name: 'Extrude',
2645
3194
  geometry,
2646
3195
  material: this.materialSnapshot,
2647
- constraints: this.constraints,
3196
+ placements: this.placements,
2648
3197
  sourceRefs: this.sourceRefs,
2649
3198
  parameters: this.allParameters(),
2650
3199
  meshTolerance: this.meshTolerance,
@@ -2834,7 +3383,7 @@ export class ModelObject<
2834
3383
  return [
2835
3384
  ...this.children,
2836
3385
  ...this.operation.inputs.map(input => input.model),
2837
- ...this.constraints.flatMap(constraintReferences),
3386
+ ...this.placements.flatMap(constraintReferences),
2838
3387
  ];
2839
3388
  }
2840
3389
 
@@ -2859,7 +3408,7 @@ export class ModelObject<
2859
3408
  );
2860
3409
  const parameters = uniqueParameters([
2861
3410
  ...this.parameters,
2862
- ...this.constraints.flatMap(
3411
+ ...this.placements.flatMap(
2863
3412
  constraint => valueTrace(constraint).parameters,
2864
3413
  ),
2865
3414
  ]);
@@ -2871,6 +3420,8 @@ export class ModelObject<
2871
3420
  compositionTransform: toTransform(pose),
2872
3421
  transform: toTransform(inComposition ? pose : identityRigidTransform),
2873
3422
  constraints,
3423
+ transformations: this.transformationSnapshots(solveContext),
3424
+ relationStages: this.relationStageSnapshots(solveContext),
2874
3425
  origin,
2875
3426
  elements: snapshotElements({
2876
3427
  ...this.elements,
@@ -2982,7 +3533,7 @@ export class ModelObject<
2982
3533
  name: 'Loft',
2983
3534
  geometry,
2984
3535
  material: this.materialSnapshot,
2985
- constraints: this.constraints,
3536
+ placements: this.placements,
2986
3537
  sourceRefs: inputs.flatMap(input => input.sourceRefs),
2987
3538
  parameters: uniqueParameters(
2988
3539
  inputs.flatMap(input => input.allParameters()),
@@ -3017,7 +3568,7 @@ export class ModelObject<
3017
3568
  geometry: evaluation.geometry,
3018
3569
  name: this.name,
3019
3570
  material: this.materialSnapshot,
3020
- constraints: this.constraints,
3571
+ placements: this.placements,
3021
3572
  sourceRefs: [this, ...others].flatMap(model => model.sourceRefs),
3022
3573
  parameters: uniqueParameters(
3023
3574
  [this, ...others].flatMap(model => model.allParameters()),
@@ -3142,7 +3693,7 @@ export class ModelObject<
3142
3693
  private allParameters(): ParameterUsage[] {
3143
3694
  return uniqueParameters([
3144
3695
  ...this.parameters,
3145
- ...this.constraints.flatMap(
3696
+ ...this.placements.flatMap(
3146
3697
  constraint => valueTrace(constraint).parameters,
3147
3698
  ),
3148
3699
  ]);
@@ -3304,6 +3855,10 @@ export class ModelObject<
3304
3855
  );
3305
3856
  }
3306
3857
 
3858
+ protected override edgeReference(id: EdgeId): RelationReference {
3859
+ return {...rotationAxisReference(this.edge(id)), model: undefined};
3860
+ }
3861
+
3307
3862
  protected override vertexPosition(id: VertexId): Vec3 {
3308
3863
  return anchorReference(this.vertex(id)).transform.position;
3309
3864
  }
@@ -3312,17 +3867,17 @@ export class ModelObject<
3312
3867
  return this.copy(init, this.operation);
3313
3868
  }
3314
3869
 
3315
- /** Fix the assembly frame once, using only its direct member origins. */
3870
+ /** Fix the assembly frame once in the first member's solved local coordinates. */
3316
3871
  private static createAssembly(
3317
3872
  children: readonly ModelObject[],
3318
3873
  ): SolveContext {
3319
3874
  const context = ModelObject.createSolveContext(children);
3320
- const center = children.length
3321
- ? boundsCenter(
3322
- pointBounds(children.map(child => child.solvePose(context).position)),
3875
+ return children.length
3876
+ ? transformSolveContext(
3877
+ context,
3878
+ invertTransform(children[0].solvePose(context)),
3323
3879
  )
3324
- : origin;
3325
- return transformSolveContext(context, translation(negateVector(center)));
3880
+ : context;
3326
3881
  }
3327
3882
 
3328
3883
  private requireGeometry(): ModelGeometry {
@@ -3407,7 +3962,7 @@ export class ModelObject<
3407
3962
  material: this.materialSnapshot,
3408
3963
  children: this.children,
3409
3964
  assembly: this.assembly,
3410
- constraints: this.constraints,
3965
+ placements: this.placements,
3411
3966
  elements: this.elements,
3412
3967
  sourceRefs: this.sourceRefs,
3413
3968
  parameters: this.parameters,
@@ -3997,6 +4552,7 @@ function normalizeReplicadSolid(shape: Shape3D): Shape3D {
3997
4552
  return solid;
3998
4553
  }
3999
4554
 
4555
+ /** Compose members in the first member's local frame; empty groups use the default frame. */
4000
4556
  export function group(children: readonly Model[], name = 'Group'): GroupModel {
4001
4557
  const runtimeChildren = children.map(child =>
4002
4558
  requireModelObject(child, 'Every group child must be a model.'),
@@ -4104,20 +4660,40 @@ export function isConstraint(value: unknown): value is Constraint {
4104
4660
  return value instanceof Constraint;
4105
4661
  }
4106
4662
 
4107
- export function isConstraintExpression(
4663
+ export function isRelationExpression(
4108
4664
  value: unknown,
4109
- ): value is ConstraintExpression {
4110
- return value instanceof ConstraintExpression;
4665
+ ): value is RelationExpression {
4666
+ return value instanceof RelationExpression;
4667
+ }
4668
+
4669
+ export function relationPreview(
4670
+ value: RelationExpression,
4671
+ preceding: readonly RelationExpression[] = [],
4672
+ ): RelationPreview | undefined {
4673
+ return value.preview(preceding);
4111
4674
  }
4112
4675
 
4113
- export function constraintPreview(
4114
- value: ConstraintExpression,
4115
- ): ConstraintPreview | undefined {
4116
- return value.preview();
4676
+ /** The current authoring receiver remains available when a free selector throws. */
4677
+ export function currentRelationSelf(): RelationObject | undefined {
4678
+ return activeRelate?.self;
4117
4679
  }
4118
4680
 
4119
- export function instrumentConstraint(
4120
- constraint: ConstraintExpression,
4681
+ /** Preview an insertion/selector prefix, retaining only placements inherited by its callback. */
4682
+ export function relationSelectionPreview(
4683
+ self: RelationObject,
4684
+ preceding: readonly RelationExpression[],
4685
+ context?: RelationExpression,
4686
+ ): RelationPreview {
4687
+ return self[previewRelation](
4688
+ undefined,
4689
+ undefined,
4690
+ preceding,
4691
+ (preceding[0] ?? context)?.continuationArguments()[1]?.original,
4692
+ );
4693
+ }
4694
+
4695
+ export function instrumentRelation(
4696
+ constraint: RelationExpression,
4121
4697
  sourceRef: SourceRef,
4122
4698
  parameters: readonly ParameterUsage[],
4123
4699
  ): void {
@@ -4140,9 +4716,9 @@ export function instrumentModelOperation(
4140
4716
  );
4141
4717
  }
4142
4718
 
4143
- export function constraintTraceReference(
4144
- constraint: ConstraintExpression,
4145
- ): ConstraintTraceReference {
4719
+ export function relationTraceReference(
4720
+ constraint: RelationExpression,
4721
+ ): RelationTraceReference {
4146
4722
  return constraint.traceReference();
4147
4723
  }
4148
4724
 
@@ -4275,7 +4851,11 @@ export type SnapshotQueryBatch = Readonly<{
4275
4851
  weight: number;
4276
4852
  sourceRef?: SourceRef;
4277
4853
  encode(): Uint8Array;
4278
- accept(query: SnapshotQuery, value: SnapshotQueryResult): void;
4854
+ accept(
4855
+ query: SnapshotQuery,
4856
+ value: SnapshotQueryResult,
4857
+ milliseconds: number,
4858
+ ): void;
4279
4859
  }>;
4280
4860
 
4281
4861
  export function planModelSnapshotQueries(
@@ -4331,7 +4911,8 @@ export function planModelSnapshotQueries(
4331
4911
  (input.geometry.topology?.surfaces.ids.length ?? 0)) *
4332
4912
  queries.length,
4333
4913
  encode: () => encodeKernelArtifact(id, input.geometry),
4334
- accept: (query, value) => snapshotQuery.accept(query.key, value),
4914
+ accept: (query, value, milliseconds) =>
4915
+ snapshotQuery.accept(query.key, value, milliseconds),
4335
4916
  }));
4336
4917
  }
4337
4918
 
@@ -4341,7 +4922,11 @@ export function executeSnapshotQueryBatch(
4341
4922
  bytes: Uint8Array,
4342
4923
  queries: readonly SnapshotQuery[],
4343
4924
  checkCancelled: () => void,
4344
- onResult: (query: SnapshotQuery, value: SnapshotQueryResult) => void,
4925
+ onResult: (
4926
+ query: SnapshotQuery,
4927
+ value: SnapshotQueryResult,
4928
+ milliseconds: number,
4929
+ ) => void,
4345
4930
  onRestore?: (milliseconds: number) => void,
4346
4931
  ): void {
4347
4932
  const started = performance.now();
@@ -4353,7 +4938,9 @@ export function executeSnapshotQueryBatch(
4353
4938
  onRestore?.(performance.now() - started);
4354
4939
  for (const query of queries) {
4355
4940
  checkCancelled();
4356
- onResult(query, computeSnapshotQuery(geometry, query));
4941
+ const started = performance.now();
4942
+ const value = computeSnapshotQuery(geometry, query);
4943
+ onResult(query, value, performance.now() - started);
4357
4944
  }
4358
4945
  } finally {
4359
4946
  geometry.shape.delete();
@@ -4429,7 +5016,14 @@ export function retainModelGeometry(
4429
5016
  }
4430
5017
 
4431
5018
  export const authoringApi = Object.freeze({
4432
- cached,
5019
+ offset,
5020
+ rotate,
5021
+ pivot,
5022
+ pivotVertex,
5023
+ pivotPoint,
5024
+ axisEdge,
5025
+ axisLine,
5026
+ cache,
4433
5027
  font,
4434
5028
  googleFont,
4435
5029
  text,
@@ -4532,7 +5126,11 @@ const kernelShape = cachedArtifact(
4532
5126
  _inputs: readonly KernelArtifact<unknown>[],
4533
5127
  compute: () => AnyShape,
4534
5128
  ) => compute(),
4535
- {key: kernelOperationKey, lifecycle: shapeLifecycle},
5129
+ {
5130
+ key: (operation, arguments_, inputs) =>
5131
+ kernelOperationKey(`kernel-shape:${operation}`, arguments_, inputs),
5132
+ lifecycle: shapeLifecycle,
5133
+ },
4536
5134
  );
4537
5135
 
4538
5136
  function evaluateKernelShape<Shape extends AnyShape>(
@@ -4566,7 +5164,11 @@ const evaluateModelGeometry = cachedArtifact(
4566
5164
  referenceBasis: result.referenceBasis,
4567
5165
  };
4568
5166
  },
4569
- {key: kernelOperationKey, lifecycle: modelGeometryLifecycle},
5167
+ {
5168
+ key: (operation, arguments_, inputs) =>
5169
+ kernelOperationKey(`model-geometry:${operation}`, arguments_, inputs),
5170
+ lifecycle: modelGeometryLifecycle,
5171
+ },
4570
5172
  );
4571
5173
 
4572
5174
  function createModelGeometryValue(
@@ -4725,7 +5327,7 @@ type PlanarSketch = Readonly<{
4725
5327
  /** Internal bridge from the kernel-independent sketch definition to model geometry. */
4726
5328
  function sketchFaceModel(
4727
5329
  region: SketchRegion,
4728
- constraints: readonly StoredConstraint[],
5330
+ placements: readonly StoredPlacement[],
4729
5331
  source: SketchFrame,
4730
5332
  ): FaceModel {
4731
5333
  const curves = [region.outer, ...region.holes].map(loop =>
@@ -4747,7 +5349,7 @@ function sketchFaceModel(
4747
5349
  return ModelObject.create<PlanarElements, 'face'>({
4748
5350
  kind: 'face',
4749
5351
  name: 'Sketch face',
4750
- constraints,
5352
+ placements,
4751
5353
  geometry,
4752
5354
  geometryAnchor: plane,
4753
5355
  elements: {plane},
@@ -4920,12 +5522,14 @@ function solidElements(bounds: LocalBounds): StoredElements {
4920
5522
  return {axis: {kind: 'line', transform: translation(boundsCenter(bounds))}};
4921
5523
  }
4922
5524
 
4923
- function constraintReferences(constraint: StoredConstraint): RelationObject[] {
5525
+ function constraintReferences(constraint: StoredPlacement): RelationObject[] {
4924
5526
  return [
4925
- constraint.source.model,
4926
- constraint.target.model,
4927
- ...constraint.rotations.map(action =>
4928
- action.pivot.kind === 'around' ? action.pivot.axis.model : undefined,
5527
+ ...(constraint.kind === 'transformation'
5528
+ ? []
5529
+ : [constraint.source.model, constraint.target.model]),
5530
+ ...(constraint.kind === 'transformation' ? constraint.actions : []).map(
5531
+ action =>
5532
+ 'pivot' in action ? selectionReference(action.pivot)?.model : undefined,
4929
5533
  ),
4930
5534
  ].filter((model): model is RelationObject => !!model);
4931
5535
  }