@constructorfleet/sketchforge-engine 1.19.0 → 1.21.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 (46) hide show
  1. package/README.md +3 -3
  2. package/dist/domain/sketchforge.d.ts +43 -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 +482 -29
  7. package/dist/engine.js.map +1 -1
  8. package/dist/faceDraftSegmentLineage.d.ts +45 -0
  9. package/dist/faceDraftSegmentLineage.d.ts.map +1 -0
  10. package/dist/faceDraftSegmentLineage.js +200 -0
  11. package/dist/faceDraftSegmentLineage.js.map +1 -0
  12. package/dist/faceLineage.d.ts +10 -0
  13. package/dist/faceLineage.d.ts.map +1 -0
  14. package/dist/faceLineage.js +84 -0
  15. package/dist/faceLineage.js.map +1 -0
  16. package/dist/parametricRecompute.d.ts +2 -0
  17. package/dist/parametricRecompute.d.ts.map +1 -1
  18. package/dist/parametricRecompute.js +152 -0
  19. package/dist/parametricRecompute.js.map +1 -1
  20. package/dist/revolveRequests.d.ts +12 -0
  21. package/dist/revolveRequests.d.ts.map +1 -1
  22. package/dist/serverCadOperations.d.ts +54 -3
  23. package/dist/serverCadOperations.d.ts.map +1 -1
  24. package/dist/serverCadOperations.js +224 -6
  25. package/dist/serverCadOperations.js.map +1 -1
  26. package/dist/serverCadShell.d.ts +3 -1
  27. package/dist/serverCadShell.d.ts.map +1 -1
  28. package/dist/serverCadShell.js +15 -5
  29. package/dist/serverCadShell.js.map +1 -1
  30. package/dist/sketchExtrusionRequest.d.ts.map +1 -1
  31. package/dist/sketchExtrusionRequest.js +1 -0
  32. package/dist/sketchExtrusionRequest.js.map +1 -1
  33. package/dist/skfProject.d.ts.map +1 -1
  34. package/dist/skfProject.js +210 -8
  35. package/dist/skfProject.js.map +1 -1
  36. package/dist/stepComponentMetadata.d.ts.map +1 -1
  37. package/dist/stepComponentMetadata.js +55 -2
  38. package/dist/stepComponentMetadata.js.map +1 -1
  39. package/dist/stepExport.d.ts.map +1 -1
  40. package/dist/stepExport.js +54 -8
  41. package/dist/stepExport.js.map +1 -1
  42. package/dist/stepGlbColors.d.ts.map +1 -1
  43. package/dist/stepGlbColors.js +10 -2
  44. package/dist/stepGlbColors.js.map +1 -1
  45. package/dist/support/lib/operator-configuration.json +10 -0
  46. package/package.json +1 -1
package/dist/engine.js CHANGED
@@ -14,7 +14,9 @@
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
+ import { draftFaceForSketchSegment, legacyDraftSegmentForShape, sketchSegmentForDraftFace } from "./faceDraftSegmentLineage.js";
19
+ import { cadSketchRegions, cadSketchSweepPaths } from "./sketchCadProfile.js";
18
20
  import { applyShell, shapeFaces, } from "./serverCadShell.js";
19
21
  import { measureBetween as measureBetweenEntities, shapeMeasurements, } from "./serverCadMeasure.js";
20
22
  import { shapeProperties } from "./serverCadProperties.js";
@@ -32,11 +34,69 @@ import { serverManifoldRuntime } from "./serverManifoldRuntime.js";
32
34
  import { createLocalId } from "./localIds.js";
33
35
  import { invalidSketchSweepProfile, sketchSweepShapeFromRequest } from "./sketchSweepRequest.js";
34
36
  import { sketchExtrusionShapeFromRequest } from "./sketchExtrusionRequest.js";
35
- import { findSketchOutput, recomputeSketchOutput } from "./parametricRecompute.js";
37
+ import { findSketchOutput, recomputeSketchOutput, replaySingleAllEdgeTreatment } from "./parametricRecompute.js";
36
38
  import { boundsForShapes, footprintCenter, shapeBounds } from "./shapeBounds.js";
37
39
  import { REQUESTABLE_SHAPE_KINDS, shapeFromRequest, shapeWithRequestedChanges, SHAPE_PARAMETERS, } from "./shapeRequests.js";
38
40
  import { canonicalizeShape, cleanNearZero, cleanRotationDegrees, cloneWorkplaneShapeTreeWithFreshIds, withHoleMode, } from "./workplaneShapes.js";
39
41
  const UNIQUE_CURVED_FACE_SURFACES = new Set(["cone", "sphere", "torus", "cylinder"]);
42
+ const FACE_DRAFT_AXES = new Set(["x", "y", "z", "-x", "-y", "-z"]);
43
+ function faceDraftInput(value) {
44
+ if (!value || typeof value !== "object" || Array.isArray(value))
45
+ return null;
46
+ const draft = value;
47
+ if (typeof draft.sourceFaceId !== "string" || !draft.sourceFaceId.trim()
48
+ || typeof draft.angle !== "number" || !Number.isFinite(draft.angle) || draft.angle === 0 || Math.abs(draft.angle) >= 45
49
+ || typeof draft.pullDirection !== "string" || !FACE_DRAFT_AXES.has(draft.pullDirection))
50
+ return null;
51
+ return {
52
+ sourceFaceId: draft.sourceFaceId,
53
+ angle: draft.angle,
54
+ pullDirection: draft.pullDirection,
55
+ };
56
+ }
57
+ function isSingleLoopSketchExtrusion(shape) {
58
+ if (shape.sketchOperation !== "extrude" || !shape.sketchProfile)
59
+ return false;
60
+ const regions = cadSketchRegions(shape.sketchProfile);
61
+ return regions.length === 1 && regions[0]?.holes.length === 0;
62
+ }
63
+ function isSingleSectionSketchSweep(shape) {
64
+ if (shape.sketchOperation !== "sweep" || !shape.sketchProfile)
65
+ return false;
66
+ try {
67
+ const { sections, spine } = cadSketchSweepPaths(shape.sketchProfile);
68
+ return sections.length === 1 && sections[0]?.holes.length === 0 && spine.steps.length > 0;
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ }
74
+ function revolveSourceScale(shape) {
75
+ const mesh = shape.importedMesh;
76
+ const ratio = (drawn, natural) => natural && natural > 0 ? drawn / natural : 1;
77
+ return {
78
+ width: ratio(shape.width, mesh?.baseWidth),
79
+ depth: ratio(shape.depth, mesh?.baseDepth),
80
+ height: ratio(shape.height, mesh?.baseHeight),
81
+ };
82
+ }
83
+ function resizeRebuiltRevolve(shape, scale) {
84
+ const mesh = shape.importedMesh;
85
+ if (!mesh)
86
+ return shape;
87
+ const width = Math.max(0.01, (mesh.baseWidth ?? shape.width) * scale.width);
88
+ const depth = Math.max(0.01, (mesh.baseDepth ?? shape.depth) * scale.depth);
89
+ const height = Math.max(0.01, (mesh.baseHeight ?? shape.height) * scale.height);
90
+ return { ...shape, width, depth, height, size: Math.max(width, depth) };
91
+ }
92
+ function faceNormalMatches(left, right) {
93
+ return Math.hypot(left.x - right.x, left.y - right.y, left.z - right.z) <= 1e-5;
94
+ }
95
+ async function findUniqueDraftFace(shape, normal) {
96
+ const matches = (await shapeFaces(shape)).filter((face) => face.surfaceType === "plane" && face.normal
97
+ && faceNormalMatches(face.normal, normal));
98
+ return matches.length === 1 ? matches[0] : null;
99
+ }
40
100
  /**
41
101
  * Carry face metadata across a parameter edit only when a face has a unique
42
102
  * counterpart. Curved faces may change their sampled normal as their defining
@@ -331,13 +391,42 @@ export function createEngine(store) {
331
391
  });
332
392
  }
333
393
  /** The shared tail of `treat` and `shell`: run the kernel, store the result on the shape. */
334
- async function cadTreatment(projectId, shapeId, run) {
394
+ async function cadTreatment(projectId, shapeId, operation, run) {
335
395
  return mutating(projectId, async (document) => {
336
396
  const found = oneShape(document, shapeId);
337
397
  if (!found.ok)
338
398
  return found;
339
399
  if (found.value.locked)
340
400
  return failed({ reason: "shape_locked", ids: [shapeId] });
401
+ const hasEdgeTreatments = (found.value.edgeTreatments?.length ?? 0) > 0
402
+ || (found.value.edgeTreatmentHistory?.length ?? 0) > 0;
403
+ const hasDraftPrism = found.value.draftPrismAngle !== undefined;
404
+ const replayablePrismTreatment = (operation === "all-edge-treatment" || operation === "all-edge-chamfer-treatment")
405
+ && !hasEdgeTreatments
406
+ && found.value.sketchOperation === "extrude"
407
+ && Boolean(found.value.sketchProfile)
408
+ && hasDraftPrism;
409
+ const replayableFaceDraftTreatment = (operation === "all-edge-treatment" || operation === "all-edge-chamfer-treatment")
410
+ && !hasEdgeTreatments
411
+ && found.value.faceDraft !== undefined
412
+ && (isPlainFullTurnSketchRevolve(found.value)
413
+ || isSingleLoopSketchExtrusion(found.value)
414
+ && (operation === "all-edge-treatment" || operation === "all-edge-chamfer-treatment")
415
+ || operation === "all-edge-treatment" && isSingleSectionSketchSweep(found.value)
416
+ || operation === "all-edge-chamfer-treatment" && isSingleSectionSketchSweep(found.value));
417
+ const incompatible = found.value.faceDraft !== undefined && !replayableFaceDraftTreatment
418
+ || operation === "edge-treatment" && hasDraftPrism
419
+ || (operation === "all-edge-treatment" || operation === "all-edge-chamfer-treatment")
420
+ && hasDraftPrism && !replayablePrismTreatment
421
+ || operation === "prism-draft" && hasEdgeTreatments
422
+ || operation === "shell" && (found.value.draftPrismAngle !== undefined || hasEdgeTreatments);
423
+ if (incompatible) {
424
+ return failed({
425
+ reason: "cad_refused",
426
+ message: "Face drafts support one symmetric 45-degree all-edge chamfer or all-edge fillet on a single-loop sketch extrusion, one symmetric 45-degree all-edge chamfer or all-edge fillet on a single-section sketch sweep, or one symmetric all-edge chamfer or all-edge fillet on a plain full-turn sketch revolve; prism drafts support one all-edge treatment only on a sketch extrusion. Other draft and treatment combinations, and shelling with persisted features, are refused.",
427
+ status: 422,
428
+ });
429
+ }
341
430
  let changed;
342
431
  try {
343
432
  changed = await run(found.value);
@@ -413,11 +502,15 @@ export function createEngine(store) {
413
502
  changeShape: (projectId, shapeId, changes) => mutating(projectId, async (document) => {
414
503
  const byOutputId = oneShape(document, shapeId);
415
504
  // 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
505
+ // When a caller edits that node's profile or revolve settings, resolve
506
+ // its dependent output and rebuild it before the single project write. The
418
507
  // output keeps its own stable object ID; SKF export derives the source
419
508
  // node from the synchronized profile on that output.
420
- const sourceOutputs = !byOutputId.ok && changes.sketchProfile !== undefined
509
+ const sourceFeatureInputChanged = changes.sketchProfile !== undefined
510
+ || changes.draftPrismAngle !== undefined
511
+ || Object.hasOwn(changes, "faceDraft")
512
+ || REVOLVE_CHANGE_FIELDS.some((field) => changes[field] !== undefined);
513
+ const sourceOutputs = !byOutputId.ok && sourceFeatureInputChanged
421
514
  ? findSketchOutput(document.shapes, shapeId)
422
515
  : [];
423
516
  const sourceSketchTarget = !byOutputId.ok && sourceOutputs.length === 1;
@@ -430,7 +523,9 @@ export function createEngine(store) {
430
523
  return found;
431
524
  const existing = found.value;
432
525
  if (sourceSketchTarget) {
433
- const outputFields = Object.keys(changes).filter((field) => field !== "sketchProfile");
526
+ const isRevolveSource = isRevolveShape(existing);
527
+ const outputFields = Object.keys(changes).filter((field) => field !== "sketchProfile" && field !== "draftPrismAngle" && field !== "faceDraft"
528
+ && !(isRevolveSource && REVOLVE_CHANGE_FIELDS.includes(field)));
434
529
  if (outputFields.length > 0) {
435
530
  return failed({ reason: "unapplicable_fields", kind: existing.kind, fields: outputFields, because: "not_this_kind" });
436
531
  }
@@ -441,9 +536,80 @@ export function createEngine(store) {
441
536
  if (existing.locked && changes.locked !== false)
442
537
  return failed({ reason: "shape_locked", ids: [shapeId] });
443
538
  const replacingSketchProfile = changes.sketchProfile !== undefined;
539
+ const faceDraftChanged = Object.hasOwn(changes, "faceDraft");
540
+ const clearingFaceDraft = faceDraftChanged && changes.faceDraft === null && existing.faceDraft !== undefined;
541
+ const requestedFaceDraft = faceDraftChanged
542
+ ? changes.faceDraft === null ? undefined : faceDraftInput(changes.faceDraft)
543
+ : existing.faceDraft;
444
544
  const isSketchSweep = existing.sketchOperation === "sweep" && Boolean(existing.sketchProfile);
445
545
  const isSketchExtrusion = existing.sketchOperation === "extrude" && Boolean(existing.sketchProfile);
446
- if (replacingSketchProfile && !isSketchSweep && !isSketchExtrusion) {
546
+ const isSketchRevolve = isRevolveShape(existing);
547
+ if (faceDraftChanged && changes.faceDraft !== null && !requestedFaceDraft) {
548
+ 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." });
549
+ }
550
+ const isExactSketchRevolve = isPlainFullTurnSketchRevolve(existing);
551
+ if ((faceDraftChanged || (replacingSketchProfile && existing.faceDraft))
552
+ && !isSketchExtrusion && !isSketchSweep && !isExactSketchRevolve) {
553
+ return failed({ reason: "unapplicable_fields", kind: existing.kind, fields: ["faceDraft"], because: "not_this_kind" });
554
+ }
555
+ const hasExistingTreatment = (existing.edgeTreatments?.length ?? 0) > 0
556
+ || (existing.edgeTreatmentHistory?.length ?? 0) > 0;
557
+ const canReplayFaceDraftTreatment = existing.faceDraft !== undefined
558
+ && existing.draftPrismAngle === undefined
559
+ && (!faceDraftChanged || requestedFaceDraft !== undefined && requestedFaceDraft !== null
560
+ && requestedFaceDraft.sourceFaceId === existing.faceDraft.sourceFaceId)
561
+ && (isPlainFullTurnSketchRevolve(existing)
562
+ || isSingleLoopSketchExtrusion(existing)
563
+ && (existing.edgeTreatments?.[0]?.kind === "fillet" || existing.edgeTreatments?.[0]?.kind === "chamfer")
564
+ || isSingleSectionSketchSweep(existing)
565
+ && (existing.edgeTreatments?.[0]?.kind === "fillet" || existing.edgeTreatments?.[0]?.kind === "chamfer"))
566
+ && existing.edgeTreatments?.length === 1
567
+ && (existing.edgeTreatments[0]?.kind === "chamfer" || existing.edgeTreatments[0]?.kind === "fillet")
568
+ && existing.edgeTreatments[0]?.allEdges === true
569
+ && existing.edgeTreatmentHistory?.length === 1
570
+ && existing.edgeTreatmentHistory[0]?.feature.kind === existing.edgeTreatments[0]?.kind
571
+ && existing.edgeTreatmentHistory[0]?.feature.allEdges === true
572
+ && (existing.edgeTreatments[0]?.kind !== "chamfer"
573
+ || Math.abs((existing.edgeTreatments[0].chamferAngle ?? 45) - 45) < 0.001
574
+ && Math.abs((existing.edgeTreatmentHistory[0]?.feature.chamferAngle ?? 45) - 45) < 0.001);
575
+ if (requestedFaceDraft
576
+ && (existing.draftPrismAngle !== undefined || hasExistingTreatment && !canReplayFaceDraftTreatment)) {
577
+ return failed({ reason: "invalid_shape_request", message: "This face draft cannot be combined with the requested persisted draft or treatment history." });
578
+ }
579
+ const canReselectUntreatedExtrusionFace = isSingleLoopSketchExtrusion(existing)
580
+ && existing.draftPrismAngle === undefined
581
+ && !hasExistingTreatment;
582
+ if (faceDraftChanged && existing.faceDraft && requestedFaceDraft
583
+ && requestedFaceDraft.sourceFaceId !== existing.faceDraft.sourceFaceId
584
+ && replacingSketchProfile) {
585
+ return failed({ reason: "invalid_shape_request", message: "Change the sketch profile and face draft selection in separate edits." });
586
+ }
587
+ if (faceDraftChanged && existing.faceDraft && requestedFaceDraft
588
+ && requestedFaceDraft.sourceFaceId !== existing.faceDraft.sourceFaceId) {
589
+ const currentOutputFaces = await shapeFaces(existing);
590
+ if (!canReselectUntreatedExtrusionFace
591
+ || !existing.faceDraftSourceFaceIds?.includes(requestedFaceDraft.sourceFaceId)
592
+ || currentOutputFaces.filter((face) => face.id === requestedFaceDraft.sourceFaceId && face.surfaceType === "plane").length !== 1) {
593
+ return failed({ reason: "invalid_shape_request", message: "Clear the existing face draft before selecting a different source face." });
594
+ }
595
+ }
596
+ const draftPrismChanged = changes.draftPrismAngle !== undefined;
597
+ const requestedDraftPrismAngle = typeof changes.draftPrismAngle === "number" ? changes.draftPrismAngle : undefined;
598
+ if (draftPrismChanged && (!isSketchExtrusion || requestedDraftPrismAngle === undefined || !Number.isFinite(requestedDraftPrismAngle)
599
+ || requestedDraftPrismAngle === 0 || Math.abs(requestedDraftPrismAngle) >= 45)) {
600
+ return failed({
601
+ reason: "invalid_shape_request",
602
+ message: "A draft prism angle must be non-zero and between -45 and 45 degrees on a sketch extrusion.",
603
+ });
604
+ }
605
+ if (draftPrismChanged && requestedFaceDraft) {
606
+ return failed({ reason: "invalid_shape_request", message: "Face drafts cannot be combined with prism drafts yet." });
607
+ }
608
+ if (draftPrismChanged && existing.draftPrismAngle === undefined
609
+ && ((existing.edgeTreatments?.length ?? 0) > 0 || (existing.edgeTreatmentHistory?.length ?? 0) > 0)) {
610
+ return failed({ reason: "invalid_shape_request", message: "A prism draft cannot be added after an edge treatment." });
611
+ }
612
+ if (replacingSketchProfile && !isSketchSweep && !isSketchExtrusion && !isSketchRevolve) {
447
613
  return failed({ reason: "unapplicable_fields", kind: existing.kind, fields: ["sketchProfile"], because: "no_sweep" });
448
614
  }
449
615
  const sweptShape = isRevolveShape(existing);
@@ -456,8 +622,10 @@ export function createEngine(store) {
456
622
  // leave a thread's pitch readable but not writable.
457
623
  const revolveChanges = sweptShape ? REVOLVE_CHANGE_FIELDS.filter((field) => changes[field] !== undefined) : [];
458
624
  let swept = existing;
459
- if (replacingSketchProfile && isSketchExtrusion) {
460
- const frame = existing.cadBrepFrame;
625
+ if ((replacingSketchProfile && isSketchExtrusion) || (draftPrismChanged && isSketchExtrusion)
626
+ || (faceDraftChanged && isSketchExtrusion)) {
627
+ const sourceFrame = existing.faceDraftSourceFrame ?? existing.draftPrismSourceFrame ?? existing.cadBrepFrame;
628
+ const sourceHeight = existing.draftPrismSourceHeight ?? existing.faceDraftSourceFrame?.height ?? existing.height;
461
629
  try {
462
630
  const rebuilt = await sketchExtrusionShapeFromRequest({
463
631
  id: existing.id,
@@ -465,34 +633,148 @@ export function createEngine(store) {
465
633
  name: existing.name,
466
634
  color: existing.color,
467
635
  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),
636
+ profile: replacingSketchProfile ? changes.sketchProfile : existing.sketchProfile,
637
+ height: sourceHeight,
638
+ x: existing.x - (sourceFrame?.x ?? 0),
639
+ z: existing.z - (sourceFrame?.z ?? 0),
640
+ elevation: (existing.elevation ?? 0) - (sourceFrame?.elevation ?? 0),
473
641
  rotation: existing.rotation,
474
642
  rotationX: existing.rotationX,
475
643
  rotationZ: existing.rotationZ,
476
644
  });
477
645
  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 };
646
+ const draftPrismAngle = draftPrismChanged ? requestedDraftPrismAngle : existing.draftPrismAngle;
647
+ const rebuiltSource = {
648
+ ...sourceMetadata,
649
+ ...rebuilt,
650
+ id: existing.id,
651
+ sketchSourceId: existing.sketchSourceId ?? rebuilt.sketchSourceId,
652
+ faceDraft: undefined,
653
+ faceDraftSourceFrame: undefined,
654
+ faceDraftSourceFaceIds: undefined,
655
+ faceDraftSourceSegmentId: undefined,
656
+ ...(faceDraftChanged && !replacingSketchProfile && !existing.faceDraft && !hasExistingTreatment
657
+ && existing.draftPrismAngle === undefined && existing.faceIds?.length === rebuilt.faceIds?.length
658
+ ? { faceIds: existing.faceIds }
659
+ : {}),
660
+ ...(!replacingSketchProfile && existing.faceDraft && existing.faceDraftSourceFaceIds?.length === rebuilt.faceIds?.length
661
+ ? { faceIds: existing.faceDraftSourceFaceIds }
662
+ : {}),
663
+ ...(draftPrismAngle === undefined ? {} : {
664
+ draftPrismSourceHeight: sourceHeight,
665
+ ...(sourceFrame ? { draftPrismSourceFrame: sourceFrame } : {}),
666
+ }),
667
+ };
668
+ const hasRetainedTreatments = (existing.edgeTreatments?.length ?? 0) > 0
669
+ || (existing.edgeTreatmentHistory?.length ?? 0) > 0;
670
+ const replayDraftAndTreatment = hasRetainedTreatments
671
+ && draftPrismAngle !== undefined
672
+ && (replacingSketchProfile || clearingFaceDraft || draftPrismChanged);
673
+ const replayFaceDraftAndTreatment = hasRetainedTreatments && faceDraftChanged && requestedFaceDraft !== undefined;
674
+ const replayOriginal = clearingFaceDraft
675
+ ? { ...existing, faceDraft: undefined }
676
+ : replayFaceDraftAndTreatment && existing.faceDraft
677
+ ? { ...existing, faceDraft: { ...existing.faceDraft, ...requestedFaceDraft } }
678
+ : existing;
679
+ const replayedSource = (replacingSketchProfile || clearingFaceDraft || draftPrismChanged || replayFaceDraftAndTreatment)
680
+ && hasRetainedTreatments
681
+ ? await replaySingleAllEdgeTreatment(replayOriginal, rebuiltSource, draftPrismChanged ? requestedDraftPrismAngle : undefined)
682
+ : rebuiltSource;
683
+ if (requestedFaceDraft && replayedSource.faceDraft) {
684
+ swept = replayedSource;
685
+ }
686
+ else if (requestedFaceDraft) {
687
+ const priorReference = existing.faceDraft?.sourceFaceId === requestedFaceDraft.sourceFaceId
688
+ ? existing.faceDraft.sourceFaceNormal
689
+ : undefined;
690
+ const selectedOnPrior = priorReference ? undefined
691
+ : (await shapeFaces(existing, isExactSketchRevolve ? { topology: "exact" } : undefined))
692
+ .find((face) => face.id === requestedFaceDraft.sourceFaceId);
693
+ const isReselection = existing.faceDraft !== undefined
694
+ && requestedFaceDraft.sourceFaceId !== existing.faceDraft.sourceFaceId;
695
+ const exactReselectedSourceFace = isReselection
696
+ ? (await shapeFaces(replayedSource)).find((face) => face.id === requestedFaceDraft.sourceFaceId)
697
+ : undefined;
698
+ const exactSelectedSourceFace = !replacingSketchProfile && !existing.faceDraft
699
+ ? (await shapeFaces(replayedSource)).find((face) => face.id === requestedFaceDraft.sourceFaceId)
700
+ : undefined;
701
+ const legacySegmentMatch = replacingSketchProfile && existing.faceDraft && !existing.faceDraftSourceSegmentId
702
+ ? await legacyDraftSegmentForShape(existing)
703
+ : undefined;
704
+ if (legacySegmentMatch?.status === "ambiguous") {
705
+ throw new Error("The saved face draft does not identify one source sketch segment.");
706
+ }
707
+ const retainedSourceSegmentId = existing.faceDraftSourceSegmentId
708
+ ?? (legacySegmentMatch?.status === "matched" ? legacySegmentMatch.segmentId : undefined);
709
+ const retainedSegmentFace = replacingSketchProfile && retainedSourceSegmentId
710
+ ? await draftFaceForSketchSegment(replayedSource, retainedSourceSegmentId)
711
+ : undefined;
712
+ if (retainedSegmentFace && retainedSegmentFace.status !== "matched") {
713
+ throw new Error("The selected sketch segment no longer maps to exactly one extrusion side face.");
714
+ }
715
+ let currentFaceId = retainedSegmentFace?.status === "matched"
716
+ ? retainedSegmentFace.faceId
717
+ : exactReselectedSourceFace?.surfaceType === "plane" ? exactReselectedSourceFace.id
718
+ : exactSelectedSourceFace?.surfaceType === "plane" ? exactSelectedSourceFace.id : undefined;
719
+ const sourceFaceNormal = retainedSegmentFace?.status === "matched"
720
+ ? retainedSegmentFace.normal
721
+ : exactReselectedSourceFace?.normal ?? exactSelectedSourceFace?.normal ?? priorReference ?? selectedOnPrior?.normal;
722
+ if (!sourceFaceNormal || selectedOnPrior && selectedOnPrior.surfaceType !== "plane") {
723
+ throw new Error("The selected source face has no stable planar reference.");
724
+ }
725
+ if (!currentFaceId)
726
+ currentFaceId = (await findUniqueDraftFace(replayedSource, sourceFaceNormal))?.id;
727
+ if (!currentFaceId)
728
+ throw new Error("The selected source face is missing or ambiguous after the sketch edit.");
729
+ const segmentMatch = retainedSegmentFace?.status === "matched"
730
+ ? retainedSegmentFace
731
+ : await sketchSegmentForDraftFace(replayedSource, currentFaceId);
732
+ if (segmentMatch.status === "ambiguous") {
733
+ throw new Error("The selected lateral face matches multiple sketch segments.");
734
+ }
735
+ if (segmentMatch.status === "missing") {
736
+ throw new Error("The selected extrusion side face has no exact sketch-segment boundary match.");
737
+ }
738
+ const faceDraft = { ...requestedFaceDraft, sourceFaceNormal };
739
+ const result = await applySketchFaceDraft(replayedSource, {
740
+ ...requestedFaceDraft,
741
+ sourceFaceId: currentFaceId,
742
+ });
743
+ swept = {
744
+ ...shapeWithCadResult(replayedSource, result),
745
+ faceDraft: { ...faceDraft, sourceFaceId: currentFaceId },
746
+ faceDraftSourceFaceIds: replayedSource.faceIds,
747
+ faceDraftSourceSegmentId: segmentMatch.status === "matched" ? segmentMatch.segmentId : undefined,
748
+ faceDraftSourceFrame: existing.faceDraftSourceFrame ?? existing.cadBrepFrame,
749
+ };
750
+ }
751
+ else if (draftPrismAngle !== undefined && !replayDraftAndTreatment) {
752
+ swept = {
753
+ ...shapeWithCadResult(replayedSource, await applyDraftPrism(replayedSource, { angle: draftPrismAngle })),
754
+ draftPrismAngle,
755
+ };
756
+ }
757
+ else {
758
+ swept = replayedSource;
759
+ }
479
760
  }
480
761
  catch (error) {
481
762
  return failed({ reason: "sweep_failed", message: error instanceof Error ? error.message : "The sketch extrusion could not be rebuilt." });
482
763
  }
483
764
  }
484
- if (replacingSketchProfile && isSketchSweep) {
485
- const invalidProfile = invalidSketchSweepProfile(changes.sketchProfile);
765
+ if ((replacingSketchProfile || faceDraftChanged) && isSketchSweep) {
766
+ const profileToBuild = replacingSketchProfile ? changes.sketchProfile : existing.sketchProfile;
767
+ const invalidProfile = replacingSketchProfile ? invalidSketchSweepProfile(profileToBuild) : undefined;
486
768
  if (invalidProfile)
487
769
  return failed({ reason: "invalid_shape_request", message: invalidProfile });
488
- const frame = existing.cadBrepFrame;
770
+ const frame = existing.faceDraftSourceFrame ?? existing.cadBrepFrame;
489
771
  try {
490
772
  const rebuilt = await sketchSweepShapeFromRequest({
491
773
  id: existing.id,
492
774
  name: existing.name,
493
775
  color: existing.color,
494
776
  hole: existing.hole,
495
- profile: changes.sketchProfile,
777
+ profile: profileToBuild,
496
778
  x: existing.x - (frame?.x ?? 0),
497
779
  z: existing.z - (frame?.z ?? 0),
498
780
  elevation: (existing.elevation ?? 0) - (frame?.elevation ?? 0),
@@ -514,14 +796,61 @@ export function createEngine(store) {
514
796
  resized.size = Math.max(resized.width, resized.depth);
515
797
  const refreshedFaceIds = (await shapeFaces(rebuilt))
516
798
  .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 = {
799
+ const { faceIds: _oldFaceIds, faceColors: _faceColors, edgeTreatments: _edgeTreatments, edgeTreatmentHistory: _edgeTreatmentHistory, cadDisplayEdges: _cadDisplayEdges, cadDisplayEdgesVersion: _cadDisplayEdgesVersion, faceDraft: _faceDraft, faceDraftSourceFrame: _faceDraftSourceFrame, ...sourceMetadata } = existing;
800
+ const rebuiltSweep = {
519
801
  ...sourceMetadata,
520
802
  ...resized,
521
803
  id: existing.id,
522
804
  sketchSourceId: existing.sketchSourceId ?? rebuilt.sketchSourceId,
523
805
  faceIds: refreshedFaceIds,
806
+ faceDraft: undefined,
807
+ faceDraftSourceFrame: undefined,
524
808
  };
809
+ const hasRetainedTreatments = (existing.edgeTreatments?.length ?? 0) > 0
810
+ || (existing.edgeTreatmentHistory?.length ?? 0) > 0;
811
+ const replayFaceDraftAndTreatment = hasRetainedTreatments && faceDraftChanged && requestedFaceDraft !== undefined;
812
+ const replayOriginal = clearingFaceDraft
813
+ ? { ...existing, faceDraft: undefined }
814
+ : replayFaceDraftAndTreatment && existing.faceDraft
815
+ ? { ...existing, faceDraft: { ...existing.faceDraft, ...requestedFaceDraft } }
816
+ : existing;
817
+ const replayedSource = (replacingSketchProfile || clearingFaceDraft || replayFaceDraftAndTreatment) && hasRetainedTreatments
818
+ ? await replaySingleAllEdgeTreatment(replayOriginal, rebuiltSweep)
819
+ : rebuiltSweep;
820
+ if (requestedFaceDraft && replayedSource.faceDraft) {
821
+ // `replaySingleAllEdgeTreatment` has already rebuilt the retained
822
+ // draft before its treatment; applying the draft again would try
823
+ // to operate on post-fillet topology and make source edits fail.
824
+ swept = replayedSource;
825
+ }
826
+ else if (requestedFaceDraft) {
827
+ const priorReference = existing.faceDraft?.sourceFaceId === requestedFaceDraft.sourceFaceId
828
+ ? existing.faceDraft.sourceFaceNormal
829
+ : undefined;
830
+ const selectedOnPrior = priorReference ? undefined
831
+ : (await shapeFaces(existing, isExactSketchRevolve ? { topology: "exact" } : undefined))
832
+ .find((face) => face.id === requestedFaceDraft.sourceFaceId);
833
+ const sourceFaceNormal = priorReference ?? selectedOnPrior?.normal;
834
+ if (!sourceFaceNormal || selectedOnPrior && selectedOnPrior.surfaceType !== "plane") {
835
+ throw new Error("The selected source face has no stable planar reference.");
836
+ }
837
+ const currentFace = await findUniqueDraftFace(replayedSource, sourceFaceNormal);
838
+ if (!currentFace)
839
+ throw new Error("The selected source face is missing or ambiguous after the sketch edit.");
840
+ const faceDraft = { ...requestedFaceDraft, sourceFaceNormal };
841
+ const result = await applySketchFaceDraft(replayedSource, {
842
+ ...requestedFaceDraft,
843
+ sourceFaceId: currentFace.id,
844
+ });
845
+ swept = {
846
+ ...shapeWithCadResult(replayedSource, result),
847
+ faceDraft,
848
+ faceDraftSourceFrame: existing.faceDraftSourceFrame ?? existing.cadBrepFrame,
849
+ };
850
+ }
851
+ else {
852
+ swept = replayedSource;
853
+ }
525
854
  }
526
855
  catch (error) {
527
856
  return failed({
@@ -530,7 +859,89 @@ export function createEngine(store) {
530
859
  });
531
860
  }
532
861
  }
533
- if (revolveChanges.length > 0 && sweptShape) {
862
+ if ((faceDraftChanged || (replacingSketchProfile && existing.faceDraft)
863
+ || (revolveChanges.length > 0 && existing.faceDraft)) && isSketchRevolve) {
864
+ try {
865
+ const runtime = await serverManifoldRuntime();
866
+ const sourceFrame = existing.faceDraftSourceFrame ?? existing.cadBrepFrame;
867
+ const rebuildSeed = sourceFrame ? {
868
+ ...existing,
869
+ x: sourceFrame.x,
870
+ z: sourceFrame.z,
871
+ elevation: sourceFrame.elevation,
872
+ } : existing;
873
+ const rebuiltAtNaturalScale = rebuiltRevolveShape(runtime, rebuildSeed, {
874
+ profile: (replacingSketchProfile ? changes.sketchProfile : existing.sketchProfile),
875
+ settings: revolveChanges.length > 0
876
+ ? requestedRevolveSettings(existing.sketchRevolve, changes)
877
+ : existing.sketchRevolve,
878
+ });
879
+ const sourceScale = existing.faceDraftSourceScale ?? revolveSourceScale(existing);
880
+ const rebuilt = resizeRebuiltRevolve(rebuiltAtNaturalScale, sourceScale);
881
+ if (requestedFaceDraft) {
882
+ const exactBase = shapeWithCadResult(rebuilt, await exactPlainSketchRevolveResult(rebuilt));
883
+ const priorReference = existing.faceDraft?.sourceFaceId === requestedFaceDraft.sourceFaceId
884
+ ? existing.faceDraft.sourceFaceNormal
885
+ : undefined;
886
+ const selectedOnPrior = priorReference ? undefined
887
+ : (await shapeFaces(existing, { topology: "exact" }))
888
+ .find((face) => face.id === requestedFaceDraft.sourceFaceId);
889
+ const sourceFaceNormal = priorReference ?? selectedOnPrior?.normal;
890
+ if (!sourceFaceNormal || selectedOnPrior && selectedOnPrior.surfaceType !== "plane") {
891
+ throw new Error("The selected source face has no stable planar reference.");
892
+ }
893
+ const currentFace = await findUniqueDraftFace(exactBase, sourceFaceNormal);
894
+ if (!currentFace)
895
+ throw new Error("The selected source face is missing or ambiguous after the sketch edit.");
896
+ const faceDraft = { ...requestedFaceDraft, sourceFaceNormal };
897
+ const result = await applySketchFaceDraft(exactBase, {
898
+ ...requestedFaceDraft,
899
+ sourceFaceId: currentFace.id,
900
+ });
901
+ swept = {
902
+ ...shapeWithCadResult(exactBase, result),
903
+ faceDraft,
904
+ faceDraftSourceFrame: existing.faceDraftSourceFrame ?? existing.cadBrepFrame ?? exactBase.cadBrepFrame,
905
+ faceDraftSourceScale: sourceScale,
906
+ };
907
+ swept = await replaySingleAllEdgeTreatment(existing, swept);
908
+ }
909
+ else {
910
+ const clearedDraft = {
911
+ ...rebuilt,
912
+ faceDraft: undefined,
913
+ faceDraftSourceFrame: undefined,
914
+ faceDraftSourceScale: undefined,
915
+ };
916
+ swept = clearingFaceDraft
917
+ ? await replaySingleAllEdgeTreatment({ ...existing, faceDraft: undefined }, clearedDraft)
918
+ : clearedDraft;
919
+ }
920
+ }
921
+ catch (error) {
922
+ return failed({
923
+ reason: "sweep_failed",
924
+ message: error instanceof Error ? error.message : "The sketch revolve could not be rebuilt.",
925
+ });
926
+ }
927
+ }
928
+ else if (replacingSketchProfile && isSketchRevolve) {
929
+ try {
930
+ const runtime = await serverManifoldRuntime();
931
+ const rebuilt = rebuiltRevolveShape(runtime, existing, {
932
+ profile: changes.sketchProfile,
933
+ settings: existing.sketchRevolve,
934
+ });
935
+ swept = await replaySingleAllEdgeTreatment(existing, rebuilt);
936
+ }
937
+ catch (error) {
938
+ return failed({
939
+ reason: "sweep_failed",
940
+ message: error instanceof Error ? error.message : "The sketch revolve could not be rebuilt.",
941
+ });
942
+ }
943
+ }
944
+ if (revolveChanges.length > 0 && sweptShape && !existing.faceDraft) {
534
945
  const outline = changes.profile ?? changes.outline ?? outlineFromProfile(existing.sketchProfile);
535
946
  if (!outline) {
536
947
  return failed({
@@ -545,10 +956,11 @@ export function createEngine(store) {
545
956
  return failed({ reason: "invalid_shape_request", message: parsed.error });
546
957
  try {
547
958
  const runtime = await serverManifoldRuntime();
548
- swept = rebuiltRevolveShape(runtime, existing, {
959
+ const rebuilt = rebuiltRevolveShape(runtime, existing, {
549
960
  profile: parsed.profile,
550
961
  settings: requestedRevolveSettings(existing.sketchRevolve, changes),
551
962
  });
963
+ swept = await replaySingleAllEdgeTreatment(existing, rebuilt);
552
964
  }
553
965
  catch (error) {
554
966
  return failed({
@@ -562,6 +974,8 @@ export function createEngine(store) {
562
974
  const sweepFields = [
563
975
  ...(sweptShape ? REVOLVE_CHANGE_FIELDS : REVOLVE_ONLY_FIELDS),
564
976
  "sketchProfile",
977
+ "draftPrismAngle",
978
+ "faceDraft",
565
979
  ];
566
980
  // Shape summaries expose the source ID for identity; echoing a full
567
981
  // summary into PATCH must not let callers replace that identity.
@@ -806,12 +1220,12 @@ export function createEngine(store) {
806
1220
  return failed(cadFailure(error));
807
1221
  }
808
1222
  }),
809
- faces: (projectId, shapeId) => reading(projectId, async (document) => {
1223
+ faces: (projectId, shapeId, topology) => reading(projectId, async (document) => {
810
1224
  const found = oneShape(document, shapeId);
811
1225
  if (!found.ok)
812
1226
  return found;
813
1227
  try {
814
- return ok(await shapeFaces(found.value));
1228
+ return ok(await shapeFaces(found.value, topology === "exact" ? { topology } : undefined));
815
1229
  }
816
1230
  catch (error) {
817
1231
  return failed(cadFailure(error));
@@ -868,8 +1282,47 @@ export function createEngine(store) {
868
1282
  return failed(cadFailure(error));
869
1283
  }
870
1284
  }),
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 })))),
1285
+ treat: (projectId, shapeId, treatment) => cadTreatment(projectId, shapeId, (treatment.edges === undefined || treatment.edges.length === 0)
1286
+ && treatment.kind === "chamfer" && (treatment.angle === undefined || Math.abs(treatment.angle - 45) < 0.001)
1287
+ ? "all-edge-chamfer-treatment"
1288
+ : (treatment.edges === undefined || treatment.edges.length === 0)
1289
+ && treatment.kind === "fillet"
1290
+ ? "all-edge-treatment"
1291
+ : "edge-treatment", async (shape) => {
1292
+ const treated = shapeWithCadResult(shape, await applyEdgeTreatment(shape, treatment));
1293
+ if (shape.draftPrismAngle === undefined && shape.faceDraft === undefined)
1294
+ return treated;
1295
+ const feature = {
1296
+ kind: treatment.kind,
1297
+ amount: treatment.amount,
1298
+ edgeCount: await allEdgeTreatmentEdgeCount(shape, treatment.kind),
1299
+ allEdges: true,
1300
+ ...(treatment.kind === "chamfer" ? { chamferAngle: treatment.angle ?? 45 } : {}),
1301
+ };
1302
+ const history = shape.edgeTreatmentHistory ?? [];
1303
+ return {
1304
+ ...treated,
1305
+ edgeTreatments: [...(shape.edgeTreatments ?? []), feature],
1306
+ edgeTreatmentHistory: [
1307
+ ...history,
1308
+ {
1309
+ id: createLocalId("edge-history"),
1310
+ createdAt: Date.now(),
1311
+ feature,
1312
+ before: structuredClone(shape),
1313
+ },
1314
+ ],
1315
+ };
1316
+ }),
1317
+ draftPrism: (projectId, shapeId, angle) => cadTreatment(projectId, shapeId, "prism-draft", async (shape) => {
1318
+ const sourceFrame = shape.draftPrismSourceFrame ?? shape.cadBrepFrame;
1319
+ return {
1320
+ ...shapeWithCadResult(shape, await applyDraftPrism(shape, { angle })),
1321
+ draftPrismAngle: angle,
1322
+ draftPrismSourceHeight: shape.draftPrismSourceHeight ?? shape.height,
1323
+ ...(sourceFrame ? { draftPrismSourceFrame: sourceFrame } : {}),
1324
+ };
1325
+ }),
873
1326
  mirrorBaked: (projectId, ids, axis) => mutating(projectId, async (document) => {
874
1327
  if (ids.length < 1)
875
1328
  return failed({ reason: "selection_too_small", needed: 1, given: 0 });
@@ -899,7 +1352,7 @@ export function createEngine(store) {
899
1352
  revision: await writeShapes(projectId, document, shapes),
900
1353
  });
901
1354
  }),
902
- shell: (projectId, shapeId, request) => cadTreatment(projectId, shapeId, async (shape) => (shapeWithCadResult(shape, await applyShell(shape, request)))),
1355
+ shell: (projectId, shapeId, request) => cadTreatment(projectId, shapeId, "shell", async (shape) => (shapeWithCadResult(shape, await applyShell(shape, request)))),
903
1356
  };
904
1357
  }
905
1358
  //# sourceMappingURL=engine.js.map