@code3d/core 0.0.1-alpha.3 → 0.0.1-alpha.6

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 (40) hide show
  1. package/README.md +31 -3
  2. package/bld/chunks/{chunk-U5G2VU2I.js → chunk-3RIMOCPP.js} +2 -2
  3. package/bld/chunks/{chunk-P5VGKAEM.js → chunk-KYW4AWKO.js} +2 -2
  4. package/bld/chunks/{chunk-RPY2WVCB.js → chunk-T3RAUNSF.js} +941 -506
  5. package/bld/chunks/chunk-T3RAUNSF.js.map +7 -0
  6. package/bld/chunks/{chunk-BF55AIEQ.js → chunk-T6COR2SG.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/index.d.ts +2 -2
  10. package/bld/library/index.d.ts.map +1 -1
  11. package/bld/library/index.js +15 -1
  12. package/bld/library/kernel-cache.d.ts +2 -1
  13. package/bld/library/kernel-cache.d.ts.map +1 -1
  14. package/bld/library/relation-solver.d.ts +12 -7
  15. package/bld/library/relation-solver.d.ts.map +1 -1
  16. package/bld/library/replicad.js +2 -2
  17. package/bld/library/runtime.d.ts +214 -76
  18. package/bld/library/runtime.d.ts.map +1 -1
  19. package/bld/library/sketch.d.ts +2 -2
  20. package/bld/library/sketch.d.ts.map +1 -1
  21. package/bld/library/topology.d.ts +2 -0
  22. package/bld/library/topology.d.ts.map +1 -1
  23. package/bld/node/index.js +17 -3
  24. package/bld/node/replicad.js +4 -4
  25. package/bld/tooling/index.d.ts +3 -3
  26. package/bld/tooling/index.d.ts.map +1 -1
  27. package/bld/tooling/index.js +16 -10
  28. package/package.json +1 -1
  29. package/src/library/bound-solver.ts +103 -73
  30. package/src/library/index.ts +13 -2
  31. package/src/library/kernel-cache.ts +8 -0
  32. package/src/library/relation-solver.ts +194 -110
  33. package/src/library/runtime.ts +1029 -424
  34. package/src/library/sketch.ts +3 -3
  35. package/src/library/topology.ts +13 -2
  36. package/src/tooling/index.ts +14 -7
  37. package/bld/chunks/chunk-RPY2WVCB.js.map +0 -7
  38. /package/bld/chunks/{chunk-U5G2VU2I.js.map → chunk-3RIMOCPP.js.map} +0 -0
  39. /package/bld/chunks/{chunk-P5VGKAEM.js.map → chunk-KYW4AWKO.js.map} +0 -0
  40. /package/bld/chunks/{chunk-BF55AIEQ.js.map → chunk-T6COR2SG.js.map} +0 -0
@@ -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,9 +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
186
  sourceRefs: readonly SourceRef[];
186
187
  parameters: readonly ParameterUsage[];
187
188
  }> &
@@ -290,6 +291,26 @@ export type ModelParameterDimension = Readonly<{
290
291
  vector: Vec3;
291
292
  }>;
292
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
+
293
314
  /** Local geometry coordinates; vector is the authored coordinates, offset, or angles. */
294
315
  export type ModelSpatialOperation = Readonly<{
295
316
  origin: Vec3;
@@ -298,20 +319,34 @@ export type ModelSpatialOperation = Readonly<{
298
319
  frame?: RigidTransform;
299
320
  rotation?: Vec3;
300
321
  axisOnly?: boolean;
322
+ reference?: RotationReferenceSnapshot;
301
323
  }>;
302
324
 
303
- export type ConstraintSpatialReference = Readonly<{
325
+ export type RelationSpatialReference = Readonly<{
304
326
  nodeId: string;
305
- 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';
306
337
  spatial: ModelSpatialOperation;
307
338
  }>;
308
339
 
309
- export type ConstraintPreview = Readonly<{
340
+ export type RelationPreview = Readonly<{
310
341
  object: Pick<
311
342
  ModelSnapshotObject,
312
- 'nodeId' | 'compositionTransform' | 'constraints'
343
+ | 'nodeId'
344
+ | 'compositionTransform'
345
+ | 'constraints'
346
+ | 'transformations'
347
+ | 'relationStages'
313
348
  >;
314
- spatial?: ConstraintSpatialReference;
349
+ spatial?: RelationSpatialReference;
315
350
  }>;
316
351
 
317
352
  export type RenderMesh = Readonly<{
@@ -334,6 +369,8 @@ export type RenderMesh = Readonly<{
334
369
  start: number;
335
370
  count: number;
336
371
  edgeId: EdgeId;
372
+ /** True for native straight edges, suitable as rotation references. */
373
+ linear?: boolean;
337
374
  }>[];
338
375
  }>;
339
376
 
@@ -348,6 +385,8 @@ export type ModelSnapshotObject = Readonly<{
348
385
  /** Placement in this snapshot tree; a root value uses model-local coordinates. */
349
386
  transform: Transform;
350
387
  constraints: readonly ConstraintSnapshot[];
388
+ transformations?: readonly TransformationSnapshot[];
389
+ relationStages?: readonly RelationStageSnapshot[];
351
390
  elements: readonly ElementSnapshot[];
352
391
  origin: Vec3;
353
392
  sourceRefs: readonly SourceRef[];
@@ -401,12 +440,21 @@ export type ModelTopologyReference = Readonly<{
401
440
  TopologySelection;
402
441
 
403
442
  export type ConstraintTraceReference = Readonly<{
443
+ kind: 'constraint';
404
444
  constraintId: string;
405
445
  source: RelationObject;
406
446
  target: RelationObject;
407
447
  self?: RelationObject;
408
448
  }>;
409
449
 
450
+ export type RelationTraceReference =
451
+ | ConstraintTraceReference
452
+ | Readonly<{
453
+ kind: 'transformation';
454
+ transformationId: string;
455
+ self?: RelationObject;
456
+ }>;
457
+
410
458
  export type ModelOperationInstrumentation = Readonly<{
411
459
  siteId: string;
412
460
  execution: number;
@@ -423,27 +471,93 @@ export type ModelObjectRuntimeInfo = Readonly<{
423
471
  sourceRefs: readonly SourceRef[];
424
472
  }>;
425
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
+
426
512
  type StoredConstraint = Readonly<{
427
513
  id: string;
428
514
  kind: 'on' | 'align';
429
515
  source: RelationReference;
430
516
  target: RelationReference;
431
- rotations: readonly ConstraintRotation[];
432
- offset: Vec3 | undefined;
433
517
  }>;
434
518
 
435
519
  // An omitted model refers to relate's self, including after immutable copies.
436
520
  type RelationReference = StoredAnchor & Readonly<{model?: RelationObject}>;
437
- type PivotSelection =
521
+ type PivotSelection = (
438
522
  | Readonly<{kind: 'pivot'; point: Vec3}>
439
523
  | Readonly<{kind: 'pivotVertex'; id: VertexId}>
440
- | Readonly<{kind: 'around'; axis: RelationReference}>;
441
- 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<{
442
554
  pivot: PivotSelection;
443
555
  angles: Vec3 | number;
444
556
  }>;
445
- type ConstraintSpatialSelection = Readonly<{
446
- kind: ConstraintSpatialReference['kind'];
557
+ type TransformationAction =
558
+ TransformationRotationAction | Readonly<{offset: Vec3}>;
559
+ type TransformationSpatialSelection = Readonly<{
560
+ kind: RelationSpatialReference['kind'];
447
561
  pivot: PivotSelection;
448
562
  }>;
449
563
  type RelateContext = Readonly<{self: RelationObject; original: RelationObject}>;
@@ -527,7 +641,7 @@ type ModelObjectInit<Kind extends ModelKind = ModelKind> = Readonly<{
527
641
  material?: ModelMaterialSnapshot;
528
642
  children?: readonly ModelObject[];
529
643
  assembly?: SolveContext;
530
- constraints?: readonly StoredConstraint[];
644
+ placements?: readonly StoredPlacement[];
531
645
  elements?: StoredElements;
532
646
  sourceRefs?: readonly SourceRef[];
533
647
  parameters?: readonly ParameterUsage[];
@@ -558,6 +672,7 @@ type SolidGeometry = KernelArtifact<
558
672
 
559
673
  type SolveContext = {
560
674
  poses: ReadonlyMap<RelationObject, RigidTransform>;
675
+ frame: RigidTransform;
561
676
  };
562
677
 
563
678
  function transformSolveContext(
@@ -565,6 +680,7 @@ function transformSolveContext(
565
680
  transform: RigidTransform,
566
681
  ): SolveContext {
567
682
  return {
683
+ frame: composeTransforms(transform, context.frame),
568
684
  poses: new Map(
569
685
  [...context.poses].map(([model, pose]) => [
570
686
  model,
@@ -738,7 +854,7 @@ export interface ModelCapabilities<
738
854
  relate(
739
855
  build: (
740
856
  self: ModelForKind<Elements, Kind>,
741
- ) => Constraint | readonly Constraint[],
857
+ ) => Relation | readonly Relation[],
742
858
  ): ModelForKind<Elements, Kind>;
743
859
  expose<const Sources extends ElementSources>(
744
860
  sources: Sources,
@@ -1125,7 +1241,7 @@ export function modelTopologyIds(
1125
1241
  : undefined;
1126
1242
  const source = isModelObject(value)
1127
1243
  ? value
1128
- : value instanceof ConstraintExpression
1244
+ : value instanceof RelationExpression
1129
1245
  ? value.self()
1130
1246
  : topology?.source;
1131
1247
  const geometry = isModelObject(source)
@@ -1140,24 +1256,14 @@ export function modelTopologyIds(
1140
1256
  );
1141
1257
  }
1142
1258
 
1143
- /** Traceable relation values, including unfinished pivot and axis selections. */
1144
- export abstract class ConstraintExpression {
1145
- private get sourceRefs(): SourceRef[] {
1146
- return valueTrace(this).sourceRefs;
1147
- }
1148
- private get parameters(): ParameterUsage[] {
1149
- return valueTrace(this).parameters;
1150
- }
1151
- protected spatialOperation?: ConstraintSpatialSelection;
1259
+ /** Traceable placement values, including unfinished pivot and axis selections. */
1260
+ export abstract class RelationExpression {
1152
1261
  protected constructor(
1153
- protected readonly kind: StoredConstraint['kind'],
1154
- protected readonly source: AnchorReference,
1155
- protected readonly target: AnchorReference,
1156
- protected readonly displacement: Vec3 | undefined,
1157
- protected readonly rotations: readonly ConstraintRotation[],
1262
+ protected readonly definition: RelationDefinition,
1158
1263
  protected readonly context: RelateContext | undefined,
1159
- protected readonly constraintId: string,
1160
- previous?: ConstraintExpression,
1264
+ protected readonly relationId: string,
1265
+ previous?: RelationExpression,
1266
+ protected readonly spatialOperation?: TransformationSpatialSelection,
1161
1267
  ) {
1162
1268
  valueTraces.set(this, {
1163
1269
  sourceRefs: [...(previous ? valueTrace(previous).sourceRefs : [])],
@@ -1166,7 +1272,7 @@ export abstract class ConstraintExpression {
1166
1272
  }
1167
1273
  /** @internal */
1168
1274
  attachSource(sourceRef: SourceRef): void {
1169
- const refs = this.sourceRefs;
1275
+ const refs = valueTrace(this).sourceRefs;
1170
1276
  if (
1171
1277
  !refs.some(
1172
1278
  ref =>
@@ -1179,107 +1285,125 @@ export abstract class ConstraintExpression {
1179
1285
  }
1180
1286
  /** @internal */
1181
1287
  attachParameters(parameters: readonly ParameterUsage[]): void {
1182
- appendUniqueParameters(this.parameters, parameters);
1288
+ appendUniqueParameters(valueTrace(this).parameters, parameters);
1183
1289
  }
1184
1290
  /** @internal */
1185
1291
  self(): RelationObject | undefined {
1186
1292
  return this.context?.self;
1187
1293
  }
1188
1294
  /** @internal */
1189
- 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
+ };
1190
1302
  const rebind = (model: RelationObject) =>
1191
1303
  model === this.context?.original ? this.context.self : model;
1192
1304
  return {
1193
- constraintId: this.constraintId,
1194
- source: rebind(this.source.model),
1195
- 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),
1196
1309
  self: this.context?.self,
1197
1310
  };
1198
1311
  }
1199
1312
  /** @internal */
1200
- storeFor(model: RelationObject, original: RelationObject): StoredConstraint {
1313
+ storeFor(model: RelationObject, original: RelationObject): StoredPlacement {
1201
1314
  const isSelf = (candidate: RelationObject | undefined) =>
1202
1315
  candidate === model || candidate === original;
1203
- if (!isSelf(this.source.model) && !isSelf(this.target.model))
1204
- throw new Error(
1205
- 'The constraint returned by relate() must involve self or the original receiver.',
1206
- );
1207
1316
  const bind = (reference: RelationReference): RelationReference => ({
1208
1317
  ...reference,
1209
1318
  model: isSelf(reference.model) ? undefined : reference.model,
1210
1319
  });
1211
- const stored: StoredConstraint = {
1212
- id: this.constraintId,
1213
- kind: this.kind,
1214
- source: bind(this.source),
1215
- target: bind(this.target),
1216
- offset: this.displacement,
1217
- rotations: this.rotations.map(action => ({
1218
- ...action,
1219
- pivot:
1220
- action.pivot.kind === 'around'
1221
- ? {...action.pivot, axis: bind(action.pivot.axis)}
1222
- : action.pivot,
1223
- })),
1224
- };
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
+ };
1225
1356
  valueTraces.set(stored, {
1226
- sourceRefs: [...this.sourceRefs],
1227
- parameters: [...this.parameters],
1357
+ sourceRefs: [...valueTrace(this).sourceRefs],
1358
+ parameters: [...valueTrace(this).parameters],
1228
1359
  });
1229
1360
  return stored;
1230
1361
  }
1231
1362
  /** @internal */
1232
- preview(): ConstraintPreview | undefined {
1363
+ preview(
1364
+ preceding: readonly RelationExpression[] = [],
1365
+ ): RelationPreview | undefined {
1233
1366
  if (!this.context) return undefined;
1234
1367
  const pivot = this.spatialOperation?.pivot;
1235
- const selection =
1236
- pivot?.kind === 'around'
1237
- ? {
1238
- ...this.spatialOperation!,
1239
- pivot: {
1240
- ...pivot,
1241
- axis: {
1242
- ...pivot.axis,
1243
- model:
1244
- pivot.axis.model === this.context.self ||
1245
- pivot.axis.model === this.context.original
1246
- ? undefined
1247
- : pivot.axis.model,
1248
- },
1249
- },
1250
- }
1251
- : this.spatialOperation;
1252
- return this.context.original[relationPreview](
1253
- 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](
1254
1382
  this.storeFor(this.context.self, this.context.original),
1255
1383
  selection,
1384
+ preceding,
1256
1385
  );
1257
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
+ }
1258
1396
  }
1259
1397
 
1260
- export class Constraint extends ConstraintExpression {
1398
+ export class Constraint extends RelationExpression {
1399
+ declare protected readonly definition: Exclude<
1400
+ RelationDefinition,
1401
+ {kind: 'transformation'}
1402
+ >;
1261
1403
  private constructor(
1262
- kind: StoredConstraint['kind'],
1263
- source: AnchorReference,
1264
- target: AnchorReference,
1265
- displacement: Vec3 | undefined = undefined,
1266
- rotations: readonly ConstraintRotation[] = [],
1267
- context = activeRelate,
1268
- constraintId = `constraint-${nextConstraintId++}`,
1269
- previous?: ConstraintExpression,
1270
- spatialOperation?: ConstraintSpatialSelection,
1404
+ definition: Exclude<RelationDefinition, {kind: 'transformation'}>,
1271
1405
  ) {
1272
- super(
1273
- kind,
1274
- source,
1275
- target,
1276
- displacement,
1277
- rotations,
1278
- context,
1279
- constraintId,
1280
- previous,
1281
- );
1282
- this.spatialOperation = spatialOperation;
1406
+ super(definition, activeRelate, `constraint-${nextConstraintId++}`);
1283
1407
  }
1284
1408
  /** @internal */
1285
1409
  static create(
@@ -1293,171 +1417,254 @@ export class Constraint extends ConstraintExpression {
1293
1417
  throw new Error(
1294
1418
  'align() requires points, curves, or surfaces; select geometry on a solid or group.',
1295
1419
  );
1296
- return new Constraint(kind, source, target);
1420
+ return new Constraint({kind, source, target});
1297
1421
  }
1298
- /**
1299
- * In the target reference axes, on() pins matching bound centers; align()
1300
- * translates self after alignment, retaining the relation's free modes.
1301
- * Explicit zero pins tangential coordinates for on() only.
1302
- * @code3d.param x {kind: 'length', default: 0, label: 'ΔX'}
1303
- * @code3d.param y {kind: 'length', default: 0, label: 'ΔY'}
1304
- * @code3d.param z {kind: 'length', default: 0, label: 'ΔZ'}
1305
- */
1306
- offset(x: number, y: number, z: number): Constraint;
1307
- offset(x = 0, y = 0, z = 0): Constraint {
1308
- assertFiniteVector('offset', [x, y, z]);
1309
- return new Constraint(
1310
- this.kind,
1311
- this.source,
1312
- this.target,
1313
- addVectors(this.displacement ?? origin, [x, y, z]),
1314
- this.rotations,
1315
- this.context,
1316
- this.constraintId,
1317
- 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,
1318
1439
  );
1440
+ valueTraces.set(action, valueTrace(this));
1319
1441
  }
1320
- /**
1321
- * Select this rotation's pivot in self's local coordinates.
1322
- * @code3d.param x {kind: 'length', default: 0, label: 'Pivot X'}
1323
- * @code3d.param y {kind: 'length', default: 0, label: 'Pivot Y'}
1324
- * @code3d.param z {kind: 'length', default: 0, label: 'Pivot Z'}
1325
- */
1326
- pivot([x, y, z]: Vec3): ConstraintPivotChain;
1327
- pivot([x = 0, y = 0, z = 0]: Vec3 = [0, 0, 0]): ConstraintPivotChain {
1328
- assertFiniteVector('pivot', [x, y, z]);
1329
- return new ConstraintPivotChain(this, {kind: 'pivot', point: [x, y, z]});
1330
- }
1331
- /** @code3d.param id {kind: 'vertex', label: 'Pivot vertex'} */
1332
- pivotVertex(id: VertexId): ConstraintPivotChain {
1333
- return new ConstraintPivotChain(this, {kind: 'pivotVertex', id});
1334
- }
1335
- /** Select a positioned axis in the composition. */
1336
- around(axis: LineAnchor): ConstraintAroundChain {
1337
- const reference = anchorReference(axis);
1338
- if (reference.kind !== 'line')
1339
- throw new Error('around() requires an axis or straight edge.');
1340
- const geometry =
1341
- reference.topology?.source[modelGeometry]()?.value ??
1342
- (reference.whole && isModelObject(reference.model)
1343
- ? reference.model[modelGeometry]()?.value
1344
- : undefined);
1345
- if (geometry) {
1346
- const straight = reference.topology
1347
- ? withTopologyShape(
1348
- geometry.shape,
1349
- geometry.topology,
1350
- reference.topology.selection,
1351
- shape => (shape as ReplicadEdge).geomType === 'LINE',
1352
- )
1353
- : (geometry.shape as ReplicadEdge).geomType === 'LINE';
1354
- if (!straight)
1355
- throw new Error(
1356
- 'around() requires a straight axis; curved edges do not define one rotation axis.',
1357
- );
1358
- }
1359
- return new ConstraintAroundChain(this, {kind: 'around', axis: reference});
1360
- }
1361
- /**
1362
- * Rotate about self's origin and local X, Y, then Z axes, in degrees.
1363
- * @code3d.param x {kind: 'angle', default: 0, label: 'Rotate X'}
1364
- * @code3d.param y {kind: 'angle', default: 0, label: 'Rotate Y'}
1365
- * @code3d.param z {kind: 'angle', default: 0, label: 'Rotate Z'}
1366
- */
1367
- rotate(x: number, y: number, z: number): Constraint;
1368
- rotate(x = 0, y = 0, z = 0): Constraint {
1369
- 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
+ );
1370
1452
  }
1371
- /** @internal */
1372
1453
  withRotation(
1373
1454
  pivot: PivotSelection,
1374
1455
  angles: Vec3 | number,
1375
- previous: ConstraintExpression,
1376
- ): Constraint {
1456
+ previous: RelationExpression,
1457
+ ): Transformation {
1377
1458
  assertFiniteVector(
1378
1459
  'rotate',
1379
1460
  typeof angles === 'number' ? [angles, 0, 0] : angles,
1380
1461
  );
1381
- return new Constraint(
1382
- this.kind,
1383
- this.source,
1384
- this.target,
1385
- this.displacement,
1386
- [...this.rotations, {pivot, angles}],
1387
- this.context,
1388
- this.constraintId,
1389
- previous,
1390
- {kind: 'rotate', pivot},
1391
- );
1462
+ return new Transformation({pivot, angles}, previous);
1392
1463
  }
1393
- /** @internal */
1394
- chainArguments(): [
1395
- StoredConstraint['kind'],
1396
- AnchorReference,
1397
- AnchorReference,
1398
- Vec3 | undefined,
1399
- readonly ConstraintRotation[],
1400
- RelateContext | undefined,
1401
- string,
1402
- ConstraintExpression,
1403
- ] {
1404
- return [
1405
- this.kind,
1406
- this.source,
1407
- this.target,
1408
- this.displacement,
1409
- this.rotations,
1410
- this.context,
1411
- this.constraintId,
1412
- this,
1413
- ];
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
+ );
1414
1495
  }
1496
+ return reference;
1415
1497
  }
1416
1498
 
1417
- export class ConstraintPivotChain extends ConstraintExpression {
1499
+ export type Relation = Constraint | Transformation;
1500
+
1501
+ export class PivotRotation extends RelationExpression {
1418
1502
  /** @internal */
1419
1503
  constructor(
1420
- private readonly constraint: Constraint,
1421
- private readonly selection: Exclude<PivotSelection, {kind: 'around'}>,
1504
+ protected readonly chain: TransformationRotation,
1505
+ protected readonly selection: PointSelection,
1506
+ previous?: RelationExpression,
1422
1507
  ) {
1423
- super(...constraint.chainArguments());
1424
- this.spatialOperation = {kind: selection.kind, pivot: selection};
1508
+ super(...(previous ?? chain).continuationArguments(), {
1509
+ kind: selection.offset ? 'pivotOffset' : selection.kind,
1510
+ pivot: selection,
1511
+ });
1425
1512
  }
1426
1513
  /**
1427
1514
  * @code3d.param x {kind: 'angle', default: 0, label: 'Rotate X'}
1428
1515
  * @code3d.param y {kind: 'angle', default: 0, label: 'Rotate Y'}
1429
1516
  * @code3d.param z {kind: 'angle', default: 0, label: 'Rotate Z'}
1430
1517
  */
1431
- rotate(x: number, y: number, z: number): Constraint;
1432
- rotate(x = 0, y = 0, z = 0): Constraint {
1433
- 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);
1434
1521
  }
1435
1522
  }
1436
- export class ConstraintAroundChain extends ConstraintExpression {
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
+ );
1539
+ }
1540
+ }
1541
+
1542
+ export class AxisRotation extends RelationExpression {
1437
1543
  /** @internal */
1438
1544
  constructor(
1439
- private readonly constraint: Constraint,
1440
- private readonly selection: Extract<PivotSelection, {kind: 'around'}>,
1545
+ protected readonly chain: TransformationRotation,
1546
+ protected readonly selection: AxisSelection,
1547
+ previous?: RelationExpression,
1441
1548
  ) {
1442
- super(...constraint.chainArguments());
1443
- this.spatialOperation = {kind: selection.kind, pivot: selection};
1549
+ super(...(previous ?? chain).continuationArguments(), {
1550
+ kind: selection.offset ? 'axisOffset' : selection.kind,
1551
+ pivot: selection,
1552
+ });
1444
1553
  }
1445
1554
  /** @code3d.param angle {kind: 'angle', default: 0, label: 'Rotate'} */
1446
- rotate(angle: number): Constraint;
1447
- rotate(angle = 0): Constraint {
1448
- 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);
1449
1558
  }
1450
1559
  }
1451
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
+ );
1576
+ }
1577
+ }
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
+
1452
1659
  const modelGeometry = Symbol('modelGeometry');
1453
1660
  const referenceBounds = Symbol('referenceBounds');
1454
1661
  const referenceBoundsParts = Symbol('referenceBoundsParts');
1455
1662
  const modelSnapshotQueries = Symbol('modelSnapshotQueries');
1456
- const relationPreview = Symbol('relationPreview');
1663
+ const previewRelation = Symbol('previewRelation');
1457
1664
 
1458
1665
  type RelationObjectInit = Readonly<{
1459
1666
  nodeId?: string;
1460
- constraints?: readonly StoredConstraint[];
1667
+ placements?: readonly StoredPlacement[];
1461
1668
  sourceRefs?: readonly SourceRef[];
1462
1669
  parameters?: readonly ParameterUsage[];
1463
1670
  }>;
@@ -1467,11 +1674,17 @@ export abstract class RelationObject {
1467
1674
  readonly nodeId: string;
1468
1675
  /** @internal */
1469
1676
  abstract readonly name: string;
1470
- 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
+ }
1471
1684
 
1472
1685
  protected constructor(init: RelationObjectInit = {}) {
1473
1686
  this.nodeId = init.nodeId ?? `node-${nextNodeId++}`;
1474
- this.constraints = [...(init.constraints ?? [])];
1687
+ this.placements = [...(init.placements ?? [])];
1475
1688
  valueTraces.set(this, {
1476
1689
  sourceRefs: [...(init.sourceRefs ?? [])],
1477
1690
  parameters: [...(init.parameters ?? [])],
@@ -1488,6 +1701,24 @@ export abstract class RelationObject {
1488
1701
 
1489
1702
  protected abstract copyRelations(init: RelationObjectInit): RelationObject;
1490
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
+
1491
1722
  protected vertexPosition(_id: VertexId): Vec3 {
1492
1723
  throw new Error(
1493
1724
  'pivotVertex() requires model topology, not a sketch reference frame. Use pivot([x, y, z]) instead.',
@@ -1512,27 +1743,30 @@ export abstract class RelationObject {
1512
1743
  /** Store relations only after the complete callback has returned. */
1513
1744
  protected addRelations(
1514
1745
  original: RelationObject,
1515
- build: () => Constraint | readonly Constraint[],
1746
+ build: () => Relation | readonly Relation[],
1516
1747
  ): readonly RelationObject[] {
1517
1748
  const previous = activeRelate;
1518
1749
  activeRelate = {self: this, original};
1519
- let built: Constraint | readonly Constraint[];
1750
+ let built: Relation | readonly Relation[];
1520
1751
  try {
1521
1752
  built = build();
1522
1753
  } finally {
1523
1754
  activeRelate = previous;
1524
1755
  }
1525
- const constraints: readonly Constraint[] = Array.isArray(built)
1756
+ const relations: readonly Relation[] = Array.isArray(built)
1526
1757
  ? built
1527
- : [built as Constraint];
1528
- const stored = constraints.map(constraint => {
1529
- 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
+ )
1530
1764
  throw new Error(
1531
- '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().',
1532
1766
  );
1533
1767
  return constraint.storeFor(this, original);
1534
1768
  });
1535
- this.constraints.push(...stored);
1769
+ this.placements.push(...stored);
1536
1770
  return uniqueModels(stored.flatMap(constraintReferences));
1537
1771
  }
1538
1772
 
@@ -1641,27 +1875,37 @@ export abstract class RelationObject {
1641
1875
  };
1642
1876
  }
1643
1877
 
1644
- private bodyRotations(
1645
- constraint: StoredConstraint,
1878
+ private bodyActions(
1879
+ constraint: StoredTransformation,
1646
1880
  indices: Map<RelationObject, number>,
1647
- ): BodyRotation[] {
1648
- return constraint.rotations.map(({pivot, angles}) => {
1649
- if (pivot.kind === 'around') {
1650
- 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
1651
1888
  ? {
1652
- body: indices.get(pivot.axis.model)!,
1653
- axis: pivot.axis.transform,
1654
- 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),
1655
1892
  }
1656
1893
  : {
1657
1894
  local: axisRotation(
1658
- pivot.axis.transform,
1659
- (angles as number) * (pivot.axis.direction ?? 1),
1895
+ selectedAxis(axis, pivot.offset),
1896
+ (angles as number) * (axis.direction ?? 1),
1660
1897
  ),
1661
1898
  };
1662
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
+ };
1663
1907
  const frame = translation(
1664
- pivot.kind === 'pivot' ? pivot.point : this.vertexPosition(pivot.id),
1908
+ addVectors(this.rotationPoint(pivot), pivot.offset ?? origin),
1665
1909
  );
1666
1910
  return {
1667
1911
  local: composeTransforms(
@@ -1673,84 +1917,167 @@ export abstract class RelationObject {
1673
1917
  }
1674
1918
 
1675
1919
  /** @internal */
1676
- [relationPreview](
1677
- nodeId: string,
1678
- constraint: StoredConstraint,
1679
- selection: ConstraintSpatialSelection | undefined,
1680
- ): ConstraintPreview {
1681
- // This expression starts from the relate receiver, before sibling constraints
1682
- // 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
+ }
1683
1948
  const owner = this.copyRelations({
1684
- nodeId,
1685
- constraints: [...this.constraints, constraint],
1949
+ nodeId: this.nodeId,
1950
+ placements,
1686
1951
  });
1687
1952
  const context = RelationObject.createSolveContext([
1688
1953
  owner,
1689
- ...(selection?.pivot.kind === 'around' && selection.pivot.axis.model
1690
- ? [selection.pivot.axis.model]
1954
+ ...(selection && selectionReference(selection.pivot)?.model
1955
+ ? [selectionReference(selection.pivot)!.model!]
1691
1956
  : []),
1692
1957
  ]);
1693
1958
  return {
1694
1959
  object: {
1695
- nodeId,
1960
+ nodeId: this.nodeId,
1696
1961
  compositionTransform: toTransform(owner.solvePose(context)),
1697
1962
  constraints: owner.constraints.map(value =>
1698
1963
  owner.constraintSnapshot(value, context),
1699
1964
  ),
1965
+ transformations: owner.transformationSnapshots(context),
1966
+ relationStages: owner.relationStageSnapshots(context),
1700
1967
  },
1701
- spatial: selection
1702
- ? owner.constraintSpatial(constraint, selection, context)
1703
- : 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,
1704
1976
  };
1705
1977
  }
1706
1978
 
1707
- private constraintSpatial(
1708
- constraint: StoredConstraint,
1709
- selection: ConstraintSpatialSelection,
1979
+ private relationSpatial(
1980
+ constraint: StoredTransformation,
1981
+ selection: TransformationSpatialSelection,
1710
1982
  context: SolveContext,
1711
- ): ConstraintSpatialReference {
1983
+ actionIndex?: number,
1984
+ ): RelationSpatialReference {
1985
+ context = this.placementStageContext(constraint, context);
1712
1986
  const models = [...context.poses.keys()];
1713
- const actions = this.bodyRotations(
1714
- constraint,
1715
- new Map(models.map((model, i) => [model, i])),
1716
- );
1717
- 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);
1718
1997
  const finalPose = this.solvePose(context);
1719
- let before = finalPose;
1720
- for (let i = actions.length - 1; i >= stage; i--) {
1721
- const action = actions[i];
1722
- before =
1723
- 'local' in action
1724
- ? composeTransforms(before, invertTransform(action.local))
1725
- : composeTransforms(
1726
- axisRotation(
1727
- composeTransforms(
1728
- models[action.body].solvePose(context),
1729
- action.axis,
1730
- ),
1731
- -action.angle,
1732
- ),
1733
- before,
1734
- );
1735
- }
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
+ );
1736
2010
  const pivot = selection.pivot;
2011
+ const axis = isAxisSelection(pivot) ? this.rotationAxis(pivot) : undefined;
1737
2012
  let frame: RigidTransform;
1738
- if (pivot.kind === 'around') {
2013
+ if (axis) {
1739
2014
  frame = composeTransforms(
1740
- pivot.axis.model ? pivot.axis.model.solvePose(context) : before,
1741
- (pivot.axis.direction ?? 1) === 1
1742
- ? pivot.axis.transform
1743
- : 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
+ ),
1744
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
+ };
1745
2035
  } else {
1746
2036
  const local = translation(
1747
- pivot.kind === 'pivot' ? pivot.point : this.vertexPosition(pivot.id),
2037
+ addVectors(
2038
+ this.rotationPoint(pivot as PointSelection),
2039
+ pivot.offset ?? origin,
2040
+ ),
1748
2041
  );
1749
2042
  frame = composeTransforms(before, local);
1750
2043
  }
1751
- const angles = constraint.rotations[stage]?.angles ?? origin;
2044
+ const selected = constraint.actions[stage];
2045
+ const angles = selected && 'angles' in selected ? selected.angles : origin;
1752
2046
  const rotationVector: Vec3 =
1753
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
+ }
1754
2081
  const localFrame = relativeTransform(frame, finalPose);
1755
2082
  return {
1756
2083
  nodeId: this.nodeId,
@@ -1760,30 +2087,289 @@ export abstract class RelationObject {
1760
2087
  vector:
1761
2088
  selection.kind === 'rotate'
1762
2089
  ? rotationVector
1763
- : pivot.kind === 'pivot'
1764
- ? pivot.point
1765
- : origin,
2090
+ : selection.kind === 'pivotOffset' ||
2091
+ selection.kind === 'axisOffset'
2092
+ ? (pivot.offset ?? origin)
2093
+ : pivot.kind === 'pivot'
2094
+ ? pivot.point
2095
+ : origin,
1766
2096
  frame: localFrame,
1767
2097
  rotation: rotationVector,
1768
- 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
+ },
1769
2122
  },
1770
2123
  };
1771
2124
  }
1772
2125
 
1773
- 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 {
1774
2325
  const models: RelationObject[] = [];
1775
2326
  const indices = new Map<RelationObject, number>();
1776
2327
  const collect = (model: RelationObject): void => {
1777
2328
  if (indices.has(model)) return;
1778
2329
  indices.set(model, models.length);
1779
2330
  models.push(model);
1780
- model.constraints.flatMap(constraintReferences).forEach(collect);
2331
+ (overrides.get(model) ?? model.placements)
2332
+ .flatMap(constraintReferences)
2333
+ .forEach(collect);
1781
2334
  };
1782
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
+ });
1783
2367
  const poses = solveBodies(
1784
- models.map((model): Body => ({
2368
+ models.map((model, index): Body => ({
2369
+ initial: programs[index].initial,
2370
+ transformations: programs[index].transformations,
1785
2371
  name: model.name,
1786
- relations: model.constraints.map(constraint =>
2372
+ relations: programs[index].constraints.map(constraint =>
1787
2373
  constraint.kind === 'align'
1788
2374
  ? {
1789
2375
  kind: 'align',
@@ -1802,8 +2388,6 @@ export abstract class RelationObject {
1802
2388
  ).alignmentGeometry(constraint.target),
1803
2389
  transform: constraint.target.transform,
1804
2390
  },
1805
- offset: constraint.offset,
1806
- rotations: model.bodyRotations(constraint, indices),
1807
2391
  }
1808
2392
  : {
1809
2393
  kind: 'on',
@@ -1822,14 +2406,13 @@ export abstract class RelationObject {
1822
2406
  transform: constraint.target.transform,
1823
2407
  facing: constraint.target.bound!.facing,
1824
2408
  },
1825
- offset: constraint.offset,
1826
- rotations: model.bodyRotations(constraint, indices),
1827
2409
  },
1828
2410
  ),
1829
2411
  })),
1830
2412
  );
1831
2413
  return {
1832
2414
  poses: new Map(models.map((model, index) => [model, poses[index]])),
2415
+ frame: identityRigidTransform,
1833
2416
  };
1834
2417
  }
1835
2418
 
@@ -1839,46 +2422,12 @@ export abstract class RelationObject {
1839
2422
  ): ConstraintSnapshot {
1840
2423
  const source = constraint.source.model ?? this;
1841
2424
  const target = constraint.target.model ?? this;
1842
- const models = [...context.poses.keys()];
1843
- const actions = this.bodyRotations(
1844
- constraint,
1845
- new Map(models.map((model, i) => [model, i])),
1846
- );
1847
- let before = this.solvePose(context);
1848
- for (const action of [...actions].reverse()) {
1849
- before =
1850
- 'local' in action
1851
- ? composeTransforms(before, invertTransform(action.local))
1852
- : composeTransforms(
1853
- axisRotation(
1854
- composeTransforms(
1855
- models[action.body].solvePose(context),
1856
- action.axis,
1857
- ),
1858
- -action.angle,
1859
- ),
1860
- before,
1861
- );
1862
- }
1863
- const sourcePose = source === this ? before : source.solvePose(context);
1864
- const targetPose = target === this ? before : target.solvePose(context);
1865
- let offsetFrame = composeTransforms(
1866
- targetPose,
2425
+ const sourcePose = source.solvePose(context);
2426
+ const contactFrame = composeTransforms(
2427
+ target.solvePose(context),
1867
2428
  constraint.target.transform,
1868
2429
  );
1869
2430
  if (constraint.kind === 'align') {
1870
- for (const action of actions)
1871
- if ('body' in action)
1872
- offsetFrame = composeTransforms(
1873
- axisRotation(
1874
- composeTransforms(
1875
- models[action.body].solvePose(context),
1876
- action.axis,
1877
- ),
1878
- action.angle,
1879
- ),
1880
- offsetFrame,
1881
- );
1882
2431
  return {
1883
2432
  id: constraint.id,
1884
2433
  kind: 'align',
@@ -1886,15 +2435,12 @@ export abstract class RelationObject {
1886
2435
  target: anchorSnapshot(target, constraint.target),
1887
2436
  sourceElement: source.relationElement(constraint.source),
1888
2437
  targetElement: target.relationElement(constraint.target),
1889
- offsetDirection: 1,
1890
- offset: constraint.offset ?? origin,
1891
- offsetFrame: toTransform(offsetFrame),
1892
2438
  sourceRefs: [...valueTrace(constraint).sourceRefs],
1893
2439
  parameters: [...valueTrace(constraint).parameters],
1894
2440
  };
1895
2441
  }
1896
2442
  const orientation = composeTransforms(
1897
- invertTransform(rotation(offsetFrame.quaternion)),
2443
+ invertTransform(rotation(contactFrame.quaternion)),
1898
2444
  rotation(sourcePose.quaternion),
1899
2445
  ).quaternion;
1900
2446
  const bounds = source[referenceBounds](
@@ -1910,9 +2456,9 @@ export abstract class RelationObject {
1910
2456
  {
1911
2457
  position: addVectors(
1912
2458
  sourcePose.position,
1913
- rotateVector(center, offsetFrame.quaternion),
2459
+ rotateVector(center, contactFrame.quaternion),
1914
2460
  ),
1915
- quaternion: offsetFrame.quaternion,
2461
+ quaternion: contactFrame.quaternion,
1916
2462
  },
1917
2463
  sourcePose,
1918
2464
  );
@@ -1921,19 +2467,6 @@ export abstract class RelationObject {
1921
2467
  bounds[1][1] - bounds[0][1],
1922
2468
  bounds[1][2] - bounds[0][2],
1923
2469
  ];
1924
- for (const action of actions) {
1925
- if ('body' in action)
1926
- offsetFrame = composeTransforms(
1927
- axisRotation(
1928
- composeTransforms(
1929
- models[action.body].solvePose(context),
1930
- action.axis,
1931
- ),
1932
- action.angle,
1933
- ),
1934
- offsetFrame,
1935
- );
1936
- }
1937
2470
  return {
1938
2471
  id: constraint.id,
1939
2472
  kind: constraint.kind,
@@ -1967,9 +2500,6 @@ export abstract class RelationObject {
1967
2500
  transform: toTransform(constraint.target.transform),
1968
2501
  bound: constraint.target.bound,
1969
2502
  },
1970
- offsetDirection: source === this ? 1 : -1,
1971
- offset: constraint.offset ?? origin,
1972
- offsetFrame: toTransform(offsetFrame),
1973
2503
  sourceRefs: [...valueTrace(constraint).sourceRefs],
1974
2504
  parameters: [...valueTrace(constraint).parameters],
1975
2505
  };
@@ -1985,7 +2515,7 @@ export class SketchFrame extends RelationObject {
1985
2515
 
1986
2516
  constructor(source?: SketchFrame, init: RelationObjectInit = {}) {
1987
2517
  super({
1988
- constraints: source?.constraints,
2518
+ placements: source?.placements,
1989
2519
  sourceRefs: source?.sourceRefs,
1990
2520
  parameters: source?.parameters,
1991
2521
  ...init,
@@ -2004,7 +2534,7 @@ export class SketchFrame extends RelationObject {
2004
2534
  relatedObjects(): readonly RelationObject[] {
2005
2535
  return [
2006
2536
  ...(this.source ? [this.source] : []),
2007
- ...this.constraints.flatMap(constraintReferences),
2537
+ ...this.placements.flatMap(constraintReferences),
2008
2538
  ];
2009
2539
  }
2010
2540
 
@@ -2060,7 +2590,7 @@ export class SketchFrame extends RelationObject {
2060
2590
  }
2061
2591
 
2062
2592
  relate(
2063
- build: (frame: SketchFrame) => Constraint | readonly Constraint[],
2593
+ build: (frame: SketchFrame) => Relation | readonly Relation[],
2064
2594
  ): SketchFrame {
2065
2595
  const related = new SketchFrame(this);
2066
2596
  related.addRelations(this, () => build(related));
@@ -2068,10 +2598,10 @@ export class SketchFrame extends RelationObject {
2068
2598
  }
2069
2599
 
2070
2600
  face(region: SketchRegion): FaceModel {
2071
- return sketchFaceModel(region, this.constraints, this);
2601
+ return sketchFaceModel(region, this.placements, this);
2072
2602
  }
2073
2603
 
2074
- snapshot(): ConstraintPreview['object'] {
2604
+ snapshot(): RelationPreview['object'] {
2075
2605
  const context = RelationObject.createSolveContext([this]);
2076
2606
  return {
2077
2607
  nodeId: this.nodeId,
@@ -2079,6 +2609,8 @@ export class SketchFrame extends RelationObject {
2079
2609
  constraints: this.constraints.map(constraint =>
2080
2610
  this.constraintSnapshot(constraint, context),
2081
2611
  ),
2612
+ transformations: this.transformationSnapshots(context),
2613
+ relationStages: this.relationStageSnapshots(context),
2082
2614
  };
2083
2615
  }
2084
2616
 
@@ -2242,7 +2774,7 @@ export class ModelObject<
2242
2774
  relate(
2243
2775
  build: (
2244
2776
  self: RuntimeModel<Elements, Kind>,
2245
- ) => Constraint | readonly Constraint[],
2777
+ ) => Relation | readonly Relation[],
2246
2778
  ): RuntimeModel<Elements, Kind> {
2247
2779
  const operation = storedOperation('relate', [
2248
2780
  {model: this, role: 'source', index: 0},
@@ -2506,7 +3038,7 @@ export class ModelObject<
2506
3038
  transformElement(element, transform),
2507
3039
  ]),
2508
3040
  ),
2509
- constraints: this.mapConstraintGeometry(
3041
+ placements: this.mapConstraintGeometry(
2510
3042
  element => transformElement(element, transform),
2511
3043
  point => composeTransforms(transform, translation(point)).position,
2512
3044
  ),
@@ -2544,23 +3076,52 @@ export class ModelObject<
2544
3076
  private mapConstraintGeometry(
2545
3077
  mapElement: (element: StoredElement) => StoredElement,
2546
3078
  mapPoint: (point: Vec3) => Vec3,
2547
- ): StoredConstraint[] {
3079
+ ): StoredPlacement[] {
2548
3080
  const mapReference = (reference: RelationReference): RelationReference =>
2549
3081
  reference.model ? reference : {...reference, ...mapElement(reference)};
2550
- return this.constraints.map(constraint => {
2551
- 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 = {
2552
3090
  ...constraint,
2553
- source: mapReference(constraint.source),
2554
- target: mapReference(constraint.target),
2555
- rotations: constraint.rotations.map(action => ({
2556
- ...action,
2557
- pivot:
2558
- action.pivot.kind === 'around'
2559
- ? {...action.pivot, axis: mapReference(action.pivot.axis)}
2560
- : action.pivot.kind === 'pivot'
2561
- ? {...action.pivot, point: mapPoint(action.pivot.point)}
2562
- : action.pivot,
2563
- })),
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
+ : {}),
2564
3125
  };
2565
3126
  valueTraces.set(mapped, valueTrace(constraint));
2566
3127
  return mapped;
@@ -2593,7 +3154,7 @@ export class ModelObject<
2593
3154
  {
2594
3155
  geometryAnchor: scaleElement(this.geometryAnchor, factor),
2595
3156
  elements: scaleElements(this.elements, factor),
2596
- constraints: this.mapConstraintGeometry(
3157
+ placements: this.mapConstraintGeometry(
2597
3158
  element => scaleElement(element, factor),
2598
3159
  point => scaleFrame(translation(point), factor).position,
2599
3160
  ),
@@ -2632,7 +3193,7 @@ export class ModelObject<
2632
3193
  name: 'Extrude',
2633
3194
  geometry,
2634
3195
  material: this.materialSnapshot,
2635
- constraints: this.constraints,
3196
+ placements: this.placements,
2636
3197
  sourceRefs: this.sourceRefs,
2637
3198
  parameters: this.allParameters(),
2638
3199
  meshTolerance: this.meshTolerance,
@@ -2822,7 +3383,7 @@ export class ModelObject<
2822
3383
  return [
2823
3384
  ...this.children,
2824
3385
  ...this.operation.inputs.map(input => input.model),
2825
- ...this.constraints.flatMap(constraintReferences),
3386
+ ...this.placements.flatMap(constraintReferences),
2826
3387
  ];
2827
3388
  }
2828
3389
 
@@ -2847,7 +3408,7 @@ export class ModelObject<
2847
3408
  );
2848
3409
  const parameters = uniqueParameters([
2849
3410
  ...this.parameters,
2850
- ...this.constraints.flatMap(
3411
+ ...this.placements.flatMap(
2851
3412
  constraint => valueTrace(constraint).parameters,
2852
3413
  ),
2853
3414
  ]);
@@ -2859,6 +3420,8 @@ export class ModelObject<
2859
3420
  compositionTransform: toTransform(pose),
2860
3421
  transform: toTransform(inComposition ? pose : identityRigidTransform),
2861
3422
  constraints,
3423
+ transformations: this.transformationSnapshots(solveContext),
3424
+ relationStages: this.relationStageSnapshots(solveContext),
2862
3425
  origin,
2863
3426
  elements: snapshotElements({
2864
3427
  ...this.elements,
@@ -2970,7 +3533,7 @@ export class ModelObject<
2970
3533
  name: 'Loft',
2971
3534
  geometry,
2972
3535
  material: this.materialSnapshot,
2973
- constraints: this.constraints,
3536
+ placements: this.placements,
2974
3537
  sourceRefs: inputs.flatMap(input => input.sourceRefs),
2975
3538
  parameters: uniqueParameters(
2976
3539
  inputs.flatMap(input => input.allParameters()),
@@ -3005,7 +3568,7 @@ export class ModelObject<
3005
3568
  geometry: evaluation.geometry,
3006
3569
  name: this.name,
3007
3570
  material: this.materialSnapshot,
3008
- constraints: this.constraints,
3571
+ placements: this.placements,
3009
3572
  sourceRefs: [this, ...others].flatMap(model => model.sourceRefs),
3010
3573
  parameters: uniqueParameters(
3011
3574
  [this, ...others].flatMap(model => model.allParameters()),
@@ -3130,7 +3693,7 @@ export class ModelObject<
3130
3693
  private allParameters(): ParameterUsage[] {
3131
3694
  return uniqueParameters([
3132
3695
  ...this.parameters,
3133
- ...this.constraints.flatMap(
3696
+ ...this.placements.flatMap(
3134
3697
  constraint => valueTrace(constraint).parameters,
3135
3698
  ),
3136
3699
  ]);
@@ -3292,6 +3855,10 @@ export class ModelObject<
3292
3855
  );
3293
3856
  }
3294
3857
 
3858
+ protected override edgeReference(id: EdgeId): RelationReference {
3859
+ return {...rotationAxisReference(this.edge(id)), model: undefined};
3860
+ }
3861
+
3295
3862
  protected override vertexPosition(id: VertexId): Vec3 {
3296
3863
  return anchorReference(this.vertex(id)).transform.position;
3297
3864
  }
@@ -3300,17 +3867,17 @@ export class ModelObject<
3300
3867
  return this.copy(init, this.operation);
3301
3868
  }
3302
3869
 
3303
- /** 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. */
3304
3871
  private static createAssembly(
3305
3872
  children: readonly ModelObject[],
3306
3873
  ): SolveContext {
3307
3874
  const context = ModelObject.createSolveContext(children);
3308
- const center = children.length
3309
- ? boundsCenter(
3310
- pointBounds(children.map(child => child.solvePose(context).position)),
3875
+ return children.length
3876
+ ? transformSolveContext(
3877
+ context,
3878
+ invertTransform(children[0].solvePose(context)),
3311
3879
  )
3312
- : origin;
3313
- return transformSolveContext(context, translation(negateVector(center)));
3880
+ : context;
3314
3881
  }
3315
3882
 
3316
3883
  private requireGeometry(): ModelGeometry {
@@ -3395,7 +3962,7 @@ export class ModelObject<
3395
3962
  material: this.materialSnapshot,
3396
3963
  children: this.children,
3397
3964
  assembly: this.assembly,
3398
- constraints: this.constraints,
3965
+ placements: this.placements,
3399
3966
  elements: this.elements,
3400
3967
  sourceRefs: this.sourceRefs,
3401
3968
  parameters: this.parameters,
@@ -3985,6 +4552,7 @@ function normalizeReplicadSolid(shape: Shape3D): Shape3D {
3985
4552
  return solid;
3986
4553
  }
3987
4554
 
4555
+ /** Compose members in the first member's local frame; empty groups use the default frame. */
3988
4556
  export function group(children: readonly Model[], name = 'Group'): GroupModel {
3989
4557
  const runtimeChildren = children.map(child =>
3990
4558
  requireModelObject(child, 'Every group child must be a model.'),
@@ -4092,20 +4660,40 @@ export function isConstraint(value: unknown): value is Constraint {
4092
4660
  return value instanceof Constraint;
4093
4661
  }
4094
4662
 
4095
- export function isConstraintExpression(
4663
+ export function isRelationExpression(
4096
4664
  value: unknown,
4097
- ): value is ConstraintExpression {
4098
- return value instanceof ConstraintExpression;
4665
+ ): value is RelationExpression {
4666
+ return value instanceof RelationExpression;
4099
4667
  }
4100
4668
 
4101
- export function constraintPreview(
4102
- value: ConstraintExpression,
4103
- ): ConstraintPreview | undefined {
4104
- return value.preview();
4669
+ export function relationPreview(
4670
+ value: RelationExpression,
4671
+ preceding: readonly RelationExpression[] = [],
4672
+ ): RelationPreview | undefined {
4673
+ return value.preview(preceding);
4105
4674
  }
4106
4675
 
4107
- export function instrumentConstraint(
4108
- constraint: ConstraintExpression,
4676
+ /** The current authoring receiver remains available when a free selector throws. */
4677
+ export function currentRelationSelf(): RelationObject | undefined {
4678
+ return activeRelate?.self;
4679
+ }
4680
+
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,
4109
4697
  sourceRef: SourceRef,
4110
4698
  parameters: readonly ParameterUsage[],
4111
4699
  ): void {
@@ -4128,9 +4716,9 @@ export function instrumentModelOperation(
4128
4716
  );
4129
4717
  }
4130
4718
 
4131
- export function constraintTraceReference(
4132
- constraint: ConstraintExpression,
4133
- ): ConstraintTraceReference {
4719
+ export function relationTraceReference(
4720
+ constraint: RelationExpression,
4721
+ ): RelationTraceReference {
4134
4722
  return constraint.traceReference();
4135
4723
  }
4136
4724
 
@@ -4417,6 +5005,13 @@ export function retainModelGeometry(
4417
5005
  }
4418
5006
 
4419
5007
  export const authoringApi = Object.freeze({
5008
+ offset,
5009
+ rotate,
5010
+ pivot,
5011
+ pivotVertex,
5012
+ pivotPoint,
5013
+ axisEdge,
5014
+ axisLine,
4420
5015
  cached,
4421
5016
  font,
4422
5017
  googleFont,
@@ -4520,7 +5115,11 @@ const kernelShape = cachedArtifact(
4520
5115
  _inputs: readonly KernelArtifact<unknown>[],
4521
5116
  compute: () => AnyShape,
4522
5117
  ) => compute(),
4523
- {key: kernelOperationKey, lifecycle: shapeLifecycle},
5118
+ {
5119
+ key: (operation, arguments_, inputs) =>
5120
+ kernelOperationKey(`kernel-shape:${operation}`, arguments_, inputs),
5121
+ lifecycle: shapeLifecycle,
5122
+ },
4524
5123
  );
4525
5124
 
4526
5125
  function evaluateKernelShape<Shape extends AnyShape>(
@@ -4554,7 +5153,11 @@ const evaluateModelGeometry = cachedArtifact(
4554
5153
  referenceBasis: result.referenceBasis,
4555
5154
  };
4556
5155
  },
4557
- {key: kernelOperationKey, lifecycle: modelGeometryLifecycle},
5156
+ {
5157
+ key: (operation, arguments_, inputs) =>
5158
+ kernelOperationKey(`model-geometry:${operation}`, arguments_, inputs),
5159
+ lifecycle: modelGeometryLifecycle,
5160
+ },
4558
5161
  );
4559
5162
 
4560
5163
  function createModelGeometryValue(
@@ -4713,7 +5316,7 @@ type PlanarSketch = Readonly<{
4713
5316
  /** Internal bridge from the kernel-independent sketch definition to model geometry. */
4714
5317
  function sketchFaceModel(
4715
5318
  region: SketchRegion,
4716
- constraints: readonly StoredConstraint[],
5319
+ placements: readonly StoredPlacement[],
4717
5320
  source: SketchFrame,
4718
5321
  ): FaceModel {
4719
5322
  const curves = [region.outer, ...region.holes].map(loop =>
@@ -4735,7 +5338,7 @@ function sketchFaceModel(
4735
5338
  return ModelObject.create<PlanarElements, 'face'>({
4736
5339
  kind: 'face',
4737
5340
  name: 'Sketch face',
4738
- constraints,
5341
+ placements,
4739
5342
  geometry,
4740
5343
  geometryAnchor: plane,
4741
5344
  elements: {plane},
@@ -4908,12 +5511,14 @@ function solidElements(bounds: LocalBounds): StoredElements {
4908
5511
  return {axis: {kind: 'line', transform: translation(boundsCenter(bounds))}};
4909
5512
  }
4910
5513
 
4911
- function constraintReferences(constraint: StoredConstraint): RelationObject[] {
5514
+ function constraintReferences(constraint: StoredPlacement): RelationObject[] {
4912
5515
  return [
4913
- constraint.source.model,
4914
- constraint.target.model,
4915
- ...constraint.rotations.map(action =>
4916
- action.pivot.kind === 'around' ? action.pivot.axis.model : undefined,
5516
+ ...(constraint.kind === 'transformation'
5517
+ ? []
5518
+ : [constraint.source.model, constraint.target.model]),
5519
+ ...(constraint.kind === 'transformation' ? constraint.actions : []).map(
5520
+ action =>
5521
+ 'pivot' in action ? selectionReference(action.pivot)?.model : undefined,
4917
5522
  ),
4918
5523
  ].filter((model): model is RelationObject => !!model);
4919
5524
  }