@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,218 @@
1
+ import { normalizeClass } from './filters';
2
+ import type {
3
+ LayoutTemplate,
4
+ LayoutElement,
5
+ ViewportElement,
6
+ TextElement,
7
+ ImageElement,
8
+ QrElement,
9
+ LabelsElement,
10
+ LegendElement,
11
+ GroupElement,
12
+ ScaleBarElement,
13
+ WhereCondition
14
+ } from './types';
15
+
16
+ /**
17
+ * Describes the input requirements inferred from a layout template.
18
+ * Use `analyzeTemplate()` to scan a template and discover what data it needs.
19
+ */
20
+ export interface TemplateRequirements {
21
+ /** Viewport IDs the template declares. */
22
+ viewportIds: string[];
23
+
24
+ /** Property paths referenced by legends, where-filters, and labels.
25
+ * Short-path format: "SetName.propName" (e.g. "BIP.spacename"). */
26
+ propertyPaths: string[];
27
+
28
+ /** Class filters referenced (e.g. "space", "walls"). Already lowercase. */
29
+ classFilters: string[];
30
+
31
+ /** Asset IDs referenced by image/northArrow elements. */
32
+ assetIds: string[];
33
+
34
+ /** Text interpolation variables (e.g. "viewport.main.scaleText"). */
35
+ interpolationVars: string[];
36
+
37
+ /** Whether the template references remote (http/https) asset URLs. */
38
+ usesRemoteAssets: boolean;
39
+ }
40
+
41
+ const INTERPOLATION_RE = /\{\{\s*([^}]+?)\s*\}\}/g;
42
+
43
+ /** Extract `{{path}}` tokens from a template string. */
44
+ function extractInterpolationVars(text: string): string[] {
45
+ const vars: string[] = [];
46
+ let m: RegExpExecArray | null;
47
+ INTERPOLATION_RE.lastIndex = 0;
48
+ while ((m = INTERPOLATION_RE.exec(text)) !== null) {
49
+ vars.push(m[1]);
50
+ }
51
+ return vars;
52
+ }
53
+
54
+ /** Collect keys from a WhereCondition (each key is a property short-path). */
55
+ function collectWherePaths(where: WhereCondition | undefined, out: Set<string>): void {
56
+ if (!where || typeof where !== 'object') return;
57
+ for (const key of Object.keys(where)) {
58
+ out.add(key);
59
+ }
60
+ }
61
+
62
+ /** Collect class filter values, normalizing to lowercase. */
63
+ function collectClassFilter(classFilter: string | string[] | undefined, out: Set<string>): void {
64
+ if (!classFilter) return;
65
+ const classes = Array.isArray(classFilter) ? classFilter : [classFilter];
66
+ for (const c of classes) {
67
+ const normalized = normalizeClass(c);
68
+ if (normalized) out.add(normalized);
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Scan a layout template and return its input requirements.
74
+ *
75
+ * This is a pure, read-only analysis — it does not modify the template or
76
+ * perform any rendering. Use the result to validate inputs before calling
77
+ * `renderDrawing()` or `renderLayout()`.
78
+ */
79
+ export function analyzeTemplate(template: LayoutTemplate): TemplateRequirements {
80
+ const viewportIds = new Set<string>();
81
+ const propertyPaths = new Set<string>();
82
+ const classFilters = new Set<string>();
83
+ const assetIds = new Set<string>();
84
+ const interpolationVars = new Set<string>();
85
+ let usesRemoteAssets = false;
86
+
87
+ // Check assets for remote URLs
88
+ if (template.assets) {
89
+ for (const asset of template.assets) {
90
+ if (asset.src && /^https?:\/\//i.test(asset.src)) {
91
+ usesRemoteAssets = true;
92
+ }
93
+ }
94
+ }
95
+
96
+ // Walk all elements (recursing into groups)
97
+ const walk = (elements: LayoutElement[]): void => {
98
+ for (const el of elements) {
99
+ switch (el.kind) {
100
+ case 'viewport': {
101
+ const vp = el as ViewportElement;
102
+ viewportIds.add(vp.id);
103
+
104
+ // Clip filter
105
+ if (vp.clip) {
106
+ collectClassFilter(vp.clip.class, classFilters);
107
+ collectWherePaths(vp.clip.where, propertyPaths);
108
+ }
109
+
110
+ // Highlight filter
111
+ if (vp.highlight) {
112
+ collectClassFilter(vp.highlight.class, classFilters);
113
+ collectWherePaths(vp.highlight.where, propertyPaths);
114
+ }
115
+
116
+ // includeClasses
117
+ if (vp.includeClasses) {
118
+ if (Array.isArray(vp.includeClasses)) {
119
+ for (const c of vp.includeClasses) {
120
+ const normalized = normalizeClass(c);
121
+ if (normalized) classFilters.add(normalized);
122
+ }
123
+ }
124
+ // Preset references are resolved at render time, nothing to collect
125
+ }
126
+
127
+ // North arrow asset
128
+ if (vp.northArrow?.assetId) {
129
+ assetIds.add(vp.northArrow.assetId);
130
+ }
131
+ break;
132
+ }
133
+
134
+ case 'text': {
135
+ const txt = el as TextElement;
136
+ if (txt.text) {
137
+ for (const v of extractInterpolationVars(txt.text)) {
138
+ interpolationVars.add(v);
139
+ }
140
+ }
141
+ break;
142
+ }
143
+
144
+ case 'image': {
145
+ const img = el as ImageElement;
146
+ if (img.assetId) assetIds.add(img.assetId);
147
+ break;
148
+ }
149
+
150
+ case 'qr': {
151
+ const qr = el as QrElement;
152
+ if (qr.dataTemplate) {
153
+ for (const v of extractInterpolationVars(qr.dataTemplate)) {
154
+ interpolationVars.add(v);
155
+ }
156
+ }
157
+ break;
158
+ }
159
+
160
+ case 'labels': {
161
+ const sl = el as LabelsElement;
162
+ collectClassFilter(sl.class, classFilters);
163
+ collectWherePaths(sl.where, propertyPaths);
164
+
165
+ // Extract property paths from text interpolation (skip viewport.* which are computed)
166
+ if (sl.text) {
167
+ for (const v of extractInterpolationVars(sl.text)) {
168
+ if (!v.startsWith('viewport.')) {
169
+ propertyPaths.add(v);
170
+ } else {
171
+ interpolationVars.add(v);
172
+ }
173
+ }
174
+ }
175
+ break;
176
+ }
177
+
178
+ case 'legend': {
179
+ const leg = el as LegendElement;
180
+ collectClassFilter(leg.class, classFilters);
181
+ collectWherePaths(leg.where, propertyPaths);
182
+
183
+ // Column property paths
184
+ if (leg.columns) {
185
+ for (const col of leg.columns) {
186
+ if (col.property) propertyPaths.add(col.property);
187
+ }
188
+ }
189
+ break;
190
+ }
191
+
192
+ case 'scaleBar': {
193
+ // scaleBar references a viewport by ID (informational)
194
+ const sb = el as ScaleBarElement;
195
+ if (sb.viewport) viewportIds.add(sb.viewport);
196
+ break;
197
+ }
198
+
199
+ case 'group': {
200
+ const grp = el as GroupElement;
201
+ if (grp.children) walk(grp.children);
202
+ break;
203
+ }
204
+ }
205
+ }
206
+ };
207
+
208
+ walk(template.elements);
209
+
210
+ return {
211
+ viewportIds: [...viewportIds],
212
+ propertyPaths: [...propertyPaths],
213
+ classFilters: [...classFilters],
214
+ assetIds: [...assetIds],
215
+ interpolationVars: [...interpolationVars],
216
+ usesRemoteAssets
217
+ };
218
+ }
@@ -0,0 +1,193 @@
1
+ import type { SvgModelView } from './types';
2
+ import { EPSILON_DIVISION_SAFETY, CENTER_FRACTION } from './geometryConstants';
3
+
4
+ export type Point = { x: number; y: number };
5
+ export type Polygon = Point[];
6
+
7
+ import { isRecord, getProp } from './typeGuards';
8
+
9
+ export const pointInPolygon = (polygon: Polygon, x: number, y: number): boolean => {
10
+ let inside = false;
11
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
12
+ const xi = polygon[i].x,
13
+ yi = polygon[i].y;
14
+ const xj = polygon[j].x,
15
+ yj = polygon[j].y;
16
+ const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi;
17
+ if (intersect) inside = !inside;
18
+ }
19
+ return inside;
20
+ };
21
+
22
+ export const filterItemsInPolygons = (
23
+ items: unknown[],
24
+ polygons: Polygon[],
25
+ xField = 'x',
26
+ yField = 'z',
27
+ getPathValue: (obj: unknown, path: string) => unknown
28
+ ): unknown[] => {
29
+ if (!Array.isArray(items) || !Array.isArray(polygons) || polygons.length === 0) return items ?? [];
30
+ return items.filter((item) => {
31
+ const xVal = getPathValue(item, xField);
32
+ const yVal = getPathValue(item, yField);
33
+ if (!Number.isFinite(xVal) || !Number.isFinite(yVal)) return false;
34
+ const x = Number(xVal);
35
+ const y = Number(yVal);
36
+ return polygons.some((poly) => pointInPolygon(poly, x, y));
37
+ });
38
+ };
39
+
40
+ export const filterEntriesByClip = (
41
+ entries: unknown[],
42
+ polygons: Array<Array<{ x: number; z: number }>>
43
+ ): unknown[] => {
44
+ if (!Array.isArray(entries) || !Array.isArray(polygons) || polygons.length === 0) return entries ?? [];
45
+
46
+ const getCenter = (entry: unknown): { x: number | null; z: number | null } => {
47
+ const product = getProp(entry, 'product') ?? entry;
48
+ try {
49
+ const boundingInfoFn = getProp(product, 'boundingInfo');
50
+ const boundingInfo = typeof boundingInfoFn === 'function' ? boundingInfoFn() : null;
51
+ const bbox = isRecord(boundingInfo) ? getProp(boundingInfo, 'boundingBox') : null;
52
+ const min = isRecord(bbox) ? (getProp(bbox, 'minimumWorld') ?? getProp(bbox, 'minimum')) : null;
53
+ const max = isRecord(bbox) ? (getProp(bbox, 'maximumWorld') ?? getProp(bbox, 'maximum')) : null;
54
+ const minX = isRecord(min) ? getProp(min, 'x') : null;
55
+ const maxX = isRecord(max) ? getProp(max, 'x') : null;
56
+ const minZ = isRecord(min) ? getProp(min, 'z') : null;
57
+ const maxZ = isRecord(max) ? getProp(max, 'z') : null;
58
+ if (Number.isFinite(minX) && Number.isFinite(maxX) && Number.isFinite(minZ) && Number.isFinite(maxZ)) {
59
+ return {
60
+ x: (Number(minX) + Number(maxX)) * CENTER_FRACTION,
61
+ z: (Number(minZ) + Number(maxZ)) * CENTER_FRACTION
62
+ };
63
+ }
64
+ } catch {
65
+ // Bounding info access may fail for products without geometry or with malformed data structures.
66
+ // This is expected behavior - we fall back to entry.x/z properties below.
67
+ }
68
+ const x = getProp(entry, 'x');
69
+ const z = getProp(entry, 'z');
70
+ return { x: typeof x === 'number' ? x : null, z: typeof z === 'number' ? z : null };
71
+ };
72
+
73
+ return entries.filter((entry) => {
74
+ const center = getCenter(entry);
75
+ if (!Number.isFinite(center.x) || !Number.isFinite(center.z)) return false;
76
+ const x = Number(center.x);
77
+ const z = Number(center.z);
78
+ return polygons.some((poly) =>
79
+ pointInPolygon(
80
+ poly.map((p) => ({ x: p.x, y: p.z })),
81
+ x,
82
+ z
83
+ )
84
+ );
85
+ });
86
+ };
87
+
88
+ export const offsetPolygons = (polys: Polygon[], delta: number): Polygon[] => {
89
+ if (!Array.isArray(polys) || !polys.length || !Number.isFinite(delta) || delta === 0) return polys ?? [];
90
+ const intersectLines = (p: Point, r: Point, q: Point, s: Point): Point | null => {
91
+ const rxs = r.x * s.y - r.y * s.x;
92
+ if (Math.abs(rxs) < EPSILON_DIVISION_SAFETY) return null; // parallel
93
+ const t = ((q.x - p.x) * s.y - (q.y - p.y) * s.x) / rxs;
94
+ return { x: p.x + t * r.x, y: p.y + t * r.y };
95
+ };
96
+ const offsetPoly = (poly: Polygon): Polygon => {
97
+ const n = poly.length;
98
+ if (n < 3) return poly;
99
+ const area = poly.reduce((sum, p, i) => {
100
+ const q = poly[(i + 1) % n];
101
+ return sum + p.x * q.y - q.x * p.y;
102
+ }, 0);
103
+ const orient = area >= 0 ? 1 : -1; // ccw => outward is right normal
104
+ const out: Polygon = [];
105
+ for (let i = 0; i < n; i++) {
106
+ const p0 = poly[(i + n - 1) % n];
107
+ const p1 = poly[i];
108
+ const p2 = poly[(i + 1) % n];
109
+ const e1x = p1.x - p0.x;
110
+ const e1y = p1.y - p0.y;
111
+ const e2x = p2.x - p1.x;
112
+ const e2y = p2.y - p1.y;
113
+ const len1 = Math.hypot(e1x, e1y) || EPSILON_DIVISION_SAFETY;
114
+ const len2 = Math.hypot(e2x, e2y) || EPSILON_DIVISION_SAFETY;
115
+ const n1x = (orient * e1y) / len1;
116
+ const n1y = (orient * -e1x) / len1;
117
+ const n2x = (orient * e2y) / len2;
118
+ const n2y = (orient * -e2x) / len2;
119
+ const line1Point = { x: p1.x + n1x * delta, y: p1.y + n1y * delta };
120
+ const line2Point = { x: p1.x + n2x * delta, y: p1.y + n2y * delta };
121
+ const inter = intersectLines(line1Point, { x: e1x, y: e1y }, line2Point, { x: e2x, y: e2y });
122
+ out.push(inter ?? line2Point);
123
+ }
124
+ return out;
125
+ };
126
+ return polys.map(offsetPoly);
127
+ };
128
+
129
+ export const projectPolygonsToLocal = (
130
+ polygonsSource: unknown,
131
+ xField: string,
132
+ yField: string,
133
+ model: SvgModelView,
134
+ rotationDeg: number,
135
+ contentOffsetX: number,
136
+ contentOffsetY: number,
137
+ mmPerModel: number
138
+ ): { local: Polygon[]; page: Polygon[] } => {
139
+ const cosA = Math.cos((rotationDeg * Math.PI) / 180);
140
+ const sinA = Math.sin((rotationDeg * Math.PI) / 180);
141
+ const toLocal = (px: number, py: number): Point => {
142
+ const viewX = px - model.transform!.viewMinX;
143
+ const viewY = model.transform!.viewMaxZ - py;
144
+ const x0 = viewX - model.widthModel / 2;
145
+ const y0 = viewY - model.heightModel / 2;
146
+ const x1 = x0 * cosA - y0 * sinA;
147
+ const y1 = x0 * sinA + y0 * cosA;
148
+ return { x: x1, y: y1 };
149
+ };
150
+ const toPage = (px: number, py: number): Point => {
151
+ const viewX = px - model.transform!.viewMinX;
152
+ const viewY = model.transform!.viewMaxZ - py;
153
+ const x0 = viewX - model.widthModel / 2;
154
+ const y0 = viewY - model.heightModel / 2;
155
+ const x1 = x0 * cosA - y0 * sinA;
156
+ const y1 = x0 * sinA + y0 * cosA;
157
+ return { x: contentOffsetX + x1 * mmPerModel, y: contentOffsetY + y1 * mmPerModel };
158
+ };
159
+ const projectPolyLocal = (poly: unknown[]): Polygon =>
160
+ poly
161
+ .map((pt: unknown) => {
162
+ const px = Number(getProp(pt, xField));
163
+ const py = Number(getProp(pt, yField));
164
+ if (!Number.isFinite(px) || !Number.isFinite(py)) return null;
165
+ return toLocal(px, py);
166
+ })
167
+ .filter(Boolean) as Polygon;
168
+ const projectPoly = (poly: unknown[]): Polygon =>
169
+ poly
170
+ .map((pt: unknown) => {
171
+ const px = Number(getProp(pt, xField));
172
+ const py = Number(getProp(pt, yField));
173
+ if (!Number.isFinite(px) || !Number.isFinite(py)) return null;
174
+ return toPage(px, py);
175
+ })
176
+ .filter(Boolean) as Polygon;
177
+
178
+ const local: Polygon[] = [];
179
+ const page: Polygon[] = [];
180
+ if (Array.isArray(polygonsSource)) {
181
+ polygonsSource.forEach((poly: unknown) => {
182
+ const points = getProp(poly, 'points');
183
+ const pl = Array.isArray(points) ? points : Array.isArray(poly) ? poly : null;
184
+ if (!pl || pl.length < 3) return;
185
+ const projectedLocal = projectPolyLocal(pl);
186
+ if (projectedLocal.length >= 3) {
187
+ local.push(projectedLocal);
188
+ page.push(projectPoly(pl));
189
+ }
190
+ });
191
+ }
192
+ return { local, page };
193
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Color computation constants
3
+ */
4
+
5
+ // RGB color value limits
6
+ export const RGB_MAX = 255; // Maximum value for RGB color components (0-255)
7
+ export const RGB_MIN = 0; // Minimum value for RGB color components
8
+
9
+ // Bit shift offsets for color packing/unpacking
10
+ export const RED_SHIFT = 16; // Shift for red component in 24-bit color
11
+ export const GREEN_SHIFT = 8; // Shift for green component in 24-bit color
12
+
13
+ // Color modulo for hash-based color generation
14
+ export const HASH_COLOR_MODULO = 255; // Modulo for generating colors from hash values
15
+ export const HASH_GREEN_SHIFT = 4; // Bit shift for green in hash-based colors
16
+ export const HASH_BLUE_SHIFT = 8; // Bit shift for blue in hash-based colors
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Constants for layout element kinds
3
+ */
4
+
5
+ export const ELEMENT_KIND_VIEWPORT = 'viewport';
6
+ export const ELEMENT_KIND_TEXT = 'text';
7
+ export const ELEMENT_KIND_IMAGE = 'image';
8
+ export const ELEMENT_KIND_LINE = 'line';
9
+ export const ELEMENT_KIND_RECT = 'rect';
10
+ export const ELEMENT_KIND_CIRCLE = 'circle';
11
+ export const ELEMENT_KIND_POLYLINE = 'polyline';
12
+ export const ELEMENT_KIND_POLYGON = 'polygon';
13
+ export const ELEMENT_KIND_SCALE_BAR = 'scaleBar';
14
+ export const ELEMENT_KIND_QR = 'qr';
15
+ export const ELEMENT_KIND_LABELS = 'labels';
16
+ export const ELEMENT_KIND_LEGEND = 'legend';
17
+ export const ELEMENT_KIND_GROUP = 'group';
18
+
19
+ /**
20
+ * Union type of all valid element kinds
21
+ */
22
+ export type ElementKind =
23
+ | typeof ELEMENT_KIND_VIEWPORT
24
+ | typeof ELEMENT_KIND_TEXT
25
+ | typeof ELEMENT_KIND_IMAGE
26
+ | typeof ELEMENT_KIND_LINE
27
+ | typeof ELEMENT_KIND_RECT
28
+ | typeof ELEMENT_KIND_CIRCLE
29
+ | typeof ELEMENT_KIND_POLYLINE
30
+ | typeof ELEMENT_KIND_POLYGON
31
+ | typeof ELEMENT_KIND_SCALE_BAR
32
+ | typeof ELEMENT_KIND_QR
33
+ | typeof ELEMENT_KIND_LABELS
34
+ | typeof ELEMENT_KIND_LEGEND
35
+ | typeof ELEMENT_KIND_GROUP;
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Drawing export utilities for converting SVG markup to various formats.
3
+ *
4
+ * This module provides functions for exporting drawings to PDF and SVG files.
5
+ * These are pure functions that take SVG markup and export options, with no
6
+ * dependencies on BIM viewer APIs or application state.
7
+ *
8
+ * @module exportDrawing
9
+ */
10
+
11
+ import { exportSectionToPdf, exportSectionToSvg } from '../printToPdf';
12
+
13
+ /**
14
+ * Extracts page dimensions from SVG markup.
15
+ * Parses the width/height attributes from the root <svg> element.
16
+ *
17
+ * @param svg - SVG markup string
18
+ * @returns Page dimensions in millimeters
19
+ * @throws {Error} If SVG dimensions cannot be parsed
20
+ */
21
+ function extractPageSizeFromSvg(svg: string): { widthMm: number; heightMm: number } {
22
+ // Match width and height attributes (supports both "420mm" and "420" formats)
23
+ const widthMatch = svg.match(/width="([0-9.]+)(?:mm)?"/);
24
+ const heightMatch = svg.match(/height="([0-9.]+)(?:mm)?"/);
25
+
26
+ if (!widthMatch || !heightMatch) {
27
+ throw new Error(
28
+ 'Cannot extract page size from SVG. SVG must have width and height attributes. ' +
29
+ 'Provide pageSize option explicitly or ensure SVG contains width/height attributes.'
30
+ );
31
+ }
32
+
33
+ const widthMm = parseFloat(widthMatch[1]);
34
+ const heightMm = parseFloat(heightMatch[1]);
35
+
36
+ if (!Number.isFinite(widthMm) || !Number.isFinite(heightMm) || widthMm <= 0 || heightMm <= 0) {
37
+ throw new Error(
38
+ `Invalid page dimensions extracted from SVG: ${widthMm}mm × ${heightMm}mm. ` +
39
+ 'Provide pageSize option explicitly.'
40
+ );
41
+ }
42
+
43
+ return { widthMm, heightMm };
44
+ }
45
+
46
+ /**
47
+ * Options for exporting a drawing to PDF format.
48
+ */
49
+ export interface ExportPdfOptions {
50
+ /**
51
+ * Output filename (with or without .pdf extension).
52
+ * Default: "drawing.pdf"
53
+ */
54
+ filename?: string;
55
+
56
+ /**
57
+ * Optional page dimensions in millimeters.
58
+ * If not provided, automatically extracted from SVG width/height attributes.
59
+ * Only needed if you want to override the SVG's embedded page size.
60
+ */
61
+ pageSize?: {
62
+ widthMm: number;
63
+ heightMm: number;
64
+ };
65
+
66
+ /**
67
+ * If true, opens PDF in a new browser tab.
68
+ * If false, downloads the PDF file.
69
+ * Default: false (download)
70
+ */
71
+ openInNewTab?: boolean;
72
+ }
73
+
74
+ /**
75
+ * Options for exporting a drawing to SVG format.
76
+ */
77
+ export interface ExportSvgOptions {
78
+ /**
79
+ * Output filename (with or without .svg extension).
80
+ * Default: "drawing.svg"
81
+ */
82
+ filename?: string;
83
+
84
+ /**
85
+ * If true, opens SVG in a new browser tab.
86
+ * If false, downloads the SVG file.
87
+ * Default: false (download)
88
+ */
89
+ openInNewTab?: boolean;
90
+ }
91
+
92
+ /**
93
+ * Exports an SVG drawing to PDF format.
94
+ *
95
+ * Converts the SVG markup to a vector PDF using jsPDF and svg2pdf.js.
96
+ * The PDF maintains full vector quality with no rasterization.
97
+ *
98
+ * **Page size is automatically extracted from the SVG** (width/height attributes),
99
+ * so you don't need to specify it manually. The template's page dimensions are
100
+ * embedded in the SVG markup by renderDrawing() or renderLayout().
101
+ *
102
+ * **Features:**
103
+ * - ✅ Vector PDF output (scalable, searchable text)
104
+ * - ✅ Preserves colors, strokes, fills, fonts
105
+ * - ✅ No quality loss (unlike raster export)
106
+ * - ✅ Small file size (vector graphics)
107
+ * - ✅ Automatic page size detection from SVG
108
+ *
109
+ * **Use cases:**
110
+ * - Print-ready construction documents
111
+ * - Archival drawings
112
+ * - Sharing with clients/contractors
113
+ * - CAD import (vector format)
114
+ *
115
+ * @param svg - SVG markup string (from renderDrawing() or renderLayout())
116
+ * @param options - Export options (filename, optional page size override, open vs download)
117
+ * @returns Promise that resolves when export completes
118
+ *
119
+ * @example
120
+ * ```typescript
121
+ * // Basic usage - page size is automatically extracted from SVG
122
+ * const { svg } = await renderDrawing({ template, models });
123
+ * await exportToPdf(svg, {
124
+ * filename: "floor-plan.pdf",
125
+ * openInNewTab: true
126
+ * });
127
+ *
128
+ * // With custom filename
129
+ * await exportToPdf(svg, {
130
+ * filename: `${template.id}-${new Date().toISOString().slice(0,10)}.pdf`
131
+ * });
132
+ *
133
+ * // Override page size if needed (rare)
134
+ * await exportToPdf(svg, {
135
+ * filename: "custom.pdf",
136
+ * pageSize: { widthMm: 420, heightMm: 297 }, // A3 landscape override
137
+ * openInNewTab: false
138
+ * });
139
+ * ```
140
+ *
141
+ * @throws {Error} If PDF generation fails or SVG page size cannot be extracted
142
+ */
143
+ export async function exportToPdf(svg: string, options: ExportPdfOptions = {}): Promise<void> {
144
+ const filename = options.filename ?? 'drawing.pdf';
145
+ const filenameWithExt = filename.endsWith('.pdf') ? filename : `${filename}.pdf`;
146
+
147
+ // Extract page size from SVG if not provided
148
+ const pageSize = options.pageSize ?? extractPageSizeFromSvg(svg);
149
+
150
+ await exportSectionToPdf({
151
+ svgMarkup: svg,
152
+ fileName: filenameWithExt,
153
+ pageSizeMm: pageSize as any,
154
+ openInNewTab: options.openInNewTab ?? false
155
+ });
156
+ }
157
+
158
+ /**
159
+ * Exports an SVG drawing to SVG file format.
160
+ *
161
+ * Saves or displays the SVG markup as a standalone .svg file.
162
+ * The SVG can be opened in browsers, vector editors (Inkscape, Illustrator),
163
+ * or imported into CAD software.
164
+ *
165
+ * **Features:**
166
+ * - ✅ Native vector format (editable in SVG editors)
167
+ * - ✅ Smallest file size (XML text)
168
+ * - ✅ Web-friendly (can embed in HTML)
169
+ * - ✅ Full styling preserved
170
+ *
171
+ * **Use cases:**
172
+ * - Web display (embed in HTML)
173
+ * - Vector editing (Inkscape, Illustrator)
174
+ * - Further processing (scripting, automation)
175
+ * - Archival (open standard format)
176
+ *
177
+ * @param svg - SVG markup string (from renderDrawing())
178
+ * @param options - Export options (filename, open vs download)
179
+ * @returns Promise that resolves when export completes
180
+ *
181
+ * @example
182
+ * ```typescript
183
+ * // Basic usage
184
+ * const { svg } = await renderDrawing({ template, models });
185
+ * await exportToSvg(svg, {
186
+ * filename: "floor-plan.svg",
187
+ * openInNewTab: true
188
+ * });
189
+ *
190
+ * // Download with custom name
191
+ * await exportToSvg(svg, {
192
+ * filename: `${template.id}-${Date.now()}.svg`,
193
+ * openInNewTab: false
194
+ * });
195
+ * ```
196
+ */
197
+ export function exportToSvg(svg: string, options: ExportSvgOptions = {}): void {
198
+ const filename = options.filename ?? 'drawing.svg';
199
+ const filenameWithExt = filename.endsWith('.svg') ? filename : `${filename}.svg`;
200
+
201
+ exportSectionToSvg({
202
+ svgMarkup: svg,
203
+ fileName: filenameWithExt,
204
+ openInNewTab: options.openInNewTab ?? false
205
+ });
206
+ }