@snaptrude/plugin-core 1.0.2 → 1.0.3

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": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -156,6 +156,9 @@ export type PluginCreateFloorItems = z.infer<typeof PluginCreateFloorItems>
156
156
  * Values are evaluated before cutting. Numeric lengths use SU; unit strings
157
157
  * are accepted in values. `facing` selects local +Z; `hinge` sets the declared
158
158
  * hand enum (left = local -X, right = +X). Both are world points.
159
+ * A door between a room and a corridor, lobby or hall swings into the room:
160
+ * pass `facing` inside the room it serves, never in the corridor. Toilets,
161
+ * cupboards, shafts and exits on an escape route open outward instead.
159
162
  *
160
163
  * | Property | Type | Description |
161
164
  * |---|---|---|
@@ -164,7 +167,7 @@ export type PluginCreateFloorItems = z.infer<typeof PluginCreateFloorItems>
164
167
  * | `values` | `Record<string, number \| string>`? | Authored parameter overrides |
165
168
  * | `hostWall` | {@linkcode ComponentHandle} | The wall to host the door |
166
169
  * | `position` | {@linkcode Vec3Handle} | World point projected onto the wall |
167
- * | `facing` | {@linkcode Vec3Handle}? | World point on the side the door swings open to (default engine-chosen) |
170
+ * | `facing` | {@linkcode Vec3Handle}? | World point on the side the door swings open to: inside the room it serves (default engine-chosen) |
168
171
  * | `hinge` | {@linkcode Vec3Handle}? | World point near the hinged jamb; requires hand control |
169
172
  * | `label` | `string`? | Instance name (optional) |
170
173
  * | `width` | `number`? | Declared width parameter (SU, > 0), not evaluated hole width |
@@ -1047,6 +1047,12 @@ export abstract class PluginDesignCreateApi {
1047
1047
  * @examplePrompt Put three doors on this wall in a single undo step
1048
1048
  * @examplePrompt Add all the doors for this floor plan together
1049
1049
  *
1050
+ * A width/height (top-level, or under the family's own keys in `values`) on a family that
1051
+ * takes its size from its Type while Custom Sizes is off is never dropped: the Type within
1052
+ * 25 mm is chosen, else Custom Sizes is turned on and that size is built, leaving the Type
1053
+ * label stale. `design.doors.checkPlacement` reports which as `sizing`; `customSizes: "No"`
1054
+ * with a size no Type matches is refused.
1055
+ *
1050
1056
  * @performance Bulk creator — the whole batch is ONE host round-trip and ONE undo
1051
1057
  * entry. Always prefer this over
1052
1058
  * calling `design.create.door` in a loop: build the full `items[]` array first
@@ -1152,6 +1158,12 @@ export abstract class PluginDesignCreateApi {
1152
1158
  * @examplePrompt Put four windows on this wall in a single undo step
1153
1159
  * @examplePrompt Add all the windows with a 0.9m sill height together
1154
1160
  *
1161
+ * A width/height (top-level, or under the family's own keys in `values`) on a family that
1162
+ * takes its size from its Type while Custom Sizes is off is never dropped: the Type within
1163
+ * 25 mm is chosen, else Custom Sizes is turned on and that size is built, leaving the Type
1164
+ * label stale. `design.windows.checkPlacement` reports which as `sizing`; `customSizes: "No"`
1165
+ * with a size no Type matches is refused.
1166
+ *
1155
1167
  * @performance Bulk creator — the whole batch is ONE host round-trip and ONE undo
1156
1168
  * entry. Always prefer this over
1157
1169
  * calling `design.create.window` in a loop: build the full `items[]` array first
@@ -2,6 +2,10 @@ 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 type {
6
+ PluginOpeningPlacementArgs,
7
+ PluginOpeningPlacementResult,
8
+ } from "../opening-placement"
5
9
 
6
10
  /**
7
11
  * `snaptrude.design.doors` — door-specific reads + edits. Creation lives at
@@ -22,6 +26,33 @@ import { PluginDesignChangeResult } from "../lock"
22
26
  export abstract class PluginDesignDoorsApi {
23
27
  constructor() {}
24
28
 
29
+ /**
30
+ * Trial the native host fit and geometry rebuild, then restore the model without publishing commands.
31
+ * The first result uses the creation options; subsequent results use candidateWidths in input order.
32
+ * Widths are authored parameters, not physical envelopes or a guaranteed maximum width.
33
+ * No catalog save, history entry or model persistence occurs. Rerun after model edits.
34
+ * A failed fit leaves cut unverified. Family-host cuts are also unverified: their rebuild
35
+ * cannot supply a reversible, authoritative trial result. Material clearance checks evaluated
36
+ * parts against physical neighbours across hosts. The intended host uses native fit/cut checks.
37
+ * Creation repeats clearance immediately before placement; this report grants no later approval.
38
+ * Editing permission is required; locked hosts and an active opening tool reject.
39
+ * It does not say which room the door opens into: read `opensInto` from
40
+ * `design.validation.access()` after placement (see {@linkcode getSwingDirection}).
41
+ * A requested width/height on a family sized by its Type (Custom Sizes off) picks the Type
42
+ * within 25 mm, else turns Custom Sizes on; each result's `sizing` says which. Creation
43
+ * (`design.create.door(s)`) applies the same rule, so the size you pass is the size built.
44
+ * @param options Door creation options and up to 12 alternative widths; numeric lengths use SU.
45
+ * @returns Separate fit/cut/clearance evidence, evaluated bounds and explicit limitations.
46
+ * @examplePrompt Check which door widths fit this host without placing a door
47
+ * @example
48
+ * const result = await snaptrude.design.doors.checkPlacement({
49
+ * familyId, version, hostWall, position, width: 3.5, candidateWidths: [3, 2.5],
50
+ * })
51
+ */
52
+ public abstract checkPlacement(
53
+ options: PluginOpeningPlacementArgs,
54
+ ): PluginApiReturn<PluginOpeningPlacementResult>
55
+
25
56
  /**
26
57
  * Get the type name of a door — the label that identifies which kind of door
27
58
  * the instance is, as shown in its properties.
@@ -163,6 +194,12 @@ export abstract class PluginDesignDoorsApi {
163
194
  * Read the saved hand parameter. left means local -X; right means local +X.
164
195
  * Returns null when the recipe does not declare that control.
165
196
  *
197
+ * Hand is the hinge side, not the room the leaf swings into. For that, read
198
+ * `(await design.validation.access()).topology.rooms[].doors[].opensInto`: a
199
+ * converted room or `"outside"`. A door whose wall is not a room edge is listed
200
+ * in `topology.unresolvedDoors` instead. To make it open into the other room,
201
+ * call {@linkcode mirror} (axis `x`) and read access again.
202
+ *
166
203
  * @param door The door to query
167
204
  * @returns `'left'` / `'right'`, or `null` if indeterminate
168
205
  *
@@ -170,12 +207,16 @@ export abstract class PluginDesignDoorsApi {
170
207
  * @examplePrompt Is the selected door left-handed or right-handed?
171
208
  * @examplePrompt Check the hinge side of this door
172
209
  * @examplePrompt Does the entrance door open to the left or the right?
210
+ * @examplePrompt Which room does this door open into?
173
211
  *
174
212
  * # Example
175
213
  * ```ts
176
214
  * const [door] = await snaptrude.design.query.listDoors({ isSelected: true })
177
215
  * const swing = await snaptrude.design.doors.getSwingDirection(door)
178
216
  * if (swing) console.log("opens as a", swing, "hand door")
217
+ * // Which room it opens into
218
+ * const { topology } = await snaptrude.design.validation.access()
219
+ * const side = topology.rooms.flatMap((r) => r.doors).find((d) => d.door === door)?.opensInto
179
220
  * ```
180
221
  */
181
222
  public abstract getSwingDirection(
@@ -188,6 +229,8 @@ export abstract class PluginDesignDoorsApi {
188
229
  * Axis y rejects. No mesh reflection occurs. Recipes use outputs.swingSide
189
230
  * (1 or -1) for the adjoining room's floor ownership. A door between two
190
231
  * room floors requires this output; a missing or invalid report rejects the cut.
232
+ * The same side decides `opensInto` in `design.validation.access()`; read it
233
+ * there to confirm which room the door now opens into.
191
234
  *
192
235
  * @param door The door to mirror
193
236
  * @param axis Reflection axis (optional; default `'x'` — the swing-flip axis; `'z'` — the hinge-jamb axis)
@@ -288,6 +288,7 @@ export * from "./query"
288
288
  export * from "./selection"
289
289
  export * from "./doors"
290
290
  export * from "./windows"
291
+ export * from "./opening-placement"
291
292
  export * from "./furniture"
292
293
  export * from "./materials"
293
294
  export * from "./lock"
@@ -0,0 +1,79 @@
1
+ import * as z from "zod"
2
+ import { PluginOpeningBaseOptions } from "./create/opening-fields"
3
+
4
+ /** Creation options plus bounded alternative widths. Each trial starts from the same model. */
5
+ export const PluginOpeningPlacementArgs = z.object({
6
+ ...PluginOpeningBaseOptions,
7
+ sillHeight: z.number().finite().nonnegative().optional(),
8
+ candidateWidths: z.array(z.number().finite().positive()).max(12).optional(),
9
+ })
10
+ export type PluginOpeningPlacementArgs = z.infer<
11
+ typeof PluginOpeningPlacementArgs
12
+ >
13
+
14
+ /** A failed fit leaves the cut unverified: no geometry trial was attempted. */
15
+ export const PluginOpeningPlacementCheck = z.object({
16
+ status: z.enum(["pass", "fail", "unverified"]),
17
+ reason: z.string(),
18
+ })
19
+
20
+ const bounds = z.object({
21
+ min: z.object({ x: z.number().finite(), y: z.number().finite(), z: z.number().finite() }),
22
+ max: z.object({ x: z.number().finite(), y: z.number().finite(), z: z.number().finite() }),
23
+ })
24
+
25
+ /** Fit, cut and material clearance are independent checks, not an accessibility certificate. */
26
+ export const PluginOpeningPlacementResult = z.object({
27
+ units: z.literal("SU"),
28
+ results: z.array(
29
+ z.object({
30
+ requestedWidth: z.number().finite().positive().optional(),
31
+ requestedHeight: z.number().finite().positive().optional(),
32
+ /** Original parameter overrides. Evaluated bounds can differ from these values. */
33
+ requestedValues: z.record(z.string(), z.union([z.number().finite(), z.string()])).optional(),
34
+ /**
35
+ * Present when the family sizes itself from a Type unless Custom Sizes is on, and a width
36
+ * or height was requested. `type`: the Type within 25 mm was chosen (its label stays true).
37
+ * `custom`: none matched, so Custom Sizes was turned on and the requested size is built;
38
+ * `type` then names the Type that no longer describes it. Creation applies the same rule.
39
+ */
40
+ sizing: z
41
+ .object({
42
+ mode: z.enum(["type", "custom"]),
43
+ typeKey: z.string(),
44
+ type: z.string(),
45
+ note: z.string(),
46
+ })
47
+ .optional(),
48
+ /** World-space bounds of evaluated material parts, never the opening proxy. */
49
+ physicalBounds: bounds.optional(),
50
+ /** Only present when the recipe declares semantic frame anatomy. */
51
+ frameBounds: bounds.optional(),
52
+ /** World enclosure of the authored cut envelope; not frame or clear passage width. */
53
+ roughOpeningBounds: bounds.optional(),
54
+ dimensions: z
55
+ .object({
56
+ width: z.number().finite().positive(),
57
+ height: z.number().finite().positive(),
58
+ thickness: z.number().finite().nonnegative(),
59
+ })
60
+ .optional(),
61
+ fit: PluginOpeningPlacementCheck,
62
+ cut: PluginOpeningPlacementCheck,
63
+ clearance: PluginOpeningPlacementCheck.extend({
64
+ candidatePairs: z.number().int().nonnegative(),
65
+ checkedPairs: z.number().int().nonnegative(),
66
+ obstructions: z.array(z.object({
67
+ entity: z.string(),
68
+ status: z.enum(["fail", "unverified"]),
69
+ reason: z.string(),
70
+ intersectionVolume: z.number().finite().nonnegative().optional(),
71
+ })),
72
+ }),
73
+ }),
74
+ ),
75
+ limitations: z.array(z.string()),
76
+ })
77
+ export type PluginOpeningPlacementResult = z.infer<
78
+ typeof PluginOpeningPlacementResult
79
+ >
@@ -3,6 +3,10 @@ import { PluginApiReturn } from "../../../types"
3
3
  import { ComponentHandle } from "../../../handles"
4
4
  import { PluginDesignChangeResult } from "../lock"
5
5
  import { PluginObjectCatalogGroup, PluginObjectCatalogItem } from "../doors"
6
+ import type {
7
+ PluginOpeningPlacementArgs,
8
+ PluginOpeningPlacementResult,
9
+ } from "../opening-placement"
6
10
 
7
11
  /**
8
12
  * `snaptrude.design.windows` — window-specific reads. Creation lives at
@@ -22,6 +26,31 @@ import { PluginObjectCatalogGroup, PluginObjectCatalogItem } from "../doors"
22
26
  export abstract class PluginDesignWindowsApi {
23
27
  constructor() {}
24
28
 
29
+ /**
30
+ * Trial the native host fit and geometry rebuild, then restore the model without publishing commands.
31
+ * The first result uses the creation options; subsequent results use candidateWidths in input order.
32
+ * Widths are authored parameters, not physical envelopes or a guaranteed maximum width.
33
+ * No catalog save, history entry or model persistence occurs. Rerun after model edits.
34
+ * A failed fit leaves cut unverified. Family-host cuts are also unverified: their rebuild
35
+ * cannot supply a reversible, authoritative trial result. Material clearance checks evaluated
36
+ * parts against physical neighbours across hosts. The intended host uses native fit/cut checks.
37
+ * Creation repeats clearance immediately before placement; this report grants no later approval.
38
+ * Editing permission is required; locked hosts and an active opening tool reject.
39
+ * A requested width/height on a family sized by its Type (Custom Sizes off) picks the Type
40
+ * within 25 mm, else turns Custom Sizes on; each result's `sizing` says which. Creation
41
+ * (`design.create.window(s)`) applies the same rule, so the size you pass is the size built.
42
+ * @param options Window creation options and up to 12 alternative widths; numeric lengths use SU.
43
+ * @returns Separate fit/cut/clearance evidence, evaluated bounds and explicit limitations.
44
+ * @examplePrompt Check whether this window fits and its host can be cut
45
+ * @example
46
+ * const result = await snaptrude.design.windows.checkPlacement({
47
+ * familyId, version, hostWall, position, width: 4, sillHeight: 3,
48
+ * })
49
+ */
50
+ public abstract checkPlacement(
51
+ options: PluginOpeningPlacementArgs,
52
+ ): PluginApiReturn<PluginOpeningPlacementResult>
53
+
25
54
  /**
26
55
  * Get the type name of a window — the label that identifies which kind of
27
56
  * window the instance is, as shown in its properties.