@constructorfleet/sketchforge-engine 1.18.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 (45) 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 +364 -21
  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 +85 -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 +4 -0
  27. package/dist/sketchExtrusionRequest.d.ts.map +1 -0
  28. package/dist/sketchExtrusionRequest.js +92 -0
  29. package/dist/sketchExtrusionRequest.js.map +1 -0
  30. package/dist/skfProject.d.ts.map +1 -1
  31. package/dist/skfProject.js +85 -8
  32. package/dist/skfProject.js.map +1 -1
  33. package/dist/stepComponentMetadata.d.ts.map +1 -1
  34. package/dist/stepComponentMetadata.js +55 -2
  35. package/dist/stepComponentMetadata.js.map +1 -1
  36. package/dist/stepExport.d.ts.map +1 -1
  37. package/dist/stepExport.js +54 -8
  38. package/dist/stepExport.js.map +1 -1
  39. package/dist/stepGlbColors.d.ts.map +1 -1
  40. package/dist/stepGlbColors.js +10 -2
  41. package/dist/stepGlbColors.js.map +1 -1
  42. package/dist/support/lib/operator-configuration.json +78 -2
  43. package/dist/support/lib/operatorConfiguration.d.ts +26 -2
  44. package/dist/support/lib/operatorConfiguration.d.ts.map +1 -1
  45. 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";
@@ -31,11 +31,53 @@ import { boundsForShapes as boundsForMirroring, restoreGroupedChildren, separate
31
31
  import { serverManifoldRuntime } from "./serverManifoldRuntime.js";
32
32
  import { createLocalId } from "./localIds.js";
33
33
  import { invalidSketchSweepProfile, sketchSweepShapeFromRequest } from "./sketchSweepRequest.js";
34
- import { findSketchOutput, recomputeSketchOutput } from "./parametricRecompute.js";
34
+ import { sketchExtrusionShapeFromRequest } from "./sketchExtrusionRequest.js";
35
+ import { findSketchOutput, recomputeSketchOutput, replaySingleAllEdgeTreatment } from "./parametricRecompute.js";
35
36
  import { boundsForShapes, footprintCenter, shapeBounds } from "./shapeBounds.js";
36
37
  import { REQUESTABLE_SHAPE_KINDS, shapeFromRequest, shapeWithRequestedChanges, SHAPE_PARAMETERS, } from "./shapeRequests.js";
37
38
  import { canonicalizeShape, cleanNearZero, cleanRotationDegrees, cloneWorkplaneShapeTreeWithFreshIds, withHoleMode, } from "./workplaneShapes.js";
38
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
+ }
39
81
  /**
40
82
  * Carry face metadata across a parameter edit only when a face has a unique
41
83
  * counterpart. Curved faces may change their sampled normal as their defining
@@ -330,13 +372,33 @@ export function createEngine(store) {
330
372
  });
331
373
  }
332
374
  /** The shared tail of `treat` and `shell`: run the kernel, store the result on the shape. */
333
- async function cadTreatment(projectId, shapeId, run) {
375
+ async function cadTreatment(projectId, shapeId, operation, run) {
334
376
  return mutating(projectId, async (document) => {
335
377
  const found = oneShape(document, shapeId);
336
378
  if (!found.ok)
337
379
  return found;
338
380
  if (found.value.locked)
339
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
+ }
340
402
  let changed;
341
403
  try {
342
404
  changed = await run(found.value);
@@ -412,11 +474,15 @@ export function createEngine(store) {
412
474
  changeShape: (projectId, shapeId, changes) => mutating(projectId, async (document) => {
413
475
  const byOutputId = oneShape(document, shapeId);
414
476
  // A persisted sketch source is an input node, not a renderable shape.
415
- // When a caller edits that node's profile, resolve its dependent sweep
416
- // 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
417
479
  // output keeps its own stable object ID; SKF export derives the source
418
480
  // node from the synchronized profile on that output.
419
- 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
420
486
  ? findSketchOutput(document.shapes, shapeId)
421
487
  : [];
422
488
  const sourceSketchTarget = !byOutputId.ok && sourceOutputs.length === 1;
@@ -429,7 +495,9 @@ export function createEngine(store) {
429
495
  return found;
430
496
  const existing = found.value;
431
497
  if (sourceSketchTarget) {
432
- 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)));
433
501
  if (outputFields.length > 0) {
434
502
  return failed({ reason: "unapplicable_fields", kind: existing.kind, fields: outputFields, because: "not_this_kind" });
435
503
  }
@@ -440,8 +508,48 @@ export function createEngine(store) {
440
508
  if (existing.locked && changes.locked !== false)
441
509
  return failed({ reason: "shape_locked", ids: [shapeId] });
442
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;
443
516
  const isSketchSweep = existing.sketchOperation === "sweep" && Boolean(existing.sketchProfile);
444
- if (replacingSketchProfile && !isSketchSweep) {
517
+ const isSketchExtrusion = existing.sketchOperation === "extrude" && Boolean(existing.sketchProfile);
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) {
445
553
  return failed({ reason: "unapplicable_fields", kind: existing.kind, fields: ["sketchProfile"], because: "no_sweep" });
446
554
  }
447
555
  const sweptShape = isRevolveShape(existing);
@@ -454,18 +562,100 @@ export function createEngine(store) {
454
562
  // leave a thread's pitch readable but not writable.
455
563
  const revolveChanges = sweptShape ? REVOLVE_CHANGE_FIELDS.filter((field) => changes[field] !== undefined) : [];
456
564
  let swept = existing;
457
- if (replacingSketchProfile && isSketchSweep) {
458
- const invalidProfile = invalidSketchSweepProfile(changes.sketchProfile);
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;
569
+ try {
570
+ const rebuilt = await sketchExtrusionShapeFromRequest({
571
+ id: existing.id,
572
+ sketchSourceId: existing.sketchSourceId ?? `sketch/${existing.id}`,
573
+ name: existing.name,
574
+ color: existing.color,
575
+ hole: existing.hole,
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),
581
+ rotation: existing.rotation,
582
+ rotationX: existing.rotationX,
583
+ rotationZ: existing.rotationZ,
584
+ });
585
+ const { faceIds: _oldFaceIds, faceColors: _faceColors, edgeTreatments: _edgeTreatments, edgeTreatmentHistory: _edgeTreatmentHistory, cadDisplayEdges: _cadDisplayEdges, cadDisplayEdgesVersion: _cadDisplayEdgesVersion, ...sourceMetadata } = existing;
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
+ }
641
+ }
642
+ catch (error) {
643
+ return failed({ reason: "sweep_failed", message: error instanceof Error ? error.message : "The sketch extrusion could not be rebuilt." });
644
+ }
645
+ }
646
+ if ((replacingSketchProfile || faceDraftChanged) && isSketchSweep) {
647
+ const profileToBuild = replacingSketchProfile ? changes.sketchProfile : existing.sketchProfile;
648
+ const invalidProfile = replacingSketchProfile ? invalidSketchSweepProfile(profileToBuild) : undefined;
459
649
  if (invalidProfile)
460
650
  return failed({ reason: "invalid_shape_request", message: invalidProfile });
461
- const frame = existing.cadBrepFrame;
651
+ const frame = existing.faceDraftSourceFrame ?? existing.cadBrepFrame;
462
652
  try {
463
653
  const rebuilt = await sketchSweepShapeFromRequest({
464
654
  id: existing.id,
465
655
  name: existing.name,
466
656
  color: existing.color,
467
657
  hole: existing.hole,
468
- profile: changes.sketchProfile,
658
+ profile: profileToBuild,
469
659
  x: existing.x - (frame?.x ?? 0),
470
660
  z: existing.z - (frame?.z ?? 0),
471
661
  elevation: (existing.elevation ?? 0) - (frame?.elevation ?? 0),
@@ -487,14 +677,47 @@ export function createEngine(store) {
487
677
  resized.size = Math.max(resized.width, resized.depth);
488
678
  const refreshedFaceIds = (await shapeFaces(rebuilt))
489
679
  .map((face) => `face_${createLocalId(`${existing.id}-${face.index}`)}`);
490
- const { faceIds: _oldFaceIds, faceColors: _faceColors, edgeTreatments: _edgeTreatments, edgeTreatmentHistory: _edgeTreatmentHistory, cadDisplayEdges: _cadDisplayEdges, cadDisplayEdgesVersion: _cadDisplayEdgesVersion, ...sourceMetadata } = existing;
491
- swept = {
680
+ const { faceIds: _oldFaceIds, faceColors: _faceColors, edgeTreatments: _edgeTreatments, edgeTreatmentHistory: _edgeTreatmentHistory, cadDisplayEdges: _cadDisplayEdges, cadDisplayEdgesVersion: _cadDisplayEdgesVersion, faceDraft: _faceDraft, faceDraftSourceFrame: _faceDraftSourceFrame, ...sourceMetadata } = existing;
681
+ const rebuiltSweep = {
492
682
  ...sourceMetadata,
493
683
  ...resized,
494
684
  id: existing.id,
495
685
  sketchSourceId: existing.sketchSourceId ?? rebuilt.sketchSourceId,
496
686
  faceIds: refreshedFaceIds,
687
+ faceDraft: undefined,
688
+ faceDraftSourceFrame: undefined,
497
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
+ }
498
721
  }
499
722
  catch (error) {
500
723
  return failed({
@@ -503,7 +726,88 @@ export function createEngine(store) {
503
726
  });
504
727
  }
505
728
  }
506
- 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) {
507
811
  const outline = changes.profile ?? changes.outline ?? outlineFromProfile(existing.sketchProfile);
508
812
  if (!outline) {
509
813
  return failed({
@@ -518,10 +822,11 @@ export function createEngine(store) {
518
822
  return failed({ reason: "invalid_shape_request", message: parsed.error });
519
823
  try {
520
824
  const runtime = await serverManifoldRuntime();
521
- swept = rebuiltRevolveShape(runtime, existing, {
825
+ const rebuilt = rebuiltRevolveShape(runtime, existing, {
522
826
  profile: parsed.profile,
523
827
  settings: requestedRevolveSettings(existing.sketchRevolve, changes),
524
828
  });
829
+ swept = await replaySingleAllEdgeTreatment(existing, rebuilt);
525
830
  }
526
831
  catch (error) {
527
832
  return failed({
@@ -535,6 +840,8 @@ export function createEngine(store) {
535
840
  const sweepFields = [
536
841
  ...(sweptShape ? REVOLVE_CHANGE_FIELDS : REVOLVE_ONLY_FIELDS),
537
842
  "sketchProfile",
843
+ "draftPrismAngle",
844
+ "faceDraft",
538
845
  ];
539
846
  // Shape summaries expose the source ID for identity; echoing a full
540
847
  // summary into PATCH must not let callers replace that identity.
@@ -779,12 +1086,12 @@ export function createEngine(store) {
779
1086
  return failed(cadFailure(error));
780
1087
  }
781
1088
  }),
782
- faces: (projectId, shapeId) => reading(projectId, async (document) => {
1089
+ faces: (projectId, shapeId, topology) => reading(projectId, async (document) => {
783
1090
  const found = oneShape(document, shapeId);
784
1091
  if (!found.ok)
785
1092
  return found;
786
1093
  try {
787
- return ok(await shapeFaces(found.value));
1094
+ return ok(await shapeFaces(found.value, topology === "exact" ? { topology } : undefined));
788
1095
  }
789
1096
  catch (error) {
790
1097
  return failed(cadFailure(error));
@@ -841,8 +1148,44 @@ export function createEngine(store) {
841
1148
  return failed(cadFailure(error));
842
1149
  }
843
1150
  }),
844
- treat: (projectId, shapeId, treatment) => cadTreatment(projectId, shapeId, async (shape) => (shapeWithCadResult(shape, await applyEdgeTreatment(shape, treatment)))),
845
- 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
+ }),
846
1189
  mirrorBaked: (projectId, ids, axis) => mutating(projectId, async (document) => {
847
1190
  if (ids.length < 1)
848
1191
  return failed({ reason: "selection_too_small", needed: 1, given: 0 });
@@ -872,7 +1215,7 @@ export function createEngine(store) {
872
1215
  revision: await writeShapes(projectId, document, shapes),
873
1216
  });
874
1217
  }),
875
- 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)))),
876
1219
  };
877
1220
  }
878
1221
  //# sourceMappingURL=engine.js.map