@constructorfleet/sketchforge-engine 1.26.0 → 1.28.0

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 (200) hide show
  1. package/dist/cadAnalysis.d.ts +94 -0
  2. package/dist/cadAnalysis.d.ts.map +1 -0
  3. package/dist/cadAnalysis.js +157 -0
  4. package/dist/cadAnalysis.js.map +1 -0
  5. package/dist/cadBakeMetadata.d.ts +11 -2
  6. package/dist/cadBakeMetadata.d.ts.map +1 -1
  7. package/dist/cadBakeMetadata.js +9 -0
  8. package/dist/cadBakeMetadata.js.map +1 -1
  9. package/dist/cadProjection.d.ts.map +1 -1
  10. package/dist/cadProjection.js.map +1 -1
  11. package/dist/cadSection.d.ts.map +1 -1
  12. package/dist/cadShell.d.ts +143 -0
  13. package/dist/cadShell.d.ts.map +1 -0
  14. package/dist/cadShell.js +313 -0
  15. package/dist/cadShell.js.map +1 -0
  16. package/dist/cadSolids.d.ts.map +1 -1
  17. package/dist/cadSolids.js +18 -1
  18. package/dist/cadSolids.js.map +1 -1
  19. package/dist/cutBrepFrameMigration.d.ts +46 -0
  20. package/dist/cutBrepFrameMigration.d.ts.map +1 -0
  21. package/dist/cutBrepFrameMigration.js +105 -0
  22. package/dist/cutBrepFrameMigration.js.map +1 -0
  23. package/dist/cutterPadding.d.ts.map +1 -1
  24. package/dist/domain/cameraProjection.js.map +1 -1
  25. package/dist/domain/sketchforge.d.ts +31 -1
  26. package/dist/domain/sketchforge.d.ts.map +1 -1
  27. package/dist/editorHistory.d.ts.map +1 -1
  28. package/dist/editorHistory.js.map +1 -1
  29. package/dist/engine.d.ts +3 -3
  30. package/dist/engine.d.ts.map +1 -1
  31. package/dist/engine.js +156 -78
  32. package/dist/engine.js.map +1 -1
  33. package/dist/exactBooleanParts.d.ts +4 -2
  34. package/dist/exactBooleanParts.d.ts.map +1 -1
  35. package/dist/exactBooleanParts.js +7 -3
  36. package/dist/exactBooleanParts.js.map +1 -1
  37. package/dist/exactBooleanSolids.d.ts.map +1 -1
  38. package/dist/gearProfile.d.ts.map +1 -1
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +1 -1
  42. package/dist/index.js.map +1 -1
  43. package/dist/loftProfile.d.ts +1 -1
  44. package/dist/loftProfile.d.ts.map +1 -1
  45. package/dist/loftProfile.js.map +1 -1
  46. package/dist/meshEdgeProjection.d.ts +104 -0
  47. package/dist/meshEdgeProjection.d.ts.map +1 -0
  48. package/dist/meshEdgeProjection.js +439 -0
  49. package/dist/meshEdgeProjection.js.map +1 -0
  50. package/dist/meshImport.d.ts.map +1 -1
  51. package/dist/meshImport.js +2 -20
  52. package/dist/meshImport.js.map +1 -1
  53. package/dist/meshMassProperties.d.ts +92 -0
  54. package/dist/meshMassProperties.d.ts.map +1 -0
  55. package/dist/meshMassProperties.js +570 -0
  56. package/dist/meshMassProperties.js.map +1 -0
  57. package/dist/meshSection.d.ts +44 -0
  58. package/dist/meshSection.d.ts.map +1 -0
  59. package/dist/meshSection.js +154 -0
  60. package/dist/meshSection.js.map +1 -0
  61. package/dist/meshShellOverlap.d.ts +75 -0
  62. package/dist/meshShellOverlap.d.ts.map +1 -0
  63. package/dist/meshShellOverlap.js +797 -0
  64. package/dist/meshShellOverlap.js.map +1 -0
  65. package/dist/meshShellSolids.d.ts +44 -0
  66. package/dist/meshShellSolids.d.ts.map +1 -0
  67. package/dist/meshShellSolids.js +116 -0
  68. package/dist/meshShellSolids.js.map +1 -0
  69. package/dist/nodeSvgDomParser.d.ts +3 -0
  70. package/dist/nodeSvgDomParser.d.ts.map +1 -0
  71. package/dist/nodeSvgDomParser.js +53 -0
  72. package/dist/nodeSvgDomParser.js.map +1 -0
  73. package/dist/parametricRecompute.d.ts.map +1 -1
  74. package/dist/parametricRecompute.js +63 -10
  75. package/dist/parametricRecompute.js.map +1 -1
  76. package/dist/placementWorkplane.d.ts.map +1 -1
  77. package/dist/placementWorkplane.js.map +1 -1
  78. package/dist/projectAssets.d.ts +1 -1
  79. package/dist/projectStore.d.ts +1 -1
  80. package/dist/projectStore.d.ts.map +1 -1
  81. package/dist/projectStore.js +23 -2
  82. package/dist/projectStore.js.map +1 -1
  83. package/dist/revolveRequests.d.ts +57 -1
  84. package/dist/revolveRequests.d.ts.map +1 -1
  85. package/dist/revolveRequests.js +103 -4
  86. package/dist/revolveRequests.js.map +1 -1
  87. package/dist/sceneExport.d.ts +6 -0
  88. package/dist/sceneExport.d.ts.map +1 -1
  89. package/dist/sceneExport.js +6 -2
  90. package/dist/sceneExport.js.map +1 -1
  91. package/dist/serverCadInterference.d.ts +8 -45
  92. package/dist/serverCadInterference.d.ts.map +1 -1
  93. package/dist/serverCadInterference.js +21 -43
  94. package/dist/serverCadInterference.js.map +1 -1
  95. package/dist/serverCadOperations.d.ts +73 -4
  96. package/dist/serverCadOperations.d.ts.map +1 -1
  97. package/dist/serverCadOperations.js +356 -71
  98. package/dist/serverCadOperations.js.map +1 -1
  99. package/dist/serverCadProjection.d.ts +14 -1
  100. package/dist/serverCadProjection.d.ts.map +1 -1
  101. package/dist/serverCadProjection.js +135 -13
  102. package/dist/serverCadProjection.js.map +1 -1
  103. package/dist/serverCadProperties.d.ts +14 -7
  104. package/dist/serverCadProperties.d.ts.map +1 -1
  105. package/dist/serverCadProperties.js +60 -24
  106. package/dist/serverCadProperties.js.map +1 -1
  107. package/dist/serverCadSection.d.ts +21 -1
  108. package/dist/serverCadSection.d.ts.map +1 -1
  109. package/dist/serverCadSection.js +157 -43
  110. package/dist/serverCadSection.js.map +1 -1
  111. package/dist/serverCadShell.d.ts +7 -94
  112. package/dist/serverCadShell.d.ts.map +1 -1
  113. package/dist/serverCadShell.js +25 -272
  114. package/dist/serverCadShell.js.map +1 -1
  115. package/dist/serverCadThickness.d.ts +8 -8
  116. package/dist/serverCadThickness.d.ts.map +1 -1
  117. package/dist/serverCadThickness.js +15 -42
  118. package/dist/serverCadThickness.js.map +1 -1
  119. package/dist/serverShapeBooleans.d.ts +75 -0
  120. package/dist/serverShapeBooleans.d.ts.map +1 -1
  121. package/dist/serverShapeBooleans.js +251 -9
  122. package/dist/serverShapeBooleans.js.map +1 -1
  123. package/dist/serverShapeGroups.js.map +1 -1
  124. package/dist/serverStepExplode.d.ts +1 -2
  125. package/dist/serverStepExplode.d.ts.map +1 -1
  126. package/dist/serverStepExplode.js +15 -148
  127. package/dist/serverStepExplode.js.map +1 -1
  128. package/dist/shapeBounds.d.ts.map +1 -1
  129. package/dist/shapeCatalog.d.ts.map +1 -1
  130. package/dist/shapeCatalog.js +1 -0
  131. package/dist/shapeCatalog.js.map +1 -1
  132. package/dist/shapeRequests.d.ts +13 -0
  133. package/dist/shapeRequests.d.ts.map +1 -1
  134. package/dist/shapeRequests.js +14 -0
  135. package/dist/shapeRequests.js.map +1 -1
  136. package/dist/sketchCadProfile.d.ts +13 -0
  137. package/dist/sketchCadProfile.d.ts.map +1 -1
  138. package/dist/sketchCadProfile.js +108 -0
  139. package/dist/sketchCadProfile.js.map +1 -1
  140. package/dist/sketchCadSweep.js.map +1 -1
  141. package/dist/sketchEdgeSegmentLineage.d.ts +32 -0
  142. package/dist/sketchEdgeSegmentLineage.d.ts.map +1 -0
  143. package/dist/sketchEdgeSegmentLineage.js +49 -0
  144. package/dist/sketchEdgeSegmentLineage.js.map +1 -0
  145. package/dist/sketchEdgeSegmentLineageKernel.d.ts +83 -0
  146. package/dist/sketchEdgeSegmentLineageKernel.d.ts.map +1 -0
  147. package/dist/sketchEdgeSegmentLineageKernel.js +125 -0
  148. package/dist/sketchEdgeSegmentLineageKernel.js.map +1 -0
  149. package/dist/sketchFaceDraftKernel.d.ts +16 -9
  150. package/dist/sketchFaceDraftKernel.d.ts.map +1 -1
  151. package/dist/sketchFaceDraftKernel.js +19 -11
  152. package/dist/sketchFaceDraftKernel.js.map +1 -1
  153. package/dist/sketchRevolve.d.ts +13 -0
  154. package/dist/sketchRevolve.d.ts.map +1 -1
  155. package/dist/sketchRevolve.js +4 -1
  156. package/dist/sketchRevolve.js.map +1 -1
  157. package/dist/skfProject.d.ts +1 -1
  158. package/dist/skfProject.d.ts.map +1 -1
  159. package/dist/skfProject.js +63 -4
  160. package/dist/skfProject.js.map +1 -1
  161. package/dist/sphereTessellation.d.ts.map +1 -1
  162. package/dist/stepAssemblyExplode.d.ts +45 -0
  163. package/dist/stepAssemblyExplode.d.ts.map +1 -0
  164. package/dist/stepAssemblyExplode.js +195 -0
  165. package/dist/stepAssemblyExplode.js.map +1 -0
  166. package/dist/stepAssemblyHierarchy.js.map +1 -1
  167. package/dist/stepComponentMetadata.d.ts +1 -1
  168. package/dist/stepComponentMetadata.d.ts.map +1 -1
  169. package/dist/stepComponentMetadata.js +93 -39
  170. package/dist/stepComponentMetadata.js.map +1 -1
  171. package/dist/stepExport.d.ts +14 -1
  172. package/dist/stepExport.d.ts.map +1 -1
  173. package/dist/stepExport.js +81 -32
  174. package/dist/stepExport.js.map +1 -1
  175. package/dist/stepImportCommon.d.ts +4 -1
  176. package/dist/stepImportCommon.d.ts.map +1 -1
  177. package/dist/stepImportCommon.js +7 -6
  178. package/dist/stepImportCommon.js.map +1 -1
  179. package/dist/stlImport.js.map +1 -1
  180. package/dist/support/cadModifierTypes.d.ts +100 -1
  181. package/dist/support/cadModifierTypes.d.ts.map +1 -1
  182. package/dist/support/i18n/LocalizedError.d.ts.map +1 -1
  183. package/dist/support/i18n/locales/en/messages.json +42 -2
  184. package/dist/support/i18n/locales.d.ts.map +1 -1
  185. package/dist/support/lib/appTheme.d.ts.map +1 -1
  186. package/dist/support/lib/materialPresets.js.map +1 -1
  187. package/dist/support/lib/measurementUnits.d.ts.map +1 -1
  188. package/dist/support/lib/operator-configuration.json +13 -1
  189. package/dist/support/lib/operatorConfiguration.d.ts +5 -5
  190. package/dist/support/lib/workplaneGrid.js.map +1 -1
  191. package/dist/svgImport.d.ts +2 -0
  192. package/dist/svgImport.d.ts.map +1 -1
  193. package/dist/svgImport.js +7 -0
  194. package/dist/svgImport.js.map +1 -1
  195. package/dist/threadCatalog.js.map +1 -1
  196. package/dist/workplaneSettings.js.map +1 -1
  197. package/dist/workplaneShapes.d.ts +2 -2
  198. package/dist/workplaneShapes.d.ts.map +1 -1
  199. package/dist/workplaneShapes.js.map +1 -1
  200. package/package.json +7 -6
package/dist/engine.js CHANGED
@@ -14,8 +14,8 @@
14
14
  * -- including the project lock, which a macro writing a directory needs and
15
15
  * does not have today -- without pulling `next/server` in with it.
16
16
  */
17
- import { allEdgeTreatmentEdgeCount, applyEdgeTreatment, applyBakedMirror, applyDraftPrism, applySketchFaceDraft, exactPlainSketchRevolveResult, isPlainFullTurnSketchRevolve, orientedSolidForShape, shapeEdges, shapeWithCadResult, ServerCadError, } from "./serverCadOperations.js";
18
- import { resolveRetainedFaceDraft, retainedFaceDraftFields, retainedPrismDraftFields } from "./sketchFaceDraftKernel.js";
17
+ import { allEdgeTreatmentEdgeCount, applyEdgeTreatment, applyBakedMirror, applyDraftPrism, applySketchFaceDraft, exactPlainSketchRevolveResult, isPlainFullTurnSketchRevolve, orientedSolidForShape, shapeEdges, shapeWithCadResult, sketchResizeFactors, ServerCadError, } from "./serverCadOperations.js";
18
+ import { resolveRetainedFaceDraft, retainedDraftSourcePlacement, retainedFaceDraftFields, retainedPrismDraftFields, } from "./sketchFaceDraftKernel.js";
19
19
  import { withCadKernel } from "./serverCadKernel.js";
20
20
  import { cadSketchRegions, cadSketchSweepPaths } from "./sketchCadProfile.js";
21
21
  import { applyShell, shapeFaces, } from "./serverCadShell.js";
@@ -35,9 +35,10 @@ import { serverManifoldRuntime } from "./serverManifoldRuntime.js";
35
35
  import { createLocalId } from "./localIds.js";
36
36
  import { invalidSketchSweepProfile, sketchSweepShapeFromRequest } from "./sketchSweepRequest.js";
37
37
  import { sketchExtrusionShapeFromRequest } from "./sketchExtrusionRequest.js";
38
+ import { sketchSourceEdgesForEdgeIndexes } from "./sketchEdgeSegmentLineage.js";
38
39
  import { edgeTreatmentAppliedFrame, findSketchOutput, recomputeSketchOutput, replaySingleAllEdgeTreatment, } from "./parametricRecompute.js";
39
40
  import { boundsForShapes, footprintCenter, shapeBounds } from "./shapeBounds.js";
40
- import { REQUESTABLE_SHAPE_KINDS, shapeFromRequest, shapeWithRequestedChanges, SHAPE_PARAMETERS, } from "./shapeRequests.js";
41
+ import { REQUESTABLE_SHAPE_KINDS, shapeFromRequest, shapeWithRequestedChanges, SHAPE_PARAMETERS, withResultAttributes, } from "./shapeRequests.js";
41
42
  import { canonicalizeShape, cleanNearZero, cleanRotationDegrees, cloneWorkplaneShapeTreeWithFreshIds, withHoleMode, } from "./workplaneShapes.js";
42
43
  const UNIQUE_CURVED_FACE_SURFACES = new Set(["cone", "sphere", "torus", "cylinder"]);
43
44
  const FACE_DRAFT_AXES = new Set(["x", "y", "z", "-x", "-y", "-z"]);
@@ -98,6 +99,12 @@ async function findUniqueDraftFace(shape, normal) {
98
99
  && faceNormalMatches(face.normal, normal));
99
100
  return matches.length === 1 ? matches[0] : null;
100
101
  }
102
+ function unmirroredDraftSource(shape) {
103
+ return { ...shape, mirrorX: undefined, mirrorY: undefined, mirrorZ: undefined };
104
+ }
105
+ function retainDraftSourceMirrors(result, source) {
106
+ return { ...result, mirrorX: source.mirrorX, mirrorY: source.mirrorY, mirrorZ: source.mirrorZ };
107
+ }
101
108
  /**
102
109
  * Carry face metadata across a parameter edit only when a face has a unique
103
110
  * counterpart. Curved faces may change their sampled normal as their defining
@@ -250,11 +257,12 @@ function alignEntry(shape, axis) {
250
257
  : { min: bounds.minY, max: bounds.maxY };
251
258
  return { id: shape.id, ...span, locked: shape.locked };
252
259
  }
253
- /** `ServerCadError` keeps its own wording and status; anything else has none to keep. */
260
+ /** `ServerCadError` keeps its own wording; unexpected kernel errors stay visible in server logs. */
254
261
  function cadFailure(error) {
255
- return error instanceof ServerCadError
256
- ? { reason: "cad_refused", message: error.message, status: error.status }
257
- : { reason: "cad_failed" };
262
+ if (error instanceof ServerCadError)
263
+ return { reason: "cad_refused", message: error.message, status: error.status };
264
+ console.error("Unexpected CAD kernel failure", error);
265
+ return { reason: "cad_failed" };
258
266
  }
259
267
  /**
260
268
  * One shape from one request, built before any lock is taken.
@@ -270,17 +278,6 @@ function cadFailure(error) {
270
278
  * and there is no reason to hold a project busy for it.
271
279
  */
272
280
  async function builtShape(request, index) {
273
- if (request.kind === "sketchSweep") {
274
- const invalidProfile = invalidSketchSweepProfile(request.profile);
275
- if (invalidProfile)
276
- return failed({ reason: "invalid_shape_request", index, message: invalidProfile });
277
- try {
278
- return ok(await sketchSweepShapeFromRequest(request));
279
- }
280
- catch (error) {
281
- return failed({ reason: "sweep_failed", index, message: error instanceof Error ? error.message : "The sketch sweep could not be built." });
282
- }
283
- }
284
281
  if (request.kind === "sketchSweep") {
285
282
  const invalidProfile = invalidSketchSweepProfile(request.profile);
286
283
  if (invalidProfile)
@@ -365,7 +362,7 @@ export function createEngine(store) {
365
362
  * them off the stored shapes, `booleans` is told which operands to treat as
366
363
  * holes. `holes` being undefined is the former.
367
364
  */
368
- async function combine(projectId, ids, holes) {
365
+ async function combine(projectId, ids, holes, attributes = {}) {
369
366
  return mutating(projectId, async (document) => {
370
367
  const resolved = shapesByIds(document, ids);
371
368
  if (!resolved.ok)
@@ -386,9 +383,10 @@ export function createEngine(store) {
386
383
  // from success. The editor treats the same case as a failed cut.
387
384
  if (outcome.cutRemovedNothing)
388
385
  return failed({ reason: "cut_removed_nothing" });
386
+ const group = withResultAttributes(outcome.group, attributes);
389
387
  const consumed = new Set(ids);
390
- const shapes = [...document.shapes.filter((shape) => !consumed.has(shape.id)), outcome.group];
391
- return ok({ shape: outcome.group, revision: await writeShapes(projectId, document, shapes) });
388
+ const shapes = [...document.shapes.filter((shape) => !consumed.has(shape.id)), group];
389
+ return ok({ shape: group, revision: await writeShapes(projectId, document, shapes) });
392
390
  });
393
391
  }
394
392
  /** The shared tail of `treat` and `shell`: run the kernel, store the result on the shape. */
@@ -595,7 +593,13 @@ export function createEngine(store) {
595
593
  }
596
594
  if (faceDraftChanged && existing.faceDraft && requestedFaceDraft
597
595
  && requestedFaceDraft.sourceFaceId !== existing.faceDraft.sourceFaceId) {
598
- const currentOutputFaces = await shapeFaces(existing);
596
+ let currentOutputFaces;
597
+ try {
598
+ currentOutputFaces = await shapeFaces(existing);
599
+ }
600
+ catch (error) {
601
+ return failed(cadFailure(error));
602
+ }
599
603
  if (!canReselectUntreatedExtrusionFace
600
604
  || !existing.faceDraftSourceFaceIds?.includes(requestedFaceDraft.sourceFaceId)
601
605
  || currentOutputFaces.filter((face) => face.id === requestedFaceDraft.sourceFaceId && face.surfaceType === "plane").length !== 1) {
@@ -633,8 +637,47 @@ export function createEngine(store) {
633
637
  let swept = existing;
634
638
  if ((replacingSketchProfile && isSketchExtrusion) || (draftPrismChanged && isSketchExtrusion)
635
639
  || (faceDraftChanged && isSketchExtrusion)) {
640
+ // A treatment is built from the extrusion at the size it was shown
641
+ // (#539), and its record keeps that shape as `before`. Replaying it on
642
+ // a new profile rebuilds at that same resize, or the edit would hand
643
+ // back the treatment at the size the sketch was first drawn.
644
+ const treatmentSource = existing.edgeTreatmentHistory?.[0]?.before;
645
+ const recordedTreatmentScale = hasExistingTreatment && !existing.faceDraft && existing.draftPrismAngle === undefined
646
+ && treatmentSource?.sketchOperation === "extrude" && treatmentSource.cadBrep
647
+ ? sketchResizeFactors(treatmentSource)
648
+ : undefined;
649
+ // A treatment recorded before #539 kept the resize in `before` but was
650
+ // built, and stored, at the drawn size. Its shape is nearer that drawn
651
+ // size than the resized one on every resized axis, and its edit keeps
652
+ // the old behaviour: rebuilt at the profile's own size.
653
+ const nearerResized = (current, resized, factor) => Math.abs(factor - 1) < 1e-8 || Math.abs(current - resized) < Math.abs(current - resized / factor);
654
+ const treatmentSourceScale = recordedTreatmentScale && treatmentSource
655
+ && nearerResized(existing.width, treatmentSource.width, recordedTreatmentScale.width)
656
+ && nearerResized(existing.depth, treatmentSource.depth, recordedTreatmentScale.depth)
657
+ && nearerResized(existing.height, treatmentSource.height, recordedTreatmentScale.height)
658
+ ? recordedTreatmentScale
659
+ : undefined;
636
660
  const sourceFrame = existing.faceDraftSourceFrame ?? existing.draftPrismSourceFrame ?? existing.cadBrepFrame;
637
- const sourceHeight = existing.draftPrismSourceHeight ?? existing.faceDraftSourceFrame?.height ?? existing.height;
661
+ const ratio = (drawn, source) => source && source > 0 && drawn > 0 ? drawn / source : 1;
662
+ const faceDraftSourceScale = (existing.faceDraft ? existing.faceDraftSourceScale : undefined) ?? (existing.faceDraft ? {
663
+ // Editor-written drafts predate this field. Their output bounds
664
+ // include the taper, so they cannot be used to infer the source
665
+ // resize ratio; these shapes were only retained before resize support.
666
+ width: 1, depth: 1, height: 1,
667
+ } : {
668
+ width: ratio(existing.width, sourceFrame?.width ?? existing.importedMesh?.baseWidth),
669
+ depth: ratio(existing.depth, sourceFrame?.depth ?? existing.importedMesh?.baseDepth),
670
+ height: ratio(existing.height, sourceFrame?.height ?? existing.importedMesh?.baseHeight),
671
+ });
672
+ const rebuildScale = existing.faceDraft || faceDraftChanged
673
+ ? faceDraftSourceScale
674
+ : treatmentSourceScale ?? { width: 1, depth: 1, height: 1 };
675
+ // The extrusion is rebuilt at the height it was drawn and then scaled
676
+ // by `rebuildScale`, as its width is. Rebuilding a height-resized
677
+ // extrusion at its shown height applied that resize twice (#546).
678
+ const sourceHeight = existing.draftPrismSourceHeight ?? existing.faceDraftSourceFrame?.height
679
+ ?? (treatmentSourceScale ? treatmentSource.height / treatmentSourceScale.height
680
+ : existing.height / rebuildScale.height);
638
681
  try {
639
682
  const rebuilt = await sketchExtrusionShapeFromRequest({
640
683
  id: existing.id,
@@ -644,9 +687,9 @@ export function createEngine(store) {
644
687
  hole: existing.hole,
645
688
  profile: replacingSketchProfile ? changes.sketchProfile : existing.sketchProfile,
646
689
  height: sourceHeight,
647
- x: existing.x - (sourceFrame?.x ?? 0),
648
- z: existing.z - (sourceFrame?.z ?? 0),
649
- elevation: (existing.elevation ?? 0) - (sourceFrame?.elevation ?? 0),
690
+ x: 0,
691
+ z: 0,
692
+ elevation: 0,
650
693
  rotation: existing.rotation,
651
694
  rotationX: existing.rotationX,
652
695
  rotationZ: existing.rotationZ,
@@ -656,6 +699,17 @@ export function createEngine(store) {
656
699
  const rebuiltSource = {
657
700
  ...sourceMetadata,
658
701
  ...rebuilt,
702
+ mirrorX: existing.mirrorX,
703
+ mirrorY: existing.mirrorY,
704
+ mirrorZ: existing.mirrorZ,
705
+ ...retainedDraftSourcePlacement(existing, rebuilt.cadBrepFrame, existing.faceDraft && !hasExistingTreatment ? existing.cadBrepFrame : undefined),
706
+ // A cleared draft's resize is not kept: the bounds carry it from here.
707
+ faceDraftSourceScale: clearingFaceDraft ? undefined
708
+ : existing.faceDraft || faceDraftChanged ? faceDraftSourceScale : existing.faceDraftSourceScale,
709
+ width: rebuilt.width * rebuildScale.width,
710
+ depth: rebuilt.depth * rebuildScale.depth,
711
+ height: rebuilt.height * rebuildScale.height,
712
+ size: Math.max(rebuilt.width * rebuildScale.width, rebuilt.depth * rebuildScale.depth),
659
713
  id: existing.id,
660
714
  sketchSourceId: existing.sketchSourceId ?? rebuilt.sketchSourceId,
661
715
  faceDraft: undefined,
@@ -699,20 +753,29 @@ export function createEngine(store) {
699
753
  source: replayedSource,
700
754
  request: requestedFaceDraft,
701
755
  profileChanged: replacingSketchProfile,
702
- }, (kernel, shape) => orientedSolidForShape(kernel, shape)));
756
+ }, (kernel, shape) => orientedSolidForShape(kernel, unmirroredDraftSource(shape))));
703
757
  const result = await applySketchFaceDraft(replayedSource, {
704
758
  ...requestedFaceDraft,
705
759
  sourceFaceId: currentFaceId,
706
760
  });
707
761
  // The same record the editor writes for a draft it makes (#384).
762
+ const drafted = shapeWithCadResult(replayedSource, result);
708
763
  swept = {
709
- ...shapeWithCadResult(replayedSource, result),
764
+ ...retainDraftSourceMirrors(drafted, replayedSource),
765
+ ...(!replayedSource.mirrorX && !replayedSource.mirrorY && !replayedSource.mirrorZ
766
+ ? retainedDraftSourcePlacement({
767
+ ...replayedSource,
768
+ faceDraftSourceFrame: undefined,
769
+ draftPrismSourceFrame: undefined,
770
+ }, drafted.cadBrepFrame)
771
+ : {}),
710
772
  ...retainedFaceDraftFields(replayedSource.faceIds, existing, { faceId: currentFaceId, normal: sourceFaceNormal, segmentMatch }, requestedFaceDraft),
773
+ faceDraftSourceScale,
711
774
  };
712
775
  }
713
776
  else if (draftPrismAngle !== undefined && !replayDraftAndTreatment) {
714
777
  swept = {
715
- ...shapeWithCadResult(replayedSource, await applyDraftPrism(replayedSource, { angle: draftPrismAngle })),
778
+ ...retainDraftSourceMirrors(shapeWithCadResult(replayedSource, await applyDraftPrism(replayedSource, { angle: draftPrismAngle })), replayedSource),
716
779
  draftPrismAngle,
717
780
  };
718
781
  }
@@ -762,6 +825,9 @@ export function createEngine(store) {
762
825
  const rebuiltSweep = {
763
826
  ...sourceMetadata,
764
827
  ...resized,
828
+ mirrorX: existing.mirrorX,
829
+ mirrorY: existing.mirrorY,
830
+ mirrorZ: existing.mirrorZ,
765
831
  id: existing.id,
766
832
  sketchSourceId: existing.sketchSourceId ?? rebuilt.sketchSourceId,
767
833
  faceIds: refreshedFaceIds,
@@ -790,13 +856,13 @@ export function createEngine(store) {
790
856
  ? existing.faceDraft.sourceFaceNormal
791
857
  : undefined;
792
858
  const selectedOnPrior = priorReference ? undefined
793
- : (await shapeFaces(existing, isExactSketchRevolve ? { topology: "exact" } : undefined))
859
+ : (await shapeFaces(unmirroredDraftSource(existing), isExactSketchRevolve ? { topology: "exact" } : undefined))
794
860
  .find((face) => face.id === requestedFaceDraft.sourceFaceId);
795
861
  const sourceFaceNormal = priorReference ?? selectedOnPrior?.normal;
796
862
  if (!sourceFaceNormal || selectedOnPrior && selectedOnPrior.surfaceType !== "plane") {
797
863
  throw new Error("The selected source face has no stable planar reference.");
798
864
  }
799
- const currentFace = await findUniqueDraftFace(replayedSource, sourceFaceNormal);
865
+ const currentFace = await findUniqueDraftFace(unmirroredDraftSource(replayedSource), sourceFaceNormal);
800
866
  if (!currentFace)
801
867
  throw new Error("The selected source face is missing or ambiguous after the sketch edit.");
802
868
  const faceDraft = { ...requestedFaceDraft, sourceFaceNormal };
@@ -805,7 +871,7 @@ export function createEngine(store) {
805
871
  sourceFaceId: currentFace.id,
806
872
  });
807
873
  swept = {
808
- ...shapeWithCadResult(replayedSource, result),
874
+ ...retainDraftSourceMirrors(shapeWithCadResult(replayedSource, result), replayedSource),
809
875
  faceDraft,
810
876
  faceDraftSourceFrame: existing.faceDraftSourceFrame ?? existing.cadBrepFrame,
811
877
  };
@@ -841,18 +907,21 @@ export function createEngine(store) {
841
907
  const sourceScale = existing.faceDraftSourceScale ?? revolveSourceScale(existing);
842
908
  const rebuilt = resizeRebuiltRevolve(rebuiltAtNaturalScale, sourceScale);
843
909
  if (requestedFaceDraft) {
844
- const exactBase = shapeWithCadResult(rebuilt, await exactPlainSketchRevolveResult(rebuilt));
910
+ const exactBase = {
911
+ ...retainDraftSourceMirrors(shapeWithCadResult(rebuilt, await exactPlainSketchRevolveResult(rebuilt)), rebuilt),
912
+ faceDraftSourceScale: sourceScale,
913
+ };
845
914
  const priorReference = existing.faceDraft?.sourceFaceId === requestedFaceDraft.sourceFaceId
846
915
  ? existing.faceDraft.sourceFaceNormal
847
916
  : undefined;
848
917
  const selectedOnPrior = priorReference ? undefined
849
- : (await shapeFaces(existing, { topology: "exact" }))
918
+ : (await shapeFaces(unmirroredDraftSource(existing), { topology: "exact" }))
850
919
  .find((face) => face.id === requestedFaceDraft.sourceFaceId);
851
920
  const sourceFaceNormal = priorReference ?? selectedOnPrior?.normal;
852
921
  if (!sourceFaceNormal || selectedOnPrior && selectedOnPrior.surfaceType !== "plane") {
853
922
  throw new Error("The selected source face has no stable planar reference.");
854
923
  }
855
- const currentFace = await findUniqueDraftFace(exactBase, sourceFaceNormal);
924
+ const currentFace = await findUniqueDraftFace(unmirroredDraftSource(exactBase), sourceFaceNormal);
856
925
  if (!currentFace)
857
926
  throw new Error("The selected source face is missing or ambiguous after the sketch edit.");
858
927
  const faceDraft = { ...requestedFaceDraft, sourceFaceNormal };
@@ -861,7 +930,7 @@ export function createEngine(store) {
861
930
  sourceFaceId: currentFace.id,
862
931
  });
863
932
  swept = {
864
- ...shapeWithCadResult(exactBase, result),
933
+ ...retainDraftSourceMirrors(shapeWithCadResult(exactBase, result), exactBase),
865
934
  faceDraft,
866
935
  faceDraftSourceFrame: existing.faceDraftSourceFrame ?? existing.cadBrepFrame ?? exactBase.cadBrepFrame,
867
936
  faceDraftSourceScale: sourceScale,
@@ -1119,8 +1188,8 @@ export function createEngine(store) {
1119
1188
  const { revision } = await store.write(projectId, { ...document, assets }, shapes);
1120
1189
  return ok({ shape: imported.shape, revision });
1121
1190
  }),
1122
- cut: (projectId, request) => combine(projectId, [...request.solidIds, ...request.holeIds], new Set(request.holeIds)),
1123
- group: (projectId, ids) => combine(projectId, ids, undefined),
1191
+ cut: (projectId, request) => combine(projectId, [...request.solidIds, ...request.holeIds], new Set(request.holeIds), request),
1192
+ group: (projectId, ids, result) => combine(projectId, ids, undefined, result),
1124
1193
  ungroup: (projectId, shapeId) => mutating(projectId, async (document) => {
1125
1194
  const found = oneShape(document, shapeId);
1126
1195
  if (!found.ok)
@@ -1244,47 +1313,56 @@ export function createEngine(store) {
1244
1313
  return failed(cadFailure(error));
1245
1314
  }
1246
1315
  }),
1247
- treat: (projectId, shapeId, treatment) => cadTreatment(projectId, shapeId, (treatment.edges === undefined || treatment.edges.length === 0)
1248
- && treatment.kind === "chamfer" && (treatment.angle === undefined || Math.abs(treatment.angle - 45) < 0.001)
1249
- ? "all-edge-chamfer-treatment"
1250
- : (treatment.edges === undefined || treatment.edges.length === 0)
1251
- && treatment.kind === "fillet"
1252
- ? "all-edge-treatment"
1253
- : "edge-treatment", async (shape) => {
1254
- const treated = shapeWithCadResult(shape, await applyEdgeTreatment(shape, treatment));
1255
- // Every treatment is recorded, as the editor records its own. A source
1256
- // edit rebuilds the solid from the sketch and replays this record; with
1257
- // no record it rebuilt a bare solid and silently dropped the treatment.
1258
- // A selected-edge or non-45-degree chamfer record cannot be replayed,
1259
- // so a later source edit is refused rather than lossy.
1260
- const allEdges = treatment.edges === undefined || treatment.edges.length === 0;
1261
- const feature = {
1262
- kind: treatment.kind,
1263
- amount: treatment.amount,
1264
- edgeCount: allEdges ? await allEdgeTreatmentEdgeCount(shape, treatment.kind) : treatment.edges.length,
1265
- ...(allEdges ? { allEdges: true } : {}),
1266
- ...(treatment.kind === "chamfer" ? { chamferAngle: treatment.angle ?? 45 } : {}),
1267
- };
1268
- // The snapshot drops its own history, as the editor's does, so each
1269
- // treatment does not nest a copy of every earlier one.
1270
- const { edgeTreatmentHistory: _ownHistory, ...before } = shape;
1271
- return {
1272
- ...treated,
1273
- edgeTreatments: [...(shape.edgeTreatments ?? []), feature],
1274
- edgeTreatmentHistory: [
1275
- ...(shape.edgeTreatmentHistory ?? []),
1276
- {
1277
- id: createLocalId("edge-history"),
1278
- createdAt: Date.now(),
1279
- feature,
1280
- before: structuredClone(before),
1281
- appliedFrame: edgeTreatmentAppliedFrame(treated),
1282
- },
1283
- ],
1284
- };
1285
- }),
1316
+ treat: (projectId, shapeId, requested) => {
1317
+ // An edge named twice is one edge: rounding it once is what the caller
1318
+ // gets, so it is counted, and recorded, once.
1319
+ const treatment = requested.edges ? { ...requested, edges: [...new Set(requested.edges)] } : requested;
1320
+ return cadTreatment(projectId, shapeId, (treatment.edges === undefined || treatment.edges.length === 0)
1321
+ && treatment.kind === "chamfer" && (treatment.angle === undefined || Math.abs(treatment.angle - 45) < 0.001)
1322
+ ? "all-edge-chamfer-treatment"
1323
+ : (treatment.edges === undefined || treatment.edges.length === 0)
1324
+ && treatment.kind === "fillet"
1325
+ ? "all-edge-treatment"
1326
+ : "edge-treatment", async (shape) => {
1327
+ const treated = shapeWithCadResult(shape, await applyEdgeTreatment(shape, treatment));
1328
+ // Every treatment is recorded, as the editor records its own. A source
1329
+ // edit rebuilds the solid from the sketch and replays this record; with
1330
+ // no record it rebuilt a bare solid and silently dropped the treatment.
1331
+ // Selected edges are recorded by sketch lineage -- segment and cap --
1332
+ // when every one of them has exactly one, never by their transient
1333
+ // indexes (#350). A selected-edge record without lineage cannot be
1334
+ // replayed, so a later source edit is refused rather than lossy.
1335
+ const allEdges = treatment.edges === undefined || treatment.edges.length === 0;
1336
+ const lineage = allEdges ? undefined : await sketchSourceEdgesForEdgeIndexes(shape, treatment.edges);
1337
+ const feature = {
1338
+ kind: treatment.kind,
1339
+ amount: treatment.amount,
1340
+ edgeCount: allEdges ? await allEdgeTreatmentEdgeCount(shape, treatment.kind) : treatment.edges.length,
1341
+ ...(allEdges ? { allEdges: true } : {}),
1342
+ ...(treatment.kind === "chamfer" ? { chamferAngle: treatment.angle ?? 45 } : {}),
1343
+ ...(lineage?.status === "matched" ? { sourceEdges: lineage.sourceEdges } : {}),
1344
+ };
1345
+ // The snapshot drops its own history, as the editor's does, so each
1346
+ // treatment does not nest a copy of every earlier one.
1347
+ const { edgeTreatmentHistory: _ownHistory, ...before } = shape;
1348
+ return {
1349
+ ...treated,
1350
+ edgeTreatments: [...(shape.edgeTreatments ?? []), feature],
1351
+ edgeTreatmentHistory: [
1352
+ ...(shape.edgeTreatmentHistory ?? []),
1353
+ {
1354
+ id: createLocalId("edge-history"),
1355
+ createdAt: Date.now(),
1356
+ feature,
1357
+ before: structuredClone(before),
1358
+ appliedFrame: edgeTreatmentAppliedFrame(treated),
1359
+ },
1360
+ ],
1361
+ };
1362
+ });
1363
+ },
1286
1364
  draftPrism: (projectId, shapeId, angle) => cadTreatment(projectId, shapeId, "prism-draft", async (shape) => ({
1287
- ...shapeWithCadResult(shape, await applyDraftPrism(shape, { angle })),
1365
+ ...retainDraftSourceMirrors(shapeWithCadResult(shape, await applyDraftPrism(shape, { angle })), shape),
1288
1366
  ...retainedPrismDraftFields(shape, angle),
1289
1367
  })),
1290
1368
  mirrorBaked: (projectId, ids, axis) => mutating(projectId, async (document) => {