@constructorfleet/sketchforge-engine 1.27.0 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/dist/cadAnalysis.d.ts.map +1 -1
  2. package/dist/cadAnalysis.js +14 -0
  3. package/dist/cadAnalysis.js.map +1 -1
  4. package/dist/cadBakeMetadata.d.ts +11 -2
  5. package/dist/cadBakeMetadata.d.ts.map +1 -1
  6. package/dist/cadBakeMetadata.js +9 -0
  7. package/dist/cadBakeMetadata.js.map +1 -1
  8. package/dist/cadProjection.d.ts.map +1 -1
  9. package/dist/cadProjection.js.map +1 -1
  10. package/dist/cadSection.d.ts.map +1 -1
  11. package/dist/cadShell.d.ts +1 -1
  12. package/dist/cadShell.d.ts.map +1 -1
  13. package/dist/cadShell.js +7 -0
  14. package/dist/cadShell.js.map +1 -1
  15. package/dist/cadSolids.d.ts.map +1 -1
  16. package/dist/cadSolids.js +18 -1
  17. package/dist/cadSolids.js.map +1 -1
  18. package/dist/cutBrepFrameMigration.d.ts +46 -0
  19. package/dist/cutBrepFrameMigration.d.ts.map +1 -0
  20. package/dist/cutBrepFrameMigration.js +105 -0
  21. package/dist/cutBrepFrameMigration.js.map +1 -0
  22. package/dist/cutterPadding.d.ts.map +1 -1
  23. package/dist/domain/cameraProjection.js.map +1 -1
  24. package/dist/domain/sketchforge.d.ts +30 -0
  25. package/dist/domain/sketchforge.d.ts.map +1 -1
  26. package/dist/editorHistory.d.ts.map +1 -1
  27. package/dist/editorHistory.js.map +1 -1
  28. package/dist/engine.d.ts +3 -3
  29. package/dist/engine.d.ts.map +1 -1
  30. package/dist/engine.js +102 -66
  31. package/dist/engine.js.map +1 -1
  32. package/dist/exactBooleanParts.d.ts +4 -2
  33. package/dist/exactBooleanParts.d.ts.map +1 -1
  34. package/dist/exactBooleanParts.js +7 -3
  35. package/dist/exactBooleanParts.js.map +1 -1
  36. package/dist/exactBooleanSolids.d.ts.map +1 -1
  37. package/dist/gearProfile.d.ts.map +1 -1
  38. package/dist/index.d.ts +1 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +1 -1
  41. package/dist/index.js.map +1 -1
  42. package/dist/loftProfile.d.ts +1 -1
  43. package/dist/loftProfile.d.ts.map +1 -1
  44. package/dist/loftProfile.js.map +1 -1
  45. package/dist/meshEdgeProjection.d.ts +104 -0
  46. package/dist/meshEdgeProjection.d.ts.map +1 -0
  47. package/dist/meshEdgeProjection.js +439 -0
  48. package/dist/meshEdgeProjection.js.map +1 -0
  49. package/dist/meshImport.d.ts.map +1 -1
  50. package/dist/meshImport.js +2 -20
  51. package/dist/meshImport.js.map +1 -1
  52. package/dist/meshMassProperties.d.ts +92 -0
  53. package/dist/meshMassProperties.d.ts.map +1 -0
  54. package/dist/meshMassProperties.js +570 -0
  55. package/dist/meshMassProperties.js.map +1 -0
  56. package/dist/meshSection.d.ts +44 -0
  57. package/dist/meshSection.d.ts.map +1 -0
  58. package/dist/meshSection.js +154 -0
  59. package/dist/meshSection.js.map +1 -0
  60. package/dist/meshShellOverlap.d.ts +75 -0
  61. package/dist/meshShellOverlap.d.ts.map +1 -0
  62. package/dist/meshShellOverlap.js +797 -0
  63. package/dist/meshShellOverlap.js.map +1 -0
  64. package/dist/meshShellSolids.d.ts +44 -0
  65. package/dist/meshShellSolids.d.ts.map +1 -0
  66. package/dist/meshShellSolids.js +116 -0
  67. package/dist/meshShellSolids.js.map +1 -0
  68. package/dist/nodeSvgDomParser.d.ts +3 -0
  69. package/dist/nodeSvgDomParser.d.ts.map +1 -0
  70. package/dist/nodeSvgDomParser.js +53 -0
  71. package/dist/nodeSvgDomParser.js.map +1 -0
  72. package/dist/parametricRecompute.d.ts.map +1 -1
  73. package/dist/parametricRecompute.js +56 -6
  74. package/dist/parametricRecompute.js.map +1 -1
  75. package/dist/placementWorkplane.d.ts.map +1 -1
  76. package/dist/placementWorkplane.js.map +1 -1
  77. package/dist/projectAssets.d.ts +1 -1
  78. package/dist/projectStore.d.ts +1 -1
  79. package/dist/projectStore.d.ts.map +1 -1
  80. package/dist/projectStore.js +23 -2
  81. package/dist/projectStore.js.map +1 -1
  82. package/dist/revolveRequests.d.ts +57 -1
  83. package/dist/revolveRequests.d.ts.map +1 -1
  84. package/dist/revolveRequests.js +103 -4
  85. package/dist/revolveRequests.js.map +1 -1
  86. package/dist/sceneExport.d.ts +6 -0
  87. package/dist/sceneExport.d.ts.map +1 -1
  88. package/dist/sceneExport.js +6 -2
  89. package/dist/sceneExport.js.map +1 -1
  90. package/dist/serverCadInterference.d.ts.map +1 -1
  91. package/dist/serverCadInterference.js +9 -2
  92. package/dist/serverCadInterference.js.map +1 -1
  93. package/dist/serverCadOperations.d.ts +73 -4
  94. package/dist/serverCadOperations.d.ts.map +1 -1
  95. package/dist/serverCadOperations.js +285 -51
  96. package/dist/serverCadOperations.js.map +1 -1
  97. package/dist/serverCadProjection.d.ts +14 -1
  98. package/dist/serverCadProjection.d.ts.map +1 -1
  99. package/dist/serverCadProjection.js +135 -13
  100. package/dist/serverCadProjection.js.map +1 -1
  101. package/dist/serverCadProperties.d.ts +14 -7
  102. package/dist/serverCadProperties.d.ts.map +1 -1
  103. package/dist/serverCadProperties.js +60 -24
  104. package/dist/serverCadProperties.js.map +1 -1
  105. package/dist/serverCadSection.d.ts +21 -1
  106. package/dist/serverCadSection.d.ts.map +1 -1
  107. package/dist/serverCadSection.js +157 -43
  108. package/dist/serverCadSection.js.map +1 -1
  109. package/dist/serverCadShell.d.ts.map +1 -1
  110. package/dist/serverCadShell.js +0 -3
  111. package/dist/serverCadShell.js.map +1 -1
  112. package/dist/serverShapeBooleans.d.ts +75 -0
  113. package/dist/serverShapeBooleans.d.ts.map +1 -1
  114. package/dist/serverShapeBooleans.js +251 -9
  115. package/dist/serverShapeBooleans.js.map +1 -1
  116. package/dist/serverShapeGroups.js.map +1 -1
  117. package/dist/shapeBounds.d.ts.map +1 -1
  118. package/dist/shapeCatalog.d.ts.map +1 -1
  119. package/dist/shapeCatalog.js +1 -0
  120. package/dist/shapeCatalog.js.map +1 -1
  121. package/dist/shapeRequests.d.ts +13 -0
  122. package/dist/shapeRequests.d.ts.map +1 -1
  123. package/dist/shapeRequests.js +14 -0
  124. package/dist/shapeRequests.js.map +1 -1
  125. package/dist/sketchCadProfile.d.ts +13 -0
  126. package/dist/sketchCadProfile.d.ts.map +1 -1
  127. package/dist/sketchCadProfile.js +108 -0
  128. package/dist/sketchCadProfile.js.map +1 -1
  129. package/dist/sketchCadSweep.js.map +1 -1
  130. package/dist/sketchEdgeSegmentLineage.d.ts +32 -0
  131. package/dist/sketchEdgeSegmentLineage.d.ts.map +1 -0
  132. package/dist/sketchEdgeSegmentLineage.js +49 -0
  133. package/dist/sketchEdgeSegmentLineage.js.map +1 -0
  134. package/dist/sketchEdgeSegmentLineageKernel.d.ts +83 -0
  135. package/dist/sketchEdgeSegmentLineageKernel.d.ts.map +1 -0
  136. package/dist/sketchEdgeSegmentLineageKernel.js +125 -0
  137. package/dist/sketchEdgeSegmentLineageKernel.js.map +1 -0
  138. package/dist/sketchFaceDraftKernel.d.ts +11 -4
  139. package/dist/sketchFaceDraftKernel.d.ts.map +1 -1
  140. package/dist/sketchFaceDraftKernel.js +8 -2
  141. package/dist/sketchFaceDraftKernel.js.map +1 -1
  142. package/dist/sketchRevolve.d.ts +13 -0
  143. package/dist/sketchRevolve.d.ts.map +1 -1
  144. package/dist/sketchRevolve.js +4 -1
  145. package/dist/sketchRevolve.js.map +1 -1
  146. package/dist/skfProject.d.ts +1 -1
  147. package/dist/skfProject.d.ts.map +1 -1
  148. package/dist/skfProject.js +63 -4
  149. package/dist/skfProject.js.map +1 -1
  150. package/dist/sphereTessellation.d.ts.map +1 -1
  151. package/dist/stepAssemblyExplode.d.ts.map +1 -1
  152. package/dist/stepAssemblyExplode.js +1 -1
  153. package/dist/stepAssemblyExplode.js.map +1 -1
  154. package/dist/stepAssemblyHierarchy.js.map +1 -1
  155. package/dist/stepComponentMetadata.d.ts +1 -1
  156. package/dist/stepComponentMetadata.d.ts.map +1 -1
  157. package/dist/stepComponentMetadata.js +93 -39
  158. package/dist/stepComponentMetadata.js.map +1 -1
  159. package/dist/stepExport.d.ts +14 -1
  160. package/dist/stepExport.d.ts.map +1 -1
  161. package/dist/stepExport.js +81 -32
  162. package/dist/stepExport.js.map +1 -1
  163. package/dist/stepImportCommon.d.ts +4 -1
  164. package/dist/stepImportCommon.d.ts.map +1 -1
  165. package/dist/stepImportCommon.js +7 -6
  166. package/dist/stepImportCommon.js.map +1 -1
  167. package/dist/stlImport.js.map +1 -1
  168. package/dist/support/cadModifierTypes.d.ts +41 -1
  169. package/dist/support/cadModifierTypes.d.ts.map +1 -1
  170. package/dist/support/i18n/LocalizedError.d.ts.map +1 -1
  171. package/dist/support/i18n/locales/en/messages.json +30 -2
  172. package/dist/support/i18n/locales.d.ts.map +1 -1
  173. package/dist/support/lib/appTheme.d.ts.map +1 -1
  174. package/dist/support/lib/materialPresets.js.map +1 -1
  175. package/dist/support/lib/measurementUnits.d.ts.map +1 -1
  176. package/dist/support/lib/operator-configuration.json +13 -1
  177. package/dist/support/lib/operatorConfiguration.d.ts +5 -5
  178. package/dist/support/lib/workplaneGrid.js.map +1 -1
  179. package/dist/svgImport.d.ts +2 -0
  180. package/dist/svgImport.d.ts.map +1 -1
  181. package/dist/svgImport.js +7 -0
  182. package/dist/svgImport.js.map +1 -1
  183. package/dist/threadCatalog.js.map +1 -1
  184. package/dist/workplaneSettings.js.map +1 -1
  185. package/dist/workplaneShapes.d.ts +2 -2
  186. package/dist/workplaneShapes.d.ts.map +1 -1
  187. package/dist/workplaneShapes.js.map +1 -1
  188. package/package.json +7 -6
@@ -12,7 +12,9 @@
12
12
  * talks to `self.postMessage` and is close to upstream's, and prising it apart
13
13
  * would conflict with every upstream change to it. What is shared instead is
14
14
  * the result format, so a shape filleted here is indistinguishable in the
15
- * editor from one filleted there.
15
+ * editor from one filleted there -- and, through `meshShellSolids`, how a mesh
16
+ * of several closed shells becomes one solid, so the two agree on which meshes
17
+ * with internal voids are solids (#527).
16
18
  */
17
19
  import type { EvolutionData, OcctKernel, ShapeHandle } from "occt-wasm";
18
20
  import type { SketchProfile, WorkplaneShape } from "./domain/sketchforge.js";
@@ -48,6 +50,28 @@ export declare class ServerCadError extends Error {
48
50
  constructor(message: string, status?: number);
49
51
  }
50
52
  export type { SketchFaceDraftRequest } from "./sketchFaceDraftKernel.js";
53
+ /**
54
+ * How far a retained sketch output has been resized since it was swept: its
55
+ * drawn dimensions over the ones its source geometry was built at.
56
+ */
57
+ export declare function sketchResizeFactors(shape: WorkplaneShape): {
58
+ width: number;
59
+ depth: number;
60
+ height: number;
61
+ };
62
+ /**
63
+ * A sketch extrusion whose exact solid is still its bare sweep -- no draft,
64
+ * treatment or shell -- has its B-rep stored at the size it was drawn. A resize
65
+ * since then lives only in its dimensions, so its exact solid at the size it is
66
+ * shown is its profile scaled by that resize and extruded to its height, the
67
+ * rebuild `applySketchFaceDraft` makes (#452 for width and depth; #546 fixed
68
+ * the engine handing a height-resized extrusion to it at twice its height).
69
+ * Null when there is no resize to apply, or the solid is something else.
70
+ *
71
+ * Without this, a fillet, chamfer or shell of a resized extrusion was built
72
+ * from the B-rep as drawn and stored back at the original size (#539).
73
+ */
74
+ export declare function resizedSketchExtrusionProfile(shape: WorkplaneShape): SketchProfile | null;
51
75
  /**
52
76
  * Whether the kernel considers a shape sound.
53
77
  *
@@ -58,8 +82,17 @@ export type { SketchFaceDraftRequest } from "./sketchFaceDraftKernel.js";
58
82
  * own check, so the two paths agree about what a usable result is.
59
83
  */
60
84
  export declare function cadResultIsValid(cad: OcctKernel, shape: ShapeHandle): boolean;
61
- /** A solid sewn from a flat list of triangle corners, which is how a stored mesh arrives. */
85
+ /**
86
+ * A solid sewn from a flat list of triangle corners, which is how a stored mesh arrives.
87
+ *
88
+ * A mesh made of several closed shells -- a part with a sealed internal void,
89
+ * or separate bodies -- is built one shell at a time and put back together by
90
+ * nesting (#501). Sewn all at once, OCCT got a shell inside a shell, could make
91
+ * no one solid of it, and refused a watertight part outright.
92
+ */
62
93
  export declare function solidFromMeshPositions(cad: OcctKernel, positions: number[]): ShapeHandle;
94
+ /** The exact path's refusal for a mesh error the kernel-free measures throw; anything else unchanged. */
95
+ export declare function asServerCadError(error: unknown): unknown;
63
96
  /**
64
97
  * Whether a treated result has the shape's rotation baked into its geometry.
65
98
  *
@@ -97,7 +130,9 @@ export declare function localSolidForShape(cad: OcctKernel, shape: WorkplaneShap
97
130
  * reads the same predicate.
98
131
  *
99
132
  * For stored geometry this is the local solid with only its mirror applied:
100
- * the rotation stays on the shape, which is where it already was.
133
+ * the rotation stays on the shape, which is where it already was. A resized
134
+ * bare sketch extrusion is the exception: it is rebuilt at its drawn size
135
+ * (`resizedSketchExtrusionProfile`), since its stored B-rep is not.
101
136
  *
102
137
  * Mirrored either way, because the result is stored with the shape's mirror
103
138
  * flags cleared. Building it unmirrored handed back the unmirrored shape and
@@ -137,6 +172,40 @@ export declare function applyBakedMirror(shape: WorkplaneShape, axis: "x" | "y"
137
172
  export declare function applyEdgeTreatment(shape: WorkplaneShape, treatment: ServerCadTreatment): Promise<ServerCadResult>;
138
173
  /** Count currently chamferable/filletable edges without preserving topology indexes. */
139
174
  export declare function allEdgeTreatmentEdgeCount(shape: WorkplaneShape, _kind: ServerCadTreatment["kind"]): Promise<number>;
175
+ /**
176
+ * Whether `placedSolidForShape` builds this shape as the exact solid of its
177
+ * revolve rather than sewing its stored mesh, which is what measuring it
178
+ * reports as its precision.
179
+ */
180
+ export declare function hasExactRevolveSolid(shape: WorkplaneShape): boolean;
181
+ /**
182
+ * The angle a mesh of a result built from these shapes may turn through
183
+ * between facets, so that it is no coarser than the finest exact revolve
184
+ * among them, or undefined when none is one.
185
+ *
186
+ * Before a revolve was exact, a cut through it was made from its faceted
187
+ * sweep, so the stored result was exactly as fine as its `sides` asked. The
188
+ * exact result is meshed at a fixed chord height instead, which on a small
189
+ * ring is coarser than the sweep it replaced and ignores `sides` altogether.
190
+ * A group record is searched through, since it is cut as its members.
191
+ *
192
+ * Twice the sweep's facet angle, because OCCT's angular deflection bounds the
193
+ * turn between a facet's normal and the surface's, half a facet each way: at
194
+ * this value a full circle gets `sides` facets, the sweep's own count, and a
195
+ * 64-sided ring cut in half meshes to the 260 triangles its sweep gave.
196
+ */
197
+ export declare function exactRevolveAngularDeflection(shapes: readonly WorkplaneShape[]): number | undefined;
198
+ /**
199
+ * The exact solid of a plain revolve, full turn or partial sweep, in the frame
200
+ * its stored mesh is drawn in -- centred on the mesh's box in X and Z, base at
201
+ * Y=0, at its drawn size -- or null when it has none (#523).
202
+ *
203
+ * That frame is what lets the caller place it exactly as the mesh is placed.
204
+ * The axis comes from the stored record rather than from the solid's own box,
205
+ * because a faceted partial sweep's box is not the exact sweep's box, and
206
+ * centring on the wrong one moves the solid off its axis.
207
+ */
208
+ export declare function exactRevolveLocalSolid(cad: OcctKernel, shape: WorkplaneShape): ShapeHandle | null;
140
209
  /**
141
210
  * The solid `GET /edges` numbers and a fillet or chamfer rounds.
142
211
  *
@@ -174,7 +243,7 @@ export declare function applySketchFaceDraft(shape: WorkplaneShape, request: Ske
174
243
  export declare function meshedCadResult(cad: OcctKernel, result: ShapeHandle, sourceShape?: WorkplaneShape, lineage?: {
175
244
  sourceFaceHashes: number[];
176
245
  evolution: EvolutionData;
177
- }, sourceSolid?: ShapeHandle): ServerCadResult;
246
+ }, sourceSolid?: ShapeHandle, angularDeflection?: number): ServerCadResult;
178
247
  /**
179
248
  * The shape as it stands after a treatment.
180
249
  *
@@ -1 +1 @@
1
- {"version":3,"file":"serverCadOperations.d.ts","sourceRoot":"","sources":["../src/serverCadOperations.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"serverCadOperations.d.ts","sourceRoot":"","sources":["../src/serverCadOperations.ts"],"names":[],"mappings":"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"}
@@ -12,17 +12,22 @@
12
12
  * talks to `self.postMessage` and is close to upstream's, and prising it apart
13
13
  * would conflict with every upstream change to it. What is shared instead is
14
14
  * the result format, so a shape filleted here is indistinguishable in the
15
- * editor from one filleted there.
15
+ * editor from one filleted there -- and, through `meshShellSolids`, how a mesh
16
+ * of several closed shells becomes one solid, so the two agree on which meshes
17
+ * with internal voids are solids (#527).
16
18
  */
17
19
  import { faceIdsFromEvolution } from "./faceLineage.js";
18
20
  import { withCadKernel } from "./serverCadKernel.js";
19
21
  import { exactSolidForShape, mirroredInShapeFrame, shapeIsTurned, unrotatedSolidForShape, UnbuildableShapeError, EXACT_SOLID_KINDS, } from "./cadSolids.js";
20
- import { canonicalizeShape } from "./workplaneShapes.js";
22
+ import { canonicalizeShape, shapeHasTaper } from "./workplaneShapes.js";
21
23
  import { createLocalId } from "./localIds.js";
22
- import { cadSketchRegions } from "./sketchCadProfile.js";
24
+ import { cadSketchRegions, draftPrismLoop } from "./sketchCadProfile.js";
23
25
  import { sketchExtrusionSolid } from "./sketchExtrusionSolid.js";
24
26
  import { normalizeSketchRevolveSettings, sketchProfileToRevolvePolygons } from "./sketchRevolve.js";
25
27
  import { triangleFaceGroups } from "./cadFaceMesh.js";
28
+ import { meshDataFromPositions } from "./meshMassProperties.js";
29
+ import { MeshShellsOverlapError } from "./meshShellOverlap.js";
30
+ import { MeshShellSolidError, nestedMeshShellSolid } from "./meshShellSolids.js";
26
31
  import { draftSketchFace, draftPrismProfileOffset, faceIdsForCadResult } from "./sketchFaceDraftKernel.js";
27
32
  export class ServerCadError extends Error {
28
33
  status;
@@ -35,9 +40,16 @@ export class ServerCadError extends Error {
35
40
  const MESH_DEFLECTION = 0.1;
36
41
  const MIN_CAD_DIMENSION = 0.01;
37
42
  const FACE_HASH_UPPER_BOUND = 2_147_483_647;
38
- function sketchResizeFactors(shape) {
43
+ /**
44
+ * How far a retained sketch output has been resized since it was swept: its
45
+ * drawn dimensions over the ones its source geometry was built at.
46
+ */
47
+ export function sketchResizeFactors(shape) {
39
48
  const mesh = shape.importedMesh;
40
- if (shape.faceDraftSourceScale)
49
+ // A face-drafted output's bounds include its taper, so the resize recorded
50
+ // with the draft is the one to use. Once the draft is gone the bounds are
51
+ // the extrusion's again, and a recorded scale would hide any resize since.
52
+ if (shape.faceDraft && shape.faceDraftSourceScale)
41
53
  return shape.faceDraftSourceScale;
42
54
  const factor = (drawn, base) => base && base > 0 ? drawn / base : 1;
43
55
  return {
@@ -60,6 +72,39 @@ function scaledSketchProfile(profile, width, depth) {
60
72
  });
61
73
  return { ...profile, points: profile.points.map(scalePoint) };
62
74
  }
75
+ const isResized = (factors) => [factors.width, factors.depth, factors.height].some((factor) => Math.abs(factor - 1) > 1e-8);
76
+ /**
77
+ * A sketch extrusion whose exact solid is still its bare sweep -- no draft,
78
+ * treatment or shell -- has its B-rep stored at the size it was drawn. A resize
79
+ * since then lives only in its dimensions, so its exact solid at the size it is
80
+ * shown is its profile scaled by that resize and extruded to its height, the
81
+ * rebuild `applySketchFaceDraft` makes (#452 for width and depth; #546 fixed
82
+ * the engine handing a height-resized extrusion to it at twice its height).
83
+ * Null when there is no resize to apply, or the solid is something else.
84
+ *
85
+ * Without this, a fillet, chamfer or shell of a resized extrusion was built
86
+ * from the B-rep as drawn and stored back at the original size (#539).
87
+ */
88
+ export function resizedSketchExtrusionProfile(shape) {
89
+ if (shape.sketchOperation !== "extrude" || !shape.sketchProfile || !shape.cadBrep)
90
+ return null;
91
+ if (shape.faceDraft !== undefined || shape.draftPrismAngle !== undefined || shape.shell
92
+ || (shape.edgeTreatments?.length ?? 0) > 0 || (shape.edgeTreatmentHistory?.length ?? 0) > 0)
93
+ return null;
94
+ const resize = sketchResizeFactors(shape);
95
+ return isResized(resize) ? scaledSketchProfile(shape.sketchProfile, resize.width, resize.depth) : null;
96
+ }
97
+ /** The bare extrusion's exact solid at its drawn size, or its stored solid when it has not been resized. */
98
+ function storedOrResizedSolid(cad, shape) {
99
+ const resizedProfile = resizedSketchExtrusionProfile(shape);
100
+ if (!resizedProfile)
101
+ return localSolidForShape(cad, shape);
102
+ const solid = sketchExtrusionSolid(cad, resizedProfile, shape.height);
103
+ if (!cadResultIsValid(cad, solid)) {
104
+ throw new ServerCadError("The resized sketch extrusion could not be rebuilt as exact CAD geometry.");
105
+ }
106
+ return solid;
107
+ }
63
108
  function meshPositionsOf(shape) {
64
109
  const positions = shape.importedMesh?.positions;
65
110
  return positions && positions.length >= 9 ? positions : null;
@@ -117,8 +162,39 @@ function solidFromBrep(cad, brep) {
117
162
  }
118
163
  throw new ServerCadError("The stored exact geometry for that shape could not be read.");
119
164
  }
120
- /** A solid sewn from a flat list of triangle corners, which is how a stored mesh arrives. */
165
+ /**
166
+ * A solid sewn from a flat list of triangle corners, which is how a stored mesh arrives.
167
+ *
168
+ * A mesh made of several closed shells -- a part with a sealed internal void,
169
+ * or separate bodies -- is built one shell at a time and put back together by
170
+ * nesting (#501). Sewn all at once, OCCT got a shell inside a shell, could make
171
+ * no one solid of it, and refused a watertight part outright.
172
+ */
121
173
  export function solidFromMeshPositions(cad, positions) {
174
+ let nested;
175
+ try {
176
+ nested = nestedMeshShellSolid(cad, meshDataFromPositions(positions), (shell) => sewnMeshSolid(cad, shell));
177
+ }
178
+ catch (error) {
179
+ if (!(error instanceof MeshShellSolidError))
180
+ throw error;
181
+ throw new ServerCadError(error.reason === "shells-overlap" ? MESH_SHELLS_OVERLAP : MESH_NOT_SOLID);
182
+ }
183
+ return nested ?? sewnMeshSolid(cad, positions);
184
+ }
185
+ /** The exact path's refusal for a mesh error the kernel-free measures throw; anything else unchanged. */
186
+ export function asServerCadError(error) {
187
+ return error instanceof MeshShellsOverlapError ? new ServerCadError(MESH_SHELLS_OVERLAP) : error;
188
+ }
189
+ const MESH_SHELLS_OVERLAP = "That shape's mesh has closed bodies that overlap without one enclosing the other, so the CAD kernel cannot tell which side of each is material. Union the bodies in the mesh before importing it.";
190
+ /**
191
+ * Why a mesh could not become a solid. Shared by every exact path that reads a
192
+ * mesh -- properties, booleans, interference, drawings -- so it names none of
193
+ * them; it used to say "cannot be filleted" to all of them.
194
+ */
195
+ const MESH_NOT_SOLID = "That shape's mesh is not a closed solid, so the CAD kernel cannot build exact geometry from it.";
196
+ /** One shell's triangles sewn into a solid, loosening the tolerance until it closes. */
197
+ function sewnMeshSolid(cad, positions) {
122
198
  const imported = cad.importStl(asciiStlFromPositions(positions));
123
199
  let shape = cad.fixShape(imported);
124
200
  if (isValidSolid(cad, shape))
@@ -139,7 +215,7 @@ export function solidFromMeshPositions(cad, positions) {
139
215
  // Try the next tolerance.
140
216
  }
141
217
  }
142
- throw new ServerCadError("That shape's mesh is not a closed solid, so it cannot be filleted.");
218
+ throw new ServerCadError(MESH_NOT_SOLID);
143
219
  }
144
220
  /**
145
221
  * Whether the shape's geometry has to be turned, or arrives already turned.
@@ -209,7 +285,9 @@ export function localSolidForShape(cad, shape) {
209
285
  * reads the same predicate.
210
286
  *
211
287
  * For stored geometry this is the local solid with only its mirror applied:
212
- * the rotation stays on the shape, which is where it already was.
288
+ * the rotation stays on the shape, which is where it already was. A resized
289
+ * bare sketch extrusion is the exception: it is rebuilt at its drawn size
290
+ * (`resizedSketchExtrusionProfile`), since its stored B-rep is not.
213
291
  *
214
292
  * Mirrored either way, because the result is stored with the shape's mirror
215
293
  * flags cleared. Building it unmirrored handed back the unmirrored shape and
@@ -217,7 +295,7 @@ export function localSolidForShape(cad, shape) {
217
295
  */
218
296
  export function orientedSolidForShape(cad, shape) {
219
297
  if (!rotationIsRebuilt(shape))
220
- return mirroredStoredSolid(cad, localSolidForShape(cad, shape), shape);
298
+ return mirroredStoredSolid(cad, storedOrResizedSolid(cad, shape), shape);
221
299
  try {
222
300
  return exactSolidForShape(cad, shape);
223
301
  }
@@ -417,11 +495,15 @@ export function applyEdgeTreatment(shape, treatment) {
417
495
  }
418
496
  }
419
497
  catch (error) {
420
- // OCCT fails when the radius does not fit the geometry, which is the
421
- // common mistake: too large a radius for a thin wall or a short edge.
422
- throw new ServerCadError(treatment.kind === "fillet"
423
- ? "That radius is too large for this shape's edges. Try a smaller one."
424
- : "That chamfer is too large for this shape's edges. Try a smaller one.");
498
+ const message = error instanceof Error ? error.message : String(error);
499
+ const wasmFault = typeof WebAssembly !== "undefined"
500
+ && (error instanceof WebAssembly.Exception || error instanceof WebAssembly.RuntimeError);
501
+ if (wasmFault || /out of memory|memory access out of bounds|allocation failed|wasm.*abort/i.test(message)) {
502
+ throw error;
503
+ }
504
+ // Keep geometric sizing failures actionable without disguising runtime
505
+ // faults as a user mistake.
506
+ throw new ServerCadError(`The requested ${treatment.kind} amount ${treatment.amount} could not be applied to ${edges.length} eligible edges. Try a smaller amount.`);
425
507
  }
426
508
  // Both fillet and chamfer hand back a compound wrapping the result, and
427
509
  // OCCT refuses a compound as the input to another treatment -- so storing
@@ -524,6 +606,145 @@ function exactRevolveSolidForShape(cad, shape) {
524
606
  const { radialX, axial } = revolveResizeFactors(shape);
525
607
  return exactPlainSketchRevolveSolid(cad, shape.sketchProfile, normalizeSketchRevolveSettings(shape.sketchRevolve), undefined, { radial: radialX, axial });
526
608
  }
609
+ /**
610
+ * Where a stored revolve's axis sits in its mesh's natural frame, or null when
611
+ * that cannot be known without sweeping it again.
612
+ *
613
+ * The mesh is centred on its own box, so the axis is `-revolveAxisOffset`
614
+ * there (#503). A full turn saved before that offset was recorded is still
615
+ * symmetric about its axis under every one of its facet rotations, so the
616
+ * axis is the centroid of its triangles in X and Z -- which is not the box
617
+ * centre when the facet count is not a multiple of four. A partial sweep with
618
+ * no recorded axis has neither, and stays tessellated.
619
+ */
620
+ function storedRevolveAxis(shape, fullTurn) {
621
+ if (shape.revolveAxisOffset)
622
+ return { x: -shape.revolveAxisOffset.x, z: -shape.revolveAxisOffset.z };
623
+ if (!fullTurn)
624
+ return null;
625
+ const positions = shape.importedMesh?.positions ?? [];
626
+ let volume = 0;
627
+ let x = 0;
628
+ let z = 0;
629
+ for (let index = 0; index + 8 < positions.length; index += 9) {
630
+ const [ax, ay, az, bx, by, bz, cx, cy, cz] = positions.slice(index, index + 9);
631
+ const tetra = (ax * (by * cz - bz * cy) - ay * (bx * cz - bz * cx) + az * (bx * cy - by * cx)) / 6;
632
+ volume += tetra;
633
+ x += tetra * (ax + bx + cx) / 4;
634
+ z += tetra * (az + bz + cz) / 4;
635
+ }
636
+ return Math.abs(volume) > 1e-12 ? { x: x / volume, z: z / volume } : null;
637
+ }
638
+ /**
639
+ * Whether any profile segment is drawn as a curve.
640
+ *
641
+ * The rule `sketchProfileToRevolvePolygons` samples by: a segment that is not
642
+ * a line and has both its handles (the start point's outgoing one and the end
643
+ * point's incoming one) is a cubic, sampled into straight pieces; anything
644
+ * else is drawn straight. A revolve of the samples would be exact about the
645
+ * axis but still an approximation of the drawn curve, so it is not reported
646
+ * as exact.
647
+ */
648
+ function profileHasCurvedSegment(profile) {
649
+ const points = new Map(profile.points.map((point) => [point.id, point]));
650
+ return profile.segments.some((segment) => segment.kind !== "line"
651
+ && Boolean(points.get(segment.startId)?.handleOut) && Boolean(points.get(segment.endId)?.handleIn));
652
+ }
653
+ /**
654
+ * Why a revolve stored as a mesh is not measured, cut or exported as its exact
655
+ * solid, or null when it is.
656
+ *
657
+ * Wider than the edge listing's rule (`revolveWithoutExactEdges`): a partial
658
+ * sweep is exact here too, because nothing numbers its edges.
659
+ */
660
+ function revolveWithoutExactSolid(shape) {
661
+ if (!isUntreatedSketchRevolve(shape))
662
+ return "not an untreated sketch revolve";
663
+ if ((shape.importedMesh?.positions.length ?? 0) < 9)
664
+ return "no stored sweep to place the solid by";
665
+ const settings = normalizeSketchRevolveSettings(shape.sketchRevolve);
666
+ if (settings.pitch !== 0)
667
+ return "a helical revolve has only its tessellated sweep";
668
+ if (settings.coreRadius > 0)
669
+ return "a revolve fused to a core has no exact solid";
670
+ const { radialX, radialZ } = revolveResizeFactors(shape);
671
+ if (Math.abs(radialX - radialZ) > 1e-6 * Math.max(radialX, radialZ))
672
+ return "resized unevenly across its axis, so no revolve describes it";
673
+ if (shapeHasTaper(shape))
674
+ return "a taper is applied to the mesh, not to a revolve";
675
+ const fullTurn = Math.abs(Math.abs(settings.sweepAngle) - 360) <= 1e-6;
676
+ if (!storedRevolveAxis(shape, fullTurn))
677
+ return "a partial sweep saved without its axis";
678
+ if (profileHasCurvedSegment(shape.sketchProfile))
679
+ return "a curved profile edge is sampled into straight segments, so no exact solid describes it";
680
+ try {
681
+ if (cadSketchRegions(revolveSectionProfile(shape.sketchProfile, settings, { radial: 1, axial: 1 })).length !== 1) {
682
+ return "the profile has several closed regions";
683
+ }
684
+ }
685
+ catch {
686
+ return "the profile has no closed region";
687
+ }
688
+ return null;
689
+ }
690
+ /**
691
+ * Whether `placedSolidForShape` builds this shape as the exact solid of its
692
+ * revolve rather than sewing its stored mesh, which is what measuring it
693
+ * reports as its precision.
694
+ */
695
+ export function hasExactRevolveSolid(shape) {
696
+ return revolveWithoutExactSolid(shape) === null;
697
+ }
698
+ /**
699
+ * The angle a mesh of a result built from these shapes may turn through
700
+ * between facets, so that it is no coarser than the finest exact revolve
701
+ * among them, or undefined when none is one.
702
+ *
703
+ * Before a revolve was exact, a cut through it was made from its faceted
704
+ * sweep, so the stored result was exactly as fine as its `sides` asked. The
705
+ * exact result is meshed at a fixed chord height instead, which on a small
706
+ * ring is coarser than the sweep it replaced and ignores `sides` altogether.
707
+ * A group record is searched through, since it is cut as its members.
708
+ *
709
+ * Twice the sweep's facet angle, because OCCT's angular deflection bounds the
710
+ * turn between a facet's normal and the surface's, half a facet each way: at
711
+ * this value a full circle gets `sides` facets, the sweep's own count, and a
712
+ * 64-sided ring cut in half meshes to the 260 triangles its sweep gave.
713
+ */
714
+ export function exactRevolveAngularDeflection(shapes) {
715
+ let finest = 0;
716
+ const visit = (shape) => {
717
+ if (hasExactRevolveSolid(shape)) {
718
+ finest = Math.max(finest, normalizeSketchRevolveSettings(shape.sketchRevolve).sides);
719
+ }
720
+ else if (!shape.importedMesh && !shape.cadBrep) {
721
+ shape.groupedShapes?.forEach(visit);
722
+ }
723
+ };
724
+ shapes.forEach(visit);
725
+ return finest > 0 ? (4 * Math.PI) / finest : undefined;
726
+ }
727
+ /**
728
+ * The exact solid of a plain revolve, full turn or partial sweep, in the frame
729
+ * its stored mesh is drawn in -- centred on the mesh's box in X and Z, base at
730
+ * Y=0, at its drawn size -- or null when it has none (#523).
731
+ *
732
+ * That frame is what lets the caller place it exactly as the mesh is placed.
733
+ * The axis comes from the stored record rather than from the solid's own box,
734
+ * because a faceted partial sweep's box is not the exact sweep's box, and
735
+ * centring on the wrong one moves the solid off its axis.
736
+ */
737
+ export function exactRevolveLocalSolid(cad, shape) {
738
+ if (!hasExactRevolveSolid(shape))
739
+ return null;
740
+ const settings = normalizeSketchRevolveSettings(shape.sketchRevolve);
741
+ const axis = storedRevolveAxis(shape, Math.abs(Math.abs(settings.sweepAngle) - 360) <= 1e-6);
742
+ const { radialX, radialZ, axial } = revolveResizeFactors(shape);
743
+ const solid = revolvedSectionSolid(cad, shape.sketchProfile, settings, { radial: radialX, axial });
744
+ if (!cad.isValid(solid) || !cad.isSolid(solid))
745
+ throw new ServerCadError("The retained sketch revolve could not be rebuilt as exact CAD geometry.");
746
+ return cad.translate(solid, axis.x * radialX, 0, axis.z * radialZ);
747
+ }
527
748
  /**
528
749
  * The solid `GET /edges` numbers and a fillet or chamfer rounds.
529
750
  *
@@ -547,16 +768,16 @@ export function isPlainFullTurnSketchRevolve(shape) {
547
768
  && (settings.pitch ?? 0) === 0 && (settings.coreRadius ?? 0) === 0
548
769
  && Math.abs(Math.abs(settings.sweepAngle) - 360) <= 1e-6;
549
770
  }
550
- /** Build the analytical BREP for a plain, single-region full-turn revolve. */
551
- export function exactPlainSketchRevolveSolid(cad, sketchProfile, sketchRevolve, targetShape, sectionScale = { radial: 1, axial: 1 }) {
552
- if (!sketchRevolve || (sketchRevolve.pitch ?? 0) !== 0 || (sketchRevolve.coreRadius ?? 0) !== 0
553
- || Math.abs(Math.abs(sketchRevolve.sweepAngle) - 360) > 1e-6) {
554
- throw new ServerCadError("Exact revolve geometry requires a plain full-turn sketch revolve.");
555
- }
771
+ /**
772
+ * A revolve's section as a sketch profile in the OCCT build frame, at the given
773
+ * section scale: x is the distance from the axis and z runs up it from the
774
+ * revolve's base, the same polygons the mesh sweep turns.
775
+ */
776
+ function revolveSectionProfile(sketchProfile, sketchRevolve, sectionScale) {
556
777
  const polygons = sketchProfileToRevolvePolygons(sketchProfile, sketchRevolve);
557
778
  if (polygons.length === 0)
558
779
  throw new ServerCadError("The retained revolve profile has no closed region.");
559
- const profile = {
780
+ return {
560
781
  points: polygons.flatMap((polygon, polygonIndex) => polygon.map(([x, z], pointIndex) => ({
561
782
  id: `revolve-${polygonIndex}-point-${pointIndex}`,
562
783
  x: x * sectionScale.radial,
@@ -569,25 +790,41 @@ export function exactPlainSketchRevolveSolid(cad, sketchProfile, sketchRevolve,
569
790
  kind: "line",
570
791
  }))),
571
792
  };
793
+ }
794
+ /**
795
+ * The exact solid of a plain revolve's one section region, turned through its
796
+ * sweep about the Y axis, in the frame the mesh sweep builds before centring.
797
+ *
798
+ * Manifold sweeps from +X towards the editor's -Z and then turns the result by
799
+ * the start angle (or, for a negative sweep, by where the sweep ends), which
800
+ * is what a positive OCCT rotation about +Y does too, so the two agree for a
801
+ * partial sweep as well as for a full turn.
802
+ */
803
+ function revolvedSectionSolid(cad, sketchProfile, sketchRevolve, sectionScale) {
804
+ const profile = revolveSectionProfile(sketchProfile, sketchRevolve, sectionScale);
572
805
  const pathWire = (path) => cad.makeWire(path.steps.map(({ from, to }) => cad.makeLineEdge({ x: from.x, y: from.z, z: 0 }, { x: to.x, y: to.z, z: 0 })));
573
806
  const regions = cadSketchRegions(profile);
574
807
  if (regions.length !== 1) {
575
808
  throw new ServerCadError("Exact revolve geometry requires one closed profile region, and this revolve's profile has several.");
576
809
  }
577
- const solids = regions.map((region) => {
578
- let face = cad.makeFace(pathWire(region.outer));
579
- if (region.holes.length)
580
- face = cad.addHolesInFace(face, region.holes.map(pathWire));
581
- const solid = cad.revolve(face, {
582
- point: { x: 0, y: 0, z: 0 },
583
- direction: { x: 0, y: 1, z: 0 },
584
- }, Math.PI * 2);
585
- return Math.abs(sketchRevolve.startAngle) < 1e-8 ? solid : cad.rotate(solid, {
586
- point: { x: 0, y: 0, z: 0 },
587
- direction: { x: 0, y: 1, z: 0 },
588
- }, (sketchRevolve.startAngle * Math.PI) / 180);
589
- });
590
- let solid = solids[0];
810
+ const axis = { point: { x: 0, y: 0, z: 0 }, direction: { x: 0, y: 1, z: 0 } };
811
+ const region = regions[0];
812
+ let face = cad.makeFace(pathWire(region.outer));
813
+ if (region.holes.length)
814
+ face = cad.addHolesInFace(face, region.holes.map(pathWire));
815
+ const sweep = Math.abs(sketchRevolve.sweepAngle);
816
+ const solid = cad.revolve(face, axis, Math.abs(sweep - 360) <= 1e-6 ? Math.PI * 2 : (sweep * Math.PI) / 180);
817
+ const rotation = sketchRevolve.sweepAngle < 0 ? sketchRevolve.startAngle + sketchRevolve.sweepAngle : sketchRevolve.startAngle;
818
+ return Math.abs(rotation) < 1e-8 ? solid : cad.rotate(solid, axis, (rotation * Math.PI) / 180);
819
+ }
820
+ /** Build the analytical BREP for a plain, single-region full-turn revolve. */
821
+ export function exactPlainSketchRevolveSolid(cad, sketchProfile, sketchRevolve, targetShape, sectionScale = { radial: 1, axial: 1 }) {
822
+ if (!sketchRevolve || (sketchRevolve.pitch ?? 0) !== 0 || (sketchRevolve.coreRadius ?? 0) !== 0
823
+ || Math.abs(Math.abs(sketchRevolve.sweepAngle) - 360) > 1e-6) {
824
+ throw new ServerCadError("Exact revolve geometry requires a plain full-turn sketch revolve.");
825
+ }
826
+ // A full turn starts where its start angle says, whichever way it is swept.
827
+ let solid = revolvedSectionSolid(cad, sketchProfile, { ...sketchRevolve, sweepAngle: 360 }, sectionScale);
591
828
  // Callers that cannot express a resized revolve analytically (for example,
592
829
  // exact face listing of a radially elliptical legacy shape) use this bounds
593
830
  // fit. Face drafting of a supported resize passes profile scales instead.
@@ -639,18 +876,13 @@ export function applyDraftPrism(shape, request) {
639
876
  }
640
877
  const scale = sketchResizeFactors(shape);
641
878
  const profile = scaledSketchProfile(shape.sketchProfile, scale.width, scale.depth);
642
- const { points, segments } = profile;
643
- if (points.length < 3 || segments.length !== points.length
644
- || segments.some((segment, index) => segment.kind && segment.kind !== "line"
645
- || segment.startId !== points[index].id
646
- || segment.endId !== points[(index + 1) % points.length].id)
647
- || points.some((point) => point.handleIn || point.handleOut || !Number.isFinite(point.x) || !Number.isFinite(point.z))) {
648
- return Promise.reject(new ServerCadError("Draft prism currently requires one closed polygonal sketch profile made from straight segments.", 400));
649
- }
879
+ const loop = draftPrismLoop(profile);
880
+ if (typeof loop === "string")
881
+ return Promise.reject(new ServerCadError(loop, 400));
650
882
  return withCadKernel((cad) => {
651
883
  const { x: centerX, z: centerZ } = draftPrismProfileOffset(profile);
652
- const edges = points.map((point, index) => {
653
- const next = points[(index + 1) % points.length];
884
+ const edges = loop.map((point, index) => {
885
+ const next = loop[(index + 1) % loop.length];
654
886
  return cad.makeLineEdge({ x: point.x - centerX, y: 0, z: point.z - centerZ }, { x: next.x - centerX, y: 0, z: next.z - centerZ });
655
887
  });
656
888
  const face = cad.makeFace(cad.makeWire(edges));
@@ -703,8 +935,7 @@ export function applySketchFaceDraft(shape, request) {
703
935
  solid = solidFromBrep(cad, shape.cadBrep);
704
936
  }
705
937
  const resize = sketchResizeFactors(shape);
706
- if (!exactRevolve && shape.sketchOperation === "extrude"
707
- && [resize.width, resize.depth, resize.height].some((factor) => Math.abs(factor - 1) > 1e-8)) {
938
+ if (!exactRevolve && shape.sketchOperation === "extrude" && isResized(resize)) {
708
939
  solid = sketchExtrusionSolid(cad, scaledSketchProfile(shape.sketchProfile, resize.width, resize.depth), shape.height);
709
940
  if (!cadResultIsValid(cad, solid) || !cad.isSolid(solid)) {
710
941
  throw new ServerCadError("The resized sketch extrusion could not be rebuilt as exact CAD geometry.");
@@ -712,7 +943,7 @@ export function applySketchFaceDraft(shape, request) {
712
943
  }
713
944
  const faces = cad.getSubShapes(solid, "face");
714
945
  const ids = shape.faceIds?.length === faces.length ? shape.faceIds
715
- : exactRevolve && !shape.faceColors ? faces.map((_, index) => `face_${encodeURIComponent(shape.id)}_${index}`)
946
+ : exactRevolve ? faces.map((_, index) => `face_${encodeURIComponent(shape.id)}_${index}`)
716
947
  : undefined;
717
948
  if (!ids || ids.length !== faces.length || new Set(ids).size !== faces.length) {
718
949
  throw new ServerCadError("Face drafting requires a complete, unique stable face ID list.");
@@ -733,8 +964,10 @@ export function applySketchFaceDraft(shape, request) {
733
964
  });
734
965
  }
735
966
  /** The exact geometry and a mesh of it, which is what a shape stores after any CAD operation. */
736
- export function meshedCadResult(cad, result, sourceShape, lineage, sourceSolid) {
737
- const mesh = cad.meshShape(result, { linearDeflection: MESH_DEFLECTION });
967
+ export function meshedCadResult(cad, result, sourceShape, lineage, sourceSolid, angularDeflection) {
968
+ const mesh = cad.meshShape(result, angularDeflection
969
+ ? { linearDeflection: MESH_DEFLECTION, angularDeflection }
970
+ : { linearDeflection: MESH_DEFLECTION });
738
971
  // The editor stores a flat triangle list, not an indexed mesh, so the
739
972
  // indices are expanded here rather than carried.
740
973
  const positions = [];
@@ -867,7 +1100,8 @@ export function shapeWithCadResult(shape, result) {
867
1100
  // was created -- which is where a test shape sits -- subtracts zero.
868
1101
  //
869
1102
  // These are the result's own bounds, which is what the editor's worker
870
- // records for exactly the same reason.
1103
+ // records for exactly the same reason, and what both cut writers now
1104
+ // record for a cut's recentred B-rep (#548).
871
1105
  cadBrepFrame: { x: centerX, z: centerZ, elevation: minY, width, depth, height },
872
1106
  cadPrimitiveFrame: undefined,
873
1107
  cadDisplayEdges: undefined,