@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,196 @@
|
|
|
1
|
+
import type { Vec3, SlabSectionGeometry, PlaneBasis, SectionPath } from '@twinfinity/geometry';
|
|
2
|
+
import type { SvgModelView, ViewportLineStyles, ViewportLineStyle, ProductPolylineEntry } from './types';
|
|
3
|
+
import { resolveCategoryLineStyle } from './renderDrawing';
|
|
4
|
+
import { escapeAttr } from './renderUtils';
|
|
5
|
+
|
|
6
|
+
/** Per-product line attribution (SlabSection with `trackProducts: true`).
|
|
7
|
+
* Structurally typed — any `{ product, cutLines?, visibleLines?, … }` works. */
|
|
8
|
+
export type SlabPerProductEntry = { product: unknown } & { [K in keyof SlabSectionGeometry]?: Vec3[][] };
|
|
9
|
+
|
|
10
|
+
type Pt2 = { x: number; y: number };
|
|
11
|
+
type LineCategory = keyof SlabSectionGeometry;
|
|
12
|
+
|
|
13
|
+
// Paint order (SVG paints later paths on top): hidden categories first, so occluded
|
|
14
|
+
// linework can never cover visible geometry — same order printToSvg uses.
|
|
15
|
+
const CATEGORIES: LineCategory[] = [
|
|
16
|
+
'hiddenLines',
|
|
17
|
+
'outlineHidden',
|
|
18
|
+
'silhouettesHidden',
|
|
19
|
+
'visibleLines',
|
|
20
|
+
'outlineVisible',
|
|
21
|
+
'silhouettesVisible',
|
|
22
|
+
'featureEdges',
|
|
23
|
+
'cutLines',
|
|
24
|
+
'stairArrows'
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function resolveStyle(category: LineCategory, lineStyles?: ViewportLineStyles): Required<ViewportLineStyle> {
|
|
28
|
+
return resolveCategoryLineStyle(category, lineStyles);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function projectPolyline(poly: Vec3[], basis: PlaneBasis): Pt2[] {
|
|
32
|
+
const ox = basis.origin[0],
|
|
33
|
+
oy = basis.origin[1],
|
|
34
|
+
oz = basis.origin[2];
|
|
35
|
+
const rx = basis.right[0],
|
|
36
|
+
ry = basis.right[1],
|
|
37
|
+
rz = basis.right[2];
|
|
38
|
+
const ux = basis.up[0],
|
|
39
|
+
uy = basis.up[1],
|
|
40
|
+
uz = basis.up[2];
|
|
41
|
+
const pts: Pt2[] = [];
|
|
42
|
+
for (let i = 0; i < poly.length; i++) {
|
|
43
|
+
const p = poly[i];
|
|
44
|
+
const dx = p[0] - ox,
|
|
45
|
+
dy = p[1] - oy,
|
|
46
|
+
dz = p[2] - oz;
|
|
47
|
+
pts.push({ x: dx * rx + dy * ry + dz * rz, y: dx * ux + dy * uy + dz * uz });
|
|
48
|
+
}
|
|
49
|
+
return pts;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function fmt(v: number): string {
|
|
53
|
+
return v.toFixed(6).replace(/\.?0+$/, '');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function buildPathD(projected: Pt2[][]): string {
|
|
57
|
+
const parts: string[] = [];
|
|
58
|
+
for (const poly of projected) {
|
|
59
|
+
if (poly.length === 0) continue;
|
|
60
|
+
parts.push(`M ${fmt(poly[0].x)} ${fmt(poly[0].y)}`);
|
|
61
|
+
for (let i = 1; i < poly.length; i++) {
|
|
62
|
+
parts.push(`L ${fmt(poly[i].x)} ${fmt(poly[i].y)}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return parts.join(' ');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Convert SlabSection geometry into an SvgModelView for the print-layout engine
|
|
70
|
+
* (`renderDrawing({ models })`). Returns null when the geometry is empty or
|
|
71
|
+
* degenerate. Use a (0,0,1) up hint when building the plane basis for standard
|
|
72
|
+
* north-up floor plans.
|
|
73
|
+
*
|
|
74
|
+
* When the geometry carries `perProduct` attribution (SlabSection option
|
|
75
|
+
* `trackProducts: true`), the model also gets `rawPaths` with per-product gid
|
|
76
|
+
* metadata — required for the renderer's gid-based features (highlight, clip
|
|
77
|
+
* include/exclude, render-time includeClasses): without rawPaths,
|
|
78
|
+
* filterModelForViewport returns the model untouched and those settings
|
|
79
|
+
* silently do nothing.
|
|
80
|
+
*/
|
|
81
|
+
export function slabSectionToModelView(
|
|
82
|
+
geometry: SlabSectionGeometry & { perProduct?: SlabPerProductEntry[] },
|
|
83
|
+
planeBasis: PlaneBasis,
|
|
84
|
+
lineStyles?: ViewportLineStyles
|
|
85
|
+
): SvgModelView | null {
|
|
86
|
+
const projected: Record<string, Pt2[][]> = {};
|
|
87
|
+
let minX = Infinity,
|
|
88
|
+
maxX = -Infinity;
|
|
89
|
+
let minY = Infinity,
|
|
90
|
+
maxY = -Infinity;
|
|
91
|
+
|
|
92
|
+
for (const cat of CATEGORIES) {
|
|
93
|
+
const polys = geometry[cat];
|
|
94
|
+
const result: Pt2[][] = [];
|
|
95
|
+
for (const poly of polys) {
|
|
96
|
+
if (poly.length === 0) continue;
|
|
97
|
+
const pts = projectPolyline(poly, planeBasis);
|
|
98
|
+
for (const pt of pts) {
|
|
99
|
+
if (pt.x < minX) minX = pt.x;
|
|
100
|
+
if (pt.x > maxX) maxX = pt.x;
|
|
101
|
+
if (pt.y < minY) minY = pt.y;
|
|
102
|
+
if (pt.y > maxY) maxY = pt.y;
|
|
103
|
+
}
|
|
104
|
+
result.push(pts);
|
|
105
|
+
}
|
|
106
|
+
projected[cat] = result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const viewWidth = maxX - minX;
|
|
110
|
+
const viewHeight = maxY - minY;
|
|
111
|
+
if (!Number.isFinite(viewWidth) || viewWidth <= 1e-6 || !Number.isFinite(viewHeight) || viewHeight <= 1e-6) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Map projected (x,y) to SVG coords: SVG_x = x - minX, SVG_y = maxY - y (flip).
|
|
116
|
+
const geometryTransform = `matrix(1 0 0 -1 ${fmt(-minX)} ${fmt(maxY)})`;
|
|
117
|
+
|
|
118
|
+
const svgParts: string[] = [`<g transform="${escapeAttr(geometryTransform)}">`];
|
|
119
|
+
for (const cat of CATEGORIES) {
|
|
120
|
+
const style = resolveStyle(cat, lineStyles);
|
|
121
|
+
if (style.enabled === false) continue;
|
|
122
|
+
const d = buildPathD(projected[cat]);
|
|
123
|
+
if (!d) continue;
|
|
124
|
+
let pathEl = `<path d="${d}" fill="none" stroke="${escapeAttr(style.color)}" stroke-width="${style.widthMm}"`;
|
|
125
|
+
if (style.dash) pathEl += ` stroke-dasharray="${escapeAttr(style.dash)}"`;
|
|
126
|
+
pathEl += ` stroke-linejoin="round" stroke-linecap="round"/>`;
|
|
127
|
+
svgParts.push(pathEl);
|
|
128
|
+
}
|
|
129
|
+
svgParts.push('</g>');
|
|
130
|
+
|
|
131
|
+
// Per-product rawPaths (same projection + styles as the merged groupSvg)
|
|
132
|
+
// so gid-based features can filter/restyle and rebuild the model.
|
|
133
|
+
let rawPaths: SectionPath[] | undefined;
|
|
134
|
+
if (geometry.perProduct?.length) {
|
|
135
|
+
rawPaths = [];
|
|
136
|
+
for (const entry of geometry.perProduct) {
|
|
137
|
+
const gid = (entry.product as { gid?: string } | undefined)?.gid;
|
|
138
|
+
if (!gid) continue;
|
|
139
|
+
for (const cat of CATEGORIES) {
|
|
140
|
+
const style = resolveStyle(cat, lineStyles);
|
|
141
|
+
if (style.enabled === false) continue;
|
|
142
|
+
const polys = entry[cat];
|
|
143
|
+
if (!polys?.length) continue;
|
|
144
|
+
const d = buildPathD(polys.filter((p) => p.length > 0).map((p) => projectPolyline(p, planeBasis)));
|
|
145
|
+
if (!d) continue;
|
|
146
|
+
rawPaths.push({
|
|
147
|
+
key: `${gid}:${cat}`,
|
|
148
|
+
d,
|
|
149
|
+
fill: null,
|
|
150
|
+
fillOpacity: 1,
|
|
151
|
+
stroke: style.color,
|
|
152
|
+
strokeWidth: style.widthMm,
|
|
153
|
+
dash: style.dash || undefined,
|
|
154
|
+
meta: { gid, isSegment: true, category: cat }
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (rawPaths.length === 0) rawPaths = undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
groupSvg: `<g>${svgParts.join('')}</g>`,
|
|
163
|
+
widthModel: viewWidth,
|
|
164
|
+
heightModel: viewHeight,
|
|
165
|
+
transform: {
|
|
166
|
+
viewMinX: minX,
|
|
167
|
+
viewMaxX: maxX,
|
|
168
|
+
viewMinZ: minY,
|
|
169
|
+
viewMaxZ: maxY,
|
|
170
|
+
viewWidth,
|
|
171
|
+
viewHeight
|
|
172
|
+
},
|
|
173
|
+
geometryTransform,
|
|
174
|
+
rawPaths
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Build `options.productEntries` for renderDrawing from SlabSection
|
|
180
|
+
* per-product attribution: each product's cut polylines in world x/z. Labels,
|
|
181
|
+
* legend, clip and highlight items are derived from these entries — the
|
|
182
|
+
* polylines must be in WORLD coordinates (use a zero lateral section origin so
|
|
183
|
+
* the projected space equals world x/z).
|
|
184
|
+
*/
|
|
185
|
+
export function slabPerProductToEntries(perProduct: SlabPerProductEntry[]): ProductPolylineEntry[] {
|
|
186
|
+
return perProduct
|
|
187
|
+
.map((entry) => ({
|
|
188
|
+
product: entry.product,
|
|
189
|
+
polylines: (entry.cutLines ?? [])
|
|
190
|
+
.filter((poly) => poly.length > 0)
|
|
191
|
+
.map((poly) => ({
|
|
192
|
+
points: poly.map((pt) => ({ x: pt[0], z: pt[2] }))
|
|
193
|
+
}))
|
|
194
|
+
}))
|
|
195
|
+
.filter((e) => e.polylines.length > 0);
|
|
196
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { LayoutTemplate, TemplateVariableDef, TemplateVariableInfo } from './types';
|
|
14
|
+
import { applyPlaceholderFilter } from './renderUtils';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Extract all variable definitions from a template.
|
|
@@ -263,14 +264,28 @@ export function resolveTemplateValues(
|
|
|
263
264
|
* // }
|
|
264
265
|
* ```
|
|
265
266
|
*/
|
|
266
|
-
export function applyTemplateVariables(
|
|
267
|
+
export function applyTemplateVariables(
|
|
268
|
+
template: LayoutTemplate,
|
|
269
|
+
values: Record<string, any>,
|
|
270
|
+
warnings?: string[]
|
|
271
|
+
): LayoutTemplate {
|
|
267
272
|
// Deep clone to avoid mutation
|
|
268
273
|
const resolved: LayoutTemplate = JSON.parse(JSON.stringify(template));
|
|
269
274
|
|
|
270
|
-
// Recursively replace {{vars.*}} in all string values
|
|
275
|
+
// Recursively replace {{vars.*}} in all string values.
|
|
276
|
+
// A value that is EXACTLY one placeholder (no surrounding text, no
|
|
277
|
+
// format filter) substitutes the variable's RAW value, preserving its
|
|
278
|
+
// type — this is what lets number variables drive numeric template
|
|
279
|
+
// fields like a viewport's cutHeight ("cutHeight": "{{vars.cutHeight}}").
|
|
280
|
+
// Mixed strings keep normal stringified interpolation.
|
|
281
|
+
const WHOLE_VALUE_RE = /^\{\{\s*vars\.(\w+)\s*\}\}$/;
|
|
271
282
|
function replaceInValue(obj: any): any {
|
|
272
283
|
if (typeof obj === 'string') {
|
|
273
|
-
|
|
284
|
+
const whole = WHOLE_VALUE_RE.exec(obj);
|
|
285
|
+
if (whole && whole[1] in values && typeof values[whole[1]] !== 'string') {
|
|
286
|
+
return values[whole[1]];
|
|
287
|
+
}
|
|
288
|
+
return interpolateVariables(obj, values, warnings);
|
|
274
289
|
}
|
|
275
290
|
if (Array.isArray(obj)) {
|
|
276
291
|
return obj.map(replaceInValue);
|
|
@@ -287,8 +302,8 @@ export function applyTemplateVariables(template: LayoutTemplate, values: Record<
|
|
|
287
302
|
|
|
288
303
|
// Apply to all template properties that might contain variable references
|
|
289
304
|
resolved.elements = replaceInValue(resolved.elements);
|
|
290
|
-
if (resolved.
|
|
291
|
-
resolved.
|
|
305
|
+
if (resolved.resources) {
|
|
306
|
+
resolved.resources = replaceInValue(resolved.resources);
|
|
292
307
|
}
|
|
293
308
|
|
|
294
309
|
return resolved;
|
|
@@ -304,14 +319,15 @@ export function applyTemplateVariables(template: LayoutTemplate, values: Record<
|
|
|
304
319
|
* @param values - Variable values
|
|
305
320
|
* @returns String with all {{vars.*}} replaced
|
|
306
321
|
*/
|
|
307
|
-
function interpolateVariables(str: string, values: Record<string, any
|
|
308
|
-
return str.replace(/\{\{vars\.(\w+)\}\}/g, (match, varName) => {
|
|
322
|
+
function interpolateVariables(str: string, values: Record<string, any>, warnings?: string[]): string {
|
|
323
|
+
return str.replace(/\{\{\s*vars\.(\w+)\s*(\|[^}]*)?\}\}/g, (match, varName, filterPart) => {
|
|
309
324
|
if (varName in values) {
|
|
310
325
|
const value = values[varName];
|
|
326
|
+
const filter = filterPart ? String(filterPart).slice(1).trim() : null;
|
|
311
327
|
// Convert to string (handles numbers, booleans, etc.)
|
|
312
|
-
return String(value);
|
|
328
|
+
return String(applyPlaceholderFilter(value, filter, warnings, str));
|
|
313
329
|
}
|
|
314
|
-
|
|
330
|
+
warnings?.push(`Variable '${varName}' has no value — placeholder kept.`);
|
|
315
331
|
return match; // Keep {{vars.varName}} as-is
|
|
316
332
|
});
|
|
317
333
|
}
|