@twinfinity/printing 6.0.1-ci.28952-beta → 6.0.2-beta

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 (82) hide show
  1. package/README.md +3 -3
  2. package/dist/BuildInfo.js +3 -3
  3. package/dist/BuildInfo.js.map +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/layout/analyzeTemplate.js.map +1 -1
  6. package/dist/layout/clipUtils.js.map +1 -1
  7. package/dist/layout/colorConstants.js.map +1 -1
  8. package/dist/layout/elementKinds.js.map +1 -1
  9. package/dist/layout/exportDrawing.js.map +1 -1
  10. package/dist/layout/filters.js.map +1 -1
  11. package/dist/layout/geometryConstants.js.map +1 -1
  12. package/dist/layout/index.js.map +1 -1
  13. package/dist/layout/itemBuilders.js.map +1 -1
  14. package/dist/layout/labelsRender.js.map +1 -1
  15. package/dist/layout/legendConstants.js.map +1 -1
  16. package/dist/layout/legendRender.js.map +1 -1
  17. package/dist/layout/legendRows.js.map +1 -1
  18. package/dist/layout/legendUtils.js.map +1 -1
  19. package/dist/layout/metadata.js.map +1 -1
  20. package/dist/layout/model.js.map +1 -1
  21. package/dist/layout/options.js.map +1 -1
  22. package/dist/layout/presets.js.map +1 -1
  23. package/dist/layout/qrRender.js.map +1 -1
  24. package/dist/layout/renderDrawing.js.map +1 -1
  25. package/dist/layout/renderUtils.js.map +1 -1
  26. package/dist/layout/sectionValidation.js.map +1 -1
  27. package/dist/layout/templateVariables.js.map +1 -1
  28. package/dist/layout/typeGuards.js.map +1 -1
  29. package/dist/layout/types.js.map +1 -1
  30. package/dist/layout/utils.js.map +1 -1
  31. package/dist/layout/validation.js.map +1 -1
  32. package/dist/layout/validationHelpers.js.map +1 -1
  33. package/dist/layout/validationUtils.js.map +1 -1
  34. package/dist/layout/viewportPrep.js.map +1 -1
  35. package/dist/layout/viewportRender.js.map +1 -1
  36. package/dist/layout/viewportUtils.js.map +1 -1
  37. package/dist/layout/warnings.js.map +1 -1
  38. package/dist/printToPdf.js.map +1 -1
  39. package/dist/printToSvg.js.map +1 -1
  40. package/dist/sections.js.map +1 -1
  41. package/dist/types.js.map +1 -1
  42. package/package.json +2 -2
  43. package/src/BuildInfo.ts +41 -41
  44. package/src/index.ts +10 -10
  45. package/src/layout/analyzeTemplate.ts +218 -218
  46. package/src/layout/clipUtils.ts +193 -193
  47. package/src/layout/colorConstants.ts +16 -16
  48. package/src/layout/elementKinds.ts +35 -35
  49. package/src/layout/exportDrawing.ts +206 -206
  50. package/src/layout/filters.ts +80 -80
  51. package/src/layout/geometryConstants.ts +11 -11
  52. package/src/layout/index.ts +671 -671
  53. package/src/layout/itemBuilders.ts +159 -159
  54. package/src/layout/labelsRender.ts +170 -170
  55. package/src/layout/legendConstants.ts +11 -11
  56. package/src/layout/legendRender.ts +543 -543
  57. package/src/layout/legendRows.ts +62 -62
  58. package/src/layout/legendUtils.ts +144 -144
  59. package/src/layout/metadata.ts +210 -210
  60. package/src/layout/model.ts +372 -372
  61. package/src/layout/options.ts +17 -17
  62. package/src/layout/presets.ts +51 -51
  63. package/src/layout/qrRender.ts +126 -126
  64. package/src/layout/qrcode.d.ts +2 -2
  65. package/src/layout/renderDrawing.ts +710 -710
  66. package/src/layout/renderUtils.ts +138 -138
  67. package/src/layout/sectionValidation.ts +2 -2
  68. package/src/layout/templateVariables.ts +317 -317
  69. package/src/layout/typeGuards.ts +9 -9
  70. package/src/layout/types.ts +835 -835
  71. package/src/layout/utils.ts +85 -85
  72. package/src/layout/validation.ts +392 -392
  73. package/src/layout/validationHelpers.ts +59 -59
  74. package/src/layout/validationUtils.ts +6 -6
  75. package/src/layout/viewportPrep.ts +351 -351
  76. package/src/layout/viewportRender.ts +442 -442
  77. package/src/layout/viewportUtils.ts +50 -50
  78. package/src/layout/warnings.ts +3 -3
  79. package/src/printToPdf.ts +108 -108
  80. package/src/printToSvg.ts +188 -188
  81. package/src/sections.ts +1019 -1019
  82. package/src/types.ts +49 -49
@@ -1,835 +1,835 @@
1
- import type { SectionPath, SectionLabel } from '@twinfinity/geometry';
2
-
3
- export type PagePresetKey = 'a1' | 'a3' | 'a4';
4
- export type PageOrientation = 'portrait' | 'landscape';
5
-
6
- export interface PageSpec {
7
- widthMm: number;
8
- heightMm: number;
9
- marginMm?: number;
10
- backgroundColor?: string;
11
- }
12
-
13
- export interface TextStyle {
14
- fontFamily: string;
15
- fontSizeMm: number;
16
- fontWeight?: 'normal' | 'bold' | 'lighter' | 'bolder' | string;
17
- color?: string;
18
- letterSpacingMm?: number;
19
- lineHeightMm?: number;
20
- }
21
-
22
- export interface StyleMap {
23
- [styleName: string]: TextStyle;
24
- }
25
-
26
- export interface AssetSpec {
27
- id: string;
28
- src?: string;
29
- content?: string;
30
- }
31
-
32
- export type WhereCondition = Record<string, string | number | boolean | Array<string | number>>;
33
-
34
- export interface ClipConfig {
35
- class?: string | string[];
36
- where: WhereCondition;
37
- expandByM?: number;
38
- clipLabels?: boolean;
39
- stroke?: string;
40
- strokeWidthMm?: number;
41
- strokeOpacity?: number;
42
- fill?: string;
43
- fillOpacity?: number;
44
- }
45
-
46
- /**
47
- * Template-driven fill rule for a viewport.
48
- *
49
- * Fill rules control which products get filled polygons in the drawing.
50
- * When no `fills` array is present on a viewport, no fills are rendered.
51
- *
52
- * Example:
53
- * ```json
54
- * { "class": "space", "fill": "sample", "fillOpacity": 0.35 }
55
- * { "class": "walls", "fill": "#cccccc", "fillOpacity": 0.5 }
56
- * ```
57
- */
58
- export interface FillRule {
59
- /** IFC class filter (e.g. "space", "walls"). Matches ifcClass or ifcClassId. */
60
- class?: string | string[];
61
- /** Property-based filter (same as highlight/clip `where`). */
62
- where?: WhereCondition;
63
- /** Fill color: CSS hex (e.g. "#80c8b4"), "sample" to read from 3D model material, or "legend" to use colors assigned by legend. */
64
- fill: string;
65
- /** Fill opacity (0–1). Default: 0.35. */
66
- fillOpacity?: number;
67
- /** When true, only apply fill to items whose centroid is spatially inside the clip polygon.
68
- * Useful when expandByM reveals adjacent spaces that shouldn't be filled.
69
- * @default false */
70
- matchClipFilter?: boolean;
71
- }
72
-
73
- export interface HighlightConfig {
74
- class?: string | string[];
75
- where: WhereCondition;
76
- /** Only highlight items that also appear in the referenced viewport (i.e., intersect both planes). */
77
- intersectsViewport?: string;
78
- stroke?: string;
79
- strokeWidthMm?: number;
80
- strokeOpacity?: number;
81
- fill?: string;
82
- fillOpacity?: number;
83
- }
84
-
85
- export type LayoutElement =
86
- | ViewportElement
87
- | TextElement
88
- | ImageElement
89
- | LineElement
90
- | RectElement
91
- | CircleElement
92
- | PolylineElement
93
- | PolygonElement
94
- | ScaleBarElement
95
- | QrElement
96
- | LabelsElement
97
- | LegendElement
98
- | GroupElement;
99
-
100
- export interface BaseElement {
101
- kind: LayoutElement['kind'];
102
- id: string;
103
- zIndex?: number;
104
- }
105
-
106
- /** Style for a single line category in a viewport. */
107
- export interface ViewportLineStyle {
108
- /** Line width in mm. */
109
- widthMm?: number;
110
- /** Line color (CSS color string). */
111
- color?: string;
112
- /** SVG dash array (e.g. "4 2"). */
113
- dash?: string;
114
- /** Whether this category is rendered. Default: true. */
115
- enabled?: boolean;
116
- }
117
-
118
- /**
119
- * Per-category line styles for a viewport.
120
- *
121
- * Resolution order: category-specific → `default` → built-in defaults.
122
- *
123
- * Example:
124
- * ```json
125
- * {
126
- * "default": { "widthMm": 0.01, "color": "#1f2937" },
127
- * "cutLines": { "widthMm": 0.02, "color": "#000000" },
128
- * "hiddenLines": { "widthMm": 0.005, "color": "#9ca3af", "dash": "4 2" }
129
- * }
130
- * ```
131
- */
132
- export interface ViewportLineStyles {
133
- /** Fallback style for any category not explicitly listed. */
134
- default?: ViewportLineStyle;
135
- cutLines?: ViewportLineStyle;
136
- visibleLines?: ViewportLineStyle;
137
- hiddenLines?: ViewportLineStyle;
138
- silhouettes?: ViewportLineStyle;
139
- featureEdges?: ViewportLineStyle;
140
- stairArrows?: ViewportLineStyle;
141
- outlineVisible?: ViewportLineStyle;
142
- outlineHidden?: ViewportLineStyle;
143
- }
144
-
145
- /**
146
- * Include filter for viewport geometry.
147
- *
148
- * Can be:
149
- * - String or string array: class names only
150
- * - Object with class + where: property-based filtering
151
- * - Preset reference: { preset: "presetName" }
152
- *
153
- * Examples:
154
- * ```json
155
- * "includeClasses": ["space", "walls"]
156
- * "includeClasses": [{ "class": "space", "where": { "BIP.spacetype": "ROOM" } }]
157
- * "includeClasses": { "preset": "defaultBuilding" }
158
- * ```
159
- */
160
- export type IncludeClassesFilter =
161
- | string
162
- | string[]
163
- | { class?: string | string[]; where?: WhereCondition }
164
- | { preset: string };
165
-
166
- /**
167
- * Resolved include filter (after preset expansion).
168
- * Either a simple class name or a class+where filter.
169
- */
170
- export type ResolvedIncludeFilter = string | { class?: string | string[]; where: WhereCondition };
171
-
172
- export interface ViewportElement extends BaseElement {
173
- kind: 'viewport';
174
- xMm: number;
175
- yMm: number;
176
- widthMm: number;
177
- heightMm: number;
178
- scaleMode?: 'fit' | 'fitAndRound' | 'fixed';
179
- fixedScaleDenom?: number;
180
- preferredScales?: number[];
181
- scalePreset?: string;
182
- align?: 'center' | 'topLeft';
183
- rotationDeg?: number;
184
- includeClasses?: IncludeClassesFilter | IncludeClassesFilter[];
185
- /**
186
- * Per-category line styles. When set, overrides `lineWidthMm`/`lineColor`.
187
- * Use `lineStyles.default` for a single style across all categories,
188
- * or specify individual categories (cutLines, hiddenLines, etc.).
189
- */
190
- lineStyles?: ViewportLineStyles;
191
- /** Simple shorthand: default line width for all categories. Overridden by `lineStyles`. */
192
- lineWidthMm?: number;
193
- /** Simple shorthand: default line color for all categories. Overridden by `lineStyles`. */
194
- lineColor?: string;
195
- /**
196
- * Fill rules controlling which products get filled polygons.
197
- * When absent, no fills are rendered (default: no fill).
198
- */
199
- fills?: FillRule[];
200
- northArrow?: { assetId: string; sizeMm?: number; offsetXMm?: number; offsetYMm?: number; rotationDeg?: number };
201
- border?: { stroke?: string; strokeWidthMm?: number; dashed?: boolean };
202
- clip?: ClipConfig;
203
- highlight?: HighlightConfig;
204
-
205
- // Section configuration (overrides app defaults if specified)
206
- /**
207
- * Override cutting plane height for horizontal plans (Y coordinate).
208
- * If omitted, uses app default. Shorthand for planeNormal=[0,1,0], planePosition=cutHeight.
209
- */
210
- cutHeight?: number;
211
- /**
212
- * Override depth above/below cutting plane. If omitted, uses app default.
213
- * Only applicable for SLAB mode (PLANE mode doesn't use depth).
214
- */
215
- depth?: { above?: number; below?: number };
216
- /**
217
- * Plane normal vector [x, y, z]. Automatically normalized.
218
- * Takes precedence over cutHeight if both are specified.
219
- * Examples:
220
- * - [1, 0, 0] = vertical section perpendicular to X axis
221
- * - [0, 1, 0] = horizontal plan (same as using cutHeight)
222
- * - [0, 0, 1] = vertical section perpendicular to Z axis
223
- * - [1, 0, 1] = diagonal section (will be normalized)
224
- */
225
- planeNormal?: [number, number, number];
226
- /**
227
- * Distance from origin along the (normalized) plane normal.
228
- * Used with planeNormal to define the cutting plane.
229
- * The plane passes through the point: normalize(planeNormal) * planePosition.
230
- */
231
- planePosition?: number;
232
- /**
233
- * Section computation mode: 'slab' or 'plane'.
234
- * - 'slab': Optimized for horizontal floor plans. Uses depth above/below to include geometry.
235
- * - 'plane': Arbitrary plane cuts (vertical sections, diagonal sections). Computes exact intersection.
236
- *
237
- * If omitted, mode is auto-detected from plane orientation:
238
- * - Horizontal planes (planeNormal ≈ Y-axis) → 'slab'
239
- * - Vertical or diagonal planes → 'plane'
240
- */
241
- mode?: 'slab' | 'plane';
242
- }
243
-
244
- export interface TextElement extends BaseElement {
245
- kind: 'text';
246
- xMm: number;
247
- yMm: number;
248
- textStyle?: string;
249
- text: string;
250
- maxWidthMm?: number;
251
- align?: 'left' | 'center' | 'right';
252
- }
253
-
254
- export interface ImageElement extends BaseElement {
255
- kind: 'image';
256
- xMm: number;
257
- yMm: number;
258
- widthMm: number;
259
- heightMm: number;
260
- assetId: string;
261
- fit?: 'contain' | 'cover' | 'stretch';
262
- }
263
-
264
- export interface LineElement extends BaseElement {
265
- kind: 'line';
266
- x1Mm: number;
267
- y1Mm: number;
268
- x2Mm: number;
269
- y2Mm: number;
270
- stroke?: string;
271
- strokeWidthMm?: number;
272
- dashed?: boolean;
273
- }
274
-
275
- export interface RectElement extends BaseElement {
276
- kind: 'rect';
277
- xMm: number;
278
- yMm: number;
279
- widthMm: number;
280
- heightMm: number;
281
- stroke?: string;
282
- strokeWidthMm?: number;
283
- fill?: string;
284
- dashed?: boolean;
285
- }
286
-
287
- export interface CircleElement extends BaseElement {
288
- kind: 'circle';
289
- cxMm: number;
290
- cyMm: number;
291
- rMm: number;
292
- stroke?: string;
293
- strokeWidthMm?: number;
294
- fill?: string;
295
- dashed?: boolean;
296
- }
297
-
298
- export interface PointMm {
299
- xMm: number;
300
- yMm: number;
301
- }
302
-
303
- export interface PolylineElement extends BaseElement {
304
- kind: 'polyline';
305
- points: PointMm[];
306
- stroke: string;
307
- strokeWidthMm: number;
308
- dashed?: boolean;
309
- }
310
-
311
- export interface PolygonElement extends BaseElement {
312
- kind: 'polygon';
313
- points: PointMm[];
314
- stroke?: string;
315
- strokeWidthMm?: number;
316
- fill?: string;
317
- dashed?: boolean;
318
- }
319
-
320
- export interface ScaleBarElement extends BaseElement {
321
- kind: 'scaleBar';
322
- xMm: number;
323
- yMm: number;
324
- maxLengthMm: number;
325
- unit?: string;
326
- segments?: number;
327
- labelEvery?: number;
328
- stroke?: string;
329
- strokeWidthMm?: number;
330
- textStyle?: string;
331
- viewport?: string;
332
- }
333
-
334
- export interface QrElement extends BaseElement {
335
- kind: 'qr';
336
- xMm: number;
337
- yMm: number;
338
- sizeMm: number;
339
- role?: 'main' | 'anchor' | 'custom';
340
- anchorCorner?: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right';
341
- viewport?: string;
342
- dataTemplate?: string;
343
- }
344
-
345
- export interface LabelsElement extends BaseElement {
346
- kind: 'labels';
347
- viewport: string;
348
- class?: string | string[];
349
- where?: WhereCondition;
350
- text: string;
351
- textStyle?: string;
352
- align?: 'left' | 'center' | 'right';
353
- maxWidthMm?: number;
354
- /** When true, only show labels for items whose position is spatially inside the clip polygon.
355
- * @default false */
356
- matchClipFilter?: boolean;
357
- }
358
-
359
- export type LegendAnchor = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
360
-
361
- export interface LegendColumn {
362
- id: string;
363
- label: string;
364
- property: string;
365
- format?: { digits?: number; prefix?: string; suffix?: string };
366
- align?: 'left' | 'center' | 'right';
367
- minWidthMm?: number;
368
- maxWidthMm?: number;
369
- /**
370
- * Enable summary (total) calculation for this column.
371
- * When true, the summary row will show the sum of all values in this column.
372
- * Only works for numerical columns.
373
- * @default false
374
- */
375
- summary?: boolean;
376
- }
377
-
378
- export interface LegendElement extends BaseElement {
379
- kind: 'legend';
380
- viewport: string;
381
- class?: string | string[];
382
- where?: WhereCondition;
383
- clipToViewport?: boolean;
384
- anchor?: LegendAnchor;
385
- anchorPositionMm?: { xMm: number; yMm: number };
386
- marginMm?: number;
387
- textStyle?: string;
388
- showColorColumn?: boolean;
389
- colorColumnWidthMm?: number;
390
- cellPaddingMm?: number;
391
- borderStrokeWidthMm?: number;
392
- borderStrokeColor?: string;
393
- gridStrokeWidthMm?: number;
394
- gridStrokeColor?: string;
395
- headerStrokeWidthMm?: number;
396
- headerStrokeColor?: string;
397
- columns: LegendColumn[];
398
- /**
399
- * Summary row configuration.
400
- * Displays a totals row at the bottom of the legend.
401
- * Columns with `summary: true` will show their sum in the summary row.
402
- *
403
- * @example
404
- * ```json
405
- * {
406
- * "summary": {
407
- * "enabled": true,
408
- * "label": "Total"
409
- * },
410
- * "columns": [
411
- * { "id": "name", "label": "Room", "property": "BIP.spacename" },
412
- * { "id": "area", "label": "Area", "property": "BIP.area", "summary": true }
413
- * ]
414
- * }
415
- * ```
416
- */
417
- summary?: {
418
- /** Enable summary row. */
419
- enabled: boolean;
420
- /** Label for first column in summary row. Defaults to "Total". */
421
- label?: string;
422
- };
423
- /**
424
- * Group legend rows by a specific column property.
425
- * When enabled, rows with identical values in the groupBy column are consolidated into a single row.
426
- * Numerical columns are summed across grouped rows; non-numerical columns take the first value.
427
- *
428
- * @example
429
- * ```json
430
- * {
431
- * "groupBy": "BIP.spacename",
432
- * "columns": [
433
- * { "id": "name", "label": "Room", "property": "BIP.spacename" },
434
- * { "id": "area", "label": "Area", "property": "BIP.area", "format": { "digits": 2 } }
435
- * ]
436
- * }
437
- * ```
438
- * This groups all rooms with the same name and sums their areas.
439
- */
440
- groupBy?: string;
441
- /**
442
- * Assign colors to legend rows and apply them to the referenced viewport.
443
- * When enabled, generates colors for each legend row (or group if groupBy is used).
444
- * The viewport can use these colors by setting fill: "legend" in its fill rules.
445
- *
446
- * @example
447
- * ```json
448
- * {
449
- * "assignColors": {
450
- * "enabled": true,
451
- * "palette": "auto"
452
- * },
453
- * "groupBy": "BIP.spacename"
454
- * }
455
- * ```
456
- * Generates distinct colors for each room type, applied to matching spaces in viewport.
457
- */
458
- assignColors?: {
459
- /** Enable color assignment to viewport items. */
460
- enabled: boolean;
461
- /** Color palette: "auto" for auto-generated colors, or array of hex colors. */
462
- palette?: 'auto' | string[];
463
- };
464
- }
465
-
466
- export interface GroupTransform {
467
- translateXMm?: number;
468
- translateYMm?: number;
469
- rotateDeg?: number;
470
- scaleX?: number;
471
- scaleY?: number;
472
- }
473
-
474
- export interface GroupClipRect {
475
- type: 'rect';
476
- xMm: number;
477
- yMm: number;
478
- widthMm: number;
479
- heightMm: number;
480
- }
481
-
482
- export interface GroupElement extends BaseElement {
483
- kind: 'group';
484
- opacity?: number;
485
- transform?: GroupTransform;
486
- clip?: GroupClipRect;
487
- children: LayoutElement[];
488
- }
489
-
490
- /**
491
- * Template variable definition.
492
- *
493
- * Variables allow templates to be parameterized and reused with different values.
494
- * All variables MUST have a default value to ensure templates work without user input.
495
- *
496
- * Variables are referenced in templates using {{vars.variableName}} syntax.
497
- *
498
- * Example:
499
- * ```json
500
- * {
501
- * "variables": {
502
- * "hyresobjektsnummer": {
503
- * "type": "string",
504
- * "default": "2022-F2008",
505
- * "label": "Hyresobjekt Number",
506
- * "description": "The rental object identifier"
507
- * }
508
- * },
509
- * "elements": [
510
- * {
511
- * "kind": "text",
512
- * "text": "Hyresobjekt: {{vars.hyresobjektsnummer}}"
513
- * }
514
- * ]
515
- * }
516
- * ```
517
- */
518
- export interface TemplateVariableDef {
519
- /** Variable type. Determines validation and UI control type. */
520
- type: 'string' | 'number' | 'boolean' | 'array';
521
-
522
- /**
523
- * Default value for this variable.
524
- * REQUIRED - ensures template always works even without user input.
525
- * Must match the specified type.
526
- */
527
- default: any;
528
-
529
- /** Human-readable label for UI generation. Defaults to variable name. */
530
- label?: string;
531
-
532
- /** Description/help text for UI generation. */
533
- description?: string;
534
-
535
- /**
536
- * Semantic format hint for UI generation.
537
- * Provides additional context for rendering appropriate controls.
538
- *
539
- * - `color`: Renders color picker (for type: 'string')
540
- * - `email`: Renders email input with validation (for type: 'string')
541
- * - `url`: Renders URL input with validation (for type: 'string')
542
- * - `date`: Renders date picker (for type: 'string')
543
- *
544
- * @example
545
- * ```json
546
- * {
547
- * "lineColor": {
548
- * "type": "string",
549
- * "format": "color",
550
- * "default": "#1f2937"
551
- * }
552
- * }
553
- * ```
554
- */
555
- format?: 'color' | 'email' | 'url' | 'date';
556
-
557
- // String validation
558
- /** Minimum string length (for type: 'string'). */
559
- minLength?: number;
560
- /** Maximum string length (for type: 'string'). */
561
- maxLength?: number;
562
- /** Regex pattern for validation (for type: 'string'). */
563
- pattern?: string;
564
- /** Allowed values (dropdown options). */
565
- enum?: string[];
566
-
567
- // Number validation
568
- /** Minimum value (for type: 'number'). */
569
- min?: number;
570
- /** Maximum value (for type: 'number'). */
571
- max?: number;
572
- /** Step size for UI controls (for type: 'number'). */
573
- step?: number;
574
-
575
- // Array validation
576
- /** Array item specification (for type: 'array'). */
577
- items?: {
578
- type: 'string' | 'number';
579
- enum?: any[];
580
- };
581
- }
582
-
583
- /**
584
- * Variable information extracted from template for UI generation.
585
- */
586
- export interface TemplateVariableInfo {
587
- name: string;
588
- type: 'string' | 'number' | 'boolean' | 'array';
589
- default: any;
590
- label: string;
591
- description?: string;
592
- format?: 'color' | 'email' | 'url' | 'date';
593
- min?: number;
594
- max?: number;
595
- step?: number;
596
- minLength?: number;
597
- maxLength?: number;
598
- pattern?: string;
599
- enum?: any[];
600
- items?: {
601
- type: 'string' | 'number';
602
- enum?: any[];
603
- };
604
- }
605
-
606
- export interface LayoutTemplate {
607
- /** JSON Schema reference for editor autocompletion. Ignored at runtime. */
608
- $schema?: string;
609
- id?: string;
610
-
611
- /**
612
- * Template variables for parameterization.
613
- *
614
- * Variables allow templates to be reused with different values.
615
- * All variables MUST have defaults - templates work without user input.
616
- * Reference variables in template using {{vars.variableName}} syntax.
617
- *
618
- * Optional - templates without variables work as before (backward compatible).
619
- */
620
- variables?: Record<string, TemplateVariableDef>;
621
-
622
- page: PageSpec;
623
- styles?: StyleMap;
624
- assets?: AssetSpec[];
625
- elements: LayoutElement[];
626
- presets?: {
627
- includeClasses?: Record<string, string[]>;
628
- scales?: Record<string, { preferredScales?: number[] }>;
629
- };
630
- /** Allow fetching remote assets (images, fonts). Default: false. */
631
- allowRemoteAssets?: boolean;
632
- }
633
-
634
- export interface ModelMetadataEntry {
635
- productId?: string;
636
- ifcClass?: string;
637
- properties?: any;
638
- [key: string]: any;
639
- }
640
-
641
- export type ModelMetadata = Record<string, ModelMetadataEntry>;
642
-
643
- export interface ProductFieldPaths {
644
- idPath?: string;
645
- namePath?: string;
646
- areaPath?: string;
647
- classPath?: string;
648
- }
649
-
650
- export interface NormalizedProductItem {
651
- gid?: string;
652
- /** IfcClassType category (e.g. "space", "walls"). Used for template class filters. */
653
- ifcClass?: string | null;
654
- /** PredefinedIfcClassId (e.g. "IfcSpace", "IfcWall"). Used as fallback for class matching. */
655
- ifcClassId?: string | null;
656
- propertySets: Record<string, Record<string, { value: any; unit?: string; type?: string }>>;
657
- productId: string;
658
- productName: string;
659
- area: number | null;
660
- x?: number | null;
661
- z?: number | null;
662
- polylines?: Array<{ points?: Array<{ x: number; z: number }> }>;
663
- [key: string]: any;
664
- }
665
-
666
- export type SectionStateValidationResult =
667
- | { ok: true }
668
- | {
669
- ok: false;
670
- errors: string[];
671
- };
672
-
673
- export interface LabelStyle {
674
- /** Text fill color. Default: "#374151" */
675
- fill?: string;
676
- /** Font family. Default: '"Segoe UI", "Helvetica Neue", Arial, sans-serif' */
677
- fontFamily?: string;
678
- /** Font weight. Default: "300" */
679
- fontWeight?: string;
680
- }
681
-
682
- export interface SvgModelView {
683
- groupSvg: string;
684
- widthModel: number;
685
- heightModel: number;
686
- geometryTransform?: string;
687
- transform?: {
688
- viewMinX: number;
689
- viewMaxX: number;
690
- viewMinZ: number;
691
- viewMaxZ: number;
692
- viewWidth: number;
693
- viewHeight: number;
694
- };
695
- rawPaths?: SectionPath[];
696
- rawLabels?: SectionLabel[];
697
- labelStyle?: LabelStyle;
698
- }
699
-
700
- export type ModelContext = Record<string, SvgModelView>;
701
- export type LayoutData = Record<string, any>;
702
- export type ViewportItemsMap = Record<string, { items: NormalizedProductItem[] }>;
703
- export type LayoutDataViewports = { viewports: ViewportItemsMap } & Record<string, any>;
704
-
705
- export interface ValidationOptions {
706
- allowRemoteAssets?: boolean;
707
- allowDataUrls?: boolean;
708
- maxViewports?: number;
709
- }
710
-
711
- export interface ValidationIssue {
712
- path: string;
713
- message: string;
714
- }
715
-
716
- export type ValidationResult = { ok: true; template: any } | { ok: false; errors: ValidationIssue[] };
717
-
718
- export interface ComputedViewportScale {
719
- denom: number;
720
- mmPerModelUnit: number;
721
- fits: boolean;
722
- }
723
-
724
- export interface SectionResult {
725
- perProduct: Array<{
726
- product: any;
727
- segments: Array<[any, any]>;
728
- polylines: Array<{ points: any[]; closed: boolean }>;
729
- }>;
730
- }
731
-
732
- export interface RenderDrawingInput {
733
- template: LayoutTemplate;
734
- /**
735
- * Section geometry to render.
736
- *
737
- * - Pass a single `SectionResult` to use the same geometry for every viewport.
738
- * - Pass a `Record<string, SectionResult>` keyed by viewport ID for per-viewport control.
739
- *
740
- * Either `sections` or `models` must be provided. When `models` is provided,
741
- * `sections` is ignored and the pre-built SvgModelViews are used directly.
742
- */
743
- sections?: SectionResult | Record<string, SectionResult>;
744
- /**
745
- * Pre-built model views keyed by viewport ID.
746
- *
747
- * Use this when geometry comes from a source other than `computePlaneSection`
748
- * (e.g. SlabSection). The caller is responsible for producing `SvgModelView`
749
- * objects; `renderDrawing` will pass them straight to `renderLayout`.
750
- */
751
- models?: ModelContext;
752
- options?: {
753
- allowRemoteAssets?: boolean;
754
- qrContext?: {
755
- host?: string | null;
756
- containerName?: string | null;
757
- floorName?: string | null;
758
- planeHeight?: number | null;
759
- };
760
- /**
761
- * Embed room labels directly in the section SVG.
762
- *
763
- * Only use this when the template does NOT have its own `labels` elements,
764
- * otherwise you will get duplicate labels. When omitted, renderDrawing
765
- * auto-detects template `labels` elements and skips embedding.
766
- */
767
- roomLabels?: {
768
- /** Dot-separated path to the label text under product.properties.data. */
769
- labelPath: string;
770
- /** IFC class name filter (e.g. "space"). If omitted, all entries are included. */
771
- classFilter?: string;
772
- };
773
- /**
774
- * Raw per-product entries for automatic item building when using `models`.
775
- *
776
- * Each entry needs a `product` (BIM object with gid, name, class, properties)
777
- * and `polylines` (for centroid computation and fill polygon rendering).
778
- *
779
- * When provided, renderDrawing builds NormalizedProductItems per viewport,
780
- * resolving fill colors from each viewport's fill rules. This replaces
781
- * manual item construction in consumer code.
782
- *
783
- * Ignored when `viewportItems` is also provided (explicit items take priority).
784
- */
785
- productEntries?: Array<{
786
- product: unknown;
787
- polylines: Array<{ points: Array<{ x: number; z: number }> }>;
788
- }>;
789
- /**
790
- * Pre-built product items when `models` is provided directly.
791
- * Keyed by viewport ID, each containing an array of NormalizedProductItems.
792
- * Used for legends, labels, and other template elements that need item data.
793
- *
794
- * Takes priority over `productEntries` when both are provided.
795
- */
796
- viewportItems?: Record<string, { items: NormalizedProductItem[] }>;
797
- /**
798
- * App default section settings. Used as fallback when viewports don't specify
799
- * their own cutHeight/depth configuration.
800
- */
801
- sectionDefaults?: {
802
- cutHeight?: number;
803
- depth?: { above?: number; below?: number };
804
- };
805
- /**
806
- * Template variable values.
807
- *
808
- * When template defines `variables`, provide values here to customize the output.
809
- * Missing values automatically use defaults from template (fail-safe).
810
- * Invalid values fall back to defaults with warnings.
811
- *
812
- * Optional - if not provided, all template defaults are used.
813
- *
814
- * @example
815
- * ```typescript
816
- * await renderDrawing({
817
- * template,
818
- * models,
819
- * options: {
820
- * variables: {
821
- * hyresobjektsnummer: '2022-F2009',
822
- * showLegend: true
823
- * }
824
- * }
825
- * });
826
- * ```
827
- */
828
- variables?: Record<string, any>;
829
- };
830
- }
831
-
832
- export interface RenderDrawingResult {
833
- svg: string;
834
- warnings: string[];
835
- }
1
+ import type { SectionPath, SectionLabel } from '@twinfinity/geometry';
2
+
3
+ export type PagePresetKey = 'a1' | 'a3' | 'a4';
4
+ export type PageOrientation = 'portrait' | 'landscape';
5
+
6
+ export interface PageSpec {
7
+ widthMm: number;
8
+ heightMm: number;
9
+ marginMm?: number;
10
+ backgroundColor?: string;
11
+ }
12
+
13
+ export interface TextStyle {
14
+ fontFamily: string;
15
+ fontSizeMm: number;
16
+ fontWeight?: 'normal' | 'bold' | 'lighter' | 'bolder' | string;
17
+ color?: string;
18
+ letterSpacingMm?: number;
19
+ lineHeightMm?: number;
20
+ }
21
+
22
+ export interface StyleMap {
23
+ [styleName: string]: TextStyle;
24
+ }
25
+
26
+ export interface AssetSpec {
27
+ id: string;
28
+ src?: string;
29
+ content?: string;
30
+ }
31
+
32
+ export type WhereCondition = Record<string, string | number | boolean | Array<string | number>>;
33
+
34
+ export interface ClipConfig {
35
+ class?: string | string[];
36
+ where: WhereCondition;
37
+ expandByM?: number;
38
+ clipLabels?: boolean;
39
+ stroke?: string;
40
+ strokeWidthMm?: number;
41
+ strokeOpacity?: number;
42
+ fill?: string;
43
+ fillOpacity?: number;
44
+ }
45
+
46
+ /**
47
+ * Template-driven fill rule for a viewport.
48
+ *
49
+ * Fill rules control which products get filled polygons in the drawing.
50
+ * When no `fills` array is present on a viewport, no fills are rendered.
51
+ *
52
+ * Example:
53
+ * ```json
54
+ * { "class": "space", "fill": "sample", "fillOpacity": 0.35 }
55
+ * { "class": "walls", "fill": "#cccccc", "fillOpacity": 0.5 }
56
+ * ```
57
+ */
58
+ export interface FillRule {
59
+ /** IFC class filter (e.g. "space", "walls"). Matches ifcClass or ifcClassId. */
60
+ class?: string | string[];
61
+ /** Property-based filter (same as highlight/clip `where`). */
62
+ where?: WhereCondition;
63
+ /** Fill color: CSS hex (e.g. "#80c8b4"), "sample" to read from 3D model material, or "legend" to use colors assigned by legend. */
64
+ fill: string;
65
+ /** Fill opacity (0–1). Default: 0.35. */
66
+ fillOpacity?: number;
67
+ /** When true, only apply fill to items whose centroid is spatially inside the clip polygon.
68
+ * Useful when expandByM reveals adjacent spaces that shouldn't be filled.
69
+ * @default false */
70
+ matchClipFilter?: boolean;
71
+ }
72
+
73
+ export interface HighlightConfig {
74
+ class?: string | string[];
75
+ where: WhereCondition;
76
+ /** Only highlight items that also appear in the referenced viewport (i.e., intersect both planes). */
77
+ intersectsViewport?: string;
78
+ stroke?: string;
79
+ strokeWidthMm?: number;
80
+ strokeOpacity?: number;
81
+ fill?: string;
82
+ fillOpacity?: number;
83
+ }
84
+
85
+ export type LayoutElement =
86
+ | ViewportElement
87
+ | TextElement
88
+ | ImageElement
89
+ | LineElement
90
+ | RectElement
91
+ | CircleElement
92
+ | PolylineElement
93
+ | PolygonElement
94
+ | ScaleBarElement
95
+ | QrElement
96
+ | LabelsElement
97
+ | LegendElement
98
+ | GroupElement;
99
+
100
+ export interface BaseElement {
101
+ kind: LayoutElement['kind'];
102
+ id: string;
103
+ zIndex?: number;
104
+ }
105
+
106
+ /** Style for a single line category in a viewport. */
107
+ export interface ViewportLineStyle {
108
+ /** Line width in mm. */
109
+ widthMm?: number;
110
+ /** Line color (CSS color string). */
111
+ color?: string;
112
+ /** SVG dash array (e.g. "4 2"). */
113
+ dash?: string;
114
+ /** Whether this category is rendered. Default: true. */
115
+ enabled?: boolean;
116
+ }
117
+
118
+ /**
119
+ * Per-category line styles for a viewport.
120
+ *
121
+ * Resolution order: category-specific → `default` → built-in defaults.
122
+ *
123
+ * Example:
124
+ * ```json
125
+ * {
126
+ * "default": { "widthMm": 0.01, "color": "#1f2937" },
127
+ * "cutLines": { "widthMm": 0.02, "color": "#000000" },
128
+ * "hiddenLines": { "widthMm": 0.005, "color": "#9ca3af", "dash": "4 2" }
129
+ * }
130
+ * ```
131
+ */
132
+ export interface ViewportLineStyles {
133
+ /** Fallback style for any category not explicitly listed. */
134
+ default?: ViewportLineStyle;
135
+ cutLines?: ViewportLineStyle;
136
+ visibleLines?: ViewportLineStyle;
137
+ hiddenLines?: ViewportLineStyle;
138
+ silhouettes?: ViewportLineStyle;
139
+ featureEdges?: ViewportLineStyle;
140
+ stairArrows?: ViewportLineStyle;
141
+ outlineVisible?: ViewportLineStyle;
142
+ outlineHidden?: ViewportLineStyle;
143
+ }
144
+
145
+ /**
146
+ * Include filter for viewport geometry.
147
+ *
148
+ * Can be:
149
+ * - String or string array: class names only
150
+ * - Object with class + where: property-based filtering
151
+ * - Preset reference: { preset: "presetName" }
152
+ *
153
+ * Examples:
154
+ * ```json
155
+ * "includeClasses": ["space", "walls"]
156
+ * "includeClasses": [{ "class": "space", "where": { "BIP.spacetype": "ROOM" } }]
157
+ * "includeClasses": { "preset": "defaultBuilding" }
158
+ * ```
159
+ */
160
+ export type IncludeClassesFilter =
161
+ | string
162
+ | string[]
163
+ | { class?: string | string[]; where?: WhereCondition }
164
+ | { preset: string };
165
+
166
+ /**
167
+ * Resolved include filter (after preset expansion).
168
+ * Either a simple class name or a class+where filter.
169
+ */
170
+ export type ResolvedIncludeFilter = string | { class?: string | string[]; where: WhereCondition };
171
+
172
+ export interface ViewportElement extends BaseElement {
173
+ kind: 'viewport';
174
+ xMm: number;
175
+ yMm: number;
176
+ widthMm: number;
177
+ heightMm: number;
178
+ scaleMode?: 'fit' | 'fitAndRound' | 'fixed';
179
+ fixedScaleDenom?: number;
180
+ preferredScales?: number[];
181
+ scalePreset?: string;
182
+ align?: 'center' | 'topLeft';
183
+ rotationDeg?: number;
184
+ includeClasses?: IncludeClassesFilter | IncludeClassesFilter[];
185
+ /**
186
+ * Per-category line styles. When set, overrides `lineWidthMm`/`lineColor`.
187
+ * Use `lineStyles.default` for a single style across all categories,
188
+ * or specify individual categories (cutLines, hiddenLines, etc.).
189
+ */
190
+ lineStyles?: ViewportLineStyles;
191
+ /** Simple shorthand: default line width for all categories. Overridden by `lineStyles`. */
192
+ lineWidthMm?: number;
193
+ /** Simple shorthand: default line color for all categories. Overridden by `lineStyles`. */
194
+ lineColor?: string;
195
+ /**
196
+ * Fill rules controlling which products get filled polygons.
197
+ * When absent, no fills are rendered (default: no fill).
198
+ */
199
+ fills?: FillRule[];
200
+ northArrow?: { assetId: string; sizeMm?: number; offsetXMm?: number; offsetYMm?: number; rotationDeg?: number };
201
+ border?: { stroke?: string; strokeWidthMm?: number; dashed?: boolean };
202
+ clip?: ClipConfig;
203
+ highlight?: HighlightConfig;
204
+
205
+ // Section configuration (overrides app defaults if specified)
206
+ /**
207
+ * Override cutting plane height for horizontal plans (Y coordinate).
208
+ * If omitted, uses app default. Shorthand for planeNormal=[0,1,0], planePosition=cutHeight.
209
+ */
210
+ cutHeight?: number;
211
+ /**
212
+ * Override depth above/below cutting plane. If omitted, uses app default.
213
+ * Only applicable for SLAB mode (PLANE mode doesn't use depth).
214
+ */
215
+ depth?: { above?: number; below?: number };
216
+ /**
217
+ * Plane normal vector [x, y, z]. Automatically normalized.
218
+ * Takes precedence over cutHeight if both are specified.
219
+ * Examples:
220
+ * - [1, 0, 0] = vertical section perpendicular to X axis
221
+ * - [0, 1, 0] = horizontal plan (same as using cutHeight)
222
+ * - [0, 0, 1] = vertical section perpendicular to Z axis
223
+ * - [1, 0, 1] = diagonal section (will be normalized)
224
+ */
225
+ planeNormal?: [number, number, number];
226
+ /**
227
+ * Distance from origin along the (normalized) plane normal.
228
+ * Used with planeNormal to define the cutting plane.
229
+ * The plane passes through the point: normalize(planeNormal) * planePosition.
230
+ */
231
+ planePosition?: number;
232
+ /**
233
+ * Section computation mode: 'slab' or 'plane'.
234
+ * - 'slab': Optimized for horizontal floor plans. Uses depth above/below to include geometry.
235
+ * - 'plane': Arbitrary plane cuts (vertical sections, diagonal sections). Computes exact intersection.
236
+ *
237
+ * If omitted, mode is auto-detected from plane orientation:
238
+ * - Horizontal planes (planeNormal ≈ Y-axis) → 'slab'
239
+ * - Vertical or diagonal planes → 'plane'
240
+ */
241
+ mode?: 'slab' | 'plane';
242
+ }
243
+
244
+ export interface TextElement extends BaseElement {
245
+ kind: 'text';
246
+ xMm: number;
247
+ yMm: number;
248
+ textStyle?: string;
249
+ text: string;
250
+ maxWidthMm?: number;
251
+ align?: 'left' | 'center' | 'right';
252
+ }
253
+
254
+ export interface ImageElement extends BaseElement {
255
+ kind: 'image';
256
+ xMm: number;
257
+ yMm: number;
258
+ widthMm: number;
259
+ heightMm: number;
260
+ assetId: string;
261
+ fit?: 'contain' | 'cover' | 'stretch';
262
+ }
263
+
264
+ export interface LineElement extends BaseElement {
265
+ kind: 'line';
266
+ x1Mm: number;
267
+ y1Mm: number;
268
+ x2Mm: number;
269
+ y2Mm: number;
270
+ stroke?: string;
271
+ strokeWidthMm?: number;
272
+ dashed?: boolean;
273
+ }
274
+
275
+ export interface RectElement extends BaseElement {
276
+ kind: 'rect';
277
+ xMm: number;
278
+ yMm: number;
279
+ widthMm: number;
280
+ heightMm: number;
281
+ stroke?: string;
282
+ strokeWidthMm?: number;
283
+ fill?: string;
284
+ dashed?: boolean;
285
+ }
286
+
287
+ export interface CircleElement extends BaseElement {
288
+ kind: 'circle';
289
+ cxMm: number;
290
+ cyMm: number;
291
+ rMm: number;
292
+ stroke?: string;
293
+ strokeWidthMm?: number;
294
+ fill?: string;
295
+ dashed?: boolean;
296
+ }
297
+
298
+ export interface PointMm {
299
+ xMm: number;
300
+ yMm: number;
301
+ }
302
+
303
+ export interface PolylineElement extends BaseElement {
304
+ kind: 'polyline';
305
+ points: PointMm[];
306
+ stroke: string;
307
+ strokeWidthMm: number;
308
+ dashed?: boolean;
309
+ }
310
+
311
+ export interface PolygonElement extends BaseElement {
312
+ kind: 'polygon';
313
+ points: PointMm[];
314
+ stroke?: string;
315
+ strokeWidthMm?: number;
316
+ fill?: string;
317
+ dashed?: boolean;
318
+ }
319
+
320
+ export interface ScaleBarElement extends BaseElement {
321
+ kind: 'scaleBar';
322
+ xMm: number;
323
+ yMm: number;
324
+ maxLengthMm: number;
325
+ unit?: string;
326
+ segments?: number;
327
+ labelEvery?: number;
328
+ stroke?: string;
329
+ strokeWidthMm?: number;
330
+ textStyle?: string;
331
+ viewport?: string;
332
+ }
333
+
334
+ export interface QrElement extends BaseElement {
335
+ kind: 'qr';
336
+ xMm: number;
337
+ yMm: number;
338
+ sizeMm: number;
339
+ role?: 'main' | 'anchor' | 'custom';
340
+ anchorCorner?: 'bottom-left' | 'bottom-right' | 'top-left' | 'top-right';
341
+ viewport?: string;
342
+ dataTemplate?: string;
343
+ }
344
+
345
+ export interface LabelsElement extends BaseElement {
346
+ kind: 'labels';
347
+ viewport: string;
348
+ class?: string | string[];
349
+ where?: WhereCondition;
350
+ text: string;
351
+ textStyle?: string;
352
+ align?: 'left' | 'center' | 'right';
353
+ maxWidthMm?: number;
354
+ /** When true, only show labels for items whose position is spatially inside the clip polygon.
355
+ * @default false */
356
+ matchClipFilter?: boolean;
357
+ }
358
+
359
+ export type LegendAnchor = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left';
360
+
361
+ export interface LegendColumn {
362
+ id: string;
363
+ label: string;
364
+ property: string;
365
+ format?: { digits?: number; prefix?: string; suffix?: string };
366
+ align?: 'left' | 'center' | 'right';
367
+ minWidthMm?: number;
368
+ maxWidthMm?: number;
369
+ /**
370
+ * Enable summary (total) calculation for this column.
371
+ * When true, the summary row will show the sum of all values in this column.
372
+ * Only works for numerical columns.
373
+ * @default false
374
+ */
375
+ summary?: boolean;
376
+ }
377
+
378
+ export interface LegendElement extends BaseElement {
379
+ kind: 'legend';
380
+ viewport: string;
381
+ class?: string | string[];
382
+ where?: WhereCondition;
383
+ clipToViewport?: boolean;
384
+ anchor?: LegendAnchor;
385
+ anchorPositionMm?: { xMm: number; yMm: number };
386
+ marginMm?: number;
387
+ textStyle?: string;
388
+ showColorColumn?: boolean;
389
+ colorColumnWidthMm?: number;
390
+ cellPaddingMm?: number;
391
+ borderStrokeWidthMm?: number;
392
+ borderStrokeColor?: string;
393
+ gridStrokeWidthMm?: number;
394
+ gridStrokeColor?: string;
395
+ headerStrokeWidthMm?: number;
396
+ headerStrokeColor?: string;
397
+ columns: LegendColumn[];
398
+ /**
399
+ * Summary row configuration.
400
+ * Displays a totals row at the bottom of the legend.
401
+ * Columns with `summary: true` will show their sum in the summary row.
402
+ *
403
+ * @example
404
+ * ```json
405
+ * {
406
+ * "summary": {
407
+ * "enabled": true,
408
+ * "label": "Total"
409
+ * },
410
+ * "columns": [
411
+ * { "id": "name", "label": "Room", "property": "BIP.spacename" },
412
+ * { "id": "area", "label": "Area", "property": "BIP.area", "summary": true }
413
+ * ]
414
+ * }
415
+ * ```
416
+ */
417
+ summary?: {
418
+ /** Enable summary row. */
419
+ enabled: boolean;
420
+ /** Label for first column in summary row. Defaults to "Total". */
421
+ label?: string;
422
+ };
423
+ /**
424
+ * Group legend rows by a specific column property.
425
+ * When enabled, rows with identical values in the groupBy column are consolidated into a single row.
426
+ * Numerical columns are summed across grouped rows; non-numerical columns take the first value.
427
+ *
428
+ * @example
429
+ * ```json
430
+ * {
431
+ * "groupBy": "BIP.spacename",
432
+ * "columns": [
433
+ * { "id": "name", "label": "Room", "property": "BIP.spacename" },
434
+ * { "id": "area", "label": "Area", "property": "BIP.area", "format": { "digits": 2 } }
435
+ * ]
436
+ * }
437
+ * ```
438
+ * This groups all rooms with the same name and sums their areas.
439
+ */
440
+ groupBy?: string;
441
+ /**
442
+ * Assign colors to legend rows and apply them to the referenced viewport.
443
+ * When enabled, generates colors for each legend row (or group if groupBy is used).
444
+ * The viewport can use these colors by setting fill: "legend" in its fill rules.
445
+ *
446
+ * @example
447
+ * ```json
448
+ * {
449
+ * "assignColors": {
450
+ * "enabled": true,
451
+ * "palette": "auto"
452
+ * },
453
+ * "groupBy": "BIP.spacename"
454
+ * }
455
+ * ```
456
+ * Generates distinct colors for each room type, applied to matching spaces in viewport.
457
+ */
458
+ assignColors?: {
459
+ /** Enable color assignment to viewport items. */
460
+ enabled: boolean;
461
+ /** Color palette: "auto" for auto-generated colors, or array of hex colors. */
462
+ palette?: 'auto' | string[];
463
+ };
464
+ }
465
+
466
+ export interface GroupTransform {
467
+ translateXMm?: number;
468
+ translateYMm?: number;
469
+ rotateDeg?: number;
470
+ scaleX?: number;
471
+ scaleY?: number;
472
+ }
473
+
474
+ export interface GroupClipRect {
475
+ type: 'rect';
476
+ xMm: number;
477
+ yMm: number;
478
+ widthMm: number;
479
+ heightMm: number;
480
+ }
481
+
482
+ export interface GroupElement extends BaseElement {
483
+ kind: 'group';
484
+ opacity?: number;
485
+ transform?: GroupTransform;
486
+ clip?: GroupClipRect;
487
+ children: LayoutElement[];
488
+ }
489
+
490
+ /**
491
+ * Template variable definition.
492
+ *
493
+ * Variables allow templates to be parameterized and reused with different values.
494
+ * All variables MUST have a default value to ensure templates work without user input.
495
+ *
496
+ * Variables are referenced in templates using {{vars.variableName}} syntax.
497
+ *
498
+ * Example:
499
+ * ```json
500
+ * {
501
+ * "variables": {
502
+ * "hyresobjektsnummer": {
503
+ * "type": "string",
504
+ * "default": "2022-F2008",
505
+ * "label": "Hyresobjekt Number",
506
+ * "description": "The rental object identifier"
507
+ * }
508
+ * },
509
+ * "elements": [
510
+ * {
511
+ * "kind": "text",
512
+ * "text": "Hyresobjekt: {{vars.hyresobjektsnummer}}"
513
+ * }
514
+ * ]
515
+ * }
516
+ * ```
517
+ */
518
+ export interface TemplateVariableDef {
519
+ /** Variable type. Determines validation and UI control type. */
520
+ type: 'string' | 'number' | 'boolean' | 'array';
521
+
522
+ /**
523
+ * Default value for this variable.
524
+ * REQUIRED - ensures template always works even without user input.
525
+ * Must match the specified type.
526
+ */
527
+ default: any;
528
+
529
+ /** Human-readable label for UI generation. Defaults to variable name. */
530
+ label?: string;
531
+
532
+ /** Description/help text for UI generation. */
533
+ description?: string;
534
+
535
+ /**
536
+ * Semantic format hint for UI generation.
537
+ * Provides additional context for rendering appropriate controls.
538
+ *
539
+ * - `color`: Renders color picker (for type: 'string')
540
+ * - `email`: Renders email input with validation (for type: 'string')
541
+ * - `url`: Renders URL input with validation (for type: 'string')
542
+ * - `date`: Renders date picker (for type: 'string')
543
+ *
544
+ * @example
545
+ * ```json
546
+ * {
547
+ * "lineColor": {
548
+ * "type": "string",
549
+ * "format": "color",
550
+ * "default": "#1f2937"
551
+ * }
552
+ * }
553
+ * ```
554
+ */
555
+ format?: 'color' | 'email' | 'url' | 'date';
556
+
557
+ // String validation
558
+ /** Minimum string length (for type: 'string'). */
559
+ minLength?: number;
560
+ /** Maximum string length (for type: 'string'). */
561
+ maxLength?: number;
562
+ /** Regex pattern for validation (for type: 'string'). */
563
+ pattern?: string;
564
+ /** Allowed values (dropdown options). */
565
+ enum?: string[];
566
+
567
+ // Number validation
568
+ /** Minimum value (for type: 'number'). */
569
+ min?: number;
570
+ /** Maximum value (for type: 'number'). */
571
+ max?: number;
572
+ /** Step size for UI controls (for type: 'number'). */
573
+ step?: number;
574
+
575
+ // Array validation
576
+ /** Array item specification (for type: 'array'). */
577
+ items?: {
578
+ type: 'string' | 'number';
579
+ enum?: any[];
580
+ };
581
+ }
582
+
583
+ /**
584
+ * Variable information extracted from template for UI generation.
585
+ */
586
+ export interface TemplateVariableInfo {
587
+ name: string;
588
+ type: 'string' | 'number' | 'boolean' | 'array';
589
+ default: any;
590
+ label: string;
591
+ description?: string;
592
+ format?: 'color' | 'email' | 'url' | 'date';
593
+ min?: number;
594
+ max?: number;
595
+ step?: number;
596
+ minLength?: number;
597
+ maxLength?: number;
598
+ pattern?: string;
599
+ enum?: any[];
600
+ items?: {
601
+ type: 'string' | 'number';
602
+ enum?: any[];
603
+ };
604
+ }
605
+
606
+ export interface LayoutTemplate {
607
+ /** JSON Schema reference for editor autocompletion. Ignored at runtime. */
608
+ $schema?: string;
609
+ id?: string;
610
+
611
+ /**
612
+ * Template variables for parameterization.
613
+ *
614
+ * Variables allow templates to be reused with different values.
615
+ * All variables MUST have defaults - templates work without user input.
616
+ * Reference variables in template using {{vars.variableName}} syntax.
617
+ *
618
+ * Optional - templates without variables work as before (backward compatible).
619
+ */
620
+ variables?: Record<string, TemplateVariableDef>;
621
+
622
+ page: PageSpec;
623
+ styles?: StyleMap;
624
+ assets?: AssetSpec[];
625
+ elements: LayoutElement[];
626
+ presets?: {
627
+ includeClasses?: Record<string, string[]>;
628
+ scales?: Record<string, { preferredScales?: number[] }>;
629
+ };
630
+ /** Allow fetching remote assets (images, fonts). Default: false. */
631
+ allowRemoteAssets?: boolean;
632
+ }
633
+
634
+ export interface ModelMetadataEntry {
635
+ productId?: string;
636
+ ifcClass?: string;
637
+ properties?: any;
638
+ [key: string]: any;
639
+ }
640
+
641
+ export type ModelMetadata = Record<string, ModelMetadataEntry>;
642
+
643
+ export interface ProductFieldPaths {
644
+ idPath?: string;
645
+ namePath?: string;
646
+ areaPath?: string;
647
+ classPath?: string;
648
+ }
649
+
650
+ export interface NormalizedProductItem {
651
+ gid?: string;
652
+ /** IfcClassType category (e.g. "space", "walls"). Used for template class filters. */
653
+ ifcClass?: string | null;
654
+ /** PredefinedIfcClassId (e.g. "IfcSpace", "IfcWall"). Used as fallback for class matching. */
655
+ ifcClassId?: string | null;
656
+ propertySets: Record<string, Record<string, { value: any; unit?: string; type?: string }>>;
657
+ productId: string;
658
+ productName: string;
659
+ area: number | null;
660
+ x?: number | null;
661
+ z?: number | null;
662
+ polylines?: Array<{ points?: Array<{ x: number; z: number }> }>;
663
+ [key: string]: any;
664
+ }
665
+
666
+ export type SectionStateValidationResult =
667
+ | { ok: true }
668
+ | {
669
+ ok: false;
670
+ errors: string[];
671
+ };
672
+
673
+ export interface LabelStyle {
674
+ /** Text fill color. Default: "#374151" */
675
+ fill?: string;
676
+ /** Font family. Default: '"Segoe UI", "Helvetica Neue", Arial, sans-serif' */
677
+ fontFamily?: string;
678
+ /** Font weight. Default: "300" */
679
+ fontWeight?: string;
680
+ }
681
+
682
+ export interface SvgModelView {
683
+ groupSvg: string;
684
+ widthModel: number;
685
+ heightModel: number;
686
+ geometryTransform?: string;
687
+ transform?: {
688
+ viewMinX: number;
689
+ viewMaxX: number;
690
+ viewMinZ: number;
691
+ viewMaxZ: number;
692
+ viewWidth: number;
693
+ viewHeight: number;
694
+ };
695
+ rawPaths?: SectionPath[];
696
+ rawLabels?: SectionLabel[];
697
+ labelStyle?: LabelStyle;
698
+ }
699
+
700
+ export type ModelContext = Record<string, SvgModelView>;
701
+ export type LayoutData = Record<string, any>;
702
+ export type ViewportItemsMap = Record<string, { items: NormalizedProductItem[] }>;
703
+ export type LayoutDataViewports = { viewports: ViewportItemsMap } & Record<string, any>;
704
+
705
+ export interface ValidationOptions {
706
+ allowRemoteAssets?: boolean;
707
+ allowDataUrls?: boolean;
708
+ maxViewports?: number;
709
+ }
710
+
711
+ export interface ValidationIssue {
712
+ path: string;
713
+ message: string;
714
+ }
715
+
716
+ export type ValidationResult = { ok: true; template: any } | { ok: false; errors: ValidationIssue[] };
717
+
718
+ export interface ComputedViewportScale {
719
+ denom: number;
720
+ mmPerModelUnit: number;
721
+ fits: boolean;
722
+ }
723
+
724
+ export interface SectionResult {
725
+ perProduct: Array<{
726
+ product: any;
727
+ segments: Array<[any, any]>;
728
+ polylines: Array<{ points: any[]; closed: boolean }>;
729
+ }>;
730
+ }
731
+
732
+ export interface RenderDrawingInput {
733
+ template: LayoutTemplate;
734
+ /**
735
+ * Section geometry to render.
736
+ *
737
+ * - Pass a single `SectionResult` to use the same geometry for every viewport.
738
+ * - Pass a `Record<string, SectionResult>` keyed by viewport ID for per-viewport control.
739
+ *
740
+ * Either `sections` or `models` must be provided. When `models` is provided,
741
+ * `sections` is ignored and the pre-built SvgModelViews are used directly.
742
+ */
743
+ sections?: SectionResult | Record<string, SectionResult>;
744
+ /**
745
+ * Pre-built model views keyed by viewport ID.
746
+ *
747
+ * Use this when geometry comes from a source other than `computePlaneSection`
748
+ * (e.g. SlabSection). The caller is responsible for producing `SvgModelView`
749
+ * objects; `renderDrawing` will pass them straight to `renderLayout`.
750
+ */
751
+ models?: ModelContext;
752
+ options?: {
753
+ allowRemoteAssets?: boolean;
754
+ qrContext?: {
755
+ host?: string | null;
756
+ containerName?: string | null;
757
+ floorName?: string | null;
758
+ planeHeight?: number | null;
759
+ };
760
+ /**
761
+ * Embed room labels directly in the section SVG.
762
+ *
763
+ * Only use this when the template does NOT have its own `labels` elements,
764
+ * otherwise you will get duplicate labels. When omitted, renderDrawing
765
+ * auto-detects template `labels` elements and skips embedding.
766
+ */
767
+ roomLabels?: {
768
+ /** Dot-separated path to the label text under product.properties.data. */
769
+ labelPath: string;
770
+ /** IFC class name filter (e.g. "space"). If omitted, all entries are included. */
771
+ classFilter?: string;
772
+ };
773
+ /**
774
+ * Raw per-product entries for automatic item building when using `models`.
775
+ *
776
+ * Each entry needs a `product` (BIM object with gid, name, class, properties)
777
+ * and `polylines` (for centroid computation and fill polygon rendering).
778
+ *
779
+ * When provided, renderDrawing builds NormalizedProductItems per viewport,
780
+ * resolving fill colors from each viewport's fill rules. This replaces
781
+ * manual item construction in consumer code.
782
+ *
783
+ * Ignored when `viewportItems` is also provided (explicit items take priority).
784
+ */
785
+ productEntries?: Array<{
786
+ product: unknown;
787
+ polylines: Array<{ points: Array<{ x: number; z: number }> }>;
788
+ }>;
789
+ /**
790
+ * Pre-built product items when `models` is provided directly.
791
+ * Keyed by viewport ID, each containing an array of NormalizedProductItems.
792
+ * Used for legends, labels, and other template elements that need item data.
793
+ *
794
+ * Takes priority over `productEntries` when both are provided.
795
+ */
796
+ viewportItems?: Record<string, { items: NormalizedProductItem[] }>;
797
+ /**
798
+ * App default section settings. Used as fallback when viewports don't specify
799
+ * their own cutHeight/depth configuration.
800
+ */
801
+ sectionDefaults?: {
802
+ cutHeight?: number;
803
+ depth?: { above?: number; below?: number };
804
+ };
805
+ /**
806
+ * Template variable values.
807
+ *
808
+ * When template defines `variables`, provide values here to customize the output.
809
+ * Missing values automatically use defaults from template (fail-safe).
810
+ * Invalid values fall back to defaults with warnings.
811
+ *
812
+ * Optional - if not provided, all template defaults are used.
813
+ *
814
+ * @example
815
+ * ```typescript
816
+ * await renderDrawing({
817
+ * template,
818
+ * models,
819
+ * options: {
820
+ * variables: {
821
+ * hyresobjektsnummer: '2022-F2009',
822
+ * showLegend: true
823
+ * }
824
+ * }
825
+ * });
826
+ * ```
827
+ */
828
+ variables?: Record<string, any>;
829
+ };
830
+ }
831
+
832
+ export interface RenderDrawingResult {
833
+ svg: string;
834
+ warnings: string[];
835
+ }