@twinfinity/printing 6.1.0-beta → 6.1.2-ci.30738-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 (157) hide show
  1. package/dist/BuildInfo.js +3 -3
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +3 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/layout/analyzeTemplate.d.ts +4 -4
  7. package/dist/layout/analyzeTemplate.d.ts.map +1 -1
  8. package/dist/layout/analyzeTemplate.js +18 -16
  9. package/dist/layout/analyzeTemplate.js.map +1 -1
  10. package/dist/layout/classClosure.d.ts +41 -0
  11. package/dist/layout/classClosure.d.ts.map +1 -0
  12. package/dist/layout/classClosure.js +143 -0
  13. package/dist/layout/classClosure.js.map +1 -0
  14. package/dist/layout/clipUtils.d.ts +11 -0
  15. package/dist/layout/clipUtils.d.ts.map +1 -1
  16. package/dist/layout/clipUtils.js +67 -6
  17. package/dist/layout/clipUtils.js.map +1 -1
  18. package/dist/layout/filters.d.ts +14 -8
  19. package/dist/layout/filters.d.ts.map +1 -1
  20. package/dist/layout/filters.js +28 -3
  21. package/dist/layout/filters.js.map +1 -1
  22. package/dist/layout/index.d.ts +24 -57
  23. package/dist/layout/index.d.ts.map +1 -1
  24. package/dist/layout/index.js +20 -337
  25. package/dist/layout/index.js.map +1 -1
  26. package/dist/layout/itemBuilders.d.ts +11 -0
  27. package/dist/layout/itemBuilders.d.ts.map +1 -1
  28. package/dist/layout/itemBuilders.js +17 -3
  29. package/dist/layout/itemBuilders.js.map +1 -1
  30. package/dist/layout/labelsRender.d.ts.map +1 -1
  31. package/dist/layout/labelsRender.js +15 -27
  32. package/dist/layout/labelsRender.js.map +1 -1
  33. package/dist/layout/legendRender.d.ts +18 -6
  34. package/dist/layout/legendRender.d.ts.map +1 -1
  35. package/dist/layout/legendRender.js +49 -79
  36. package/dist/layout/legendRender.js.map +1 -1
  37. package/dist/layout/metadata.d.ts.map +1 -1
  38. package/dist/layout/metadata.js +26 -5
  39. package/dist/layout/metadata.js.map +1 -1
  40. package/dist/layout/model.d.ts +4 -3
  41. package/dist/layout/model.d.ts.map +1 -1
  42. package/dist/layout/model.js +17 -57
  43. package/dist/layout/model.js.map +1 -1
  44. package/dist/layout/options.d.ts +1 -2
  45. package/dist/layout/options.d.ts.map +1 -1
  46. package/dist/layout/options.js.map +1 -1
  47. package/dist/layout/preflight.d.ts +32 -0
  48. package/dist/layout/preflight.d.ts.map +1 -0
  49. package/dist/layout/preflight.js +69 -0
  50. package/dist/layout/preflight.js.map +1 -0
  51. package/dist/layout/qrRender.d.ts.map +1 -1
  52. package/dist/layout/qrRender.js +11 -10
  53. package/dist/layout/qrRender.js.map +1 -1
  54. package/dist/layout/renderDrawing.d.ts +28 -17
  55. package/dist/layout/renderDrawing.d.ts.map +1 -1
  56. package/dist/layout/renderDrawing.js +107 -88
  57. package/dist/layout/renderDrawing.js.map +1 -1
  58. package/dist/layout/renderLayout.d.ts +25 -0
  59. package/dist/layout/renderLayout.d.ts.map +1 -0
  60. package/dist/layout/renderLayout.js +315 -0
  61. package/dist/layout/renderLayout.js.map +1 -0
  62. package/dist/layout/renderTemplateSheet.d.ts +99 -0
  63. package/dist/layout/renderTemplateSheet.d.ts.map +1 -0
  64. package/dist/layout/renderTemplateSheet.js +296 -0
  65. package/dist/layout/renderTemplateSheet.js.map +1 -0
  66. package/dist/layout/renderUtils.d.ts +19 -0
  67. package/dist/layout/renderUtils.d.ts.map +1 -1
  68. package/dist/layout/renderUtils.js +47 -4
  69. package/dist/layout/renderUtils.js.map +1 -1
  70. package/dist/layout/sectionDefs.d.ts +65 -0
  71. package/dist/layout/sectionDefs.d.ts.map +1 -0
  72. package/dist/layout/sectionDefs.js +88 -0
  73. package/dist/layout/sectionDefs.js.map +1 -0
  74. package/dist/layout/slabModelView.d.ts +34 -0
  75. package/dist/layout/slabModelView.d.ts.map +1 -0
  76. package/dist/layout/slabModelView.js +175 -0
  77. package/dist/layout/slabModelView.js.map +1 -0
  78. package/dist/layout/templateVariables.d.ts +1 -1
  79. package/dist/layout/templateVariables.d.ts.map +1 -1
  80. package/dist/layout/templateVariables.js +21 -9
  81. package/dist/layout/templateVariables.js.map +1 -1
  82. package/dist/layout/types.d.ts +164 -83
  83. package/dist/layout/types.d.ts.map +1 -1
  84. package/dist/layout/types.js.map +1 -1
  85. package/dist/layout/validation.d.ts.map +1 -1
  86. package/dist/layout/validation.js +78 -33
  87. package/dist/layout/validation.js.map +1 -1
  88. package/dist/layout/viewportPrep.d.ts +8 -6
  89. package/dist/layout/viewportPrep.d.ts.map +1 -1
  90. package/dist/layout/viewportPrep.js +35 -23
  91. package/dist/layout/viewportPrep.js.map +1 -1
  92. package/dist/layout/viewportRender.d.ts +9 -7
  93. package/dist/layout/viewportRender.d.ts.map +1 -1
  94. package/dist/layout/viewportRender.js +93 -126
  95. package/dist/layout/viewportRender.js.map +1 -1
  96. package/dist/layout/viewportUtils.d.ts +20 -0
  97. package/dist/layout/viewportUtils.d.ts.map +1 -1
  98. package/dist/layout/viewportUtils.js +107 -4
  99. package/dist/layout/viewportUtils.js.map +1 -1
  100. package/dist/printToPdf.d.ts +7 -1
  101. package/dist/printToPdf.d.ts.map +1 -1
  102. package/dist/printToPdf.js +81 -14
  103. package/dist/printToPdf.js.map +1 -1
  104. package/dist/printToSvg.d.ts.map +1 -1
  105. package/dist/printToSvg.js +7 -3
  106. package/dist/printToSvg.js.map +1 -1
  107. package/dist/schemaUtils.d.ts +9 -0
  108. package/dist/schemaUtils.d.ts.map +1 -0
  109. package/dist/schemaUtils.js +14 -0
  110. package/dist/schemaUtils.js.map +1 -0
  111. package/dist/templates/default-template.json +30 -8
  112. package/dist/types.d.ts +2 -1
  113. package/dist/types.d.ts.map +1 -1
  114. package/dist/types.js +10 -1
  115. package/dist/types.js.map +1 -1
  116. package/package.json +8 -3
  117. package/src/index.ts +4 -0
  118. package/src/layout/analyzeTemplate.ts +21 -19
  119. package/src/layout/classClosure.ts +142 -0
  120. package/src/layout/clipUtils.ts +65 -6
  121. package/src/layout/filters.ts +31 -3
  122. package/src/layout/index.ts +61 -571
  123. package/src/layout/itemBuilders.ts +20 -5
  124. package/src/layout/labelsRender.ts +23 -40
  125. package/src/layout/legendRender.ts +64 -84
  126. package/src/layout/metadata.ts +26 -5
  127. package/src/layout/model.ts +21 -66
  128. package/src/layout/options.ts +1 -2
  129. package/src/layout/preflight.ts +96 -0
  130. package/src/layout/qrRender.ts +11 -10
  131. package/src/layout/renderDrawing.ts +132 -110
  132. package/src/layout/renderLayout.ts +483 -0
  133. package/src/layout/renderTemplateSheet.ts +435 -0
  134. package/src/layout/renderUtils.ts +53 -4
  135. package/src/layout/sectionDefs.ts +131 -0
  136. package/src/layout/slabModelView.ts +196 -0
  137. package/src/layout/templateVariables.ts +25 -9
  138. package/src/layout/types.ts +160 -70
  139. package/src/layout/validation.ts +98 -32
  140. package/src/layout/viewportPrep.ts +47 -29
  141. package/src/layout/viewportRender.ts +114 -141
  142. package/src/layout/viewportUtils.ts +118 -5
  143. package/src/printToPdf.ts +84 -15
  144. package/src/printToSvg.ts +7 -3
  145. package/src/schemaUtils.ts +13 -0
  146. package/src/templates/default-template.json +30 -8
  147. package/src/types.ts +12 -2
  148. package/templates/default-a0-landscape.json +171 -66
  149. package/templates/default-a0-portrait.json +159 -60
  150. package/templates/default-a1-landscape.json +135 -48
  151. package/templates/default-a1-portrait.json +123 -42
  152. package/templates/default-a3-landscape.json +87 -24
  153. package/templates/default-a3-portrait.json +99 -30
  154. package/templates/default-a4-landscape.json +75 -18
  155. package/templates/default-a4-portrait.json +75 -18
  156. package/templates/llm-authoring-guide.md +247 -0
  157. package/templates/template-schema.json +295 -120
@@ -3,20 +3,58 @@
3
3
  "id": "default-a4-portrait",
4
4
  "presets": {
5
5
  "includeClasses": {
6
- "defaultBuilding": ["space", "walls", "windows", "doors", "beam", "columns", "stairs", "floor"]
6
+ "defaultBuilding": [
7
+ "space",
8
+ "walls",
9
+ "windows",
10
+ "doors",
11
+ "beam",
12
+ "columns",
13
+ "stairs",
14
+ "floor"
15
+ ]
7
16
  },
8
17
  "scales": {
9
- "default": { "preferredScales": [2, 10, 25, 50, 100, 150, 250, 500, 1000, 2500] }
18
+ "default": {
19
+ "preferredScales": [
20
+ 2,
21
+ 10,
22
+ 25,
23
+ 50,
24
+ 100,
25
+ 150,
26
+ 250,
27
+ 500,
28
+ 1000,
29
+ 2500
30
+ ]
31
+ }
10
32
  }
11
33
  },
12
- "page": { "widthMm": 210, "heightMm": 297, "marginMm": 5, "backgroundColor": "#ffffff" },
34
+ "page": {
35
+ "widthMm": 210,
36
+ "heightMm": 297,
37
+ "marginMm": 5,
38
+ "backgroundColor": "#ffffff"
39
+ },
13
40
  "styles": {
14
- "body": { "fontFamily": "Arial", "fontSizeMm": 3.5, "color": "#000000" },
15
- "spaceLabels": { "fontFamily": "Arial", "fontSizeMm": 5, "color": "#000000" }
41
+ "body": {
42
+ "fontFamily": "Arial",
43
+ "fontSizeMm": 3.5,
44
+ "color": "#000000"
45
+ },
46
+ "spaceLabels": {
47
+ "fontFamily": "Arial",
48
+ "fontSizeMm": 5,
49
+ "color": "#000000"
50
+ }
16
51
  },
17
- "allowRemoteAssets": true,
18
- "assets": [
19
- { "id": "logo-main", "src": "https://twinfinity.com/theme-assets/logo.svg" }
52
+ "allowRemoteResources": true,
53
+ "resources": [
54
+ {
55
+ "id": "logo-main",
56
+ "src": "https://twinfinity.com/theme-assets/logo.svg"
57
+ }
20
58
  ],
21
59
  "elements": [
22
60
  {
@@ -26,7 +64,7 @@
26
64
  "yMm": 7,
27
65
  "widthMm": 40,
28
66
  "heightMm": 10,
29
- "assetId": "logo-main",
67
+ "resourceId": "logo-main",
30
68
  "fit": "contain"
31
69
  },
32
70
  {
@@ -47,19 +85,38 @@
47
85
  "yMm": 20,
48
86
  "widthMm": 200,
49
87
  "heightMm": 250,
50
- "includeClasses": { "preset": "defaultBuilding" },
88
+ "includeClasses": {
89
+ "preset": "defaultBuilding"
90
+ },
51
91
  "scaleMode": "fitAndRound",
52
92
  "scalePreset": "default",
53
93
  "align": "center",
54
- "rotationDeg": 0,
55
- "fills": [{ "class": "space", "fill": "sample", "fillOpacity": 0.35 }],
94
+ "fills": [
95
+ {
96
+ "class": "space",
97
+ "fill": "sample",
98
+ "fillOpacity": 0.35
99
+ }
100
+ ],
56
101
  "lineStyles": {
57
- "default": { "widthMm": 0.01, "color": "#1f2937" },
58
- "visibleLines": { "widthMm": 0.01, "color": "#1f2937" },
59
- "hiddenLines": { "widthMm": 0.005, "color": "#9ca3af", "dash": "4 2" },
60
- "stairArrows": { "widthMm": 0.015, "color": "#1f2937" }
61
- },
62
- "border": { "stroke": "none", "strokeWidthMm": 0.25 }
102
+ "default": {
103
+ "widthMm": 0.25,
104
+ "color": "#1f2937"
105
+ },
106
+ "visibleLines": {
107
+ "widthMm": 0.25,
108
+ "color": "#1f2937"
109
+ },
110
+ "hiddenLines": {
111
+ "widthMm": 0.13,
112
+ "color": "#9ca3af",
113
+ "dash": "4 2"
114
+ },
115
+ "stairArrows": {
116
+ "widthMm": 0.35,
117
+ "color": "#1f2937"
118
+ }
119
+ }
63
120
  },
64
121
  {
65
122
  "kind": "text",
@@ -0,0 +1,247 @@
1
+ # Layout-template authoring guide for AI assistants
2
+
3
+ You are helping a user author a **layout template** for `@twinfinity/printing`
4
+ — a JSON document that renders BIM building data (floor plans, sections) onto
5
+ a print sheet as SVG/PDF. This file is a complete, self-contained
6
+ specification: follow it and the output will validate and render. The
7
+ machine-checkable truth is the sibling `template-schema.json`; when in doubt,
8
+ that schema wins.
9
+
10
+ ## Your output contract
11
+
12
+ - Produce **one valid JSON object** in a single code block. No comments, no
13
+ trailing commas, no placeholders — every value concrete and renderable.
14
+ - Ask the user (or assume and say so): page size, what the sheet should show
15
+ (which floor / which spaces), and which property set holds their data
16
+ (`BIP` is the common Swedish default).
17
+ - The user iterates in the **template-workbench** app, which shows render
18
+ warnings — mention that warnings (not silence) are how missing data shows
19
+ up.
20
+
21
+ ## Units — the #1 mistake
22
+
23
+ - **Page space is millimetres**: every `xMm`/`yMm`/`widthMm`/`heightMm`/
24
+ `sizeMm` field, line widths (`widthMm`, `strokeWidthMm`) and `dash`
25
+ patterns (e.g. `"4 2"` = 4 mm dash, 2 mm gap).
26
+ - **Model space is metres**: `cutHeight`, `planePosition`, `depth.above`/
27
+ `depth.below`, `expandBy`.
28
+ - `cutHeight` must be a real storey cut height (floor elevation + ~1.2 m),
29
+ not 0 and not a guess — ask the user or tell them to adjust it in the
30
+ workbench (wrong height = empty or wrong-floor drawing with no error).
31
+
32
+ ## Root structure
33
+
34
+ ```json
35
+ {
36
+ "id": "my-template",
37
+ "page": { "widthMm": 420, "heightMm": 297, "backgroundColor": "#ffffff" },
38
+ "styles": { "body": { "fontFamily": "Arial", "fontSizeMm": 3.5, "color": "#000000" } },
39
+ "variables": {},
40
+ "presets": {},
41
+ "resources": [],
42
+ "elements": []
43
+ }
44
+ ```
45
+
46
+ - `page` is required (`marginMm` only affects legend auto-placement).
47
+ - `styles` is a name → text-style map: `{ fontFamily, fontSizeMm, fontWeight?,
48
+ color?, letterSpacingMm?, lineHeightMm? }`. **Every `textStyle` you
49
+ reference on an element MUST exist here** — a missing style silently skips
50
+ the element (with a warning).
51
+ - `resources`: `[{ id, src }]` or `[{ id, content }]` (inline SVG). Remote
52
+ `https://` sources also need root `"allowRemoteResources": true`.
53
+ - `presets`: reusable lists — `"includeClasses": { "name": ["walls", …] }`
54
+ and `"scales": { "name": { "preferredScales": [50, 100, …] } }`.
55
+ - `variables`: declared parameters, each **must have a `default`**:
56
+ `{ "spacenumber": { "type": "string", "default": "6043", "label": "Space number" } }`.
57
+ Names must be **simple identifiers** (`[A-Za-z_][A-Za-z0-9_]*` — no dashes
58
+ or dots; they'd collide with the `{{vars.<name>}}` syntax). Reference
59
+ anywhere in element strings — including `where` values — as
60
+ `{{vars.spacenumber}}`. Types: `string | number | boolean | array`;
61
+ optional `format` (`color|email|url|date`), `enum`, `min`/`max`,
62
+ `pattern` drive the editing UI.
63
+
64
+ ## Elements (all have required `kind` + unique `id`; optional `zIndex`)
65
+
66
+ ### viewport — a cut of the model on the page
67
+ Required: `xMm, yMm, widthMm, heightMm`, and in practice **`preferredScales`**
68
+ (e.g. `[50, 100, 200, 400]`) or a `scalePreset` — without candidates the
69
+ scale is NaN and the viewport is skipped.
70
+
71
+ - Section definition (self-describing cut): plans use `"cutHeight": 3.0` +
72
+ `"depth": { "above": 0.5, "below": 1.5 }`; vertical sections use
73
+ `"planeNormal": [0,0,1]` (or `[1,0,0]`, negated variants) +
74
+ `"planePosition": 31` (metres along the normal). Never both.
75
+ - **Prefer binding the cut to a variable** so the user can adjust it without
76
+ editing JSON: `"cutHeight": "{{vars.cutHeight}}"` + a number variable
77
+ `"cutHeight": { "type": "number", "default": 3, "step": 0.1, "label": "Cut height (m)" }`.
78
+ (Whole-value bindings keep the number type; only these two section fields
79
+ support it.)
80
+ - `scaleMode`: `"fitAndRound"` (default — picks the nearest preferred scale
81
+ that fits) | `"fit"` (exact fill, odd denominators) | `"fixed"` (+
82
+ `fixedScaleDenom`).
83
+ - `includeClasses`: what to draw — `["walls", "windows", "doors", "columns",
84
+ "stairs", "floor"]`, entries may also be `{ "class": "space", "where": {…} }`,
85
+ or `{ "preset": "name" }`. Omitted = everything (slow). Class names:
86
+ category form (`walls`, `space`) or IFC form (`IfcWall`, `IfcSpace`) —
87
+ interchangeable everywhere.
88
+ - `lineStyles`: per-category styling `{ color?, widthMm?, dash?, enabled? }`
89
+ for `default, defaultVisibleLines, defaultHiddenLines, cutLines,
90
+ visibleLines, hiddenLines, silhouettes, featureEdges, stairArrows,
91
+ outlineVisible, outlineHidden`. `defaultVisibleLines` styles every
92
+ visible-class category (cutLines, visibleLines, silhouettes, featureEdges,
93
+ outlineVisible) and `defaultHiddenLines` every hidden-class one
94
+ (hiddenLines, outlineHidden) unless the specific category key overrides it;
95
+ `stairArrows` follows only `default`. Sensible print widths:
96
+ default/visible **0.25**, hidden **0.13** + `"dash": "4 2"`, heavy
97
+ **0.35** (mm on paper).
98
+ - `stairArrows` (viewport-level, plans only): geometry of the generated stair
99
+ arrows — `{ "startMarker": "filled" | "open" | "none", "arrowhead":
100
+ "filled" | "outline" | "open", "lengthScale"?, "minLength"?, "maxLength"?,
101
+ "landingExtension"? }`. Defaults: filled circle at the bottom, filled
102
+ triangle at the top. Styling/hiding stays in `lineStyles.stairArrows`.
103
+ - `fills`: `[{ "class": "space", "where": {…}, "fill": "#80c8b4" | "sample" |
104
+ "legend", "fillOpacity": 0.35, "matchClipFilter": true }]` — `"legend"`
105
+ takes colors from a legend with `assignColors` on the SAME viewport;
106
+ `matchClipFilter` restricts fills to items inside the clip.
107
+ - `clip`: crop to matching items — `{ "class": "space", "where": {…}
108
+ (required), "expandBy": 1.5, "clipLabels": true, "color": "#000",
109
+ "widthMm": 0.7, "dash": "6 2", "opacity": 1 }`. The boundary is the matched
110
+ items' polygons expanded `expandBy` metres.
111
+ - `highlight`: restyle matching items — `{ "class": …, "where": {…}
112
+ (required), "color": "#f00", "widthMm": 0.5, "fill": "#ff0",
113
+ "fillOpacity": 1, "intersectsViewport": "otherViewportId"? }`.
114
+ - Highlight/clip classes are **exempt** from `includeClasses` — a highlighted
115
+ space still shows in a walls-only view.
116
+ - `includeClasses` gates `fills` too: a fill rule for an excluded class draws
117
+ nothing. Exception: a rule with `"matchClipFilter": true` under an active
118
+ clip follows the clip's item set instead (the clipped apartment's space
119
+ fills render even when spaces are excluded from drawing).
120
+ - `includeClasses` also scopes COMPUTATION: only the viewport's include
121
+ classes (plus its highlight/clip/fill/label/legend classes) take part in
122
+ section generation — excluded geometry neither draws nor occludes, and
123
+ costs nothing. Always list `includeClasses`; a viewport without them
124
+ computes against the entire model and is dramatically slower on big twins.
125
+ - Include entries can carry per-class behavior flags (both default `true`):
126
+ `{ "class": "flow", "occludes": false, "classify": false }` —
127
+ `occludes: false` = the class hides nothing (a pipe run must not erase the
128
+ wall behind it); `classify: false` = its own lines skip hidden-line testing
129
+ and draw as-is. Use both for schematic MEP linework — it is also the single
130
+ biggest performance lever when including dense curved classes like pipes.
131
+ - `border`: `{ "stroke": "#000", "strokeWidthMm": 0.25, "dash"? }`.
132
+ - `northArrow`: `{ "resourceId": …, "sizeMm": 12 }` — anchored top-right.
133
+ - Maximum **8 viewports** per template.
134
+
135
+ ### text
136
+ `{ "kind": "text", "id", "xMm", "yMm", "text", "textStyle", "align"? }`.
137
+ `xMm/yMm` anchor the **baseline**. Interpolations: `{{vars.*}}` and
138
+ `{{viewport.<id>.scaleText}}` (renders e.g. `1:100`).
139
+
140
+ ### labels — per-item text at item centroids (PLAN viewports only)
141
+ `{ "kind": "labels", "id", "viewport" (required), "text" (required),
142
+ "textStyle", "class"?, "where"?, "align"?, "matchClipFilter"? }`.
143
+ `text` interpolates item properties: `"{{BIP.spacename}} {{BIP.area | round:1}} m²"`.
144
+ Do not attach labels to elevation viewports — they would be misplaced.
145
+
146
+ ### legend — table of matching items
147
+ `{ "kind": "legend", "id", "viewport" (required), "columns" (required),
148
+ "class"?, "where"?, "clipToViewport"?, "textStyle", "anchor":
149
+ "top-right|top-left|bottom-right|bottom-left", "anchorPositionMm"?:
150
+ {xMm,yMm}, "groupBy"?: "BIP.spacename", "assignColors"?: { "enabled": true,
151
+ "palette": "auto" }, "showColorColumn"?, "summary"?: { "enabled": true,
152
+ "label": "Total" }, "border"/"grid"/"header"?: { stroke, strokeWidthMm } }`.
153
+ Columns: `[{ "id", "label", "property": "BIP.area", "format": { "digits": 1,
154
+ "suffix": " m²" }, "align"?, "summary": true? }]` — `summary: true` columns
155
+ are summed in the totals row. `groupBy` merges equal-valued rows (numeric
156
+ columns summed). `assignColors` + a viewport fill `"fill": "legend"` =
157
+ color-coded plan matching the legend swatches.
158
+
159
+ ### shapes — line / rect / circle / polyline / polygon (page mm)
160
+ - `line`: `{ x1Mm, y1Mm, x2Mm, y2Mm, stroke?, strokeWidthMm?, dash? }`
161
+ - `rect`: `{ xMm, yMm, widthMm, heightMm, stroke?, strokeWidthMm?, fill?, dash? }`
162
+ - `circle`: `{ cxMm, cyMm, rMm (required), stroke?, strokeWidthMm?, fill?, dash? }`
163
+ - `polyline`: `{ points: [{xMm,yMm}…] (≥2), stroke (required), strokeWidthMm (required), dash? }`
164
+ - `polygon`: `{ points (≥3), stroke?, strokeWidthMm?, fill?, dash? }`
165
+
166
+ ### scaleBar
167
+ `{ "kind": "scaleBar", "id", "xMm", "yMm", "maxLengthMm" (required),
168
+ "segments": 10, "labelEvery": 2, "unit": "m", "stroke", "strokeWidthMm",
169
+ "textStyle" (needed for the numbers), "viewport"? }`.
170
+
171
+ ### qr
172
+ `{ "kind": "qr", "id", "xMm", "yMm", "sizeMm" (required), "role":
173
+ "main"|"anchor"|"custom", "viewport"?, "anchorCorner"?, "dataTemplate"? }`.
174
+ `main` (default) encodes a viewer link at the drawing location; `anchor` +
175
+ `anchorCorner` are AR positioning markers; `custom` + `dataTemplate` is an
176
+ arbitrary interpolated payload.
177
+
178
+ ### image
179
+ `{ "kind": "image", "id", "xMm", "yMm", "widthMm", "heightMm", "resourceId"
180
+ (required), "fit": "contain"|"cover"|"stretch" }`.
181
+
182
+ ### group
183
+ `{ "kind": "group", "id", "children": […], "transform"?:
184
+ { translateXMm, translateYMm, rotateDeg, scaleX, scaleY }, "opacity"?,
185
+ "clip"?: { "type": "rect", xMm, yMm, widthMm, heightMm } }`.
186
+ Note: `labels`/`qr` children render as page-level overlays — the group's
187
+ transform does not move them.
188
+
189
+ ## Where-conditions (used by clip/highlight/fills/labels/legend/includeClasses)
190
+
191
+ `{ "BIP.spacetype": "ROOM" }` — key is `<PropertySet>.<property>`, value is
192
+ a scalar (case-insensitive equality) or an array (any-of). Multiple keys AND
193
+ together. Values may reference variables: `{ "BIP.spacenumber":
194
+ "{{vars.spacenumber}}" }`.
195
+
196
+ ## Placeholder format filters
197
+
198
+ Any `{{…}}` placeholder accepts `| round:N` (round, trim trailing zeros) or
199
+ `| fixed:N` (exactly N decimals): `{{BIP.area | round:1}}` → `344.6`.
200
+ Raw numeric properties are unrounded doubles — **always filter area-like
201
+ values**.
202
+
203
+ ## Minimal valid skeleton
204
+
205
+ ```json
206
+ {
207
+ "id": "basic-plan",
208
+ "page": { "widthMm": 420, "heightMm": 297 },
209
+ "styles": { "body": { "fontFamily": "Arial", "fontSizeMm": 3.5, "color": "#000000" } },
210
+ "elements": [
211
+ {
212
+ "kind": "viewport",
213
+ "id": "main",
214
+ "xMm": 5, "yMm": 15, "widthMm": 410, "heightMm": 260,
215
+ "cutHeight": 3, "depth": { "above": 0.5, "below": 1.5 },
216
+ "scaleMode": "fitAndRound", "preferredScales": [50, 100, 200, 400],
217
+ "includeClasses": ["walls", "windows", "doors", "columns", "stairs", "floor"],
218
+ "lineStyles": {
219
+ "default": { "widthMm": 0.25, "color": "#1f2937" },
220
+ "hiddenLines": { "widthMm": 0.13, "color": "#9ca3af", "dash": "4 2" }
221
+ }
222
+ },
223
+ { "kind": "text", "id": "scale", "xMm": 210, "yMm": 290, "textStyle": "body",
224
+ "align": "center", "text": "Scale: {{viewport.main.scaleText}}" }
225
+ ]
226
+ }
227
+ ```
228
+
229
+ For a full-featured example (variables, clip + expand, highlights, legend
230
+ with colors + summary, labels, plan + section viewports) see the
231
+ `hyresgastbilaga` template in the template-workbench gallery.
232
+
233
+ ## Checklist before you emit the JSON
234
+
235
+ 1. Valid JSON, unique element `id`s, ≤ 8 viewports.
236
+ 2. Every viewport has `preferredScales` (or `scalePreset`) and exactly one
237
+ section definition (`cutHeight` XOR `planeNormal`+`planePosition`).
238
+ 3. Every `textStyle` reference exists in `styles`; scaleBar has a
239
+ `textStyle` if it should show numbers.
240
+ 4. Every variable has a `default`; every `{{vars.*}}` is declared.
241
+ 5. `labels` only on plan viewports; numeric placeholders filtered
242
+ (`| round:1`).
243
+ 6. `fill: "legend"` only if a legend with `assignColors` targets the same
244
+ viewport.
245
+ 7. Every `resourceId` exists in `resources`; remote URLs have
246
+ `allowRemoteResources: true`.
247
+ 8. Units sanity: mm on the page, metres in the model.