@twinfinity/printing 6.1.1-beta → 6.1.2-ci.30738-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/dist/BuildInfo.js +3 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/layout/analyzeTemplate.d.ts +4 -4
- package/dist/layout/analyzeTemplate.d.ts.map +1 -1
- package/dist/layout/analyzeTemplate.js +18 -16
- package/dist/layout/analyzeTemplate.js.map +1 -1
- package/dist/layout/classClosure.d.ts +41 -0
- package/dist/layout/classClosure.d.ts.map +1 -0
- package/dist/layout/classClosure.js +143 -0
- package/dist/layout/classClosure.js.map +1 -0
- package/dist/layout/clipUtils.d.ts +11 -0
- package/dist/layout/clipUtils.d.ts.map +1 -1
- package/dist/layout/clipUtils.js +67 -6
- package/dist/layout/clipUtils.js.map +1 -1
- package/dist/layout/filters.d.ts +14 -8
- package/dist/layout/filters.d.ts.map +1 -1
- package/dist/layout/filters.js +28 -3
- package/dist/layout/filters.js.map +1 -1
- package/dist/layout/index.d.ts +24 -57
- package/dist/layout/index.d.ts.map +1 -1
- package/dist/layout/index.js +20 -337
- package/dist/layout/index.js.map +1 -1
- package/dist/layout/itemBuilders.d.ts +11 -0
- package/dist/layout/itemBuilders.d.ts.map +1 -1
- package/dist/layout/itemBuilders.js +17 -3
- package/dist/layout/itemBuilders.js.map +1 -1
- package/dist/layout/labelsRender.d.ts.map +1 -1
- package/dist/layout/labelsRender.js +15 -27
- package/dist/layout/labelsRender.js.map +1 -1
- package/dist/layout/legendRender.d.ts +18 -6
- package/dist/layout/legendRender.d.ts.map +1 -1
- package/dist/layout/legendRender.js +49 -79
- package/dist/layout/legendRender.js.map +1 -1
- package/dist/layout/metadata.d.ts.map +1 -1
- package/dist/layout/metadata.js +26 -5
- package/dist/layout/metadata.js.map +1 -1
- package/dist/layout/model.d.ts +4 -3
- package/dist/layout/model.d.ts.map +1 -1
- package/dist/layout/model.js +17 -57
- package/dist/layout/model.js.map +1 -1
- package/dist/layout/options.d.ts +1 -2
- package/dist/layout/options.d.ts.map +1 -1
- package/dist/layout/options.js.map +1 -1
- package/dist/layout/preflight.d.ts +32 -0
- package/dist/layout/preflight.d.ts.map +1 -0
- package/dist/layout/preflight.js +69 -0
- package/dist/layout/preflight.js.map +1 -0
- package/dist/layout/qrRender.d.ts.map +1 -1
- package/dist/layout/qrRender.js +11 -10
- package/dist/layout/qrRender.js.map +1 -1
- package/dist/layout/renderDrawing.d.ts +28 -17
- package/dist/layout/renderDrawing.d.ts.map +1 -1
- package/dist/layout/renderDrawing.js +107 -88
- package/dist/layout/renderDrawing.js.map +1 -1
- package/dist/layout/renderLayout.d.ts +25 -0
- package/dist/layout/renderLayout.d.ts.map +1 -0
- package/dist/layout/renderLayout.js +315 -0
- package/dist/layout/renderLayout.js.map +1 -0
- package/dist/layout/renderTemplateSheet.d.ts +99 -0
- package/dist/layout/renderTemplateSheet.d.ts.map +1 -0
- package/dist/layout/renderTemplateSheet.js +296 -0
- package/dist/layout/renderTemplateSheet.js.map +1 -0
- package/dist/layout/renderUtils.d.ts +19 -0
- package/dist/layout/renderUtils.d.ts.map +1 -1
- package/dist/layout/renderUtils.js +47 -4
- package/dist/layout/renderUtils.js.map +1 -1
- package/dist/layout/sectionDefs.d.ts +65 -0
- package/dist/layout/sectionDefs.d.ts.map +1 -0
- package/dist/layout/sectionDefs.js +88 -0
- package/dist/layout/sectionDefs.js.map +1 -0
- package/dist/layout/slabModelView.d.ts +34 -0
- package/dist/layout/slabModelView.d.ts.map +1 -0
- package/dist/layout/slabModelView.js +175 -0
- package/dist/layout/slabModelView.js.map +1 -0
- package/dist/layout/templateVariables.d.ts +1 -1
- package/dist/layout/templateVariables.d.ts.map +1 -1
- package/dist/layout/templateVariables.js +21 -9
- package/dist/layout/templateVariables.js.map +1 -1
- package/dist/layout/types.d.ts +164 -83
- package/dist/layout/types.d.ts.map +1 -1
- package/dist/layout/types.js.map +1 -1
- package/dist/layout/validation.d.ts.map +1 -1
- package/dist/layout/validation.js +78 -33
- package/dist/layout/validation.js.map +1 -1
- package/dist/layout/viewportPrep.d.ts +8 -6
- package/dist/layout/viewportPrep.d.ts.map +1 -1
- package/dist/layout/viewportPrep.js +35 -23
- package/dist/layout/viewportPrep.js.map +1 -1
- package/dist/layout/viewportRender.d.ts +9 -7
- package/dist/layout/viewportRender.d.ts.map +1 -1
- package/dist/layout/viewportRender.js +93 -126
- package/dist/layout/viewportRender.js.map +1 -1
- package/dist/layout/viewportUtils.d.ts +20 -0
- package/dist/layout/viewportUtils.d.ts.map +1 -1
- package/dist/layout/viewportUtils.js +107 -4
- package/dist/layout/viewportUtils.js.map +1 -1
- package/dist/printToPdf.d.ts +7 -1
- package/dist/printToPdf.d.ts.map +1 -1
- package/dist/printToPdf.js +81 -14
- package/dist/printToPdf.js.map +1 -1
- package/dist/printToSvg.d.ts.map +1 -1
- package/dist/printToSvg.js +7 -3
- package/dist/printToSvg.js.map +1 -1
- package/dist/schemaUtils.d.ts +9 -0
- package/dist/schemaUtils.d.ts.map +1 -0
- package/dist/schemaUtils.js +14 -0
- package/dist/schemaUtils.js.map +1 -0
- package/dist/templates/default-template.json +30 -8
- package/dist/types.d.ts +2 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +10 -1
- package/dist/types.js.map +1 -1
- package/package.json +8 -3
- package/src/index.ts +4 -0
- package/src/layout/analyzeTemplate.ts +21 -19
- package/src/layout/classClosure.ts +142 -0
- package/src/layout/clipUtils.ts +65 -6
- package/src/layout/filters.ts +31 -3
- package/src/layout/index.ts +61 -571
- package/src/layout/itemBuilders.ts +20 -5
- package/src/layout/labelsRender.ts +23 -40
- package/src/layout/legendRender.ts +64 -84
- package/src/layout/metadata.ts +26 -5
- package/src/layout/model.ts +21 -66
- package/src/layout/options.ts +1 -2
- package/src/layout/preflight.ts +96 -0
- package/src/layout/qrRender.ts +11 -10
- package/src/layout/renderDrawing.ts +132 -110
- package/src/layout/renderLayout.ts +483 -0
- package/src/layout/renderTemplateSheet.ts +435 -0
- package/src/layout/renderUtils.ts +53 -4
- package/src/layout/sectionDefs.ts +131 -0
- package/src/layout/slabModelView.ts +196 -0
- package/src/layout/templateVariables.ts +25 -9
- package/src/layout/types.ts +160 -70
- package/src/layout/validation.ts +98 -32
- package/src/layout/viewportPrep.ts +47 -29
- package/src/layout/viewportRender.ts +114 -141
- package/src/layout/viewportUtils.ts +118 -5
- package/src/printToPdf.ts +84 -15
- package/src/printToSvg.ts +7 -3
- package/src/schemaUtils.ts +13 -0
- package/src/templates/default-template.json +30 -8
- package/src/types.ts +12 -2
- package/templates/default-a0-landscape.json +171 -66
- package/templates/default-a0-portrait.json +159 -60
- package/templates/default-a1-landscape.json +135 -48
- package/templates/default-a1-portrait.json +123 -42
- package/templates/default-a3-landscape.json +87 -24
- package/templates/default-a3-portrait.json +99 -30
- package/templates/default-a4-landscape.json +75 -18
- package/templates/default-a4-portrait.json +75 -18
- package/templates/llm-authoring-guide.md +247 -0
- package/templates/template-schema.json +295 -120
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { buildPlaneBasis, planeFromPointAndNormal } from '@twinfinity/geometry';
|
|
2
|
+
import { computePlaneSectionsForProducts, SlabSection } from '@twinfinity/product-geometry';
|
|
3
|
+
import { buildSectionStateForEntries } from '../sections';
|
|
4
|
+
import { renderDrawing } from './renderDrawing';
|
|
5
|
+
import { sectionStateToModelView } from './itemBuilders';
|
|
6
|
+
import { slabSectionToModelView, slabPerProductToEntries } from './slabModelView';
|
|
7
|
+
import { resolveViewportSection, sectionDefKey } from './sectionDefs';
|
|
8
|
+
import { computeViewportClassClosure, filterProductsByClosure, collectClassBehavior } from './classClosure';
|
|
9
|
+
import { buildMetadataMap } from './metadata';
|
|
10
|
+
/**
|
|
11
|
+
* Plain world AABB from a viewer product's duck-typed `boundingInfo()` (BimIfcObject
|
|
12
|
+
* exposes Babylon BoundingInfo; plain ProductLike adapters usually have none → null).
|
|
13
|
+
*/
|
|
14
|
+
function productAabb(product) {
|
|
15
|
+
const withBi = product;
|
|
16
|
+
if (typeof withBi.boundingInfo !== 'function')
|
|
17
|
+
return null;
|
|
18
|
+
const bi = withBi.boundingInfo();
|
|
19
|
+
if (!bi)
|
|
20
|
+
return null;
|
|
21
|
+
const bb = bi.boundingBox;
|
|
22
|
+
const bMin = (bb?.minimumWorld ?? bi.minimum);
|
|
23
|
+
const bMax = (bb?.maximumWorld ?? bi.maximum);
|
|
24
|
+
if (!bMin || !bMax)
|
|
25
|
+
return null;
|
|
26
|
+
return {
|
|
27
|
+
min: [bMin[0] ?? bMin.x ?? 0, bMin[1] ?? bMin.y ?? 0, bMin[2] ?? bMin.z ?? 0],
|
|
28
|
+
max: [bMax[0] ?? bMax.x ?? 0, bMax[1] ?? bMax.y ?? 0, bMax[2] ?? bMax.z ?? 0]
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Section-geometry cache, one entry per distinct section definition. Owned by
|
|
33
|
+
* the caller (its lifetime = one loaded model; clear it when the model
|
|
34
|
+
* changes); renderTemplateSheet evicts entries no viewport references any
|
|
35
|
+
* more, keeping it bounded while sharing repeated definitions.
|
|
36
|
+
*/
|
|
37
|
+
export class SectionCache {
|
|
38
|
+
constructor() {
|
|
39
|
+
this._map = new Map();
|
|
40
|
+
}
|
|
41
|
+
get(key) {
|
|
42
|
+
return this._map.get(key);
|
|
43
|
+
}
|
|
44
|
+
set(key, value) {
|
|
45
|
+
this._map.set(key, value);
|
|
46
|
+
}
|
|
47
|
+
evictExcept(used) {
|
|
48
|
+
for (const key of this._map.keys()) {
|
|
49
|
+
if (!used.has(key))
|
|
50
|
+
this._map.delete(key);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
clear() {
|
|
54
|
+
this._map.clear();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Read a viewport's `stairArrows` geometry options into a canonical object:
|
|
59
|
+
* known fields only, fixed order, invalid values dropped — so equal options
|
|
60
|
+
* always serialize to the same cache-key fragment and unknown junk from a
|
|
61
|
+
* hand-written template can't reach the geometry pass.
|
|
62
|
+
*/
|
|
63
|
+
export function readViewportStairArrowOptions(vp) {
|
|
64
|
+
const raw = vp.stairArrows;
|
|
65
|
+
if (!raw || typeof raw !== 'object')
|
|
66
|
+
return undefined;
|
|
67
|
+
const r = raw;
|
|
68
|
+
const out = {};
|
|
69
|
+
if (r.startMarker === 'filled' || r.startMarker === 'open' || r.startMarker === 'none') {
|
|
70
|
+
out.startMarker = r.startMarker;
|
|
71
|
+
}
|
|
72
|
+
if (r.arrowhead === 'filled' || r.arrowhead === 'outline' || r.arrowhead === 'open') {
|
|
73
|
+
out.arrowhead = r.arrowhead;
|
|
74
|
+
}
|
|
75
|
+
for (const k of ['lengthScale', 'minLength', 'maxLength', 'landingExtension']) {
|
|
76
|
+
const v = r[k];
|
|
77
|
+
if (typeof v === 'number' && Number.isFinite(v) && v >= 0)
|
|
78
|
+
out[k] = v;
|
|
79
|
+
}
|
|
80
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
81
|
+
}
|
|
82
|
+
async function computeSectionForDef(products, def, onPhase, classBehavior, stairArrows) {
|
|
83
|
+
if (!def.plan) {
|
|
84
|
+
// Elevations/sections: a thin plane section projected by the section-
|
|
85
|
+
// state builder — its planeNormal projection is the canonical
|
|
86
|
+
// orientation (a hand-rolled basis renders elevations mirrored).
|
|
87
|
+
onPhase?.(0, 'Computing plane sections');
|
|
88
|
+
const n = def.normal;
|
|
89
|
+
// AABB gate: a product whose bounds do not straddle the section plane
|
|
90
|
+
// cannot contribute plane sections — skip it before any mesh access.
|
|
91
|
+
const straddling = products.filter((p) => {
|
|
92
|
+
const bi = productAabb(p);
|
|
93
|
+
if (!bi)
|
|
94
|
+
return true;
|
|
95
|
+
const c0 = (bi.min[0] + bi.max[0]) * 0.5 * n[0];
|
|
96
|
+
const c1 = (bi.min[1] + bi.max[1]) * 0.5 * n[1];
|
|
97
|
+
const c2 = (bi.min[2] + bi.max[2]) * 0.5 * n[2];
|
|
98
|
+
const e = ((bi.max[0] - bi.min[0]) / 2) * Math.abs(n[0]) +
|
|
99
|
+
((bi.max[1] - bi.min[1]) / 2) * Math.abs(n[1]) +
|
|
100
|
+
((bi.max[2] - bi.min[2]) / 2) * Math.abs(n[2]);
|
|
101
|
+
const center = c0 + c1 + c2;
|
|
102
|
+
return center - e <= def.position && center + e >= def.position;
|
|
103
|
+
});
|
|
104
|
+
const origin = [n[0] * def.position, n[1] * def.position, n[2] * def.position];
|
|
105
|
+
const plane = planeFromPointAndNormal(origin, n);
|
|
106
|
+
const section = await computePlaneSectionsForProducts(straddling, plane);
|
|
107
|
+
onPhase?.(100, 'Plane sections complete');
|
|
108
|
+
return {
|
|
109
|
+
kind: 'elevation',
|
|
110
|
+
perProduct: section.perProduct,
|
|
111
|
+
normal: n,
|
|
112
|
+
segmentCount: section.segments.length,
|
|
113
|
+
entries: []
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// Plans go through SlabSection (cut/visible/hidden lines with occlusion).
|
|
117
|
+
const viewDir = [0, -1, 0];
|
|
118
|
+
const above = Math.max(0, def.depth.above);
|
|
119
|
+
const below = Math.max(0, def.depth.below);
|
|
120
|
+
const slab = new SlabSection({
|
|
121
|
+
cutHeight: def.position,
|
|
122
|
+
viewDir,
|
|
123
|
+
cutOrigin: [0, def.position, 0],
|
|
124
|
+
depth: { above, below },
|
|
125
|
+
// stairArrows: generated only for stair-category products (className fallback in
|
|
126
|
+
// the gather), so enabling it unconditionally costs nothing on stairless slabs —
|
|
127
|
+
// and the stairArrows lineStyles category has been in the template schema all
|
|
128
|
+
// along, styling output this pipeline never produced.
|
|
129
|
+
lines: { cut: true, visible: true, hidden: true, stairArrows: true },
|
|
130
|
+
// Template-driven marker shapes / run sizing; SlabSection merges the
|
|
131
|
+
// partial object with its own defaults.
|
|
132
|
+
...(stairArrows ? { stairArrows } : {}),
|
|
133
|
+
// Per-product attribution feeds labels/legend items.
|
|
134
|
+
trackProducts: true,
|
|
135
|
+
onProgress: onPhase,
|
|
136
|
+
// Forward-compatible pass-through: the published product-geometry ignores unknown
|
|
137
|
+
// options; the classBehavior contract activates with TWINS-3784's SlabSection.
|
|
138
|
+
...(classBehavior?.length ? { classBehavior } : {})
|
|
139
|
+
});
|
|
140
|
+
const geometry = await slab.compute(products);
|
|
141
|
+
const segmentCount = geometry.cutLines.length + geometry.visibleLines.length + geometry.hiddenLines.length;
|
|
142
|
+
const entries = slabPerProductToEntries(geometry.perProduct ?? []);
|
|
143
|
+
const basis = buildPlaneBasis([0, def.position + above, 0], viewDir, [0, 0, 1]);
|
|
144
|
+
return { kind: 'plan', geometry, basis, segmentCount, entries };
|
|
145
|
+
}
|
|
146
|
+
/** Build a viewport's model view from cached geometry + its own lineStyles. */
|
|
147
|
+
function buildViewForViewport(computed, vp) {
|
|
148
|
+
const lineStyles = vp.lineStyles;
|
|
149
|
+
if (computed.kind === 'plan') {
|
|
150
|
+
return slabSectionToModelView(computed.geometry, computed.basis, lineStyles);
|
|
151
|
+
}
|
|
152
|
+
const state = buildSectionStateForEntries(computed.perProduct, {
|
|
153
|
+
strokeWidth: lineStyles?.default?.widthMm ?? 0.25,
|
|
154
|
+
includeMeta: true,
|
|
155
|
+
includeLabels: false,
|
|
156
|
+
fills: vp.fills,
|
|
157
|
+
planeNormal: computed.normal
|
|
158
|
+
});
|
|
159
|
+
return state ? sectionStateToModelView(state) : null;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Render a template against a product set: resolve each viewport's section
|
|
163
|
+
* definition, compute (or reuse from `cache`) the section per distinct
|
|
164
|
+
* definition, and feed renderDrawing one model per viewport id.
|
|
165
|
+
*/
|
|
166
|
+
export async function renderTemplateSheet(input) {
|
|
167
|
+
const warnings = [];
|
|
168
|
+
const cache = input.cache ?? new SectionCache();
|
|
169
|
+
// Resolve template variables BEFORE section resolution — variables can
|
|
170
|
+
// drive section placement ("cutHeight": "{{vars.cutHeight}}", typed
|
|
171
|
+
// whole-value substitution), and the geometry cache must key on the
|
|
172
|
+
// resolved values. The resolved template carries no {{vars.*}} left, so
|
|
173
|
+
// renderDrawing gets no variables input.
|
|
174
|
+
let template = input.template;
|
|
175
|
+
if (template.variables) {
|
|
176
|
+
const { resolveTemplateValues, applyTemplateVariables } = await import('./templateVariables');
|
|
177
|
+
const { values, warnings: varWarnings } = resolveTemplateValues(template, input.variables);
|
|
178
|
+
varWarnings.forEach((w) => warnings.push(`[Variables] ${w}`));
|
|
179
|
+
template = applyTemplateVariables(template, values, warnings);
|
|
180
|
+
}
|
|
181
|
+
const viewports = (template.elements ?? []).filter((e) => e.kind === 'viewport' && typeof e.id === 'string');
|
|
182
|
+
if (viewports.length === 0) {
|
|
183
|
+
warnings.push('Template has no viewport elements — nothing to section.');
|
|
184
|
+
}
|
|
185
|
+
// Per-viewport compute-class closure (classClosure.ts): only the viewport's include
|
|
186
|
+
// classes take part in its drawing generation, hidden lines included — geometry
|
|
187
|
+
// outside the closure neither draws nor OCCLUDES there, and pays no compute cost
|
|
188
|
+
// (the dominant lever on MEP-heavy models). The closure contains the viewport's
|
|
189
|
+
// highlight/clip/fill/label/legend classes too, so the "sections computed unfiltered"
|
|
190
|
+
// invariant above still holds WITHIN it. The closure is part of the section cache
|
|
191
|
+
// key: viewports sharing (sectionDef, closure) share geometry, and editing
|
|
192
|
+
// includeClasses re-sections.
|
|
193
|
+
const productsByClosureKey = new Map();
|
|
194
|
+
const productsForClosure = (closure) => {
|
|
195
|
+
const closureKey = closure ? [...closure].sort().join(',') : 'all';
|
|
196
|
+
let products = productsByClosureKey.get(closureKey);
|
|
197
|
+
if (!products) {
|
|
198
|
+
products = closure ? filterProductsByClosure(input.products, closure) : input.products;
|
|
199
|
+
productsByClosureKey.set(closureKey, products);
|
|
200
|
+
}
|
|
201
|
+
return { products, closureKey };
|
|
202
|
+
};
|
|
203
|
+
const models = {};
|
|
204
|
+
const usedKeys = new Set();
|
|
205
|
+
let segmentCount = 0;
|
|
206
|
+
// Labels/legend items are placed in world x/z (floor-plane convention) and must see ALL products
|
|
207
|
+
// regardless of viewport class filters — so they come from each viewport's unfiltered section.
|
|
208
|
+
//
|
|
209
|
+
// Keyed by viewport id rather than collapsed into one list: a legend declares which viewport it
|
|
210
|
+
// describes (`legend.viewport`, required) and renderLayout reads that viewport's items, so a
|
|
211
|
+
// sheet may carry several legends describing several plans. Passing one flat list handed every
|
|
212
|
+
// legend the first plan's contents no matter what it was bound to.
|
|
213
|
+
// No fallback to another viewport's entries: a legend describing a viewport that yielded nothing
|
|
214
|
+
// renders empty and renderLayout warns by name ("Legend 'x' has no items from viewport 'y'"),
|
|
215
|
+
// which is more use than quietly showing another viewport's contents under its heading.
|
|
216
|
+
const entriesByViewport = {};
|
|
217
|
+
// gid → class/property metadata for EVERY product in every section. The entries
|
|
218
|
+
// arrays only cover cut-bearing products, but render-time includeClasses filtering
|
|
219
|
+
// must classify products whose geometry never crosses the cut (furniture below a
|
|
220
|
+
// plan cut) — without this map their visible/hidden lines vanish from the drawing.
|
|
221
|
+
const fullMetadata = {};
|
|
222
|
+
for (const vp of viewports) {
|
|
223
|
+
let def = resolveViewportSection(vp, input.defaults);
|
|
224
|
+
if (!def) {
|
|
225
|
+
if (!input.modelCenter) {
|
|
226
|
+
warnings.push(`Viewport '${vp.id}' has no section definition and no modelCenter fallback — skipped.`);
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
def = {
|
|
230
|
+
plan: true,
|
|
231
|
+
normal: [0, 1, 0],
|
|
232
|
+
position: input.modelCenter[1],
|
|
233
|
+
depth: { above: 0.5, below: 1.5 }
|
|
234
|
+
};
|
|
235
|
+
warnings.push(`Viewport '${vp.id}' has no section definition — using a default plan at ${def.position.toFixed(1)} m.`);
|
|
236
|
+
}
|
|
237
|
+
const { products, closureKey } = productsForClosure(computeViewportClassClosure(vp, template));
|
|
238
|
+
const classBehavior = collectClassBehavior(vp, template);
|
|
239
|
+
// Behavior is part of the cache key: same def+closure with different behavior
|
|
240
|
+
// flags must not share geometry.
|
|
241
|
+
const behaviorKey = classBehavior.length ? JSON.stringify(classBehavior) : '';
|
|
242
|
+
// Stair-arrow geometry options are part of the cache key too: the arrows are
|
|
243
|
+
// baked into the section geometry, so viewports with different marker styles
|
|
244
|
+
// must not share it. readViewportStairArrowOptions canonicalizes field order.
|
|
245
|
+
const stairArrows = readViewportStairArrowOptions(vp);
|
|
246
|
+
const stairKey = stairArrows ? JSON.stringify(stairArrows) : '';
|
|
247
|
+
const key = `${sectionDefKey(def)}|${closureKey}|${behaviorKey}|${stairKey}`;
|
|
248
|
+
usedKeys.add(key);
|
|
249
|
+
let computed = cache.get(key);
|
|
250
|
+
if (!computed) {
|
|
251
|
+
// Section compute dominates total time; reserve the last 10% for the
|
|
252
|
+
// render step below. Cached viewports jump ahead without reporting.
|
|
253
|
+
const vpIndex = viewports.indexOf(vp);
|
|
254
|
+
computed = await computeSectionForDef(products, def, (pct, phase) => input.onProgress?.(((vpIndex + pct / 100) / viewports.length) * 0.9, `Viewport '${vp.id}': ${phase}`), classBehavior, stairArrows);
|
|
255
|
+
cache.set(key, computed);
|
|
256
|
+
}
|
|
257
|
+
segmentCount += computed.segmentCount;
|
|
258
|
+
if (computed.kind === 'plan') {
|
|
259
|
+
Object.assign(fullMetadata, buildMetadataMap((computed.geometry.perProduct ?? [])));
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
Object.assign(fullMetadata, buildMetadataMap(computed.perProduct));
|
|
263
|
+
}
|
|
264
|
+
if (computed.entries?.length)
|
|
265
|
+
entriesByViewport[vp.id] = computed.entries;
|
|
266
|
+
const view = buildViewForViewport(computed, vp);
|
|
267
|
+
if (!view) {
|
|
268
|
+
warnings.push(`Viewport '${vp.id}': section produced no drawable geometry at this cut.`);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
models[vp.id] = view;
|
|
272
|
+
}
|
|
273
|
+
cache.evictExcept(usedKeys);
|
|
274
|
+
if (Object.keys(models).length === 0) {
|
|
275
|
+
return { svg: '', warnings, segmentCount };
|
|
276
|
+
}
|
|
277
|
+
input.onProgress?.(0.9, 'Rendering sheet');
|
|
278
|
+
const { svg, warnings: renderWarnings } = await renderDrawing({
|
|
279
|
+
// Already variable-resolved above — pass no variables down.
|
|
280
|
+
template,
|
|
281
|
+
models,
|
|
282
|
+
options: {
|
|
283
|
+
allowRemoteResources: template.allowRemoteResources ?? input.allowRemoteResources,
|
|
284
|
+
qrContext: input.qrContext,
|
|
285
|
+
metadataById: fullMetadata,
|
|
286
|
+
// Feeds labels/legend items, keyed by viewport so each legend describes the viewport it
|
|
287
|
+
// declares — a sheet may carry several legends over several plans. This used to be one
|
|
288
|
+
// flat list taken from the first plan viewport, which handed every legend that plan's
|
|
289
|
+
// contents no matter what it was bound to.
|
|
290
|
+
productEntries: Object.keys(entriesByViewport).length ? entriesByViewport : undefined
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
input.onProgress?.(1, 'Complete');
|
|
294
|
+
return { svg, warnings: [...warnings, ...renderWarnings], segmentCount };
|
|
295
|
+
}
|
|
296
|
+
//# sourceMappingURL=renderTemplateSheet.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"renderTemplateSheet.js","sourceRoot":"/","sources":["layout/renderTemplateSheet.ts"],"names":[],"mappings":"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","sourcesContent":["import { buildPlaneBasis, planeFromPointAndNormal, type PlaneBasis, type Vec3 } from '@twinfinity/geometry';\nimport {\n computePlaneSectionsForProducts,\n SlabSection,\n type ProductLike,\n type SlabSectionResult\n} from '@twinfinity/product-geometry';\nimport { buildSectionStateForEntries } from '../sections';\nimport { renderDrawing } from './renderDrawing';\nimport { sectionStateToModelView } from './itemBuilders';\nimport { slabSectionToModelView, slabPerProductToEntries } from './slabModelView';\nimport { resolveViewportSection, sectionDefKey, type ResolvedSectionDef, type SectionDefaults } from './sectionDefs';\nimport {\n computeViewportClassClosure,\n filterProductsByClosure,\n collectClassBehavior,\n type ClassBehaviorRule\n} from './classClosure';\nimport { buildMetadataMap } from './metadata';\n\n/**\n * Plain world AABB from a viewer product's duck-typed `boundingInfo()` (BimIfcObject\n * exposes Babylon BoundingInfo; plain ProductLike adapters usually have none → null).\n */\nfunction productAabb(product: ProductLike): { min: [number, number, number]; max: [number, number, number] } | null {\n const withBi = product as ProductLike & { boundingInfo?: () => Record<string, unknown> };\n if (typeof withBi.boundingInfo !== 'function') return null;\n const bi = withBi.boundingInfo();\n if (!bi) return null;\n const bb = bi.boundingBox as Record<string, unknown> | undefined;\n const bMin = (bb?.minimumWorld ?? bi.minimum) as (number[] & Record<string, number>) | undefined;\n const bMax = (bb?.maximumWorld ?? bi.maximum) as (number[] & Record<string, number>) | undefined;\n if (!bMin || !bMax) return null;\n return {\n min: [bMin[0] ?? bMin.x ?? 0, bMin[1] ?? bMin.y ?? 0, bMin[2] ?? bMin.z ?? 0],\n max: [bMax[0] ?? bMax.x ?? 0, bMax[1] ?? bMax.y ?? 0, bMax[2] ?? bMax.z ?? 0]\n };\n}\nimport type {\n LayoutTemplate,\n SvgModelView,\n ViewportLineStyles,\n ViewportStairArrowOptions,\n ProductPolylineEntry\n} from './types';\n\n// Products + template → rendered sheet, in one call. This is the consumer\n// contract for the template's self-describing sections: every viewport's\n// cutHeight/planeNormal/planePosition/depth is resolved (sectionDefs),\n// computed (SlabSection for plans, plane sections for elevations/sections),\n// adapted to SvgModelViews and fed to renderDrawing — so the workbench, the\n// viewer print dialog and any future consumer render a template identically\n// instead of re-implementing this pipeline.\n//\n// Invariants encoded here (learned the hard way — do not \"optimize\" away):\n// - Sections are computed UNFILTERED: the renderer applies each viewport's\n// includeClasses at render time via rawPaths + metadata, and highlight/clip\n// need the highlight-class geometry present even when includeClasses\n// excludes it from drawing (e.g. highlighting a space in a walls-only view).\n// - The plan's lateral section origin stays at world zero so the projected\n// space equals world x/z — labels/legend items are placed in world x/z, and\n// a shifted origin would displace them.\n// - The plan projection plane sits `above` metres over the cut (matches\n// ExportDrawingPdf/printToSvg), with a (0,0,1) up hint for north-up plans.\n\ntype SectionEntries = Parameters<typeof buildSectionStateForEntries>[0];\ntype SectionStateOptions = Parameters<typeof buildSectionStateForEntries>[1];\n\n/**\n * The expensive template-independent half of one viewport's render: only the\n * geometry is cached — the SvgModelView is built per viewport at render time,\n * so template-driven styling (lineStyles incl. dash, fills) applies without\n * re-sectioning.\n */\nexport type ComputedSection =\n | {\n kind: 'plan';\n geometry: SlabSectionResult<ProductLike>;\n basis: PlaneBasis;\n segmentCount: number;\n /** Per-product cut polylines (world x/z) — feeds labels/legend items. */\n entries: ProductPolylineEntry[];\n }\n | {\n kind: 'elevation';\n perProduct: SectionEntries;\n normal: Vec3;\n segmentCount: number;\n entries: ProductPolylineEntry[];\n };\n\n/**\n * Section-geometry cache, one entry per distinct section definition. Owned by\n * the caller (its lifetime = one loaded model; clear it when the model\n * changes); renderTemplateSheet evicts entries no viewport references any\n * more, keeping it bounded while sharing repeated definitions.\n */\nexport class SectionCache {\n private readonly _map = new Map<string, ComputedSection>();\n\n get(key: string): ComputedSection | undefined {\n return this._map.get(key);\n }\n\n set(key: string, value: ComputedSection): void {\n this._map.set(key, value);\n }\n\n evictExcept(used: ReadonlySet<string>): void {\n for (const key of this._map.keys()) {\n if (!used.has(key)) this._map.delete(key);\n }\n }\n\n clear(): void {\n this._map.clear();\n }\n}\n\nexport interface RenderTemplateSheetInput {\n template: LayoutTemplate;\n /** Products with geometry — pass ALL of them (unfiltered; see above). */\n products: ProductLike[];\n /**\n * Fallback plan position (world) for viewports without a section\n * definition — typically the model AABB centre. Without it such\n * viewports are skipped with a warning.\n */\n modelCenter?: [number, number, number];\n /** Section defaults applied where viewports omit cutHeight/depth. */\n defaults?: SectionDefaults;\n /** Reuse section geometry across renders (strongly recommended). */\n cache?: SectionCache;\n /** Context for role:'main'/'anchor' QR elements. */\n qrContext?: {\n host?: string | null;\n containerName?: string | null;\n floorName?: string | null;\n planeHeight?: number | null;\n };\n /** Values for the template's declared variables ({{vars.*}}). */\n variables?: Record<string, unknown>;\n /**\n * Host-side authorisation for fetching `https://` resources, matching the option `renderDrawing`\n * already takes. Without it a caller rendering a sheet had no say at all — only the template\n * could enable remote loading — so a host that wanted to permit it for its own templates could\n * not, and one that wanted to observe the setting had nowhere to look.\n */\n allowRemoteResources?: boolean;\n /**\n * Progress callback: overall fraction 0–1 plus a human-readable phase.\n * Section phases stream from SlabSection per viewport (cached sections\n * skip ahead); the final render step reports as its own phase. Wire this\n * to a busy indicator — large models can take tens of seconds, and\n * without feedback a long compute is indistinguishable from a hang.\n */\n onProgress?: (progress: number, phase: string) => void;\n}\n\nexport interface RenderedTemplateSheet {\n svg: string;\n warnings: string[];\n /** Sum over viewports — 0 means no cut produced drawable geometry. */\n segmentCount: number;\n}\n\n/**\n * Read a viewport's `stairArrows` geometry options into a canonical object:\n * known fields only, fixed order, invalid values dropped — so equal options\n * always serialize to the same cache-key fragment and unknown junk from a\n * hand-written template can't reach the geometry pass.\n */\nexport function readViewportStairArrowOptions(vp: Record<string, unknown>): ViewportStairArrowOptions | undefined {\n const raw = vp.stairArrows;\n if (!raw || typeof raw !== 'object') return undefined;\n const r = raw as Record<string, unknown>;\n const out: ViewportStairArrowOptions = {};\n if (r.startMarker === 'filled' || r.startMarker === 'open' || r.startMarker === 'none') {\n out.startMarker = r.startMarker;\n }\n if (r.arrowhead === 'filled' || r.arrowhead === 'outline' || r.arrowhead === 'open') {\n out.arrowhead = r.arrowhead;\n }\n for (const k of ['lengthScale', 'minLength', 'maxLength', 'landingExtension'] as const) {\n const v = r[k];\n if (typeof v === 'number' && Number.isFinite(v) && v >= 0) out[k] = v;\n }\n return Object.keys(out).length > 0 ? out : undefined;\n}\n\nasync function computeSectionForDef(\n products: ProductLike[],\n def: ResolvedSectionDef,\n onPhase?: (pct: number, phase: string) => void,\n classBehavior?: ClassBehaviorRule[],\n stairArrows?: ViewportStairArrowOptions\n): Promise<ComputedSection> {\n if (!def.plan) {\n // Elevations/sections: a thin plane section projected by the section-\n // state builder — its planeNormal projection is the canonical\n // orientation (a hand-rolled basis renders elevations mirrored).\n onPhase?.(0, 'Computing plane sections');\n const n = def.normal;\n // AABB gate: a product whose bounds do not straddle the section plane\n // cannot contribute plane sections — skip it before any mesh access.\n const straddling = products.filter((p) => {\n const bi = productAabb(p);\n if (!bi) return true;\n const c0 = (bi.min[0] + bi.max[0]) * 0.5 * n[0];\n const c1 = (bi.min[1] + bi.max[1]) * 0.5 * n[1];\n const c2 = (bi.min[2] + bi.max[2]) * 0.5 * n[2];\n const e =\n ((bi.max[0] - bi.min[0]) / 2) * Math.abs(n[0]) +\n ((bi.max[1] - bi.min[1]) / 2) * Math.abs(n[1]) +\n ((bi.max[2] - bi.min[2]) / 2) * Math.abs(n[2]);\n const center = c0 + c1 + c2;\n return center - e <= def.position && center + e >= def.position;\n });\n const origin: Vec3 = [n[0] * def.position, n[1] * def.position, n[2] * def.position];\n const plane = planeFromPointAndNormal(origin, n as unknown as Vec3);\n const section = await computePlaneSectionsForProducts(straddling, plane);\n onPhase?.(100, 'Plane sections complete');\n return {\n kind: 'elevation',\n perProduct: section.perProduct as unknown as SectionEntries,\n normal: n as unknown as Vec3,\n segmentCount: section.segments.length,\n entries: []\n };\n }\n\n // Plans go through SlabSection (cut/visible/hidden lines with occlusion).\n const viewDir: [number, number, number] = [0, -1, 0];\n const above = Math.max(0, def.depth.above);\n const below = Math.max(0, def.depth.below);\n const slab = new SlabSection<ProductLike>({\n cutHeight: def.position,\n viewDir,\n cutOrigin: [0, def.position, 0],\n depth: { above, below },\n // stairArrows: generated only for stair-category products (className fallback in\n // the gather), so enabling it unconditionally costs nothing on stairless slabs —\n // and the stairArrows lineStyles category has been in the template schema all\n // along, styling output this pipeline never produced.\n lines: { cut: true, visible: true, hidden: true, stairArrows: true },\n // Template-driven marker shapes / run sizing; SlabSection merges the\n // partial object with its own defaults.\n ...(stairArrows ? { stairArrows } : {}),\n // Per-product attribution feeds labels/legend items.\n trackProducts: true,\n onProgress: onPhase,\n // Forward-compatible pass-through: the published product-geometry ignores unknown\n // options; the classBehavior contract activates with TWINS-3784's SlabSection.\n ...(classBehavior?.length ? { classBehavior } : {})\n });\n const geometry = await slab.compute(products);\n const segmentCount = geometry.cutLines.length + geometry.visibleLines.length + geometry.hiddenLines.length;\n const entries = slabPerProductToEntries(geometry.perProduct ?? []);\n const basis = buildPlaneBasis([0, def.position + above, 0], viewDir, [0, 0, 1]);\n return { kind: 'plan', geometry, basis, segmentCount, entries };\n}\n\n/** Build a viewport's model view from cached geometry + its own lineStyles. */\nfunction buildViewForViewport(computed: ComputedSection, vp: Record<string, unknown>): SvgModelView | null {\n const lineStyles = vp.lineStyles as ViewportLineStyles | undefined;\n if (computed.kind === 'plan') {\n return slabSectionToModelView(computed.geometry, computed.basis, lineStyles);\n }\n const state = buildSectionStateForEntries(computed.perProduct, {\n strokeWidth: lineStyles?.default?.widthMm ?? 0.25,\n includeMeta: true,\n includeLabels: false,\n fills: vp.fills as SectionStateOptions['fills'],\n planeNormal: computed.normal\n });\n return state ? sectionStateToModelView(state) : null;\n}\n\n/**\n * Render a template against a product set: resolve each viewport's section\n * definition, compute (or reuse from `cache`) the section per distinct\n * definition, and feed renderDrawing one model per viewport id.\n */\nexport async function renderTemplateSheet(input: RenderTemplateSheetInput): Promise<RenderedTemplateSheet> {\n const warnings: string[] = [];\n const cache = input.cache ?? new SectionCache();\n\n // Resolve template variables BEFORE section resolution — variables can\n // drive section placement (\"cutHeight\": \"{{vars.cutHeight}}\", typed\n // whole-value substitution), and the geometry cache must key on the\n // resolved values. The resolved template carries no {{vars.*}} left, so\n // renderDrawing gets no variables input.\n let template = input.template;\n if (template.variables) {\n const { resolveTemplateValues, applyTemplateVariables } = await import('./templateVariables');\n const { values, warnings: varWarnings } = resolveTemplateValues(template, input.variables);\n varWarnings.forEach((w) => warnings.push(`[Variables] ${w}`));\n template = applyTemplateVariables(template, values, warnings);\n }\n\n const viewports = ((template.elements ?? []) as unknown as Array<Record<string, unknown>>).filter(\n (e): e is Record<string, unknown> & { id: string } => e.kind === 'viewport' && typeof e.id === 'string'\n );\n\n if (viewports.length === 0) {\n warnings.push('Template has no viewport elements — nothing to section.');\n }\n\n // Per-viewport compute-class closure (classClosure.ts): only the viewport's include\n // classes take part in its drawing generation, hidden lines included — geometry\n // outside the closure neither draws nor OCCLUDES there, and pays no compute cost\n // (the dominant lever on MEP-heavy models). The closure contains the viewport's\n // highlight/clip/fill/label/legend classes too, so the \"sections computed unfiltered\"\n // invariant above still holds WITHIN it. The closure is part of the section cache\n // key: viewports sharing (sectionDef, closure) share geometry, and editing\n // includeClasses re-sections.\n const productsByClosureKey = new Map<string, ProductLike[]>();\n const productsForClosure = (closure: Set<string> | null): { products: ProductLike[]; closureKey: string } => {\n const closureKey = closure ? [...closure].sort().join(',') : 'all';\n let products = productsByClosureKey.get(closureKey);\n if (!products) {\n products = closure ? filterProductsByClosure(input.products, closure) : input.products;\n productsByClosureKey.set(closureKey, products);\n }\n return { products, closureKey };\n };\n\n const models: Record<string, SvgModelView> = {};\n const usedKeys = new Set<string>();\n let segmentCount = 0;\n // Labels/legend items are placed in world x/z (floor-plane convention) and must see ALL products\n // regardless of viewport class filters — so they come from each viewport's unfiltered section.\n //\n // Keyed by viewport id rather than collapsed into one list: a legend declares which viewport it\n // describes (`legend.viewport`, required) and renderLayout reads that viewport's items, so a\n // sheet may carry several legends describing several plans. Passing one flat list handed every\n // legend the first plan's contents no matter what it was bound to.\n // No fallback to another viewport's entries: a legend describing a viewport that yielded nothing\n // renders empty and renderLayout warns by name (\"Legend 'x' has no items from viewport 'y'\"),\n // which is more use than quietly showing another viewport's contents under its heading.\n const entriesByViewport: Record<string, ProductPolylineEntry[]> = {};\n // gid → class/property metadata for EVERY product in every section. The entries\n // arrays only cover cut-bearing products, but render-time includeClasses filtering\n // must classify products whose geometry never crosses the cut (furniture below a\n // plan cut) — without this map their visible/hidden lines vanish from the drawing.\n const fullMetadata: ReturnType<typeof buildMetadataMap> = {};\n\n for (const vp of viewports) {\n let def = resolveViewportSection(vp as never, input.defaults);\n if (!def) {\n if (!input.modelCenter) {\n warnings.push(`Viewport '${vp.id}' has no section definition and no modelCenter fallback — skipped.`);\n continue;\n }\n def = {\n plan: true,\n normal: [0, 1, 0],\n position: input.modelCenter[1],\n depth: { above: 0.5, below: 1.5 }\n };\n warnings.push(\n `Viewport '${vp.id}' has no section definition — using a default plan at ${def.position.toFixed(1)} m.`\n );\n }\n const { products, closureKey } = productsForClosure(computeViewportClassClosure(vp, template));\n const classBehavior = collectClassBehavior(vp, template);\n // Behavior is part of the cache key: same def+closure with different behavior\n // flags must not share geometry.\n const behaviorKey = classBehavior.length ? JSON.stringify(classBehavior) : '';\n // Stair-arrow geometry options are part of the cache key too: the arrows are\n // baked into the section geometry, so viewports with different marker styles\n // must not share it. readViewportStairArrowOptions canonicalizes field order.\n const stairArrows = readViewportStairArrowOptions(vp);\n const stairKey = stairArrows ? JSON.stringify(stairArrows) : '';\n const key = `${sectionDefKey(def)}|${closureKey}|${behaviorKey}|${stairKey}`;\n usedKeys.add(key);\n let computed = cache.get(key);\n if (!computed) {\n // Section compute dominates total time; reserve the last 10% for the\n // render step below. Cached viewports jump ahead without reporting.\n const vpIndex = viewports.indexOf(vp);\n computed = await computeSectionForDef(\n products,\n def,\n (pct, phase) =>\n input.onProgress?.(\n ((vpIndex + pct / 100) / viewports.length) * 0.9,\n `Viewport '${vp.id}': ${phase}`\n ),\n classBehavior,\n stairArrows\n );\n cache.set(key, computed);\n }\n segmentCount += computed.segmentCount;\n if (computed.kind === 'plan') {\n Object.assign(fullMetadata, buildMetadataMap((computed.geometry.perProduct ?? []) as unknown[]));\n } else {\n Object.assign(fullMetadata, buildMetadataMap(computed.perProduct as unknown[]));\n }\n if (computed.entries?.length) entriesByViewport[vp.id] = computed.entries;\n const view = buildViewForViewport(computed, vp);\n if (!view) {\n warnings.push(`Viewport '${vp.id}': section produced no drawable geometry at this cut.`);\n continue;\n }\n models[vp.id] = view;\n }\n\n cache.evictExcept(usedKeys);\n\n if (Object.keys(models).length === 0) {\n return { svg: '', warnings, segmentCount };\n }\n\n input.onProgress?.(0.9, 'Rendering sheet');\n const { svg, warnings: renderWarnings } = await renderDrawing({\n // Already variable-resolved above — pass no variables down.\n template,\n models,\n options: {\n allowRemoteResources:\n (template as { allowRemoteResources?: boolean }).allowRemoteResources ?? input.allowRemoteResources,\n qrContext: input.qrContext,\n metadataById: fullMetadata,\n // Feeds labels/legend items, keyed by viewport so each legend describes the viewport it\n // declares — a sheet may carry several legends over several plans. This used to be one\n // flat list taken from the first plan viewport, which handed every legend that plan's\n // contents no matter what it was bound to.\n productEntries: Object.keys(entriesByViewport).length ? entriesByViewport : undefined\n }\n });\n input.onProgress?.(1, 'Complete');\n return { svg, warnings: [...warnings, ...renderWarnings], segmentCount };\n}\n"]}
|
|
@@ -1,6 +1,25 @@
|
|
|
1
|
+
/** Round a coordinate for SVG output (3 decimals ≈ 1 µm at mm scale) —
|
|
2
|
+
* emitting raw doubles bloats the SVG with 17-digit numbers. */
|
|
3
|
+
export declare const formatNumber: (value: number) => number;
|
|
1
4
|
export declare const escapeAttr: (value: unknown) => string;
|
|
2
5
|
export declare const escapeText: (value: unknown) => string;
|
|
3
6
|
export declare const getPathValue: (source: unknown, path: string) => unknown;
|
|
7
|
+
/**
|
|
8
|
+
* Split a placeholder expression into its value path and optional format
|
|
9
|
+
* filter: "area | round:1" → { path: 'area', filter: 'round:1' }.
|
|
10
|
+
*/
|
|
11
|
+
export declare const splitPlaceholder: (expr: string) => {
|
|
12
|
+
path: string;
|
|
13
|
+
filter: string | null;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Apply a placeholder format filter to a resolved value:
|
|
17
|
+
* - `round` / `round:N` — round to N decimals (default 0), trailing zeros trimmed
|
|
18
|
+
* - `fixed` / `fixed:N` — exactly N decimals (default 0), for aligned output
|
|
19
|
+
* Non-numeric values pass through unchanged; unknown filters warn and pass
|
|
20
|
+
* through so a typo degrades to the unformatted value, not an empty label.
|
|
21
|
+
*/
|
|
22
|
+
export declare const applyPlaceholderFilter: (value: unknown, filter: string | null, warnings?: string[], context?: string) => unknown;
|
|
4
23
|
export declare const interpolate: (template: string, data: any, warnings?: string[]) => string;
|
|
5
24
|
export declare const interpolateWithSources: (template: string, sources: any[]) => string;
|
|
6
25
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderUtils.d.ts","sourceRoot":"/","sources":["layout/renderUtils.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,UAAU,GAAI,OAAO,OAAO,KAAG,MACgE,CAAC;AAE7G,eAAO,MAAM,UAAU,GAAI,OAAO,OAAO,KAAG,MACwC,CAAC;AAErF,eAAO,MAAM,YAAY,GAAI,QAAQ,OAAO,EAAE,MAAM,MAAM,KAAG,OAa5D,CAAC;AAEF,eAAO,MAAM,WAAW,GAAI,UAAU,MAAM,EAAE,MAAM,GAAG,EAAE,WAAW,MAAM,EAAE,KAAG,
|
|
1
|
+
{"version":3,"file":"renderUtils.d.ts","sourceRoot":"/","sources":["layout/renderUtils.ts"],"names":[],"mappings":"AAEA;iEACiE;AACjE,eAAO,MAAM,YAAY,GAAI,OAAO,MAAM,KAAG,MACuB,CAAC;AAErE,eAAO,MAAM,UAAU,GAAI,OAAO,OAAO,KAAG,MACgE,CAAC;AAE7G,eAAO,MAAM,UAAU,GAAI,OAAO,OAAO,KAAG,MACwC,CAAC;AAErF,eAAO,MAAM,YAAY,GAAI,QAAQ,OAAO,EAAE,MAAM,MAAM,KAAG,OAa5D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAIpF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,GAC/B,OAAO,OAAO,EACd,QAAQ,MAAM,GAAG,IAAI,EACrB,WAAW,MAAM,EAAE,EACnB,UAAU,MAAM,KACjB,OAkBF,CAAC;AAEF,eAAO,MAAM,WAAW,GAAI,UAAU,MAAM,EAAE,MAAM,GAAG,EAAE,WAAW,MAAM,EAAE,KAAG,MAmBzE,CAAC;AAEP,eAAO,MAAM,sBAAsB,GAAI,UAAU,MAAM,EAAE,SAAS,GAAG,EAAE,KAAG,MAUpE,CAAC;AAKP;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,GAAI,MAAM,OAAO,EAAE,WAAW,MAAM,KAAG,OAwBlE,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,KAAK,OAAO,EAAE,MAAM,MAAM,KAAG,OAOxD,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,GAC9B,MAAM,OAAO,EACb,QAAQ;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,EAAE,CAAC,EAAE,OAAO,EAAE,CAAA;CAAE,GAAG,SAAS,KACxE,OAmBF,CAAC"}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { isRecord } from './typeGuards';
|
|
2
|
+
/** Round a coordinate for SVG output (3 decimals ≈ 1 µm at mm scale) —
|
|
3
|
+
* emitting raw doubles bloats the SVG with 17-digit numbers. */
|
|
4
|
+
export const formatNumber = (value) => Number.isFinite(value) ? Number.parseFloat(value.toFixed(3)) : 0;
|
|
2
5
|
export const escapeAttr = (value) => String(value).replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
3
6
|
export const escapeText = (value) => String(value).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
4
7
|
export const getPathValue = (source, path) => {
|
|
@@ -15,7 +18,46 @@ export const getPathValue = (source, path) => {
|
|
|
15
18
|
}
|
|
16
19
|
return current;
|
|
17
20
|
};
|
|
18
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Split a placeholder expression into its value path and optional format
|
|
23
|
+
* filter: "area | round:1" → { path: 'area', filter: 'round:1' }.
|
|
24
|
+
*/
|
|
25
|
+
export const splitPlaceholder = (expr) => {
|
|
26
|
+
const pipe = expr.indexOf('|');
|
|
27
|
+
if (pipe < 0)
|
|
28
|
+
return { path: expr.trim(), filter: null };
|
|
29
|
+
return { path: expr.slice(0, pipe).trim(), filter: expr.slice(pipe + 1).trim() };
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Apply a placeholder format filter to a resolved value:
|
|
33
|
+
* - `round` / `round:N` — round to N decimals (default 0), trailing zeros trimmed
|
|
34
|
+
* - `fixed` / `fixed:N` — exactly N decimals (default 0), for aligned output
|
|
35
|
+
* Non-numeric values pass through unchanged; unknown filters warn and pass
|
|
36
|
+
* through so a typo degrades to the unformatted value, not an empty label.
|
|
37
|
+
*/
|
|
38
|
+
export const applyPlaceholderFilter = (value, filter, warnings, context) => {
|
|
39
|
+
if (!filter)
|
|
40
|
+
return value;
|
|
41
|
+
const m = /^(round|fixed)(?:\s*:\s*(\d+))?$/.exec(filter);
|
|
42
|
+
if (!m) {
|
|
43
|
+
warnings?.push(`Unknown placeholder filter '${filter}'${context ? ` in "${context}"` : ''}.`);
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
const num = typeof value === 'number' ? value : Number(value);
|
|
47
|
+
if (typeof value === 'boolean' || value === '' || !Number.isFinite(num))
|
|
48
|
+
return value;
|
|
49
|
+
const digits = m[2] ? Number(m[2]) : 0;
|
|
50
|
+
// toFixed throws RangeError outside 0..100, and the digit count comes from the template — a
|
|
51
|
+
// stray `{{ x | fixed:200 }}` would take the whole render down rather than warning about itself.
|
|
52
|
+
if (!Number.isInteger(digits) || digits < 0 || digits > 100) {
|
|
53
|
+
warnings?.push(`Filter '${filter}' needs 0-100 decimals${context ? ` in "${context}"` : ''}.`);
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
const fixed = num.toFixed(digits);
|
|
57
|
+
return m[1] === 'fixed' ? fixed : String(Number(fixed));
|
|
58
|
+
};
|
|
59
|
+
export const interpolate = (template, data, warnings) => template.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (match, expr) => {
|
|
60
|
+
const { path, filter } = splitPlaceholder(expr);
|
|
19
61
|
const value = getPathValue(data, path);
|
|
20
62
|
if (value === undefined || value === null) {
|
|
21
63
|
if (warnings) {
|
|
@@ -29,13 +71,14 @@ export const interpolate = (template, data, warnings) => template.replace(/\{\{\
|
|
|
29
71
|
}
|
|
30
72
|
return '';
|
|
31
73
|
}
|
|
32
|
-
return String(value);
|
|
74
|
+
return String(applyPlaceholderFilter(value, filter, warnings, template));
|
|
33
75
|
});
|
|
34
|
-
export const interpolateWithSources = (template, sources) => template.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (_match,
|
|
76
|
+
export const interpolateWithSources = (template, sources) => template.replace(/\{\{\s*([^}]+?)\s*\}\}/g, (_match, expr) => {
|
|
77
|
+
const { path, filter } = splitPlaceholder(expr);
|
|
35
78
|
for (const source of sources) {
|
|
36
79
|
const value = getPathValue(source, path);
|
|
37
80
|
if (value !== undefined && value !== null && typeof value !== 'object') {
|
|
38
|
-
return String(value);
|
|
81
|
+
return String(applyPlaceholderFilter(value, filter));
|
|
39
82
|
}
|
|
40
83
|
}
|
|
41
84
|
return '';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"renderUtils.js","sourceRoot":"/","sources":["layout/renderUtils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAExC,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,KAAc,EAAU,EAAE,CACjD,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAE7G,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,KAAc,EAAU,EAAE,CACjD,MAAM,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAErF,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAAe,EAAE,IAAY,EAAW,EAAE;IACnE,MAAM,KAAK,GAAG,IAAI;SACb,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,OAAO,CAAC,CAAC;IACrB,IAAI,OAAO,GAAY,MAAM,CAAC;IAC9B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACvB,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YACrB,OAAO,SAAS,CAAC;QACrB,CAAC;QACD,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC;IACD,OAAO,OAAO,CAAC;AACnB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,QAAgB,EAAE,IAAS,EAAE,QAAmB,EAAU,EAAE,CACpF,QAAQ,CAAC,OAAO,CAAC,yBAAyB,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;IACxD,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;IACvC,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACxC,IAAI,QAAQ,EAAE,CAAC;YACX,QAAQ,CAAC,IAAI,CAAC,iCAAiC,IAAI,SAAS,QAAQ,IAAI,CAAC,CAAC;QAC9E,CAAC;QACD,OAAO,EAAE,CAAC;IACd,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC5B,IAAI,QAAQ,EAAE,CAAC;YACX,QAAQ,CAAC,IAAI,CACT,sBAAsB,IAAI,mDAAmD,QAAQ,IAAI,CAC5F,CAAC;QACN,CAAC;QACD,OAAO,EAAE,CAAC;IACd,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACzB,CAAC,CAAC,CAAC;AAEP,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,QAAgB,EAAE,OAAc,EAAU,EAAE,CAC/E,QAAQ,CAAC,OAAO,CAAC,yBAAyB,EAAE,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE;IACzD,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,YAAY,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACzC,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YACrE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;QACzB,CAAC;IACL,CAAC;IACD,OAAO,EAAE,CAAC;AACd,CAAC,CAAC,CAAC;AAEP,yDAAyD;AACzD,6EAA6E;AAE7E;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAAa,EAAE,SAAiB,EAAW,EAAE;IACzE,IAAI,CAAC,SAAS,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IAEpD,MAAM,YAAY,GAAI,IAAgC,CAAC,YAAY,CAAC;IACpE,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC;QAAE,OAAO,SAAS,CAAC;IAE9C,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,SAAS,CAAC;IAEvC,MAAM,CAAC,OAAO,EAAE,GAAG,SAAS,CAAC,GAAG,KAAK,CAAC;IACtC,MAAM,QAAQ,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAErC,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC;IACtC,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;QAAE,OAAO,SAAS,CAAC;IAEzC,uDAAuD;IACvD,MAAM,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;IACzC,MAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC;IAE5F,6CAA6C;IAC7C,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,OAAO,IAAI,IAAI,EAAE,CAAC;QACpC,OAAO,IAAI,CAAC,KAAK,CAAC;IACtB,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,GAAY,EAAE,IAAY,EAAW,EAAE;IAC/D,IAAI,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;QACnC,OAAO,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACnC,CAAC;IACD,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACvC,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IACxC,OAAO,eAAe,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;AACtC,CAAC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CACjC,IAAa,EACb,MAAuE,EAChE,EAAE;IACT,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IAEzC,MAAM,KAAK,GAAG,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC;IACjD,MAAM,eAAe,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;IAEvF,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC9B,MAAM,gBAAgB,GAAG,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC;QAChH,OAAO,eAAe,KAAK,gBAAgB,CAAC;IAChD,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC;QAC3B,OAAO,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE;YACxB,MAAM,WAAW,GAAG,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YACvE,OAAO,eAAe,KAAK,WAAW,CAAC;QAC3C,CAAC,CAAC,CAAC;IACP,CAAC;IAED,OAAO,IAAI,CAAC;AAChB,CAAC,CAAC","sourcesContent":["import { isRecord } from './typeGuards';\n\nexport const escapeAttr = (value: unknown): string =>\n String(value).replace(/&/g, '&').replace(/\"/g, '"').replace(/</g, '<').replace(/>/g, '>');\n\nexport const escapeText = (value: unknown): string =>\n String(value).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');\n\nexport const getPathValue = (source: unknown, path: string): unknown => {\n const parts = path\n .split('.')\n .map((p) => p.trim())\n .filter(Boolean);\n let current: unknown = source;\n for (const part of parts) {\n if (!isRecord(current)) {\n return undefined;\n }\n current = current[part];\n }\n return current;\n};\n\nexport const interpolate = (template: string, data: any, warnings?: string[]): string =>\n template.replace(/\\{\\{\\s*([^}]+?)\\s*\\}\\}/g, (match, path) => {\n const value = getPathValue(data, path);\n if (value === undefined || value === null) {\n if (warnings) {\n warnings.push(`Unresolved template variable '${path}' in \"${template}\".`);\n }\n return '';\n }\n if (typeof value === 'object') {\n if (warnings) {\n warnings.push(\n `Template variable '${path}' resolved to an object instead of a string in \"${template}\".`\n );\n }\n return '';\n }\n return String(value);\n });\n\nexport const interpolateWithSources = (template: string, sources: any[]): string =>\n template.replace(/\\{\\{\\s*([^}]+?)\\s*\\}\\}/g, (_match, path) => {\n for (const source of sources) {\n const value = getPathValue(source, path);\n if (value !== undefined && value !== null && typeof value !== 'object') {\n return String(value);\n }\n }\n return '';\n });\n\n// colorFromString moved to utils.ts to avoid duplication\n// Import from utils.ts if needed: import { colorFromString } from './utils';\n\n/**\n * Resolve a short property path from an item's propertySets.\n *\n * Template paths use the format: SetName.propertyName\n * Example: \"SetName.propName\" resolves to item.propertySets.SetName.propName.value\n *\n * @param item - An object with propertySets structure\n * @param shortPath - Short path like \"SetName.propName\"\n * @returns The property value, or undefined if not found\n */\nexport const resolveProperty = (item: unknown, shortPath: string): unknown => {\n if (!shortPath || !isRecord(item)) return undefined;\n\n const propertySets = (item as Record<string, unknown>).propertySets;\n if (!isRecord(propertySets)) return undefined;\n\n const parts = shortPath.split('.');\n if (parts.length < 2) return undefined;\n\n const [setName, ...propParts] = parts;\n const propName = propParts.join('.');\n\n const propSet = propertySets[setName];\n if (!isRecord(propSet)) return undefined;\n\n // New T8 API: T8PropertySet has .properties sub-record\n const innerProps = propSet['properties'];\n const prop = (isRecord(innerProps) ? innerProps[propName] : undefined) ?? propSet[propName];\n\n // Handle both wrapped {value} and raw values\n if (isRecord(prop) && 'value' in prop) {\n return prop.value;\n }\n return prop;\n};\n\n/**\n * Resolve a property value using either short or full path.\n * Short path: \"SetName.propName\" → resolves via propertySets\n * Full path: \"propertySets.SetName.propName.value\" → resolves via direct traversal\n * Also handles top-level properties like \"color\".\n */\nexport const resolvePath = (obj: unknown, path: string): unknown => {\n if (path.startsWith('propertySets.')) {\n return getPathValue(obj, path);\n }\n const direct = getPathValue(obj, path);\n if (direct !== undefined) return direct;\n return resolveProperty(obj, path);\n};\n\n/**\n * Check if an item matches a filter condition using short property paths.\n *\n * @param item - An object with propertySets structure\n * @param filter - Filter with path (short format), equals, or in conditions\n * @returns true if item matches the filter\n */\nexport const matchesPropertyFilter = (\n item: unknown,\n filter: { path?: string; equals?: unknown; in?: unknown[] } | undefined\n): boolean => {\n if (!filter || !filter.path) return true;\n\n const value = resolveProperty(item, filter.path);\n const normalizedValue = typeof value === 'string' ? value.toLowerCase().trim() : value;\n\n if (filter.equals !== undefined) {\n const normalizedEquals = typeof filter.equals === 'string' ? filter.equals.toLowerCase().trim() : filter.equals;\n return normalizedValue === normalizedEquals;\n }\n\n if (Array.isArray(filter.in)) {\n return filter.in.some((v) => {\n const normalizedV = typeof v === 'string' ? v.toLowerCase().trim() : v;\n return normalizedValue === normalizedV;\n });\n }\n\n return true;\n};\n"]}
|
|
1
|
+
{"version":3,"file":"renderUtils.js","sourceRoot":"/","sources":["layout/renderUtils.ts"],"names":[],"mappings":"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","sourcesContent":["import { isRecord } from './typeGuards';\n\n/** Round a coordinate for SVG output (3 decimals ≈ 1 µm at mm scale) —\n * emitting raw doubles bloats the SVG with 17-digit numbers. */\nexport const formatNumber = (value: number): number =>\n Number.isFinite(value) ? Number.parseFloat(value.toFixed(3)) : 0;\n\nexport const escapeAttr = (value: unknown): string =>\n String(value).replace(/&/g, '&').replace(/\"/g, '"').replace(/</g, '<').replace(/>/g, '>');\n\nexport const escapeText = (value: unknown): string =>\n String(value).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');\n\nexport const getPathValue = (source: unknown, path: string): unknown => {\n const parts = path\n .split('.')\n .map((p) => p.trim())\n .filter(Boolean);\n let current: unknown = source;\n for (const part of parts) {\n if (!isRecord(current)) {\n return undefined;\n }\n current = current[part];\n }\n return current;\n};\n\n/**\n * Split a placeholder expression into its value path and optional format\n * filter: \"area | round:1\" → { path: 'area', filter: 'round:1' }.\n */\nexport const splitPlaceholder = (expr: string): { path: string; filter: string | null } => {\n const pipe = expr.indexOf('|');\n if (pipe < 0) return { path: expr.trim(), filter: null };\n return { path: expr.slice(0, pipe).trim(), filter: expr.slice(pipe + 1).trim() };\n};\n\n/**\n * Apply a placeholder format filter to a resolved value:\n * - `round` / `round:N` — round to N decimals (default 0), trailing zeros trimmed\n * - `fixed` / `fixed:N` — exactly N decimals (default 0), for aligned output\n * Non-numeric values pass through unchanged; unknown filters warn and pass\n * through so a typo degrades to the unformatted value, not an empty label.\n */\nexport const applyPlaceholderFilter = (\n value: unknown,\n filter: string | null,\n warnings?: string[],\n context?: string\n): unknown => {\n if (!filter) return value;\n const m = /^(round|fixed)(?:\\s*:\\s*(\\d+))?$/.exec(filter);\n if (!m) {\n warnings?.push(`Unknown placeholder filter '${filter}'${context ? ` in \"${context}\"` : ''}.`);\n return value;\n }\n const num = typeof value === 'number' ? value : Number(value);\n if (typeof value === 'boolean' || value === '' || !Number.isFinite(num)) return value;\n const digits = m[2] ? Number(m[2]) : 0;\n // toFixed throws RangeError outside 0..100, and the digit count comes from the template — a\n // stray `{{ x | fixed:200 }}` would take the whole render down rather than warning about itself.\n if (!Number.isInteger(digits) || digits < 0 || digits > 100) {\n warnings?.push(`Filter '${filter}' needs 0-100 decimals${context ? ` in \"${context}\"` : ''}.`);\n return value;\n }\n const fixed = num.toFixed(digits);\n return m[1] === 'fixed' ? fixed : String(Number(fixed));\n};\n\nexport const interpolate = (template: string, data: any, warnings?: string[]): string =>\n template.replace(/\\{\\{\\s*([^}]+?)\\s*\\}\\}/g, (match, expr) => {\n const { path, filter } = splitPlaceholder(expr);\n const value = getPathValue(data, path);\n if (value === undefined || value === null) {\n if (warnings) {\n warnings.push(`Unresolved template variable '${path}' in \"${template}\".`);\n }\n return '';\n }\n if (typeof value === 'object') {\n if (warnings) {\n warnings.push(\n `Template variable '${path}' resolved to an object instead of a string in \"${template}\".`\n );\n }\n return '';\n }\n return String(applyPlaceholderFilter(value, filter, warnings, template));\n });\n\nexport const interpolateWithSources = (template: string, sources: any[]): string =>\n template.replace(/\\{\\{\\s*([^}]+?)\\s*\\}\\}/g, (_match, expr) => {\n const { path, filter } = splitPlaceholder(expr);\n for (const source of sources) {\n const value = getPathValue(source, path);\n if (value !== undefined && value !== null && typeof value !== 'object') {\n return String(applyPlaceholderFilter(value, filter));\n }\n }\n return '';\n });\n\n// colorFromString moved to utils.ts to avoid duplication\n// Import from utils.ts if needed: import { colorFromString } from './utils';\n\n/**\n * Resolve a short property path from an item's propertySets.\n *\n * Template paths use the format: SetName.propertyName\n * Example: \"SetName.propName\" resolves to item.propertySets.SetName.propName.value\n *\n * @param item - An object with propertySets structure\n * @param shortPath - Short path like \"SetName.propName\"\n * @returns The property value, or undefined if not found\n */\nexport const resolveProperty = (item: unknown, shortPath: string): unknown => {\n if (!shortPath || !isRecord(item)) return undefined;\n\n const propertySets = (item as Record<string, unknown>).propertySets;\n if (!isRecord(propertySets)) return undefined;\n\n const parts = shortPath.split('.');\n if (parts.length < 2) return undefined;\n\n const [setName, ...propParts] = parts;\n const propName = propParts.join('.');\n\n const propSet = propertySets[setName];\n if (!isRecord(propSet)) return undefined;\n\n // New T8 API: T8PropertySet has .properties sub-record\n const innerProps = propSet['properties'];\n const prop = (isRecord(innerProps) ? innerProps[propName] : undefined) ?? propSet[propName];\n\n // Handle both wrapped {value} and raw values\n if (isRecord(prop) && 'value' in prop) {\n return prop.value;\n }\n return prop;\n};\n\n/**\n * Resolve a property value using either short or full path.\n * Short path: \"SetName.propName\" → resolves via propertySets\n * Full path: \"propertySets.SetName.propName.value\" → resolves via direct traversal\n * Also handles top-level properties like \"color\".\n */\nexport const resolvePath = (obj: unknown, path: string): unknown => {\n if (path.startsWith('propertySets.')) {\n return getPathValue(obj, path);\n }\n const direct = getPathValue(obj, path);\n if (direct !== undefined) return direct;\n return resolveProperty(obj, path);\n};\n\n/**\n * Check if an item matches a filter condition using short property paths.\n *\n * @param item - An object with propertySets structure\n * @param filter - Filter with path (short format), equals, or in conditions\n * @returns true if item matches the filter\n */\nexport const matchesPropertyFilter = (\n item: unknown,\n filter: { path?: string; equals?: unknown; in?: unknown[] } | undefined\n): boolean => {\n if (!filter || !filter.path) return true;\n\n const value = resolveProperty(item, filter.path);\n const normalizedValue = typeof value === 'string' ? value.toLowerCase().trim() : value;\n\n if (filter.equals !== undefined) {\n const normalizedEquals = typeof filter.equals === 'string' ? filter.equals.toLowerCase().trim() : filter.equals;\n return normalizedValue === normalizedEquals;\n }\n\n if (Array.isArray(filter.in)) {\n return filter.in.some((v) => {\n const normalizedV = typeof v === 'string' ? v.toLowerCase().trim() : v;\n return normalizedValue === normalizedV;\n });\n }\n\n return true;\n};\n"]}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ViewportElement } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* The template's per-viewport section contract, resolved to one canonical
|
|
4
|
+
* shape. Templates declare either `cutHeight` (+ `depth`) for horizontal
|
|
5
|
+
* plans or `planeNormal` + `planePosition` for elevations/sections; this
|
|
6
|
+
* module is the single reader/writer of those fields so every consumer
|
|
7
|
+
* (workbench, viewer print dialog, …) interprets them identically.
|
|
8
|
+
*
|
|
9
|
+
* Canonical form: plans always have `normal = [0, 1, 0]` and `position` =
|
|
10
|
+
* world Y (a `planeNormal: [0,-1,0]` declaration is folded into that), so
|
|
11
|
+
* `position` can be used directly as the cut height.
|
|
12
|
+
*/
|
|
13
|
+
export interface ResolvedSectionDef {
|
|
14
|
+
/** True when the cut is a horizontal plan (normal along Y). */
|
|
15
|
+
plan: boolean;
|
|
16
|
+
/** Normalized cut-plane normal (world). `[0, 1, 0]` for plans. */
|
|
17
|
+
normal: [number, number, number];
|
|
18
|
+
/** Position along the normal in world metres — world Y for plans. */
|
|
19
|
+
position: number;
|
|
20
|
+
/** Slab thickness in front of / behind the plane (metres). */
|
|
21
|
+
depth: {
|
|
22
|
+
above: number;
|
|
23
|
+
below: number;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export declare const DEFAULT_SECTION_DEPTH: {
|
|
27
|
+
readonly above: 0.5;
|
|
28
|
+
readonly below: 1.5;
|
|
29
|
+
};
|
|
30
|
+
export interface SectionDefaults {
|
|
31
|
+
cutHeight?: number;
|
|
32
|
+
depth?: {
|
|
33
|
+
above?: number;
|
|
34
|
+
below?: number;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* True when a section field carries a whole-value variable binding
|
|
39
|
+
* (`"{{vars.cutHeight}}"`). Bound fields resolve to numbers only after
|
|
40
|
+
* `applyTemplateVariables` — resolveViewportSection treats them as
|
|
41
|
+
* unresolvable rather than guessing.
|
|
42
|
+
*/
|
|
43
|
+
export declare function isVariableBinding(v: unknown): boolean;
|
|
44
|
+
/** True when any of the viewport's section fields is variable-bound. */
|
|
45
|
+
export declare function sectionUsesVariables(vp: Pick<ViewportElement, 'cutHeight' | 'planePosition'>): boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Read a viewport element's section definition. `planeNormal` +
|
|
48
|
+
* `planePosition` takes precedence over `cutHeight` (matching the field
|
|
49
|
+
* docs); returns null when the viewport declares nothing (and no default
|
|
50
|
+
* `cutHeight` is provided) or when a section field is still variable-bound
|
|
51
|
+
* (resolve the template's variables first).
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveViewportSection(vp: Pick<ViewportElement, 'cutHeight' | 'planeNormal' | 'planePosition' | 'depth'>, defaults?: SectionDefaults): ResolvedSectionDef | null;
|
|
54
|
+
/**
|
|
55
|
+
* Write a section definition back onto a viewport element in canonical form:
|
|
56
|
+
* plans use the `cutHeight` shorthand (and drop `planeNormal`/`planePosition`),
|
|
57
|
+
* everything else uses `planeNormal` + `planePosition` (and drops
|
|
58
|
+
* `cutHeight`). Keeping reader and writer together prevents the two
|
|
59
|
+
* shorthands from coexisting on one element, where reader precedence would
|
|
60
|
+
* silently decide.
|
|
61
|
+
*/
|
|
62
|
+
export declare function writeViewportSection(vp: Partial<Pick<ViewportElement, 'cutHeight' | 'planeNormal' | 'planePosition' | 'depth'>>, def: ResolvedSectionDef): void;
|
|
63
|
+
/** Stable cache key for a resolved section definition (same cut ⇒ same key). */
|
|
64
|
+
export declare function sectionDefKey(def: ResolvedSectionDef): string;
|
|
65
|
+
//# sourceMappingURL=sectionDefs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sectionDefs.d.ts","sourceRoot":"/","sources":["layout/sectionDefs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C;;;;;;;;;;GAUG;AACH,MAAM,WAAW,kBAAkB;IAC/B,+DAA+D;IAC/D,IAAI,EAAE,OAAO,CAAC;IACd,kEAAkE;IAClE,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3C;AAED,eAAO,MAAM,qBAAqB;;;CAAsC,CAAC;AAEzE,MAAM,WAAW,eAAe;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9C;AAID;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAErD;AAED,wEAAwE;AACxE,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,IAAI,CAAC,eAAe,EAAE,WAAW,GAAG,eAAe,CAAC,GAAG,OAAO,CAEtG;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAClC,EAAE,EAAE,IAAI,CAAC,eAAe,EAAE,WAAW,GAAG,aAAa,GAAG,eAAe,GAAG,OAAO,CAAC,EAClF,QAAQ,CAAC,EAAE,eAAe,GAC3B,kBAAkB,GAAG,IAAI,CAoC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAChC,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,eAAe,EAAE,WAAW,GAAG,aAAa,GAAG,eAAe,GAAG,OAAO,CAAC,CAAC,EAC3F,GAAG,EAAE,kBAAkB,GACxB,IAAI,CAWN;AAID,gFAAgF;AAChF,wBAAgB,aAAa,CAAC,GAAG,EAAE,kBAAkB,GAAG,MAAM,CAO7D"}
|