@snaptrude/plugin-core 1.0.1 → 1.0.2-plainpoint-20261102.2

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 (112) hide show
  1. package/AGENTS.md +1 -0
  2. package/CHANGELOG.md +6 -0
  3. package/api-manifest.json +1682 -118
  4. package/dist/api/core/camera/index.d.ts +16 -6
  5. package/dist/api/core/camera/index.d.ts.map +1 -1
  6. package/dist/api/core/comment/index.d.ts +7 -3
  7. package/dist/api/core/comment/index.d.ts.map +1 -1
  8. package/dist/api/core/geom/create/index.d.ts +69 -21
  9. package/dist/api/core/geom/create/index.d.ts.map +1 -1
  10. package/dist/api/core/geom/query/circle.d.ts +31 -11
  11. package/dist/api/core/geom/query/circle.d.ts.map +1 -1
  12. package/dist/api/core/geom/query/contour.d.ts +19 -7
  13. package/dist/api/core/geom/query/contour.d.ts.map +1 -1
  14. package/dist/api/core/geom/query/curve.d.ts +72 -24
  15. package/dist/api/core/geom/query/curve.d.ts.map +1 -1
  16. package/dist/api/core/geom/query/profile.d.ts +19 -7
  17. package/dist/api/core/geom/query/profile.d.ts.map +1 -1
  18. package/dist/api/core/geom/update/contour.d.ts +36 -12
  19. package/dist/api/core/geom/update/contour.d.ts.map +1 -1
  20. package/dist/api/core/geom/update/curve.d.ts +13 -5
  21. package/dist/api/core/geom/update/curve.d.ts.map +1 -1
  22. package/dist/api/core/geom/update/profile.d.ts +36 -12
  23. package/dist/api/core/geom/update/profile.d.ts.map +1 -1
  24. package/dist/api/core/io/import/index.d.ts +7 -3
  25. package/dist/api/core/io/import/index.d.ts.map +1 -1
  26. package/dist/api/core/math/quat.d.ts +2 -2
  27. package/dist/api/core/math/quat.d.ts.map +1 -1
  28. package/dist/api/core/math/vec3.d.ts +15 -15
  29. package/dist/api/core/math/vec3.d.ts.map +1 -1
  30. package/dist/api/core/storeys/index.d.ts +5 -0
  31. package/dist/api/core/storeys/index.d.ts.map +1 -1
  32. package/dist/api/design/boolean/index.d.ts +20 -4
  33. package/dist/api/design/boolean/index.d.ts.map +1 -1
  34. package/dist/api/design/create/bulk-items.d.ts +84 -16
  35. package/dist/api/design/create/bulk-items.d.ts.map +1 -1
  36. package/dist/api/design/create/index.d.ts +134 -59
  37. package/dist/api/design/create/index.d.ts.map +1 -1
  38. package/dist/api/design/create/opening-fields.d.ts +18 -6
  39. package/dist/api/design/create/opening-fields.d.ts.map +1 -1
  40. package/dist/api/design/delete/index.d.ts +14 -7
  41. package/dist/api/design/delete/index.d.ts.map +1 -1
  42. package/dist/api/design/dimensions.d.ts +12 -4
  43. package/dist/api/design/dimensions.d.ts.map +1 -1
  44. package/dist/api/design/edit/index.d.ts +7 -3
  45. package/dist/api/design/edit/index.d.ts.map +1 -1
  46. package/dist/api/design/family.d.ts +5 -1
  47. package/dist/api/design/family.d.ts.map +1 -1
  48. package/dist/api/design/index.d.ts +5 -0
  49. package/dist/api/design/index.d.ts.map +1 -1
  50. package/dist/api/design/lock.d.ts +3 -0
  51. package/dist/api/design/lock.d.ts.map +1 -1
  52. package/dist/api/design/text.d.ts +400 -0
  53. package/dist/api/design/text.d.ts.map +1 -0
  54. package/dist/api/design/transform/index.d.ts +28 -8
  55. package/dist/api/design/transform/index.d.ts.map +1 -1
  56. package/dist/api/entity/space.d.ts +30 -6
  57. package/dist/api/entity/space.d.ts.map +1 -1
  58. package/dist/api/entity/story.d.ts +4 -0
  59. package/dist/api/entity/story.d.ts.map +1 -1
  60. package/dist/api/presentation/drawings.d.ts +46 -1
  61. package/dist/api/presentation/drawings.d.ts.map +1 -1
  62. package/dist/api/presentation/graphics.d.ts +4 -0
  63. package/dist/api/presentation/graphics.d.ts.map +1 -1
  64. package/dist/api/presentation/placedViews.d.ts +29 -1
  65. package/dist/api/presentation/placedViews.d.ts.map +1 -1
  66. package/dist/api/presentation/standards.d.ts +14 -0
  67. package/dist/api/presentation/standards.d.ts.map +1 -1
  68. package/dist/api/presentation/views.d.ts +5 -1
  69. package/dist/api/presentation/views.d.ts.map +1 -1
  70. package/dist/handles.d.ts +8 -0
  71. package/dist/handles.d.ts.map +1 -1
  72. package/dist/index.cjs +2298 -2179
  73. package/dist/index.cjs.map +1 -1
  74. package/dist/index.js +2285 -2179
  75. package/dist/index.js.map +1 -1
  76. package/package.json +13 -13
  77. package/src/api/core/camera/index.ts +9 -7
  78. package/src/api/core/comment/index.ts +3 -3
  79. package/src/api/core/geom/create/index.ts +25 -25
  80. package/src/api/core/geom/query/circle.ts +11 -11
  81. package/src/api/core/geom/query/contour.ts +7 -6
  82. package/src/api/core/geom/query/curve.ts +25 -25
  83. package/src/api/core/geom/query/profile.ts +7 -6
  84. package/src/api/core/geom/update/contour.ts +12 -11
  85. package/src/api/core/geom/update/curve.ts +5 -5
  86. package/src/api/core/geom/update/profile.ts +12 -11
  87. package/src/api/core/io/import/index.ts +3 -2
  88. package/src/api/core/math/quat.ts +2 -2
  89. package/src/api/core/math/vec3.ts +16 -16
  90. package/src/api/core/storeys/index.ts +5 -0
  91. package/src/api/design/boolean/index.ts +2 -2
  92. package/src/api/design/create/bulk-items.ts +7 -2
  93. package/src/api/design/create/index.ts +34 -46
  94. package/src/api/design/create/opening-fields.ts +7 -7
  95. package/src/api/design/delete/index.ts +14 -7
  96. package/src/api/design/dimensions.ts +5 -4
  97. package/src/api/design/edit/index.ts +3 -3
  98. package/src/api/design/family.ts +2 -1
  99. package/src/api/design/index.ts +5 -0
  100. package/src/api/design/lock.ts +6 -0
  101. package/src/api/design/text.ts +394 -0
  102. package/src/api/design/transform/index.ts +15 -7
  103. package/src/api/entity/space.ts +5 -4
  104. package/src/api/entity/story.ts +4 -0
  105. package/src/api/presentation/graphics.ts +4 -1
  106. package/src/api/presentation/placedViews.ts +12 -0
  107. package/src/api/presentation/standards.ts +6 -0
  108. package/src/api/presentation/views.ts +7 -2
  109. package/src/handles.ts +11 -0
  110. package/test/designText.test.mjs +72 -0
  111. package/test/handles.test.mjs +36 -0
  112. package/test/paperPensContract.test.mjs +39 -0
@@ -0,0 +1,394 @@
1
+ import * as z from "zod"
2
+ import { PluginApiReturn } from "../../types"
3
+ import { ComponentHandle, Vec3Components, Vec3Handle } from "../../handles"
4
+
5
+ /**
6
+ * Authored text — what the Text and Annotate tools place on a drawing.
7
+ *
8
+ * A text is a single line of words drawn flat in its drawing: a **plain text**
9
+ * is centred on its position; an **annotation** is the same text with a leader,
10
+ * a line from an arrowhead (the point it labels) to the text's position, then a
11
+ * short landing to where the words start. Texts are scene components, so their
12
+ * id is a {@linkcode ComponentHandle} and the generic component APIs work on
13
+ * them:
14
+ *
15
+ * - delete: `design.delete.entities`
16
+ * - move / rotate / mirror: `design.transform.*` (the leader travels with the text)
17
+ * - copy: `design.create.copy`
18
+ * - hide / show / isolate: `design.visibility.*`
19
+ * - lock / unlock: `design.lock` / `design.unlock`
20
+ * - select: `design.selection.*`
21
+ *
22
+ * **Where a text lives.** Plan text belongs to one storey and is drawn only in
23
+ * that storey's plan (never in 3D). Text placed while a section or elevation is
24
+ * open belongs to that drawing and is drawn only there. The record's `drawing`
25
+ * and `drawingId` say which.
26
+ *
27
+ * **Size and pens.** A text has no size of its own: every text draws at the
28
+ * project's Text size (Object Styles → Text, `presentation.standards`
29
+ * `annotation.Text.textMm`, paper mm) at the active view scale, and the leader
30
+ * draws with the Text category's projection pen
31
+ * (`presentation.graphics` / `presentation.standards` `objectStyles.Text`).
32
+ * Present sheets restyle all texts of a placed view at once through the
33
+ * `"Text"` and `"Annotation"` categories of
34
+ * `presentation.placedViews.updateStyles` (colour, font, leader visibility).
35
+ *
36
+ * **Fonts.** `fontFamily` is a Google Fonts family (`"Inter"`, `"Roboto"`,
37
+ * `"Open Sans"`, …) or one of the built-ins `"Arial"`, `"Helvetica"`,
38
+ * `"Times New Roman"`, `"Courier New"`, matched case-insensitively and stored
39
+ * in its canonical spelling; an unknown family is refused (`VALIDATION`). A
40
+ * font style the family does not have falls back to `"Regular"`, as in the
41
+ * Properties panel, so read back the style that applied.
42
+ *
43
+ * **Units.** Positions are world points in Babylon units (1 BU = 0.254 m).
44
+ *
45
+ * Every mutator here commits through the engine's command manager: one undo
46
+ * entry per call, autosaved, replayed to collaborators.
47
+ *
48
+ * Accessed via `snaptrude.design.text`.
49
+ */
50
+ export abstract class PluginDesignTextApi {
51
+ constructor() {}
52
+
53
+ /**
54
+ * Place a text, or an annotation when `options.arrowhead` is given.
55
+ *
56
+ * Where it goes:
57
+ * - `options.storey` given → plan text on that storey, at its floor level
58
+ * (the `y` of `position` is ignored).
59
+ * - otherwise, with a section or elevation open on the canvas → that
60
+ * drawing; `position` (and `arrowhead`) are projected onto its plane.
61
+ * - otherwise → plan text on the active storey.
62
+ *
63
+ * The text is created unrotated (it reads along +X in plan); rotate it with
64
+ * `design.transform.rotate`. The font style falls back to `"Regular"` when the
65
+ * family has no such style.
66
+ *
67
+ * @param position - World point the text is placed at: the centre of a plain
68
+ * text, or the leader's text end (where the landing starts) of an
69
+ * annotation.
70
+ * @param content - The words, one line (no line breaks), not blank.
71
+ * @param options - `arrowhead` (world point the leader points at; makes an
72
+ * annotation), `color` (`#rrggbb`, default `#000000`), `fontFamily`
73
+ * (see **Fonts** above, default `"Inter"`), `fontStyle` (default
74
+ * `"Regular"`), `storey` (plan text on this storey).
75
+ * @returns The new text's {@linkcode ComponentHandle}.
76
+ * @throws `VALIDATION` for malformed arguments, blank or multi-line
77
+ * `content`, an unknown `fontFamily`, or an `arrowhead` on top of
78
+ * `position`; `PRECONDITION_FAILED`
79
+ * when `storey` does not exist or the editor is not mounted;
80
+ * `METHOD_NOT_PERMITTED` when plugin writes are disabled.
81
+ *
82
+ * @examplePrompt Label the living room "LIVING" on the plan
83
+ * @examplePrompt Add an annotation pointing at this column saying "RCC 300x300"
84
+ * @examplePrompt Write "NORTH ENTRY" in red bold text at the entrance on level 2
85
+ *
86
+ * # Example
87
+ * ```ts
88
+ * const { design, core } = snaptrude
89
+ * const v = core.math.vec3
90
+ * // A plain label on storey 1.
91
+ * await design.text.create(await v.new(10, 0, 20), "LIVING", { storey: 1 })
92
+ * // An annotation whose leader points at a wall corner.
93
+ * await design.text.create(await v.new(30, 0, 40), "Shear wall", {
94
+ * arrowhead: await v.new(22, 0, 32),
95
+ * color: "#c62828",
96
+ * fontStyle: "Bold",
97
+ * })
98
+ * ```
99
+ */
100
+ public abstract create(
101
+ position: Vec3Handle,
102
+ content: string,
103
+ options?: {
104
+ arrowhead?: Vec3Handle
105
+ color?: string
106
+ fontFamily?: string
107
+ fontStyle?: PluginTextFontStyle
108
+ storey?: number
109
+ },
110
+ ): PluginApiReturn<ComponentHandle>
111
+
112
+ /**
113
+ * List the texts and annotations of every building in the active design
114
+ * option as full records (each carries its `buildingId`).
115
+ *
116
+ * Filters combine with AND: `storeys` keeps texts on those storeys (plan
117
+ * text, and texts of a section or elevation, carry the storey they were
118
+ * placed from), `kind` keeps only plain texts or only annotations,
119
+ * `isHidden` only the ones whose user Hide flag equals the value given.
120
+ *
121
+ * @param options - `storeys`, `kind`, `isHidden` filters (all optional,
122
+ * ANDed).
123
+ * @returns The matching {@linkcode PluginText} records (`[]` when nothing
124
+ * matches).
125
+ * @throws `VALIDATION` for malformed filters.
126
+ *
127
+ * @examplePrompt List all the text labels on level 1
128
+ * @examplePrompt Which annotations are in this model?
129
+ * @examplePrompt Find the text that says "LOBBY"
130
+ *
131
+ * @performance Array read — one host round-trip returns every record.
132
+ * Filter in one call rather than calling `get` per text.
133
+ *
134
+ * # Example
135
+ * ```ts
136
+ * const texts = await snaptrude.design.text.list({ storeys: [1] })
137
+ * const lobby = texts.find((t) => t.content === "LOBBY")
138
+ * ```
139
+ */
140
+ public abstract list(options?: {
141
+ storeys?: number[]
142
+ kind?: PluginTextKind
143
+ isHidden?: boolean
144
+ }): PluginApiReturn<PluginText[]>
145
+
146
+ /**
147
+ * Read one text by handle.
148
+ *
149
+ * Returns `null` — rather than throwing — when the id is not a live text
150
+ * (deleted, undone, or another kind of component), so a handle kept across a
151
+ * delete or an undo can be polled safely.
152
+ *
153
+ * @param text - The text to read.
154
+ * @returns Its {@linkcode PluginText} record, or `null`.
155
+ * @throws `VALIDATION` if `text` is not a non-empty id string.
156
+ *
157
+ * @examplePrompt What does this text say?
158
+ * @examplePrompt Read the annotation I just placed
159
+ *
160
+ * # Example
161
+ * ```ts
162
+ * const t = await snaptrude.design.text.get(handle)
163
+ * if (t?.leader) console.log(t.content, "points at", t.leader.arrowhead)
164
+ * ```
165
+ */
166
+ public abstract get(text: ComponentHandle): PluginApiReturn<PluginText | null>
167
+
168
+ /**
169
+ * Change the words, colour or font of one or more texts — the same edit as
170
+ * the Properties panel. Fields left out keep each text's own value. A font
171
+ * style the (new) family does not have falls back to `"Regular"`, as in the
172
+ * panel. One undo entry for the whole batch.
173
+ *
174
+ * @param texts - The texts to change (at least one).
175
+ * @param patch - `content`, `color`, `fontFamily`, `fontStyle` (at least
176
+ * one).
177
+ * @returns The texts that actually changed, as
178
+ * {@linkcode PluginTextChangeResult} — texts already matching the patch
179
+ * are omitted.
180
+ * @throws `VALIDATION` for malformed arguments, blank / multi-line
181
+ * `content` or an unknown `fontFamily`; `HANDLE_INVALID` if any handle
182
+ * is not a live text (the whole call rejects before anything changes);
183
+ * `PRECONDITION_FAILED` if any text is locked or outside the active
184
+ * design option; `METHOD_NOT_PERMITTED`
185
+ * when plugin writes are disabled.
186
+ *
187
+ * @examplePrompt Make all the room labels bold
188
+ * @examplePrompt Change this annotation to read "Fire exit"
189
+ * @examplePrompt Use Roboto in blue for every text on level 2
190
+ *
191
+ * @performance Array API — pass the whole set in one call (one host
192
+ * round-trip, one undo entry). Never loop this per text.
193
+ *
194
+ * # Example
195
+ * ```ts
196
+ * const text = snaptrude.design.text
197
+ * const level2 = await text.list({ storeys: [2] })
198
+ * await text.update(level2.map((t) => t.id), { fontFamily: "Roboto", color: "#1565c0" })
199
+ * ```
200
+ */
201
+ public abstract update(
202
+ texts: ComponentHandle[],
203
+ patch: PluginTextPatch,
204
+ ): PluginApiReturn<PluginTextChangeResult>
205
+
206
+ /**
207
+ * Point a text's leader at a new place — the same edit as dragging an
208
+ * annotation's arrowhead with the Edit tool. Passing `null` removes the
209
+ * leader (the annotation becomes a plain text); giving a plain text an
210
+ * arrowhead turns it into an annotation. The text itself does not move. To
211
+ * move the text while its arrowhead stays put, move it with
212
+ * `design.transform.move`, then call this with the old `leader.arrowhead`.
213
+ *
214
+ * @param text - The text to change.
215
+ * @param arrowhead - World point the leader points at (projected onto the
216
+ * text's drawing plane), or `null` to remove the leader.
217
+ * @returns The updated {@linkcode PluginText} record.
218
+ * @throws `VALIDATION` for malformed arguments or an arrowhead on top of the
219
+ * text's position; `HANDLE_INVALID` if `text` is not a live text;
220
+ * `PRECONDITION_FAILED` if it is locked or outside the active design
221
+ * option; `METHOD_NOT_PERMITTED` when plugin writes are disabled.
222
+ *
223
+ * @examplePrompt Point this annotation at the door instead
224
+ * @examplePrompt Remove the leader from this label
225
+ *
226
+ * # Example
227
+ * ```ts
228
+ * const { design, core } = snaptrude
229
+ * await design.text.setLeader(handle, await core.math.vec3.new(12, 0, 8))
230
+ * ```
231
+ */
232
+ public abstract setLeader(
233
+ text: ComponentHandle,
234
+ arrowhead: Vec3Handle | null,
235
+ ): PluginApiReturn<PluginText>
236
+ }
237
+
238
+ /** The four font styles a text can use (the Properties panel's Style list). */
239
+ export const PluginTextFontStyle = z.enum([
240
+ "Regular",
241
+ "Bold",
242
+ "Italic",
243
+ "Bold Italic",
244
+ ])
245
+ export type PluginTextFontStyle = z.infer<typeof PluginTextFontStyle>
246
+
247
+ /** `"text"` — a plain text; `"annotation"` — a text with a leader. */
248
+ export const PluginTextKind = z.enum(["text", "annotation"])
249
+ export type PluginTextKind = z.infer<typeof PluginTextKind>
250
+
251
+ /**
252
+ * The drawing a text belongs to: `"plan"` (its storey's plan), `"section"`,
253
+ * `"elevation"`, or `"view"` (a saved 2D view).
254
+ */
255
+ export const PluginTextDrawing = z.enum(["plan", "section", "elevation", "view"])
256
+ export type PluginTextDrawing = z.infer<typeof PluginTextDrawing>
257
+
258
+ const TextContent = z
259
+ .string()
260
+ .refine((s) => s.trim().length > 0, { message: "content must not be blank" })
261
+ .refine((s) => !/[\r\n]/.test(s), {
262
+ message: "content must be one line (no line breaks)",
263
+ })
264
+ const TextColor = z
265
+ .string()
266
+ .regex(/^#[0-9a-fA-F]{6}$/, "color must be a #rrggbb hex color")
267
+ const TextFontFamily = z
268
+ .string()
269
+ .refine((s) => s.trim().length > 0, { message: "fontFamily must not be blank" })
270
+
271
+ /**
272
+ * One text or annotation. Positions are world points in Babylon units.
273
+ *
274
+ * | Property | Type | Description |
275
+ * |---|---|---|
276
+ * | `id` | {@linkcode ComponentHandle} | The text's handle |
277
+ * | `kind` | {@linkcode PluginTextKind} | `"text"` or `"annotation"` (has a leader) |
278
+ * | `content` | `string` | The words |
279
+ * | `color` | `string` | `#rrggbb` |
280
+ * | `fontFamily` | `string` | Font family |
281
+ * | `fontStyle` | {@linkcode PluginTextFontStyle} | Font style |
282
+ * | `position` | {@linkcode Vec3Components} | Centre of a plain text; the leader's text end of an annotation |
283
+ * | `rotation` | `number` | Rotation in its drawing in degrees, positive clockwise as seen on screen — in plan, the sense a positive `design.transform.rotate` angle turns it (0 = reads along +X) |
284
+ * | `leader` | `{ arrowhead: Vec3Components } \| null` | Where the leader points; `null` for plain text |
285
+ * | `storey` | `number` | Storey it belongs to |
286
+ * | `buildingId` | `string \| null` | Building it belongs to |
287
+ * | `drawing` | {@linkcode PluginTextDrawing} | The drawing it is drawn in |
288
+ * | `drawingId` | `string \| null` | The section id, elevation preset or view id; `null` for plan text |
289
+ * | `isHidden` | `boolean` | The user Hide flag (`design.visibility.hide` / `show`) |
290
+ * | `isLocked` | `boolean` | Locked against edits — its own lock (`design.lock` / `unlock`) or a locked group or design option, as `design.isLocked` reports |
291
+ */
292
+ export const PluginText = z.object({
293
+ id: ComponentHandle,
294
+ kind: PluginTextKind,
295
+ content: z.string(),
296
+ color: z.string(),
297
+ fontFamily: z.string(),
298
+ fontStyle: PluginTextFontStyle,
299
+ position: Vec3Components,
300
+ rotation: z.number(),
301
+ leader: z.object({ arrowhead: Vec3Components }).nullable(),
302
+ storey: z.number(),
303
+ buildingId: z.string().nullable(),
304
+ drawing: PluginTextDrawing,
305
+ drawingId: z.string().nullable(),
306
+ isHidden: z.boolean(),
307
+ isLocked: z.boolean(),
308
+ })
309
+ export type PluginText = z.infer<typeof PluginText>
310
+
311
+ /**
312
+ * A sparse edit for {@linkcode PluginDesignTextApi.update} — at least one
313
+ * field.
314
+ *
315
+ * | Property | Type | Description |
316
+ * |---|---|---|
317
+ * | `content` | `string`? | New words (one line, not blank) |
318
+ * | `color` | `string`? | `#rrggbb` |
319
+ * | `fontFamily` | `string`? | Google Fonts family (`"Inter"`, `"Roboto"`, …) or `"Arial"` / `"Helvetica"` / `"Times New Roman"` / `"Courier New"` |
320
+ * | `fontStyle` | {@linkcode PluginTextFontStyle}? | Falls back to `"Regular"` when the family lacks it |
321
+ */
322
+ export const PluginTextPatch = z
323
+ .object({
324
+ content: TextContent.optional(),
325
+ color: TextColor.optional(),
326
+ fontFamily: TextFontFamily.optional(),
327
+ fontStyle: PluginTextFontStyle.optional(),
328
+ })
329
+ .strict()
330
+ .refine((p) => Object.keys(p).length > 0, {
331
+ message: "patch must set at least one field",
332
+ })
333
+ export type PluginTextPatch = z.infer<typeof PluginTextPatch>
334
+
335
+ /**
336
+ * Result of {@linkcode PluginDesignTextApi.update}: the texts the call
337
+ * changed.
338
+ *
339
+ * | Property | Type | Description |
340
+ * |---|---|---|
341
+ * | `affected` | {@linkcode ComponentHandle}`[]` | The texts the call changed |
342
+ */
343
+ export const PluginTextChangeResult = z.object({
344
+ affected: z.array(ComponentHandle),
345
+ })
346
+ export type PluginTextChangeResult = z.infer<typeof PluginTextChangeResult>
347
+
348
+ /**
349
+ * Arguments for {@linkcode PluginDesignTextApi.create} (options flattened).
350
+ *
351
+ * TRANSPORT: positional args — validated host-side as this object.
352
+ */
353
+ export const PluginDesignTextCreateArgs = z.object({
354
+ position: Vec3Handle,
355
+ content: TextContent,
356
+ arrowhead: Vec3Handle.optional(),
357
+ color: TextColor.optional(),
358
+ fontFamily: TextFontFamily.optional(),
359
+ fontStyle: PluginTextFontStyle.optional(),
360
+ storey: z.number().int().optional(),
361
+ })
362
+ export type PluginDesignTextCreateArgs = z.infer<
363
+ typeof PluginDesignTextCreateArgs
364
+ >
365
+
366
+ /** Arguments for {@linkcode PluginDesignTextApi.list} (options flattened). */
367
+ export const PluginDesignTextListArgs = z.object({
368
+ storeys: z.array(z.number()).optional(),
369
+ kind: PluginTextKind.optional(),
370
+ isHidden: z.boolean().optional(),
371
+ })
372
+ export type PluginDesignTextListArgs = z.infer<typeof PluginDesignTextListArgs>
373
+
374
+ /** Arguments for {@linkcode PluginDesignTextApi.get}. */
375
+ export const PluginDesignTextGetArgs = z.object({ text: ComponentHandle })
376
+ export type PluginDesignTextGetArgs = z.infer<typeof PluginDesignTextGetArgs>
377
+
378
+ /** Arguments for {@linkcode PluginDesignTextApi.update}. */
379
+ export const PluginDesignTextUpdateArgs = z.object({
380
+ texts: z.array(ComponentHandle).min(1),
381
+ patch: PluginTextPatch,
382
+ })
383
+ export type PluginDesignTextUpdateArgs = z.infer<
384
+ typeof PluginDesignTextUpdateArgs
385
+ >
386
+
387
+ /** Arguments for {@linkcode PluginDesignTextApi.setLeader}. */
388
+ export const PluginDesignTextSetLeaderArgs = z.object({
389
+ text: ComponentHandle,
390
+ arrowhead: Vec3Handle.nullable(),
391
+ })
392
+ export type PluginDesignTextSetLeaderArgs = z.infer<
393
+ typeof PluginDesignTextSetLeaderArgs
394
+ >
@@ -1,6 +1,6 @@
1
1
  import * as z from "zod"
2
2
  import { PluginApiReturn } from "../../../types"
3
- import { ComponentHandle, Vec3Handle } from "../../../handles"
3
+ import { ComponentHandle, Vec3Handle, Vec3Input } from "../../../handles"
4
4
  import { PluginDesignChangeResult } from "../lock"
5
5
 
6
6
  /**
@@ -22,9 +22,17 @@ export abstract class PluginDesignTransformApi {
22
22
  * component's absolute position as the anchor). Undoable.
23
23
  *
24
24
  * @param components - Entities to move
25
- * @param displacement - Relative translation as a {@linkcode Vec3Handle}
25
+ * @param displacement - Relative translation as a {@linkcode Vec3Handle}. A displacement
26
+ * shorter than 1e-6 is a no-op that succeeds (hosts before Oct 2026 throw
27
+ * `OPERATION_FAILED` "No movement occurred" instead, so skip the call when nothing moves).
26
28
  * @param options - `is2D`, `trackParentChange`
27
29
  * @returns {@linkcode PluginDesignChangeResult}
30
+ * @throws `OPERATION_FAILED` when the engine drops the whole move. A single connected
31
+ * parametric wall moves only across its own line: the vertical part and the part along
32
+ * the line are dropped. Move the whole connected run in one call to lift walls; to slide
33
+ * or lengthen one wall, delete it and create it again on the new line. Graph-bound walls
34
+ * made by Sketch-to-BIM refuse `move` outright (`TOPOLOGY_ERROR` / `ZERO_MOVEMENT`):
35
+ * adjust the room boundary that owns the wall instead.
28
36
  *
29
37
  * @examplePrompt Move the selected room 5 metres to the right
30
38
  * @examplePrompt Shift these walls 3m along the x axis
@@ -43,7 +51,7 @@ export abstract class PluginDesignTransformApi {
43
51
  */
44
52
  public abstract move(
45
53
  components: ComponentHandle[],
46
- displacement: Vec3Handle,
54
+ displacement: Vec3Input,
47
55
  options?: { is2D?: boolean; trackParentChange?: boolean },
48
56
  ): PluginApiReturn<PluginDesignChangeResult>
49
57
 
@@ -81,7 +89,7 @@ export abstract class PluginDesignTransformApi {
81
89
  public abstract rotate(
82
90
  components: ComponentHandle[],
83
91
  angleInDegrees: number,
84
- options?: { axis?: Vec3Handle; pivot?: Vec3Handle; is2D?: boolean },
92
+ options?: { axis?: Vec3Input; pivot?: Vec3Input; is2D?: boolean },
85
93
  ): PluginApiReturn<PluginDesignChangeResult>
86
94
 
87
95
  /**
@@ -205,7 +213,7 @@ export type PluginAlignEdge = z.infer<typeof PluginAlignEdge>
205
213
  */
206
214
  export const PluginDesignMoveArgs = z.object({
207
215
  components: z.array(ComponentHandle).min(1),
208
- displacement: Vec3Handle,
216
+ displacement: Vec3Input,
209
217
  is2D: z.boolean().optional(),
210
218
  trackParentChange: z.boolean().optional(),
211
219
  })
@@ -217,9 +225,9 @@ export type PluginDesignMoveArgs = z.infer<typeof PluginDesignMoveArgs>
217
225
  export const PluginDesignRotateArgs = z.object({
218
226
  components: z.array(ComponentHandle).min(1),
219
227
  angleInDegrees: z.number(),
220
- axis: Vec3Handle.optional(),
228
+ axis: Vec3Input.optional(),
221
229
  is2D: z.boolean().optional(),
222
- pivot: Vec3Handle.optional(),
230
+ pivot: Vec3Input.optional(),
223
231
  })
224
232
  export type PluginDesignRotateArgs = z.infer<typeof PluginDesignRotateArgs>
225
233
 
@@ -6,6 +6,7 @@ import * as z from "zod"
6
6
  import {
7
7
  ProfileHandle,
8
8
  Vec3Handle,
9
+ Vec3Input,
9
10
  Vec3Components,
10
11
  QuatComponents,
11
12
  CurveHandle,
@@ -21,7 +22,7 @@ import {
21
22
  * | `dimensions` | `{ width, height, depth }` | Box dimensions in Babylon units |
22
23
  */
23
24
  export const PluginSpaceCreateRectangularArgs = z.object({
24
- position: Vec3Handle,
25
+ position: Vec3Input,
25
26
  dimensions: z.object({
26
27
  width: z.number(),
27
28
  height: z.number(),
@@ -62,7 +63,7 @@ export const PluginSpaceCreateFromProfileArgs = z.object({
62
63
  profile: ProfileHandle,
63
64
  innerProfiles: z.array(ProfileHandle).optional(),
64
65
  extrudeHeight: z.number(),
65
- position: Vec3Handle,
66
+ position: Vec3Input,
66
67
  })
67
68
 
68
69
  export type PluginSpaceCreateFromProfileArgs = z.infer<
@@ -466,7 +467,7 @@ export type PluginSpaceUpdateResult = z.infer<typeof PluginSpaceUpdateResult>
466
467
  export const PluginSpaceBulkCreateItem = z.discriminatedUnion("kind", [
467
468
  z.object({
468
469
  kind: z.literal("rectangular"),
469
- position: Vec3Handle,
470
+ position: Vec3Input,
470
471
  dimensions: z.object({
471
472
  width: z.number(),
472
473
  height: z.number(),
@@ -482,7 +483,7 @@ export const PluginSpaceBulkCreateItem = z.discriminatedUnion("kind", [
482
483
  profile: ProfileHandle,
483
484
  innerProfiles: z.array(ProfileHandle).optional(),
484
485
  extrudeHeight: z.number(),
485
- position: Vec3Handle,
486
+ position: Vec3Input,
486
487
  room_type: z.string().optional(),
487
488
  spaceType: PluginSpaceType.optional(),
488
489
  massType: PluginMassType.optional(),
@@ -88,6 +88,10 @@ export abstract class PluginStoryApi {
88
88
  * @returns A {@linkcode PluginStoryCreateResult} with `storyId` and `storyValue`
89
89
  * @throws If a story with the given value already exists or creation fails
90
90
  *
91
+ * Storeys are added at the ends of the stack only: a value above the top also creates
92
+ * every storey in between (returning only the requested one), and a value inside a gap
93
+ * is refused.
94
+ *
91
95
  * @examplePrompt Add a third floor to the building
92
96
  * @examplePrompt Create a new storey above the second floor
93
97
  * @examplePrompt Add a basement level to this project
@@ -41,17 +41,20 @@ export const PluginGraphicsModelCategory = z.enum([
41
41
  "Walls",
42
42
  "Windows",
43
43
  ])
44
+ /** Text is authored text and annotations; its projection pen draws their leader lines. */
44
45
  export const PluginGraphicsAnnotationCategory = z.enum([
45
46
  "Dimension",
46
47
  "Grids",
47
48
  "Sections",
49
+ "Text",
48
50
  ])
49
51
  export const PluginGraphicsCategory = z.enum([
50
52
  ...PluginGraphicsModelCategory.options,
51
53
  ...PluginGraphicsAnnotationCategory.options,
52
54
  ])
55
+ /** Text has no view override: its meshes are not hidden or halftoned by category. */
53
56
  export const PluginGraphicsOverrideCategory = z.enum([
54
- ...PluginGraphicsCategory.options,
57
+ ...PluginGraphicsCategory.exclude(["Text"]).options,
55
58
  "Lines",
56
59
  ])
57
60
  export type PluginGraphicsOverrideCategory = z.infer<
@@ -820,6 +820,8 @@ export const PluginViewSettingCategory = z.enum([
820
820
  "Reference Line",
821
821
  "Adjacency Line",
822
822
  "Landscape elements",
823
+ "Text",
824
+ "Annotation",
823
825
  ])
824
826
  export type PluginViewSettingCategory = z.infer<
825
827
  typeof PluginViewSettingCategory
@@ -864,6 +866,10 @@ export type PluginViewLineStyle = z.infer<typeof PluginViewLineStyle>
864
866
  * | `viewStyle` | `"NONE" \| "FILLET" \| "BUBBLE"`? | Shape restyle: block / rounded corner / bubble |
865
867
  * | `radius` | `number`? | Corner radius for `FILLET` (feet) |
866
868
  * | `inset` | `number`? | Shape inset/offset |
869
+ * | `textColor` | `string`? | Dimension / Text / Annotation: `#rrggbb`, or `inherit` (each text keeps its own) |
870
+ * | `textFontFamily` | `string`? | Text / Annotation: one font family for every text, or `inherit` |
871
+ * | `textFontStyle` | `"inherit" \| "Regular" \| "Bold" \| "Italic" \| "Bold Italic"`? | Text / Annotation: one font style, or `inherit` (a family without it draws Regular) |
872
+ * | `leaderVisible` | `boolean`? | Annotation: draw the leaders (line, landing, arrowhead) |
867
873
  */
868
874
  export const PluginPlacedViewStylePatch = z
869
875
  .object({
@@ -921,6 +927,12 @@ export const PluginPlacedViewStylePatch = z
921
927
  .regex(/^#[0-9a-fA-F]{6}$/)
922
928
  .optional(),
923
929
  textMaskPadding: z.number().finite().min(0).max(5).optional(),
930
+ // Text and Annotation rows: one font for every authored text, or `inherit`.
931
+ textFontFamily: z.string().trim().min(1).optional(),
932
+ textFontStyle: z
933
+ .enum(["inherit", "Regular", "Bold", "Italic", "Bold Italic"])
934
+ .optional(),
935
+ leaderVisible: z.boolean().optional(),
924
936
  })
925
937
  .strict()
926
938
  .refine((p) => Object.keys(p).length > 0, {
@@ -82,6 +82,7 @@ const StoredText = z
82
82
  })
83
83
  .strict()
84
84
  .optional(),
85
+ Text: z.object({ textMm: TextSize.optional() }).strict().optional(),
85
86
  })
86
87
  .strict()
87
88
  const StoredDimension = z
@@ -226,6 +227,10 @@ export const PluginGraphicStandardPatch = z
226
227
  })
227
228
  .strict()
228
229
  .optional(),
230
+ Text: z
231
+ .object({ textMm: TextSize.nullable().optional() })
232
+ .strict()
233
+ .optional(),
229
234
  })
230
235
  .strict()
231
236
  .optional(),
@@ -329,6 +334,7 @@ export const PluginResolvedGraphicStandard = z.object({
329
334
  gridBubbleRadiusMm: HeadRadius,
330
335
  sectionTextMm: TextSize,
331
336
  sectionHeadRadiusMm: HeadRadius,
337
+ annotationMm: TextSize,
332
338
  }),
333
339
  dimension: z.object({
334
340
  tickMm: TickLength,
@@ -522,8 +522,13 @@ export type PluginPresentationViewsCreateArgs = z.infer<
522
522
  typeof PluginPresentationViewsCreateArgs
523
523
  >
524
524
 
525
- /** Result of {@linkcode PluginPresentationViewsApi.create} — the saved view. */
526
- export const PluginPresentationViewsCreateResult = PluginPresentationView
525
+ /**
526
+ * Result of {@linkcode PluginPresentationViewsApi.create} — the saved view, plus `framed`:
527
+ * whether the view was framed on the given elements or bounds (always `false` without `frame`).
528
+ */
529
+ export const PluginPresentationViewsCreateResult = PluginPresentationView.extend({
530
+ framed: z.boolean().optional(),
531
+ })
527
532
  export type PluginPresentationViewsCreateResult = z.infer<
528
533
  typeof PluginPresentationViewsCreateResult
529
534
  >
package/src/handles.ts CHANGED
@@ -288,6 +288,17 @@ export const EdgeHandle = handleSchema("edge")
288
288
  export const HalfEdgeHandle = handleSchema("halfedge")
289
289
  export const VertexHandle = handleSchema("vertex")
290
290
  export const Vec3Components = z.object({ x: z.number(), y: z.number(), z: z.number() })
291
+ /**
292
+ * A vec3 ARGUMENT: a {@linkcode Vec3Handle} in any inbound form, or a plain finite
293
+ * `{ x, y, z }` point. Geometry reads return plain points, and callers pass them straight
294
+ * back where a vec3 is expected; the host resolves the point as a vector. Returns stay
295
+ * {@linkcode Vec3Handle}. Explicitly typed to keep handles.d.ts inside its size budget.
296
+ */
297
+ export type Vec3Input = Handle<"vec3"> | Vec3Components
298
+ export const Vec3Input: z.ZodUnion<readonly [typeof Vec3Handle, typeof Vec3Components]> = z.union(
299
+ [Vec3Handle, Vec3Components],
300
+ { error: 'expected a "vec3" handle or a plain { x, y, z } point' }
301
+ )
291
302
  export const QuatComponents = z.object({
292
303
  x: z.number(),
293
304
  y: z.number(),