@twinfinity/printing 5.0.3-ci.28538-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 (209) hide show
  1. package/README.md +208 -0
  2. package/dist/BuildInfo.d.ts +25 -0
  3. package/dist/BuildInfo.d.ts.map +1 -0
  4. package/dist/BuildInfo.js +15 -0
  5. package/dist/BuildInfo.js.map +1 -0
  6. package/dist/index.d.ts +7 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +9 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/layout/analyzeTemplate.d.ts +29 -0
  11. package/dist/layout/analyzeTemplate.d.ts.map +1 -0
  12. package/dist/layout/analyzeTemplate.js +168 -0
  13. package/dist/layout/analyzeTemplate.js.map +1 -0
  14. package/dist/layout/clipUtils.d.ts +18 -0
  15. package/dist/layout/clipUtils.d.ts.map +1 -0
  16. package/dist/layout/clipUtils.js +165 -0
  17. package/dist/layout/clipUtils.js.map +1 -0
  18. package/dist/layout/colorConstants.d.ts +11 -0
  19. package/dist/layout/colorConstants.d.ts.map +1 -0
  20. package/dist/layout/colorConstants.js +14 -0
  21. package/dist/layout/colorConstants.js.map +1 -0
  22. package/dist/layout/elementKinds.d.ts +21 -0
  23. package/dist/layout/elementKinds.d.ts.map +1 -0
  24. package/dist/layout/elementKinds.js +17 -0
  25. package/dist/layout/elementKinds.js.map +1 -0
  26. package/dist/layout/exportDrawing.d.ts +143 -0
  27. package/dist/layout/exportDrawing.d.ts.map +1 -0
  28. package/dist/layout/exportDrawing.js +146 -0
  29. package/dist/layout/exportDrawing.js.map +1 -0
  30. package/dist/layout/filters.d.ts +33 -0
  31. package/dist/layout/filters.d.ts.map +1 -0
  32. package/dist/layout/filters.js +77 -0
  33. package/dist/layout/filters.js.map +1 -0
  34. package/dist/layout/geometryConstants.d.ts +8 -0
  35. package/dist/layout/geometryConstants.d.ts.map +1 -0
  36. package/dist/layout/geometryConstants.js +10 -0
  37. package/dist/layout/geometryConstants.js.map +1 -0
  38. package/dist/layout/index.d.ts +86 -0
  39. package/dist/layout/index.d.ts.map +1 -0
  40. package/dist/layout/index.js +363 -0
  41. package/dist/layout/index.js.map +1 -0
  42. package/dist/layout/itemBuilders.d.ts +29 -0
  43. package/dist/layout/itemBuilders.d.ts.map +1 -0
  44. package/dist/layout/itemBuilders.js +135 -0
  45. package/dist/layout/itemBuilders.js.map +1 -0
  46. package/dist/layout/labelsRender.d.ts +29 -0
  47. package/dist/layout/labelsRender.d.ts.map +1 -0
  48. package/dist/layout/labelsRender.js +109 -0
  49. package/dist/layout/labelsRender.js.map +1 -0
  50. package/dist/layout/legendConstants.d.ts +12 -0
  51. package/dist/layout/legendConstants.d.ts.map +1 -0
  52. package/dist/layout/legendConstants.js +12 -0
  53. package/dist/layout/legendConstants.js.map +1 -0
  54. package/dist/layout/legendRender.d.ts +13 -0
  55. package/dist/layout/legendRender.d.ts.map +1 -0
  56. package/dist/layout/legendRender.js +424 -0
  57. package/dist/layout/legendRender.js.map +1 -0
  58. package/dist/layout/legendRows.d.ts +26 -0
  59. package/dist/layout/legendRows.d.ts.map +1 -0
  60. package/dist/layout/legendRows.js +45 -0
  61. package/dist/layout/legendRows.js.map +1 -0
  62. package/dist/layout/legendUtils.d.ts +21 -0
  63. package/dist/layout/legendUtils.d.ts.map +1 -0
  64. package/dist/layout/legendUtils.js +110 -0
  65. package/dist/layout/legendUtils.js.map +1 -0
  66. package/dist/layout/metadata.d.ts +43 -0
  67. package/dist/layout/metadata.d.ts.map +1 -0
  68. package/dist/layout/metadata.js +191 -0
  69. package/dist/layout/metadata.js.map +1 -0
  70. package/dist/layout/model.d.ts +15 -0
  71. package/dist/layout/model.d.ts.map +1 -0
  72. package/dist/layout/model.js +310 -0
  73. package/dist/layout/model.js.map +1 -0
  74. package/dist/layout/options.d.ts +17 -0
  75. package/dist/layout/options.d.ts.map +1 -0
  76. package/dist/layout/options.js +2 -0
  77. package/dist/layout/options.js.map +1 -0
  78. package/dist/layout/presets.d.ts +23 -0
  79. package/dist/layout/presets.d.ts.map +1 -0
  80. package/dist/layout/presets.js +41 -0
  81. package/dist/layout/presets.js.map +1 -0
  82. package/dist/layout/qrRender.d.ts +13 -0
  83. package/dist/layout/qrRender.d.ts.map +1 -0
  84. package/dist/layout/qrRender.js +97 -0
  85. package/dist/layout/qrRender.js.map +1 -0
  86. package/dist/layout/renderDrawing.d.ts +62 -0
  87. package/dist/layout/renderDrawing.d.ts.map +1 -0
  88. package/dist/layout/renderDrawing.js +555 -0
  89. package/dist/layout/renderDrawing.js.map +1 -0
  90. package/dist/layout/renderUtils.d.ts +36 -0
  91. package/dist/layout/renderUtils.d.ts.map +1 -0
  92. package/dist/layout/renderUtils.js +117 -0
  93. package/dist/layout/renderUtils.js.map +1 -0
  94. package/dist/layout/sectionValidation.d.ts +3 -0
  95. package/dist/layout/sectionValidation.d.ts.map +1 -0
  96. package/dist/layout/sectionValidation.js +2 -0
  97. package/dist/layout/sectionValidation.js.map +1 -0
  98. package/dist/layout/templateVariables.d.ts +137 -0
  99. package/dist/layout/templateVariables.d.ts.map +1 -0
  100. package/dist/layout/templateVariables.js +295 -0
  101. package/dist/layout/templateVariables.js.map +1 -0
  102. package/dist/layout/typeGuards.d.ts +5 -0
  103. package/dist/layout/typeGuards.d.ts.map +1 -0
  104. package/dist/layout/typeGuards.js +9 -0
  105. package/dist/layout/typeGuards.js.map +1 -0
  106. package/dist/layout/types.d.ts +816 -0
  107. package/dist/layout/types.d.ts.map +1 -0
  108. package/dist/layout/types.js +2 -0
  109. package/dist/layout/types.js.map +1 -0
  110. package/dist/layout/utils.d.ts +9 -0
  111. package/dist/layout/utils.d.ts.map +1 -0
  112. package/dist/layout/utils.js +84 -0
  113. package/dist/layout/utils.js.map +1 -0
  114. package/dist/layout/validation.d.ts +7 -0
  115. package/dist/layout/validation.d.ts.map +1 -0
  116. package/dist/layout/validation.js +352 -0
  117. package/dist/layout/validation.js.map +1 -0
  118. package/dist/layout/validationHelpers.d.ts +15 -0
  119. package/dist/layout/validationHelpers.d.ts.map +1 -0
  120. package/dist/layout/validationHelpers.js +43 -0
  121. package/dist/layout/validationHelpers.js.map +1 -0
  122. package/dist/layout/validationUtils.d.ts +5 -0
  123. package/dist/layout/validationUtils.d.ts.map +1 -0
  124. package/dist/layout/validationUtils.js +5 -0
  125. package/dist/layout/validationUtils.js.map +1 -0
  126. package/dist/layout/viewportPrep.d.ts +47 -0
  127. package/dist/layout/viewportPrep.d.ts.map +1 -0
  128. package/dist/layout/viewportPrep.js +275 -0
  129. package/dist/layout/viewportPrep.js.map +1 -0
  130. package/dist/layout/viewportRender.d.ts +42 -0
  131. package/dist/layout/viewportRender.d.ts.map +1 -0
  132. package/dist/layout/viewportRender.js +337 -0
  133. package/dist/layout/viewportRender.js.map +1 -0
  134. package/dist/layout/viewportUtils.d.ts +9 -0
  135. package/dist/layout/viewportUtils.d.ts.map +1 -0
  136. package/dist/layout/viewportUtils.js +40 -0
  137. package/dist/layout/viewportUtils.js.map +1 -0
  138. package/dist/layout/warnings.d.ts +2 -0
  139. package/dist/layout/warnings.d.ts.map +1 -0
  140. package/dist/layout/warnings.js +4 -0
  141. package/dist/layout/warnings.js.map +1 -0
  142. package/dist/printToPdf.d.ts +38 -0
  143. package/dist/printToPdf.d.ts.map +1 -0
  144. package/dist/printToPdf.js +79 -0
  145. package/dist/printToPdf.js.map +1 -0
  146. package/dist/printToSvg.d.ts +11 -0
  147. package/dist/printToSvg.d.ts.map +1 -0
  148. package/dist/printToSvg.js +168 -0
  149. package/dist/printToSvg.js.map +1 -0
  150. package/dist/sections.d.ts +118 -0
  151. package/dist/sections.d.ts.map +1 -0
  152. package/dist/sections.js +839 -0
  153. package/dist/sections.js.map +1 -0
  154. package/dist/templates/default-template.json +137 -0
  155. package/dist/types.d.ts +54 -0
  156. package/dist/types.d.ts.map +1 -0
  157. package/dist/types.js +11 -0
  158. package/dist/types.js.map +1 -0
  159. package/package.json +47 -0
  160. package/src/BuildInfo.ts +41 -0
  161. package/src/index.ts +10 -0
  162. package/src/layout/analyzeTemplate.ts +218 -0
  163. package/src/layout/clipUtils.ts +193 -0
  164. package/src/layout/colorConstants.ts +16 -0
  165. package/src/layout/elementKinds.ts +35 -0
  166. package/src/layout/exportDrawing.ts +206 -0
  167. package/src/layout/filters.ts +80 -0
  168. package/src/layout/geometryConstants.ts +11 -0
  169. package/src/layout/index.ts +671 -0
  170. package/src/layout/itemBuilders.ts +159 -0
  171. package/src/layout/labelsRender.ts +170 -0
  172. package/src/layout/legendConstants.ts +11 -0
  173. package/src/layout/legendRender.ts +543 -0
  174. package/src/layout/legendRows.ts +62 -0
  175. package/src/layout/legendUtils.ts +144 -0
  176. package/src/layout/metadata.ts +210 -0
  177. package/src/layout/model.ts +372 -0
  178. package/src/layout/options.ts +17 -0
  179. package/src/layout/presets.ts +51 -0
  180. package/src/layout/qrRender.ts +126 -0
  181. package/src/layout/qrcode.d.ts +2 -0
  182. package/src/layout/renderDrawing.ts +710 -0
  183. package/src/layout/renderUtils.ts +138 -0
  184. package/src/layout/sectionValidation.ts +2 -0
  185. package/src/layout/templateVariables.ts +317 -0
  186. package/src/layout/typeGuards.ts +9 -0
  187. package/src/layout/types.ts +835 -0
  188. package/src/layout/utils.ts +85 -0
  189. package/src/layout/validation.ts +392 -0
  190. package/src/layout/validationHelpers.ts +59 -0
  191. package/src/layout/validationUtils.ts +6 -0
  192. package/src/layout/viewportPrep.ts +351 -0
  193. package/src/layout/viewportRender.ts +442 -0
  194. package/src/layout/viewportUtils.ts +50 -0
  195. package/src/layout/warnings.ts +3 -0
  196. package/src/printToPdf.ts +108 -0
  197. package/src/printToSvg.ts +188 -0
  198. package/src/sections.ts +1019 -0
  199. package/src/templates/default-template.json +137 -0
  200. package/src/types.ts +49 -0
  201. package/templates/default-a0-landscape.json +209 -0
  202. package/templates/default-a0-portrait.json +195 -0
  203. package/templates/default-a1-landscape.json +167 -0
  204. package/templates/default-a1-portrait.json +153 -0
  205. package/templates/default-a3-landscape.json +111 -0
  206. package/templates/default-a3-portrait.json +125 -0
  207. package/templates/default-a4-landscape.json +97 -0
  208. package/templates/default-a4-portrait.json +97 -0
  209. package/templates/template-schema.json +1395 -0
@@ -0,0 +1,1395 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "twinfinity-print-template-v2",
4
+ "title": "Twinfinity Print Template",
5
+ "description": "Layout template for Twinfinity BIM section printing. Defines page size, viewports, legends, labels, and decorative elements for PDF/SVG output.",
6
+ "markdownDescription": "# Twinfinity Print Template\n\nLayout template for automated BIM drawing generation.\n\n## Template Structure\n\n- **`page`** — Page dimensions (A4, A3, custom)\n- **`styles`** — Named text styles (body, title, etc.)\n- **`assets`** — Reusable images/logos\n- **`presets`** — Reusable class filters and scale lists\n- **`elements`** — All content on the page\n\n## Available Elements\n\n| Element | Purpose |\n|---------|--------|\n| `viewport` | BIM section geometry at a position/scale |\n| `text` | Static or templated text |\n| `labels` | Per-item labels (room names, etc.) |\n| `legend` | Table with item properties and colors |\n| `scaleBar` | Graphical scale indicator |\n| `qr` | QR codes for navigation/anchoring |\n| `image` | Logo or decorative image |\n| `line`, `rect`, `circle` | Basic shapes |\n| `polyline`, `polygon` | Custom vector shapes |\n| `group` | Container with transforms/clipping |\n\n## Viewport Modes\n\n- **SLAB** — Optimized for horizontal floor plans (uses depth above/below)\n- **PLANE** — Arbitrary plane cuts (vertical sections, diagonals)\n\nAuto-detected from `planeNormal` orientation if not explicitly set.\n\n## Filtering & Highlighting\n\n- **`includeClasses`** — Which IFC classes to render\n- **`clip`** — Crop viewport to specific areas\n- **`highlight`** — Custom stroke/fill for matching items\n- **`fills`** — Filled polygons for spaces or other classes\n\n## Template Variables\n\nUse `{{variable}}` syntax in text elements:\n- `{{viewport.<id>.scaleText}}` — e.g. \"1:100\"\n- `{{buildStamp}}` — Build timestamp\n- `{{BIP.spacename}}` — Property values in labels",
7
+ "type": "object",
8
+ "required": ["page", "elements"],
9
+ "additionalProperties": false,
10
+ "defaultSnippets": [
11
+ {
12
+ "label": "New template",
13
+ "description": "Full template skeleton with page, styles, presets, and a viewport",
14
+ "body": {
15
+ "$$schema": "./template-schema.json",
16
+ "id": "${1:my-template}",
17
+ "page": { "widthMm": ${2:420}, "heightMm": ${3:297}, "marginMm": 10, "backgroundColor": "#ffffff" },
18
+ "styles": {
19
+ "body": { "fontFamily": "Arial", "fontSizeMm": 3.5, "color": "#000000" },
20
+ "spaceLabels": { "fontFamily": "Arial", "fontSizeMm": 2.5, "color": "#000000" }
21
+ },
22
+ "presets": {
23
+ "includeClasses": {
24
+ "defaultBuilding": ["space", "walls", "windows", "doors", "columns", "stairs", "floor"]
25
+ },
26
+ "scales": {
27
+ "default": { "preferredScales": [50, 100, 150, 200, 250, 500, 1000] }
28
+ }
29
+ },
30
+ "elements": [
31
+ {
32
+ "kind": "viewport",
33
+ "id": "main",
34
+ "xMm": 10,
35
+ "yMm": 30,
36
+ "widthMm": 400,
37
+ "heightMm": 240,
38
+ "includeClasses": { "preset": "defaultBuilding" },
39
+ "scaleMode": "fitAndRound",
40
+ "scalePreset": "default",
41
+ "align": "center",
42
+ "fills": [{ "class": "space", "fill": "sample", "fillOpacity": 0.35 }]
43
+ },
44
+ {
45
+ "kind": "text",
46
+ "id": "title",
47
+ "xMm": 10,
48
+ "yMm": 15,
49
+ "textStyle": "body",
50
+ "text": "${4:Drawing title} — {{viewport.main.scaleText}}"
51
+ },
52
+ {
53
+ "kind": "scaleBar",
54
+ "id": "scale-bar",
55
+ "xMm": 10,
56
+ "yMm": 280,
57
+ "maxLengthMm": 100,
58
+ "unit": "m",
59
+ "segments": 4,
60
+ "stroke": "#000000",
61
+ "strokeWidthMm": 0.25,
62
+ "textStyle": "body"
63
+ }
64
+ ]
65
+ }
66
+ },
67
+ {
68
+ "label": "Minimal template",
69
+ "description": "Bare minimum: page + one viewport",
70
+ "body": {
71
+ "$$schema": "./template-schema.json",
72
+ "id": "${1:minimal}",
73
+ "page": { "widthMm": ${2:420}, "heightMm": ${3:297} },
74
+ "elements": [
75
+ {
76
+ "kind": "viewport",
77
+ "id": "main",
78
+ "xMm": 10,
79
+ "yMm": 10,
80
+ "widthMm": 400,
81
+ "heightMm": 277,
82
+ "scaleMode": "fitAndRound",
83
+ "align": "center"
84
+ }
85
+ ]
86
+ }
87
+ }
88
+ ],
89
+ "properties": {
90
+ "$schema": {
91
+ "type": "string",
92
+ "description": "Path to this JSON schema file (for editor autocompletion).",
93
+ "markdownDescription": "Path to this JSON schema file.\n\n**Always set to:** `\"./template-schema.json\"`\n\nEnables autocomplete, validation, and inline documentation in VS Code and other editors."
94
+ },
95
+ "id": {
96
+ "type": "string",
97
+ "description": "Unique identifier for this template.",
98
+ "markdownDescription": "Unique identifier for this template.\n\n**Used for:**\n- Template selection in UI\n- Logging and debugging\n- File naming conventions\n\n**Format:** `kebab-case` (lowercase with hyphens)\n\n**Examples:** `floor-plan-a3`, `section-detail`, `tenant-appendix`"
99
+ },
100
+ "page": {
101
+ "$ref": "#/$defs/page",
102
+ "description": "Page dimensions and background."
103
+ },
104
+ "styles": {
105
+ "$ref": "#/$defs/styles",
106
+ "description": "Named text styles (body, title, etc.).",
107
+ "markdownDescription": "Named text styles referenced by elements.\n\n**Define reusable styles:**\n```json\n\"styles\": {\n \"body\": { \"fontFamily\": \"Arial\", \"fontSizeMm\": 3.5, \"color\": \"#000000\" },\n \"title\": { \"fontFamily\": \"Arial\", \"fontSizeMm\": 8, \"fontWeight\": \"bold\" },\n \"small\": { \"fontFamily\": \"Arial\", \"fontSizeMm\": 2.5, \"color\": \"#666666\" }\n}\n```\n\n**Reference in elements:**\n```json\n{ \"kind\": \"text\", \"textStyle\": \"body\", \"text\": \"Hello\" }\n```"
108
+ },
109
+ "assets": {
110
+ "type": "array",
111
+ "description": "Reusable assets (images, SVG logos) referenced by elements via assetId.",
112
+ "markdownDescription": "Reusable assets (images, SVG logos, icons).\n\n**Define assets:**\n```json\n\"assets\": [\n {\n \"id\": \"logo-main\",\n \"src\": \"https://example.com/logo.svg\"\n },\n {\n \"id\": \"north-arrow\",\n \"src\": \"data:image/svg+xml;base64,...\"\n }\n]\n```\n\n**Reference in elements:**\n```json\n{ \"kind\": \"image\", \"assetId\": \"logo-main\", \"...\" }\n{ \"northArrow\": { \"assetId\": \"north-arrow\" } }\n```\n\n**⚠️ Remote URLs require:** `\"allowRemoteAssets\": true`",
113
+ "items": { "$ref": "#/$defs/asset" }
114
+ },
115
+ "elements": {
116
+ "type": "array",
117
+ "description": "All layout elements to render on the page.",
118
+ "markdownDescription": "All layout elements to render on the page.\n\n**Available element types:**\n- `viewport` — BIM section geometry\n- `text` — Static or template text\n- `labels` — Per-item labels (room names)\n- `legend` — Table with properties\n- `scaleBar` — Graphical scale\n- `qr` — QR codes\n- `image` — Logos, icons\n- `line`, `rect`, `circle` — Basic shapes\n- `polyline`, `polygon` — Custom vectors\n- `group` — Container with transforms\n\n**Order matters:**\n- Elements are rendered in array order\n- Later elements appear on top (unless `zIndex` is used)\n- Use `zIndex` for explicit layering",
119
+ "items": { "$ref": "#/$defs/element" }
120
+ },
121
+ "presets": {
122
+ "$ref": "#/$defs/presets",
123
+ "description": "Reusable presets for class filters and scales.",
124
+ "markdownDescription": "Reusable presets for class filters and scales.\n\n**Define presets:**\n```json\n\"presets\": {\n \"includeClasses\": {\n \"defaultBuilding\": [\"space\", \"walls\", \"windows\", \"doors\", \"columns\", \"stairs\", \"floor\"],\n \"structureOnly\": [\"walls\", \"columns\", \"beam\", \"floor\"]\n },\n \"scales\": {\n \"default\": { \"preferredScales\": [50, 100, 200, 500, 1000] },\n \"detail\": { \"preferredScales\": [2, 5, 10, 20, 50] }\n }\n}\n```\n\n**Reference in viewports:**\n```json\n\"includeClasses\": { \"preset\": \"defaultBuilding\" },\n\"scalePreset\": \"default\"\n```"
125
+ },
126
+ "allowRemoteAssets": {
127
+ "type": "boolean",
128
+ "default": false,
129
+ "description": "Allow fetching remote assets from https:// URLs. Required for remote images/logos.",
130
+ "markdownDescription": "Allow fetching remote assets from `https://` URLs.\n\n**Default:** `false` (only local/data URIs allowed)\n\n**Set to `true` when:**\n- Assets use `https://` URLs\n- Loading logos from a CDN\n- Fetching remote images\n\n**Example:**\n```json\n\"allowRemoteAssets\": true,\n\"assets\": [\n { \"id\": \"logo\", \"src\": \"https://example.com/logo.svg\" }\n]\n```\n\n**⚠️ Security note:** Only enable for trusted URLs."
131
+ }
132
+ },
133
+
134
+ "$defs": {
135
+ "cssColor": {
136
+ "type": "string",
137
+ "description": "CSS color value.",
138
+ "examples": ["#000000", "#ff0000", "#9ca3af", "none", "rgb(255,0,0)", "rgba(0,0,0,0.5)"]
139
+ },
140
+
141
+ "ifcClassName": {
142
+ "type": "string",
143
+ "description": "IFC class name. Use short aliases (space, walls, doors, etc.) or full IFC names (IfcWall, IfcDoor, etc.). Case-insensitive.",
144
+ "markdownDescription": "IFC class name. Case-insensitive.\n\n**Short aliases** (match all subtypes in category):\n`space` `walls` `windows` `doors` `columns` `beam` `stairs` `floor` `roof` `covering` `furniture` `reinforcement` `flow` `annotations` `other`\n\n**Full IFC names** also work: `IfcWall`, `IfcDoor`, `IfcSpace`, etc.",
145
+ "anyOf": [
146
+ {
147
+ "enum": [
148
+ "space", "walls", "windows", "doors", "columns", "beam", "stairs",
149
+ "floor", "roof", "covering", "furniture", "reinforcement", "flow",
150
+ "annotations", "other"
151
+ ],
152
+ "description": "Short alias — matches all IFC subtypes in this category."
153
+ },
154
+ {
155
+ "enum": [
156
+ "IfcWall", "IfcWallStandardCase", "IfcWallElementedCase",
157
+ "IfcDoor", "IfcDoorStandardCase",
158
+ "IfcWindow", "IfcWindowStandardCase",
159
+ "IfcColumn", "IfcColumnStandardCase",
160
+ "IfcBeam", "IfcBeamStandardCase",
161
+ "IfcSlab", "IfcSlabStandardCase", "IfcSlabElementedCase",
162
+ "IfcStair", "IfcStairFlight",
163
+ "IfcRamp", "IfcRampFlight",
164
+ "IfcRailing",
165
+ "IfcRoof",
166
+ "IfcSpace",
167
+ "IfcCurtainWall",
168
+ "IfcCovering",
169
+ "IfcPlate", "IfcPlateStandardCase",
170
+ "IfcMember", "IfcMemberStandardCase",
171
+ "IfcFooting",
172
+ "IfcPile",
173
+ "IfcBuildingElementProxy",
174
+ "IfcFurnishingElement", "IfcFurniture", "IfcSystemFurnitureElement",
175
+ "IfcOpeningElement", "IfcOpeningStandardCase",
176
+ "IfcChimney",
177
+ "IfcShadingDevice",
178
+ "IfcReinforcingBar", "IfcReinforcingMesh",
179
+ "IfcTendon", "IfcTendonAnchor", "IfcTendonConduit",
180
+ "IfcBuildingElementPart",
181
+ "IfcDiscreteAccessory", "IfcFastener", "IfcMechanicalFastener",
182
+ "IfcAnnotation",
183
+ "IfcGrid",
184
+ "IfcSite", "IfcBuilding", "IfcBuildingStorey", "IfcProject",
185
+ "IfcProxy",
186
+ "IfcDistributionElement", "IfcDistributionFlowElement",
187
+ "IfcDistributionControlElement", "IfcDistributionPort",
188
+ "IfcFlowTerminal", "IfcFlowSegment", "IfcFlowFitting",
189
+ "IfcFlowController", "IfcFlowMovingDevice",
190
+ "IfcFlowStorageDevice", "IfcFlowTreatmentDevice",
191
+ "IfcEnergyConversionDevice",
192
+ "IfcPipeFitting", "IfcPipeSegment",
193
+ "IfcDuctFitting", "IfcDuctSegment", "IfcDuctSilencer",
194
+ "IfcCableCarrierFitting", "IfcCableCarrierSegment",
195
+ "IfcCableFitting", "IfcCableSegment",
196
+ "IfcSanitaryTerminal", "IfcWasteTerminal",
197
+ "IfcFireSuppressionTerminal", "IfcStackTerminal",
198
+ "IfcAirTerminal", "IfcAirTerminalBox",
199
+ "IfcLightFixture", "IfcLamp",
200
+ "IfcOutlet", "IfcSwitchingDevice", "IfcJunctionBox",
201
+ "IfcElectricAppliance", "IfcElectricDistributionBoard",
202
+ "IfcElectricGenerator", "IfcElectricMotor",
203
+ "IfcSpaceHeater", "IfcCooledBeam",
204
+ "IfcBoiler", "IfcChiller", "IfcCompressor", "IfcCondenser",
205
+ "IfcHeatExchanger", "IfcHumidifier", "IfcEvaporator", "IfcEvaporativeCooler",
206
+ "IfcFan", "IfcPump", "IfcValve", "IfcDamper", "IfcFilter",
207
+ "IfcTank",
208
+ "IfcSensor", "IfcActuator", "IfcAlarm", "IfcController",
209
+ "IfcFlowMeter", "IfcFlowInstrument",
210
+ "IfcUnitaryEquipment", "IfcUnitaryControlElement",
211
+ "IfcTransformer", "IfcMotorConnection",
212
+ "IfcProtectiveDevice", "IfcProtectiveDeviceTrippingUnit",
213
+ "IfcDistributionBoard", "IfcDistributionChamberElement",
214
+ "IfcCommunicationsAppliance", "IfcAudioVisualAppliance",
215
+ "IfcMedicalDevice", "IfcTransportElement",
216
+ "IfcElementAssembly",
217
+ "IfcCivilElement", "IfcGeographicElement",
218
+ "IfcBridge", "IfcBridgePart",
219
+ "IfcRoad", "IfcRoadPart",
220
+ "IfcRailway", "IfcRailwayPart",
221
+ "IfcMarineFacility", "IfcMarinePart",
222
+ "IfcAlignment", "IfcReferent", "IfcLinearElement",
223
+ "IfcBuiltElement", "IfcVirtualElement",
224
+ "IfcSurfaceFeature", "IfcVoidingFeature",
225
+ "IfcEarthworksElement", "IfcEarthworksCut", "IfcEarthworksFill",
226
+ "IfcReinforcedSoil", "IfcGeomodel", "IfcGeoslice",
227
+ "IfcCourse", "IfcKerb", "IfcPavement",
228
+ "IfcSign", "IfcSignal",
229
+ "IfcRail", "IfcTrackElement",
230
+ "IfcBorehole", "IfcCaissonFoundation", "IfcDeepFoundation",
231
+ "IfcNavigationElement", "IfcMooringDevice",
232
+ "IfcImpactProtectionDevice", "IfcVibrationDamper", "IfcVibrationIsolator",
233
+ "IfcBearing", "IfcSolarDevice",
234
+ "IfcBurner", "IfcEngine", "IfcCoolingTower",
235
+ "IfcInterceptor", "IfcLiquidTerminal",
236
+ "IfcConveyorSegment", "IfcVehicle",
237
+ "IfcMobileTelecommunicationsAppliance",
238
+ "IfcElectricFlowStorageDevice", "IfcElectricFlowTreatmentDevice",
239
+ "IfcElectricTimeControl",
240
+ "IfcAirToAirHeatRecovery", "IfcTubeBundle",
241
+ "IfcSpatialZone", "IfcExternalSpatialElement",
242
+ "IfcFacility", "IfcFacilityPartCommon",
243
+ "IfcUnknown"
244
+ ],
245
+ "description": "Full IFC class name — matches this specific IFC type."
246
+ },
247
+ { "type": "string" }
248
+ ]
249
+ },
250
+
251
+ "page": {
252
+ "type": "object",
253
+ "description": "Page dimensions and background.",
254
+ "markdownDescription": "Page dimensions and background.\n\n**Common sizes:**\n| Size | Landscape | Portrait |\n|------|-----------|----------|\n| A4 | 297 × 210 | 210 × 297 |\n| A3 | 420 × 297 | 297 × 420 |\n| A2 | 594 × 420 | 420 × 594 |\n| A1 | 841 × 594 | 594 × 841 |",
255
+ "required": ["widthMm", "heightMm"],
256
+ "additionalProperties": false,
257
+ "properties": {
258
+ "widthMm": {
259
+ "type": "number",
260
+ "description": "Page width in millimeters.",
261
+ "markdownDescription": "Page width in millimeters.\n\nCommon values: `210` (A4 portrait), `297` (A4 landscape / A3 portrait), `420` (A3 landscape), `594` (A2 landscape / A1 portrait), `841` (A1 landscape)"
262
+ },
263
+ "heightMm": {
264
+ "type": "number",
265
+ "description": "Page height in millimeters.",
266
+ "markdownDescription": "Page height in millimeters.\n\nCommon values: `297` (A4 portrait / A3 landscape), `210` (A4 landscape), `420` (A3 portrait / A2 landscape), `594` (A1 landscape)"
267
+ },
268
+ "marginMm": { "type": "number", "default": 10, "description": "Page margin in mm." },
269
+ "backgroundColor": { "$ref": "#/$defs/cssColor", "default": "#ffffff", "description": "Page background color." }
270
+ }
271
+ },
272
+
273
+ "styles": {
274
+ "type": "object",
275
+ "description": "Named text styles referenced by elements via textStyle. Each key is a style name.",
276
+ "markdownDescription": "Named text styles referenced by elements via `textStyle`.\n\nEach key becomes a reusable style name. Example:\n```json\n\"styles\": {\n \"body\": { \"fontFamily\": \"Arial\", \"fontSizeMm\": 3.5 },\n \"title\": { \"fontFamily\": \"Arial\", \"fontSizeMm\": 8, \"fontWeight\": \"bold\" }\n}\n```",
277
+ "additionalProperties": { "$ref": "#/$defs/textStyle" }
278
+ },
279
+
280
+ "textStyle": {
281
+ "type": "object",
282
+ "description": "Text style definition.",
283
+ "required": ["fontFamily", "fontSizeMm"],
284
+ "additionalProperties": false,
285
+ "properties": {
286
+ "fontFamily": { "type": "string", "description": "CSS font-family.", "examples": ["Arial", "Helvetica", "Times New Roman", "Courier New"] },
287
+ "fontSizeMm": { "type": "number", "description": "Font size in millimeters (not points or pixels)." },
288
+ "fontWeight": {
289
+ "type": "string",
290
+ "description": "CSS font weight.",
291
+ "examples": ["normal", "bold", "lighter", "100", "300", "700"]
292
+ },
293
+ "color": { "$ref": "#/$defs/cssColor", "description": "Text fill color." },
294
+ "letterSpacingMm": { "type": "number", "description": "Letter spacing in mm." },
295
+ "lineHeightMm": { "type": "number", "description": "Line height in mm." }
296
+ }
297
+ },
298
+
299
+ "asset": {
300
+ "type": "object",
301
+ "description": "A reusable asset (image, SVG) that can be referenced by image elements.",
302
+ "required": ["id"],
303
+ "additionalProperties": false,
304
+ "properties": {
305
+ "id": { "type": "string", "description": "Unique asset ID, referenced by image elements via assetId." },
306
+ "src": {
307
+ "type": "string",
308
+ "description": "URL or data URI for the asset. Remote URLs require allowRemoteAssets: true.",
309
+ "markdownDescription": "URL or data URI for the asset.\n\n- **Data URI**: `data:image/svg+xml;base64,PHN2Zy...` (self-contained, no network)\n- **Remote URL**: `https://example.com/logo.svg` (requires `allowRemoteAssets: true`)",
310
+ "examples": [
311
+ "data:image/svg+xml;base64,PHN2Zy...",
312
+ "https://example.com/logo.svg"
313
+ ]
314
+ },
315
+ "content": { "type": "string", "description": "Inline SVG content (alternative to src)." }
316
+ }
317
+ },
318
+
319
+ "presets": {
320
+ "type": "object",
321
+ "description": "Reusable presets for class filters and scale preferences.",
322
+ "additionalProperties": false,
323
+ "properties": {
324
+ "includeClasses": {
325
+ "type": "object",
326
+ "description": "Named sets of IFC class names. Referenced by viewports via { \"preset\": \"presetName\" }.",
327
+ "markdownDescription": "Named sets of IFC class names. Referenced by viewports via `{ \"preset\": \"presetName\" }`.\n\nExample:\n```json\n\"includeClasses\": {\n \"defaultBuilding\": [\"space\", \"walls\", \"windows\", \"doors\", \"columns\", \"stairs\", \"floor\"],\n \"structureOnly\": [\"walls\", \"columns\", \"beam\", \"floor\"]\n}\n```",
328
+ "additionalProperties": {
329
+ "type": "array",
330
+ "items": { "$ref": "#/$defs/ifcClassName" },
331
+ "description": "Array of IFC class names to include. Names are case-insensitive."
332
+ }
333
+ },
334
+ "scales": {
335
+ "type": "object",
336
+ "description": "Named scale presets. Referenced by viewports via scalePreset.",
337
+ "markdownDescription": "Named scale presets. Referenced by viewports via `scalePreset`.\n\nValues are scale denominators: `[50, 100, 200]` means 1:50, 1:100, 1:200.\nThe renderer picks the best fit from these values.",
338
+ "additionalProperties": {
339
+ "type": "object",
340
+ "properties": {
341
+ "preferredScales": {
342
+ "type": "array",
343
+ "items": { "type": "number" },
344
+ "description": "Preferred scale denominators (e.g. [50, 100, 200] means 1:50, 1:100, 1:200)."
345
+ }
346
+ }
347
+ }
348
+ }
349
+ }
350
+ },
351
+
352
+ "whereCondition": {
353
+ "type": "object",
354
+ "description": "Filter BIM items by property values. Keys are short property paths. Multiple keys = AND. Array values = OR. String comparison is case-insensitive.",
355
+ "markdownDescription": "Filter BIM items by property values.\n\n**Keys** are short property paths like `BIP.spacetype` which resolve to `propertySets.BIP.spacetype.value`.\n\n**Rules:**\n- Multiple keys = **AND** (all must match)\n- Array values = **OR** (any value matches)\n- String comparison is **case-insensitive**\n\n**Examples:**\n```json\n{ \"BIP.spacetype\": \"ROOM\" }\n{ \"BIP.hyresobjektsnummer\": \"2022-F2008\" }\n{ \"BIP.spacetype\": [\"ROOM\", \"CORRIDOR\"] }\n```",
356
+ "additionalProperties": {
357
+ "oneOf": [
358
+ { "type": "string" },
359
+ { "type": "number" },
360
+ { "type": "boolean" },
361
+ {
362
+ "type": "array",
363
+ "items": {
364
+ "oneOf": [
365
+ { "type": "string" },
366
+ { "type": "number" }
367
+ ]
368
+ }
369
+ }
370
+ ]
371
+ },
372
+ "examples": [
373
+ { "BIP.spacetype": "ROOM" },
374
+ { "BIP.hyresobjektsnummer": "2022-F2008" },
375
+ { "BIP.spacetype": ["ROOM", "CORRIDOR"] }
376
+ ]
377
+ },
378
+
379
+ "clipConfig": {
380
+ "type": "object",
381
+ "description": "Clip the viewport to show only the area around matching BIM items (e.g. a specific tenant's spaces).",
382
+ "markdownDescription": "Clip the viewport to show only the area around matching BIM items.\n\nUse `class` + `where` to select which items define the clip boundary. The viewport is cropped to the bounding box of those items, optionally expanded by `expandByM`.\n\n**Example — clip to a tenant's spaces:**\n```json\n\"clip\": {\n \"class\": \"space\",\n \"where\": { \"BIP.hyresobjektsnummer\": \"2022-F2008\" },\n \"expandByM\": 1.5\n}\n```",
383
+ "required": ["where"],
384
+ "additionalProperties": false,
385
+ "properties": {
386
+ "class": {
387
+ "oneOf": [
388
+ { "$ref": "#/$defs/ifcClassName" },
389
+ { "type": "array", "items": { "$ref": "#/$defs/ifcClassName" } }
390
+ ],
391
+ "description": "IFC class filter (e.g. \"space\"). Only items of this class are used for the clip boundary."
392
+ },
393
+ "where": { "$ref": "#/$defs/whereCondition" },
394
+ "expandByM": {
395
+ "type": "number",
396
+ "default": 0,
397
+ "description": "Expand the clip boundary outward by this many meters. Use 0.5–1.5 to show surrounding context.",
398
+ "markdownDescription": "Expand the clip boundary outward by this many meters.\n\n**Purpose:** Show surrounding context (adjacent walls, neighboring rooms) while keeping the clip focused on the target area.\n\n**How it works:**\n1. The original clip polygon is calculated from matching items\n2. The polygon is expanded outward by `expandByM` meters (morphological dilation)\n3. Geometry **paths** are clipped to the **expanded** boundary (shows more context)\n4. Fill filtering uses the **original** (unexpanded) boundary (controlled by `matchClipFilter`)\n\n**Common values:**\n- `0.5` — Small context (0.5m around clip area)\n- `1.5` — Medium context (1.5m around clip area) **← Recommended**\n- `3.0` — Large context (3m around clip area)\n- `0` — No expansion (strict clip to exact boundary)\n\n**Example:**\nClipping to a tenant's spaces with 1.5m surrounding context:\n```json\n\"clip\": {\n \"class\": \"space\",\n \"where\": { \"BIP.hyresobjektsnummer\": \"2022-F2008\" },\n \"expandByM\": 1.5\n}\n```\n\n**⚠️ Note:** When using `expandByM` with fills, set `matchClipFilter: true` on fill rules to prevent adjacent spaces from being filled."
399
+ },
400
+ "clipLabels": {
401
+ "type": "boolean",
402
+ "default": false,
403
+ "description": "Also clip labels to the clip boundary (hide labels outside the clip)."
404
+ },
405
+ "stroke": {
406
+ "$ref": "#/$defs/cssColor",
407
+ "default": "#000000",
408
+ "description": "Border color around the clipped area. Set to \"none\" to hide the clip border."
409
+ },
410
+ "strokeWidthMm": {
411
+ "type": "number",
412
+ "default": 0.5,
413
+ "description": "Border width in mm around the clipped area."
414
+ },
415
+ "strokeOpacity": {
416
+ "type": "number",
417
+ "default": 1,
418
+ "minimum": 0,
419
+ "maximum": 1,
420
+ "description": "Border opacity (0 = invisible, 1 = fully opaque)."
421
+ },
422
+ "fill": {
423
+ "$ref": "#/$defs/cssColor",
424
+ "default": "none",
425
+ "description": "Fill color inside the clip boundary. Usually \"none\"."
426
+ },
427
+ "fillOpacity": {
428
+ "type": "number",
429
+ "default": 0,
430
+ "minimum": 0,
431
+ "maximum": 1,
432
+ "description": "Fill opacity (0 = transparent, 1 = fully opaque)."
433
+ }
434
+ }
435
+ },
436
+
437
+ "highlightConfig": {
438
+ "type": "object",
439
+ "description": "Highlight matching BIM items with custom stroke/fill colors (e.g. highlight a tenant's spaces in yellow).",
440
+ "markdownDescription": "Highlight matching BIM items with custom stroke/fill colors.\n\nUse `class` + `where` to select which items get highlighted. Matching items are drawn with the specified stroke and fill colors.\n\n**Example — highlight tenant spaces in yellow:**\n```json\n\"highlight\": {\n \"class\": \"space\",\n \"where\": { \"BIP.hyresobjektsnummer\": \"2022-F2008\" },\n \"stroke\": \"#f00\",\n \"fill\": \"#ff0\",\n \"fillOpacity\": 1\n}\n```\n\n**Example — highlight spaces that appear in both plan and section:**\n```json\n\"highlight\": {\n \"class\": \"space\",\n \"where\": { \"BIP.hyresobjektsnummer\": \"2022-F2008\" },\n \"intersectsViewport\": \"main\",\n \"stroke\": \"#f00\",\n \"fill\": \"#ff0\",\n \"fillOpacity\": 1\n}\n```",
441
+ "required": ["where"],
442
+ "additionalProperties": false,
443
+ "properties": {
444
+ "class": {
445
+ "oneOf": [
446
+ { "$ref": "#/$defs/ifcClassName" },
447
+ { "type": "array", "items": { "$ref": "#/$defs/ifcClassName" } }
448
+ ],
449
+ "description": "IFC class filter (e.g. \"space\")."
450
+ },
451
+ "where": { "$ref": "#/$defs/whereCondition" },
452
+ "intersectsViewport": {
453
+ "type": "string",
454
+ "description": "Viewport ID to intersect with. Only highlights items that appear in BOTH this viewport and the referenced viewport (i.e., their geometry intersects both cutting planes).",
455
+ "markdownDescription": "Viewport ID to intersect with.\n\nOnly highlights items that appear in **both** this viewport and the referenced viewport. Useful for highlighting spaces at a specific level in a section view.\n\n**Example:** In a vertical section, highlight only the spaces that also appear in the plan view at Y=3:\n```json\n\"intersectsViewport\": \"plan\"\n```"
456
+ },
457
+ "stroke": {
458
+ "$ref": "#/$defs/cssColor",
459
+ "default": "#ff0000",
460
+ "description": "Stroke color for highlighted items."
461
+ },
462
+ "strokeWidthMm": {
463
+ "type": "number",
464
+ "default": 0.5,
465
+ "description": "Stroke width in mm for highlighted items."
466
+ },
467
+ "strokeOpacity": {
468
+ "type": "number",
469
+ "default": 1,
470
+ "minimum": 0,
471
+ "maximum": 1,
472
+ "description": "Stroke opacity for highlighted items."
473
+ },
474
+ "fill": {
475
+ "$ref": "#/$defs/cssColor",
476
+ "default": "#ffff00",
477
+ "description": "Fill color for highlighted items."
478
+ },
479
+ "fillOpacity": {
480
+ "type": "number",
481
+ "default": 0.5,
482
+ "minimum": 0,
483
+ "maximum": 1,
484
+ "description": "Fill opacity for highlighted items (0.5 = semi-transparent yellow by default)."
485
+ }
486
+ }
487
+ },
488
+
489
+ "viewportLineStyle": {
490
+ "type": "object",
491
+ "description": "Style for a single line category.",
492
+ "additionalProperties": false,
493
+ "properties": {
494
+ "widthMm": { "type": "number", "description": "Line width in mm." },
495
+ "color": { "$ref": "#/$defs/cssColor", "description": "Line color." },
496
+ "dash": {
497
+ "type": "string",
498
+ "description": "SVG stroke-dasharray pattern. Empty string for solid.",
499
+ "markdownDescription": "SVG `stroke-dasharray` pattern.\n\n- `\"\"` — solid line\n- `\"4 2\"` — dashed (4mm dash, 2mm gap)\n- `\"2 1\"` — short dashes\n- `\"1 1\"` — dotted",
500
+ "examples": ["", "4 2", "2 1", "1 1"]
501
+ },
502
+ "enabled": {
503
+ "type": "boolean",
504
+ "default": true,
505
+ "description": "Whether this line category is rendered. Set to false to hide it completely."
506
+ }
507
+ }
508
+ },
509
+
510
+ "viewportLineStyles": {
511
+ "type": "object",
512
+ "description": "Per-category line styles for a viewport. Resolution order: category-specific → default → built-in defaults.",
513
+ "markdownDescription": "Per-category line styles for a viewport.\n\n**Resolution order:** category-specific → `default` → built-in defaults\n\n**Line categories:**\n\n| Category | Description | Typical Style |\n|----------|-------------|---------------|\n| `cutLines` | Where plane cuts geometry | Thick, solid black |\n| `visibleLines` | Visible edges below plane | Medium, solid black |\n| `hiddenLines` | Hidden behind geometry | Thin, dashed gray |\n| `silhouettes` | Outline silhouettes | Medium, solid black |\n| `featureEdges` | Sharp edges (angle-based) | Thin, solid gray |\n| `stairArrows` | Direction arrows on stairs | Medium, solid black |\n| `outlineVisible` | Visible outline edges | Medium, solid black |\n| `outlineHidden` | Hidden outline edges | Thin, dashed gray |\n\n**Example — Standard floor plan:**\n```json\n\"lineStyles\": {\n \"default\": { \"widthMm\": 0.01, \"color\": \"#1f2937\" },\n \"cutLines\": { \"widthMm\": 0.02, \"color\": \"#000000\" },\n \"hiddenLines\": { \"widthMm\": 0.005, \"color\": \"#9ca3af\", \"dash\": \"4 2\" },\n \"stairArrows\": { \"widthMm\": 0.015, \"color\": \"#1f2937\" }\n}\n```\n\n**Example — Hide specific categories:**\n```json\n\"lineStyles\": {\n \"default\": { \"widthMm\": 0.01, \"color\": \"#000000\" },\n \"hiddenLines\": { \"enabled\": false },\n \"featureEdges\": { \"enabled\": false }\n}\n```\n\n**⚠️ Performance tip:** Set `\"enabled\": false` for categories you don't need to speed up rendering.",
514
+ "additionalProperties": false,
515
+ "properties": {
516
+ "default": {
517
+ "$ref": "#/$defs/viewportLineStyle",
518
+ "description": "Fallback style for any category not explicitly listed."
519
+ },
520
+ "cutLines": { "$ref": "#/$defs/viewportLineStyle", "description": "Lines where the section plane cuts through geometry." },
521
+ "visibleLines": { "$ref": "#/$defs/viewportLineStyle", "description": "Visible edges below the cut plane." },
522
+ "hiddenLines": { "$ref": "#/$defs/viewportLineStyle", "description": "Edges hidden behind other geometry. Typically dashed and lighter." },
523
+ "silhouettes": { "$ref": "#/$defs/viewportLineStyle", "description": "Outline silhouettes of objects." },
524
+ "featureEdges": { "$ref": "#/$defs/viewportLineStyle", "description": "Sharp edges detected by angle threshold." },
525
+ "stairArrows": { "$ref": "#/$defs/viewportLineStyle", "description": "Directional arrows on stairs." },
526
+ "outlineVisible": { "$ref": "#/$defs/viewportLineStyle", "description": "Visible outline edges." },
527
+ "outlineHidden": { "$ref": "#/$defs/viewportLineStyle", "description": "Hidden outline edges. Typically dashed." }
528
+ }
529
+ },
530
+
531
+ "border": {
532
+ "type": "object",
533
+ "description": "Border around a viewport.",
534
+ "additionalProperties": false,
535
+ "properties": {
536
+ "stroke": {
537
+ "$ref": "#/$defs/cssColor",
538
+ "default": "#000000",
539
+ "description": "Border color. Set to \"none\" to hide."
540
+ },
541
+ "strokeWidthMm": {
542
+ "type": "number",
543
+ "default": 0.25,
544
+ "description": "Border width in mm."
545
+ },
546
+ "dashed": {
547
+ "type": "boolean",
548
+ "default": false,
549
+ "description": "Use a dashed border."
550
+ }
551
+ }
552
+ },
553
+
554
+ "legendColumn": {
555
+ "type": "object",
556
+ "description": "A column in the legend table.",
557
+ "required": ["id", "label", "property"],
558
+ "additionalProperties": false,
559
+ "properties": {
560
+ "id": { "type": "string", "description": "Unique column ID." },
561
+ "label": { "type": "string", "description": "Column header text displayed in the legend." },
562
+ "property": {
563
+ "type": "string",
564
+ "description": "Short property path to read from each BIM item.",
565
+ "markdownDescription": "Short property path to read from each BIM item.\n\n`BIP.spacename` resolves to `propertySets.BIP.spacename.value`.\n\nCommon paths: `BIP.spacename`, `BIP.area`, `BIP.hyresobjektsnummer`, `BIP.spacetype`",
566
+ "examples": ["BIP.spacename", "BIP.area", "BIP.hyresobjektsnummer"]
567
+ },
568
+ "format": {
569
+ "type": "object",
570
+ "description": "Number formatting options.",
571
+ "markdownDescription": "Number formatting options.\n\nExample: `{ \"digits\": 2, \"suffix\": \" m²\" }` → `12.50 m²`",
572
+ "additionalProperties": false,
573
+ "properties": {
574
+ "digits": { "type": "integer", "description": "Number of decimal places." },
575
+ "prefix": { "type": "string", "description": "Text prepended to the value." },
576
+ "suffix": { "type": "string", "description": "Text appended to the value.", "examples": [" m²", " m", " st"] }
577
+ }
578
+ },
579
+ "align": {
580
+ "type": "string",
581
+ "enum": ["left", "center", "right"],
582
+ "default": "left",
583
+ "description": "Text alignment within the column."
584
+ },
585
+ "minWidthMm": { "type": "number", "description": "Minimum column width in mm." },
586
+ "maxWidthMm": { "type": "number", "description": "Maximum column width in mm." }
587
+ }
588
+ },
589
+
590
+ "element": {
591
+ "defaultSnippets": [
592
+ {
593
+ "label": "viewport",
594
+ "description": "A viewport rendering BIM section geometry",
595
+ "body": {
596
+ "kind": "viewport",
597
+ "id": "${1:main}",
598
+ "xMm": "${2:10}",
599
+ "yMm": "${3:40}",
600
+ "widthMm": "${4:400}",
601
+ "heightMm": "${5:230}",
602
+ "includeClasses": { "preset": "${6:defaultBuilding}" },
603
+ "scaleMode": "fitAndRound",
604
+ "scalePreset": "default",
605
+ "align": "center",
606
+ "fills": [{ "class": "space", "fill": "sample", "fillOpacity": 0.35 }]
607
+ }
608
+ },
609
+ {
610
+ "label": "labels",
611
+ "description": "Text labels at item centroids",
612
+ "body": {
613
+ "kind": "labels",
614
+ "id": "${1:room-labels}",
615
+ "viewport": "${2:main}",
616
+ "class": "space",
617
+ "where": { "${3:BIP.spacetype}": "${4:ROOM}" },
618
+ "text": "{{${5:BIP.spacename}}}",
619
+ "textStyle": "${6:spaceLabels}",
620
+ "align": "center"
621
+ }
622
+ },
623
+ {
624
+ "label": "legend",
625
+ "description": "Table legend with room names and areas",
626
+ "body": {
627
+ "kind": "legend",
628
+ "id": "${1:legend}",
629
+ "viewport": "${2:main}",
630
+ "class": "space",
631
+ "where": { "${3:BIP.spacetype}": "${4:ROOM}" },
632
+ "clipToViewport": true,
633
+ "textStyle": "${5:body}",
634
+ "anchor": "top-right",
635
+ "anchorPositionMm": { "xMm": 415, "yMm": 40 },
636
+ "columns": [
637
+ { "id": "name", "label": "Name", "property": "BIP.spacename" },
638
+ { "id": "area", "label": "Area", "property": "BIP.area", "format": { "digits": 2, "suffix": " m²" }, "align": "right" }
639
+ ]
640
+ }
641
+ },
642
+ {
643
+ "label": "scaleBar",
644
+ "description": "Graphical scale bar for a viewport",
645
+ "body": {
646
+ "kind": "scaleBar",
647
+ "id": "${1:scale-bar}",
648
+ "viewport": "${2:main}",
649
+ "xMm": "${3:160}",
650
+ "yMm": "${4:285}",
651
+ "maxLengthMm": "${5:100}",
652
+ "unit": "m",
653
+ "segments": 10,
654
+ "labelEvery": 2,
655
+ "stroke": "#000",
656
+ "strokeWidthMm": 0.25,
657
+ "textStyle": "${6:body}"
658
+ }
659
+ },
660
+ {
661
+ "label": "text",
662
+ "description": "Static or template text",
663
+ "body": {
664
+ "kind": "text",
665
+ "id": "${1:my-text}",
666
+ "xMm": "${2:10}",
667
+ "yMm": "${3:10}",
668
+ "textStyle": "${4:body}",
669
+ "text": "${5:Hello}",
670
+ "align": "left"
671
+ }
672
+ },
673
+ {
674
+ "label": "image",
675
+ "description": "Image from an asset",
676
+ "body": {
677
+ "kind": "image",
678
+ "id": "${1:logo}",
679
+ "xMm": "${2:170}",
680
+ "yMm": "${3:10}",
681
+ "widthMm": "${4:80}",
682
+ "heightMm": "${5:20}",
683
+ "assetId": "${6:logo-main}",
684
+ "fit": "contain"
685
+ }
686
+ },
687
+ {
688
+ "label": "rect",
689
+ "description": "Rectangle (frame, box)",
690
+ "body": {
691
+ "kind": "rect",
692
+ "id": "${1:frame}",
693
+ "xMm": "${2:5}",
694
+ "yMm": "${3:5}",
695
+ "widthMm": "${4:410}",
696
+ "heightMm": "${5:287}",
697
+ "stroke": "#000",
698
+ "strokeWidthMm": 0.25,
699
+ "fill": "none"
700
+ }
701
+ },
702
+ {
703
+ "label": "line",
704
+ "description": "Straight line",
705
+ "body": {
706
+ "kind": "line",
707
+ "id": "${1:my-line}",
708
+ "x1Mm": "${2:10}",
709
+ "y1Mm": "${3:10}",
710
+ "x2Mm": "${4:410}",
711
+ "y2Mm": "${5:10}",
712
+ "stroke": "#000",
713
+ "strokeWidthMm": 0.25
714
+ }
715
+ },
716
+ {
717
+ "label": "qr",
718
+ "description": "QR code for navigation or anchoring",
719
+ "body": {
720
+ "kind": "qr",
721
+ "id": "${1:qr-main}",
722
+ "role": "${2:main}",
723
+ "xMm": "${3:5}",
724
+ "yMm": "${4:270}",
725
+ "sizeMm": "${5:25}",
726
+ "viewport": "${6:main}"
727
+ }
728
+ }
729
+ ],
730
+ "oneOf": [
731
+ { "$ref": "#/$defs/viewportElement" },
732
+ { "$ref": "#/$defs/textElement" },
733
+ { "$ref": "#/$defs/imageElement" },
734
+ { "$ref": "#/$defs/lineElement" },
735
+ { "$ref": "#/$defs/rectElement" },
736
+ { "$ref": "#/$defs/circleElement" },
737
+ { "$ref": "#/$defs/polylineElement" },
738
+ { "$ref": "#/$defs/polygonElement" },
739
+ { "$ref": "#/$defs/scaleBarElement" },
740
+ { "$ref": "#/$defs/qrElement" },
741
+ { "$ref": "#/$defs/labelsElement" },
742
+ { "$ref": "#/$defs/legendElement" },
743
+ { "$ref": "#/$defs/groupElement" }
744
+ ]
745
+ },
746
+
747
+ "viewportElement": {
748
+ "type": "object",
749
+ "description": "A viewport renders BIM section geometry at a given position and scale on the page.",
750
+ "required": ["kind", "id", "xMm", "yMm", "widthMm", "heightMm"],
751
+ "additionalProperties": false,
752
+ "properties": {
753
+ "kind": { "const": "viewport" },
754
+ "id": {
755
+ "type": "string",
756
+ "description": "Unique element ID. Also used to match with section data.",
757
+ "markdownDescription": "Unique element ID.\n\nThis ID is used to:\n- Match with section data passed to `renderDrawing`\n- Link `labels`, `legend`, `scaleBar`, and `qr` elements to this viewport"
758
+ },
759
+ "zIndex": { "type": "integer", "description": "Render order (higher = on top)." },
760
+ "xMm": { "type": "number", "description": "X position on page in mm." },
761
+ "yMm": { "type": "number", "description": "Y position on page in mm." },
762
+ "widthMm": { "type": "number", "description": "Viewport width in mm." },
763
+ "heightMm": { "type": "number", "description": "Viewport height in mm." },
764
+ "scaleMode": {
765
+ "type": "string",
766
+ "enum": ["fit", "fitAndRound", "fixed"],
767
+ "default": "fitAndRound",
768
+ "description": "How scale is determined.",
769
+ "markdownDescription": "How the drawing scale is determined.\n\n**Modes:**\n\n- **`fitAndRound`** (recommended) — Fit content, then round to nearest standard scale\n - Result: Standard scales like 1:50, 1:100, 1:200\n - Best for: Publication, matching standard drawing conventions\n - Example: Content needs 1:73 → rounds to 1:100\n\n- **`fit`** — Best fit to viewport (exact)\n - Result: Odd scales like 1:73.4, 1:127.8\n - Best for: When content must fill viewport exactly\n - Example: Content needs 1:73.4 → uses 1:73.4\n\n- **`fixed`** — Use exact scale specified in `fixedScaleDenom`\n - Result: Exactly the scale you specify\n - Best for: When scale must match other drawings\n - Example: `fixedScaleDenom: 100` → always 1:100\n - ⚠️ Content may be clipped if too large\n\n**Default:** `fitAndRound` with standard architectural scales"
770
+ },
771
+ "fixedScaleDenom": {
772
+ "type": "number",
773
+ "description": "Scale denominator when scaleMode is \"fixed\". E.g. 100 means 1:100.",
774
+ "markdownDescription": "Scale denominator when `scaleMode: \"fixed\"`.\n\n**How it works:**\n- Value `100` → scale is 1:100\n- Value `50` → scale is 1:50\n- Value `200` → scale is 1:200\n\n**Example:**\n```json\n\"scaleMode\": \"fixed\",\n\"fixedScaleDenom\": 100\n```\nAlways renders at exactly 1:100.\n\n**⚠️ Warning:** Content larger than the viewport at this scale will be clipped.",
775
+ "examples": [50, 100, 200, 250, 500]
776
+ },
777
+ "preferredScales": {
778
+ "type": "array",
779
+ "items": { "type": "number" },
780
+ "description": "Preferred scale denominators for this viewport. Overrides scalePreset.",
781
+ "markdownDescription": "Preferred scale denominators for this viewport (when `scaleMode: \"fitAndRound\"`).\n\n**Overrides** `scalePreset` if both are specified.\n\n**Standard architectural scales:**\n```json\n\"preferredScales\": [50, 100, 200, 250, 500, 1000]\n```\n\n**Engineering scales:**\n```json\n\"preferredScales\": [10, 20, 50, 100, 200, 500]\n```\n\n**Detail scales:**\n```json\n\"preferredScales\": [2, 5, 10, 20, 50]\n```\n\nThe renderer picks the smallest scale that fits the content.",
782
+ "examples": [
783
+ [50, 100, 200, 500],
784
+ [2, 5, 10, 20, 50],
785
+ [10, 25, 50, 100, 250]
786
+ ]
787
+ },
788
+ "scalePreset": {
789
+ "type": "string",
790
+ "description": "Name of a scale preset from presets.scales (when scaleMode is fitAndRound).",
791
+ "markdownDescription": "Name of a scale preset from `presets.scales`.\n\nUsed when `scaleMode: \"fitAndRound\"`. Allows reusing scale lists across multiple viewports.\n\n**Example:**\n```json\n\"scalePreset\": \"default\"\n```\nReferences `presets.scales.default.preferredScales`.\n\n**Define preset at top level:**\n```json\n\"presets\": {\n \"scales\": {\n \"default\": { \"preferredScales\": [50, 100, 200, 500] }\n }\n}\n```",
792
+ "examples": ["default", "detail", "overview"]
793
+ },
794
+ "align": {
795
+ "type": "string",
796
+ "enum": ["center", "topLeft", "Left"],
797
+ "default": "center",
798
+ "description": "How the content is aligned within the viewport.",
799
+ "markdownDescription": "How the drawing content is aligned within the viewport.\n\n- **`center`** (default) — Center the drawing in the viewport\n - Best for: Most cases, balanced composition\n - Drawing is centered both horizontally and vertically\n\n- **`topLeft`** — Align to top-left corner\n - Best for: When you want consistent positioning\n - Drawing starts at viewport's top-left corner\n\n- **`Left`** — Align to left edge, center vertically\n - Best for: When horizontal alignment matters\n - Drawing is left-aligned, vertically centered"
800
+ },
801
+ "rotationDeg": {
802
+ "type": "number",
803
+ "default": 0,
804
+ "description": "Rotate the viewport content by this many degrees.",
805
+ "markdownDescription": "Rotate the viewport content by this many degrees.\n\n**Rotation is clockwise:**\n- `0` — No rotation (default)\n- `90` — 90° clockwise\n- `180` — Upside down\n- `270` — 90° counter-clockwise\n- `-90` — 90° counter-clockwise (same as 270)\n\n**Use cases:**\n- Rotate a vertical section to horizontal orientation\n- Match drawing orientation to page layout\n- Align building orientation with page direction\n\n**Example:**\n```json\n{\n \"kind\": \"viewport\",\n \"id\": \"section\",\n \"rotationDeg\": 90,\n \"...\": \"...\"\n}\n```"
806
+ },
807
+ "includeClasses": {
808
+ "oneOf": [
809
+ {
810
+ "type": "array",
811
+ "items": { "$ref": "#/$defs/ifcClassName" },
812
+ "description": "Array of IFC class names to include."
813
+ },
814
+ {
815
+ "type": "object",
816
+ "required": ["preset"],
817
+ "additionalProperties": false,
818
+ "properties": {
819
+ "preset": {
820
+ "type": "string",
821
+ "description": "Name of an includeClasses preset from presets.includeClasses."
822
+ }
823
+ }
824
+ }
825
+ ],
826
+ "description": "Which IFC classes to render in this viewport. Can be an array of class names or a preset reference.",
827
+ "markdownDescription": "Which IFC classes to render in this viewport.\n\n**Option 1: Inline array** (simple, good for unique viewports)\n```json\n\"includeClasses\": [\"space\", \"walls\", \"windows\", \"doors\"]\n```\n\n**Option 2: Preset reference** (reusable, good for multiple viewports)\n```json\n\"includeClasses\": { \"preset\": \"defaultBuilding\" }\n```\nReferences `presets.includeClasses.defaultBuilding` from the top level.\n\n**Common patterns:**\n\n| Viewport Type | Typical Classes |\n|---------------|----------------|\n| **Floor plan** | `[\"space\", \"walls\", \"windows\", \"doors\", \"columns\", \"stairs\"]` |\n| **Structure only** | `[\"walls\", \"columns\", \"beam\", \"floor\"]` |\n| **Detail view** | `[\"walls\", \"windows\", \"beam\", \"columns\"]` |\n| **Section view** | `[\"space\", \"walls\", \"windows\", \"floor\", \"roof\"]` |\n| **MEP** | `[\"flow\", \"furniture\", \"walls\"]` |\n\n**⚠️ Performance tip:** Fewer classes = faster rendering. Only include what you need for the drawing.\n\n**If omitted:** All classes with geometry are included (may be slow for large models)"
828
+ },
829
+ "lineStyles": {
830
+ "$ref": "#/$defs/viewportLineStyles",
831
+ "description": "Per-category line styles. Overrides lineWidthMm and lineColor."
832
+ },
833
+ "lineWidthMm": {
834
+ "type": "number",
835
+ "description": "Simple shorthand: default line width for all categories. Overridden by lineStyles."
836
+ },
837
+ "lineColor": {
838
+ "$ref": "#/$defs/cssColor",
839
+ "description": "Simple shorthand: default line color for all categories. Overridden by lineStyles."
840
+ },
841
+ "fills": {
842
+ "type": "array",
843
+ "description": "Fill rules controlling which products get filled polygons. When absent, no fills are rendered.",
844
+ "markdownDescription": "Fill rules controlling which products get filled polygons.\n\n**Default:** No fills (when property is omitted)\n\n**Fill color options:**\n- `\"sample\"` — Use the 3D model's material color\n- `\"#80c8b4\"` — Any CSS hex color\n- `\"rgb(128, 200, 180)\"` — CSS rgb/rgba\n\n**Example — Color spaces by material:**\n```json\n\"fills\": [\n { \"class\": \"space\", \"fill\": \"sample\", \"fillOpacity\": 0.35 }\n]\n```\n\n**Example — Different colors per space type:**\n```json\n\"fills\": [\n { \"class\": \"space\", \"where\": { \"BIP.spacetype\": \"ROOM\" }, \"fill\": \"#80c8b4\", \"fillOpacity\": 0.35 },\n { \"class\": \"space\", \"where\": { \"BIP.spacetype\": \"CORRIDOR\" }, \"fill\": \"#c8b480\", \"fillOpacity\": 0.35 }\n]\n```",
845
+ "items": {
846
+ "type": "object",
847
+ "required": ["fill"],
848
+ "additionalProperties": false,
849
+ "properties": {
850
+ "class": {
851
+ "oneOf": [
852
+ { "type": "string" },
853
+ { "type": "array", "items": { "type": "string" } }
854
+ ],
855
+ "description": "IFC class filter (e.g. 'space', 'walls').",
856
+ "markdownDescription": "IFC class filter.\n\nCan be a single class or array of classes:\n- `\"class\": \"space\"`\n- `\"class\": [\"space\", \"walls\"]`"
857
+ },
858
+ "where": {
859
+ "$ref": "#/$defs/whereCondition",
860
+ "description": "Property-based filter (same as highlight/clip where)."
861
+ },
862
+ "fill": {
863
+ "type": "string",
864
+ "description": "Fill color: CSS hex (e.g. '#80c8b4') or 'sample' to read from 3D model material.",
865
+ "markdownDescription": "Fill color.\n\n- `\"sample\"` — Use 3D model material color\n- `\"#80c8b4\"` — CSS hex color\n- `\"rgb(128,200,180)\"` — CSS rgb/rgba",
866
+ "examples": ["sample", "#80c8b4", "#ff0000", "rgba(128,200,180,0.5)"]
867
+ },
868
+ "fillOpacity": {
869
+ "type": "number",
870
+ "minimum": 0,
871
+ "maximum": 1,
872
+ "default": 0.35,
873
+ "description": "Fill opacity (0 = transparent, 1 = fully opaque). Default: 0.35"
874
+ },
875
+ "matchClipFilter": {
876
+ "type": "boolean",
877
+ "default": false,
878
+ "description": "Only fill items whose centroid is inside the original (unexpanded) clip polygon. Useful when expandByM reveals adjacent spaces that shouldn't be filled.",
879
+ "markdownDescription": "Only fill items whose centroid is inside the **original** (unexpanded) clip polygon.\n\nWhen `clip.expandByM` is used, the viewport shows geometry in an expanded area, but you may only want fills for items in the original clip area.\n\n**Example:**\n```json\n\"clip\": {\n \"class\": \"space\",\n \"where\": { \"BIP.hyresobjektsnummer\": \"2022-F2008\" },\n \"expandByM\": 1.5\n},\n\"fills\": [{\n \"class\": \"space\",\n \"where\": { \"BIP.spacetype\": \"ROOM\" },\n \"fill\": \"sample\",\n \"matchClipFilter\": true\n}]\n```\n\nThis ensures only the tenant's own spaces are filled, not surrounding spaces visible due to `expandByM`."
880
+ }
881
+ }
882
+ }
883
+ },
884
+ "northArrow": {
885
+ "type": "object",
886
+ "description": "Optional north arrow overlay showing building orientation.",
887
+ "markdownDescription": "Optional north arrow overlay showing building orientation.\n\n**Purpose:**\nDisplays a north arrow on the viewport to indicate building orientation relative to true north.\n\n**Example:**\n```json\n\"northArrow\": {\n \"assetId\": \"north-arrow-icon\",\n \"sizeMm\": 15,\n \"offsetXMm\": 5,\n \"offsetYMm\": 5,\n \"rotationDeg\": 45\n}\n```\n\n**Positioning:**\n- Arrow is positioned relative to viewport's **top-left corner**\n- Use positive offsets to move right (X) and down (Y)\n\n**Rotation:**\n- `rotationDeg` rotates the arrow to point north\n- Example: If building is rotated 45° from north, set `rotationDeg: 45`\n\n**Requires:**\n- Define the north arrow image in `assets`\n- Reference it via `assetId`",
888
+ "required": ["assetId"],
889
+ "additionalProperties": false,
890
+ "properties": {
891
+ "assetId": {
892
+ "type": "string",
893
+ "description": "Asset ID for the north arrow image (SVG or raster).",
894
+ "examples": ["north-arrow", "north-icon"]
895
+ },
896
+ "sizeMm": {
897
+ "type": "number",
898
+ "description": "Size of the north arrow in mm (width and height).",
899
+ "default": 15,
900
+ "examples": [10, 15, 20]
901
+ },
902
+ "offsetXMm": {
903
+ "type": "number",
904
+ "description": "Horizontal offset from viewport's top-left corner in mm.",
905
+ "default": 5,
906
+ "examples": [5, 10]
907
+ },
908
+ "offsetYMm": {
909
+ "type": "number",
910
+ "description": "Vertical offset from viewport's top-left corner in mm.",
911
+ "default": 5,
912
+ "examples": [5, 10]
913
+ },
914
+ "rotationDeg": {
915
+ "type": "number",
916
+ "description": "Rotation of the arrow in degrees (to point toward true north).",
917
+ "default": 0,
918
+ "examples": [0, 45, 90, 180]
919
+ }
920
+ }
921
+ },
922
+ "border": { "$ref": "#/$defs/border" },
923
+ "clip": { "$ref": "#/$defs/clipConfig" },
924
+ "highlight": { "$ref": "#/$defs/highlightConfig" },
925
+ "cutHeight": {
926
+ "type": "number",
927
+ "description": "Override cutting plane height for horizontal plans (Y coordinate). Shorthand for planeNormal=[0,1,0], planePosition=cutHeight.",
928
+ "markdownDescription": "Override cutting plane height for horizontal plans (Y coordinate).\n\n**Shorthand for:**\n```json\n\"planeNormal\": [0, 1, 0],\n\"planePosition\": <cutHeight value>\n```\n\n**Common values:**\n- `3.0` — 3 meters above origin (typical floor plan height)\n- `1.5` — 1.5 meters (lower section height)\n- `5.0` — 5 meters (upper level)\n\n**When to use:**\n- For simple horizontal floor plans at a specific elevation\n- When you don't need vertical or diagonal sections\n\n**If omitted:** Uses app default height (typically 3.0m)"
929
+ },
930
+ "depth": {
931
+ "type": "object",
932
+ "description": "Override depth above/below cutting plane. Only applicable for SLAB mode (ignored in PLANE mode).",
933
+ "markdownDescription": "Override depth above/below cutting plane.\n\n**⚠️ SLAB mode only** — This property is ignored in PLANE mode (vertical sections).\n\nDefines the thickness of the \"slab\" that includes geometry:\n- `above` — How far above the cutting plane to include geometry\n- `below` — How far below the cutting plane to include geometry\n\n**Example — Standard floor plan:**\n```json\n\"cutHeight\": 3.0,\n\"depth\": {\n \"above\": 1.0,\n \"below\": 2.0\n}\n```\nThis includes geometry from Y=1.0 to Y=5.0 (3.0 - 2.0 to 3.0 + 1.0).\n\n**If omitted:** Uses app default depth settings",
934
+ "additionalProperties": false,
935
+ "properties": {
936
+ "above": {
937
+ "type": "number",
938
+ "description": "Distance above cutting plane (meters). How far upward from the plane to include geometry.",
939
+ "examples": [1.0, 2.0, 0.5]
940
+ },
941
+ "below": {
942
+ "type": "number",
943
+ "description": "Distance below cutting plane (meters). How far downward from the plane to include geometry.",
944
+ "examples": [2.0, 3.0, 1.0]
945
+ }
946
+ }
947
+ },
948
+ "planeNormal": {
949
+ "type": "array",
950
+ "description": "Plane normal vector [x, y, z]. Automatically normalized. Takes precedence over cutHeight.",
951
+ "markdownDescription": "Plane normal vector `[x, y, z]` defining the cutting plane orientation.\n\n**Automatically normalized** — You can use unnormalized vectors like `[1, 0, 1]`\n\n**Takes precedence over `cutHeight`** if both are specified.\n\n**Common patterns:**\n\n| Normal | Orientation | Use Case |\n|--------|------------|----------|\n| `[0, 1, 0]` | Horizontal plan | Floor plan (same as `cutHeight`) |\n| `[1, 0, 0]` | Vertical, ⊥ X-axis | Section looking along X |\n| `[0, 0, 1]` | Vertical, ⊥ Z-axis | Section looking along Z |\n| `[1, 0, 1]` | Diagonal (45°) | Angled section |\n\n**⚠️ Important:**\n- Non-horizontal normals require `mode: \"plane\"` (auto-detected)\n- Vertical sections (`[1,0,0]` or `[0,0,1]`) will NOT work correctly in SLAB mode\n\n**Example — Vertical section perpendicular to Z:**\n```json\n\"planeNormal\": [0, 0, 1],\n\"planePosition\": 30\n```\nThis creates a vertical section at Z=30 meters.",
952
+ "items": { "type": "number" },
953
+ "minItems": 3,
954
+ "maxItems": 3
955
+ },
956
+ "planePosition": {
957
+ "type": "number",
958
+ "description": "Distance from origin along the (normalized) plane normal. Required when using planeNormal.",
959
+ "markdownDescription": "Distance from origin along the (normalized) plane normal.\n\n**How it works:**\nThe plane passes through the point: `normalize(planeNormal) * planePosition`\n\n**Examples:**\n\n**Vertical section at X=50:**\n```json\n\"planeNormal\": [1, 0, 0],\n\"planePosition\": 50\n```\nPlane perpendicular to X-axis, 50 meters from origin.\n\n**Vertical section at Z=30:**\n```json\n\"planeNormal\": [0, 0, 1],\n\"planePosition\": 30\n```\nPlane perpendicular to Z-axis, 30 meters from origin.\n\n**Horizontal plan at Y=3:**\n```json\n\"planeNormal\": [0, 1, 0],\n\"planePosition\": 3\n```\nSame as `\"cutHeight\": 3`.\n\n**⚠️ Must be used together with `planeNormal`**"
960
+ },
961
+ "mode": {
962
+ "type": "string",
963
+ "enum": ["slab", "plane"],
964
+ "description": "Section computation mode: 'slab' (horizontal plans) or 'plane' (vertical sections). Auto-detected if omitted.",
965
+ "markdownDescription": "Section computation mode:\n\n- **`slab`** — Optimized for horizontal floor plans. Uses `depth.above`/`depth.below` to include geometry in a thickness slab around the cutting plane. Fast and efficient for plan views.\n- **`plane`** — Arbitrary plane cuts (vertical sections, diagonal sections). Computes exact intersection with the plane. Required for non-horizontal sections.\n\n**Auto-detection (if omitted):**\n- Horizontal planes (`planeNormal` ≈ Y-axis) → `slab`\n- Vertical or diagonal planes → `plane`\n\n**⚠️ Important limitations:**\n- **Vertical sections MUST use `plane` mode** — SLAB mode only works correctly for horizontal plans\n- **Diagonal sections require `plane` mode**\n- If ANY viewport in a template requires `plane` mode, the entire export uses `plane` mode (auto-switching with console warning)\n\n**When to set explicitly:**\n- Set `\"mode\": \"plane\"` for vertical sections to be explicit\n- Set `\"mode\": \"slab\"` for horizontal plans if you want to ensure SLAB optimizations\n- Omit for auto-detection (recommended for most cases)"
966
+ }
967
+ }
968
+ },
969
+
970
+ "textElement": {
971
+ "type": "object",
972
+ "description": "Static or template text placed at a fixed position on the page.",
973
+ "markdownDescription": "Static or template text placed at a fixed position on the page.\n\n**Use cases:**\n- Drawing titles\n- Scale indicators (\"Scale: 1:100\")\n- Project metadata (dates, revision numbers)\n- Fixed labels (\"PLAN\", \"SECTION A-A\")\n\n**Template variables:**\nUse `{{variable}}` syntax for dynamic content:\n\n| Variable | Output | Example |\n|----------|--------|----------|\n| `{{viewport.<id>.scaleText}}` | Scale ratio | `1:100` |\n| `{{buildStamp}}` | Build timestamp | `2025-02-24T14:30Z` |\n| Custom data keys | Passed via options | Any value |\n\n**Examples:**\n\n**Scale indicator:**\n```json\n{\n \"kind\": \"text\",\n \"id\": \"scale-text\",\n \"xMm\": 210,\n \"yMm\": 275,\n \"textStyle\": \"body\",\n \"align\": \"center\",\n \"text\": \"Scale: {{viewport.main.scaleText}}\"\n}\n```\nOutput: `Scale: 1:100`\n\n**Drawing title with scale:**\n```json\n{\n \"kind\": \"text\",\n \"id\": \"title\",\n \"xMm\": 10,\n \"yMm\": 15,\n \"textStyle\": \"title\",\n \"text\": \"Floor Plan 02 — {{viewport.main.scaleText}}\"\n}\n```\nOutput: `Floor Plan 02 — 1:100`\n\n**Static text:**\n```json\n{\n \"kind\": \"text\",\n \"id\": \"section-label\",\n \"xMm\": 320,\n \"yMm\": 95,\n \"textStyle\": \"body\",\n \"text\": \"SECTION A-A\"\n}\n```",
974
+ "required": ["kind", "id", "xMm", "yMm", "text"],
975
+ "additionalProperties": false,
976
+ "properties": {
977
+ "kind": { "const": "text" },
978
+ "id": { "type": "string", "description": "Unique element ID." },
979
+ "zIndex": { "type": "integer" },
980
+ "xMm": { "type": "number", "description": "X position on page in mm." },
981
+ "yMm": { "type": "number", "description": "Y position on page in mm." },
982
+ "textStyle": {
983
+ "type": "string",
984
+ "description": "Name of a style from the styles section.",
985
+ "examples": ["body", "title", "spaceLabels", "small"]
986
+ },
987
+ "text": {
988
+ "type": "string",
989
+ "description": "Text content. Use {{variable}} for template interpolation.",
990
+ "examples": [
991
+ "Scale: {{viewport.main.scaleText}}",
992
+ "Build: {{buildStamp}}",
993
+ "Hyresobjekt: 2022-F2008"
994
+ ]
995
+ },
996
+ "maxWidthMm": { "type": "number", "description": "Maximum text width in mm before wrapping." },
997
+ "align": {
998
+ "type": "string",
999
+ "enum": ["left", "center", "right"],
1000
+ "default": "left",
1001
+ "description": "Text alignment."
1002
+ }
1003
+ }
1004
+ },
1005
+
1006
+ "imageElement": {
1007
+ "type": "object",
1008
+ "description": "An image element placed on the page. References an asset by ID.",
1009
+ "required": ["kind", "id", "xMm", "yMm", "widthMm", "heightMm", "assetId"],
1010
+ "additionalProperties": false,
1011
+ "properties": {
1012
+ "kind": { "const": "image" },
1013
+ "id": { "type": "string" },
1014
+ "zIndex": { "type": "integer" },
1015
+ "xMm": { "type": "number", "description": "X position on page in mm." },
1016
+ "yMm": { "type": "number", "description": "Y position on page in mm." },
1017
+ "widthMm": { "type": "number", "description": "Image width in mm." },
1018
+ "heightMm": { "type": "number", "description": "Image height in mm." },
1019
+ "assetId": { "type": "string", "description": "ID of an asset from the assets array." },
1020
+ "fit": {
1021
+ "type": "string",
1022
+ "enum": ["contain", "cover", "stretch"],
1023
+ "default": "contain",
1024
+ "description": "How the image fits within its bounds.",
1025
+ "markdownDescription": "How the image fits within its bounds.\n\n- **`contain`** — Scale to fit, preserving aspect ratio (may have empty space)\n- **`cover`** — Scale to fill, cropping if needed\n- **`stretch`** — Stretch to exact dimensions (may distort)"
1026
+ }
1027
+ }
1028
+ },
1029
+
1030
+ "lineElement": {
1031
+ "type": "object",
1032
+ "description": "A straight line between two points on the page.",
1033
+ "required": ["kind", "id", "x1Mm", "y1Mm", "x2Mm", "y2Mm"],
1034
+ "additionalProperties": false,
1035
+ "properties": {
1036
+ "kind": { "const": "line" },
1037
+ "id": { "type": "string" },
1038
+ "zIndex": { "type": "integer" },
1039
+ "x1Mm": { "type": "number", "description": "Start X in mm." },
1040
+ "y1Mm": { "type": "number", "description": "Start Y in mm." },
1041
+ "x2Mm": { "type": "number", "description": "End X in mm." },
1042
+ "y2Mm": { "type": "number", "description": "End Y in mm." },
1043
+ "stroke": { "$ref": "#/$defs/cssColor", "default": "#000", "description": "Line color." },
1044
+ "strokeWidthMm": { "type": "number", "default": 0.25, "description": "Line width in mm." },
1045
+ "dashed": { "type": "boolean", "default": false, "description": "Use a dashed line." }
1046
+ }
1047
+ },
1048
+
1049
+ "rectElement": {
1050
+ "type": "object",
1051
+ "description": "A rectangle on the page.",
1052
+ "required": ["kind", "id", "xMm", "yMm", "widthMm", "heightMm"],
1053
+ "additionalProperties": false,
1054
+ "properties": {
1055
+ "kind": { "const": "rect" },
1056
+ "id": { "type": "string" },
1057
+ "zIndex": { "type": "integer" },
1058
+ "xMm": { "type": "number", "description": "X position in mm." },
1059
+ "yMm": { "type": "number", "description": "Y position in mm." },
1060
+ "widthMm": { "type": "number", "description": "Width in mm." },
1061
+ "heightMm": { "type": "number", "description": "Height in mm." },
1062
+ "stroke": { "$ref": "#/$defs/cssColor", "description": "Border color." },
1063
+ "strokeWidthMm": { "type": "number", "description": "Border width in mm." },
1064
+ "fill": { "$ref": "#/$defs/cssColor", "default": "none", "description": "Fill color. \"none\" for no fill." },
1065
+ "dashed": { "type": "boolean", "default": false, "description": "Use a dashed border." }
1066
+ }
1067
+ },
1068
+
1069
+ "circleElement": {
1070
+ "type": "object",
1071
+ "description": "A circle on the page.",
1072
+ "required": ["kind", "id", "cxMm", "cyMm", "rMm"],
1073
+ "additionalProperties": false,
1074
+ "properties": {
1075
+ "kind": { "const": "circle" },
1076
+ "id": { "type": "string" },
1077
+ "zIndex": { "type": "integer" },
1078
+ "cxMm": { "type": "number", "description": "Center X in mm." },
1079
+ "cyMm": { "type": "number", "description": "Center Y in mm." },
1080
+ "rMm": { "type": "number", "description": "Radius in mm." },
1081
+ "stroke": { "$ref": "#/$defs/cssColor", "description": "Border color." },
1082
+ "strokeWidthMm": { "type": "number", "description": "Border width in mm." },
1083
+ "fill": { "$ref": "#/$defs/cssColor", "default": "none", "description": "Fill color." },
1084
+ "dashed": { "type": "boolean", "default": false }
1085
+ }
1086
+ },
1087
+
1088
+ "pointMm": {
1089
+ "type": "object",
1090
+ "required": ["xMm", "yMm"],
1091
+ "additionalProperties": false,
1092
+ "properties": {
1093
+ "xMm": { "type": "number" },
1094
+ "yMm": { "type": "number" }
1095
+ }
1096
+ },
1097
+
1098
+ "polylineElement": {
1099
+ "type": "object",
1100
+ "description": "An open polyline (connected line segments).",
1101
+ "required": ["kind", "id", "points", "stroke", "strokeWidthMm"],
1102
+ "additionalProperties": false,
1103
+ "properties": {
1104
+ "kind": { "const": "polyline" },
1105
+ "id": { "type": "string" },
1106
+ "zIndex": { "type": "integer" },
1107
+ "points": { "type": "array", "items": { "$ref": "#/$defs/pointMm" }, "description": "Array of points in mm." },
1108
+ "stroke": { "$ref": "#/$defs/cssColor", "description": "Line color." },
1109
+ "strokeWidthMm": { "type": "number", "description": "Line width in mm." },
1110
+ "dashed": { "type": "boolean", "default": false }
1111
+ }
1112
+ },
1113
+
1114
+ "polygonElement": {
1115
+ "type": "object",
1116
+ "description": "A closed polygon.",
1117
+ "required": ["kind", "id", "points"],
1118
+ "additionalProperties": false,
1119
+ "properties": {
1120
+ "kind": { "const": "polygon" },
1121
+ "id": { "type": "string" },
1122
+ "zIndex": { "type": "integer" },
1123
+ "points": { "type": "array", "items": { "$ref": "#/$defs/pointMm" }, "description": "Array of points in mm." },
1124
+ "stroke": { "$ref": "#/$defs/cssColor", "description": "Border color." },
1125
+ "strokeWidthMm": { "type": "number", "description": "Border width in mm." },
1126
+ "fill": { "$ref": "#/$defs/cssColor", "default": "none", "description": "Fill color." },
1127
+ "dashed": { "type": "boolean", "default": false }
1128
+ }
1129
+ },
1130
+
1131
+ "scaleBarElement": {
1132
+ "type": "object",
1133
+ "description": "A graphical scale bar showing the actual scale of a viewport.",
1134
+ "markdownDescription": "A graphical scale bar showing the actual scale of a viewport.\n\n**Purpose:**\nProvides a visual reference for measuring distances on the drawing. The scale bar automatically adjusts based on the viewport's actual scale.\n\n**How it works:**\n1. References a viewport by ID via `viewport` property\n2. Reads the viewport's computed scale (e.g. 1:100)\n3. Generates a bar divided into `segments`\n4. Each segment represents a round number in the specified `unit`\n5. Labels appear every `labelEvery` segments\n\n**Example — 10-segment bar, labels every 2 segments:**\n```json\n{\n \"kind\": \"scaleBar\",\n \"id\": \"scale-bar-main\",\n \"viewport\": \"main\",\n \"xMm\": 160,\n \"yMm\": 285,\n \"maxLengthMm\": 100,\n \"unit\": \"m\",\n \"segments\": 10,\n \"labelEvery\": 2,\n \"stroke\": \"#000\",\n \"strokeWidthMm\": 0.5,\n \"textStyle\": \"body\"\n}\n```\n\nAt scale 1:100, this might show: `0 — 2m — 4m — 6m — 8m — 10m`\n\n**⚠️ Must reference a viewport** — The scale bar gets its scale from the referenced viewport.",
1135
+ "required": ["kind", "id", "xMm", "yMm", "maxLengthMm"],
1136
+ "additionalProperties": false,
1137
+ "properties": {
1138
+ "kind": { "const": "scaleBar" },
1139
+ "id": { "type": "string" },
1140
+ "zIndex": { "type": "integer" },
1141
+ "xMm": { "type": "number", "description": "X position on page in mm." },
1142
+ "yMm": { "type": "number", "description": "Y position on page in mm." },
1143
+ "maxLengthMm": { "type": "number", "description": "Maximum length of the scale bar in mm." },
1144
+ "unit": { "type": "string", "default": "m", "description": "Display unit label.", "examples": ["m", "mm", "ft"] },
1145
+ "segments": { "type": "integer", "default": 10, "description": "Number of segments in the scale bar." },
1146
+ "labelEvery": { "type": "integer", "default": 2, "description": "Show a numeric label every N segments." },
1147
+ "stroke": { "$ref": "#/$defs/cssColor", "default": "#000", "description": "Scale bar line color." },
1148
+ "strokeWidthMm": { "type": "number", "default": 0.25, "description": "Scale bar line width in mm." },
1149
+ "textStyle": { "type": "string", "description": "Name of a style from the styles section for scale labels." },
1150
+ "viewport": { "type": "string", "description": "ID of the viewport this scale bar measures." }
1151
+ }
1152
+ },
1153
+
1154
+ "qrElement": {
1155
+ "type": "object",
1156
+ "description": "A QR code element for navigation, AR spatial anchoring, or custom data.",
1157
+ "markdownDescription": "A QR code element for various purposes.\n\n**Common uses:**\n\n1. **Navigation QR** (`role: \"main\"`)\n - Links to Twinfinity web viewer at the drawing's location\n - Users scan to view the 3D model at this section\n\n2. **Spatial anchor** (`role: \"anchor\"`)\n - AR positioning reference\n - Four QR codes (one per corner) define the page location in 3D space\n - Enables accurate AR overlay of digital content on printed drawings\n\n3. **Custom data** (`role: \"custom\"`)\n - Encode arbitrary data via `dataTemplate`\n - Example: Project ID, revision number, document URL\n\n**Example — Navigation QR:**\n```json\n{\n \"kind\": \"qr\",\n \"id\": \"qr-main\",\n \"role\": \"main\",\n \"xMm\": 5,\n \"yMm\": 270,\n \"sizeMm\": 25,\n \"viewport\": \"main\"\n}\n```\n\n**Example — Four anchor QRs for AR:**\n```json\n[\n { \"kind\": \"qr\", \"role\": \"anchor\", \"anchorCorner\": \"bottom-left\", \"xMm\": 5, \"yMm\": 287, \"sizeMm\": 15 },\n { \"kind\": \"qr\", \"role\": \"anchor\", \"anchorCorner\": \"bottom-right\", \"xMm\": 400, \"yMm\": 287, \"sizeMm\": 15 },\n { \"kind\": \"qr\", \"role\": \"anchor\", \"anchorCorner\": \"top-left\", \"xMm\": 5, \"yMm\": 5, \"sizeMm\": 15 },\n { \"kind\": \"qr\", \"role\": \"anchor\", \"anchorCorner\": \"top-right\", \"xMm\": 400, \"yMm\": 5, \"sizeMm\": 15 }\n]\n```",
1158
+ "required": ["kind", "id", "xMm", "yMm", "sizeMm"],
1159
+ "additionalProperties": false,
1160
+ "properties": {
1161
+ "kind": { "const": "qr" },
1162
+ "id": { "type": "string" },
1163
+ "zIndex": { "type": "integer" },
1164
+ "xMm": { "type": "number", "description": "X position on page in mm." },
1165
+ "yMm": { "type": "number", "description": "Y position on page in mm." },
1166
+ "sizeMm": { "type": "number", "description": "QR code size (width and height) in mm." },
1167
+ "role": {
1168
+ "type": "string",
1169
+ "enum": ["main", "anchor", "custom"],
1170
+ "default": "main",
1171
+ "description": "QR role.",
1172
+ "markdownDescription": "QR code role.\n\n- **`main`** — Primary navigation QR code (links to Twinfinity viewer)\n- **`anchor`** — Spatial anchor for augmented reality positioning\n- **`custom`** — Custom data via `dataTemplate`"
1173
+ },
1174
+ "anchorCorner": {
1175
+ "type": "string",
1176
+ "enum": ["bottom-left", "bottom-right", "top-left", "top-right"],
1177
+ "description": "Which page corner this anchor represents (for spatial anchoring)."
1178
+ },
1179
+ "viewport": { "type": "string", "description": "ID of the viewport this QR code is associated with." },
1180
+ "dataTemplate": {
1181
+ "type": "string",
1182
+ "description": "Template string for custom QR data. Supports {{variable}} interpolation."
1183
+ }
1184
+ }
1185
+ },
1186
+
1187
+ "labelsElement": {
1188
+ "type": "object",
1189
+ "description": "Renders text labels at the centroid of each matching BIM item inside a viewport.",
1190
+ "markdownDescription": "Renders text labels at the centroid of each matching BIM item inside a viewport.\n\nThe `text` property supports `{{property.path}}` template variables resolved per item.\n\n**Examples:**\n```json\n\"text\": \"{{BIP.spacename}}\"\n\"text\": \"{{BIP.spacename}} ({{BIP.area}} m²)\"\n```\n\nPaths like `BIP.spacename` resolve to `propertySets.BIP.spacename.value`.",
1191
+ "required": ["kind", "id", "viewport", "text"],
1192
+ "additionalProperties": false,
1193
+ "properties": {
1194
+ "kind": { "const": "labels" },
1195
+ "id": { "type": "string" },
1196
+ "zIndex": { "type": "integer" },
1197
+ "viewport": { "type": "string", "description": "ID of the viewport to place labels in." },
1198
+ "class": {
1199
+ "oneOf": [
1200
+ { "$ref": "#/$defs/ifcClassName" },
1201
+ { "type": "array", "items": { "$ref": "#/$defs/ifcClassName" } }
1202
+ ],
1203
+ "description": "IFC class filter. Only items of this class get labels."
1204
+ },
1205
+ "where": { "$ref": "#/$defs/whereCondition" },
1206
+ "text": {
1207
+ "type": "string",
1208
+ "description": "Label text template. Use {{property.path}} for per-item values.",
1209
+ "markdownDescription": "Label text template. Use `{{property.path}}` for per-item values.\n\nPaths like `BIP.spacename` resolve to `propertySets.BIP.spacename.value`.\n\nMultiple variables work: `{{BIP.spacename}} - {{BIP.area}} m²`",
1210
+ "examples": [
1211
+ "{{BIP.spacename}}",
1212
+ "{{BIP.spacename}} ({{BIP.area}} m²)",
1213
+ "{{BIP.spacenumber}}"
1214
+ ]
1215
+ },
1216
+ "textStyle": {
1217
+ "type": "string",
1218
+ "description": "Name of a style from the styles section.",
1219
+ "examples": ["spaceLabels"]
1220
+ },
1221
+ "align": {
1222
+ "type": "string",
1223
+ "enum": ["left", "center", "right"],
1224
+ "default": "center",
1225
+ "description": "Label text alignment.",
1226
+ "markdownDescription": "Label text alignment.\n\n- **`center`** (default) — Centered on item centroid (best for room labels)\n- **`left`** — Aligned left from centroid\n- **`right`** — Aligned right from centroid"
1227
+ },
1228
+ "maxWidthMm": {
1229
+ "type": "number",
1230
+ "description": "Maximum label width in mm before wrapping.",
1231
+ "markdownDescription": "Maximum label width in mm before text wrapping.\n\n**Example:**\n```json\n\"maxWidthMm\": 30\n```\nLabels longer than 30mm will wrap to multiple lines.\n\n**If omitted:** No wrapping (single line, may overflow)"
1232
+ },
1233
+ "matchClipFilter": {
1234
+ "type": "boolean",
1235
+ "default": false,
1236
+ "description": "Only show labels for items inside the viewport's clip boundary (if clip is configured).",
1237
+ "markdownDescription": "Only show labels for items inside the viewport's clip boundary.\n\n**When to use:**\n- You have a `clip` configured on the viewport\n- You only want labels for items in the clipped area\n- You're using `expandByM` and don't want labels outside the original clip\n\n**Example:**\n```json\n\"clip\": {\n \"class\": \"space\",\n \"where\": { \"BIP.hyresobjektsnummer\": \"2022-F2008\" },\n \"expandByM\": 1.5\n},\n\"labels\": {\n \"class\": \"space\",\n \"where\": { \"BIP.spacetype\": \"ROOM\" },\n \"text\": \"{{BIP.spacename}}\",\n \"matchClipFilter\": true\n}\n```\n\nLabels only appear for rooms whose centroid is in the ORIGINAL (unexpanded) clip polygon."
1238
+ }
1239
+ }
1240
+ },
1241
+
1242
+ "legendElement": {
1243
+ "type": "object",
1244
+ "description": "A table legend showing BIM item properties (room names, areas, etc.) with optional color swatches.",
1245
+ "markdownDescription": "A table legend showing BIM item properties with optional color swatches.\n\nThe legend is anchored to a page position and auto-sizes based on content.\n\n**Example:**\n```json\n{\n \"kind\": \"legend\",\n \"id\": \"legend\",\n \"viewport\": \"main\",\n \"class\": \"space\",\n \"where\": { \"BIP.spacetype\": \"ROOM\" },\n \"clipToViewport\": true,\n \"columns\": [\n { \"id\": \"name\", \"label\": \"Rum\", \"property\": \"BIP.spacename\" },\n { \"id\": \"area\", \"label\": \"Area\", \"property\": \"BIP.area\", \"format\": { \"digits\": 2, \"suffix\": \" m²\" } }\n ]\n}\n```",
1246
+ "required": ["kind", "id", "viewport", "columns"],
1247
+ "additionalProperties": false,
1248
+ "properties": {
1249
+ "kind": { "const": "legend" },
1250
+ "id": { "type": "string" },
1251
+ "zIndex": { "type": "integer" },
1252
+ "viewport": { "type": "string", "description": "ID of the viewport whose items populate the legend." },
1253
+ "class": {
1254
+ "oneOf": [
1255
+ { "$ref": "#/$defs/ifcClassName" },
1256
+ { "type": "array", "items": { "$ref": "#/$defs/ifcClassName" } }
1257
+ ],
1258
+ "description": "IFC class filter. Only items of this class appear in the legend."
1259
+ },
1260
+ "where": { "$ref": "#/$defs/whereCondition" },
1261
+ "clipToViewport": {
1262
+ "type": "boolean",
1263
+ "default": false,
1264
+ "description": "Only show items that are inside the viewport's clip boundary."
1265
+ },
1266
+ "anchor": {
1267
+ "type": "string",
1268
+ "enum": ["top-right", "top-left", "bottom-right", "bottom-left"],
1269
+ "default": "top-right",
1270
+ "description": "Which corner of the legend is anchored to anchorPositionMm."
1271
+ },
1272
+ "anchorPositionMm": {
1273
+ "type": "object",
1274
+ "description": "Position of the anchor corner on the page.",
1275
+ "required": ["xMm", "yMm"],
1276
+ "additionalProperties": false,
1277
+ "properties": {
1278
+ "xMm": { "type": "number" },
1279
+ "yMm": { "type": "number" }
1280
+ }
1281
+ },
1282
+ "marginMm": {
1283
+ "type": "number",
1284
+ "default": 5,
1285
+ "description": "Margin around the legend table in mm."
1286
+ },
1287
+ "textStyle": {
1288
+ "type": "string",
1289
+ "description": "Name of a style from the styles section for legend text.",
1290
+ "examples": ["body"]
1291
+ },
1292
+ "showColorColumn": {
1293
+ "type": "boolean",
1294
+ "default": true,
1295
+ "description": "Show a color swatch column on the left side of the legend."
1296
+ },
1297
+ "colorColumnWidthMm": {
1298
+ "type": "number",
1299
+ "default": 8,
1300
+ "description": "Width of the color swatch column in mm."
1301
+ },
1302
+ "cellPaddingMm": {
1303
+ "type": "number",
1304
+ "default": 2,
1305
+ "description": "Padding inside each legend cell in mm."
1306
+ },
1307
+ "borderStrokeWidthMm": {
1308
+ "type": "number",
1309
+ "default": 0.25,
1310
+ "description": "Outer border stroke width in mm."
1311
+ },
1312
+ "borderStrokeColor": {
1313
+ "$ref": "#/$defs/cssColor",
1314
+ "default": "#000000",
1315
+ "description": "Outer border color."
1316
+ },
1317
+ "gridStrokeWidthMm": {
1318
+ "type": "number",
1319
+ "default": 0.25,
1320
+ "description": "Internal grid line width in mm."
1321
+ },
1322
+ "gridStrokeColor": {
1323
+ "$ref": "#/$defs/cssColor",
1324
+ "default": "#000000",
1325
+ "description": "Internal grid line color."
1326
+ },
1327
+ "headerStrokeWidthMm": {
1328
+ "type": "number",
1329
+ "default": 0.65,
1330
+ "description": "Stroke width under the header row in mm (thicker for visual emphasis)."
1331
+ },
1332
+ "headerStrokeColor": {
1333
+ "$ref": "#/$defs/cssColor",
1334
+ "default": "#000000",
1335
+ "description": "Color of the stroke under the header row."
1336
+ },
1337
+ "columns": {
1338
+ "type": "array",
1339
+ "description": "Column definitions for the legend table.",
1340
+ "items": { "$ref": "#/$defs/legendColumn" },
1341
+ "minItems": 1
1342
+ }
1343
+ }
1344
+ },
1345
+
1346
+ "groupElement": {
1347
+ "type": "object",
1348
+ "description": "A container grouping child elements with optional transforms, opacity, and clipping.",
1349
+ "markdownDescription": "A container grouping child elements with optional transforms, opacity, and clipping.\n\n**Use cases:**\n- Apply transforms to multiple elements at once\n- Set opacity for a group of elements\n- Clip multiple elements to a rectangular region\n- Organize related elements (e.g., title block components)\n\n**Features:**\n- **Transform** — Translate, rotate, or scale all children\n- **Opacity** — Fade entire group (0 = invisible, 1 = opaque)\n- **Clip** — Rectangular clipping region\n- **Z-index** — Control render order\n\n**Example — Title block group:**\n```json\n{\n \"kind\": \"group\",\n \"id\": \"title-block\",\n \"transform\": {\n \"translateXMm\": 300,\n \"translateYMm\": 260\n },\n \"children\": [\n { \"kind\": \"rect\", \"xMm\": 0, \"yMm\": 0, \"widthMm\": 115, \"heightMm\": 30, \"stroke\": \"#000\", \"strokeWidthMm\": 0.5 },\n { \"kind\": \"text\", \"xMm\": 5, \"yMm\": 10, \"text\": \"Project Name\", \"textStyle\": \"body\" },\n { \"kind\": \"text\", \"xMm\": 5, \"yMm\": 20, \"text\": \"Drawing Number: A-101\", \"textStyle\": \"body\" }\n ]\n}\n```\n\n**Example — Semi-transparent watermark:**\n```json\n{\n \"kind\": \"group\",\n \"id\": \"watermark\",\n \"opacity\": 0.15,\n \"children\": [\n { \"kind\": \"text\", \"xMm\": 150, \"yMm\": 120, \"text\": \"DRAFT\", \"textStyle\": \"watermark\" }\n ]\n}\n```",
1350
+ "required": ["kind", "id", "children"],
1351
+ "additionalProperties": false,
1352
+ "properties": {
1353
+ "kind": { "const": "group" },
1354
+ "id": { "type": "string" },
1355
+ "zIndex": { "type": "integer" },
1356
+ "opacity": {
1357
+ "type": "number",
1358
+ "minimum": 0,
1359
+ "maximum": 1,
1360
+ "description": "Group opacity (0 = invisible, 1 = fully opaque)."
1361
+ },
1362
+ "transform": {
1363
+ "type": "object",
1364
+ "description": "Transform applied to the group.",
1365
+ "additionalProperties": false,
1366
+ "properties": {
1367
+ "translateXMm": { "type": "number", "description": "Horizontal translation in mm." },
1368
+ "translateYMm": { "type": "number", "description": "Vertical translation in mm." },
1369
+ "rotateDeg": { "type": "number", "description": "Rotation in degrees." },
1370
+ "scaleX": { "type": "number", "description": "Horizontal scale factor." },
1371
+ "scaleY": { "type": "number", "description": "Vertical scale factor." }
1372
+ }
1373
+ },
1374
+ "clip": {
1375
+ "type": "object",
1376
+ "description": "Rectangular clip region for the group.",
1377
+ "required": ["type", "xMm", "yMm", "widthMm", "heightMm"],
1378
+ "additionalProperties": false,
1379
+ "properties": {
1380
+ "type": { "const": "rect" },
1381
+ "xMm": { "type": "number" },
1382
+ "yMm": { "type": "number" },
1383
+ "widthMm": { "type": "number" },
1384
+ "heightMm": { "type": "number" }
1385
+ }
1386
+ },
1387
+ "children": {
1388
+ "type": "array",
1389
+ "description": "Child elements within this group.",
1390
+ "items": { "$ref": "#/$defs/element" }
1391
+ }
1392
+ }
1393
+ }
1394
+ }
1395
+ }