@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,372 @@
1
+ import type { SectionPath, SectionLabel } from '@twinfinity/geometry';
2
+ import type { SvgModelView, ViewportElement, LabelStyle, ResolvedIncludeFilter } from './types';
3
+ import type { ModelMetadata } from './metadata';
4
+ import { resolveIncludeClasses, resolveMeta } from './metadata';
5
+ import { normalizeClass, matchesWhere } from './filters';
6
+ import { escapeAttr, escapeText } from './renderUtils';
7
+ import { BUILTIN_LABEL_DEFAULTS } from './renderDrawing';
8
+
9
+ import { getProp, isRecord } from './typeGuards';
10
+
11
+ export const buildModelViewFromRaw = (
12
+ rawPaths: SectionPath[] | undefined,
13
+ rawLabels: SectionLabel[] | undefined,
14
+ transform: SvgModelView['transform'] | undefined,
15
+ geometryTransform?: string,
16
+ labelStyle?: LabelStyle
17
+ ): SvgModelView | null => {
18
+ if (!rawPaths || !transform || !geometryTransform) {
19
+ return null;
20
+ }
21
+ const labelFill = labelStyle?.fill ?? BUILTIN_LABEL_DEFAULTS.fill;
22
+ const labelFontFamily = labelStyle?.fontFamily ?? BUILTIN_LABEL_DEFAULTS.fontFamily;
23
+ const labelFontWeight = labelStyle?.fontWeight ?? BUILTIN_LABEL_DEFAULTS.fontWeight;
24
+ const escapedFontFamily = escapeAttr(labelFontFamily);
25
+
26
+ const parts: string[] = [];
27
+ parts.push(`<g transform="${escapeAttr(geometryTransform)}">`);
28
+ rawPaths.forEach((path) => {
29
+ parts.push(
30
+ `<path d="${escapeAttr(path.d)}"` +
31
+ (path.fill ? ` fill="${escapeAttr(path.fill)}"` : ' fill="none"') +
32
+ (path.fillOpacity !== undefined ? ` fill-opacity="${path.fillOpacity}"` : '') +
33
+ (path.stroke ? ` stroke="${escapeAttr(path.stroke)}"` : '') +
34
+ (path.strokeWidth ? ` stroke-width="${path.strokeWidth}"` : '') +
35
+ (path.strokeOpacity !== undefined ? ` stroke-opacity="${path.strokeOpacity}"` : '') +
36
+ (path.fillRule ? ` fill-rule="${escapeAttr(path.fillRule)}"` : '') +
37
+ ` stroke-linejoin="round" stroke-linecap="round"` +
38
+ ` />`
39
+ );
40
+ });
41
+ parts.push(`</g>`);
42
+ if (rawLabels && rawLabels.length) {
43
+ parts.push(`<g pointer-events="none">`);
44
+ rawLabels.forEach((label) => {
45
+ const viewX = label.x - transform.viewMinX;
46
+ const viewY = transform.viewMaxZ - label.z;
47
+ if (!Number.isFinite(viewX) || !Number.isFinite(viewY)) {
48
+ return;
49
+ }
50
+ parts.push(
51
+ `<text x="${viewX}" y="${viewY}" font-size="${label.fontSize}" text-anchor="middle" dominant-baseline="middle" fill="${escapeAttr(labelFill)}" font-family="${escapedFontFamily}" font-weight="${labelFontWeight}">${escapeText(label.text)}</text>`
52
+ );
53
+ });
54
+ parts.push(`</g>`);
55
+ }
56
+ const svg = `<g>${parts.join('')}</g>`;
57
+ return {
58
+ groupSvg: svg,
59
+ widthModel: transform.viewWidth,
60
+ heightModel: transform.viewHeight,
61
+ transform,
62
+ geometryTransform,
63
+ rawPaths,
64
+ rawLabels,
65
+ labelStyle
66
+ };
67
+ };
68
+
69
+ /**
70
+ * Check whether a path's resolved class matches any of the filter classes.
71
+ * Used to determine which SVG paths are subject to GID-based include/exclude.
72
+ */
73
+ const matchesFilterClass = (
74
+ cls: string | null,
75
+ clsId: unknown,
76
+ clsFromMap: string | null,
77
+ filterClasses: Set<string>
78
+ ): boolean => {
79
+ if (!filterClasses.size) return false;
80
+ if (cls && filterClasses.has(cls)) return true;
81
+ if (clsFromMap && filterClasses.has(clsFromMap)) return true;
82
+ if (clsId && typeof clsId === 'string') {
83
+ const normalized = normalizeClass(clsId);
84
+ if (normalized && filterClasses.has(normalized)) return true;
85
+ }
86
+ return false;
87
+ };
88
+
89
+ /**
90
+ * Check if an item matches any of the include filters.
91
+ *
92
+ * @param includeFilters - Array of filters (strings or class+where objects)
93
+ * @param cls - Normalized IFC class name
94
+ * @param clsId - Predefined IFC class ID
95
+ * @param meta - Item metadata (for property matching)
96
+ * @returns true if the item matches any filter, false otherwise
97
+ */
98
+ const matchesIncludeFilter = (
99
+ includeFilters: ResolvedIncludeFilter[],
100
+ cls: string | null,
101
+ clsId: unknown,
102
+ meta: unknown
103
+ ): boolean => {
104
+ for (const filter of includeFilters) {
105
+ // String filter: class name only
106
+ if (typeof filter === 'string') {
107
+ const normalizedFilter = normalizeClass(filter);
108
+ if (cls && normalizedFilter === cls) return true;
109
+ if (clsId && typeof clsId === 'string') {
110
+ const normalizedId = normalizeClass(clsId);
111
+ if (normalizedId === normalizedFilter) return true;
112
+ }
113
+ continue;
114
+ }
115
+
116
+ // Object filter: class + where
117
+ if (isRecord(filter)) {
118
+ const filterClass = filter.class;
119
+ const filterWhere = filter.where;
120
+
121
+ // Check class match (if specified)
122
+ let classMatches = true;
123
+ if (filterClass !== undefined) {
124
+ const classes = Array.isArray(filterClass) ? filterClass : [filterClass];
125
+ classMatches = false;
126
+ for (const c of classes) {
127
+ const normalizedC = normalizeClass(c);
128
+ if (normalizedC && cls === normalizedC) {
129
+ classMatches = true;
130
+ break;
131
+ }
132
+ if (clsId && typeof clsId === 'string') {
133
+ const normalizedId = normalizeClass(clsId);
134
+ if (normalizedId === normalizedC) {
135
+ classMatches = true;
136
+ break;
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ // Check where condition (if specified)
143
+ let whereMatches = true;
144
+ if (filterWhere && Object.keys(filterWhere).length > 0) {
145
+ whereMatches = matchesWhere(meta, filterWhere);
146
+ }
147
+
148
+ if (classMatches && whereMatches) return true;
149
+ }
150
+ }
151
+
152
+ return false;
153
+ };
154
+
155
+ export const filterModelForViewport = (
156
+ model: SvgModelView,
157
+ vp: ViewportElement,
158
+ metadataById?: ModelMetadata,
159
+ includePresets?: Record<string, string[]>,
160
+ allowedGids?: Set<string> | null,
161
+ highlight?: {
162
+ gids: Set<string> | null;
163
+ styles?: {
164
+ stroke?: string;
165
+ strokeWidthMm?: number;
166
+ strokeOpacity?: number;
167
+ fill?: string;
168
+ fillOpacity?: number;
169
+ };
170
+ },
171
+ filterClasses?: Set<string> | null
172
+ ): SvgModelView => {
173
+ const resolvedIncludeClasses = resolveIncludeClasses(vp.includeClasses as unknown, includePresets ?? {});
174
+ const includeFilters = resolvedIncludeClasses ?? null;
175
+ // Classes subject to GID-based filtering (highlight/clip include/exclude).
176
+ // Derived from template highlight.class and clip.class by the caller.
177
+ const effectiveFilterClasses = filterClasses ?? new Set<string>();
178
+ let styleMutated = false;
179
+ if (!model.rawPaths || !model.geometryTransform || !model.transform) {
180
+ return model;
181
+ }
182
+ const filteredPaths: SectionPath[] = [];
183
+ model.rawPaths.forEach((p: SectionPath) => {
184
+ const rawMeta = getProp(p, 'meta');
185
+ const meta = resolveMeta(rawMeta, metadataById);
186
+ const metaFromMap = (() => {
187
+ const pid =
188
+ getProp(meta, 'productId') ??
189
+ getProp(meta, 'gid') ??
190
+ getProp(meta, 'id') ??
191
+ getProp(rawMeta, 'gid') ??
192
+ getProp(rawMeta, 'id');
193
+ return pid && metadataById ? metadataById[String(pid)] : null;
194
+ })();
195
+ const productIdForMeta =
196
+ getProp(meta, 'productId') ??
197
+ getProp(meta, 'gid') ??
198
+ getProp(meta, 'id') ??
199
+ getProp(rawMeta, 'gid') ??
200
+ getProp(rawMeta, 'id') ??
201
+ metaFromMap?.productId ??
202
+ metaFromMap?.gid ??
203
+ null;
204
+ const pid = productIdForMeta ? String(productIdForMeta) : null;
205
+ const cls = normalizeClass(getProp(meta, 'ifcClass') ?? getProp(rawMeta, 'ifcClass') ?? metaFromMap?.ifcClass);
206
+ const clsId = getProp(meta, 'ifcClassId') ?? getProp(rawMeta, 'ifcClassId') ?? metaFromMap?.ifcClassId ?? null;
207
+ const clsFromMap = normalizeClass(metaFromMap?.ifcClass);
208
+ const isFilterable = matchesFilterClass(cls, clsId, clsFromMap, effectiveFilterClasses);
209
+ const shouldHighlight = !!highlight?.gids && highlight.gids.size > 0 && !!pid && highlight.gids.has(pid);
210
+ if (includeFilters && includeFilters.length > 0) {
211
+ if (isFilterable) {
212
+ // Highlight/clip class: only check includeClasses if no GID filtering will be applied
213
+ const hasGidFiltering =
214
+ (highlight?.gids && highlight.gids.size > 0) || (allowedGids && allowedGids.size > 0);
215
+ if (!hasGidFiltering) {
216
+ // No GID filtering active, so apply includeClasses filter
217
+ const matches = matchesIncludeFilter(includeFilters, cls, clsId, meta);
218
+ if (!matches) {
219
+ return;
220
+ }
221
+ }
222
+ } else {
223
+ // Not a highlight/clip class: always apply includeClasses filter
224
+ const matches = matchesIncludeFilter(includeFilters, cls, clsId, meta);
225
+ if (!matches) {
226
+ return;
227
+ }
228
+ }
229
+ }
230
+ if (isFilterable) {
231
+ if (highlight?.gids && highlight.gids.size > 0) {
232
+ if (!pid || !highlight.gids.has(pid)) {
233
+ return;
234
+ }
235
+ }
236
+ // Clip filtering logic: null means no filtering, empty Set means filtering active but no matches
237
+ const allowed =
238
+ !allowedGids || (allowedGids.size > 0 && !!(pid && allowedGids.has(pid))) || shouldHighlight;
239
+ if (!allowed) return;
240
+ }
241
+ // CRITICAL: Exclude space fills from model - buildFillSvg will handle them with matchClipFilter support
242
+ // Only keep space segments (outlines) in the model
243
+ // EXCEPT: Keep highlighted space fills so they can be styled with highlight colors
244
+ const isFill = getProp(meta, 'isFill') ?? getProp(rawMeta, 'isFill');
245
+ if (isFilterable && isFill && !shouldHighlight) {
246
+ return;
247
+ }
248
+ // Apply highlight styling when requested
249
+ if (shouldHighlight) {
250
+ const styledPath: SectionPath = { ...p };
251
+ if (highlight?.styles?.stroke !== undefined) {
252
+ styledPath.stroke = highlight.styles.stroke;
253
+ }
254
+ if (highlight?.styles?.strokeWidthMm !== undefined) {
255
+ styledPath.strokeWidth = highlight.styles.strokeWidthMm;
256
+ }
257
+ if (highlight?.styles?.strokeOpacity !== undefined) {
258
+ styledPath.strokeOpacity = highlight.styles.strokeOpacity;
259
+ }
260
+ if (highlight?.styles?.fill !== undefined) {
261
+ styledPath.fill = highlight.styles.fill;
262
+ }
263
+ if (highlight?.styles?.fillOpacity !== undefined) {
264
+ styledPath.fillOpacity = highlight.styles.fillOpacity;
265
+ }
266
+ styleMutated = true;
267
+ filteredPaths.push(styledPath);
268
+ } else {
269
+ filteredPaths.push(p);
270
+ }
271
+ });
272
+
273
+ const filteredLabels: SectionLabel[] = [];
274
+ (model.rawLabels ?? []).forEach((l: SectionLabel) => {
275
+ const rawMeta = getProp(l, 'meta');
276
+ const meta = resolveMeta(rawMeta, metadataById);
277
+ const cls = normalizeClass(getProp(meta, 'ifcClass'));
278
+ const clsId = getProp(meta, 'ifcClassId') ?? getProp(rawMeta, 'ifcClassId') ?? null;
279
+ const productIdForMeta =
280
+ getProp(meta, 'productId') ??
281
+ getProp(meta, 'gid') ??
282
+ getProp(meta, 'id') ??
283
+ getProp(rawMeta, 'gid') ??
284
+ getProp(rawMeta, 'id') ??
285
+ null;
286
+ if (includeFilters && includeFilters.length > 0) {
287
+ if (!matchesIncludeFilter(includeFilters, cls, clsId, meta)) {
288
+ return;
289
+ }
290
+ }
291
+ if (matchesFilterClass(cls, clsId, null, effectiveFilterClasses)) {
292
+ const pid = productIdForMeta ? String(productIdForMeta) : null;
293
+ if (highlight?.gids && highlight.gids.size > 0) {
294
+ if (!pid || !highlight.gids.has(pid)) {
295
+ return;
296
+ }
297
+ }
298
+ if (allowedGids && allowedGids.size > 0) {
299
+ if (!pid || !allowedGids.has(pid)) {
300
+ return;
301
+ }
302
+ }
303
+ }
304
+ filteredLabels.push(l);
305
+ });
306
+
307
+ // Count paths by IFC class before and after filtering
308
+ const classCountsBefore = new Map<string, number>();
309
+ const classCountsAfter = new Map<string, number>();
310
+ const excluded: Array<{ cls: string | null; clsId: unknown }> = [];
311
+
312
+ model.rawPaths.forEach((p: SectionPath) => {
313
+ const rawMeta = getProp(p, 'meta');
314
+ const meta = resolveMeta(rawMeta, metadataById);
315
+ const metaFromMap = (() => {
316
+ const pid =
317
+ getProp(meta, 'productId') ??
318
+ getProp(meta, 'gid') ??
319
+ getProp(meta, 'id') ??
320
+ getProp(rawMeta, 'gid') ??
321
+ getProp(rawMeta, 'id');
322
+ return pid && metadataById ? metadataById[String(pid)] : null;
323
+ })();
324
+ const cls = normalizeClass(getProp(meta, 'ifcClass') ?? getProp(rawMeta, 'ifcClass') ?? metaFromMap?.ifcClass);
325
+ const clsId = getProp(meta, 'ifcClassId') ?? getProp(rawMeta, 'ifcClassId') ?? metaFromMap?.ifcClassId ?? null;
326
+ const key = cls ?? String(clsId) ?? 'unknown';
327
+ classCountsBefore.set(key, (classCountsBefore.get(key) ?? 0) + 1);
328
+ });
329
+
330
+ filteredPaths.forEach((p: SectionPath) => {
331
+ const rawMeta = getProp(p, 'meta');
332
+ const meta = resolveMeta(rawMeta, metadataById);
333
+ const metaFromMap = (() => {
334
+ const pid =
335
+ getProp(meta, 'productId') ??
336
+ getProp(meta, 'gid') ??
337
+ getProp(meta, 'id') ??
338
+ getProp(rawMeta, 'gid') ??
339
+ getProp(rawMeta, 'id');
340
+ return pid && metadataById ? metadataById[String(pid)] : null;
341
+ })();
342
+ const cls = normalizeClass(getProp(meta, 'ifcClass') ?? getProp(rawMeta, 'ifcClass') ?? metaFromMap?.ifcClass);
343
+ const clsId = getProp(meta, 'ifcClassId') ?? getProp(rawMeta, 'ifcClassId') ?? metaFromMap?.ifcClassId ?? null;
344
+ const key = cls ?? String(clsId) ?? 'unknown';
345
+ classCountsAfter.set(key, (classCountsAfter.get(key) ?? 0) + 1);
346
+ });
347
+
348
+ // Find excluded classes
349
+ classCountsBefore.forEach((countBefore, cls) => {
350
+ const countAfter = classCountsAfter.get(cls) ?? 0;
351
+ if (countAfter < countBefore) {
352
+ excluded.push({ cls, clsId: `${countBefore - countAfter} paths removed` });
353
+ }
354
+ });
355
+
356
+ if (
357
+ !styleMutated &&
358
+ filteredPaths.length === model.rawPaths.length &&
359
+ filteredLabels.length === (model.rawLabels?.length ?? 0)
360
+ ) {
361
+ return model;
362
+ }
363
+
364
+ const rebuilt = buildModelViewFromRaw(
365
+ filteredPaths,
366
+ filteredLabels,
367
+ model.transform,
368
+ model.geometryTransform,
369
+ model.labelStyle
370
+ );
371
+ return rebuilt ?? model;
372
+ };
@@ -0,0 +1,17 @@
1
+ import { ModelMetadata } from './metadata';
2
+
3
+ export interface RenderLayoutOptions {
4
+ scaleDenominators?: number[];
5
+ maxViewports?: number;
6
+ allowRemoteAssets?: boolean;
7
+ allowDataUrls?: boolean;
8
+ qrContext?: {
9
+ host?: string | null;
10
+ containerName?: string | null;
11
+ floorName?: string | null;
12
+ planeHeight?: number | null;
13
+ };
14
+ legendSources?: Record<string, any[]>;
15
+ /** Optional metadata map keyed by productId referenced from path/label meta */
16
+ metadataById?: ModelMetadata;
17
+ }
@@ -0,0 +1,51 @@
1
+ export const DEFAULT_SCALE_DENOMS = [2, 10, 25, 50, 100, 250, 500, 1000, 2500] as const;
2
+
3
+ /**
4
+ * Example building class preset. Not auto-applied — consumers must include it
5
+ * in their template's `presets.includeClasses` if they want to use it.
6
+ *
7
+ * ```json
8
+ * { "presets": { "includeClasses": { "defaultBuilding": [...] } } }
9
+ * ```
10
+ */
11
+ export const EXAMPLE_BUILDING_CLASSES: string[] = [
12
+ 'space',
13
+ 'ifcspace',
14
+ 'wall',
15
+ 'walls',
16
+ 'ifcwall',
17
+ 'ifcwallstandardcase',
18
+ 'window',
19
+ 'windows',
20
+ 'ifcwindow',
21
+ 'door',
22
+ 'doors',
23
+ 'ifcdoor',
24
+ 'beam',
25
+ 'ifcbeam',
26
+ 'column',
27
+ 'columns',
28
+ 'ifccolumn',
29
+ 'stair',
30
+ 'stairs',
31
+ 'ifcstair',
32
+ 'slab',
33
+ 'ifcslab'
34
+ ];
35
+
36
+ const defaultScalePresets: Record<string, { preferredScales?: number[] }> = {
37
+ default: { preferredScales: Array.from(DEFAULT_SCALE_DENOMS) }
38
+ };
39
+
40
+ export interface ResolvedPresets {
41
+ includeClasses: Record<string, string[]>;
42
+ scales: Record<string, { preferredScales?: number[] }>;
43
+ }
44
+
45
+ export const resolvePresets = (presets?: {
46
+ includeClasses?: Record<string, string[]>;
47
+ scales?: Record<string, { preferredScales?: number[] }>;
48
+ }): ResolvedPresets => ({
49
+ includeClasses: presets?.includeClasses ?? {},
50
+ scales: { ...defaultScalePresets, ...(presets?.scales ?? {}) }
51
+ });
@@ -0,0 +1,126 @@
1
+ import { escapeAttr } from './renderUtils';
2
+ import { interpolate } from './renderUtils';
3
+ import { QrElement, SvgModelView } from './types';
4
+
5
+ async function getQRCode(): Promise<typeof import('qrcode')> {
6
+ try {
7
+ return await import('qrcode');
8
+ } catch {
9
+ throw new Error('qrcode is required for QR code rendering. Install it: bun add qrcode');
10
+ }
11
+ }
12
+
13
+ export interface QrContext {
14
+ host?: string | null;
15
+ containerName?: string | null;
16
+ floorName?: string | null;
17
+ planeHeight?: number | null;
18
+ }
19
+
20
+ export const renderQrElements = async (
21
+ qrElements: QrElement[],
22
+ viewports: any[],
23
+ viewportScales: Record<string, { mmPerModelUnit: number }>,
24
+ models: Record<string, SvgModelView | undefined>,
25
+ options: { qrContext?: QrContext },
26
+ dataWithViewport: any,
27
+ warnings: string[]
28
+ ): Promise<string[]> => {
29
+ const parts: string[] = [];
30
+
31
+ for (const qr of qrElements) {
32
+ const boundViewportId = qr.viewport ?? viewports[0]?.id ?? null;
33
+ const viewportScale = boundViewportId ? viewportScales[boundViewportId] : null;
34
+ const boundViewportDef = boundViewportId ? viewports.find((v) => v.id === boundViewportId) : null;
35
+ // In v2, viewport id IS the model key
36
+ const boundModel = boundViewportDef ? (models[boundViewportDef.id] ?? null) : null;
37
+
38
+ let coordinate: { x: number; y: number; z: number } | null = null;
39
+ if (boundViewportDef && viewportScale && boundModel?.transform && (boundViewportDef.rotationDeg ?? 0) === 0) {
40
+ const mmPerModel = viewportScale.mmPerModelUnit;
41
+ const contentWidthMm = boundModel.widthModel * mmPerModel;
42
+ const contentHeightMm = boundModel.heightModel * mmPerModel;
43
+ const centerX =
44
+ boundViewportDef.align === 'topLeft'
45
+ ? boundViewportDef.xMm + contentWidthMm * 0.5
46
+ : boundViewportDef.xMm + boundViewportDef.widthMm * 0.5;
47
+ const centerY =
48
+ boundViewportDef.align === 'topLeft'
49
+ ? boundViewportDef.yMm + contentHeightMm * 0.5
50
+ : boundViewportDef.yMm + boundViewportDef.heightMm * 0.5;
51
+
52
+ const pointMmX = qr.xMm + qr.sizeMm * 0.5;
53
+ const pointMmY = qr.yMm + qr.sizeMm * 0.5;
54
+
55
+ const modelX = (pointMmX - centerX) / mmPerModel + boundModel.widthModel * 0.5;
56
+ const modelY = (pointMmY - centerY) / mmPerModel + boundModel.heightModel * 0.5;
57
+
58
+ const t = boundModel.transform;
59
+ const worldX = t.viewMinX + modelX;
60
+ const worldZ = t.viewMaxZ - modelY;
61
+ const planeHeight = options?.qrContext?.planeHeight ?? 0;
62
+ coordinate = { x: formatNumber(worldX), y: formatNumber(planeHeight ?? 0), z: formatNumber(worldZ) };
63
+ }
64
+
65
+ let payload: unknown = qr.dataTemplate ? interpolate(qr.dataTemplate, dataWithViewport) : null;
66
+ if (!payload || typeof payload === 'string') {
67
+ if (qr.role === 'anchor' && coordinate) {
68
+ const corner =
69
+ qr.anchorCorner === 'bottom-left'
70
+ ? 'bl'
71
+ : qr.anchorCorner === 'bottom-right'
72
+ ? 'br'
73
+ : qr.anchorCorner === 'top-left'
74
+ ? 'tl'
75
+ : qr.anchorCorner === 'top-right'
76
+ ? 'tr'
77
+ : 'na';
78
+ payload = { v: 1, c: corner, p: coordinate };
79
+ } else if (qr.role === 'main') {
80
+ const cornerShort =
81
+ qr.anchorCorner === 'bottom-left'
82
+ ? 'bl'
83
+ : qr.anchorCorner === 'bottom-right'
84
+ ? 'br'
85
+ : qr.anchorCorner === 'top-left'
86
+ ? 'tl'
87
+ : qr.anchorCorner === 'top-right'
88
+ ? 'tr'
89
+ : 'bl';
90
+ payload = {
91
+ v: 1,
92
+ c: cornerShort,
93
+ h: options?.qrContext?.host ?? null,
94
+ a: options?.qrContext?.containerName ?? null,
95
+ fn: options?.qrContext?.floorName ?? null,
96
+ p: coordinate
97
+ };
98
+ }
99
+ }
100
+
101
+ if (!payload) {
102
+ warnings.push(`QR '${qr.id}' has no payload.`);
103
+ continue;
104
+ }
105
+
106
+ try {
107
+ const QRCode = await getQRCode();
108
+ const dataUrl = await QRCode.toDataURL(typeof payload === 'string' ? payload : JSON.stringify(payload), {
109
+ margin: 0,
110
+ errorCorrectionLevel: 'M'
111
+ });
112
+ parts.push(
113
+ `<image href="${escapeAttr(dataUrl)}" x="${qr.xMm}" y="${qr.yMm}" width="${qr.sizeMm}" height="${qr.sizeMm}" />`
114
+ );
115
+ } catch (error) {
116
+ warnings.push(`Failed to generate QR '${qr.id}': ${(error as Error).message}`);
117
+ }
118
+ }
119
+
120
+ return parts;
121
+ };
122
+
123
+ const formatNumber = (value: number): number => {
124
+ if (!Number.isFinite(value)) return 0;
125
+ return Number.parseFloat(value.toFixed(3));
126
+ };
@@ -0,0 +1,2 @@
1
+ // @ts-ignore
2
+ declare module 'qrcode' { function toDataURL(text: string, options?: any): Promise<string>; }