@snaptrude/plugin-core 0.9.3 → 0.9.5

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snaptrude/plugin-core",
3
- "version": "0.9.3",
3
+ "version": "0.9.5",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -16,17 +16,6 @@
16
16
  "publishConfig": {
17
17
  "access": "public"
18
18
  },
19
- "devDependencies": {
20
- "ts-morph": "^28.0.0",
21
- "tsup": "^8.5.1",
22
- "typescript": "^5.5.4"
23
- },
24
- "dependencies": {
25
- "zod": "^4.3.6"
26
- },
27
- "peerDependencies": {
28
- "zod": "^3.25.0 || ^4.0.0"
29
- },
30
19
  "scripts": {
31
20
  "check-types": "tsc --noEmit",
32
21
  "build": "tsup --clean",
@@ -37,5 +26,16 @@
37
26
  "generate:manifest:check": "node scripts/generate-manifest.mjs --check",
38
27
  "generate:manifest:all": "node scripts/generate-manifest.mjs --all",
39
28
  "generate:manifest:all:check": "node scripts/generate-manifest.mjs --all --check"
29
+ },
30
+ "devDependencies": {
31
+ "ts-morph": "^28.0.0",
32
+ "tsup": "^8.5.1",
33
+ "typescript": "^5.5.4"
34
+ },
35
+ "dependencies": {
36
+ "zod": "^4.3.6"
37
+ },
38
+ "peerDependencies": {
39
+ "zod": "^3.25.0 || ^4.0.0"
40
40
  }
41
- }
41
+ }
@@ -264,28 +264,52 @@ export abstract class PluginGeomCreateApi {
264
264
  /**
265
265
  * Create a closed solid **B-rep** by lofting between a bottom and a top
266
266
  * contour, optionally through intermediate cross-sections. Host API call —
267
- * returns a {@linkcode BrepHandle}. All sections must have the same number
268
- * of edges; side faces connect corresponding edges by authored index.
269
- * Corresponding edges must stay coplanar segment to segment (matching hole
270
- * counts allowed in the two-section form) a twisted loft would produce
271
- * non-planar side faces and is rejected. With intermediates the loft is a
272
- * chain of ruled segments folded into one solid: sections must be planar
273
- * and hole-free, ordered bottom intermediates → top. The contours are
274
- * copied the input handles are never mutated.
267
+ * returns a {@linkcode BrepHandle}. Sections with different edge counts are
268
+ * auto-matched: vertices are deterministically inserted on the smaller
269
+ * sections at the larger section's perimeter positions, so the same input
270
+ * always yields the same solid. For straight-edge (all-line) sections the
271
+ * seam is auto-aligned too winding and start-vertex correspondence are
272
+ * chosen to avoid a twist, so outlines authored from any starting corner
273
+ * loft cleanly; arc-bearing sections keep the authored correspondence
274
+ * unchanged. A pair whose side faces would be non-planar under every
275
+ * correspondence still builds for hole-free straight-edged pairs — planar
276
+ * side quads stay quads and warped ones split into planar triangles (the
277
+ * triangulated lane); arc-bearing or holed twisted pairs are rejected, as is
278
+ * auto-matching that would pair an arc with a straight edge. Matching hole
279
+ * counts are allowed in the two-section form. With intermediates the loft
280
+ * is a chain of ruled segments folded into one solid: sections must be
281
+ * planar and hole-free, ordered bottom → intermediates → top. The contours
282
+ * are copied — the input handles are never mutated.
275
283
  *
276
284
  * Inspect the result via `core.geom.query.brep.*`, or commit it to the scene
277
285
  * with `design.create.massFromBrep`.
278
286
  *
279
287
  * @param bottomContour The bottom cross-section
280
- * @param topContour The top cross-section (same edge count as the bottom)
288
+ * @param topContour The top cross-section (edge count may differ from the
289
+ * bottom — sections are auto-matched)
281
290
  * @param intermediateContours Optional in-between cross-sections, ordered
282
- * bottom to top (hole-free, same edge count as the bottom and top)
291
+ * bottom to top (hole-free; edge counts may differ every section is
292
+ * normalized to the largest)
293
+ * @param options Optional behavior switches. `compatibility` — `"auto"`
294
+ * (default) inserts vertices to match differing edge counts; `"strict"`
295
+ * skips normalization and throws VALIDATION when outer edge counts (or,
296
+ * in the two-section form, hole-pair counts) differ. `seamAlignment` —
297
+ * `"auto"` (default) searches seam rotations on straight-edge sections to
298
+ * avoid a twist; `"authored"` keeps the authored start-vertex
299
+ * correspondence unchanged (a twisted correspondence then builds through
300
+ * the triangulated lane for hole-free straight-edged pairs).
301
+ * `{ compatibility: "strict", seamAlignment: "authored" }` reproduces
302
+ * pre-0.9.4 lofting exactly.
283
303
  * @returns The new solid as a {@linkcode BrepHandle}
284
- * @throws VALIDATION if the contours are coincident, their edge or hole
285
- * counts differ, any section has holes when intermediates are present
304
+ * @throws VALIDATION if the contours are coincident, their hole counts
305
+ * differ, any section has holes when intermediates are present
286
306
  * (multi-section lofts take hole-free sections), a section is non-planar,
287
- * or a side face between corresponding edges would be non-planar (a
288
- * twisted section pairthe error names the offending sections)
307
+ * sections are too dissimilar to auto-match, auto-matching would pair an
308
+ * arc with a straight edge, or for arc-bearing or holed pairs only — no vertex correspondence avoids a twist (a
309
+ * side face between corresponding edges would be non-planar — the error
310
+ * names the offending sections); with `compatibility: "strict"`, if
311
+ * section edge counts (or hole-pair counts) differ — the error names the
312
+ * sections and counts and points at `compatibility: "auto"`
289
313
  * @throws OPERATION_FAILED if the lofted segments cannot be joined into a
290
314
  * valid solid
291
315
  *
@@ -293,6 +317,10 @@ export abstract class PluginGeomCreateApi {
293
317
  * @examplePrompt Loft between a large base and a smaller top
294
318
  * @examplePrompt Create a frustum from two squares
295
319
  * @examplePrompt Loft the tower through these three floor outlines
320
+ * @examplePrompt Loft a four-sided base to a five-sided top
321
+ * @examplePrompt Loft these outlines even though their corners don't line up
322
+ * @examplePrompt Loft these outlines exactly as authored, corner to corner
323
+ * @examplePrompt Fail the loft if my sections don't have matching edge counts
296
324
  *
297
325
  * # Example
298
326
  * ```ts
@@ -310,6 +338,7 @@ export abstract class PluginGeomCreateApi {
310
338
  bottomContour: ContourHandle,
311
339
  topContour: ContourHandle,
312
340
  intermediateContours?: ContourHandle[],
341
+ options?: { compatibility?: "strict" | "auto"; seamAlignment?: "authored" | "auto" },
313
342
  ): PluginApiReturn<BrepHandle>
314
343
 
315
344
  /**
@@ -636,24 +665,59 @@ export abstract class PluginGeomCreateApi {
636
665
  ): PluginApiReturn<BrepHandle>
637
666
 
638
667
  /**
639
- * Sweeps a planar profile along an open polyline path, with mitred corners
640
- * at each bend. Host API call — returns a new {@linkcode BrepHandle}; the
668
+ * Sweeps a planar profile along a polyline path, with mitred corners at
669
+ * each bend. Host API call — returns a new {@linkcode BrepHandle}; the
641
670
  * input contour is read-only. The profile must be hole-free and must not
642
- * lie in a plane containing the first path segment's direction.
643
- *
644
- * Runs on the OpenCascade kernel (the first kernel call loads a wasm of
645
- * tens of MBexpect a pause of seconds).
671
+ * lie in a plane containing the first path segment's direction. Repeating
672
+ * the first point as the last closes the path into a ring solid (a picture
673
+ * frame): closed paths must be flat and straight-segmented and take a
674
+ * straight-edged (arc-free) profilethe ring's cross-section is the
675
+ * authored profile projected along the first leg onto the first mitre
676
+ * plane, so a profile not perpendicular to the first leg appears sheared.
677
+ * An optional scale law tapers the sweep: `startScale`/`endScale`
678
+ * interpolate linearly by arc length, or `scales` gives one factor per path
679
+ * point (scaling is about the profile centroid). A uniform scale (all
680
+ * factors equal) works on any path today's sweep accepts; a varying scale
681
+ * takes straight (collinear) open paths with straight-edged profiles —
682
+ * tapering across a bend or along an arc edge would create faces Snaptrude
683
+ * cannot represent and throws (sweep each straight run separately and union
684
+ * the results instead). An optional `transition` picks the corner
685
+ * treatment: `"miter"` (default) is today's sharp mitred bend;
686
+ * `{ bevel: b }` chamfers each turning corner by cutting `b` back along
687
+ * both adjacent legs (works on open and closed paths — each leg must be
688
+ * long enough for its cuts). On a corner-less path `transition` has no effect, and the
689
+ * varying-scale lane takes straight paths, so it never sees a corner.
690
+ *
691
+ * Open paths run on the OpenCascade kernel (the first kernel call loads a
692
+ * wasm of tens of MB — expect a pause of seconds); closed paths are
693
+ * computed host-side and never load the kernel.
646
694
  *
647
695
  * Inspect the result via `core.geom.query.brep.*`, or commit it to the scene
648
696
  * with `design.create.massFromBrep`.
649
697
  *
650
698
  * @param profile The cross-section to sweep (hole-free contour)
651
- * @param path The polyline path as plain `{x, y, z}` points (≥2, open — first ≠ last)
699
+ * @param path The polyline path as plain `{x, y, z}` points (≥2; repeat the
700
+ * first point as the last to close the path into a ring)
701
+ * @param options Optional scale law and corner transition. Scale law:
702
+ * `startScale`/`endScale` (linear taper by arc length) or `scales` (one
703
+ * factor per path point; mutually exclusive with
704
+ * `startScale`/`endScale`); every factor must be finite and within
705
+ * 1e-3..1e3. `transition`: `"miter"` (default, sharp corners) or
706
+ * `{ bevel: b }` (chamfered corners, `b` finite within 1e-3..1e3; open
707
+ * and closed paths)
652
708
  * @returns The swept solid as a new {@linkcode BrepHandle}
653
709
  * @throws VALIDATION if the profile has holes, the path has fewer than 2
654
- * points, consecutive path points coincide, the path is closed (open
655
- * paths only in v1), a coordinate is not finite, or the profile plane
656
- * contains the first path segment's direction (degenerate sweep)
710
+ * points, consecutive path points coincide, a coordinate is not finite,
711
+ * or the profile plane contains the first path segment's direction
712
+ * (degenerate sweep); for closed paths, if the ring has fewer than 3
713
+ * distinct corners, the path is non-planar, a corner doubles back, the
714
+ * profile has an arc edge, or the profile is too large for a mitred
715
+ * corner; for scale options, if a factor is outside 1e-3..1e3, `scales`
716
+ * is combined with `startScale`/`endScale`, `scales` does not match the
717
+ * path length, or a varying scale is used on a bent or closed path or
718
+ * with an arc-edged profile; for `{ bevel }`, if a leg is too short for
719
+ * the cuts its beveled corners take from it or a corner nearly doubles
720
+ * back so its chamfer collapses (the errors name the corner)
657
721
  * @throws OPERATION_FAILED if the kernel cannot sweep the profile into a
658
722
  * valid solid or a transition patch is a curved surface Snaptrude cannot
659
723
  * represent
@@ -661,6 +725,10 @@ export abstract class PluginGeomCreateApi {
661
725
  * @examplePrompt Sweep this profile along the corridor path
662
726
  * @examplePrompt Extrude the railing section along this route
663
727
  * @examplePrompt Run a duct profile through these points
728
+ * @examplePrompt Sweep a frame profile around a closed rectangular loop
729
+ * @examplePrompt Taper this duct from 1x1 to 2x2 along its run
730
+ * @examplePrompt Sweep a column section that narrows toward the top
731
+ * @examplePrompt Chamfer the corners of this swept frame by 200mm
664
732
  *
665
733
  * # Example
666
734
  * ```ts
@@ -673,11 +741,34 @@ export abstract class PluginGeomCreateApi {
673
741
  * { x: 0, y: 2.8, z: 0 },
674
742
  * { x: 10, y: 2.8, z: 0 },
675
743
  * ])
744
+ * // A closed ring (picture frame): repeat the first point as the last
745
+ * const frame = await snaptrude.core.geom.create.brepFromSweep(profile, [
746
+ * { x: 0, y: 0, z: 0 },
747
+ * { x: 0, y: 3, z: 0 },
748
+ * { x: 4, y: 3, z: 0 },
749
+ * { x: 4, y: 0, z: 0 },
750
+ * { x: 0, y: 0, z: 0 },
751
+ * ])
752
+ * // A tapered straight run: 0.4x0.4 at the start, 0.8x0.8 at the end
753
+ * const flue = await snaptrude.core.geom.create.brepFromSweep(
754
+ * profile,
755
+ * [
756
+ * { x: 0, y: 0, z: 0 },
757
+ * { x: 0, y: 10, z: 0 },
758
+ * ],
759
+ * { startScale: 1, endScale: 2 },
760
+ * )
676
761
  * ```
677
762
  */
678
763
  public abstract brepFromSweep(
679
764
  profile: ContourHandle,
680
765
  path: Vec3Components[],
766
+ options?: {
767
+ startScale?: number
768
+ endScale?: number
769
+ scales?: number[]
770
+ transition?: "miter" | { bevel: number }
771
+ },
681
772
  ): PluginApiReturn<BrepHandle>
682
773
 
683
774
  /**
@@ -897,11 +988,18 @@ export type PluginGeomCreateBrepFromExtrusionArgs = z.infer<
897
988
  * | `bottomContour` | {@linkcode ContourHandle} | The bottom cross-section |
898
989
  * | `topContour` | {@linkcode ContourHandle} | The top cross-section |
899
990
  * | `intermediateContours` | {@linkcode ContourHandle}`[]`? | Optional in-between cross-sections, ordered bottom to top |
991
+ * | `options` | `object`? | Optional behavior switches: `compatibility` (`"strict"` throws on mismatched edge counts instead of auto-matching; default `"auto"`) and `seamAlignment` (`"authored"` keeps the authored correspondence instead of searching seam rotations; default `"auto"`) |
900
992
  */
901
993
  export const PluginGeomCreateBrepFromLoftArgs = z.object({
902
994
  bottomContour: ContourHandle,
903
995
  topContour: ContourHandle,
904
996
  intermediateContours: z.array(ContourHandle).optional(),
997
+ options: z
998
+ .object({
999
+ compatibility: z.enum(["strict", "auto"]).optional(),
1000
+ seamAlignment: z.enum(["authored", "auto"]).optional(),
1001
+ })
1002
+ .optional(),
905
1003
  })
906
1004
 
907
1005
  export type PluginGeomCreateBrepFromLoftArgs = z.infer<typeof PluginGeomCreateBrepFromLoftArgs>
@@ -1034,18 +1132,53 @@ export type PluginGeomCreateBrepFromChamferArgs = z.infer<
1034
1132
  typeof PluginGeomCreateBrepFromChamferArgs
1035
1133
  >
1036
1134
 
1135
+ // Taper scale factor — bounded so a near-zero scale cannot pinch a section
1136
+ // into a degenerate sliver the downstream planarity gates would miss.
1137
+ const SweepScaleFactor = z.number().finite().min(1e-3).max(1e3)
1138
+
1037
1139
  /**
1038
1140
  * Arguments for {@linkcode PluginGeomCreateApi.brepFromSweep}.
1039
1141
  *
1040
1142
  * | Property | Type | Description |
1041
1143
  * |---|---|---|
1042
1144
  * | `profile` | {@linkcode ContourHandle} | The cross-section to sweep (hole-free) |
1043
- * | `path` | {@linkcode Vec3Components}`[]` | The open polyline path (≥2 points, finite components) |
1145
+ * | `path` | {@linkcode Vec3Components}`[]` | The polyline path (≥2 points, finite components; repeating the first point as the last closes it into a ring — closed paths must be flat and straight-segmented and take a straight-edged profile) |
1146
+ * | `options` | `object`? | Optional scale law: `startScale`/`endScale` (linear taper by arc length) or per-point `scales` (mutually exclusive; factors finite, 1e-3..1e3; `scales.length` must equal `path.length`). Optional corner `transition`: `"miter"` (default) or `{ bevel }` (chamfered corners; finite, 1e-3..1e3) |
1044
1147
  */
1045
- export const PluginGeomCreateBrepFromSweepArgs = z.object({
1046
- profile: ContourHandle,
1047
- path: z.array(FiniteVec3Components).min(2),
1048
- })
1148
+ export const PluginGeomCreateBrepFromSweepArgs = z
1149
+ .object({
1150
+ profile: ContourHandle,
1151
+ path: z.array(FiniteVec3Components).min(2),
1152
+ options: z
1153
+ .object({
1154
+ startScale: SweepScaleFactor.optional(),
1155
+ endScale: SweepScaleFactor.optional(),
1156
+ scales: z.array(SweepScaleFactor).optional(),
1157
+ transition: z
1158
+ .union([
1159
+ z.literal("miter"),
1160
+ z.object({ bevel: z.number().finite().min(1e-3).max(1e3) }),
1161
+ ])
1162
+ .optional(),
1163
+ })
1164
+ .optional(),
1165
+ })
1166
+ .superRefine((a, ctx) => {
1167
+ const options = a.options
1168
+ if (!options || options.scales === undefined) return
1169
+ if (options.startScale !== undefined || options.endScale !== undefined) {
1170
+ ctx.addIssue({
1171
+ code: z.ZodIssueCode.custom,
1172
+ message: "scales is mutually exclusive with startScale/endScale",
1173
+ })
1174
+ }
1175
+ if (options.scales.length !== a.path.length) {
1176
+ ctx.addIssue({
1177
+ code: z.ZodIssueCode.custom,
1178
+ message: "scales must have exactly one entry per path point",
1179
+ })
1180
+ }
1181
+ })
1049
1182
 
1050
1183
  export type PluginGeomCreateBrepFromSweepArgs = z.infer<typeof PluginGeomCreateBrepFromSweepArgs>
1051
1184
 
@@ -163,6 +163,64 @@ export abstract class PluginGeomQueryBrepApi {
163
163
  vertexIndexB: number
164
164
  ): PluginApiReturn<EdgeHandle[]>
165
165
 
166
+ /**
167
+ * Read the exact curve of a brep edge as plain values — a discriminated
168
+ * union on `type`. Pure read: no geometry kernel is loaded. Points and
169
+ * directions are {@linkcode Vec3Components}; lengths and radii are raw
170
+ * Babylon units.
171
+ *
172
+ * | `type` | Fields |
173
+ * |---|---|
174
+ * | `"line"` | `startPoint`, `endPoint`, `length` |
175
+ * | `"arc"` | `startPoint`, `endPoint`, `centre`, `axis`, `radius`, `length` |
176
+ * | `"circle"` | `centre`, `axis`, `radius`, `length` — a full-circle edge (e.g. a cylinder cap rim) |
177
+ *
178
+ * @param brep The brep the edge belongs to
179
+ * @param edge The edge whose curve to read
180
+ * @returns The curve data as a discriminated union on `type` (see table above)
181
+ * @throws VALIDATION if the edge is not on `brep`
182
+ *
183
+ * @examplePrompt Read the radius of this curved edge
184
+ * @examplePrompt Is this edge straight or an arc?
185
+ * @examplePrompt Get the exact start and end points of this edge
186
+ * @examplePrompt Measure the length of each edge of this mass
187
+ *
188
+ * # Example
189
+ * ```ts
190
+ * const [mass] = await snaptrude.design.query.listMasses()
191
+ * const brep = await snaptrude.design.query.geometry.getBrep(mass)
192
+ * if (brep) {
193
+ * const edges = await snaptrude.core.geom.query.brep.listEdges(brep)
194
+ * for (const edge of edges) {
195
+ * const curve = await snaptrude.core.geom.query.brep.getEdgeCurve(brep, edge)
196
+ * if (curve.type === "arc") console.log("arc radius:", curve.radius)
197
+ * }
198
+ * }
199
+ * ```
200
+ */
201
+ public abstract getEdgeCurve(
202
+ brep: BrepHandle,
203
+ edge: EdgeHandle
204
+ ): PluginApiReturn<
205
+ | { type: "line"; startPoint: Vec3Components; endPoint: Vec3Components; length: number }
206
+ | {
207
+ type: "arc"
208
+ startPoint: Vec3Components
209
+ endPoint: Vec3Components
210
+ centre: Vec3Components
211
+ axis: Vec3Components
212
+ radius: number
213
+ length: number
214
+ }
215
+ | {
216
+ type: "circle"
217
+ centre: Vec3Components
218
+ axis: Vec3Components
219
+ radius: number
220
+ length: number
221
+ }
222
+ >
223
+
166
224
  /**
167
225
  * Get the position of a vertex within a brep.
168
226
  * @param brep The brep to query
@@ -408,6 +466,22 @@ export type PluginGeomQueryBrepListEdgesBetweenArgs = z.infer<
408
466
  typeof PluginGeomQueryBrepListEdgesBetweenArgs
409
467
  >
410
468
 
469
+ /**
470
+ * Arguments for {@linkcode PluginGeomQueryBrepApi.getEdgeCurve}.
471
+ *
472
+ * | Property | Type | Description |
473
+ * |---|---|---|
474
+ * | `brep` | {@linkcode BrepHandle} | The brep the edge belongs to |
475
+ * | `edge` | {@linkcode EdgeHandle} | The edge whose curve to read |
476
+ */
477
+ export const PluginGeomQueryBrepGetEdgeCurveArgs = z.object({
478
+ brep: BrepHandle,
479
+ edge: EdgeHandle,
480
+ })
481
+ export type PluginGeomQueryBrepGetEdgeCurveArgs = z.infer<
482
+ typeof PluginGeomQueryBrepGetEdgeCurveArgs
483
+ >
484
+
411
485
  /**
412
486
  * Arguments for {@linkcode PluginGeomQueryBrepApi.getVertexPosition}.
413
487
  *