@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,88 @@
|
|
|
1
|
+
export const DEFAULT_SECTION_DEPTH = { above: 0.5, below: 1.5 };
|
|
2
|
+
const AXIS_EPSILON = 1e-6;
|
|
3
|
+
/**
|
|
4
|
+
* True when a section field carries a whole-value variable binding
|
|
5
|
+
* (`"{{vars.cutHeight}}"`). Bound fields resolve to numbers only after
|
|
6
|
+
* `applyTemplateVariables` — resolveViewportSection treats them as
|
|
7
|
+
* unresolvable rather than guessing.
|
|
8
|
+
*/
|
|
9
|
+
export function isVariableBinding(v) {
|
|
10
|
+
return typeof v === 'string' && /^\{\{\s*vars\.\w+\s*\}\}$/.test(v);
|
|
11
|
+
}
|
|
12
|
+
/** True when any of the viewport's section fields is variable-bound. */
|
|
13
|
+
export function sectionUsesVariables(vp) {
|
|
14
|
+
return isVariableBinding(vp.cutHeight) || isVariableBinding(vp.planePosition);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Read a viewport element's section definition. `planeNormal` +
|
|
18
|
+
* `planePosition` takes precedence over `cutHeight` (matching the field
|
|
19
|
+
* docs); returns null when the viewport declares nothing (and no default
|
|
20
|
+
* `cutHeight` is provided) or when a section field is still variable-bound
|
|
21
|
+
* (resolve the template's variables first).
|
|
22
|
+
*/
|
|
23
|
+
export function resolveViewportSection(vp, defaults) {
|
|
24
|
+
// Unresolved bindings: never guess a position — callers must apply
|
|
25
|
+
// template variables before resolving sections.
|
|
26
|
+
if (sectionUsesVariables(vp))
|
|
27
|
+
return null;
|
|
28
|
+
const above = typeof vp.depth?.above === 'number' ? vp.depth.above : (defaults?.depth?.above ?? DEFAULT_SECTION_DEPTH.above);
|
|
29
|
+
const below = typeof vp.depth?.below === 'number' ? vp.depth.below : (defaults?.depth?.below ?? DEFAULT_SECTION_DEPTH.below);
|
|
30
|
+
const depth = { above: Math.max(0, above), below: Math.max(0, below) };
|
|
31
|
+
const n = vp.planeNormal;
|
|
32
|
+
if (Array.isArray(n) && n.length === 3 && n.every((v) => typeof v === 'number')) {
|
|
33
|
+
const len = Math.hypot(n[0], n[1], n[2]);
|
|
34
|
+
if (len > AXIS_EPSILON) {
|
|
35
|
+
let normal = [n[0] / len, n[1] / len, n[2] / len];
|
|
36
|
+
let position = typeof vp.planePosition === 'number' ? vp.planePosition : 0;
|
|
37
|
+
const plan = Math.abs(normal[0]) < AXIS_EPSILON && Math.abs(normal[2]) < AXIS_EPSILON;
|
|
38
|
+
if (plan && normal[1] < 0) {
|
|
39
|
+
// Canonicalize plans to +Y so position === world Y.
|
|
40
|
+
normal = [0, 1, 0];
|
|
41
|
+
position = -position;
|
|
42
|
+
}
|
|
43
|
+
else if (plan) {
|
|
44
|
+
normal = [0, 1, 0];
|
|
45
|
+
}
|
|
46
|
+
return { plan, normal, position, depth };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (typeof vp.cutHeight === 'number') {
|
|
50
|
+
return { plan: true, normal: [0, 1, 0], position: vp.cutHeight, depth };
|
|
51
|
+
}
|
|
52
|
+
if (typeof defaults?.cutHeight === 'number') {
|
|
53
|
+
return { plan: true, normal: [0, 1, 0], position: defaults.cutHeight, depth };
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Write a section definition back onto a viewport element in canonical form:
|
|
59
|
+
* plans use the `cutHeight` shorthand (and drop `planeNormal`/`planePosition`),
|
|
60
|
+
* everything else uses `planeNormal` + `planePosition` (and drops
|
|
61
|
+
* `cutHeight`). Keeping reader and writer together prevents the two
|
|
62
|
+
* shorthands from coexisting on one element, where reader precedence would
|
|
63
|
+
* silently decide.
|
|
64
|
+
*/
|
|
65
|
+
export function writeViewportSection(vp, def) {
|
|
66
|
+
vp.depth = { above: def.depth.above, below: def.depth.below };
|
|
67
|
+
if (def.plan) {
|
|
68
|
+
vp.cutHeight = def.normal[1] >= 0 ? def.position : -def.position;
|
|
69
|
+
delete vp.planeNormal;
|
|
70
|
+
delete vp.planePosition;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
vp.planeNormal = [def.normal[0], def.normal[1], def.normal[2]];
|
|
74
|
+
vp.planePosition = def.position;
|
|
75
|
+
delete vp.cutHeight;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const fmtNum = (v) => String(Math.round(v * 1e6) / 1e6);
|
|
79
|
+
/** Stable cache key for a resolved section definition (same cut ⇒ same key). */
|
|
80
|
+
export function sectionDefKey(def) {
|
|
81
|
+
return [
|
|
82
|
+
def.plan ? 'plan' : 'section',
|
|
83
|
+
def.normal.map(fmtNum).join(','),
|
|
84
|
+
fmtNum(def.position),
|
|
85
|
+
`${fmtNum(def.depth.above)}/${fmtNum(def.depth.below)}`
|
|
86
|
+
].join('|');
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=sectionDefs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sectionDefs.js","sourceRoot":"/","sources":["layout/sectionDefs.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ViewportElement } from './types';\n\n/**\n * The template's per-viewport section contract, resolved to one canonical\n * shape. Templates declare either `cutHeight` (+ `depth`) for horizontal\n * plans or `planeNormal` + `planePosition` for elevations/sections; this\n * module is the single reader/writer of those fields so every consumer\n * (workbench, viewer print dialog, …) interprets them identically.\n *\n * Canonical form: plans always have `normal = [0, 1, 0]` and `position` =\n * world Y (a `planeNormal: [0,-1,0]` declaration is folded into that), so\n * `position` can be used directly as the cut height.\n */\nexport interface ResolvedSectionDef {\n /** True when the cut is a horizontal plan (normal along Y). */\n plan: boolean;\n /** Normalized cut-plane normal (world). `[0, 1, 0]` for plans. */\n normal: [number, number, number];\n /** Position along the normal in world metres — world Y for plans. */\n position: number;\n /** Slab thickness in front of / behind the plane (metres). */\n depth: { above: number; below: number };\n}\n\nexport const DEFAULT_SECTION_DEPTH = { above: 0.5, below: 1.5 } as const;\n\nexport interface SectionDefaults {\n cutHeight?: number;\n depth?: { above?: number; below?: number };\n}\n\nconst AXIS_EPSILON = 1e-6;\n\n/**\n * True when a section field carries a whole-value variable binding\n * (`\"{{vars.cutHeight}}\"`). Bound fields resolve to numbers only after\n * `applyTemplateVariables` — resolveViewportSection treats them as\n * unresolvable rather than guessing.\n */\nexport function isVariableBinding(v: unknown): boolean {\n return typeof v === 'string' && /^\\{\\{\\s*vars\\.\\w+\\s*\\}\\}$/.test(v);\n}\n\n/** True when any of the viewport's section fields is variable-bound. */\nexport function sectionUsesVariables(vp: Pick<ViewportElement, 'cutHeight' | 'planePosition'>): boolean {\n return isVariableBinding(vp.cutHeight) || isVariableBinding(vp.planePosition);\n}\n\n/**\n * Read a viewport element's section definition. `planeNormal` +\n * `planePosition` takes precedence over `cutHeight` (matching the field\n * docs); returns null when the viewport declares nothing (and no default\n * `cutHeight` is provided) or when a section field is still variable-bound\n * (resolve the template's variables first).\n */\nexport function resolveViewportSection(\n vp: Pick<ViewportElement, 'cutHeight' | 'planeNormal' | 'planePosition' | 'depth'>,\n defaults?: SectionDefaults\n): ResolvedSectionDef | null {\n // Unresolved bindings: never guess a position — callers must apply\n // template variables before resolving sections.\n if (sectionUsesVariables(vp)) return null;\n\n const above =\n typeof vp.depth?.above === 'number' ? vp.depth.above : (defaults?.depth?.above ?? DEFAULT_SECTION_DEPTH.above);\n const below =\n typeof vp.depth?.below === 'number' ? vp.depth.below : (defaults?.depth?.below ?? DEFAULT_SECTION_DEPTH.below);\n const depth = { above: Math.max(0, above), below: Math.max(0, below) };\n\n const n = vp.planeNormal;\n if (Array.isArray(n) && n.length === 3 && n.every((v) => typeof v === 'number')) {\n const len = Math.hypot(n[0], n[1], n[2]);\n if (len > AXIS_EPSILON) {\n let normal: [number, number, number] = [n[0] / len, n[1] / len, n[2] / len];\n let position = typeof vp.planePosition === 'number' ? vp.planePosition : 0;\n const plan = Math.abs(normal[0]) < AXIS_EPSILON && Math.abs(normal[2]) < AXIS_EPSILON;\n if (plan && normal[1] < 0) {\n // Canonicalize plans to +Y so position === world Y.\n normal = [0, 1, 0];\n position = -position;\n } else if (plan) {\n normal = [0, 1, 0];\n }\n return { plan, normal, position, depth };\n }\n }\n\n if (typeof vp.cutHeight === 'number') {\n return { plan: true, normal: [0, 1, 0], position: vp.cutHeight, depth };\n }\n if (typeof defaults?.cutHeight === 'number') {\n return { plan: true, normal: [0, 1, 0], position: defaults.cutHeight, depth };\n }\n return null;\n}\n\n/**\n * Write a section definition back onto a viewport element in canonical form:\n * plans use the `cutHeight` shorthand (and drop `planeNormal`/`planePosition`),\n * everything else uses `planeNormal` + `planePosition` (and drops\n * `cutHeight`). Keeping reader and writer together prevents the two\n * shorthands from coexisting on one element, where reader precedence would\n * silently decide.\n */\nexport function writeViewportSection(\n vp: Partial<Pick<ViewportElement, 'cutHeight' | 'planeNormal' | 'planePosition' | 'depth'>>,\n def: ResolvedSectionDef\n): void {\n vp.depth = { above: def.depth.above, below: def.depth.below };\n if (def.plan) {\n vp.cutHeight = def.normal[1] >= 0 ? def.position : -def.position;\n delete vp.planeNormal;\n delete vp.planePosition;\n } else {\n vp.planeNormal = [def.normal[0], def.normal[1], def.normal[2]];\n vp.planePosition = def.position;\n delete vp.cutHeight;\n }\n}\n\nconst fmtNum = (v: number): string => String(Math.round(v * 1e6) / 1e6);\n\n/** Stable cache key for a resolved section definition (same cut ⇒ same key). */\nexport function sectionDefKey(def: ResolvedSectionDef): string {\n return [\n def.plan ? 'plan' : 'section',\n def.normal.map(fmtNum).join(','),\n fmtNum(def.position),\n `${fmtNum(def.depth.above)}/${fmtNum(def.depth.below)}`\n ].join('|');\n}\n"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Vec3, SlabSectionGeometry, PlaneBasis } from '@twinfinity/geometry';
|
|
2
|
+
import type { SvgModelView, ViewportLineStyles, ProductPolylineEntry } from './types';
|
|
3
|
+
/** Per-product line attribution (SlabSection with `trackProducts: true`).
|
|
4
|
+
* Structurally typed — any `{ product, cutLines?, visibleLines?, … }` works. */
|
|
5
|
+
export type SlabPerProductEntry = {
|
|
6
|
+
product: unknown;
|
|
7
|
+
} & {
|
|
8
|
+
[K in keyof SlabSectionGeometry]?: Vec3[][];
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Convert SlabSection geometry into an SvgModelView for the print-layout engine
|
|
12
|
+
* (`renderDrawing({ models })`). Returns null when the geometry is empty or
|
|
13
|
+
* degenerate. Use a (0,0,1) up hint when building the plane basis for standard
|
|
14
|
+
* north-up floor plans.
|
|
15
|
+
*
|
|
16
|
+
* When the geometry carries `perProduct` attribution (SlabSection option
|
|
17
|
+
* `trackProducts: true`), the model also gets `rawPaths` with per-product gid
|
|
18
|
+
* metadata — required for the renderer's gid-based features (highlight, clip
|
|
19
|
+
* include/exclude, render-time includeClasses): without rawPaths,
|
|
20
|
+
* filterModelForViewport returns the model untouched and those settings
|
|
21
|
+
* silently do nothing.
|
|
22
|
+
*/
|
|
23
|
+
export declare function slabSectionToModelView(geometry: SlabSectionGeometry & {
|
|
24
|
+
perProduct?: SlabPerProductEntry[];
|
|
25
|
+
}, planeBasis: PlaneBasis, lineStyles?: ViewportLineStyles): SvgModelView | null;
|
|
26
|
+
/**
|
|
27
|
+
* Build `options.productEntries` for renderDrawing from SlabSection
|
|
28
|
+
* per-product attribution: each product's cut polylines in world x/z. Labels,
|
|
29
|
+
* legend, clip and highlight items are derived from these entries — the
|
|
30
|
+
* polylines must be in WORLD coordinates (use a zero lateral section origin so
|
|
31
|
+
* the projected space equals world x/z).
|
|
32
|
+
*/
|
|
33
|
+
export declare function slabPerProductToEntries(perProduct: SlabPerProductEntry[]): ProductPolylineEntry[];
|
|
34
|
+
//# sourceMappingURL=slabModelView.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slabModelView.d.ts","sourceRoot":"/","sources":["layout/slabModelView.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,mBAAmB,EAAE,UAAU,EAAe,MAAM,sBAAsB,CAAC;AAC/F,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAqB,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAIzG;iFACiF;AACjF,MAAM,MAAM,mBAAmB,GAAG;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,GAAG;KAAG,CAAC,IAAI,MAAM,mBAAmB,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE;CAAE,CAAC;AA4DzG;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CAClC,QAAQ,EAAE,mBAAmB,GAAG;IAAE,UAAU,CAAC,EAAE,mBAAmB,EAAE,CAAA;CAAE,EACtE,UAAU,EAAE,UAAU,EACtB,UAAU,CAAC,EAAE,kBAAkB,GAChC,YAAY,GAAG,IAAI,CA2FrB;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,mBAAmB,EAAE,GAAG,oBAAoB,EAAE,CAWjG"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { resolveCategoryLineStyle } from './renderDrawing';
|
|
2
|
+
import { escapeAttr } from './renderUtils';
|
|
3
|
+
// Paint order (SVG paints later paths on top): hidden categories first, so occluded
|
|
4
|
+
// linework can never cover visible geometry — same order printToSvg uses.
|
|
5
|
+
const CATEGORIES = [
|
|
6
|
+
'hiddenLines',
|
|
7
|
+
'outlineHidden',
|
|
8
|
+
'silhouettesHidden',
|
|
9
|
+
'visibleLines',
|
|
10
|
+
'outlineVisible',
|
|
11
|
+
'silhouettesVisible',
|
|
12
|
+
'featureEdges',
|
|
13
|
+
'cutLines',
|
|
14
|
+
'stairArrows'
|
|
15
|
+
];
|
|
16
|
+
function resolveStyle(category, lineStyles) {
|
|
17
|
+
return resolveCategoryLineStyle(category, lineStyles);
|
|
18
|
+
}
|
|
19
|
+
function projectPolyline(poly, basis) {
|
|
20
|
+
const ox = basis.origin[0], oy = basis.origin[1], oz = basis.origin[2];
|
|
21
|
+
const rx = basis.right[0], ry = basis.right[1], rz = basis.right[2];
|
|
22
|
+
const ux = basis.up[0], uy = basis.up[1], uz = basis.up[2];
|
|
23
|
+
const pts = [];
|
|
24
|
+
for (let i = 0; i < poly.length; i++) {
|
|
25
|
+
const p = poly[i];
|
|
26
|
+
const dx = p[0] - ox, dy = p[1] - oy, dz = p[2] - oz;
|
|
27
|
+
pts.push({ x: dx * rx + dy * ry + dz * rz, y: dx * ux + dy * uy + dz * uz });
|
|
28
|
+
}
|
|
29
|
+
return pts;
|
|
30
|
+
}
|
|
31
|
+
function fmt(v) {
|
|
32
|
+
return v.toFixed(6).replace(/\.?0+$/, '');
|
|
33
|
+
}
|
|
34
|
+
function buildPathD(projected) {
|
|
35
|
+
const parts = [];
|
|
36
|
+
for (const poly of projected) {
|
|
37
|
+
if (poly.length === 0)
|
|
38
|
+
continue;
|
|
39
|
+
parts.push(`M ${fmt(poly[0].x)} ${fmt(poly[0].y)}`);
|
|
40
|
+
for (let i = 1; i < poly.length; i++) {
|
|
41
|
+
parts.push(`L ${fmt(poly[i].x)} ${fmt(poly[i].y)}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return parts.join(' ');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Convert SlabSection geometry into an SvgModelView for the print-layout engine
|
|
48
|
+
* (`renderDrawing({ models })`). Returns null when the geometry is empty or
|
|
49
|
+
* degenerate. Use a (0,0,1) up hint when building the plane basis for standard
|
|
50
|
+
* north-up floor plans.
|
|
51
|
+
*
|
|
52
|
+
* When the geometry carries `perProduct` attribution (SlabSection option
|
|
53
|
+
* `trackProducts: true`), the model also gets `rawPaths` with per-product gid
|
|
54
|
+
* metadata — required for the renderer's gid-based features (highlight, clip
|
|
55
|
+
* include/exclude, render-time includeClasses): without rawPaths,
|
|
56
|
+
* filterModelForViewport returns the model untouched and those settings
|
|
57
|
+
* silently do nothing.
|
|
58
|
+
*/
|
|
59
|
+
export function slabSectionToModelView(geometry, planeBasis, lineStyles) {
|
|
60
|
+
const projected = {};
|
|
61
|
+
let minX = Infinity, maxX = -Infinity;
|
|
62
|
+
let minY = Infinity, maxY = -Infinity;
|
|
63
|
+
for (const cat of CATEGORIES) {
|
|
64
|
+
const polys = geometry[cat];
|
|
65
|
+
const result = [];
|
|
66
|
+
for (const poly of polys) {
|
|
67
|
+
if (poly.length === 0)
|
|
68
|
+
continue;
|
|
69
|
+
const pts = projectPolyline(poly, planeBasis);
|
|
70
|
+
for (const pt of pts) {
|
|
71
|
+
if (pt.x < minX)
|
|
72
|
+
minX = pt.x;
|
|
73
|
+
if (pt.x > maxX)
|
|
74
|
+
maxX = pt.x;
|
|
75
|
+
if (pt.y < minY)
|
|
76
|
+
minY = pt.y;
|
|
77
|
+
if (pt.y > maxY)
|
|
78
|
+
maxY = pt.y;
|
|
79
|
+
}
|
|
80
|
+
result.push(pts);
|
|
81
|
+
}
|
|
82
|
+
projected[cat] = result;
|
|
83
|
+
}
|
|
84
|
+
const viewWidth = maxX - minX;
|
|
85
|
+
const viewHeight = maxY - minY;
|
|
86
|
+
if (!Number.isFinite(viewWidth) || viewWidth <= 1e-6 || !Number.isFinite(viewHeight) || viewHeight <= 1e-6) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
// Map projected (x,y) to SVG coords: SVG_x = x - minX, SVG_y = maxY - y (flip).
|
|
90
|
+
const geometryTransform = `matrix(1 0 0 -1 ${fmt(-minX)} ${fmt(maxY)})`;
|
|
91
|
+
const svgParts = [`<g transform="${escapeAttr(geometryTransform)}">`];
|
|
92
|
+
for (const cat of CATEGORIES) {
|
|
93
|
+
const style = resolveStyle(cat, lineStyles);
|
|
94
|
+
if (style.enabled === false)
|
|
95
|
+
continue;
|
|
96
|
+
const d = buildPathD(projected[cat]);
|
|
97
|
+
if (!d)
|
|
98
|
+
continue;
|
|
99
|
+
let pathEl = `<path d="${d}" fill="none" stroke="${escapeAttr(style.color)}" stroke-width="${style.widthMm}"`;
|
|
100
|
+
if (style.dash)
|
|
101
|
+
pathEl += ` stroke-dasharray="${escapeAttr(style.dash)}"`;
|
|
102
|
+
pathEl += ` stroke-linejoin="round" stroke-linecap="round"/>`;
|
|
103
|
+
svgParts.push(pathEl);
|
|
104
|
+
}
|
|
105
|
+
svgParts.push('</g>');
|
|
106
|
+
// Per-product rawPaths (same projection + styles as the merged groupSvg)
|
|
107
|
+
// so gid-based features can filter/restyle and rebuild the model.
|
|
108
|
+
let rawPaths;
|
|
109
|
+
if (geometry.perProduct?.length) {
|
|
110
|
+
rawPaths = [];
|
|
111
|
+
for (const entry of geometry.perProduct) {
|
|
112
|
+
const gid = entry.product?.gid;
|
|
113
|
+
if (!gid)
|
|
114
|
+
continue;
|
|
115
|
+
for (const cat of CATEGORIES) {
|
|
116
|
+
const style = resolveStyle(cat, lineStyles);
|
|
117
|
+
if (style.enabled === false)
|
|
118
|
+
continue;
|
|
119
|
+
const polys = entry[cat];
|
|
120
|
+
if (!polys?.length)
|
|
121
|
+
continue;
|
|
122
|
+
const d = buildPathD(polys.filter((p) => p.length > 0).map((p) => projectPolyline(p, planeBasis)));
|
|
123
|
+
if (!d)
|
|
124
|
+
continue;
|
|
125
|
+
rawPaths.push({
|
|
126
|
+
key: `${gid}:${cat}`,
|
|
127
|
+
d,
|
|
128
|
+
fill: null,
|
|
129
|
+
fillOpacity: 1,
|
|
130
|
+
stroke: style.color,
|
|
131
|
+
strokeWidth: style.widthMm,
|
|
132
|
+
dash: style.dash || undefined,
|
|
133
|
+
meta: { gid, isSegment: true, category: cat }
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (rawPaths.length === 0)
|
|
138
|
+
rawPaths = undefined;
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
groupSvg: `<g>${svgParts.join('')}</g>`,
|
|
142
|
+
widthModel: viewWidth,
|
|
143
|
+
heightModel: viewHeight,
|
|
144
|
+
transform: {
|
|
145
|
+
viewMinX: minX,
|
|
146
|
+
viewMaxX: maxX,
|
|
147
|
+
viewMinZ: minY,
|
|
148
|
+
viewMaxZ: maxY,
|
|
149
|
+
viewWidth,
|
|
150
|
+
viewHeight
|
|
151
|
+
},
|
|
152
|
+
geometryTransform,
|
|
153
|
+
rawPaths
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Build `options.productEntries` for renderDrawing from SlabSection
|
|
158
|
+
* per-product attribution: each product's cut polylines in world x/z. Labels,
|
|
159
|
+
* legend, clip and highlight items are derived from these entries — the
|
|
160
|
+
* polylines must be in WORLD coordinates (use a zero lateral section origin so
|
|
161
|
+
* the projected space equals world x/z).
|
|
162
|
+
*/
|
|
163
|
+
export function slabPerProductToEntries(perProduct) {
|
|
164
|
+
return perProduct
|
|
165
|
+
.map((entry) => ({
|
|
166
|
+
product: entry.product,
|
|
167
|
+
polylines: (entry.cutLines ?? [])
|
|
168
|
+
.filter((poly) => poly.length > 0)
|
|
169
|
+
.map((poly) => ({
|
|
170
|
+
points: poly.map((pt) => ({ x: pt[0], z: pt[2] }))
|
|
171
|
+
}))
|
|
172
|
+
}))
|
|
173
|
+
.filter((e) => e.polylines.length > 0);
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=slabModelView.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slabModelView.js","sourceRoot":"/","sources":["layout/slabModelView.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAS3C,oFAAoF;AACpF,0EAA0E;AAC1E,MAAM,UAAU,GAAmB;IAC/B,aAAa;IACb,eAAe;IACf,mBAAmB;IACnB,cAAc;IACd,gBAAgB;IAChB,oBAAoB;IACpB,cAAc;IACd,UAAU;IACV,aAAa;CAChB,CAAC;AAEF,SAAS,YAAY,CAAC,QAAsB,EAAE,UAA+B;IACzE,OAAO,wBAAwB,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;AAC1D,CAAC;AAED,SAAS,eAAe,CAAC,IAAY,EAAE,KAAiB;IACpD,MAAM,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EACtB,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EACpB,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACzB,MAAM,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EACrB,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,EACnB,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IACxB,MAAM,EAAE,GAAG,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,EAClB,EAAE,GAAG,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,EAChB,EAAE,GAAG,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACrB,MAAM,GAAG,GAAU,EAAE,CAAC;IACtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACnC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,EAChB,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,EACd,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;QACnB,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC;IACjF,CAAC;IACD,OAAO,GAAG,CAAC;AACf,CAAC;AAED,SAAS,GAAG,CAAC,CAAS;IAClB,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;AAC9C,CAAC;AAED,SAAS,UAAU,CAAC,SAAkB;IAClC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;QAC3B,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAChC,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACpD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACnC,KAAK,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QACxD,CAAC;IACL,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC3B,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,sBAAsB,CAClC,QAAsE,EACtE,UAAsB,EACtB,UAA+B;IAE/B,MAAM,SAAS,GAA4B,EAAE,CAAC;IAC9C,IAAI,IAAI,GAAG,QAAQ,EACf,IAAI,GAAG,CAAC,QAAQ,CAAC;IACrB,IAAI,IAAI,GAAG,QAAQ,EACf,IAAI,GAAG,CAAC,QAAQ,CAAC;IAErB,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC5B,MAAM,MAAM,GAAY,EAAE,CAAC;QAC3B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACvB,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS;YAChC,MAAM,GAAG,GAAG,eAAe,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;YAC9C,KAAK,MAAM,EAAE,IAAI,GAAG,EAAE,CAAC;gBACnB,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI;oBAAE,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC;gBAC7B,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI;oBAAE,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC;gBAC7B,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI;oBAAE,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC;gBAC7B,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI;oBAAE,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC;YACjC,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACrB,CAAC;QACD,SAAS,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC;IAC5B,CAAC;IAED,MAAM,SAAS,GAAG,IAAI,GAAG,IAAI,CAAC;IAC9B,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,CAAC;IAC/B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,SAAS,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,UAAU,IAAI,IAAI,EAAE,CAAC;QACzG,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,gFAAgF;IAChF,MAAM,iBAAiB,GAAG,mBAAmB,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC;IAExE,MAAM,QAAQ,GAAa,CAAC,iBAAiB,UAAU,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;IAChF,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC3B,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;QAC5C,IAAI,KAAK,CAAC,OAAO,KAAK,KAAK;YAAE,SAAS;QACtC,MAAM,CAAC,GAAG,UAAU,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;QACrC,IAAI,CAAC,CAAC;YAAE,SAAS;QACjB,IAAI,MAAM,GAAG,YAAY,CAAC,yBAAyB,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,mBAAmB,KAAK,CAAC,OAAO,GAAG,CAAC;QAC9G,IAAI,KAAK,CAAC,IAAI;YAAE,MAAM,IAAI,sBAAsB,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC;QAC1E,MAAM,IAAI,mDAAmD,CAAC;QAC9D,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC1B,CAAC;IACD,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAEtB,yEAAyE;IACzE,kEAAkE;IAClE,IAAI,QAAmC,CAAC;IACxC,IAAI,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC;QAC9B,QAAQ,GAAG,EAAE,CAAC;QACd,KAAK,MAAM,KAAK,IAAI,QAAQ,CAAC,UAAU,EAAE,CAAC;YACtC,MAAM,GAAG,GAAI,KAAK,CAAC,OAAwC,EAAE,GAAG,CAAC;YACjE,IAAI,CAAC,GAAG;gBAAE,SAAS;YACnB,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;gBAC3B,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;gBAC5C,IAAI,KAAK,CAAC,OAAO,KAAK,KAAK;oBAAE,SAAS;gBACtC,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;gBACzB,IAAI,CAAC,KAAK,EAAE,MAAM;oBAAE,SAAS;gBAC7B,MAAM,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,eAAe,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;gBACnG,IAAI,CAAC,CAAC;oBAAE,SAAS;gBACjB,QAAQ,CAAC,IAAI,CAAC;oBACV,GAAG,EAAE,GAAG,GAAG,IAAI,GAAG,EAAE;oBACpB,CAAC;oBACD,IAAI,EAAE,IAAI;oBACV,WAAW,EAAE,CAAC;oBACd,MAAM,EAAE,KAAK,CAAC,KAAK;oBACnB,WAAW,EAAE,KAAK,CAAC,OAAO;oBAC1B,IAAI,EAAE,KAAK,CAAC,IAAI,IAAI,SAAS;oBAC7B,IAAI,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE;iBAChD,CAAC,CAAC;YACP,CAAC;QACL,CAAC;QACD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;YAAE,QAAQ,GAAG,SAAS,CAAC;IACpD,CAAC;IAED,OAAO;QACH,QAAQ,EAAE,MAAM,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,MAAM;QACvC,UAAU,EAAE,SAAS;QACrB,WAAW,EAAE,UAAU;QACvB,SAAS,EAAE;YACP,QAAQ,EAAE,IAAI;YACd,QAAQ,EAAE,IAAI;YACd,QAAQ,EAAE,IAAI;YACd,QAAQ,EAAE,IAAI;YACd,SAAS;YACT,UAAU;SACb;QACD,iBAAiB;QACjB,QAAQ;KACX,CAAC;AACN,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,uBAAuB,CAAC,UAAiC;IACrE,OAAO,UAAU;SACZ,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACb,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,SAAS,EAAE,CAAC,KAAK,CAAC,QAAQ,IAAI,EAAE,CAAC;aAC5B,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;aACjC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;YACZ,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;SACrD,CAAC,CAAC;KACV,CAAC,CAAC;SACF,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AAC/C,CAAC","sourcesContent":["import type { Vec3, SlabSectionGeometry, PlaneBasis, SectionPath } from '@twinfinity/geometry';\nimport type { SvgModelView, ViewportLineStyles, ViewportLineStyle, ProductPolylineEntry } from './types';\nimport { resolveCategoryLineStyle } from './renderDrawing';\nimport { escapeAttr } from './renderUtils';\n\n/** Per-product line attribution (SlabSection with `trackProducts: true`).\n * Structurally typed — any `{ product, cutLines?, visibleLines?, … }` works. */\nexport type SlabPerProductEntry = { product: unknown } & { [K in keyof SlabSectionGeometry]?: Vec3[][] };\n\ntype Pt2 = { x: number; y: number };\ntype LineCategory = keyof SlabSectionGeometry;\n\n// Paint order (SVG paints later paths on top): hidden categories first, so occluded\n// linework can never cover visible geometry — same order printToSvg uses.\nconst CATEGORIES: LineCategory[] = [\n 'hiddenLines',\n 'outlineHidden',\n 'silhouettesHidden',\n 'visibleLines',\n 'outlineVisible',\n 'silhouettesVisible',\n 'featureEdges',\n 'cutLines',\n 'stairArrows'\n];\n\nfunction resolveStyle(category: LineCategory, lineStyles?: ViewportLineStyles): Required<ViewportLineStyle> {\n return resolveCategoryLineStyle(category, lineStyles);\n}\n\nfunction projectPolyline(poly: Vec3[], basis: PlaneBasis): Pt2[] {\n const ox = basis.origin[0],\n oy = basis.origin[1],\n oz = basis.origin[2];\n const rx = basis.right[0],\n ry = basis.right[1],\n rz = basis.right[2];\n const ux = basis.up[0],\n uy = basis.up[1],\n uz = basis.up[2];\n const pts: Pt2[] = [];\n for (let i = 0; i < poly.length; i++) {\n const p = poly[i];\n const dx = p[0] - ox,\n dy = p[1] - oy,\n dz = p[2] - oz;\n pts.push({ x: dx * rx + dy * ry + dz * rz, y: dx * ux + dy * uy + dz * uz });\n }\n return pts;\n}\n\nfunction fmt(v: number): string {\n return v.toFixed(6).replace(/\\.?0+$/, '');\n}\n\nfunction buildPathD(projected: Pt2[][]): string {\n const parts: string[] = [];\n for (const poly of projected) {\n if (poly.length === 0) continue;\n parts.push(`M ${fmt(poly[0].x)} ${fmt(poly[0].y)}`);\n for (let i = 1; i < poly.length; i++) {\n parts.push(`L ${fmt(poly[i].x)} ${fmt(poly[i].y)}`);\n }\n }\n return parts.join(' ');\n}\n\n/**\n * Convert SlabSection geometry into an SvgModelView for the print-layout engine\n * (`renderDrawing({ models })`). Returns null when the geometry is empty or\n * degenerate. Use a (0,0,1) up hint when building the plane basis for standard\n * north-up floor plans.\n *\n * When the geometry carries `perProduct` attribution (SlabSection option\n * `trackProducts: true`), the model also gets `rawPaths` with per-product gid\n * metadata — required for the renderer's gid-based features (highlight, clip\n * include/exclude, render-time includeClasses): without rawPaths,\n * filterModelForViewport returns the model untouched and those settings\n * silently do nothing.\n */\nexport function slabSectionToModelView(\n geometry: SlabSectionGeometry & { perProduct?: SlabPerProductEntry[] },\n planeBasis: PlaneBasis,\n lineStyles?: ViewportLineStyles\n): SvgModelView | null {\n const projected: Record<string, Pt2[][]> = {};\n let minX = Infinity,\n maxX = -Infinity;\n let minY = Infinity,\n maxY = -Infinity;\n\n for (const cat of CATEGORIES) {\n const polys = geometry[cat];\n const result: Pt2[][] = [];\n for (const poly of polys) {\n if (poly.length === 0) continue;\n const pts = projectPolyline(poly, planeBasis);\n for (const pt of pts) {\n if (pt.x < minX) minX = pt.x;\n if (pt.x > maxX) maxX = pt.x;\n if (pt.y < minY) minY = pt.y;\n if (pt.y > maxY) maxY = pt.y;\n }\n result.push(pts);\n }\n projected[cat] = result;\n }\n\n const viewWidth = maxX - minX;\n const viewHeight = maxY - minY;\n if (!Number.isFinite(viewWidth) || viewWidth <= 1e-6 || !Number.isFinite(viewHeight) || viewHeight <= 1e-6) {\n return null;\n }\n\n // Map projected (x,y) to SVG coords: SVG_x = x - minX, SVG_y = maxY - y (flip).\n const geometryTransform = `matrix(1 0 0 -1 ${fmt(-minX)} ${fmt(maxY)})`;\n\n const svgParts: string[] = [`<g transform=\"${escapeAttr(geometryTransform)}\">`];\n for (const cat of CATEGORIES) {\n const style = resolveStyle(cat, lineStyles);\n if (style.enabled === false) continue;\n const d = buildPathD(projected[cat]);\n if (!d) continue;\n let pathEl = `<path d=\"${d}\" fill=\"none\" stroke=\"${escapeAttr(style.color)}\" stroke-width=\"${style.widthMm}\"`;\n if (style.dash) pathEl += ` stroke-dasharray=\"${escapeAttr(style.dash)}\"`;\n pathEl += ` stroke-linejoin=\"round\" stroke-linecap=\"round\"/>`;\n svgParts.push(pathEl);\n }\n svgParts.push('</g>');\n\n // Per-product rawPaths (same projection + styles as the merged groupSvg)\n // so gid-based features can filter/restyle and rebuild the model.\n let rawPaths: SectionPath[] | undefined;\n if (geometry.perProduct?.length) {\n rawPaths = [];\n for (const entry of geometry.perProduct) {\n const gid = (entry.product as { gid?: string } | undefined)?.gid;\n if (!gid) continue;\n for (const cat of CATEGORIES) {\n const style = resolveStyle(cat, lineStyles);\n if (style.enabled === false) continue;\n const polys = entry[cat];\n if (!polys?.length) continue;\n const d = buildPathD(polys.filter((p) => p.length > 0).map((p) => projectPolyline(p, planeBasis)));\n if (!d) continue;\n rawPaths.push({\n key: `${gid}:${cat}`,\n d,\n fill: null,\n fillOpacity: 1,\n stroke: style.color,\n strokeWidth: style.widthMm,\n dash: style.dash || undefined,\n meta: { gid, isSegment: true, category: cat }\n });\n }\n }\n if (rawPaths.length === 0) rawPaths = undefined;\n }\n\n return {\n groupSvg: `<g>${svgParts.join('')}</g>`,\n widthModel: viewWidth,\n heightModel: viewHeight,\n transform: {\n viewMinX: minX,\n viewMaxX: maxX,\n viewMinZ: minY,\n viewMaxZ: maxY,\n viewWidth,\n viewHeight\n },\n geometryTransform,\n rawPaths\n };\n}\n\n/**\n * Build `options.productEntries` for renderDrawing from SlabSection\n * per-product attribution: each product's cut polylines in world x/z. Labels,\n * legend, clip and highlight items are derived from these entries — the\n * polylines must be in WORLD coordinates (use a zero lateral section origin so\n * the projected space equals world x/z).\n */\nexport function slabPerProductToEntries(perProduct: SlabPerProductEntry[]): ProductPolylineEntry[] {\n return perProduct\n .map((entry) => ({\n product: entry.product,\n polylines: (entry.cutLines ?? [])\n .filter((poly) => poly.length > 0)\n .map((poly) => ({\n points: poly.map((pt) => ({ x: pt[0], z: pt[2] }))\n }))\n }))\n .filter((e) => e.polylines.length > 0);\n}\n"]}
|
|
@@ -133,5 +133,5 @@ export declare function resolveTemplateValues(template: LayoutTemplate, userValu
|
|
|
133
133
|
* // }
|
|
134
134
|
* ```
|
|
135
135
|
*/
|
|
136
|
-
export declare function applyTemplateVariables(template: LayoutTemplate, values: Record<string, any
|
|
136
|
+
export declare function applyTemplateVariables(template: LayoutTemplate, values: Record<string, any>, warnings?: string[]): LayoutTemplate;
|
|
137
137
|
//# sourceMappingURL=templateVariables.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templateVariables.d.ts","sourceRoot":"/","sources":["layout/templateVariables.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAuB,oBAAoB,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"templateVariables.d.ts","sourceRoot":"/","sources":["layout/templateVariables.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAuB,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAGzF;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,cAAc,GAAG,oBAAoB,EAAE,CAmBrF;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAQjF;AAkED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAgB,qBAAqB,CACjC,QAAQ,EAAE,cAAc,EACxB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GACjC;IAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAkCrD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,sBAAsB,CAClC,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,QAAQ,CAAC,EAAE,MAAM,EAAE,GACpB,cAAc,CAuChB"}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* @module templateVariables
|
|
11
11
|
*/
|
|
12
|
+
import { applyPlaceholderFilter } from './renderUtils';
|
|
12
13
|
/**
|
|
13
14
|
* Extract all variable definitions from a template.
|
|
14
15
|
*
|
|
@@ -244,13 +245,23 @@ export function resolveTemplateValues(template, userValues) {
|
|
|
244
245
|
* // }
|
|
245
246
|
* ```
|
|
246
247
|
*/
|
|
247
|
-
export function applyTemplateVariables(template, values) {
|
|
248
|
+
export function applyTemplateVariables(template, values, warnings) {
|
|
248
249
|
// Deep clone to avoid mutation
|
|
249
250
|
const resolved = JSON.parse(JSON.stringify(template));
|
|
250
|
-
// Recursively replace {{vars.*}} in all string values
|
|
251
|
+
// Recursively replace {{vars.*}} in all string values.
|
|
252
|
+
// A value that is EXACTLY one placeholder (no surrounding text, no
|
|
253
|
+
// format filter) substitutes the variable's RAW value, preserving its
|
|
254
|
+
// type — this is what lets number variables drive numeric template
|
|
255
|
+
// fields like a viewport's cutHeight ("cutHeight": "{{vars.cutHeight}}").
|
|
256
|
+
// Mixed strings keep normal stringified interpolation.
|
|
257
|
+
const WHOLE_VALUE_RE = /^\{\{\s*vars\.(\w+)\s*\}\}$/;
|
|
251
258
|
function replaceInValue(obj) {
|
|
252
259
|
if (typeof obj === 'string') {
|
|
253
|
-
|
|
260
|
+
const whole = WHOLE_VALUE_RE.exec(obj);
|
|
261
|
+
if (whole && whole[1] in values && typeof values[whole[1]] !== 'string') {
|
|
262
|
+
return values[whole[1]];
|
|
263
|
+
}
|
|
264
|
+
return interpolateVariables(obj, values, warnings);
|
|
254
265
|
}
|
|
255
266
|
if (Array.isArray(obj)) {
|
|
256
267
|
return obj.map(replaceInValue);
|
|
@@ -266,8 +277,8 @@ export function applyTemplateVariables(template, values) {
|
|
|
266
277
|
}
|
|
267
278
|
// Apply to all template properties that might contain variable references
|
|
268
279
|
resolved.elements = replaceInValue(resolved.elements);
|
|
269
|
-
if (resolved.
|
|
270
|
-
resolved.
|
|
280
|
+
if (resolved.resources) {
|
|
281
|
+
resolved.resources = replaceInValue(resolved.resources);
|
|
271
282
|
}
|
|
272
283
|
return resolved;
|
|
273
284
|
}
|
|
@@ -281,14 +292,15 @@ export function applyTemplateVariables(template, values) {
|
|
|
281
292
|
* @param values - Variable values
|
|
282
293
|
* @returns String with all {{vars.*}} replaced
|
|
283
294
|
*/
|
|
284
|
-
function interpolateVariables(str, values) {
|
|
285
|
-
return str.replace(/\{\{vars\.(\w+)\}\}/g, (match, varName) => {
|
|
295
|
+
function interpolateVariables(str, values, warnings) {
|
|
296
|
+
return str.replace(/\{\{\s*vars\.(\w+)\s*(\|[^}]*)?\}\}/g, (match, varName, filterPart) => {
|
|
286
297
|
if (varName in values) {
|
|
287
298
|
const value = values[varName];
|
|
299
|
+
const filter = filterPart ? String(filterPart).slice(1).trim() : null;
|
|
288
300
|
// Convert to string (handles numbers, booleans, etc.)
|
|
289
|
-
return String(value);
|
|
301
|
+
return String(applyPlaceholderFilter(value, filter, warnings, str));
|
|
290
302
|
}
|
|
291
|
-
|
|
303
|
+
warnings?.push(`Variable '${varName}' has no value — placeholder kept.`);
|
|
292
304
|
return match; // Keep {{vars.varName}} as-is
|
|
293
305
|
});
|
|
294
306
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templateVariables.js","sourceRoot":"/","sources":["layout/templateVariables.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Template variable extraction, validation, and resolution.\n *\n * This module provides utilities for working with parameterized templates:\n * - Extract variable definitions for UI generation\n * - Get default values (fail-safe fallback)\n * - Resolve user-provided values with validation\n * - Apply variables to template (substitute {{vars.*}} references)\n *\n * @module templateVariables\n */\n\nimport type { LayoutTemplate, TemplateVariableDef, TemplateVariableInfo } from './types';\n\n/**\n * Extract all variable definitions from a template.\n *\n * Returns an array of variable info objects suitable for UI generation.\n * Returns empty array if template has no variables.\n *\n * @param template - Layout template (may or may not have variables)\n * @returns Array of variable definitions with metadata\n *\n * @example\n * ```typescript\n * const vars = getTemplateVariables(template);\n * // [\n * // { name: 'hyresobjektsnummer', type: 'string', default: '2022-F2008', label: 'Hyresobjekt', ... },\n * // { name: 'showLegend', type: 'boolean', default: true, label: 'Show Legend', ... }\n * // ]\n *\n * // Use for UI generation\n * vars.forEach(v => {\n * if (v.type === 'string') {\n * renderTextField(v.label, v.default);\n * } else if (v.type === 'boolean') {\n * renderCheckbox(v.label, v.default);\n * }\n * });\n * ```\n */\nexport function getTemplateVariables(template: LayoutTemplate): TemplateVariableInfo[] {\n if (!template.variables) return [];\n\n return Object.entries(template.variables).map(([name, def]) => ({\n name,\n type: def.type,\n default: def.default,\n label: def.label ?? name,\n description: def.description,\n format: def.format,\n min: def.min,\n max: def.max,\n step: def.step,\n minLength: def.minLength,\n maxLength: def.maxLength,\n pattern: def.pattern,\n enum: def.enum,\n items: def.items\n }));\n}\n\n/**\n * Extract all default values from a template.\n *\n * Returns an object with all variable names mapped to their default values.\n * These defaults are guaranteed to work - template can always render with them.\n *\n * Returns empty object if template has no variables.\n *\n * @param template - Layout template\n * @returns Object mapping variable names to default values\n *\n * @example\n * ```typescript\n * const defaults = getTemplateDefaults(template);\n * // { hyresobjektsnummer: '2022-F2008', showLegend: true, scaleDenom: 100 }\n *\n * // Can always render with defaults (fail-safe)\n * const { svg } = await renderDrawing({\n * template,\n * models,\n * variables: defaults // Guaranteed to work\n * });\n * ```\n */\nexport function getTemplateDefaults(template: LayoutTemplate): Record<string, any> {\n if (!template.variables) return {};\n\n const defaults: Record<string, any> = {};\n for (const [name, def] of Object.entries(template.variables)) {\n defaults[name] = def.default;\n }\n return defaults;\n}\n\n/**\n * Validate a value against a variable definition.\n *\n * Checks type and constraint validation (min/max, pattern, enum, etc.).\n *\n * @param value - Value to validate\n * @param varDef - Variable definition with constraints\n * @returns Validation result with ok flag and optional error message\n */\nfunction validateVariable(value: any, varDef: TemplateVariableDef): { ok: boolean; error?: string } {\n // Type check\n const actualType = Array.isArray(value) ? 'array' : typeof value;\n if (varDef.type === 'number' && actualType !== 'number') {\n return { ok: false, error: `Expected number, got ${actualType}` };\n }\n if (varDef.type === 'string' && actualType !== 'string') {\n return { ok: false, error: `Expected string, got ${actualType}` };\n }\n if (varDef.type === 'boolean' && actualType !== 'boolean') {\n return { ok: false, error: `Expected boolean, got ${actualType}` };\n }\n if (varDef.type === 'array' && actualType !== 'array') {\n return { ok: false, error: `Expected array, got ${actualType}` };\n }\n\n // String validation\n if (varDef.type === 'string' && typeof value === 'string') {\n if (varDef.minLength !== undefined && value.length < varDef.minLength) {\n return { ok: false, error: `String too short (min ${varDef.minLength})` };\n }\n if (varDef.maxLength !== undefined && value.length > varDef.maxLength) {\n return { ok: false, error: `String too long (max ${varDef.maxLength})` };\n }\n if (varDef.pattern) {\n try {\n const regex = new RegExp(varDef.pattern);\n if (!regex.test(value)) {\n return { ok: false, error: `Does not match pattern ${varDef.pattern}` };\n }\n } catch {\n return { ok: false, error: `Invalid pattern: ${varDef.pattern}` };\n }\n }\n if (varDef.enum && !varDef.enum.includes(value)) {\n return { ok: false, error: `Not in allowed values: ${varDef.enum.join(', ')}` };\n }\n }\n\n // Number validation\n if (varDef.type === 'number' && typeof value === 'number') {\n if (!Number.isFinite(value)) {\n return { ok: false, error: 'Must be a finite number' };\n }\n if (varDef.min !== undefined && value < varDef.min) {\n return { ok: false, error: `Too small (min ${varDef.min})` };\n }\n if (varDef.max !== undefined && value > varDef.max) {\n return { ok: false, error: `Too large (max ${varDef.max})` };\n }\n }\n\n return { ok: true };\n}\n\n/**\n * Resolve template variables by merging user values with defaults.\n *\n * This is a **fail-safe** function:\n * - Missing values → use default\n * - Invalid values → use default + warn\n * - Unknown variables → warn + ignore\n *\n * The result is ALWAYS valid and complete (safe to use for rendering).\n *\n * @param template - Layout template with variable definitions\n * @param userValues - Optional user-provided values (can be partial or undefined)\n * @returns Object with resolved values and any validation warnings\n *\n * @example\n * ```typescript\n * // User provides partial values\n * const { values, warnings } = resolveTemplateValues(template, {\n * hyresobjektsnummer: '2022-F2009' // Valid\n * // Other vars use defaults\n * });\n * // values = { hyresobjektsnummer: '2022-F2009', showLegend: true, scaleDenom: 100 }\n * // warnings = []\n *\n * // User provides invalid value\n * const { values, warnings } = resolveTemplateValues(template, {\n * scaleDenom: 9999 // Out of range (max: 500)\n * });\n * // values = { scaleDenom: 100, ... } ← Fell back to default\n * // warnings = [\"Invalid 'scaleDenom': Too large (max 500) - using default '100'\"]\n *\n * // Always safe to render\n * const resolvedTemplate = applyTemplateVariables(template, values);\n * ```\n */\nexport function resolveTemplateValues(\n template: LayoutTemplate,\n userValues?: Record<string, any>\n): { values: Record<string, any>; warnings: string[] } {\n const defaults = getTemplateDefaults(template);\n const warnings: string[] = [];\n\n // Start with all defaults (fail-safe!)\n const values = { ...defaults };\n\n // No user values or no variables → return defaults\n if (!userValues || !template.variables) {\n return { values, warnings };\n }\n\n // Merge and validate user values\n for (const [name, value] of Object.entries(userValues)) {\n const varDef = template.variables[name];\n\n // Unknown variable\n if (!varDef) {\n warnings.push(`Unknown variable '${name}' - ignoring`);\n continue;\n }\n\n // Validate value\n const validation = validateVariable(value, varDef);\n if (!validation.ok) {\n warnings.push(`Invalid '${name}': ${validation.error} - using default '${defaults[name]}'`);\n continue;\n }\n\n // Valid → use it\n values[name] = value;\n }\n\n return { values, warnings };\n}\n\n/**\n * Apply variables to a template by substituting {{vars.*}} references.\n *\n * Recursively scans the template and replaces all {{vars.variableName}} references\n * with the corresponding values from the provided values object.\n *\n * Returns a new template with all variables resolved (does not mutate input).\n *\n * @param template - Original template with {{vars.*}} references\n * @param values - Resolved variable values (from resolveTemplateValues)\n * @returns New template with all {{vars.*}} replaced by actual values\n *\n * @example\n * ```typescript\n * const template = {\n * elements: [\n * { kind: 'text', text: 'Hyresobjekt: {{vars.hyresobjektsnummer}}' },\n * { kind: 'viewport', clip: { where: { 'BIP.id': '{{vars.hyresobjektsnummer}}' } } }\n * ]\n * };\n *\n * const values = { hyresobjektsnummer: '2022-F2009' };\n * const resolved = applyTemplateVariables(template, values);\n * // {\n * // elements: [\n * // { kind: 'text', text: 'Hyresobjekt: 2022-F2009' },\n * // { kind: 'viewport', clip: { where: { 'BIP.id': '2022-F2009' } } }\n * // ]\n * // }\n * ```\n */\nexport function applyTemplateVariables(template: LayoutTemplate, values: Record<string, any>): LayoutTemplate {\n // Deep clone to avoid mutation\n const resolved: LayoutTemplate = JSON.parse(JSON.stringify(template));\n\n // Recursively replace {{vars.*}} in all string values\n function replaceInValue(obj: any): any {\n if (typeof obj === 'string') {\n return interpolateVariables(obj, values);\n }\n if (Array.isArray(obj)) {\n return obj.map(replaceInValue);\n }\n if (obj && typeof obj === 'object') {\n const result: any = {};\n for (const [key, val] of Object.entries(obj)) {\n result[key] = replaceInValue(val);\n }\n return result;\n }\n return obj;\n }\n\n // Apply to all template properties that might contain variable references\n resolved.elements = replaceInValue(resolved.elements);\n if (resolved.assets) {\n resolved.assets = replaceInValue(resolved.assets);\n }\n\n return resolved;\n}\n\n/**\n * Interpolate {{vars.*}} references in a string.\n *\n * Replaces all occurrences of {{vars.variableName}} with the corresponding value.\n * If variable is not found, keeps the placeholder and logs a warning.\n *\n * @param str - String potentially containing {{vars.*}} references\n * @param values - Variable values\n * @returns String with all {{vars.*}} replaced\n */\nfunction interpolateVariables(str: string, values: Record<string, any>): string {\n return str.replace(/\\{\\{vars\\.(\\w+)\\}\\}/g, (match, varName) => {\n if (varName in values) {\n const value = values[varName];\n // Convert to string (handles numbers, booleans, etc.)\n return String(value);\n }\n console.warn(`[templateVariables] Variable '${varName}' not found - keeping placeholder`);\n return match; // Keep {{vars.varName}} as-is\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"templateVariables.js","sourceRoot":"/","sources":["layout/templateVariables.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Template variable extraction, validation, and resolution.\n *\n * This module provides utilities for working with parameterized templates:\n * - Extract variable definitions for UI generation\n * - Get default values (fail-safe fallback)\n * - Resolve user-provided values with validation\n * - Apply variables to template (substitute {{vars.*}} references)\n *\n * @module templateVariables\n */\n\nimport type { LayoutTemplate, TemplateVariableDef, TemplateVariableInfo } from './types';\nimport { applyPlaceholderFilter } from './renderUtils';\n\n/**\n * Extract all variable definitions from a template.\n *\n * Returns an array of variable info objects suitable for UI generation.\n * Returns empty array if template has no variables.\n *\n * @param template - Layout template (may or may not have variables)\n * @returns Array of variable definitions with metadata\n *\n * @example\n * ```typescript\n * const vars = getTemplateVariables(template);\n * // [\n * // { name: 'hyresobjektsnummer', type: 'string', default: '2022-F2008', label: 'Hyresobjekt', ... },\n * // { name: 'showLegend', type: 'boolean', default: true, label: 'Show Legend', ... }\n * // ]\n *\n * // Use for UI generation\n * vars.forEach(v => {\n * if (v.type === 'string') {\n * renderTextField(v.label, v.default);\n * } else if (v.type === 'boolean') {\n * renderCheckbox(v.label, v.default);\n * }\n * });\n * ```\n */\nexport function getTemplateVariables(template: LayoutTemplate): TemplateVariableInfo[] {\n if (!template.variables) return [];\n\n return Object.entries(template.variables).map(([name, def]) => ({\n name,\n type: def.type,\n default: def.default,\n label: def.label ?? name,\n description: def.description,\n format: def.format,\n min: def.min,\n max: def.max,\n step: def.step,\n minLength: def.minLength,\n maxLength: def.maxLength,\n pattern: def.pattern,\n enum: def.enum,\n items: def.items\n }));\n}\n\n/**\n * Extract all default values from a template.\n *\n * Returns an object with all variable names mapped to their default values.\n * These defaults are guaranteed to work - template can always render with them.\n *\n * Returns empty object if template has no variables.\n *\n * @param template - Layout template\n * @returns Object mapping variable names to default values\n *\n * @example\n * ```typescript\n * const defaults = getTemplateDefaults(template);\n * // { hyresobjektsnummer: '2022-F2008', showLegend: true, scaleDenom: 100 }\n *\n * // Can always render with defaults (fail-safe)\n * const { svg } = await renderDrawing({\n * template,\n * models,\n * variables: defaults // Guaranteed to work\n * });\n * ```\n */\nexport function getTemplateDefaults(template: LayoutTemplate): Record<string, any> {\n if (!template.variables) return {};\n\n const defaults: Record<string, any> = {};\n for (const [name, def] of Object.entries(template.variables)) {\n defaults[name] = def.default;\n }\n return defaults;\n}\n\n/**\n * Validate a value against a variable definition.\n *\n * Checks type and constraint validation (min/max, pattern, enum, etc.).\n *\n * @param value - Value to validate\n * @param varDef - Variable definition with constraints\n * @returns Validation result with ok flag and optional error message\n */\nfunction validateVariable(value: any, varDef: TemplateVariableDef): { ok: boolean; error?: string } {\n // Type check\n const actualType = Array.isArray(value) ? 'array' : typeof value;\n if (varDef.type === 'number' && actualType !== 'number') {\n return { ok: false, error: `Expected number, got ${actualType}` };\n }\n if (varDef.type === 'string' && actualType !== 'string') {\n return { ok: false, error: `Expected string, got ${actualType}` };\n }\n if (varDef.type === 'boolean' && actualType !== 'boolean') {\n return { ok: false, error: `Expected boolean, got ${actualType}` };\n }\n if (varDef.type === 'array' && actualType !== 'array') {\n return { ok: false, error: `Expected array, got ${actualType}` };\n }\n\n // String validation\n if (varDef.type === 'string' && typeof value === 'string') {\n if (varDef.minLength !== undefined && value.length < varDef.minLength) {\n return { ok: false, error: `String too short (min ${varDef.minLength})` };\n }\n if (varDef.maxLength !== undefined && value.length > varDef.maxLength) {\n return { ok: false, error: `String too long (max ${varDef.maxLength})` };\n }\n if (varDef.pattern) {\n try {\n const regex = new RegExp(varDef.pattern);\n if (!regex.test(value)) {\n return { ok: false, error: `Does not match pattern ${varDef.pattern}` };\n }\n } catch {\n return { ok: false, error: `Invalid pattern: ${varDef.pattern}` };\n }\n }\n if (varDef.enum && !varDef.enum.includes(value)) {\n return { ok: false, error: `Not in allowed values: ${varDef.enum.join(', ')}` };\n }\n }\n\n // Number validation\n if (varDef.type === 'number' && typeof value === 'number') {\n if (!Number.isFinite(value)) {\n return { ok: false, error: 'Must be a finite number' };\n }\n if (varDef.min !== undefined && value < varDef.min) {\n return { ok: false, error: `Too small (min ${varDef.min})` };\n }\n if (varDef.max !== undefined && value > varDef.max) {\n return { ok: false, error: `Too large (max ${varDef.max})` };\n }\n }\n\n return { ok: true };\n}\n\n/**\n * Resolve template variables by merging user values with defaults.\n *\n * This is a **fail-safe** function:\n * - Missing values → use default\n * - Invalid values → use default + warn\n * - Unknown variables → warn + ignore\n *\n * The result is ALWAYS valid and complete (safe to use for rendering).\n *\n * @param template - Layout template with variable definitions\n * @param userValues - Optional user-provided values (can be partial or undefined)\n * @returns Object with resolved values and any validation warnings\n *\n * @example\n * ```typescript\n * // User provides partial values\n * const { values, warnings } = resolveTemplateValues(template, {\n * hyresobjektsnummer: '2022-F2009' // Valid\n * // Other vars use defaults\n * });\n * // values = { hyresobjektsnummer: '2022-F2009', showLegend: true, scaleDenom: 100 }\n * // warnings = []\n *\n * // User provides invalid value\n * const { values, warnings } = resolveTemplateValues(template, {\n * scaleDenom: 9999 // Out of range (max: 500)\n * });\n * // values = { scaleDenom: 100, ... } ← Fell back to default\n * // warnings = [\"Invalid 'scaleDenom': Too large (max 500) - using default '100'\"]\n *\n * // Always safe to render\n * const resolvedTemplate = applyTemplateVariables(template, values);\n * ```\n */\nexport function resolveTemplateValues(\n template: LayoutTemplate,\n userValues?: Record<string, any>\n): { values: Record<string, any>; warnings: string[] } {\n const defaults = getTemplateDefaults(template);\n const warnings: string[] = [];\n\n // Start with all defaults (fail-safe!)\n const values = { ...defaults };\n\n // No user values or no variables → return defaults\n if (!userValues || !template.variables) {\n return { values, warnings };\n }\n\n // Merge and validate user values\n for (const [name, value] of Object.entries(userValues)) {\n const varDef = template.variables[name];\n\n // Unknown variable\n if (!varDef) {\n warnings.push(`Unknown variable '${name}' - ignoring`);\n continue;\n }\n\n // Validate value\n const validation = validateVariable(value, varDef);\n if (!validation.ok) {\n warnings.push(`Invalid '${name}': ${validation.error} - using default '${defaults[name]}'`);\n continue;\n }\n\n // Valid → use it\n values[name] = value;\n }\n\n return { values, warnings };\n}\n\n/**\n * Apply variables to a template by substituting {{vars.*}} references.\n *\n * Recursively scans the template and replaces all {{vars.variableName}} references\n * with the corresponding values from the provided values object.\n *\n * Returns a new template with all variables resolved (does not mutate input).\n *\n * @param template - Original template with {{vars.*}} references\n * @param values - Resolved variable values (from resolveTemplateValues)\n * @returns New template with all {{vars.*}} replaced by actual values\n *\n * @example\n * ```typescript\n * const template = {\n * elements: [\n * { kind: 'text', text: 'Hyresobjekt: {{vars.hyresobjektsnummer}}' },\n * { kind: 'viewport', clip: { where: { 'BIP.id': '{{vars.hyresobjektsnummer}}' } } }\n * ]\n * };\n *\n * const values = { hyresobjektsnummer: '2022-F2009' };\n * const resolved = applyTemplateVariables(template, values);\n * // {\n * // elements: [\n * // { kind: 'text', text: 'Hyresobjekt: 2022-F2009' },\n * // { kind: 'viewport', clip: { where: { 'BIP.id': '2022-F2009' } } }\n * // ]\n * // }\n * ```\n */\nexport function applyTemplateVariables(\n template: LayoutTemplate,\n values: Record<string, any>,\n warnings?: string[]\n): LayoutTemplate {\n // Deep clone to avoid mutation\n const resolved: LayoutTemplate = JSON.parse(JSON.stringify(template));\n\n // Recursively replace {{vars.*}} in all string values.\n // A value that is EXACTLY one placeholder (no surrounding text, no\n // format filter) substitutes the variable's RAW value, preserving its\n // type — this is what lets number variables drive numeric template\n // fields like a viewport's cutHeight (\"cutHeight\": \"{{vars.cutHeight}}\").\n // Mixed strings keep normal stringified interpolation.\n const WHOLE_VALUE_RE = /^\\{\\{\\s*vars\\.(\\w+)\\s*\\}\\}$/;\n function replaceInValue(obj: any): any {\n if (typeof obj === 'string') {\n const whole = WHOLE_VALUE_RE.exec(obj);\n if (whole && whole[1] in values && typeof values[whole[1]] !== 'string') {\n return values[whole[1]];\n }\n return interpolateVariables(obj, values, warnings);\n }\n if (Array.isArray(obj)) {\n return obj.map(replaceInValue);\n }\n if (obj && typeof obj === 'object') {\n const result: any = {};\n for (const [key, val] of Object.entries(obj)) {\n result[key] = replaceInValue(val);\n }\n return result;\n }\n return obj;\n }\n\n // Apply to all template properties that might contain variable references\n resolved.elements = replaceInValue(resolved.elements);\n if (resolved.resources) {\n resolved.resources = replaceInValue(resolved.resources);\n }\n\n return resolved;\n}\n\n/**\n * Interpolate {{vars.*}} references in a string.\n *\n * Replaces all occurrences of {{vars.variableName}} with the corresponding value.\n * If variable is not found, keeps the placeholder and logs a warning.\n *\n * @param str - String potentially containing {{vars.*}} references\n * @param values - Variable values\n * @returns String with all {{vars.*}} replaced\n */\nfunction interpolateVariables(str: string, values: Record<string, any>, warnings?: string[]): string {\n return str.replace(/\\{\\{\\s*vars\\.(\\w+)\\s*(\\|[^}]*)?\\}\\}/g, (match, varName, filterPart) => {\n if (varName in values) {\n const value = values[varName];\n const filter = filterPart ? String(filterPart).slice(1).trim() : null;\n // Convert to string (handles numbers, booleans, etc.)\n return String(applyPlaceholderFilter(value, filter, warnings, str));\n }\n warnings?.push(`Variable '${varName}' has no value — placeholder kept.`);\n return match; // Keep {{vars.varName}} as-is\n });\n}\n"]}
|