@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,839 @@
1
+ /**
2
+ * Resolve a dotted property path (e.g. "BIP.spacetype") against a product.
3
+ * Handles both the new T8 API (propertySets → .properties → .value) and
4
+ * legacy BimIfcObject (properties.sets / properties.data).
5
+ *
6
+ * Returns the resolved value, or undefined if not found.
7
+ */
8
+ export function resolveProductProperty(product, path) {
9
+ const p = product;
10
+ const parts = path.split('.');
11
+ if (parts.length < 2)
12
+ return undefined;
13
+ const [setName, ...propParts] = parts;
14
+ const propName = propParts.join('.');
15
+ // New T8 API: propertySets['BIP'].properties['spacetype'].value
16
+ const pset = p?.propertySets?.[setName];
17
+ if (pset) {
18
+ const prop = pset.properties?.[propName];
19
+ if (prop !== undefined) {
20
+ return prop?.value !== undefined ? prop.value : prop;
21
+ }
22
+ }
23
+ // Legacy: properties.sets['BIP']['spacetype']
24
+ const legacyPset = p?.properties?.sets?.[setName];
25
+ if (legacyPset) {
26
+ const val = legacyPset[propName];
27
+ if (val !== undefined) {
28
+ return val?.value !== undefined ? val.value : val;
29
+ }
30
+ }
31
+ // Fallback: properties.data['BIP']['spacetype'] (old printing code path)
32
+ let value = p?.properties?.data;
33
+ if (value) {
34
+ for (const part of parts) {
35
+ value = value?.[part];
36
+ if (value === undefined)
37
+ break;
38
+ }
39
+ if (value !== undefined) {
40
+ return value?.value !== undefined ? value.value : value;
41
+ }
42
+ }
43
+ return undefined;
44
+ }
45
+ /**
46
+ * Sample the actual rendered color of a product's first mesh.
47
+ * Uses BimProductMesh.copyColorTo/getColorComponent for accurate colors
48
+ * (includes overrides, highlights). Falls back to style.surfaceColor/color.
49
+ */
50
+ const sampleProductColorRgba = (product) => {
51
+ const p = product;
52
+ const meshes = (p?.meshes ?? p?.productMeshes);
53
+ if (!meshes || !meshes.length)
54
+ return undefined;
55
+ const rgba = new Uint8Array(4);
56
+ for (const mesh of meshes) {
57
+ try {
58
+ // BimProductMesh: read actual rendered color via copyColorTo or getColorComponent
59
+ if (typeof mesh.copyColorTo === 'function') {
60
+ mesh.copyColorTo(rgba, 0);
61
+ }
62
+ else if (typeof mesh.getColorComponent === 'function') {
63
+ rgba[0] = mesh.getColorComponent(0); // Red
64
+ rgba[1] = mesh.getColorComponent(1); // Green
65
+ rgba[2] = mesh.getColorComponent(2); // Blue
66
+ rgba[3] = mesh.getColorComponent(3); // Alpha
67
+ }
68
+ else {
69
+ // ModelMesh fallback: read from style
70
+ const style = mesh.style;
71
+ const c = style?.surfaceColor ?? style?.color;
72
+ if (c)
73
+ return { r: c[0], g: c[1], b: c[2], a: (c[3] ?? 255) / 255 };
74
+ continue;
75
+ }
76
+ }
77
+ catch {
78
+ continue;
79
+ }
80
+ const [r, g, b, a] = rgba;
81
+ if (a === 0 && r === 0 && g === 0 && b === 0)
82
+ continue;
83
+ return { r, g, b, a: a / 255 };
84
+ }
85
+ return undefined;
86
+ };
87
+ /** Base SVG display width in layout units. */
88
+ export const BASE_WIDTH = 240;
89
+ /** Minimum SVG display height in layout units. */
90
+ export const MIN_HEIGHT = 140;
91
+ /** Margin ratio applied around section bounds (fraction of world extent). */
92
+ export const SECTION_MARGIN_RATIO = 0.05;
93
+ /** Maximum zoom factor for viewport clamping. */
94
+ export const MAX_ZOOM_FACTOR = 12;
95
+ /** Multiplicative step for zooming in. */
96
+ export const ZOOM_IN_STEP = 0.8;
97
+ /** Multiplicative step for zooming out. */
98
+ export const ZOOM_OUT_STEP = 1 / ZOOM_IN_STEP;
99
+ const OUTLINE_THICKNESS_SCALE = 0.5;
100
+ /** Clamp a number to a min/max range. */
101
+ export function clampValue(value, min, max) {
102
+ return Math.min(max, Math.max(min, value));
103
+ }
104
+ /**
105
+ * Extract product metadata for path/label attribution.
106
+ * Casts to `any` once here to avoid scattering `as any` across the codebase.
107
+ * BimIfcObject's runtime shape exposes these fields but the public typings
108
+ * don't always surface them.
109
+ */
110
+ function extractProductMeta(product) {
111
+ const p = product;
112
+ return {
113
+ productId: p?.gid ?? p?.id ?? String(product),
114
+ ifcClass: p?.class?.type ?? null,
115
+ ifcClassId: p?.class?.id ?? null,
116
+ properties: p?.properties?.data ?? p?.properties ?? null
117
+ };
118
+ }
119
+ /** Default fill opacity when a fill rule omits `fillOpacity`. */
120
+ export const DEFAULT_FILL_OPACITY = 0.35;
121
+ /** Convert plane section polylines to SVG path data with fill colors and stroke styles. */
122
+ export function createSectionSvg(perProduct, viewport, strokeWidthHint, labels, options) {
123
+ if (!perProduct.length) {
124
+ return null;
125
+ }
126
+ const polylines = perProduct.flatMap((entry) => entry.polylines);
127
+ const hasSegments = perProduct.some((entry) => entry.segments && entry.segments.length > 0);
128
+ if (!polylines.length && !hasSegments) {
129
+ return null;
130
+ }
131
+ const planeNormal = options?.planeNormal;
132
+ const axes = getProjectionAxes(planeNormal);
133
+ let viewMinX;
134
+ let viewMaxX;
135
+ let viewMinZ;
136
+ let viewMaxZ;
137
+ if (viewport) {
138
+ viewMinX = viewport.viewMinX;
139
+ viewMaxX = viewport.viewMaxX;
140
+ viewMinZ = viewport.viewMinZ;
141
+ viewMaxZ = viewport.viewMaxZ;
142
+ }
143
+ else {
144
+ let minU = Number.POSITIVE_INFINITY;
145
+ let maxU = Number.NEGATIVE_INFINITY;
146
+ let minV = Number.POSITIVE_INFINITY;
147
+ let maxV = Number.NEGATIVE_INFINITY;
148
+ polylines.forEach((polyline) => {
149
+ polyline.points.forEach((point) => {
150
+ if (!point) {
151
+ return;
152
+ }
153
+ const u = axes.getU(point);
154
+ const v = axes.getV(point);
155
+ if (!Number.isFinite(u) || !Number.isFinite(v)) {
156
+ return;
157
+ }
158
+ if (u < minU)
159
+ minU = u;
160
+ if (u > maxU)
161
+ maxU = u;
162
+ if (v < minV)
163
+ minV = v;
164
+ if (v > maxV)
165
+ maxV = v;
166
+ });
167
+ });
168
+ // Also include segments in bounding box calculation
169
+ perProduct.forEach((entry) => {
170
+ if (entry.segments && entry.segments.length) {
171
+ entry.segments.forEach((segment) => {
172
+ const [p1, p2] = segment;
173
+ const u1 = axes.getU(p1);
174
+ const v1 = axes.getV(p1);
175
+ const u2 = axes.getU(p2);
176
+ const v2 = axes.getV(p2);
177
+ if (Number.isFinite(u1) && Number.isFinite(v1)) {
178
+ if (u1 < minU)
179
+ minU = u1;
180
+ if (u1 > maxU)
181
+ maxU = u1;
182
+ if (v1 < minV)
183
+ minV = v1;
184
+ if (v1 > maxV)
185
+ maxV = v1;
186
+ }
187
+ if (Number.isFinite(u2) && Number.isFinite(v2)) {
188
+ if (u2 < minU)
189
+ minU = u2;
190
+ if (u2 > maxU)
191
+ maxU = u2;
192
+ if (v2 < minV)
193
+ minV = v2;
194
+ if (v2 > maxV)
195
+ maxV = v2;
196
+ }
197
+ });
198
+ }
199
+ });
200
+ if (!Number.isFinite(minU) || !Number.isFinite(maxU) || !Number.isFinite(minV) || !Number.isFinite(maxV)) {
201
+ return null;
202
+ }
203
+ if (maxU - minU < 1e-3) {
204
+ minU -= 0.5;
205
+ maxU += 0.5;
206
+ }
207
+ if (maxV - minV < 1e-3) {
208
+ minV -= 0.5;
209
+ maxV += 0.5;
210
+ }
211
+ const worldWidth = maxU - minU;
212
+ const worldHeight = maxV - minV;
213
+ const marginU = Math.max(worldWidth * SECTION_MARGIN_RATIO, 0.5);
214
+ const marginV = Math.max(worldHeight * SECTION_MARGIN_RATIO, 0.5);
215
+ viewMinX = minU - marginU;
216
+ viewMaxX = maxU + marginU;
217
+ viewMinZ = minV - marginV;
218
+ viewMaxZ = maxV + marginV;
219
+ }
220
+ const viewWidth = viewMaxX - viewMinX;
221
+ const viewHeight = viewMaxZ - viewMinZ;
222
+ if (!Number.isFinite(viewWidth) || !Number.isFinite(viewHeight) || viewWidth <= 1e-6 || viewHeight <= 1e-6) {
223
+ return null;
224
+ }
225
+ const autoStrokeBase = Math.max(Math.min(viewWidth, viewHeight) * 0.008, 0.08);
226
+ const autoStroke = Math.max(autoStrokeBase * OUTLINE_THICKNESS_SCALE, 0.02);
227
+ const strokeWidth = Math.max(strokeWidthHint ?? autoStroke, 0.02);
228
+ const geometryTransform = buildGeometryTransform(viewMinX, viewMaxX, viewMinZ, viewMaxZ);
229
+ const includeMeta = !!options?.includeMeta;
230
+ const createFills = options?.createFills !== false; // Default to true for backward compatibility
231
+ const allPaths = [];
232
+ if (includeMeta) {
233
+ perProduct.forEach((entry, idx) => {
234
+ // Skip entries with neither polylines nor segments
235
+ if (!entry.polylines.length && (!entry.segments || !entry.segments.length)) {
236
+ return;
237
+ }
238
+ const colorInfo = sampleProductColorRgba(entry.product);
239
+ const fillColor = colorInfo ? `rgb(${colorInfo.r},${colorInfo.g},${colorInfo.b})` : '#ffffff';
240
+ const fillOpacity = colorInfo ? clampValue(colorInfo.a, 0, 1) : 1;
241
+ const meta = extractProductMeta(entry.product);
242
+ const closedSubpaths = [];
243
+ const holeSubpaths = [];
244
+ const openSubpaths = [];
245
+ entry.polylines.forEach((polyline) => {
246
+ const pathData = getCachedPolylinePath(polyline, planeNormal);
247
+ if (!pathData) {
248
+ return;
249
+ }
250
+ if (pathData.closed) {
251
+ if (pathData.hole) {
252
+ holeSubpaths.push(pathData.d);
253
+ }
254
+ else {
255
+ closedSubpaths.push(pathData.d);
256
+ }
257
+ }
258
+ else {
259
+ openSubpaths.push(pathData.d);
260
+ }
261
+ });
262
+ // Only create fill paths if createFills is true
263
+ if (createFills && (closedSubpaths.length || holeSubpaths.length)) {
264
+ const fillSubpaths = closedSubpaths.length ? closedSubpaths.concat(holeSubpaths) : holeSubpaths;
265
+ allPaths.push({
266
+ key: `fill-${idx}`,
267
+ d: fillSubpaths.join(' '),
268
+ fill: fillColor,
269
+ fillOpacity: clampValue(fillOpacity, 0, 1),
270
+ stroke: '#1f2937',
271
+ strokeWidth,
272
+ fillRule: 'evenodd',
273
+ meta: meta ? { ...meta, isFill: true } : undefined
274
+ });
275
+ }
276
+ if (openSubpaths.length) {
277
+ allPaths.push({
278
+ key: `outline-${idx}`,
279
+ d: openSubpaths.join(' '),
280
+ fill: 'none',
281
+ fillOpacity: 1,
282
+ stroke: '#1f2937',
283
+ strokeWidth,
284
+ meta: meta ? { ...meta, isOutline: true } : undefined
285
+ });
286
+ }
287
+ // Render segments (cut edges) as strokes
288
+ if (entry.segments && entry.segments.length) {
289
+ const segmentPaths = [];
290
+ for (const segment of entry.segments) {
291
+ const path = segmentToPath(segment, planeNormal);
292
+ if (path)
293
+ segmentPaths.push(path);
294
+ }
295
+ if (segmentPaths.length) {
296
+ allPaths.push({
297
+ key: `segments-${idx}`,
298
+ d: segmentPaths.join(' '),
299
+ fill: 'none',
300
+ fillOpacity: 1,
301
+ stroke: '#1f2937',
302
+ strokeWidth,
303
+ meta: meta ? { ...meta, isSegment: true } : undefined
304
+ });
305
+ }
306
+ }
307
+ });
308
+ }
309
+ else {
310
+ const fillGroups = new Map();
311
+ const openSubpaths = [];
312
+ const allSegmentPaths = [];
313
+ perProduct.forEach((entry) => {
314
+ // Process segments first (always render, even without polylines)
315
+ if (entry.segments && entry.segments.length) {
316
+ for (const segment of entry.segments) {
317
+ const path = segmentToPath(segment, planeNormal);
318
+ if (path)
319
+ allSegmentPaths.push(path);
320
+ }
321
+ }
322
+ if (!entry.polylines.length) {
323
+ return;
324
+ }
325
+ const colorInfo = sampleProductColorRgba(entry.product);
326
+ const fillColor = colorInfo ? `rgb(${colorInfo.r},${colorInfo.g},${colorInfo.b})` : '#ffffff';
327
+ const fillOpacity = colorInfo ? clampValue(colorInfo.a, 0, 1) : 1;
328
+ const groupKey = `${fillColor}|${Math.round(fillOpacity * 1000)}`;
329
+ let group = fillGroups.get(groupKey);
330
+ if (!group) {
331
+ group = {
332
+ color: fillColor,
333
+ opacity: fillOpacity,
334
+ closed: [],
335
+ holes: []
336
+ };
337
+ fillGroups.set(groupKey, group);
338
+ }
339
+ entry.polylines.forEach((polyline) => {
340
+ const pathData = getCachedPolylinePath(polyline, planeNormal);
341
+ if (!pathData) {
342
+ return;
343
+ }
344
+ if (pathData.closed) {
345
+ if (pathData.hole) {
346
+ group.holes.push(pathData.d);
347
+ }
348
+ else {
349
+ group.closed.push(pathData.d);
350
+ }
351
+ return;
352
+ }
353
+ openSubpaths.push(pathData.d);
354
+ });
355
+ });
356
+ let groupIndex = 0;
357
+ fillGroups.forEach((group) => {
358
+ const fillSubpaths = group.closed.length ? group.closed.concat(group.holes) : group.holes;
359
+ if (!fillSubpaths.length) {
360
+ return;
361
+ }
362
+ allPaths.push({
363
+ key: `fill-${groupIndex++}`,
364
+ d: fillSubpaths.join(' '),
365
+ fill: group.color,
366
+ fillOpacity: clampValue(group.opacity, 0, 1),
367
+ stroke: '#1f2937',
368
+ strokeWidth,
369
+ fillRule: 'evenodd'
370
+ });
371
+ });
372
+ if (openSubpaths.length) {
373
+ allPaths.push({
374
+ key: 'outlines',
375
+ d: openSubpaths.join(' '),
376
+ fill: 'none',
377
+ fillOpacity: 1,
378
+ stroke: '#1f2937',
379
+ strokeWidth
380
+ });
381
+ }
382
+ // Render all segments (collected above in forEach loop)
383
+ if (allSegmentPaths.length) {
384
+ allPaths.push({
385
+ key: 'segments',
386
+ d: allSegmentPaths.join(' '),
387
+ fill: 'none',
388
+ fillOpacity: 1,
389
+ stroke: '#1f2937',
390
+ strokeWidth
391
+ });
392
+ }
393
+ }
394
+ if (!allPaths.length) {
395
+ return null;
396
+ }
397
+ const displayWidth = BASE_WIDTH;
398
+ const displayHeight = Math.max(MIN_HEIGHT, (viewHeight / viewWidth) * BASE_WIDTH);
399
+ return {
400
+ paths: allPaths,
401
+ transform: {
402
+ viewMinX,
403
+ viewMaxX,
404
+ viewMinZ,
405
+ viewMaxZ,
406
+ viewWidth,
407
+ viewHeight,
408
+ displayWidth,
409
+ displayHeight
410
+ },
411
+ strokeWidth,
412
+ geometryTransform,
413
+ labels: labels?.length
414
+ ? labels
415
+ .filter((label) => {
416
+ if (!Number.isFinite(label.x) || !Number.isFinite(label.z)) {
417
+ return false;
418
+ }
419
+ return label.x >= viewMinX && label.x <= viewMaxX && label.z >= viewMinZ && label.z <= viewMaxZ;
420
+ })
421
+ .map((label) => ({
422
+ key: label.key,
423
+ text: label.text,
424
+ x: label.x,
425
+ y: label.y,
426
+ z: label.z,
427
+ fontSize: label.fontSize,
428
+ meta: includeMeta ? (label.meta ?? {}) : undefined
429
+ }))
430
+ : []
431
+ };
432
+ }
433
+ /**
434
+ * Build a complete SectionRenderState from plane section entries, with optional
435
+ * fill-rule-based separation, ordering, and label generation.
436
+ *
437
+ * When `fills` rules are provided, entries matching a fill rule are rendered
438
+ * below non-matching entries, with their fill opacity set by the matching rule.
439
+ */
440
+ export function buildSectionStateForEntries(perProduct, options) {
441
+ if (!perProduct.length) {
442
+ return null;
443
+ }
444
+ const fills = options.fills;
445
+ const includeMeta = !!options?.includeMeta;
446
+ const includeLabels = options?.includeLabels ?? false;
447
+ const viewport = options?.viewport;
448
+ const strokeWidth = options?.strokeWidth;
449
+ const labelFontSize = includeLabels && typeof options?.labelFontSize === 'number' ? options.labelFontSize : null;
450
+ const buildLabels = options?.buildLabels;
451
+ // Point-in-polygon test (ray casting algorithm)
452
+ const pointInPolygon = (polygon, x, z) => {
453
+ let inside = false;
454
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
455
+ const xi = polygon[i].x;
456
+ const zi = polygon[i].z;
457
+ const xj = polygon[j].x;
458
+ const zj = polygon[j].z;
459
+ const intersect = zi > z !== zj > z && x < ((xj - xi) * (z - zi)) / (zj - zi) + xi;
460
+ if (intersect)
461
+ inside = !inside;
462
+ }
463
+ return inside;
464
+ };
465
+ // Calculate centroid from entry's polylines
466
+ const getCentroid = (entry) => {
467
+ const polylines = entry.polylines;
468
+ if (!polylines.length)
469
+ return null;
470
+ let sumX = 0;
471
+ let sumZ = 0;
472
+ let count = 0;
473
+ for (const polyline of polylines) {
474
+ for (const point of polyline.points) {
475
+ sumX += point[0];
476
+ sumZ += point[2];
477
+ count++;
478
+ }
479
+ }
480
+ if (count === 0)
481
+ return null;
482
+ return { x: sumX / count, z: sumZ / count };
483
+ };
484
+ // Spatial matcher for clip filter - checks if product centroid is inside clip polygon
485
+ const isInsideClipPolygon = (entry) => {
486
+ if (!options?.clipPolygon?.length)
487
+ return true;
488
+ const centroid = getCentroid(entry);
489
+ if (!centroid)
490
+ return false;
491
+ // Check if centroid is inside any of the clip polygons
492
+ return options.clipPolygon.some((polygon) => pointInPolygon(polygon, centroid.x, centroid.z));
493
+ };
494
+ // Match an entry against fill rules. Returns the first matching rule, or undefined.
495
+ const matchFillRule = (entry) => {
496
+ if (!fills?.length)
497
+ return undefined;
498
+ const product = entry.product;
499
+ for (const rule of fills) {
500
+ // If rule requires clip filter match, check if product centroid is inside clip polygon
501
+ if (rule.matchClipFilter && !isInsideClipPolygon(entry)) {
502
+ continue;
503
+ }
504
+ // Check class match
505
+ let classMatches;
506
+ if (rule.class) {
507
+ const classes = Array.isArray(rule.class) ? rule.class : [rule.class];
508
+ const cls = product.class;
509
+ classMatches = classes.some((c) => {
510
+ if (cls?.is)
511
+ return cls.is(c);
512
+ const pName = (product.className ?? '').toLowerCase();
513
+ return pName === c.toLowerCase() || pName === `ifc${c.toLowerCase()}`;
514
+ });
515
+ if (!classMatches)
516
+ continue;
517
+ }
518
+ // Check where condition (property-based filter)
519
+ if (rule.where && Object.keys(rule.where).length > 0) {
520
+ let whereMatches = true;
521
+ for (const [path, expected] of Object.entries(rule.where)) {
522
+ const actualValue = resolveProductProperty(product, path);
523
+ if (actualValue === undefined) {
524
+ whereMatches = false;
525
+ break;
526
+ }
527
+ const normalize = (v) => (typeof v === 'string' ? v.toLowerCase().trim() : v);
528
+ if (Array.isArray(expected)) {
529
+ if (!expected.some((exp) => normalize(exp) === normalize(actualValue))) {
530
+ whereMatches = false;
531
+ break;
532
+ }
533
+ }
534
+ else {
535
+ if (normalize(actualValue) !== normalize(expected)) {
536
+ whereMatches = false;
537
+ break;
538
+ }
539
+ }
540
+ }
541
+ if (!whereMatches)
542
+ continue;
543
+ }
544
+ return rule;
545
+ }
546
+ return undefined;
547
+ };
548
+ const hasFills = !!fills?.length;
549
+ // Split entries by fill rule match
550
+ const fillEntries = [];
551
+ const fillOpacities = []; // parallel to fillEntries
552
+ const nonFillEntries = [];
553
+ for (const entry of perProduct) {
554
+ const rule = hasFills ? matchFillRule(entry) : undefined;
555
+ if (rule) {
556
+ const opacity = rule.fillOpacity ?? DEFAULT_FILL_OPACITY;
557
+ // Skip entries with zero opacity - don't render them at all
558
+ if (opacity > 0) {
559
+ fillEntries.push(entry);
560
+ fillOpacities.push(opacity);
561
+ }
562
+ }
563
+ else {
564
+ nonFillEntries.push(entry);
565
+ }
566
+ }
567
+ // Build labels from fill-matched entries (e.g. spaces)
568
+ const fillLabels = includeLabels && fillEntries.length && labelFontSize !== null && buildLabels
569
+ ? buildLabels(fillEntries.map((entry) => entry.product), labelFontSize)
570
+ : undefined;
571
+ // When fill rules are present, render fill and non-fill groups separately
572
+ // so we can adjust opacity and control ordering.
573
+ // When no fill rules, render everything in one pass (unchanged behavior).
574
+ if (hasFills && (fillEntries.length > 0 || nonFillEntries.length > 0)) {
575
+ const nonFillState = nonFillEntries.length > 0
576
+ ? createSectionSvg(nonFillEntries, viewport, strokeWidth, undefined, {
577
+ includeMeta,
578
+ planeNormal: options.planeNormal
579
+ })
580
+ : null;
581
+ const fillState = fillEntries.length > 0
582
+ ? createSectionSvg(fillEntries, viewport, strokeWidth, includeLabels ? fillLabels : undefined, {
583
+ includeMeta,
584
+ planeNormal: options.planeNormal
585
+ })
586
+ : null;
587
+ const baseState = nonFillState ?? fillState ?? null;
588
+ if (!baseState)
589
+ return null;
590
+ // Adjust fill opacity per-entry. When includeMeta is true, each entry has
591
+ // its own path, so we can apply per-entry opacity. When false, paths are
592
+ // grouped by color — apply the minimum matching opacity.
593
+ const adjustedFillPaths = fillState?.paths.map((path, idx) => {
594
+ // When fill rules match, use the rule opacity directly (don't multiply by material alpha).
595
+ // Material alpha was useful for auto-coloring, but fill rules should have full control.
596
+ const ruleOpacity = idx < fillOpacities.length ? fillOpacities[idx] : (fills[0]?.fillOpacity ?? 0.35);
597
+ const finalOpacity = clampValue(ruleOpacity, 0, 1);
598
+ return {
599
+ ...path,
600
+ fillOpacity: finalOpacity
601
+ };
602
+ }) ?? [];
603
+ const nonFillPaths = nonFillState?.paths ?? [];
604
+ // Fill-matched entries always render below non-fill entries
605
+ const mergedPaths = [...adjustedFillPaths, ...nonFillPaths];
606
+ const merged = {
607
+ ...baseState,
608
+ paths: mergedPaths,
609
+ labels: includeLabels ? (fillState?.labels ?? fillLabels ?? []) : []
610
+ };
611
+ return merged;
612
+ }
613
+ // No fill rules — create paths with fills for highlight support, but set opacity to 0
614
+ const state = createSectionSvg(perProduct, viewport, strokeWidth, includeLabels ? fillLabels : undefined, {
615
+ includeMeta,
616
+ createFills: true,
617
+ planeNormal: options.planeNormal
618
+ });
619
+ if (!state)
620
+ return null;
621
+ // Set all fill opacities to 0 when no fill rules exist (but keep paths for highlight support)
622
+ const pathsWithZeroFillOpacity = state.paths.map((path) => ({
623
+ ...path,
624
+ fillOpacity: 0
625
+ }));
626
+ return {
627
+ ...state,
628
+ paths: pathsWithZeroFillOpacity,
629
+ labels: includeLabels ? (state.labels ?? []) : []
630
+ };
631
+ }
632
+ /** Extract the viewport bounding box from a TransformInfo. */
633
+ export function extractViewWindow(transform) {
634
+ return {
635
+ viewMinX: transform.viewMinX,
636
+ viewMaxX: transform.viewMaxX,
637
+ viewMinZ: transform.viewMinZ,
638
+ viewMaxZ: transform.viewMaxZ
639
+ };
640
+ }
641
+ /** Compare two viewport windows for equality within a tolerance. */
642
+ export function windowsEqual(a, b, epsilon = 1e-4) {
643
+ return (Math.abs(a.viewMinX - b.viewMinX) <= epsilon &&
644
+ Math.abs(a.viewMaxX - b.viewMaxX) <= epsilon &&
645
+ Math.abs(a.viewMinZ - b.viewMinZ) <= epsilon &&
646
+ Math.abs(a.viewMaxZ - b.viewMaxZ) <= epsilon);
647
+ }
648
+ /** Constrain a viewport window within base bounds, preserving aspect ratio. */
649
+ export function clampViewWindow(base, candidate) {
650
+ const baseWidth = base.viewWidth;
651
+ const baseHeight = base.viewHeight;
652
+ const aspect = baseWidth / baseHeight;
653
+ let width = candidate.viewMaxX - candidate.viewMinX;
654
+ if (!Number.isFinite(width) || width <= 1e-6) {
655
+ width = baseWidth;
656
+ }
657
+ width = clampValue(width, baseWidth / MAX_ZOOM_FACTOR, baseWidth);
658
+ const height = width / aspect;
659
+ let centerX = (candidate.viewMinX + candidate.viewMaxX) * 0.5;
660
+ if (!Number.isFinite(centerX)) {
661
+ centerX = (base.viewMinX + base.viewMaxX) * 0.5;
662
+ }
663
+ let centerZ = (candidate.viewMinZ + candidate.viewMaxZ) * 0.5;
664
+ if (!Number.isFinite(centerZ)) {
665
+ centerZ = (base.viewMinZ + base.viewMaxZ) * 0.5;
666
+ }
667
+ let minX = centerX - width * 0.5;
668
+ let maxX = minX + width;
669
+ if (minX < base.viewMinX) {
670
+ minX = base.viewMinX;
671
+ maxX = minX + width;
672
+ }
673
+ if (maxX > base.viewMaxX) {
674
+ maxX = base.viewMaxX;
675
+ minX = maxX - width;
676
+ }
677
+ let minZ = centerZ - height * 0.5;
678
+ let maxZ = minZ + height;
679
+ if (minZ < base.viewMinZ) {
680
+ minZ = base.viewMinZ;
681
+ maxZ = minZ + height;
682
+ }
683
+ if (maxZ > base.viewMaxZ) {
684
+ maxZ = base.viewMaxZ;
685
+ minZ = maxZ - height;
686
+ }
687
+ return {
688
+ viewMinX: minX,
689
+ viewMaxX: maxX,
690
+ viewMinZ: minZ,
691
+ viewMaxZ: maxZ
692
+ };
693
+ }
694
+ /** Adjust stroke width proportionally to zoom level. */
695
+ export function strokeWidthForWindow(baseStroke, baseTransform, window) {
696
+ const baseWidth = baseTransform.viewWidth;
697
+ const currentWidth = window.viewMaxX - window.viewMinX;
698
+ if (!Number.isFinite(baseWidth) || baseWidth <= 1e-6 || !Number.isFinite(currentWidth) || currentWidth <= 1e-6) {
699
+ return baseStroke;
700
+ }
701
+ const ratio = clampValue(currentWidth / baseWidth, 1 / MAX_ZOOM_FACTOR, 1);
702
+ const scaled = baseStroke * ratio;
703
+ return Math.max(scaled, baseStroke / (MAX_ZOOM_FACTOR + 1));
704
+ }
705
+ const polylinePathCache = new WeakMap();
706
+ /**
707
+ * Determine which axes to use for 2D projection based on plane normal.
708
+ * Returns accessor functions for horizontal and vertical SVG coordinates.
709
+ */
710
+ function getProjectionAxes(planeNormal) {
711
+ if (!planeNormal) {
712
+ // Default: horizontal plan (looking down Y axis) -> X,Z
713
+ return {
714
+ getU: (p) => p[0],
715
+ getV: (p) => p[2],
716
+ axisNames: 'XZ'
717
+ };
718
+ }
719
+ // Normalize and find dominant axis
720
+ const absX = Math.abs(planeNormal[0]);
721
+ const absY = Math.abs(planeNormal[1]);
722
+ const absZ = Math.abs(planeNormal[2]);
723
+ if (absY > absX && absY > absZ) {
724
+ // Horizontal plan (normal ~ Y axis) -> looking down Y, use X,Z
725
+ return {
726
+ getU: (p) => p[0],
727
+ getV: (p) => p[2],
728
+ axisNames: 'XZ'
729
+ };
730
+ }
731
+ else if (absZ > absX) {
732
+ // Vertical section perpendicular to Z -> looking along Z, use X,Y
733
+ return {
734
+ getU: (p) => p[0],
735
+ getV: (p) => p[1],
736
+ axisNames: 'XY'
737
+ };
738
+ }
739
+ else {
740
+ // Vertical section perpendicular to X -> looking along X, use Z,Y
741
+ return {
742
+ getU: (p) => p[2],
743
+ getV: (p) => p[1],
744
+ axisNames: 'ZY'
745
+ };
746
+ }
747
+ }
748
+ /**
749
+ * Convert a segment (pair of points) to an SVG path string.
750
+ */
751
+ function segmentToPath(segment, planeNormal) {
752
+ const [p1, p2] = segment;
753
+ const axes = getProjectionAxes(planeNormal);
754
+ const u1 = axes.getU(p1);
755
+ const v1 = axes.getV(p1);
756
+ const u2 = axes.getU(p2);
757
+ const v2 = axes.getV(p2);
758
+ if (!Number.isFinite(u1) || !Number.isFinite(v1) || !Number.isFinite(u2) || !Number.isFinite(v2)) {
759
+ return '';
760
+ }
761
+ return `M${formatNumber(u1)} ${formatNumber(v1)} L${formatNumber(u2)} ${formatNumber(v2)}`;
762
+ }
763
+ function getCachedPolylinePath(polyline, planeNormal) {
764
+ if (!polyline.points.length) {
765
+ return null;
766
+ }
767
+ const signature = computePointsSignature(polyline.points, polyline.closed, planeNormal);
768
+ const cached = polylinePathCache.get(polyline);
769
+ if (cached && cached.pointsRef === polyline.points && cached.signature === signature) {
770
+ return cached.data;
771
+ }
772
+ const axes = getProjectionAxes(planeNormal);
773
+ const commands = [];
774
+ polyline.points.forEach((point, index) => {
775
+ const prefix = index === 0 ? 'M' : 'L';
776
+ const u = axes.getU(point);
777
+ const v = axes.getV(point);
778
+ commands.push(`${prefix}${formatNumber(u)} ${formatNumber(v)}`);
779
+ });
780
+ if (commands.length < 2) {
781
+ return null;
782
+ }
783
+ let closed = false;
784
+ let hole = false;
785
+ if (polyline.closed && commands.length > 2) {
786
+ const area = signedArea(polyline.points, axes);
787
+ hole = area < 0;
788
+ closed = true;
789
+ commands.push('Z');
790
+ }
791
+ const data = { d: commands.join(' '), closed, hole };
792
+ polylinePathCache.set(polyline, { pointsRef: polyline.points, signature, data });
793
+ return data;
794
+ }
795
+ function signedArea(points, axes) {
796
+ if (points.length < 3) {
797
+ return 0;
798
+ }
799
+ let area = 0;
800
+ for (let i = 0; i < points.length; i++) {
801
+ const current = points[i];
802
+ const next = points[(i + 1) % points.length];
803
+ const u1 = axes.getU(current);
804
+ const v1 = axes.getV(current);
805
+ const u2 = axes.getU(next);
806
+ const v2 = axes.getV(next);
807
+ area += u1 * v2 - u2 * v1;
808
+ }
809
+ return area * 0.5;
810
+ }
811
+ function formatNumber(value) {
812
+ if (!Number.isFinite(value)) {
813
+ return '0';
814
+ }
815
+ const formatted = value.toFixed(3);
816
+ return formatted.replace(/\.?0+$/, '');
817
+ }
818
+ function buildGeometryTransform(viewMinX, viewMaxX, viewMinZ, viewMaxZ) {
819
+ const translateX = formatNumber(-viewMinX);
820
+ const translateY = formatNumber(viewMaxZ);
821
+ return `matrix(1 0 0 -1 ${translateX} ${translateY})`;
822
+ }
823
+ function computePointsSignature(points, closed, planeNormal) {
824
+ const axes = getProjectionAxes(planeNormal);
825
+ // FNV-1a-inspired hash — compact fixed-size key instead of concatenating all coordinates
826
+ let hash = 2166136261 ^ (points.length * 16777619) ^ (closed ? 1 : 0);
827
+ for (let i = 0; i < points.length; i++) {
828
+ const point = points[i];
829
+ if (!point)
830
+ continue;
831
+ const u = axes.getU(point);
832
+ const v = axes.getV(point);
833
+ // Mix in coordinate bits (multiply by large prime, XOR)
834
+ hash = Math.imul(hash ^ ((u * 1e4) | 0), 16777619);
835
+ hash = Math.imul(hash ^ ((v * 1e4) | 0), 16777619);
836
+ }
837
+ return hash;
838
+ }
839
+ //# sourceMappingURL=sections.js.map