@constructorfleet/sketchforge-engine 1.19.0 → 1.20.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 (42) hide show
  1. package/README.md +3 -3
  2. package/dist/domain/sketchforge.d.ts +39 -0
  3. package/dist/domain/sketchforge.d.ts.map +1 -1
  4. package/dist/engine.d.ts +1 -1
  5. package/dist/engine.d.ts.map +1 -1
  6. package/dist/engine.js +345 -29
  7. package/dist/engine.js.map +1 -1
  8. package/dist/faceLineage.d.ts +10 -0
  9. package/dist/faceLineage.d.ts.map +1 -0
  10. package/dist/faceLineage.js +84 -0
  11. package/dist/faceLineage.js.map +1 -0
  12. package/dist/parametricRecompute.d.ts +2 -0
  13. package/dist/parametricRecompute.d.ts.map +1 -1
  14. package/dist/parametricRecompute.js +69 -0
  15. package/dist/parametricRecompute.js.map +1 -1
  16. package/dist/revolveRequests.d.ts +10 -0
  17. package/dist/revolveRequests.d.ts.map +1 -1
  18. package/dist/serverCadOperations.d.ts +54 -3
  19. package/dist/serverCadOperations.d.ts.map +1 -1
  20. package/dist/serverCadOperations.js +224 -6
  21. package/dist/serverCadOperations.js.map +1 -1
  22. package/dist/serverCadShell.d.ts +3 -1
  23. package/dist/serverCadShell.d.ts.map +1 -1
  24. package/dist/serverCadShell.js +15 -5
  25. package/dist/serverCadShell.js.map +1 -1
  26. package/dist/sketchExtrusionRequest.d.ts.map +1 -1
  27. package/dist/sketchExtrusionRequest.js +1 -0
  28. package/dist/sketchExtrusionRequest.js.map +1 -1
  29. package/dist/skfProject.d.ts.map +1 -1
  30. package/dist/skfProject.js +85 -8
  31. package/dist/skfProject.js.map +1 -1
  32. package/dist/stepComponentMetadata.d.ts.map +1 -1
  33. package/dist/stepComponentMetadata.js +55 -2
  34. package/dist/stepComponentMetadata.js.map +1 -1
  35. package/dist/stepExport.d.ts.map +1 -1
  36. package/dist/stepExport.js +54 -8
  37. package/dist/stepExport.js.map +1 -1
  38. package/dist/stepGlbColors.d.ts.map +1 -1
  39. package/dist/stepGlbColors.js +10 -2
  40. package/dist/stepGlbColors.js.map +1 -1
  41. package/dist/support/lib/operator-configuration.json +10 -0
  42. package/package.json +1 -1
package/dist/engine.js CHANGED
@@ -14,7 +14,7 @@
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 { applyEdgeTreatment, applyBakedMirror, applyDraftPrism, shapeEdges, shapeWithCadResult, ServerCadError, } from "./serverCadOperations.js";
17
+ import { allEdgeTreatmentEdgeCount, applyEdgeTreatment, applyBakedMirror, applyDraftPrism, applySketchFaceDraft, exactPlainSketchRevolveResult, isPlainFullTurnSketchRevolve, shapeEdges, shapeWithCadResult, ServerCadError, } from "./serverCadOperations.js";
18
18
  import { applyShell, shapeFaces, } from "./serverCadShell.js";
19
19
  import { measureBetween as measureBetweenEntities, shapeMeasurements, } from "./serverCadMeasure.js";
20
20
  import { shapeProperties } from "./serverCadProperties.js";
@@ -32,11 +32,52 @@ import { serverManifoldRuntime } from "./serverManifoldRuntime.js";
32
32
  import { createLocalId } from "./localIds.js";
33
33
  import { invalidSketchSweepProfile, sketchSweepShapeFromRequest } from "./sketchSweepRequest.js";
34
34
  import { sketchExtrusionShapeFromRequest } from "./sketchExtrusionRequest.js";
35
- import { findSketchOutput, recomputeSketchOutput } from "./parametricRecompute.js";
35
+ import { findSketchOutput, recomputeSketchOutput, replaySingleAllEdgeTreatment } from "./parametricRecompute.js";
36
36
  import { boundsForShapes, footprintCenter, shapeBounds } from "./shapeBounds.js";
37
37
  import { REQUESTABLE_SHAPE_KINDS, shapeFromRequest, shapeWithRequestedChanges, SHAPE_PARAMETERS, } from "./shapeRequests.js";
38
38
  import { canonicalizeShape, cleanNearZero, cleanRotationDegrees, cloneWorkplaneShapeTreeWithFreshIds, withHoleMode, } from "./workplaneShapes.js";
39
39
  const UNIQUE_CURVED_FACE_SURFACES = new Set(["cone", "sphere", "torus", "cylinder"]);
40
+ const FACE_DRAFT_AXES = new Set(["x", "y", "z", "-x", "-y", "-z"]);
41
+ function faceDraftInput(value) {
42
+ if (!value || typeof value !== "object" || Array.isArray(value))
43
+ return null;
44
+ const draft = value;
45
+ if (typeof draft.sourceFaceId !== "string" || !draft.sourceFaceId.trim()
46
+ || typeof draft.angle !== "number" || !Number.isFinite(draft.angle) || draft.angle === 0 || Math.abs(draft.angle) >= 45
47
+ || typeof draft.pullDirection !== "string" || !FACE_DRAFT_AXES.has(draft.pullDirection))
48
+ return null;
49
+ return {
50
+ sourceFaceId: draft.sourceFaceId,
51
+ angle: draft.angle,
52
+ pullDirection: draft.pullDirection,
53
+ };
54
+ }
55
+ function revolveSourceScale(shape) {
56
+ const mesh = shape.importedMesh;
57
+ const ratio = (drawn, natural) => natural && natural > 0 ? drawn / natural : 1;
58
+ return {
59
+ width: ratio(shape.width, mesh?.baseWidth),
60
+ depth: ratio(shape.depth, mesh?.baseDepth),
61
+ height: ratio(shape.height, mesh?.baseHeight),
62
+ };
63
+ }
64
+ function resizeRebuiltRevolve(shape, scale) {
65
+ const mesh = shape.importedMesh;
66
+ if (!mesh)
67
+ return shape;
68
+ const width = Math.max(0.01, (mesh.baseWidth ?? shape.width) * scale.width);
69
+ const depth = Math.max(0.01, (mesh.baseDepth ?? shape.depth) * scale.depth);
70
+ const height = Math.max(0.01, (mesh.baseHeight ?? shape.height) * scale.height);
71
+ return { ...shape, width, depth, height, size: Math.max(width, depth) };
72
+ }
73
+ function faceNormalMatches(left, right) {
74
+ return Math.hypot(left.x - right.x, left.y - right.y, left.z - right.z) <= 1e-5;
75
+ }
76
+ async function findUniqueDraftFace(shape, normal) {
77
+ const matches = (await shapeFaces(shape)).filter((face) => face.surfaceType === "plane" && face.normal
78
+ && faceNormalMatches(face.normal, normal));
79
+ return matches.length === 1 ? matches[0] : null;
80
+ }
40
81
  /**
41
82
  * Carry face metadata across a parameter edit only when a face has a unique
42
83
  * counterpart. Curved faces may change their sampled normal as their defining
@@ -331,13 +372,33 @@ export function createEngine(store) {
331
372
  });
332
373
  }
333
374
  /** The shared tail of `treat` and `shell`: run the kernel, store the result on the shape. */
334
- async function cadTreatment(projectId, shapeId, run) {
375
+ async function cadTreatment(projectId, shapeId, operation, run) {
335
376
  return mutating(projectId, async (document) => {
336
377
  const found = oneShape(document, shapeId);
337
378
  if (!found.ok)
338
379
  return found;
339
380
  if (found.value.locked)
340
381
  return failed({ reason: "shape_locked", ids: [shapeId] });
382
+ const hasEdgeTreatments = (found.value.edgeTreatments?.length ?? 0) > 0
383
+ || (found.value.edgeTreatmentHistory?.length ?? 0) > 0;
384
+ const hasDraftPrism = found.value.draftPrismAngle !== undefined;
385
+ const replayableDraftTreatment = operation === "all-edge-treatment"
386
+ && !hasEdgeTreatments
387
+ && found.value.sketchOperation === "extrude"
388
+ && Boolean(found.value.sketchProfile)
389
+ && hasDraftPrism;
390
+ const incompatible = found.value.faceDraft !== undefined
391
+ || operation === "edge-treatment" && hasDraftPrism
392
+ || operation === "all-edge-treatment" && hasDraftPrism && !replayableDraftTreatment
393
+ || operation === "prism-draft" && hasEdgeTreatments
394
+ || operation === "shell" && (found.value.draftPrismAngle !== undefined || hasEdgeTreatments);
395
+ if (incompatible) {
396
+ return failed({
397
+ reason: "cad_refused",
398
+ message: "Face drafts cannot be combined with edge treatments, prism drafts, or shelling. Draft prisms only support one all-edge treatment on a retained sketch extrusion; shelling cannot be combined with persisted draft or treatment features.",
399
+ status: 422,
400
+ });
401
+ }
341
402
  let changed;
342
403
  try {
343
404
  changed = await run(found.value);
@@ -413,11 +474,15 @@ export function createEngine(store) {
413
474
  changeShape: (projectId, shapeId, changes) => mutating(projectId, async (document) => {
414
475
  const byOutputId = oneShape(document, shapeId);
415
476
  // A persisted sketch source is an input node, not a renderable shape.
416
- // When a caller edits that node's profile, resolve its dependent sweep
417
- // output and rebuild it before the single project write below. The
477
+ // When a caller edits that node's profile or revolve settings, resolve
478
+ // its dependent output and rebuild it before the single project write. The
418
479
  // output keeps its own stable object ID; SKF export derives the source
419
480
  // node from the synchronized profile on that output.
420
- const sourceOutputs = !byOutputId.ok && changes.sketchProfile !== undefined
481
+ const sourceFeatureInputChanged = changes.sketchProfile !== undefined
482
+ || changes.draftPrismAngle !== undefined
483
+ || Object.hasOwn(changes, "faceDraft")
484
+ || REVOLVE_CHANGE_FIELDS.some((field) => changes[field] !== undefined);
485
+ const sourceOutputs = !byOutputId.ok && sourceFeatureInputChanged
421
486
  ? findSketchOutput(document.shapes, shapeId)
422
487
  : [];
423
488
  const sourceSketchTarget = !byOutputId.ok && sourceOutputs.length === 1;
@@ -430,7 +495,9 @@ export function createEngine(store) {
430
495
  return found;
431
496
  const existing = found.value;
432
497
  if (sourceSketchTarget) {
433
- const outputFields = Object.keys(changes).filter((field) => field !== "sketchProfile");
498
+ const isRevolveSource = isRevolveShape(existing);
499
+ const outputFields = Object.keys(changes).filter((field) => field !== "sketchProfile" && field !== "draftPrismAngle" && field !== "faceDraft"
500
+ && !(isRevolveSource && REVOLVE_CHANGE_FIELDS.includes(field)));
434
501
  if (outputFields.length > 0) {
435
502
  return failed({ reason: "unapplicable_fields", kind: existing.kind, fields: outputFields, because: "not_this_kind" });
436
503
  }
@@ -441,9 +508,48 @@ export function createEngine(store) {
441
508
  if (existing.locked && changes.locked !== false)
442
509
  return failed({ reason: "shape_locked", ids: [shapeId] });
443
510
  const replacingSketchProfile = changes.sketchProfile !== undefined;
511
+ const faceDraftChanged = Object.hasOwn(changes, "faceDraft");
512
+ const clearingFaceDraft = faceDraftChanged && changes.faceDraft === null && existing.faceDraft !== undefined;
513
+ const requestedFaceDraft = faceDraftChanged
514
+ ? changes.faceDraft === null ? undefined : faceDraftInput(changes.faceDraft)
515
+ : existing.faceDraft;
444
516
  const isSketchSweep = existing.sketchOperation === "sweep" && Boolean(existing.sketchProfile);
445
517
  const isSketchExtrusion = existing.sketchOperation === "extrude" && Boolean(existing.sketchProfile);
446
- if (replacingSketchProfile && !isSketchSweep && !isSketchExtrusion) {
518
+ const isSketchRevolve = isRevolveShape(existing);
519
+ if (faceDraftChanged && changes.faceDraft !== null && !requestedFaceDraft) {
520
+ return failed({ reason: "invalid_shape_request", message: "A face draft requires a stable source face ID, a non-zero angle below 45 degrees, and a signed local pull axis." });
521
+ }
522
+ const isExactSketchRevolve = isPlainFullTurnSketchRevolve(existing);
523
+ if ((faceDraftChanged || (replacingSketchProfile && existing.faceDraft))
524
+ && !isSketchExtrusion && !isSketchSweep && !isExactSketchRevolve) {
525
+ return failed({ reason: "unapplicable_fields", kind: existing.kind, fields: ["faceDraft"], because: "not_this_kind" });
526
+ }
527
+ if (requestedFaceDraft
528
+ && ((existing.edgeTreatments?.length ?? 0) > 0 || (existing.edgeTreatmentHistory?.length ?? 0) > 0
529
+ || existing.draftPrismAngle !== undefined)) {
530
+ return failed({ reason: "invalid_shape_request", message: "Face drafts cannot be combined with prism drafts or edge treatments yet." });
531
+ }
532
+ if (faceDraftChanged && existing.faceDraft && requestedFaceDraft
533
+ && requestedFaceDraft.sourceFaceId !== existing.faceDraft.sourceFaceId) {
534
+ return failed({ reason: "invalid_shape_request", message: "Clear the existing face draft before selecting a different source face." });
535
+ }
536
+ const draftPrismChanged = changes.draftPrismAngle !== undefined;
537
+ const requestedDraftPrismAngle = typeof changes.draftPrismAngle === "number" ? changes.draftPrismAngle : undefined;
538
+ if (draftPrismChanged && (!isSketchExtrusion || requestedDraftPrismAngle === undefined || !Number.isFinite(requestedDraftPrismAngle)
539
+ || requestedDraftPrismAngle === 0 || Math.abs(requestedDraftPrismAngle) >= 45)) {
540
+ return failed({
541
+ reason: "invalid_shape_request",
542
+ message: "A draft prism angle must be non-zero and between -45 and 45 degrees on a sketch extrusion.",
543
+ });
544
+ }
545
+ if (draftPrismChanged && requestedFaceDraft) {
546
+ return failed({ reason: "invalid_shape_request", message: "Face drafts cannot be combined with prism drafts yet." });
547
+ }
548
+ if (draftPrismChanged && existing.draftPrismAngle === undefined
549
+ && ((existing.edgeTreatments?.length ?? 0) > 0 || (existing.edgeTreatmentHistory?.length ?? 0) > 0)) {
550
+ return failed({ reason: "invalid_shape_request", message: "A prism draft cannot be added after an edge treatment." });
551
+ }
552
+ if (replacingSketchProfile && !isSketchSweep && !isSketchExtrusion && !isSketchRevolve) {
447
553
  return failed({ reason: "unapplicable_fields", kind: existing.kind, fields: ["sketchProfile"], because: "no_sweep" });
448
554
  }
449
555
  const sweptShape = isRevolveShape(existing);
@@ -456,8 +562,10 @@ export function createEngine(store) {
456
562
  // leave a thread's pitch readable but not writable.
457
563
  const revolveChanges = sweptShape ? REVOLVE_CHANGE_FIELDS.filter((field) => changes[field] !== undefined) : [];
458
564
  let swept = existing;
459
- if (replacingSketchProfile && isSketchExtrusion) {
460
- const frame = existing.cadBrepFrame;
565
+ if ((replacingSketchProfile && isSketchExtrusion) || (draftPrismChanged && isSketchExtrusion)
566
+ || (faceDraftChanged && isSketchExtrusion)) {
567
+ const sourceFrame = existing.faceDraftSourceFrame ?? existing.draftPrismSourceFrame ?? existing.cadBrepFrame;
568
+ const sourceHeight = existing.draftPrismSourceHeight ?? existing.faceDraftSourceFrame?.height ?? existing.height;
461
569
  try {
462
570
  const rebuilt = await sketchExtrusionShapeFromRequest({
463
571
  id: existing.id,
@@ -465,34 +573,89 @@ export function createEngine(store) {
465
573
  name: existing.name,
466
574
  color: existing.color,
467
575
  hole: existing.hole,
468
- profile: changes.sketchProfile,
469
- height: existing.height,
470
- x: existing.x - (frame?.x ?? 0),
471
- z: existing.z - (frame?.z ?? 0),
472
- elevation: (existing.elevation ?? 0) - (frame?.elevation ?? 0),
576
+ profile: replacingSketchProfile ? changes.sketchProfile : existing.sketchProfile,
577
+ height: sourceHeight,
578
+ x: existing.x - (sourceFrame?.x ?? 0),
579
+ z: existing.z - (sourceFrame?.z ?? 0),
580
+ elevation: (existing.elevation ?? 0) - (sourceFrame?.elevation ?? 0),
473
581
  rotation: existing.rotation,
474
582
  rotationX: existing.rotationX,
475
583
  rotationZ: existing.rotationZ,
476
584
  });
477
585
  const { faceIds: _oldFaceIds, faceColors: _faceColors, edgeTreatments: _edgeTreatments, edgeTreatmentHistory: _edgeTreatmentHistory, cadDisplayEdges: _cadDisplayEdges, cadDisplayEdgesVersion: _cadDisplayEdgesVersion, ...sourceMetadata } = existing;
478
- swept = { ...sourceMetadata, ...rebuilt, id: existing.id, sketchSourceId: existing.sketchSourceId ?? rebuilt.sketchSourceId };
586
+ const draftPrismAngle = draftPrismChanged ? requestedDraftPrismAngle : existing.draftPrismAngle;
587
+ const rebuiltSource = {
588
+ ...sourceMetadata,
589
+ ...rebuilt,
590
+ id: existing.id,
591
+ sketchSourceId: existing.sketchSourceId ?? rebuilt.sketchSourceId,
592
+ faceDraft: undefined,
593
+ faceDraftSourceFrame: undefined,
594
+ ...(draftPrismAngle === undefined ? {} : {
595
+ draftPrismSourceHeight: sourceHeight,
596
+ ...(sourceFrame ? { draftPrismSourceFrame: sourceFrame } : {}),
597
+ }),
598
+ };
599
+ const hasRetainedTreatments = (existing.edgeTreatments?.length ?? 0) > 0
600
+ || (existing.edgeTreatmentHistory?.length ?? 0) > 0;
601
+ const replayDraftAndTreatment = hasRetainedTreatments
602
+ && draftPrismAngle !== undefined
603
+ && (replacingSketchProfile || clearingFaceDraft || draftPrismChanged);
604
+ const replayedSource = (replacingSketchProfile || clearingFaceDraft || draftPrismChanged) && hasRetainedTreatments
605
+ ? await replaySingleAllEdgeTreatment(clearingFaceDraft ? { ...existing, faceDraft: undefined } : existing, rebuiltSource, draftPrismChanged ? requestedDraftPrismAngle : undefined)
606
+ : rebuiltSource;
607
+ if (requestedFaceDraft) {
608
+ const priorReference = existing.faceDraft?.sourceFaceId === requestedFaceDraft.sourceFaceId
609
+ ? existing.faceDraft.sourceFaceNormal
610
+ : undefined;
611
+ const selectedOnPrior = priorReference ? undefined
612
+ : (await shapeFaces(existing, isExactSketchRevolve ? { topology: "exact" } : undefined))
613
+ .find((face) => face.id === requestedFaceDraft.sourceFaceId);
614
+ const sourceFaceNormal = priorReference ?? selectedOnPrior?.normal;
615
+ if (!sourceFaceNormal || selectedOnPrior && selectedOnPrior.surfaceType !== "plane") {
616
+ throw new Error("The selected source face has no stable planar reference.");
617
+ }
618
+ const currentFace = await findUniqueDraftFace(replayedSource, sourceFaceNormal);
619
+ if (!currentFace)
620
+ throw new Error("The selected source face is missing or ambiguous after the sketch edit.");
621
+ const faceDraft = { ...requestedFaceDraft, sourceFaceNormal };
622
+ const result = await applySketchFaceDraft(replayedSource, {
623
+ ...requestedFaceDraft,
624
+ sourceFaceId: currentFace.id,
625
+ });
626
+ swept = {
627
+ ...shapeWithCadResult(replayedSource, result),
628
+ faceDraft,
629
+ faceDraftSourceFrame: existing.faceDraftSourceFrame ?? existing.cadBrepFrame,
630
+ };
631
+ }
632
+ else if (draftPrismAngle !== undefined && !replayDraftAndTreatment) {
633
+ swept = {
634
+ ...shapeWithCadResult(replayedSource, await applyDraftPrism(replayedSource, { angle: draftPrismAngle })),
635
+ draftPrismAngle,
636
+ };
637
+ }
638
+ else {
639
+ swept = replayedSource;
640
+ }
479
641
  }
480
642
  catch (error) {
481
643
  return failed({ reason: "sweep_failed", message: error instanceof Error ? error.message : "The sketch extrusion could not be rebuilt." });
482
644
  }
483
645
  }
484
- if (replacingSketchProfile && isSketchSweep) {
485
- const invalidProfile = invalidSketchSweepProfile(changes.sketchProfile);
646
+ if ((replacingSketchProfile || faceDraftChanged) && isSketchSweep) {
647
+ const profileToBuild = replacingSketchProfile ? changes.sketchProfile : existing.sketchProfile;
648
+ const invalidProfile = replacingSketchProfile ? invalidSketchSweepProfile(profileToBuild) : undefined;
486
649
  if (invalidProfile)
487
650
  return failed({ reason: "invalid_shape_request", message: invalidProfile });
488
- const frame = existing.cadBrepFrame;
651
+ const frame = existing.faceDraftSourceFrame ?? existing.cadBrepFrame;
489
652
  try {
490
653
  const rebuilt = await sketchSweepShapeFromRequest({
491
654
  id: existing.id,
492
655
  name: existing.name,
493
656
  color: existing.color,
494
657
  hole: existing.hole,
495
- profile: changes.sketchProfile,
658
+ profile: profileToBuild,
496
659
  x: existing.x - (frame?.x ?? 0),
497
660
  z: existing.z - (frame?.z ?? 0),
498
661
  elevation: (existing.elevation ?? 0) - (frame?.elevation ?? 0),
@@ -514,14 +677,47 @@ export function createEngine(store) {
514
677
  resized.size = Math.max(resized.width, resized.depth);
515
678
  const refreshedFaceIds = (await shapeFaces(rebuilt))
516
679
  .map((face) => `face_${createLocalId(`${existing.id}-${face.index}`)}`);
517
- const { faceIds: _oldFaceIds, faceColors: _faceColors, edgeTreatments: _edgeTreatments, edgeTreatmentHistory: _edgeTreatmentHistory, cadDisplayEdges: _cadDisplayEdges, cadDisplayEdgesVersion: _cadDisplayEdgesVersion, ...sourceMetadata } = existing;
518
- swept = {
680
+ const { faceIds: _oldFaceIds, faceColors: _faceColors, edgeTreatments: _edgeTreatments, edgeTreatmentHistory: _edgeTreatmentHistory, cadDisplayEdges: _cadDisplayEdges, cadDisplayEdgesVersion: _cadDisplayEdgesVersion, faceDraft: _faceDraft, faceDraftSourceFrame: _faceDraftSourceFrame, ...sourceMetadata } = existing;
681
+ const rebuiltSweep = {
519
682
  ...sourceMetadata,
520
683
  ...resized,
521
684
  id: existing.id,
522
685
  sketchSourceId: existing.sketchSourceId ?? rebuilt.sketchSourceId,
523
686
  faceIds: refreshedFaceIds,
687
+ faceDraft: undefined,
688
+ faceDraftSourceFrame: undefined,
524
689
  };
690
+ const replayedSource = replacingSketchProfile || clearingFaceDraft
691
+ ? await replaySingleAllEdgeTreatment(clearingFaceDraft ? { ...existing, faceDraft: undefined } : existing, rebuiltSweep)
692
+ : rebuiltSweep;
693
+ if (requestedFaceDraft) {
694
+ const priorReference = existing.faceDraft?.sourceFaceId === requestedFaceDraft.sourceFaceId
695
+ ? existing.faceDraft.sourceFaceNormal
696
+ : undefined;
697
+ const selectedOnPrior = priorReference ? undefined
698
+ : (await shapeFaces(existing, isExactSketchRevolve ? { topology: "exact" } : undefined))
699
+ .find((face) => face.id === requestedFaceDraft.sourceFaceId);
700
+ const sourceFaceNormal = priorReference ?? selectedOnPrior?.normal;
701
+ if (!sourceFaceNormal || selectedOnPrior && selectedOnPrior.surfaceType !== "plane") {
702
+ throw new Error("The selected source face has no stable planar reference.");
703
+ }
704
+ const currentFace = await findUniqueDraftFace(replayedSource, sourceFaceNormal);
705
+ if (!currentFace)
706
+ throw new Error("The selected source face is missing or ambiguous after the sketch edit.");
707
+ const faceDraft = { ...requestedFaceDraft, sourceFaceNormal };
708
+ const result = await applySketchFaceDraft(replayedSource, {
709
+ ...requestedFaceDraft,
710
+ sourceFaceId: currentFace.id,
711
+ });
712
+ swept = {
713
+ ...shapeWithCadResult(replayedSource, result),
714
+ faceDraft,
715
+ faceDraftSourceFrame: existing.faceDraftSourceFrame ?? existing.cadBrepFrame,
716
+ };
717
+ }
718
+ else {
719
+ swept = replayedSource;
720
+ }
525
721
  }
526
722
  catch (error) {
527
723
  return failed({
@@ -530,7 +726,88 @@ export function createEngine(store) {
530
726
  });
531
727
  }
532
728
  }
533
- if (revolveChanges.length > 0 && sweptShape) {
729
+ if ((faceDraftChanged || (replacingSketchProfile && existing.faceDraft)
730
+ || (revolveChanges.length > 0 && existing.faceDraft)) && isSketchRevolve) {
731
+ try {
732
+ const runtime = await serverManifoldRuntime();
733
+ const sourceFrame = existing.faceDraftSourceFrame ?? existing.cadBrepFrame;
734
+ const rebuildSeed = sourceFrame ? {
735
+ ...existing,
736
+ x: sourceFrame.x,
737
+ z: sourceFrame.z,
738
+ elevation: sourceFrame.elevation,
739
+ } : existing;
740
+ const rebuiltAtNaturalScale = rebuiltRevolveShape(runtime, rebuildSeed, {
741
+ profile: (replacingSketchProfile ? changes.sketchProfile : existing.sketchProfile),
742
+ settings: revolveChanges.length > 0
743
+ ? requestedRevolveSettings(existing.sketchRevolve, changes)
744
+ : existing.sketchRevolve,
745
+ });
746
+ const sourceScale = existing.faceDraftSourceScale ?? revolveSourceScale(existing);
747
+ const rebuilt = resizeRebuiltRevolve(rebuiltAtNaturalScale, sourceScale);
748
+ if (requestedFaceDraft) {
749
+ const exactBase = shapeWithCadResult(rebuilt, await exactPlainSketchRevolveResult(rebuilt));
750
+ const priorReference = existing.faceDraft?.sourceFaceId === requestedFaceDraft.sourceFaceId
751
+ ? existing.faceDraft.sourceFaceNormal
752
+ : undefined;
753
+ const selectedOnPrior = priorReference ? undefined
754
+ : (await shapeFaces(existing, { topology: "exact" }))
755
+ .find((face) => face.id === requestedFaceDraft.sourceFaceId);
756
+ const sourceFaceNormal = priorReference ?? selectedOnPrior?.normal;
757
+ if (!sourceFaceNormal || selectedOnPrior && selectedOnPrior.surfaceType !== "plane") {
758
+ throw new Error("The selected source face has no stable planar reference.");
759
+ }
760
+ const currentFace = await findUniqueDraftFace(exactBase, sourceFaceNormal);
761
+ if (!currentFace)
762
+ throw new Error("The selected source face is missing or ambiguous after the sketch edit.");
763
+ const faceDraft = { ...requestedFaceDraft, sourceFaceNormal };
764
+ const result = await applySketchFaceDraft(exactBase, {
765
+ ...requestedFaceDraft,
766
+ sourceFaceId: currentFace.id,
767
+ });
768
+ swept = {
769
+ ...shapeWithCadResult(exactBase, result),
770
+ faceDraft,
771
+ faceDraftSourceFrame: existing.faceDraftSourceFrame ?? existing.cadBrepFrame ?? exactBase.cadBrepFrame,
772
+ faceDraftSourceScale: sourceScale,
773
+ };
774
+ }
775
+ else {
776
+ const clearedDraft = {
777
+ ...rebuilt,
778
+ faceDraft: undefined,
779
+ faceDraftSourceFrame: undefined,
780
+ faceDraftSourceScale: undefined,
781
+ };
782
+ swept = clearingFaceDraft
783
+ ? await replaySingleAllEdgeTreatment({ ...existing, faceDraft: undefined }, clearedDraft)
784
+ : clearedDraft;
785
+ }
786
+ }
787
+ catch (error) {
788
+ return failed({
789
+ reason: "sweep_failed",
790
+ message: error instanceof Error ? error.message : "The sketch revolve could not be rebuilt.",
791
+ });
792
+ }
793
+ }
794
+ else if (replacingSketchProfile && isSketchRevolve) {
795
+ try {
796
+ const runtime = await serverManifoldRuntime();
797
+ const rebuilt = rebuiltRevolveShape(runtime, existing, {
798
+ profile: changes.sketchProfile,
799
+ settings: existing.sketchRevolve,
800
+ });
801
+ swept = await replaySingleAllEdgeTreatment(existing, rebuilt);
802
+ }
803
+ catch (error) {
804
+ return failed({
805
+ reason: "sweep_failed",
806
+ message: error instanceof Error ? error.message : "The sketch revolve could not be rebuilt.",
807
+ });
808
+ }
809
+ }
810
+ if (revolveChanges.length > 0 && sweptShape && !existing.faceDraft) {
534
811
  const outline = changes.profile ?? changes.outline ?? outlineFromProfile(existing.sketchProfile);
535
812
  if (!outline) {
536
813
  return failed({
@@ -545,10 +822,11 @@ export function createEngine(store) {
545
822
  return failed({ reason: "invalid_shape_request", message: parsed.error });
546
823
  try {
547
824
  const runtime = await serverManifoldRuntime();
548
- swept = rebuiltRevolveShape(runtime, existing, {
825
+ const rebuilt = rebuiltRevolveShape(runtime, existing, {
549
826
  profile: parsed.profile,
550
827
  settings: requestedRevolveSettings(existing.sketchRevolve, changes),
551
828
  });
829
+ swept = await replaySingleAllEdgeTreatment(existing, rebuilt);
552
830
  }
553
831
  catch (error) {
554
832
  return failed({
@@ -562,6 +840,8 @@ export function createEngine(store) {
562
840
  const sweepFields = [
563
841
  ...(sweptShape ? REVOLVE_CHANGE_FIELDS : REVOLVE_ONLY_FIELDS),
564
842
  "sketchProfile",
843
+ "draftPrismAngle",
844
+ "faceDraft",
565
845
  ];
566
846
  // Shape summaries expose the source ID for identity; echoing a full
567
847
  // summary into PATCH must not let callers replace that identity.
@@ -806,12 +1086,12 @@ export function createEngine(store) {
806
1086
  return failed(cadFailure(error));
807
1087
  }
808
1088
  }),
809
- faces: (projectId, shapeId) => reading(projectId, async (document) => {
1089
+ faces: (projectId, shapeId, topology) => reading(projectId, async (document) => {
810
1090
  const found = oneShape(document, shapeId);
811
1091
  if (!found.ok)
812
1092
  return found;
813
1093
  try {
814
- return ok(await shapeFaces(found.value));
1094
+ return ok(await shapeFaces(found.value, topology === "exact" ? { topology } : undefined));
815
1095
  }
816
1096
  catch (error) {
817
1097
  return failed(cadFailure(error));
@@ -868,8 +1148,44 @@ export function createEngine(store) {
868
1148
  return failed(cadFailure(error));
869
1149
  }
870
1150
  }),
871
- treat: (projectId, shapeId, treatment) => cadTreatment(projectId, shapeId, async (shape) => (shapeWithCadResult(shape, await applyEdgeTreatment(shape, treatment)))),
872
- draftPrism: (projectId, shapeId, angle) => cadTreatment(projectId, shapeId, async (shape) => (shapeWithCadResult(shape, await applyDraftPrism(shape, { angle })))),
1151
+ treat: (projectId, shapeId, treatment) => cadTreatment(projectId, shapeId, (treatment.edges === undefined || treatment.edges.length === 0)
1152
+ && (treatment.kind === "fillet" || treatment.angle === undefined || Math.abs(treatment.angle - 45) < 0.001)
1153
+ ? "all-edge-treatment"
1154
+ : "edge-treatment", async (shape) => {
1155
+ const treated = shapeWithCadResult(shape, await applyEdgeTreatment(shape, treatment));
1156
+ if (shape.draftPrismAngle === undefined)
1157
+ return treated;
1158
+ const feature = {
1159
+ kind: treatment.kind,
1160
+ amount: treatment.amount,
1161
+ edgeCount: await allEdgeTreatmentEdgeCount(shape, treatment.kind),
1162
+ allEdges: true,
1163
+ ...(treatment.kind === "chamfer" ? { chamferAngle: treatment.angle ?? 45 } : {}),
1164
+ };
1165
+ const history = shape.edgeTreatmentHistory ?? [];
1166
+ return {
1167
+ ...treated,
1168
+ edgeTreatments: [...(shape.edgeTreatments ?? []), feature],
1169
+ edgeTreatmentHistory: [
1170
+ ...history,
1171
+ {
1172
+ id: createLocalId("edge-history"),
1173
+ createdAt: Date.now(),
1174
+ feature,
1175
+ before: structuredClone(shape),
1176
+ },
1177
+ ],
1178
+ };
1179
+ }),
1180
+ draftPrism: (projectId, shapeId, angle) => cadTreatment(projectId, shapeId, "prism-draft", async (shape) => {
1181
+ const sourceFrame = shape.draftPrismSourceFrame ?? shape.cadBrepFrame;
1182
+ return {
1183
+ ...shapeWithCadResult(shape, await applyDraftPrism(shape, { angle })),
1184
+ draftPrismAngle: angle,
1185
+ draftPrismSourceHeight: shape.draftPrismSourceHeight ?? shape.height,
1186
+ ...(sourceFrame ? { draftPrismSourceFrame: sourceFrame } : {}),
1187
+ };
1188
+ }),
873
1189
  mirrorBaked: (projectId, ids, axis) => mutating(projectId, async (document) => {
874
1190
  if (ids.length < 1)
875
1191
  return failed({ reason: "selection_too_small", needed: 1, given: 0 });
@@ -899,7 +1215,7 @@ export function createEngine(store) {
899
1215
  revision: await writeShapes(projectId, document, shapes),
900
1216
  });
901
1217
  }),
902
- shell: (projectId, shapeId, request) => cadTreatment(projectId, shapeId, async (shape) => (shapeWithCadResult(shape, await applyShell(shape, request)))),
1218
+ shell: (projectId, shapeId, request) => cadTreatment(projectId, shapeId, "shell", async (shape) => (shapeWithCadResult(shape, await applyShell(shape, request)))),
903
1219
  };
904
1220
  }
905
1221
  //# sourceMappingURL=engine.js.map