@snaptrude/plugin-core 0.9.17 → 0.9.18-networks-20261020.1
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/dist/api/analysis/daylight.d.ts +5 -5
- package/dist/api/analysis/solar.d.ts +2 -2
- package/dist/api/core/camera/index.d.ts +29 -0
- package/dist/api/core/camera/index.d.ts.map +1 -1
- package/dist/api/core/geom/create/index.d.ts +7 -1
- package/dist/api/core/geom/create/index.d.ts.map +1 -1
- package/dist/api/core/geom/update/contour.d.ts +8 -2
- package/dist/api/core/geom/update/contour.d.ts.map +1 -1
- package/dist/api/core/storeys/index.d.ts +5 -2
- package/dist/api/core/storeys/index.d.ts.map +1 -1
- package/dist/api/core/zoom/index.d.ts +13 -0
- package/dist/api/core/zoom/index.d.ts.map +1 -1
- package/dist/api/design/bim.d.ts +299 -0
- package/dist/api/design/bim.d.ts.map +1 -0
- package/dist/api/design/boolean/index.d.ts +2 -0
- package/dist/api/design/boolean/index.d.ts.map +1 -1
- package/dist/api/design/catalog.d.ts +320 -0
- package/dist/api/design/catalog.d.ts.map +1 -0
- package/dist/api/design/create/bulk-items.d.ts +65 -25
- package/dist/api/design/create/bulk-items.d.ts.map +1 -1
- package/dist/api/design/create/index.d.ts +272 -99
- package/dist/api/design/create/index.d.ts.map +1 -1
- package/dist/api/design/create/opening-fields.d.ts +13 -12
- package/dist/api/design/create/opening-fields.d.ts.map +1 -1
- package/dist/api/design/delete/index.d.ts +1 -0
- package/dist/api/design/delete/index.d.ts.map +1 -1
- package/dist/api/design/doors/index.d.ts +18 -24
- package/dist/api/design/doors/index.d.ts.map +1 -1
- package/dist/api/design/family.d.ts +801 -28
- package/dist/api/design/family.d.ts.map +1 -1
- package/dist/api/design/index.d.ts +16 -0
- package/dist/api/design/index.d.ts.map +1 -1
- package/dist/api/design/networks.d.ts +1703 -0
- package/dist/api/design/networks.d.ts.map +1 -0
- package/dist/api/design/query/index.d.ts +42 -32
- package/dist/api/design/query/index.d.ts.map +1 -1
- package/dist/api/design/selection/index.d.ts +10 -10
- package/dist/api/design/types/index.d.ts +40 -24
- package/dist/api/design/types/index.d.ts.map +1 -1
- package/dist/api/design/update/index.d.ts +43 -3
- package/dist/api/design/update/index.d.ts.map +1 -1
- package/dist/api/design/windows/index.d.ts +8 -8
- package/dist/api/design/windows/index.d.ts.map +1 -1
- package/dist/api/presentation/aiInspiration.d.ts +10 -10
- package/dist/api/presentation/annotate.d.ts +38 -22
- package/dist/api/presentation/annotate.d.ts.map +1 -1
- package/dist/api/presentation/drawingContract.d.ts +32 -0
- package/dist/api/presentation/drawingContract.d.ts.map +1 -0
- package/dist/api/presentation/drawings.d.ts +3523 -0
- package/dist/api/presentation/drawings.d.ts.map +1 -0
- package/dist/api/presentation/export.d.ts +23 -5
- package/dist/api/presentation/export.d.ts.map +1 -1
- package/dist/api/presentation/index.d.ts +32 -12
- package/dist/api/presentation/index.d.ts.map +1 -1
- package/dist/api/presentation/placedViews.d.ts +106 -6
- package/dist/api/presentation/placedViews.d.ts.map +1 -1
- package/dist/api/presentation/shapes.d.ts +2 -2
- package/dist/api/presentation/sheets.d.ts +175 -19
- package/dist/api/presentation/sheets.d.ts.map +1 -1
- package/dist/api/presentation/viewPresets.d.ts +222 -0
- package/dist/api/presentation/viewPresets.d.ts.map +1 -0
- package/dist/api/presentation/views.d.ts +141 -8
- package/dist/api/presentation/views.d.ts.map +1 -1
- package/dist/api/presentation/walkthroughs.d.ts +6 -6
- package/dist/errors/codes.d.ts +3 -1
- package/dist/errors/codes.d.ts.map +1 -1
- package/dist/index.cjs +3744 -2383
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3621 -2383
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/generate-manifest.mjs +5 -0
- package/src/api/core/camera/index.ts +29 -0
- package/src/api/core/geom/create/index.ts +7 -1
- package/src/api/core/geom/update/contour.ts +8 -2
- package/src/api/core/storeys/index.ts +5 -2
- package/src/api/core/zoom/index.ts +13 -0
- package/src/api/design/bim.ts +210 -0
- package/src/api/design/boolean/index.ts +2 -0
- package/src/api/design/catalog.ts +203 -0
- package/src/api/design/create/bulk-items.ts +57 -17
- package/src/api/design/create/index.ts +288 -120
- package/src/api/design/create/opening-fields.ts +15 -12
- package/src/api/design/delete/index.ts +1 -0
- package/src/api/design/doors/index.ts +58 -42
- package/src/api/design/family.ts +666 -33
- package/src/api/design/index.ts +34 -4
- package/src/api/design/networks.ts +607 -0
- package/src/api/design/query/index.ts +12 -2
- package/src/api/design/types/index.ts +31 -24
- package/src/api/design/update/index.ts +44 -3
- package/src/api/design/windows/index.ts +40 -22
- package/src/api/presentation/annotate.ts +90 -58
- package/src/api/presentation/drawingContract.ts +70 -0
- package/src/api/presentation/drawings.ts +1179 -0
- package/src/api/presentation/export.ts +32 -6
- package/src/api/presentation/index.ts +39 -14
- package/src/api/presentation/placedViews.ts +119 -28
- package/src/api/presentation/sheets.ts +181 -20
- package/src/api/presentation/viewPresets.ts +241 -0
- package/src/api/presentation/views.ts +78 -9
- package/src/errors/codes.ts +7 -0
- package/test/drawingContract.test.mjs +288 -0
- package/test/errors.test.mjs +1 -0
- package/test/presentationViewSettings.test.mjs +85 -0
- package/api-manifest.json +0 -9711
|
@@ -2,7 +2,7 @@ import * as z from "zod"
|
|
|
2
2
|
import { PluginApiReturn } from "../../types"
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Presentation views — read the saved
|
|
5
|
+
* Presentation views — read the saved plan, 3D, and elevation views used to prepare presentations.
|
|
6
6
|
*
|
|
7
7
|
* A **view** is a saved camera/plan: a sheet-ready 2D plan or a saved 3D view.
|
|
8
8
|
* A plugin can list and read views, capture them to images, activate one (move
|
|
@@ -24,10 +24,10 @@ export abstract class PluginPresentationViewsApi {
|
|
|
24
24
|
* List the saved views in the active project.
|
|
25
25
|
*
|
|
26
26
|
* @returns A {@linkcode PluginPresentationViewsListResult} with a `views` array
|
|
27
|
-
* of
|
|
27
|
+
* of plan, 3D, and elevation views (empty when the project has none).
|
|
28
28
|
*
|
|
29
29
|
* @examplePrompt List all the saved views
|
|
30
|
-
* @examplePrompt What
|
|
30
|
+
* @examplePrompt What plan, 3D, and elevation views are in this project?
|
|
31
31
|
* @examplePrompt Which view is currently active?
|
|
32
32
|
* @examplePrompt Get the names of every presentation view
|
|
33
33
|
* @examplePrompt Show me the 3D views I can put on a sheet
|
|
@@ -115,6 +115,10 @@ export abstract class PluginPresentationViewsApi {
|
|
|
115
115
|
* Applies the view's camera, visibility, and storey state to the live scene.
|
|
116
116
|
* Paired with {@linkcode PluginPresentationViewsApi.getActive}.
|
|
117
117
|
*
|
|
118
|
+
* While Present is open this sets the Design frame a view saved in Present
|
|
119
|
+
* is taken from; it never moves the Present sheet canvas; use
|
|
120
|
+
* `presentation.camera` for that.
|
|
121
|
+
*
|
|
118
122
|
* @param viewId - The id of the view to activate.
|
|
119
123
|
* @returns The id of the now-active view.
|
|
120
124
|
* @throws If no view has the given id.
|
|
@@ -138,6 +142,15 @@ export abstract class PluginPresentationViewsApi {
|
|
|
138
142
|
*
|
|
139
143
|
* Creates a camera bookmark (not geometry) from where the camera is now.
|
|
140
144
|
*
|
|
145
|
+
* Works while Present mode is open. Present draws its drawings on the same
|
|
146
|
+
* 3D scene, so a view saved in Present takes its framing (camera, 2D or 3D
|
|
147
|
+
* mode, projection, storey, building) from the Design frame instead: the one
|
|
148
|
+
* the user had when Present opened, or one a plugin set since with
|
|
149
|
+
* `setActive`, `updateSettings({ viewMode })`, `core.camera`, `core.zoom`,
|
|
150
|
+
* `core.storeys.setActive`, `core.storeys.layers.setActive` or
|
|
151
|
+
* `core.buildings.setActive`. It never moves the Present sheet canvas; use
|
|
152
|
+
* `presentation.camera` for that.
|
|
153
|
+
*
|
|
141
154
|
* @param name - Name for the new view (a default is used when omitted).
|
|
142
155
|
* @param options - Optional `kind` (what kind of view to save, default
|
|
143
156
|
* `"view"`) and `storey` (save the view on a specific storey — the storey is
|
|
@@ -171,12 +184,16 @@ export abstract class PluginPresentationViewsApi {
|
|
|
171
184
|
*
|
|
172
185
|
* @param viewId - The id of the view to read settings for.
|
|
173
186
|
* @returns The view's {@linkcode PluginPresentationViewSettings}, or `null` if
|
|
174
|
-
* no view has that id.
|
|
187
|
+
* no view has that id. A plan view also reports its `cutPlaneHeight`,
|
|
188
|
+
* `viewDepthStorey` and `showOverheadObjects` (saved values, or the product
|
|
189
|
+
* defaults for a plan saved before cut ranges existed); other views omit them.
|
|
175
190
|
*
|
|
176
191
|
* @examplePrompt What color mode is the Ground Floor view using?
|
|
177
192
|
* @examplePrompt Get the display settings for this view
|
|
178
193
|
* @examplePrompt Does the site plan view show axes and labels?
|
|
179
194
|
* @examplePrompt Read the background color of the active view
|
|
195
|
+
* @examplePrompt What is the cut height of this plan?
|
|
196
|
+
* @examplePrompt How deep does this plan look below the cut?
|
|
180
197
|
*
|
|
181
198
|
* # Example
|
|
182
199
|
* ```ts
|
|
@@ -226,14 +243,24 @@ export abstract class PluginPresentationViewsApi {
|
|
|
226
243
|
* toggle the view-settings modal runs); on other views the mode is applied
|
|
227
244
|
* the next time the view is activated.
|
|
228
245
|
*
|
|
246
|
+
* `cutPlaneHeight`, `viewDepthStorey` and `showOverheadObjects` set the plan's
|
|
247
|
+
* cut range and apply to **plan views only** (patching them on a 3D, elevation
|
|
248
|
+
* or site-plan view is rejected). `cutPlaneHeight` is in engine units, the same
|
|
249
|
+
* unit as `story.height`, and must be greater than 0 and less than the plan
|
|
250
|
+
* storey's height. `viewDepthStorey` must be an existing storey at or below the
|
|
251
|
+
* plan's storey. A sparse patch leaves the other two range fields as they
|
|
252
|
+
* were; on the active plan the canvas redraws live.
|
|
253
|
+
*
|
|
229
254
|
* @param viewId - The id of the view to update.
|
|
230
255
|
* @param settings - A partial {@linkcode PluginPresentationViewSettings}; any
|
|
231
256
|
* omitted field is left unchanged.
|
|
232
257
|
* @returns The updated view's `id`.
|
|
233
258
|
* @throws If writes are disabled, no view has the given id, the patch is
|
|
234
259
|
* contradictory (`showLabels: false` with a non-empty `labels`, or
|
|
235
|
-
* `showLabels: true` with an explicitly empty `labels` list),
|
|
236
|
-
* `viewMode: "perspective"` is patched on a 2D plan view
|
|
260
|
+
* `showLabels: true` with an explicitly empty `labels` list),
|
|
261
|
+
* `viewMode: "perspective"` is patched on a 2D plan view, a plan-range
|
|
262
|
+
* field is patched on a view that is not a plan, or the cut height / depth
|
|
263
|
+
* is out of range.
|
|
237
264
|
*
|
|
238
265
|
* @examplePrompt Switch this view to monochrome color mode
|
|
239
266
|
* @examplePrompt Turn off the axis in the Ground Floor view
|
|
@@ -241,6 +268,9 @@ export abstract class PluginPresentationViewsApi {
|
|
|
241
268
|
* @examplePrompt Show area labels on this view
|
|
242
269
|
* @examplePrompt Enable hidden-line mode for the section view
|
|
243
270
|
* @examplePrompt Make the 3D view orthographic instead of perspective
|
|
271
|
+
* @examplePrompt Raise the cut plane of the Ground Floor plan to 1.8 m
|
|
272
|
+
* @examplePrompt Show the basement below in this plan
|
|
273
|
+
* @examplePrompt Hide the overhead objects in this plan
|
|
244
274
|
*
|
|
245
275
|
* # Example
|
|
246
276
|
* ```ts
|
|
@@ -252,6 +282,12 @@ export abstract class PluginPresentationViewsApi {
|
|
|
252
282
|
* await snaptrude.presentation.views.updateSettings("view_123", {
|
|
253
283
|
* viewMode: "isometric",
|
|
254
284
|
* })
|
|
285
|
+
* // set a plan's range: cut 1.2 m up, show the basement below, no overhead
|
|
286
|
+
* await snaptrude.presentation.views.updateSettings("plan_1", {
|
|
287
|
+
* cutPlaneHeight: 4.72,
|
|
288
|
+
* viewDepthStorey: -1,
|
|
289
|
+
* showOverheadObjects: false,
|
|
290
|
+
* })
|
|
255
291
|
* ```
|
|
256
292
|
*/
|
|
257
293
|
public abstract updateSettings(
|
|
@@ -315,13 +351,36 @@ export abstract class PluginPresentationViewsApi {
|
|
|
315
351
|
}
|
|
316
352
|
|
|
317
353
|
/**
|
|
318
|
-
* The
|
|
354
|
+
* The rendering category of a saved view. Existing plan/3D values are preserved;
|
|
355
|
+
* elevations are identified independently of their orthographic 3D camera.
|
|
319
356
|
*/
|
|
320
|
-
export const PluginPresentationViewType = z.enum(["2d", "3d"])
|
|
357
|
+
export const PluginPresentationViewType = z.enum(["2d", "3d", "elevation"])
|
|
321
358
|
export type PluginPresentationViewType = z.infer<
|
|
322
359
|
typeof PluginPresentationViewType
|
|
323
360
|
>
|
|
324
361
|
|
|
362
|
+
/** The native semantic kind persisted for a saved view. */
|
|
363
|
+
export const PluginPresentationViewKind = z.enum([
|
|
364
|
+
"plan",
|
|
365
|
+
"sitePlan",
|
|
366
|
+
"threeD",
|
|
367
|
+
"elevation",
|
|
368
|
+
])
|
|
369
|
+
export type PluginPresentationViewKind = z.infer<
|
|
370
|
+
typeof PluginPresentationViewKind
|
|
371
|
+
>
|
|
372
|
+
|
|
373
|
+
/** Cardinal direction for an elevation; non-elevation views return `null`. */
|
|
374
|
+
export const PluginPresentationElevationDirection = z.enum([
|
|
375
|
+
"north",
|
|
376
|
+
"east",
|
|
377
|
+
"south",
|
|
378
|
+
"west",
|
|
379
|
+
])
|
|
380
|
+
export type PluginPresentationElevationDirection = z.infer<
|
|
381
|
+
typeof PluginPresentationElevationDirection
|
|
382
|
+
>
|
|
383
|
+
|
|
325
384
|
/**
|
|
326
385
|
* A saved presentation view.
|
|
327
386
|
*
|
|
@@ -329,7 +388,9 @@ export type PluginPresentationViewType = z.infer<
|
|
|
329
388
|
* |---|---|---|
|
|
330
389
|
* | `id` | `string` | Unique view id |
|
|
331
390
|
* | `name` | `string` | Display name |
|
|
332
|
-
* | `type` | `"2d" \| "3d"` |
|
|
391
|
+
* | `type` | `"2d" \| "3d" \| "elevation"` | Rendering category; elevations are distinct from ordinary 3D views |
|
|
392
|
+
* | `kind` | {@linkcode PluginPresentationViewKind} | Native semantic view kind |
|
|
393
|
+
* | `direction` | {@linkcode PluginPresentationElevationDirection}` \| null` | Cardinal direction for elevations; otherwise `null` |
|
|
333
394
|
* | `isActive` | `boolean` | Whether this view is the currently active view |
|
|
334
395
|
* | `storey` | `number \| null` | The storey this view was saved on (`null` on older views saved without one) |
|
|
335
396
|
*/
|
|
@@ -337,6 +398,8 @@ export const PluginPresentationView = z.object({
|
|
|
337
398
|
id: z.string(),
|
|
338
399
|
name: z.string(),
|
|
339
400
|
type: PluginPresentationViewType,
|
|
401
|
+
kind: PluginPresentationViewKind,
|
|
402
|
+
direction: PluginPresentationElevationDirection.nullable(),
|
|
340
403
|
isActive: z.boolean(),
|
|
341
404
|
storey: z.number().nullable(),
|
|
342
405
|
})
|
|
@@ -533,6 +596,9 @@ export type PluginPresentationViewMode = z.infer<
|
|
|
533
596
|
* | `showEdges` | `boolean` | Whether edges are shown; `false` = hidden-line rendering |
|
|
534
597
|
* | `showLabels` | `boolean` | Master label switch — whether on-canvas labels are shown at all. `true` always pairs with a non-empty `labels` selection (labels render only while the selection is non-empty; there is no labels-on-with-empty-selection state) |
|
|
535
598
|
* | `labels` | {@linkcode PluginPresentationViewLabel}`[]` | Which label categories are selected. When `showLabels` is `false` the view stores no selection and this reads `[]` |
|
|
599
|
+
* | `cutPlaneHeight` | `number`? | **Plan views only.** Height of the plan's cut plane above its storey base, in engine units (the unit `story.height` uses). Absent on 3D and elevation views |
|
|
600
|
+
* | `viewDepthStorey` | `number`? | **Plan views only.** The lowest storey the plan shows below the cut; defaults to the plan's own storey. Absent on 3D and elevation views |
|
|
601
|
+
* | `showOverheadObjects` | `boolean`? | **Plan views only.** Whether objects above the cut are drawn as dashed overhead linework. Absent on 3D and elevation views |
|
|
536
602
|
*/
|
|
537
603
|
export const PluginPresentationViewSettings = z.object({
|
|
538
604
|
backgroundColor: z.string(),
|
|
@@ -542,6 +608,9 @@ export const PluginPresentationViewSettings = z.object({
|
|
|
542
608
|
showEdges: z.boolean(),
|
|
543
609
|
showLabels: z.boolean(),
|
|
544
610
|
labels: z.array(PluginPresentationViewLabel),
|
|
611
|
+
cutPlaneHeight: z.number().finite().positive().optional(),
|
|
612
|
+
viewDepthStorey: z.number().int().optional(),
|
|
613
|
+
showOverheadObjects: z.boolean().optional(),
|
|
545
614
|
})
|
|
546
615
|
export type PluginPresentationViewSettings = z.infer<
|
|
547
616
|
typeof PluginPresentationViewSettings
|
package/src/errors/codes.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* (`HANDLE_INVALID`) so error responses cannot be used as an existence /
|
|
8
8
|
* ownership oracle (PLUGIN_HANDLE_MIGRATION_HLD §6). `HANDLE_KIND_MISMATCH`
|
|
9
9
|
* stays distinct because the kind is information the caller already holds.
|
|
10
|
+
* `HANDLE_STALE` is the one deliberate exception: a handle the caller
|
|
11
|
+
* legitimately held whose entity was since retired (deleted or rebuilt).
|
|
10
12
|
*/
|
|
11
13
|
export const PLUGIN_ERROR_CODES = [
|
|
12
14
|
"VALIDATION",
|
|
@@ -34,6 +36,7 @@ export const PLUGIN_ERROR_CODES = [
|
|
|
34
36
|
"PROPOSAL_SWITCH_BLOCKED",
|
|
35
37
|
"INTERNAL",
|
|
36
38
|
"UNKNOWN",
|
|
39
|
+
"HANDLE_STALE",
|
|
37
40
|
] as const
|
|
38
41
|
|
|
39
42
|
export type PluginErrorCode = (typeof PLUGIN_ERROR_CODES)[number]
|
|
@@ -85,6 +88,10 @@ export const CODE_META: Record<PluginErrorCode, CodeMeta> = {
|
|
|
85
88
|
hintTemplate: "Re-query the entity; do not cache handles across undo/reload.",
|
|
86
89
|
},
|
|
87
90
|
HANDLE_KIND_MISMATCH: { category: "handle" },
|
|
91
|
+
HANDLE_STALE: {
|
|
92
|
+
category: "handle",
|
|
93
|
+
hintTemplate: "The entity was deleted or rebuilt; re-query it for a fresh handle.",
|
|
94
|
+
},
|
|
88
95
|
RESOURCE_QUOTA_EXCEEDED: {
|
|
89
96
|
category: "quota",
|
|
90
97
|
hintTemplate: "Release unused handles (core.handles.release) or use scopes.",
|
|
@@ -0,0 +1,288 @@
|
|
|
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
|
+
})
|
package/test/errors.test.mjs
CHANGED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Node built-in test runner suite for the presentation view settings schemas.
|
|
2
|
+
// Run: node --test test/
|
|
3
|
+
// Imports the BUILT package (dist) — run `pnpm build` first.
|
|
4
|
+
import test from "node:test"
|
|
5
|
+
import assert from "node:assert/strict"
|
|
6
|
+
import {
|
|
7
|
+
PluginPresentationViewSettings,
|
|
8
|
+
PluginPresentationViewsGetSettingsResult,
|
|
9
|
+
PluginPresentationViewsUpdateSettingsArgs,
|
|
10
|
+
} from "../dist/index.js"
|
|
11
|
+
|
|
12
|
+
const base = {
|
|
13
|
+
backgroundColor: "#ffffff",
|
|
14
|
+
colorMode: "monochrome",
|
|
15
|
+
viewMode: "isometric",
|
|
16
|
+
showAxis: true,
|
|
17
|
+
showEdges: true,
|
|
18
|
+
showLabels: false,
|
|
19
|
+
labels: [],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test("a plan's settings read back its cut range", () => {
|
|
23
|
+
const range = { cutPlaneHeight: 5.9055, viewDepthStorey: -1, showOverheadObjects: false }
|
|
24
|
+
assert.deepEqual(PluginPresentationViewsGetSettingsResult.parse({ ...base, ...range }), {
|
|
25
|
+
...base,
|
|
26
|
+
...range,
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test("a view that is not a plan omits the range, and legacy callers still parse", () => {
|
|
31
|
+
const parsed = PluginPresentationViewSettings.parse(base)
|
|
32
|
+
for (const key of ["cutPlaneHeight", "viewDepthStorey", "showOverheadObjects"])
|
|
33
|
+
assert.equal(key in parsed, false)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test("a range patch survives the update args instead of being stripped", () => {
|
|
37
|
+
const parsed = PluginPresentationViewsUpdateSettingsArgs.parse({
|
|
38
|
+
id: "plan_1",
|
|
39
|
+
settings: { cutPlaneHeight: 4.72, viewDepthStorey: 0, showOverheadObjects: false },
|
|
40
|
+
})
|
|
41
|
+
assert.deepEqual(parsed.settings, {
|
|
42
|
+
cutPlaneHeight: 4.72,
|
|
43
|
+
viewDepthStorey: 0,
|
|
44
|
+
showOverheadObjects: false,
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test("depth 0 is a real storey; depth null is rejected", () => {
|
|
49
|
+
const ground = PluginPresentationViewsUpdateSettingsArgs.parse({
|
|
50
|
+
id: "plan_1",
|
|
51
|
+
settings: { viewDepthStorey: 0 },
|
|
52
|
+
})
|
|
53
|
+
assert.equal(ground.settings.viewDepthStorey, 0)
|
|
54
|
+
assert.throws(() =>
|
|
55
|
+
PluginPresentationViewsUpdateSettingsArgs.parse({
|
|
56
|
+
id: "plan_1",
|
|
57
|
+
settings: { viewDepthStorey: null },
|
|
58
|
+
}),
|
|
59
|
+
)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test("a range patch rejects values no plan could use", () => {
|
|
63
|
+
for (const settings of [
|
|
64
|
+
{ cutPlaneHeight: 0 },
|
|
65
|
+
{ cutPlaneHeight: -3 },
|
|
66
|
+
{ cutPlaneHeight: Number.POSITIVE_INFINITY },
|
|
67
|
+
{ cutPlaneHeight: Number.NaN },
|
|
68
|
+
{ viewDepthStorey: 1.5 },
|
|
69
|
+
{ showOverheadObjects: "no" },
|
|
70
|
+
])
|
|
71
|
+
assert.throws(
|
|
72
|
+
() => PluginPresentationViewsUpdateSettingsArgs.parse({ id: "plan_1", settings }),
|
|
73
|
+
undefined,
|
|
74
|
+
JSON.stringify(settings),
|
|
75
|
+
)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test("the existing label contradiction checks still apply beside a range patch", () => {
|
|
79
|
+
assert.throws(() =>
|
|
80
|
+
PluginPresentationViewsUpdateSettingsArgs.parse({
|
|
81
|
+
id: "plan_1",
|
|
82
|
+
settings: { showLabels: false, labels: ["areas"], cutPlaneHeight: 10 },
|
|
83
|
+
}),
|
|
84
|
+
)
|
|
85
|
+
})
|