@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.
- package/README.md +208 -0
- package/dist/BuildInfo.d.ts +25 -0
- package/dist/BuildInfo.d.ts.map +1 -0
- package/dist/BuildInfo.js +15 -0
- package/dist/BuildInfo.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/layout/analyzeTemplate.d.ts +29 -0
- package/dist/layout/analyzeTemplate.d.ts.map +1 -0
- package/dist/layout/analyzeTemplate.js +168 -0
- package/dist/layout/analyzeTemplate.js.map +1 -0
- package/dist/layout/clipUtils.d.ts +18 -0
- package/dist/layout/clipUtils.d.ts.map +1 -0
- package/dist/layout/clipUtils.js +165 -0
- package/dist/layout/clipUtils.js.map +1 -0
- package/dist/layout/colorConstants.d.ts +11 -0
- package/dist/layout/colorConstants.d.ts.map +1 -0
- package/dist/layout/colorConstants.js +14 -0
- package/dist/layout/colorConstants.js.map +1 -0
- package/dist/layout/elementKinds.d.ts +21 -0
- package/dist/layout/elementKinds.d.ts.map +1 -0
- package/dist/layout/elementKinds.js +17 -0
- package/dist/layout/elementKinds.js.map +1 -0
- package/dist/layout/exportDrawing.d.ts +143 -0
- package/dist/layout/exportDrawing.d.ts.map +1 -0
- package/dist/layout/exportDrawing.js +146 -0
- package/dist/layout/exportDrawing.js.map +1 -0
- package/dist/layout/filters.d.ts +33 -0
- package/dist/layout/filters.d.ts.map +1 -0
- package/dist/layout/filters.js +77 -0
- package/dist/layout/filters.js.map +1 -0
- package/dist/layout/geometryConstants.d.ts +8 -0
- package/dist/layout/geometryConstants.d.ts.map +1 -0
- package/dist/layout/geometryConstants.js +10 -0
- package/dist/layout/geometryConstants.js.map +1 -0
- package/dist/layout/index.d.ts +86 -0
- package/dist/layout/index.d.ts.map +1 -0
- package/dist/layout/index.js +363 -0
- package/dist/layout/index.js.map +1 -0
- package/dist/layout/itemBuilders.d.ts +29 -0
- package/dist/layout/itemBuilders.d.ts.map +1 -0
- package/dist/layout/itemBuilders.js +135 -0
- package/dist/layout/itemBuilders.js.map +1 -0
- package/dist/layout/labelsRender.d.ts +29 -0
- package/dist/layout/labelsRender.d.ts.map +1 -0
- package/dist/layout/labelsRender.js +109 -0
- package/dist/layout/labelsRender.js.map +1 -0
- package/dist/layout/legendConstants.d.ts +12 -0
- package/dist/layout/legendConstants.d.ts.map +1 -0
- package/dist/layout/legendConstants.js +12 -0
- package/dist/layout/legendConstants.js.map +1 -0
- package/dist/layout/legendRender.d.ts +13 -0
- package/dist/layout/legendRender.d.ts.map +1 -0
- package/dist/layout/legendRender.js +424 -0
- package/dist/layout/legendRender.js.map +1 -0
- package/dist/layout/legendRows.d.ts +26 -0
- package/dist/layout/legendRows.d.ts.map +1 -0
- package/dist/layout/legendRows.js +45 -0
- package/dist/layout/legendRows.js.map +1 -0
- package/dist/layout/legendUtils.d.ts +21 -0
- package/dist/layout/legendUtils.d.ts.map +1 -0
- package/dist/layout/legendUtils.js +110 -0
- package/dist/layout/legendUtils.js.map +1 -0
- package/dist/layout/metadata.d.ts +43 -0
- package/dist/layout/metadata.d.ts.map +1 -0
- package/dist/layout/metadata.js +191 -0
- package/dist/layout/metadata.js.map +1 -0
- package/dist/layout/model.d.ts +15 -0
- package/dist/layout/model.d.ts.map +1 -0
- package/dist/layout/model.js +310 -0
- package/dist/layout/model.js.map +1 -0
- package/dist/layout/options.d.ts +17 -0
- package/dist/layout/options.d.ts.map +1 -0
- package/dist/layout/options.js +2 -0
- package/dist/layout/options.js.map +1 -0
- package/dist/layout/presets.d.ts +23 -0
- package/dist/layout/presets.d.ts.map +1 -0
- package/dist/layout/presets.js +41 -0
- package/dist/layout/presets.js.map +1 -0
- package/dist/layout/qrRender.d.ts +13 -0
- package/dist/layout/qrRender.d.ts.map +1 -0
- package/dist/layout/qrRender.js +97 -0
- package/dist/layout/qrRender.js.map +1 -0
- package/dist/layout/renderDrawing.d.ts +62 -0
- package/dist/layout/renderDrawing.d.ts.map +1 -0
- package/dist/layout/renderDrawing.js +555 -0
- package/dist/layout/renderDrawing.js.map +1 -0
- package/dist/layout/renderUtils.d.ts +36 -0
- package/dist/layout/renderUtils.d.ts.map +1 -0
- package/dist/layout/renderUtils.js +117 -0
- package/dist/layout/renderUtils.js.map +1 -0
- package/dist/layout/sectionValidation.d.ts +3 -0
- package/dist/layout/sectionValidation.d.ts.map +1 -0
- package/dist/layout/sectionValidation.js +2 -0
- package/dist/layout/sectionValidation.js.map +1 -0
- package/dist/layout/templateVariables.d.ts +137 -0
- package/dist/layout/templateVariables.d.ts.map +1 -0
- package/dist/layout/templateVariables.js +295 -0
- package/dist/layout/templateVariables.js.map +1 -0
- package/dist/layout/typeGuards.d.ts +5 -0
- package/dist/layout/typeGuards.d.ts.map +1 -0
- package/dist/layout/typeGuards.js +9 -0
- package/dist/layout/typeGuards.js.map +1 -0
- package/dist/layout/types.d.ts +816 -0
- package/dist/layout/types.d.ts.map +1 -0
- package/dist/layout/types.js +2 -0
- package/dist/layout/types.js.map +1 -0
- package/dist/layout/utils.d.ts +9 -0
- package/dist/layout/utils.d.ts.map +1 -0
- package/dist/layout/utils.js +84 -0
- package/dist/layout/utils.js.map +1 -0
- package/dist/layout/validation.d.ts +7 -0
- package/dist/layout/validation.d.ts.map +1 -0
- package/dist/layout/validation.js +352 -0
- package/dist/layout/validation.js.map +1 -0
- package/dist/layout/validationHelpers.d.ts +15 -0
- package/dist/layout/validationHelpers.d.ts.map +1 -0
- package/dist/layout/validationHelpers.js +43 -0
- package/dist/layout/validationHelpers.js.map +1 -0
- package/dist/layout/validationUtils.d.ts +5 -0
- package/dist/layout/validationUtils.d.ts.map +1 -0
- package/dist/layout/validationUtils.js +5 -0
- package/dist/layout/validationUtils.js.map +1 -0
- package/dist/layout/viewportPrep.d.ts +47 -0
- package/dist/layout/viewportPrep.d.ts.map +1 -0
- package/dist/layout/viewportPrep.js +275 -0
- package/dist/layout/viewportPrep.js.map +1 -0
- package/dist/layout/viewportRender.d.ts +42 -0
- package/dist/layout/viewportRender.d.ts.map +1 -0
- package/dist/layout/viewportRender.js +337 -0
- package/dist/layout/viewportRender.js.map +1 -0
- package/dist/layout/viewportUtils.d.ts +9 -0
- package/dist/layout/viewportUtils.d.ts.map +1 -0
- package/dist/layout/viewportUtils.js +40 -0
- package/dist/layout/viewportUtils.js.map +1 -0
- package/dist/layout/warnings.d.ts +2 -0
- package/dist/layout/warnings.d.ts.map +1 -0
- package/dist/layout/warnings.js +4 -0
- package/dist/layout/warnings.js.map +1 -0
- package/dist/printToPdf.d.ts +38 -0
- package/dist/printToPdf.d.ts.map +1 -0
- package/dist/printToPdf.js +79 -0
- package/dist/printToPdf.js.map +1 -0
- package/dist/printToSvg.d.ts +11 -0
- package/dist/printToSvg.d.ts.map +1 -0
- package/dist/printToSvg.js +168 -0
- package/dist/printToSvg.js.map +1 -0
- package/dist/sections.d.ts +118 -0
- package/dist/sections.d.ts.map +1 -0
- package/dist/sections.js +839 -0
- package/dist/sections.js.map +1 -0
- package/dist/templates/default-template.json +137 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +11 -0
- package/dist/types.js.map +1 -0
- package/package.json +47 -0
- package/src/BuildInfo.ts +41 -0
- package/src/index.ts +10 -0
- package/src/layout/analyzeTemplate.ts +218 -0
- package/src/layout/clipUtils.ts +193 -0
- package/src/layout/colorConstants.ts +16 -0
- package/src/layout/elementKinds.ts +35 -0
- package/src/layout/exportDrawing.ts +206 -0
- package/src/layout/filters.ts +80 -0
- package/src/layout/geometryConstants.ts +11 -0
- package/src/layout/index.ts +671 -0
- package/src/layout/itemBuilders.ts +159 -0
- package/src/layout/labelsRender.ts +170 -0
- package/src/layout/legendConstants.ts +11 -0
- package/src/layout/legendRender.ts +543 -0
- package/src/layout/legendRows.ts +62 -0
- package/src/layout/legendUtils.ts +144 -0
- package/src/layout/metadata.ts +210 -0
- package/src/layout/model.ts +372 -0
- package/src/layout/options.ts +17 -0
- package/src/layout/presets.ts +51 -0
- package/src/layout/qrRender.ts +126 -0
- package/src/layout/qrcode.d.ts +2 -0
- package/src/layout/renderDrawing.ts +710 -0
- package/src/layout/renderUtils.ts +138 -0
- package/src/layout/sectionValidation.ts +2 -0
- package/src/layout/templateVariables.ts +317 -0
- package/src/layout/typeGuards.ts +9 -0
- package/src/layout/types.ts +835 -0
- package/src/layout/utils.ts +85 -0
- package/src/layout/validation.ts +392 -0
- package/src/layout/validationHelpers.ts +59 -0
- package/src/layout/validationUtils.ts +6 -0
- package/src/layout/viewportPrep.ts +351 -0
- package/src/layout/viewportRender.ts +442 -0
- package/src/layout/viewportUtils.ts +50 -0
- package/src/layout/warnings.ts +3 -0
- package/src/printToPdf.ts +108 -0
- package/src/printToSvg.ts +188 -0
- package/src/sections.ts +1019 -0
- package/src/templates/default-template.json +137 -0
- package/src/types.ts +49 -0
- package/templates/default-a0-landscape.json +209 -0
- package/templates/default-a0-portrait.json +195 -0
- package/templates/default-a1-landscape.json +167 -0
- package/templates/default-a1-portrait.json +153 -0
- package/templates/default-a3-landscape.json +111 -0
- package/templates/default-a3-portrait.json +125 -0
- package/templates/default-a4-landscape.json +97 -0
- package/templates/default-a4-portrait.json +97 -0
- 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
|
+
};
|