@snaptrude/plugin-core 0.9.4 → 0.9.6

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.4",
3
+ "version": "0.9.6",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -62,6 +62,23 @@ export abstract class PluginCameraApi {
62
62
  view: PluginStandardView,
63
63
  ): PluginApiReturn<boolean>
64
64
 
65
+ /**
66
+ * Get the current camera mode. Paired with {@linkcode PluginCameraApi.setMode}.
67
+ *
68
+ * @returns `"2d"` when the editor is in plan view, `"3d"` otherwise.
69
+ *
70
+ * @examplePrompt Am I in 2D or 3D?
71
+ * @examplePrompt What view mode is the editor in?
72
+ *
73
+ * # Example
74
+ * ```ts
75
+ * if ((await snaptrude.core.camera.getMode()) === "2d") {
76
+ * await snaptrude.core.camera.setMode("3d")
77
+ * }
78
+ * ```
79
+ */
80
+ public abstract getMode(): PluginApiReturn<PluginCameraMode>
81
+
65
82
  /**
66
83
  * Toggle the modelling mode between `2d` (plan) and `3d`. Mirrors the canvas
67
84
  * 2D/3D toggle: `3d` enters the isometric perspective view, `2d` drops to the
@@ -1,6 +1,10 @@
1
1
  import * as z from "zod"
2
2
  import { PluginApiReturn } from "../../../../types"
3
- import { UnderlayHandle, BBoxComponents } from "../../../../handles"
3
+ import {
4
+ UnderlayHandle,
5
+ BBoxComponents,
6
+ Vec3Components,
7
+ } from "../../../../handles"
4
8
 
5
9
  /**
6
10
  * Underlays — inspect and edit the placed reference planes imported into the
@@ -65,7 +69,65 @@ export abstract class PluginCoreIoUnderlayApi {
65
69
  * if (bb) console.log(`width ${bb.max.x - bb.min.x}`)
66
70
  * ```
67
71
  */
68
- public abstract getBounds(underlay: UnderlayHandle): PluginApiReturn<BBoxComponents | null>
72
+ public abstract getBounds(
73
+ underlay: UnderlayHandle,
74
+ ): PluginApiReturn<BBoxComponents | null>
75
+
76
+ /**
77
+ * List the distinct original AutoCAD layer names retained by a placed CAD
78
+ * underlay. Returns `[]` for non-CAD underlays, legacy imports created before
79
+ * source-layer retention, missing underlays, or CAD drawings without layer tags.
80
+ *
81
+ * @param underlay - The placed CAD underlay to inspect.
82
+ * @returns the retained AutoCAD layer names in first-appearance order.
83
+ *
84
+ * @examplePrompt List the original AutoCAD layers in this imported drawing
85
+ * @examplePrompt Which CAD layers are available in this underlay?
86
+ *
87
+ * # Example
88
+ * ```ts
89
+ * const layers = await snaptrude.core.io.underlay.listCadLayers(cad)
90
+ * console.log(layers)
91
+ * ```
92
+ */
93
+ public abstract listCadLayers(
94
+ underlay: UnderlayHandle,
95
+ ): PluginApiReturn<string[]>
96
+
97
+ /**
98
+ * Read one original AutoCAD layer's retained line and arc geometry in
99
+ * **Snaptrude world space** and Snaptrude internal units. Results are paged so
100
+ * large drawings can be inspected without returning the whole DWG at once.
101
+ *
102
+ * The world-space coordinates include the CAD underlay's current position,
103
+ * rotation, scale, and storey elevation. `null` means the underlay no longer
104
+ * resolves. An unknown layer returns a page with `total: 0`.
105
+ *
106
+ * @param underlay - The placed CAD underlay to inspect.
107
+ * @param layer - Exact, case-sensitive original AutoCAD layer name.
108
+ * @param options - Optional zero-based offset and page limit (default 500,
109
+ * maximum 1000).
110
+ * @returns a page of retained CAD curves, or `null` if the underlay is gone.
111
+ * @throws if the handle resolves to an image/PDF rather than a CAD underlay.
112
+ *
113
+ * @examplePrompt Read the wall geometry from the A-WALL layer in this CAD underlay
114
+ * @examplePrompt Get the next 500 curves from the structural CAD layer
115
+ *
116
+ * # Example
117
+ * ```ts
118
+ * const page = await snaptrude.core.io.underlay.getCadLayerGeometry(
119
+ * cad,
120
+ * "A-WALL",
121
+ * { offset: 0, limit: 500 },
122
+ * )
123
+ * if (page) console.log(`${page.curves.length} of ${page.total} curves`)
124
+ * ```
125
+ */
126
+ public abstract getCadLayerGeometry(
127
+ underlay: UnderlayHandle,
128
+ layer: string,
129
+ options?: PluginCadLayerGeometryOptions,
130
+ ): PluginApiReturn<PluginCadLayerGeometryPage | null>
69
131
 
70
132
  /**
71
133
  * Read an underlay's scale. Works for **image and PDF** underlays. Returns
@@ -158,7 +220,9 @@ export abstract class PluginCoreIoUnderlayApi {
158
220
  * await snaptrude.core.io.underlay.resetScale(plan)
159
221
  * ```
160
222
  */
161
- public abstract resetScale(underlay: UnderlayHandle): PluginApiReturn<{ scaleFactor: number }>
223
+ public abstract resetScale(
224
+ underlay: UnderlayHandle,
225
+ ): PluginApiReturn<{ scaleFactor: number }>
162
226
 
163
227
  /**
164
228
  * Read an underlay's opacity (`0`..`1`), or `null` if it has no material.
@@ -175,7 +239,9 @@ export abstract class PluginCoreIoUnderlayApi {
175
239
  * const o = await snaptrude.core.io.underlay.getOpacity(plan)
176
240
  * ```
177
241
  */
178
- public abstract getOpacity(underlay: UnderlayHandle): PluginApiReturn<number | null>
242
+ public abstract getOpacity(
243
+ underlay: UnderlayHandle,
244
+ ): PluginApiReturn<number | null>
179
245
 
180
246
  /**
181
247
  * Set an underlay's opacity (`0` = transparent .. `1` = opaque). Undoable.
@@ -197,7 +263,10 @@ export abstract class PluginCoreIoUnderlayApi {
197
263
  * await snaptrude.core.io.underlay.setOpacity(plan, 0.3)
198
264
  * ```
199
265
  */
200
- public abstract setOpacity(underlay: UnderlayHandle, opacity: number): PluginApiReturn<void>
266
+ public abstract setOpacity(
267
+ underlay: UnderlayHandle,
268
+ opacity: number,
269
+ ): PluginApiReturn<void>
201
270
 
202
271
  /**
203
272
  * Delete an underlay from the scene (and its backend record). Applies to image,
@@ -218,6 +287,106 @@ export abstract class PluginCoreIoUnderlayApi {
218
287
  * ```
219
288
  */
220
289
  public abstract delete(underlay: UnderlayHandle): PluginApiReturn<void>
290
+
291
+ /**
292
+ * Extract **continuous wall centrelines** from one retained AutoCAD layer's
293
+ * double-line wall faces — the deterministic tracing primitive behind
294
+ * CAD-to-BIM wall conversion. Instead of paging raw curves and re-deriving
295
+ * the geometry in worker code, this reads the whole layer and runs the vetted
296
+ * pipeline: cull fragments, dedupe, merge collinear runs into *faces*, pair
297
+ * parallel faces a wall-thickness apart into centrelines (midline = axis,
298
+ * separation = thickness), bridge collinear gaps up to
299
+ * `bridgeOpenings.maxWidth` into ONE continuous centreline while recording
300
+ * each bridged span as an opening, then snap near-touching endpoints at
301
+ * junctions.
302
+ *
303
+ * CAD drafters interrupt wall faces at every door/window, so raw wall layers
304
+ * always have gaps; Snaptrude doors/windows must host into a continuous wall.
305
+ * The returned centrelines are ready for `design.create.walls`, and each
306
+ * centreline's `openings` records where the CAD had a gap — feed those to the
307
+ * door/window placement step. `unpaired` faces (no parallel partner at wall
308
+ * thickness) are evidence, not noise: a run of unpaired perimeter faces
309
+ * usually means that stretch of facade is glazed, not solid.
310
+ *
311
+ * All lengths are in **Snaptrude internal units** (convert from project units
312
+ * with `core.units.convert`). Coordinates are world-space plan (x, z).
313
+ *
314
+ * Thin parallel pairs (glass lines, mullion faces) are the same algorithm at
315
+ * a smaller separation: pass e.g. `thicknessRange: [0.01, 0.25]` with
316
+ * `bridgeOpenings: null` to detect glazing runs on a glazing layer.
317
+ *
318
+ * @param underlay - The placed CAD underlay to read.
319
+ * @param layer - Exact, case-sensitive original AutoCAD layer name.
320
+ * @param options - Tolerances; see {@linkcode PluginCadCenterlineOptions}.
321
+ * @returns centrelines + unpaired faces + stats, or `null` if the underlay is gone.
322
+ * @throws if the handle resolves to an image/PDF rather than a CAD underlay.
323
+ *
324
+ * @examplePrompt Trace the walls from the a-wall CAD layer
325
+ * @examplePrompt Convert the AutoCAD wall linework into Snaptrude walls
326
+ * @examplePrompt Extract wall centrelines with thickness from the imported DWG
327
+ * @examplePrompt Find the glazing runs on the a-glazing layer
328
+ *
329
+ * # Example
330
+ * ```ts
331
+ * const [cad] = await snaptrude.core.io.underlay.list()
332
+ * const r = await snaptrude.core.io.underlay.extractCenterlines(cad, "a-wall")
333
+ * if (r) {
334
+ * const items = r.centerlines.map((c) => ({
335
+ * profile: [c.start, c.end],
336
+ * thickness: c.thickness,
337
+ * }))
338
+ * console.log(`${items.length} walls, ${r.unpaired.length} unpaired faces`)
339
+ * }
340
+ * ```
341
+ */
342
+ public abstract extractCenterlines(
343
+ underlay: UnderlayHandle,
344
+ layer: string,
345
+ options?: PluginCadCenterlineOptions,
346
+ ): PluginApiReturn<PluginCadCenterlinesResult | null>
347
+
348
+ /**
349
+ * Classify one retained AutoCAD layer's **arcs as door swings** — the
350
+ * deterministic tracing primitive behind CAD-to-BIM door placement. A door
351
+ * swing is drawn as a ~90° arc whose radius IS the leaf width and whose
352
+ * centre IS the hinge point. This filters the layer's arcs by plausible
353
+ * radius and sweep (rejecting inch-scale fillets, full-circle symbols and
354
+ * long shallow curves), reads each survivor as a door candidate (hinge, leaf
355
+ * width, swing direction, hosting wall direction), merges mirrored pairs
356
+ * sharing a chord line into double doors, and returns a radius histogram —
357
+ * real drawings use a handful of standard door sizes, so the histogram's
358
+ * clusters are the drawing's door widths (sanity-check them against the
359
+ * catalog; distrust arcs in no cluster).
360
+ *
361
+ * All lengths are in **Snaptrude internal units**. `rejected` lists filtered
362
+ * arcs with reasons so nothing is silently dropped.
363
+ *
364
+ * @param underlay - The placed CAD underlay to read.
365
+ * @param layer - Exact, case-sensitive original AutoCAD layer name.
366
+ * @param options - Gates and binning; see {@linkcode PluginCadArcClassifyOptions}.
367
+ * @returns door candidates + rejected arcs + histogram, or `null` if the underlay is gone.
368
+ * @throws if the handle resolves to an image/PDF rather than a CAD underlay.
369
+ *
370
+ * @examplePrompt Find the doors on the a-door CAD layer
371
+ * @examplePrompt Classify the door swing arcs in the imported drawing
372
+ * @examplePrompt What door sizes does this DWG use?
373
+ * @examplePrompt Read hinge points and leaf widths from the CAD door layer
374
+ *
375
+ * # Example
376
+ * ```ts
377
+ * const [cad] = await snaptrude.core.io.underlay.list()
378
+ * const r = await snaptrude.core.io.underlay.classifyArcs(cad, "a-door")
379
+ * if (r) {
380
+ * console.log(`${r.doors.length} doors; sizes:`,
381
+ * r.radiusHistogram.filter((b) => b.count > 1))
382
+ * }
383
+ * ```
384
+ */
385
+ public abstract classifyArcs(
386
+ underlay: UnderlayHandle,
387
+ layer: string,
388
+ options?: PluginCadArcClassifyOptions,
389
+ ): PluginApiReturn<PluginCadArcClassifyResult | null>
221
390
  }
222
391
 
223
392
  /**
@@ -251,7 +420,9 @@ export const PluginUnderlaySetScaleValue = z.union([
251
420
  z.number().positive(),
252
421
  PluginUnderlayPlanSize,
253
422
  ])
254
- export type PluginUnderlaySetScaleValue = z.infer<typeof PluginUnderlaySetScaleValue>
423
+ export type PluginUnderlaySetScaleValue = z.infer<
424
+ typeof PluginUnderlaySetScaleValue
425
+ >
255
426
 
256
427
  /**
257
428
  * Arguments for {@link PluginCoreIoUnderlayApi.setScale}.
@@ -265,7 +436,9 @@ export const PluginUnderlaySetScaleArgs = z.object({
265
436
  underlay: UnderlayHandle,
266
437
  scale: PluginUnderlaySetScaleValue,
267
438
  })
268
- export type PluginUnderlaySetScaleArgs = z.infer<typeof PluginUnderlaySetScaleArgs>
439
+ export type PluginUnderlaySetScaleArgs = z.infer<
440
+ typeof PluginUnderlaySetScaleArgs
441
+ >
269
442
 
270
443
  /**
271
444
  * Arguments for {@link PluginCoreIoUnderlayApi.setOpacity}.
@@ -279,7 +452,9 @@ export const PluginUnderlaySetOpacityArgs = z.object({
279
452
  underlay: UnderlayHandle,
280
453
  opacity: z.number().min(0).max(1),
281
454
  })
282
- export type PluginUnderlaySetOpacityArgs = z.infer<typeof PluginUnderlaySetOpacityArgs>
455
+ export type PluginUnderlaySetOpacityArgs = z.infer<
456
+ typeof PluginUnderlaySetOpacityArgs
457
+ >
283
458
 
284
459
  /**
285
460
  * Arguments for the single-underlay methods
@@ -293,3 +468,238 @@ export const PluginUnderlayRefArgs = z.object({
293
468
  underlay: UnderlayHandle,
294
469
  })
295
470
  export type PluginUnderlayRefArgs = z.infer<typeof PluginUnderlayRefArgs>
471
+
472
+ /** A retained AutoCAD line represented as a world-space read record. */
473
+ export const PluginCadLineGeometry = z.object({
474
+ type: z.literal("line"),
475
+ start: Vec3Components,
476
+ end: Vec3Components,
477
+ })
478
+ export type PluginCadLineGeometry = z.infer<typeof PluginCadLineGeometry>
479
+
480
+ /** A retained AutoCAD arc represented as a world-space read record. */
481
+ export const PluginCadArcGeometry = z.object({
482
+ type: z.literal("arc"),
483
+ centre: Vec3Components,
484
+ axis: Vec3Components,
485
+ start: Vec3Components,
486
+ end: Vec3Components,
487
+ })
488
+ export type PluginCadArcGeometry = z.infer<typeof PluginCadArcGeometry>
489
+
490
+ /** Line or arc geometry returned by `getCadLayerGeometry`. */
491
+ export const PluginCadGeometry = z.discriminatedUnion("type", [
492
+ PluginCadLineGeometry,
493
+ PluginCadArcGeometry,
494
+ ])
495
+ export type PluginCadGeometry = z.infer<typeof PluginCadGeometry>
496
+
497
+ /** Pagination options for `getCadLayerGeometry`. */
498
+ export const PluginCadLayerGeometryOptions = z.object({
499
+ offset: z.number().int().nonnegative().default(0),
500
+ limit: z.number().int().positive().max(1000).default(500),
501
+ })
502
+ export type PluginCadLayerGeometryOptions = z.input<
503
+ typeof PluginCadLayerGeometryOptions
504
+ >
505
+
506
+ /** Arguments for {@link PluginCoreIoUnderlayApi.getCadLayerGeometry}. */
507
+ export const PluginCadLayerGeometryArgs = z.object({
508
+ underlay: UnderlayHandle,
509
+ layer: z.string(),
510
+ options: PluginCadLayerGeometryOptions.default({ offset: 0, limit: 500 }),
511
+ })
512
+ export type PluginCadLayerGeometryArgs = z.infer<
513
+ typeof PluginCadLayerGeometryArgs
514
+ >
515
+
516
+ /** A page of one original AutoCAD layer's geometry in Snaptrude world space. */
517
+ export const PluginCadLayerGeometryPage = z.object({
518
+ layer: z.string(),
519
+ coordinateSpace: z.literal("snaptrude-world"),
520
+ units: z.literal("snaptrude-internal"),
521
+ total: z.number().int().nonnegative(),
522
+ offset: z.number().int().nonnegative(),
523
+ curves: z.array(PluginCadGeometry),
524
+ })
525
+ export type PluginCadLayerGeometryPage = z.infer<
526
+ typeof PluginCadLayerGeometryPage
527
+ >
528
+
529
+
530
+ /** A point in the world-space plan (x, z) projection, Snaptrude internal units. */
531
+ export const PluginPlanPoint = z.object({ x: z.number(), z: z.number() })
532
+ export type PluginPlanPoint = z.infer<typeof PluginPlanPoint>
533
+
534
+ /**
535
+ * Options for {@link PluginCoreIoUnderlayApi.extractCenterlines}. All lengths in
536
+ * Snaptrude internal units.
537
+ *
538
+ * | Property | Type | Description |
539
+ * |---|---|---|
540
+ * | `thicknessRange` | `[number, number]`? | Plausible wall thickness `[min, max]` (default `[0.3, 1.6]` ≈ 3–16 in) |
541
+ * | `mergeGap` | `number`? | Noise tolerance joining collinear segments that are really one face (default `0.05`) |
542
+ * | `minLength` | `number`? | Cull fragments shorter than this (default `0.1`) |
543
+ * | `parallelTolDeg` | `number`? | Max angle in degrees between faces still considered parallel (default `2`) |
544
+ * | `minOverlap` | `number`? | Fraction of the shorter face that must overlap its pair (default `0.6`) |
545
+ * | `bridgeOpenings` | `{ maxWidth }` \| `null`? | Close collinear centreline gaps up to `maxWidth`, recording each as an opening (default `{ maxWidth: 9.6 }` ≈ 8 ft); `null` disables bridging |
546
+ */
547
+ export const PluginCadCenterlineOptions = z.object({
548
+ thicknessRange: z
549
+ .tuple([z.number().positive(), z.number().positive()])
550
+ .default([0.3, 1.6]),
551
+ mergeGap: z.number().nonnegative().default(0.05),
552
+ minLength: z.number().nonnegative().default(0.1),
553
+ parallelTolDeg: z.number().positive().max(15).default(2),
554
+ minOverlap: z.number().min(0).max(1).default(0.6),
555
+ bridgeOpenings: z
556
+ .object({ maxWidth: z.number().positive() })
557
+ .nullable()
558
+ .default({ maxWidth: 9.6 }),
559
+ })
560
+ export type PluginCadCenterlineOptions = z.input<
561
+ typeof PluginCadCenterlineOptions
562
+ >
563
+
564
+ /** Arguments for {@link PluginCoreIoUnderlayApi.extractCenterlines}. */
565
+ export const PluginCadCenterlinesArgs = z.object({
566
+ underlay: UnderlayHandle,
567
+ layer: z.string(),
568
+ options: PluginCadCenterlineOptions.default({
569
+ thicknessRange: [0.3, 1.6],
570
+ mergeGap: 0.05,
571
+ minLength: 0.1,
572
+ parallelTolDeg: 2,
573
+ minOverlap: 0.6,
574
+ bridgeOpenings: { maxWidth: 9.6 },
575
+ }),
576
+ })
577
+ export type PluginCadCenterlinesArgs = z.infer<typeof PluginCadCenterlinesArgs>
578
+
579
+ /** A span bridged over a CAD gap in a continuous centreline — a door/window opening candidate. */
580
+ export const PluginCadOpening = z.object({
581
+ start: PluginPlanPoint,
582
+ end: PluginPlanPoint,
583
+ width: z.number(),
584
+ })
585
+ export type PluginCadOpening = z.infer<typeof PluginCadOpening>
586
+
587
+ /**
588
+ * One continuous wall centreline extracted from CAD wall faces. `thickness` is
589
+ * the measured face separation; `openings` are the CAD gaps bridged into this
590
+ * centreline (feed the door/window step).
591
+ */
592
+ export const PluginCadCenterline = z.object({
593
+ start: PluginPlanPoint,
594
+ end: PluginPlanPoint,
595
+ thickness: z.number(),
596
+ length: z.number(),
597
+ sourceFaceCount: z.number().int().nonnegative(),
598
+ openings: z.array(PluginCadOpening),
599
+ })
600
+ export type PluginCadCenterline = z.infer<typeof PluginCadCenterline>
601
+
602
+ /** A merged face with no parallel partner at wall thickness — evidence (often glazing), not noise. */
603
+ export const PluginCadUnpairedFace = z.object({
604
+ start: PluginPlanPoint,
605
+ end: PluginPlanPoint,
606
+ length: z.number(),
607
+ })
608
+ export type PluginCadUnpairedFace = z.infer<typeof PluginCadUnpairedFace>
609
+
610
+ /** Result of {@link PluginCoreIoUnderlayApi.extractCenterlines}. */
611
+ export const PluginCadCenterlinesResult = z.object({
612
+ layer: z.string(),
613
+ coordinateSpace: z.literal("snaptrude-world"),
614
+ units: z.literal("snaptrude-internal"),
615
+ centerlines: z.array(PluginCadCenterline),
616
+ unpaired: z.array(PluginCadUnpairedFace),
617
+ stats: z.object({
618
+ curvesRead: z.number().int().nonnegative(),
619
+ arcsSkipped: z.number().int().nonnegative(),
620
+ fragmentsCulled: z.number().int().nonnegative(),
621
+ duplicates: z.number().int().nonnegative(),
622
+ faces: z.number().int().nonnegative(),
623
+ }),
624
+ })
625
+ export type PluginCadCenterlinesResult = z.infer<
626
+ typeof PluginCadCenterlinesResult
627
+ >
628
+
629
+ /**
630
+ * Options for {@link PluginCoreIoUnderlayApi.classifyArcs}. Lengths in Snaptrude
631
+ * internal units, sweeps in degrees.
632
+ *
633
+ * | Property | Type | Description |
634
+ * |---|---|---|
635
+ * | `radiusRange` | `[number, number]`? | Plausible door-leaf widths (default `[3, 5.4]` ≈ 2.5–4.5 ft) |
636
+ * | `sweepRangeDeg` | `[number, number]`? | Accepted arc sweep in degrees (default `[60, 120]`; a swing is ~90°) |
637
+ * | `clusterTol` | `number`? | Radius bin size for the histogram (default `0.1`) |
638
+ */
639
+ export const PluginCadArcClassifyOptions = z.object({
640
+ radiusRange: z
641
+ .tuple([z.number().positive(), z.number().positive()])
642
+ .default([3, 5.4]),
643
+ sweepRangeDeg: z
644
+ .tuple([z.number().positive(), z.number().positive()])
645
+ .default([60, 120]),
646
+ clusterTol: z.number().positive().default(0.1),
647
+ })
648
+ export type PluginCadArcClassifyOptions = z.input<
649
+ typeof PluginCadArcClassifyOptions
650
+ >
651
+
652
+ /** Arguments for {@link PluginCoreIoUnderlayApi.classifyArcs}. */
653
+ export const PluginCadArcClassifyArgs = z.object({
654
+ underlay: UnderlayHandle,
655
+ layer: z.string(),
656
+ options: PluginCadArcClassifyOptions.default({
657
+ radiusRange: [3, 5.4],
658
+ sweepRangeDeg: [60, 120],
659
+ clusterTol: 0.1,
660
+ }),
661
+ })
662
+ export type PluginCadArcClassifyArgs = z.infer<typeof PluginCadArcClassifyArgs>
663
+
664
+ /**
665
+ * One door candidate read from a CAD swing arc. `hinge` is the arc centre,
666
+ * `leafWidth` the arc radius, `swingDir` the unit direction from hinge toward
667
+ * the open leaf, `wallDir` the unit direction of the hosting wall (hinge →
668
+ * closed-leaf chord end). `openingWidth` equals `leafWidth` for singles and
669
+ * `2 × leafWidth` for doubles.
670
+ */
671
+ export const PluginCadDoorCandidate = z.object({
672
+ hinge: PluginPlanPoint,
673
+ leafWidth: z.number(),
674
+ swingDir: PluginPlanPoint,
675
+ wallDir: PluginPlanPoint,
676
+ openingWidth: z.number(),
677
+ double: z.boolean(),
678
+ })
679
+ export type PluginCadDoorCandidate = z.infer<typeof PluginCadDoorCandidate>
680
+
681
+ /** Result of {@link PluginCoreIoUnderlayApi.classifyArcs}. */
682
+ export const PluginCadArcClassifyResult = z.object({
683
+ layer: z.string(),
684
+ coordinateSpace: z.literal("snaptrude-world"),
685
+ units: z.literal("snaptrude-internal"),
686
+ doors: z.array(PluginCadDoorCandidate),
687
+ rejected: z.array(
688
+ z.object({
689
+ centre: PluginPlanPoint,
690
+ radius: z.number(),
691
+ sweepDeg: z.number(),
692
+ reason: z.string(),
693
+ }),
694
+ ),
695
+ radiusHistogram: z.array(
696
+ z.object({ radius: z.number(), count: z.number().int().positive() }),
697
+ ),
698
+ stats: z.object({
699
+ curvesRead: z.number().int().nonnegative(),
700
+ arcsConsidered: z.number().int().nonnegative(),
701
+ }),
702
+ })
703
+ export type PluginCadArcClassifyResult = z.infer<
704
+ typeof PluginCadArcClassifyResult
705
+ >
@@ -1,15 +1,47 @@
1
1
  import * as z from "zod"
2
2
  import { PluginApiReturn } from "../../../types"
3
+ import { PUnitType } from "../units"
3
4
 
4
5
  /**
5
6
  * Project-level settings and info.
6
7
  *
7
- * Accessed via `snaptrude.core.project`. Currently exposes
8
+ * Accessed via `snaptrude.core.project`. Exposes
9
+ * {@linkcode PluginProjectApi.getInfo} (project identity) and
8
10
  * {@linkcode PluginProjectApi.settings} (snap + grid controls).
9
11
  */
10
12
  export abstract class PluginProjectApi {
11
13
  constructor() {}
12
14
 
15
+ /**
16
+ * Read identity and headline facts about the currently open project — id,
17
+ * display name, unit type, storey count, active storey, and site location
18
+ * when the project is geo-located.
19
+ *
20
+ * `activeStorey` and `storeyCount` are both scoped to the ACTIVE BUILDING, so
21
+ * they always describe the same building. In a multi-building project
22
+ * `storeyCount` is therefore NOT the total across every building — use
23
+ * `core.storeys.list()`, which spans all buildings, for that.
24
+ *
25
+ * @returns A {@linkcode PluginProjectInfo}. `name` is `null` only when the
26
+ * project genuinely has no title; `location` is `null` only when the project
27
+ * is not geo-located. Neither is used to signal a failure.
28
+ * @throws PRECONDITION_FAILED when no project is open.
29
+ * @throws OPERATION_FAILED when the project lookup or the site-location read
30
+ * fails. A failed read is never reported as `null`.
31
+ *
32
+ * @examplePrompt What is this project called?
33
+ * @examplePrompt Where is this project located?
34
+ * @examplePrompt Give me a summary of this project
35
+ * @examplePrompt How many storeys does this project have?
36
+ *
37
+ * # Example
38
+ * ```ts
39
+ * const info = await snaptrude.core.project.getInfo()
40
+ * console.log(info.name, info.units, info.storeyCount)
41
+ * ```
42
+ */
43
+ public abstract getInfo(): PluginApiReturn<PluginProjectInfo>
44
+
13
45
  /** Project settings — snaps and grid. See {@linkcode PluginProjectSettingsApi}. */
14
46
  public abstract settings: PluginProjectSettingsApi
15
47
  }
@@ -416,3 +448,32 @@ export const PluginToleranceArgs = z.object({
416
448
  })
417
449
 
418
450
  export type PluginToleranceArgs = z.infer<typeof PluginToleranceArgs>
451
+
452
+ /**
453
+ * Identity and headline facts about the currently open project.
454
+ *
455
+ * `location` is a read-only projection of {@linkcode PluginProgramSiteApi.getLocation}
456
+ * — `program.site.*` remains the full site surface (context, weather, polygons).
457
+ * It is mirrored here because "what and where is this project" is one question.
458
+ *
459
+ * | Property | Type | Description |
460
+ * |---|---|---|
461
+ * | `projectId` | `string` | The open project's id (floorkey) |
462
+ * | `name` | `string \| null` | Display name; `null` only when the project has no title (a failed lookup throws) |
463
+ * | `units` | {@linkcode PUnitType} | The project's unit type |
464
+ * | `activeStorey` | `number` | The active storey value, in the active building |
465
+ * | `storeyCount` | `number` | How many storeys the ACTIVE BUILDING has (not the project-wide total — `core.storeys.list()` spans all buildings) |
466
+ * | `location` | `{ latitude, longitude } \| null` | Site location; `null` only when not geo-located (a failed read throws) |
467
+ */
468
+ export const PluginProjectInfo = z.object({
469
+ projectId: z.string(),
470
+ name: z.string().nullable(),
471
+ units: PUnitType,
472
+ activeStorey: z.number(),
473
+ storeyCount: z.number(),
474
+ location: z
475
+ .object({ latitude: z.number(), longitude: z.number() })
476
+ .nullable(),
477
+ })
478
+
479
+ export type PluginProjectInfo = z.infer<typeof PluginProjectInfo>
@@ -160,6 +160,21 @@ export abstract class PluginCoreStoreysApi {
160
160
  options?: { name?: string },
161
161
  ): PluginApiReturn<PluginStoryUpdateResult>
162
162
 
163
+ /**
164
+ * Get the active storey's value. Paired with {@linkcode PluginCoreStoreysApi.setActive}.
165
+ *
166
+ * @returns The active storey value, in the same numbering `list` and `get` use.
167
+ *
168
+ * @examplePrompt Which storey am I on?
169
+ * @examplePrompt What is the current storey?
170
+ *
171
+ * # Example
172
+ * ```ts
173
+ * const storey = await snaptrude.core.storeys.getActive()
174
+ * ```
175
+ */
176
+ public abstract getActive(): PluginApiReturn<number>
177
+
163
178
  /**
164
179
  * Make a storey the active storey — the same as clicking it in the storey/layer
165
180
  * panel. Subsequent draws and creates target this storey, and in 2D the
@@ -2,6 +2,7 @@ import * as z from "zod"
2
2
  import { PluginApiReturn } from "../../../types"
3
3
  import { ComponentHandle } from "../../../handles"
4
4
  import { PluginDesignChangeResult } from "../lock"
5
+ import { PluginObjectCatalogGroup } from "../doors"
5
6
 
6
7
  /**
7
8
  * `snaptrude.design.furniture` — the placeable furniture **catalog** (a library of
@@ -92,9 +93,42 @@ export abstract class PluginDesignFurnitureApi {
92
93
  * const items = await snaptrude.design.furniture.listCatalog(undefined, first)
93
94
  * console.log(first, "→", items.length, "items")
94
95
  * ```
96
+ *
97
+ * @deprecated Use `design.furniture.listCatalogGroups` — the same name
98
+ * `design.doors` and `design.windows` use. Still supported.
95
99
  */
96
100
  public abstract listCategories(): PluginApiReturn<string[]>
97
101
 
102
+ /**
103
+ * List the furniture catalog's groups. Matches
104
+ * {@linkcode PluginDesignDoorsApi.listCatalogGroups} and
105
+ * {@linkcode PluginDesignWindowsApi.listCatalogGroups}, so all three catalog
106
+ * surfaces are named alike.
107
+ *
108
+ * `source` is `"default"` for the built-in picker groups and `"team"` for the
109
+ * sub-types of THIS project's team library — exactly the split
110
+ * {@linkcode PluginDesignFurnitureApi.listCatalog}'s `source` filter uses, so
111
+ * every `"team"` group is guaranteed to match at least one team item here.
112
+ * For furniture the group token and its label are the same string (the
113
+ * category), so `dbType === label`; `design.doors`/`design.windows` have a
114
+ * distinct engine `dbType`.
115
+ *
116
+ * @returns The catalog groups as {@linkcode PluginObjectCatalogGroup}`[]`
117
+ * (`[]` when empty). Pass a group's `dbType` (=== `label`) to
118
+ * {@linkcode PluginDesignFurnitureApi.listCatalog}'s `category` filter to
119
+ * list its items.
120
+ *
121
+ * @examplePrompt What furniture groups are available?
122
+ * @examplePrompt List the furniture categories in this project
123
+ *
124
+ * # Example
125
+ * ```ts
126
+ * const groups = await snaptrude.design.furniture.listCatalogGroups()
127
+ * for (const g of groups) console.log(g.dbType, g.label, g.source)
128
+ * ```
129
+ */
130
+ public abstract listCatalogGroups(): PluginApiReturn<PluginObjectCatalogGroup[]>
131
+
98
132
  /**
99
133
  * List the placeable furniture catalog (team + general libraries),
100
134
  * optionally filtered by library `source` and/or `category`.