@snaptrude/plugin-core 0.9.16-networks-20261012.1 → 0.9.17

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 (88) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/api-manifest.json +9711 -0
  3. package/dist/api/analysis/daylight.d.ts +5 -5
  4. package/dist/api/analysis/solar.d.ts +2 -2
  5. package/dist/api/core/camera/index.d.ts +0 -29
  6. package/dist/api/core/camera/index.d.ts.map +1 -1
  7. package/dist/api/core/zoom/index.d.ts +0 -13
  8. package/dist/api/core/zoom/index.d.ts.map +1 -1
  9. package/dist/api/design/create/bulk-items.d.ts +21 -40
  10. package/dist/api/design/create/bulk-items.d.ts.map +1 -1
  11. package/dist/api/design/create/index.d.ts +97 -120
  12. package/dist/api/design/create/index.d.ts.map +1 -1
  13. package/dist/api/design/create/opening-fields.d.ts +12 -13
  14. package/dist/api/design/create/opening-fields.d.ts.map +1 -1
  15. package/dist/api/design/doors/index.d.ts +24 -18
  16. package/dist/api/design/doors/index.d.ts.map +1 -1
  17. package/dist/api/design/family.d.ts +27 -797
  18. package/dist/api/design/family.d.ts.map +1 -1
  19. package/dist/api/design/index.d.ts +0 -16
  20. package/dist/api/design/index.d.ts.map +1 -1
  21. package/dist/api/design/query/index.d.ts +30 -30
  22. package/dist/api/design/selection/index.d.ts +10 -10
  23. package/dist/api/design/types/index.d.ts +24 -40
  24. package/dist/api/design/types/index.d.ts.map +1 -1
  25. package/dist/api/design/update/index.d.ts +3 -40
  26. package/dist/api/design/update/index.d.ts.map +1 -1
  27. package/dist/api/design/windows/index.d.ts +8 -8
  28. package/dist/api/design/windows/index.d.ts.map +1 -1
  29. package/dist/api/presentation/aiInspiration.d.ts +10 -10
  30. package/dist/api/presentation/annotate.d.ts +22 -38
  31. package/dist/api/presentation/annotate.d.ts.map +1 -1
  32. package/dist/api/presentation/export.d.ts +5 -23
  33. package/dist/api/presentation/export.d.ts.map +1 -1
  34. package/dist/api/presentation/index.d.ts +21 -32
  35. package/dist/api/presentation/index.d.ts.map +1 -1
  36. package/dist/api/presentation/placedViews.d.ts +6 -106
  37. package/dist/api/presentation/placedViews.d.ts.map +1 -1
  38. package/dist/api/presentation/sections.d.ts +461 -0
  39. package/dist/api/presentation/sections.d.ts.map +1 -0
  40. package/dist/api/presentation/shapes.d.ts +2 -2
  41. package/dist/api/presentation/sheets.d.ts +19 -175
  42. package/dist/api/presentation/sheets.d.ts.map +1 -1
  43. package/dist/api/presentation/views.d.ts +5 -103
  44. package/dist/api/presentation/views.d.ts.map +1 -1
  45. package/dist/api/presentation/walkthroughs.d.ts +6 -6
  46. package/dist/index.cjs +2434 -3619
  47. package/dist/index.cjs.map +1 -1
  48. package/dist/index.js +2421 -3501
  49. package/dist/index.js.map +1 -1
  50. package/package.json +1 -1
  51. package/scripts/generate-manifest.mjs +0 -5
  52. package/src/api/core/camera/index.ts +0 -29
  53. package/src/api/core/zoom/index.ts +0 -13
  54. package/src/api/design/create/bulk-items.ts +16 -23
  55. package/src/api/design/create/index.ts +119 -129
  56. package/src/api/design/create/opening-fields.ts +12 -15
  57. package/src/api/design/doors/index.ts +42 -58
  58. package/src/api/design/family.ts +32 -662
  59. package/src/api/design/index.ts +4 -34
  60. package/src/api/design/types/index.ts +24 -31
  61. package/src/api/design/update/index.ts +3 -41
  62. package/src/api/design/windows/index.ts +22 -40
  63. package/src/api/presentation/annotate.ts +58 -90
  64. package/src/api/presentation/export.ts +6 -32
  65. package/src/api/presentation/index.ts +23 -39
  66. package/src/api/presentation/placedViews.ts +28 -119
  67. package/src/api/presentation/sections.ts +420 -0
  68. package/src/api/presentation/sheets.ts +20 -181
  69. package/src/api/presentation/views.ts +6 -46
  70. package/dist/api/design/bim.d.ts +0 -209
  71. package/dist/api/design/bim.d.ts.map +0 -1
  72. package/dist/api/design/catalog.d.ts +0 -320
  73. package/dist/api/design/catalog.d.ts.map +0 -1
  74. package/dist/api/design/networks.d.ts +0 -1703
  75. package/dist/api/design/networks.d.ts.map +0 -1
  76. package/dist/api/presentation/drawingContract.d.ts +0 -32
  77. package/dist/api/presentation/drawingContract.d.ts.map +0 -1
  78. package/dist/api/presentation/drawings.d.ts +0 -3523
  79. package/dist/api/presentation/drawings.d.ts.map +0 -1
  80. package/dist/api/presentation/viewPresets.d.ts +0 -222
  81. package/dist/api/presentation/viewPresets.d.ts.map +0 -1
  82. package/src/api/design/bim.ts +0 -121
  83. package/src/api/design/catalog.ts +0 -203
  84. package/src/api/design/networks.ts +0 -607
  85. package/src/api/presentation/drawingContract.ts +0 -70
  86. package/src/api/presentation/drawings.ts +0 -1179
  87. package/src/api/presentation/viewPresets.ts +0 -241
  88. package/test/drawingContract.test.mjs +0 -288
@@ -1,241 +0,0 @@
1
- import * as z from "zod"
2
- import type { PluginApiReturn } from "../../types"
3
-
4
- /** Presets are document-local. Their ids are stable across reload; built-ins are immutable. */
5
- export const PluginViewPreset = z.object({
6
- id: z.string(),
7
- name: z.string(),
8
- builtIn: z.boolean(),
9
- revision: z.number().int().nonnegative(),
10
- })
11
- export type PluginViewPreset = z.infer<typeof PluginViewPreset>
12
- export const PluginViewPresetIdArgs = z
13
- .object({ presetId: z.string().min(1) })
14
- .strict()
15
- export type PluginViewPresetIdArgs = z.infer<typeof PluginViewPresetIdArgs>
16
- export const PluginViewPresetCreateArgs = z
17
- .object({ name: z.string().trim().min(1), fromDrawingId: z.string().min(1) })
18
- .strict()
19
- export type PluginViewPresetCreateArgs = z.infer<
20
- typeof PluginViewPresetCreateArgs
21
- >
22
- export const PluginViewPresetRenameArgs = PluginViewPresetIdArgs.extend({
23
- name: z.string().trim().min(1),
24
- })
25
- export type PluginViewPresetRenameArgs = z.infer<
26
- typeof PluginViewPresetRenameArgs
27
- >
28
- export const PluginViewPresetUpdateArgs = PluginViewPresetIdArgs.extend({
29
- fromDrawingId: z.string().min(1),
30
- })
31
- export type PluginViewPresetUpdateArgs = z.infer<
32
- typeof PluginViewPresetUpdateArgs
33
- >
34
- export const PluginViewPresetDeleteArgs = PluginViewPresetIdArgs.extend({
35
- replacementId: z.string().min(1),
36
- })
37
- export type PluginViewPresetDeleteArgs = z.infer<
38
- typeof PluginViewPresetDeleteArgs
39
- >
40
-
41
- /**
42
- * Reusable styles for kernel-plan drawings, stored in the Present document.
43
- *
44
- * A preset holds every style category of a native kernel plan. A drawing is
45
- * bound to one with {@linkcode PluginPresentationDrawingsApi.applyPreset}; its
46
- * own style changes are overrides on top, dropped by
47
- * {@linkcode PluginPresentationDrawingsApi.resetOverrides}. Presets apply only
48
- * to drawings whose `drawings.getCapabilities(drawingId).presets` is `true`.
49
- * Local title text is excluded from preset values.
50
- *
51
- * Presets are document-local; their ids survive reload. Built-in presets are
52
- * immutable. Catalog writes (`create`, `copy`, `rename`, `update`, `delete`)
53
- * are native document metadata and are **not undoable**; binding a drawing or
54
- * changing its overrides uses Present undo. Every method requires **Present
55
- * mode to be open** and throws `PRECONDITION_FAILED` ("Present mode is not
56
- * open") when it is closed; no method switches tabs.
57
- *
58
- * Accessed via `snaptrude.presentation.viewPresets`.
59
- */
60
- export abstract class PluginPresentationViewPresetsApi {
61
- /**
62
- * List the preset catalog: built-ins first, then custom presets by name.
63
- *
64
- * @returns Every {@linkcode PluginViewPreset}.
65
- * @throws `PRECONDITION_FAILED` if Present mode is not open.
66
- *
67
- * @examplePrompt List the available drawing style presets
68
- * @examplePrompt Which custom presets does this presentation have?
69
- * @examplePrompt Is there a Planning Issue preset?
70
- *
71
- * # Example
72
- * ```ts
73
- * const presets = await snaptrude.presentation.viewPresets.list()
74
- * const custom = presets.filter((preset) => !preset.builtIn)
75
- * ```
76
- */
77
- public abstract list(): PluginApiReturn<PluginViewPreset[]>
78
-
79
- /**
80
- * Read one preset by id.
81
- *
82
- * @param presetId - The preset to read.
83
- * @returns The {@linkcode PluginViewPreset}, or `null` if no preset has that id.
84
- * @throws `PRECONDITION_FAILED` if Present mode is not open.
85
- *
86
- * @examplePrompt Which preset is this drawing using?
87
- * @examplePrompt Is this preset a built-in one?
88
- * @examplePrompt Read the preset bound to the ground floor plan
89
- *
90
- * # Example
91
- * ```ts
92
- * const drawing = await snaptrude.presentation.drawings.get(drawingId)
93
- * const preset = drawing?.presetId
94
- * ? await snaptrude.presentation.viewPresets.get(drawing.presetId)
95
- * : null
96
- * ```
97
- */
98
- public abstract get(
99
- presetId: string,
100
- ): PluginApiReturn<PluginViewPreset | null>
101
-
102
- /**
103
- * Save a kernel-plan drawing's effective styles as a new custom preset and
104
- * bind that drawing to it. Not undoable (catalog write).
105
- *
106
- * @param name - A unique preset name (non-empty after trimming).
107
- * @param options - `fromDrawingId`: the kernel-plan drawing to take styles from.
108
- * @returns The new {@linkcode PluginViewPreset}.
109
- * @throws `VALIDATION` for an empty or duplicate name (with a hint);
110
- * `PRECONDITION_FAILED` if Present mode is not open, the document is
111
- * read-only, or the drawing is not a kernel vector plan (each with a
112
- * hint); `HANDLE_INVALID` if `fromDrawingId` is not a drawing.
113
- *
114
- * @examplePrompt Save this drawing's styles as a new preset
115
- * @examplePrompt Make a preset called Planning Issue from this plan
116
- * @examplePrompt Turn my restyled drawing into a reusable style
117
- *
118
- * # Example
119
- * ```ts
120
- * const capabilities = await snaptrude.presentation.drawings.getCapabilities(drawing.id)
121
- * if (capabilities.presets) {
122
- * const preset = await snaptrude.presentation.viewPresets.create("Planning Issue", {
123
- * fromDrawingId: drawing.id,
124
- * })
125
- * }
126
- * ```
127
- */
128
- public abstract create(
129
- name: string,
130
- options: { fromDrawingId: string },
131
- ): PluginApiReturn<PluginViewPreset>
132
-
133
- /**
134
- * Duplicate a preset under a native-generated unique name. Drawing bindings
135
- * do not change. Not undoable (catalog write). Built-ins can be copied.
136
- *
137
- * @param presetId - The preset to copy.
138
- * @returns The new custom {@linkcode PluginViewPreset}.
139
- * @throws `PRECONDITION_FAILED` if Present mode is not open or the document
140
- * is read-only (each with a hint); `HANDLE_INVALID` if `presetId` does not
141
- * exist.
142
- *
143
- * @examplePrompt Duplicate this preset
144
- * @examplePrompt Make an editable copy of the built-in preset
145
- * @examplePrompt Copy the Planning Issue preset so I can change it
146
- *
147
- * # Example
148
- * ```ts
149
- * const copy = await snaptrude.presentation.viewPresets.copy(preset.id)
150
- * await snaptrude.presentation.viewPresets.rename(copy.id, "Planning Issue B")
151
- * ```
152
- */
153
- public abstract copy(presetId: string): PluginApiReturn<PluginViewPreset>
154
-
155
- /**
156
- * Rename a custom preset. Names must stay unique; built-ins cannot be
157
- * renamed. Not undoable (catalog write).
158
- *
159
- * @param presetId - The custom preset to rename.
160
- * @param name - The new unique name (non-empty after trimming).
161
- * @returns The renamed {@linkcode PluginViewPreset}.
162
- * @throws `VALIDATION` when the catalog refuses: an empty or duplicate name,
163
- * a built-in preset, or an unknown `presetId` (with a hint);
164
- * `PRECONDITION_FAILED` if Present mode is not open or the document is
165
- * read-only (each with a hint).
166
- *
167
- * @examplePrompt Rename this preset to Client Review
168
- * @examplePrompt Call the copied preset Planning Issue B
169
- * @examplePrompt Change the name of my custom drawing style
170
- *
171
- * # Example
172
- * ```ts
173
- * await snaptrude.presentation.viewPresets.rename(preset.id, "Client Review")
174
- * ```
175
- */
176
- public abstract rename(
177
- presetId: string,
178
- name: string,
179
- ): PluginApiReturn<PluginViewPreset>
180
-
181
- /**
182
- * Replace a custom preset's values with the effective styles of a drawing
183
- * already bound to it, then refresh every bound drawing. Built-ins cannot
184
- * be updated. Not undoable (catalog write).
185
- *
186
- * @param presetId - The custom preset to update.
187
- * @param options - `fromDrawingId`: a kernel-plan drawing bound to this preset.
188
- * @returns The updated {@linkcode PluginViewPreset} (its `revision` grows).
189
- * @throws `VALIDATION` when the catalog refuses: a built-in preset (with a
190
- * hint); `PRECONDITION_FAILED` if Present mode is not open, the document
191
- * is read-only, or the drawing is not a kernel vector plan bound to this
192
- * preset (each with a hint); `HANDLE_INVALID` if `fromDrawingId` is not a
193
- * drawing.
194
- *
195
- * @examplePrompt Update this preset from the current drawing styles
196
- * @examplePrompt Push my style changes to every drawing using this preset
197
- * @examplePrompt Save these overrides into the Planning Issue preset
198
- *
199
- * # Example
200
- * ```ts
201
- * await snaptrude.presentation.viewPresets.update(drawing.presetId!, {
202
- * fromDrawingId: drawing.id,
203
- * })
204
- * ```
205
- */
206
- public abstract update(
207
- presetId: string,
208
- options: { fromDrawingId: string },
209
- ): PluginApiReturn<PluginViewPreset>
210
-
211
- /**
212
- * Delete a custom preset and rebind its drawings to another existing
213
- * preset. Built-ins cannot be deleted. Not undoable (catalog write).
214
- *
215
- * @param presetId - The custom preset to delete.
216
- * @param options - `replacementId`: the existing preset its drawings move to
217
- * (not the deleted one).
218
- * @returns Nothing.
219
- * @throws `VALIDATION` when the catalog refuses: a built-in or unknown
220
- * `presetId`, or an unknown `replacementId` (with a hint);
221
- * `PRECONDITION_FAILED` if Present mode is not open or the document is
222
- * read-only (each with a hint).
223
- *
224
- * @examplePrompt Delete this preset
225
- * @examplePrompt Remove the old style preset and switch its drawings to the default
226
- * @examplePrompt Get rid of the Planning Issue B preset
227
- *
228
- * # Example
229
- * ```ts
230
- * const presets = await snaptrude.presentation.viewPresets.list()
231
- * const replacement = presets.find((each) => each.builtIn)!
232
- * await snaptrude.presentation.viewPresets.delete(customPreset.id, {
233
- * replacementId: replacement.id,
234
- * })
235
- * ```
236
- */
237
- public abstract delete(
238
- presetId: string,
239
- options: { replacementId: string },
240
- ): PluginApiReturn<void>
241
- }
@@ -1,288 +0,0 @@
1
- // The drawing tool contract the agent reads: export file names are base names
2
- // without an extension, and architectural scales are standard denominators only.
3
- // Run: node --test test/
4
- // Imports the BUILT package (dist) — run `pnpm build` first.
5
- import test from "node:test"
6
- import assert from "node:assert/strict"
7
- import { readFileSync } from "node:fs"
8
- import {
9
- STANDARD_DRAWING_SCALES,
10
- METRIC_DRAWING_SCALES,
11
- IMPERIAL_DRAWING_SCALES,
12
- IMPERIAL_DETAIL_DRAWING_SCALES,
13
- IMPERIAL_SITE_DRAWING_SCALES,
14
- PluginDrawingCreateOptions,
15
- PluginDrawingCreateArgs,
16
- PluginDrawingScaleArgs,
17
- PluginDrawingExportOptions,
18
- PluginDrawingExportArgs,
19
- PluginPresentationExportArgs,
20
- PluginPresentationSheetsPlaceArgs,
21
- PluginPresentationSheetsPlaceDrawingArgs,
22
- PluginPresentationSheetsPlaceDrawingResult,
23
- PluginPresentationSheetsPlaceResult,
24
- PluginPresentationPlacedViewsSetScaleArgs,
25
- } from "../dist/index.js"
26
- import { buildManifest } from "../scripts/generate-manifest.mjs"
27
-
28
- // The Scale dropdown's values in the frontend (ALL_SCALES_DICTIONARY in
29
- // src/app/common/utils/scaleMeasurements.ts): metric, imperial detail, imperial site.
30
- const METRIC = [10, 20, 50, 100, 150, 200, 250, 500, 1000]
31
- const IMPERIAL_DETAIL = [12, 16, 24, 32, 48, 64, 96, 128, 192, 384, 768, 1536]
32
- const IMPERIAL_SITE = [120, 240, 360, 480, 600, 1200, 2400, 3600, 4800]
33
-
34
- const messagesOf = (result) => result.error.issues.map((issue) => issue.message)
35
-
36
- // --- standard scales ---------------------------------------------------------
37
-
38
- test("STANDARD_DRAWING_SCALES is the frontend's metric and imperial list, ascending", () => {
39
- const expected = [...METRIC, ...IMPERIAL_DETAIL, ...IMPERIAL_SITE].sort((a, b) => a - b)
40
- assert.deepEqual([...STANDARD_DRAWING_SCALES], expected)
41
- assert.deepEqual([...METRIC_DRAWING_SCALES], METRIC)
42
- assert.deepEqual([...IMPERIAL_DRAWING_SCALES], [...IMPERIAL_DETAIL, ...IMPERIAL_SITE].sort((a, b) => a - b))
43
- assert.deepEqual([...IMPERIAL_DETAIL_DRAWING_SCALES], IMPERIAL_DETAIL)
44
- assert.deepEqual([...IMPERIAL_SITE_DRAWING_SCALES], IMPERIAL_SITE)
45
- })
46
-
47
- const scaleSchemas = [
48
- ["drawings.create options", (scale) => PluginDrawingCreateOptions.safeParse({ scale })],
49
- ["drawings.create args", (scale) => PluginDrawingCreateArgs.safeParse({ viewId: "v1", scale })],
50
- ["drawings.setScale", (scale) => PluginDrawingScaleArgs.safeParse({ drawingId: "d1", scale })],
51
- ["sheets.place", (scale) => PluginPresentationSheetsPlaceArgs.safeParse({ sheetId: "s1", viewId: "v1", scale })],
52
- ["placedViews.setScale", (scale) => PluginPresentationPlacedViewsSetScaleArgs.safeParse({ shapeId: "p1", scale })],
53
- ]
54
-
55
- for (const [name, parse] of scaleSchemas) {
56
- test(`${name} accepts every standard scale`, () => {
57
- for (const scale of STANDARD_DRAWING_SCALES) {
58
- assert.equal(parse(scale).success, true, `${name} rejected ${scale}`)
59
- }
60
- })
61
-
62
- test(`${name} rejects a non-standard scale and lists the allowed ones`, () => {
63
- for (const scale of [137, 75, 1, 99.5]) {
64
- const result = parse(scale)
65
- assert.equal(result.success, false, `${name} accepted ${scale}`)
66
- const message = messagesOf(result).join("\n")
67
- assert.match(message, /standard/i)
68
- assert.ok(message.includes(`Metric: ${METRIC.join(", ")}`), message)
69
- assert.ok(message.includes(`imperial detail: ${IMPERIAL_DETAIL.join(", ")}`), message)
70
- assert.ok(message.includes(`imperial site: ${IMPERIAL_SITE.join(", ")}`), message)
71
- assert.match(message, /largest/i)
72
- }
73
- })
74
- }
75
-
76
- test("create options still allow omitting the scale", () => {
77
- assert.equal(PluginDrawingCreateOptions.safeParse({}).success, true)
78
- assert.equal(PluginPresentationSheetsPlaceArgs.safeParse({ sheetId: "s", viewId: "v" }).success, true)
79
- })
80
-
81
- // --- placing a drawing on a sheet -------------------------------------------
82
-
83
- test("sheets.placeDrawing takes an optional boolean fit", () => {
84
- const base = { sheetId: "s", drawingId: "d" }
85
- assert.equal(PluginPresentationSheetsPlaceDrawingArgs.safeParse(base).success, true)
86
- assert.equal(PluginPresentationSheetsPlaceDrawingArgs.safeParse({ ...base, fit: true }).success, true)
87
- assert.equal(PluginPresentationSheetsPlaceDrawingArgs.safeParse({ ...base, fit: false }).success, true)
88
- assert.equal(PluginPresentationSheetsPlaceDrawingArgs.safeParse({ ...base, fit: "yes" }).success, false)
89
- })
90
-
91
- test("sheets.placeDrawing reports the scale it used and whether it rescaled", () => {
92
- const ok = (value) => PluginPresentationSheetsPlaceDrawingResult.safeParse(value).success
93
- assert.equal(ok({ shapeId: "shape:a", scale: 192, rescaled: true }), true)
94
- assert.equal(ok({ shapeId: "shape:a", scale: null, rescaled: false }), true)
95
- assert.equal(ok({ shapeId: "shape:a" }), false)
96
- assert.equal(ok({ shapeId: "shape:a", scale: 100 }), false)
97
- assert.equal(ok({ shapeId: "shape:a", scale: 100, rescaled: false, overlaps: true }), true)
98
- assert.equal(ok({ shapeId: "shape:a", scale: 100, rescaled: false, overlaps: "yes" }), false)
99
- })
100
-
101
- test("sheets.place may report the scale, whether it rescaled and whether it overlaps", () => {
102
- const ok = (value) => PluginPresentationSheetsPlaceResult.safeParse(value).success
103
- assert.equal(ok({ shapeId: "shape:a" }), true)
104
- assert.equal(ok({ shapeId: "shape:a", scale: 192, rescaled: true, overlaps: false }), true)
105
- assert.equal(ok({ shapeId: "shape:a", scale: null, rescaled: false }), true)
106
- assert.equal(ok({ shapeId: "shape:a", overlaps: "no" }), false)
107
- assert.equal(ok({ shapeId: "shape:a", rescaled: 1 }), false)
108
- })
109
-
110
- test("sheets.place may report that a drawing at an explicit scale overflows the sheet; placeDrawing never does", () => {
111
- const place = (value) => PluginPresentationSheetsPlaceResult.safeParse(value).success
112
- assert.equal(place({ shapeId: "shape:a", scale: 100, rescaled: false, overflows: true }), true)
113
- assert.equal(place({ shapeId: "shape:a", scale: 100, rescaled: false, overflows: "yes" }), false)
114
- const placeDrawing = PluginPresentationSheetsPlaceDrawingResult.safeParse({
115
- shapeId: "shape:a",
116
- scale: 100,
117
- rescaled: false,
118
- overflows: true,
119
- })
120
- assert.equal(placeDrawing.success, true)
121
- assert.equal("overflows" in placeDrawing.data, false)
122
- })
123
-
124
- // --- file names --------------------------------------------------------------
125
-
126
- const fileNameSchemas = [
127
- ["drawings.export options", (fileName) => PluginDrawingExportOptions.safeParse({ fileName })],
128
- ["drawings.export args", (fileName) => PluginDrawingExportArgs.safeParse({ drawingId: "d1", format: "pdf", fileName })],
129
- ["presentation.export", (fileName) => PluginPresentationExportArgs.safeParse({ format: "pdf", fileName })],
130
- ]
131
-
132
- for (const [name, parse] of fileNameSchemas) {
133
- test(`${name} rejects a file name with an extension`, () => {
134
- for (const fileName of ["Floor Pattern View.pdf", "plan.PDF", "a.svg", "a.png", "a.jpg", "a.JPEG"]) {
135
- const result = parse(fileName)
136
- assert.equal(result.success, false, `${name} accepted ${fileName}`)
137
- assert.match(messagesOf(result).join("\n"), /without an extension/i)
138
- }
139
- })
140
-
141
- test(`${name} rejects a file name ending in a dot or whitespace`, () => {
142
- // The host appends the extension, so "a.pdf." would become "a.pdf..pdf".
143
- for (const fileName of ["a.pdf.", "plan.", "plan..", "plan ", "plan\t", "a.pdf .", "a.pdf. "]) {
144
- const result = parse(fileName)
145
- assert.equal(result.success, false, `${name} accepted ${JSON.stringify(fileName)}`)
146
- assert.match(messagesOf(result).join("\n"), /without an extension/i)
147
- }
148
- })
149
-
150
- test(`${name} accepts a base name`, () => {
151
- for (const fileName of ["Floor Pattern View", "Level 1.2 plan", "pdf plan", "notes.v2"]) {
152
- assert.equal(parse(fileName).success, true, `${name} rejected ${fileName}`)
153
- }
154
- })
155
- }
156
-
157
- // --- the contract the agent reads (api-manifest summaries) -------------------
158
-
159
- // Overloaded methods produce one leaf per signature; the documented one comes first.
160
- const manifest = new Map()
161
- for (const leaf of buildManifest()) if (!manifest.has(leaf.path)) manifest.set(leaf.path, leaf)
162
- const summaryOf = (path) => {
163
- const leaf = manifest.get(path)
164
- assert.ok(leaf, `no manifest leaf ${path}`)
165
- return leaf.summary
166
- }
167
-
168
- test("export summaries tell the agent to pass fileName without an extension", () => {
169
- for (const path of ["presentation.drawings.export", "presentation.export"]) {
170
- assert.match(summaryOf(path), /fileName[^.]*without (an|its) extension/i, path)
171
- }
172
- })
173
-
174
- test("scale summaries list the standard scales and say how to choose one", () => {
175
- for (const path of [
176
- "presentation.drawings.create",
177
- "presentation.drawings.setScale",
178
- "presentation.sheets.place",
179
- "presentation.placedViews.setScale",
180
- ]) {
181
- const summary = summaryOf(path)
182
- assert.ok(summary.includes(`Metric: ${METRIC.join(", ")}.`), `${path}: ${summary}`)
183
- assert.ok(summary.includes(`Imperial detail: ${IMPERIAL_DETAIL.join(", ")}`), `${path}: ${summary}`)
184
- assert.ok(summary.includes(`Imperial site: ${IMPERIAL_SITE.join(", ")}`), `${path}: ${summary}`)
185
- assert.match(summary, /Pick the LARGEST standard scale \(smallest denominator\)/, path)
186
- assert.match(summary, /fits its frame or sheet/i, path)
187
- assert.match(summary, /usually 1:50 to 1:200/, path)
188
- assert.match(summary, /Never use a site-sized scale/, path)
189
- assert.match(summary, /project's unit system/, path)
190
- }
191
- })
192
-
193
- test("drawings.create tells the agent to omit position and follow the label layout's suggested scale", () => {
194
- const summary = summaryOf("presentation.drawings.create")
195
- assert.match(summary, /Omit `position` unless the user asked for a specific place/, summary)
196
- assert.match(summary, /next to its source view/, summary)
197
- assert.match(summary, /labels\.getLayout\(drawingId\)/, summary)
198
- assert.match(summary, /suggestedScale/, summary)
199
- })
200
-
201
- test("drawing writes send the agent to presentation.camera instead of promising to frame the camera", () => {
202
- for (const path of [
203
- "presentation.drawings.create",
204
- "presentation.drawings.setScale",
205
- "presentation.drawings.setCrop",
206
- ]) {
207
- const summary = summaryOf(path)
208
- assert.doesNotMatch(summary, /frames the camera/i, path)
209
- assert.match(summary, /Changes the drawing, not the Present camera/, path)
210
- assert.match(summary, /presentation\.camera\.zoomTo\(drawing\.id\)/, path)
211
- assert.match(summary, /presentation\.camera\.capture\(\)/, path)
212
- }
213
- for (const path of ["presentation.drawings.setScale", "presentation.drawings.setCrop"]) {
214
- assert.match(summaryOf(path), /moves\s+to the nearest free spot/, path)
215
- }
216
- })
217
-
218
- test("drawings.create says it never puts a drawing on a sheet and names the sheet calls", () => {
219
- const summary = summaryOf("presentation.drawings.create")
220
- assert.match(summary, /beside its source view/, summary)
221
- assert.match(summary, /never on a sheet/, summary)
222
- assert.match(summary, /sheets\.place\(sheetId, viewId\)/, summary)
223
- assert.match(summary, /sheets\.placeDrawing\(sheetId, drawingId\)/, summary)
224
- })
225
-
226
- test("sheets.placeDrawing documents fitting, fit: false and one sheet per drawing", () => {
227
- const summary = summaryOf("presentation.sheets.placeDrawing")
228
- assert.match(summary, /scaled down to fit/, summary)
229
- assert.match(summary, /never scaled up/, summary)
230
- assert.match(summary, /`fit: false`/, summary)
231
- assert.match(summary, /naming the\s+drawing, the sheet and the scale/, summary)
232
- assert.match(summary, /without\s+`fit: false`\s+to fit it, or\s+use a larger sheet/, summary)
233
- assert.doesNotMatch(summary, /use\s+`?sheets\.place`?\s+to\s+fit/, summary)
234
- assert.match(summary, /reports the architectural scale/, summary)
235
- })
236
-
237
- // Without a position the host puts the drawing at the sheet centre or the nearest free spot,
238
- // and only centres it over other drawings (reporting overlaps: true) when there is none.
239
- const NO_STACK = /lands\s+on\s+top/
240
- const FREE_SPOT = /nearest\s+free\s+spot/
241
- const CENTRED_WHEN_FULL = /no\s+free\s+spot[^.]*centred[^.]*`overlaps:\s*true`/
242
- const EXPLICIT_POSITION = /`position`[^.]*placed\s+as\s+given[^.]*`overlaps:\s*true`[^.]*not\s+moved/
243
- const DRAWINGS_ONLY = /only\s+other\s+drawings\s+count[^.]*text,\s+images\s+and\s+other\s+shapes/
244
-
245
- const assertPlacementLayout = (summary) => {
246
- assert.doesNotMatch(summary, NO_STACK, summary)
247
- assert.match(summary, FREE_SPOT, summary)
248
- assert.match(summary, CENTRED_WHEN_FULL, summary)
249
- assert.match(summary, EXPLICIT_POSITION, summary)
250
- assert.match(summary, DRAWINGS_ONLY, summary)
251
- }
252
-
253
- test("sheets.placeDrawing says where a drawing goes without a position and when it overlaps", () => {
254
- assertPlacementLayout(summaryOf("presentation.sheets.placeDrawing"))
255
- })
256
-
257
- test("sheets.place says it is the call that puts a view on a sheet and fits it to that sheet", () => {
258
- const summary = summaryOf("presentation.sheets.place")
259
- assert.match(summary, /This is the call that puts a view on a\s+sheet/, summary)
260
- assert.match(summary, /largest\s+standard scale at which it fits inside the target sheet's margin/, summary)
261
- assert.match(summary, /never at a larger scale than the normal view drop/, summary)
262
- assert.match(summary, /3D view[^.]*shrunk\s+evenly/, summary)
263
- assertPlacementLayout(summary)
264
- })
265
-
266
- // The full JSDoc of sheets.place, for the @returns and @throws tags the summary leaves out.
267
- const placeJsDoc = () => {
268
- const source = readFileSync(new URL("../src/api/presentation/sheets.ts", import.meta.url), "utf8")
269
- const end = source.indexOf("public abstract place(")
270
- const start = source.lastIndexOf("/**", end)
271
- return source
272
- .slice(start, end)
273
- .replace(/\{@linkcode (\w+)\}/g, "$1")
274
- .replace(/\s*\*\s*/g, " ")
275
- }
276
-
277
- test("sheets.place documents the scale and rescaled it returns", () => {
278
- const doc = placeJsDoc()
279
- assert.match(doc, /@returns[^@]*`scale` used \(`null` for a 3D view\)/, doc)
280
- assert.match(doc, /@returns[^@]*`rescaled`/, doc)
281
- assert.doesNotMatch(doc, /a host that reports them/, doc)
282
- })
283
-
284
- test("sheets.place documents PRECONDITION_FAILED for a view that does not fit", () => {
285
- const doc = placeJsDoc()
286
- assert.match(doc, /@throws[^@]*`PRECONDITION_FAILED`[^@]*smallest standard scale/, doc)
287
- assert.match(doc, /@throws[^@]*`PRECONDITION_FAILED`[^@]*title block alone/, doc)
288
- })