@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
package/src/layout/validation.ts
CHANGED
|
@@ -6,6 +6,9 @@ import {
|
|
|
6
6
|
ELEMENT_KIND_IMAGE,
|
|
7
7
|
ELEMENT_KIND_LINE,
|
|
8
8
|
ELEMENT_KIND_RECT,
|
|
9
|
+
ELEMENT_KIND_CIRCLE,
|
|
10
|
+
ELEMENT_KIND_POLYLINE,
|
|
11
|
+
ELEMENT_KIND_POLYGON,
|
|
9
12
|
ELEMENT_KIND_SCALE_BAR,
|
|
10
13
|
ELEMENT_KIND_QR,
|
|
11
14
|
ELEMENT_KIND_LABELS,
|
|
@@ -20,9 +23,9 @@ export type { ValidationOptions, ValidationIssue, ValidationResult };
|
|
|
20
23
|
* Lightweight template validation (structure only) for v2 schema.
|
|
21
24
|
*/
|
|
22
25
|
export function validateLayoutTemplate(template: unknown, options?: ValidationOptions): ValidationResult {
|
|
23
|
-
const allowRemoteAssets = options?.allowRemoteAssets ?? true;
|
|
24
26
|
const allowDataUrls = options?.allowDataUrls ?? true;
|
|
25
|
-
const maxViewports = options?.maxViewports ??
|
|
27
|
+
const maxViewports = options?.maxViewports ?? 8;
|
|
28
|
+
const strictResourceRefs = options?.strictResourceRefs ?? false;
|
|
26
29
|
const errors: ValidationIssue[] = [];
|
|
27
30
|
|
|
28
31
|
if (!isRecord(template)) {
|
|
@@ -30,6 +33,10 @@ export function validateLayoutTemplate(template: unknown, options?: ValidationOp
|
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
const t = template;
|
|
36
|
+
// Remote resources are opt-in (matches the schema default and the
|
|
37
|
+
// renderer's warnings): explicit option wins, else the template's own
|
|
38
|
+
// root flag, else disallowed.
|
|
39
|
+
const allowRemoteResources = options?.allowRemoteResources ?? getProp(t, 'allowRemoteResources') === true;
|
|
33
40
|
|
|
34
41
|
const page = getProp(t, 'page');
|
|
35
42
|
if (!isRecord(page)) {
|
|
@@ -45,39 +52,45 @@ export function validateLayoutTemplate(template: unknown, options?: ValidationOp
|
|
|
45
52
|
}
|
|
46
53
|
}
|
|
47
54
|
|
|
48
|
-
const
|
|
49
|
-
const
|
|
50
|
-
if (Array.isArray(
|
|
51
|
-
|
|
52
|
-
if (!isRecord(
|
|
53
|
-
errors.push({ path: `
|
|
55
|
+
const resourceIds = new Set<string>();
|
|
56
|
+
const resources = getProp(t, 'resources');
|
|
57
|
+
if (Array.isArray(resources)) {
|
|
58
|
+
resources.forEach((resource: unknown, index: number) => {
|
|
59
|
+
if (!isRecord(resource)) {
|
|
60
|
+
errors.push({ path: `resources[${index}]`, message: 'Resource must be an object.' });
|
|
54
61
|
return;
|
|
55
62
|
}
|
|
56
|
-
const id = getProp(
|
|
63
|
+
const id = getProp(resource, 'id');
|
|
57
64
|
if (!isNonEmptyString(id)) {
|
|
58
|
-
errors.push({ path: `
|
|
59
|
-
} else if (
|
|
60
|
-
errors.push({ path: `
|
|
65
|
+
errors.push({ path: `resources[${index}].id`, message: 'Resource id is required.' });
|
|
66
|
+
} else if (resourceIds.has(id as string)) {
|
|
67
|
+
errors.push({ path: `resources[${index}].id`, message: 'Duplicate resource id.' });
|
|
61
68
|
} else {
|
|
62
|
-
|
|
69
|
+
resourceIds.add(id as string);
|
|
63
70
|
}
|
|
64
71
|
|
|
65
|
-
const src = getProp(
|
|
66
|
-
const content = getProp(
|
|
72
|
+
const src = getProp(resource, 'src');
|
|
73
|
+
const content = getProp(resource, 'content');
|
|
67
74
|
if (!isNonEmptyString(src) && !isNonEmptyString(content)) {
|
|
68
|
-
errors.push({ path: `
|
|
75
|
+
errors.push({ path: `resources[${index}]`, message: 'Resource must have either src or content.' });
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
if (isNonEmptyString(src)) {
|
|
72
|
-
if (isRemoteUrl(src as string) && !
|
|
73
|
-
errors.push({
|
|
79
|
+
if (isRemoteUrl(src as string) && !allowRemoteResources) {
|
|
80
|
+
errors.push({
|
|
81
|
+
path: `resources[${index}].src`,
|
|
82
|
+
message: 'Remote URLs are not allowed for resources.'
|
|
83
|
+
});
|
|
74
84
|
} else if (isDataUrl(src as string) && !allowDataUrls) {
|
|
75
|
-
errors.push({
|
|
85
|
+
errors.push({
|
|
86
|
+
path: `resources[${index}].src`,
|
|
87
|
+
message: 'Data URLs are not allowed for resources.'
|
|
88
|
+
});
|
|
76
89
|
}
|
|
77
90
|
}
|
|
78
91
|
});
|
|
79
|
-
} else if (
|
|
80
|
-
errors.push({ path: '
|
|
92
|
+
} else if (resources !== undefined) {
|
|
93
|
+
errors.push({ path: 'resources', message: 'Resources must be an array when provided.' });
|
|
81
94
|
}
|
|
82
95
|
|
|
83
96
|
const styleNames = new Set<string>();
|
|
@@ -130,8 +143,8 @@ export function validateLayoutTemplate(template: unknown, options?: ValidationOp
|
|
|
130
143
|
ensure(isPositiveNumber(getProp(el, 'heightMm')), `${path}.heightMm`, 'heightMm must be > 0.');
|
|
131
144
|
const xMm = getProp(el, 'xMm');
|
|
132
145
|
const yMm = getProp(el, 'yMm');
|
|
133
|
-
ensure(
|
|
134
|
-
ensure(
|
|
146
|
+
ensure(typeof xMm === 'number' && Number.isFinite(xMm), `${path}.xMm`, 'xMm is required.');
|
|
147
|
+
ensure(typeof yMm === 'number' && Number.isFinite(yMm), `${path}.yMm`, 'yMm is required.');
|
|
135
148
|
const border = getProp(el, 'border');
|
|
136
149
|
if (border) {
|
|
137
150
|
// border.stroke and border.strokeWidthMm are optional in v2
|
|
@@ -159,8 +172,8 @@ export function validateLayoutTemplate(template: unknown, options?: ValidationOp
|
|
|
159
172
|
} else if (kind === ELEMENT_KIND_TEXT) {
|
|
160
173
|
const xMm = getProp(el, 'xMm');
|
|
161
174
|
const yMm = getProp(el, 'yMm');
|
|
162
|
-
ensure(
|
|
163
|
-
ensure(
|
|
175
|
+
ensure(typeof xMm === 'number' && Number.isFinite(xMm), `${path}.xMm`, 'xMm is required.');
|
|
176
|
+
ensure(typeof yMm === 'number' && Number.isFinite(yMm), `${path}.yMm`, 'yMm is required.');
|
|
164
177
|
const textStyle = getProp(el, 'textStyle');
|
|
165
178
|
if (textStyle !== undefined) {
|
|
166
179
|
if (typeof textStyle === 'string' && styleNames.size > 0 && !styleNames.has(textStyle)) {
|
|
@@ -174,12 +187,47 @@ export function validateLayoutTemplate(template: unknown, options?: ValidationOp
|
|
|
174
187
|
} else if (kind === ELEMENT_KIND_IMAGE) {
|
|
175
188
|
ensure(isPositiveNumber(getProp(el, 'widthMm')), `${path}.widthMm`, 'widthMm must be > 0.');
|
|
176
189
|
ensure(isPositiveNumber(getProp(el, 'heightMm')), `${path}.heightMm`, 'heightMm must be > 0.');
|
|
177
|
-
|
|
190
|
+
const resourceId = getProp(el, 'resourceId');
|
|
191
|
+
ensure(isNonEmptyString(resourceId), `${path}.resourceId`, 'resourceId is required.');
|
|
192
|
+
// Opt-in only. `renderLayout` throws on any validation error, so treating a dangling
|
|
193
|
+
// reference as one turns a template that used to print without a single logo into a
|
|
194
|
+
// template that does not print at all — the renderer already warns and skips that
|
|
195
|
+
// element, which is the better outcome mid-print. Authoring tools that want the typo
|
|
196
|
+
// caught up front can ask for it.
|
|
197
|
+
if (strictResourceRefs && isNonEmptyString(resourceId) && !resourceIds.has(resourceId)) {
|
|
198
|
+
ensure(false, `${path}.resourceId`, `No resource is declared with id '${resourceId}'.`);
|
|
199
|
+
}
|
|
178
200
|
} else if (kind === ELEMENT_KIND_LINE) {
|
|
179
201
|
// stroke and strokeWidthMm optional in v2
|
|
180
202
|
} else if (kind === ELEMENT_KIND_RECT) {
|
|
181
203
|
ensure(isPositiveNumber(getProp(el, 'widthMm')), `${path}.widthMm`, 'widthMm must be > 0.');
|
|
182
204
|
ensure(isPositiveNumber(getProp(el, 'heightMm')), `${path}.heightMm`, 'heightMm must be > 0.');
|
|
205
|
+
} else if (kind === ELEMENT_KIND_CIRCLE) {
|
|
206
|
+
ensure(isPositiveNumber(getProp(el, 'rMm')), `${path}.rMm`, 'rMm must be > 0.');
|
|
207
|
+
const cxMm = getProp(el, 'cxMm');
|
|
208
|
+
const cyMm = getProp(el, 'cyMm');
|
|
209
|
+
ensure(typeof cxMm === 'number' && Number.isFinite(cxMm), `${path}.cxMm`, 'cxMm is required.');
|
|
210
|
+
ensure(typeof cyMm === 'number' && Number.isFinite(cyMm), `${path}.cyMm`, 'cyMm is required.');
|
|
211
|
+
} else if (kind === ELEMENT_KIND_POLYLINE) {
|
|
212
|
+
const points = getProp(el, 'points');
|
|
213
|
+
ensure(
|
|
214
|
+
Array.isArray(points) && points.length >= 2,
|
|
215
|
+
`${path}.points`,
|
|
216
|
+
'points is required (at least 2 points).'
|
|
217
|
+
);
|
|
218
|
+
ensure(isNonEmptyString(getProp(el, 'stroke')), `${path}.stroke`, 'stroke is required.');
|
|
219
|
+
ensure(
|
|
220
|
+
isPositiveNumber(getProp(el, 'strokeWidthMm')),
|
|
221
|
+
`${path}.strokeWidthMm`,
|
|
222
|
+
'strokeWidthMm must be > 0.'
|
|
223
|
+
);
|
|
224
|
+
} else if (kind === ELEMENT_KIND_POLYGON) {
|
|
225
|
+
const points = getProp(el, 'points');
|
|
226
|
+
ensure(
|
|
227
|
+
Array.isArray(points) && points.length >= 3,
|
|
228
|
+
`${path}.points`,
|
|
229
|
+
'points is required (at least 3 points).'
|
|
230
|
+
);
|
|
183
231
|
} else if (kind === ELEMENT_KIND_SCALE_BAR) {
|
|
184
232
|
ensure(isPositiveNumber(getProp(el, 'maxLengthMm')), `${path}.maxLengthMm`, 'maxLengthMm must be > 0.');
|
|
185
233
|
const textStyle = getProp(el, 'textStyle');
|
|
@@ -195,8 +243,8 @@ export function validateLayoutTemplate(template: unknown, options?: ValidationOp
|
|
|
195
243
|
ensure(isPositiveNumber(getProp(el, 'sizeMm')), `${path}.sizeMm`, 'sizeMm must be > 0.');
|
|
196
244
|
const xMm = getProp(el, 'xMm');
|
|
197
245
|
const yMm = getProp(el, 'yMm');
|
|
198
|
-
ensure(
|
|
199
|
-
ensure(
|
|
246
|
+
ensure(typeof xMm === 'number' && Number.isFinite(xMm), `${path}.xMm`, 'xMm is required.');
|
|
247
|
+
ensure(typeof yMm === 'number' && Number.isFinite(yMm), `${path}.yMm`, 'yMm is required.');
|
|
200
248
|
} else if (kind === ELEMENT_KIND_LABELS) {
|
|
201
249
|
ensure(isNonEmptyString(getProp(el, 'viewport')), `${path}.viewport`, 'viewport is required.');
|
|
202
250
|
ensure(isNonEmptyString(getProp(el, 'text')), `${path}.text`, 'text is required.');
|
|
@@ -288,8 +336,16 @@ export function validateLayoutTemplate(template: unknown, options?: ValidationOp
|
|
|
288
336
|
);
|
|
289
337
|
const clipXMm = getProp(clip, 'xMm');
|
|
290
338
|
const clipYMm = getProp(clip, 'yMm');
|
|
291
|
-
ensure(
|
|
292
|
-
|
|
339
|
+
ensure(
|
|
340
|
+
typeof clipXMm === 'number' && Number.isFinite(clipXMm),
|
|
341
|
+
`${path}.clip.xMm`,
|
|
342
|
+
'clip.xMm is required.'
|
|
343
|
+
);
|
|
344
|
+
ensure(
|
|
345
|
+
typeof clipYMm === 'number' && Number.isFinite(clipYMm),
|
|
346
|
+
`${path}.clip.yMm`,
|
|
347
|
+
'clip.yMm is required.'
|
|
348
|
+
);
|
|
293
349
|
}
|
|
294
350
|
const children = getProp(el, 'children');
|
|
295
351
|
if (Array.isArray(children)) {
|
|
@@ -305,8 +361,8 @@ export function validateLayoutTemplate(template: unknown, options?: ValidationOp
|
|
|
305
361
|
const elements = getProp(t, 'elements');
|
|
306
362
|
if (Array.isArray(elements)) {
|
|
307
363
|
elements.forEach((el: unknown, index: number) => validateElement(el, `elements[${index}]`));
|
|
308
|
-
} else
|
|
309
|
-
errors.push({ path: 'elements', message: '
|
|
364
|
+
} else {
|
|
365
|
+
errors.push({ path: 'elements', message: 'elements is required and must be an array.' });
|
|
310
366
|
}
|
|
311
367
|
|
|
312
368
|
// Validate template variables (if present)
|
|
@@ -319,6 +375,16 @@ export function validateLayoutTemplate(template: unknown, options?: ValidationOp
|
|
|
319
375
|
for (const [varName, varDef] of Object.entries(variables)) {
|
|
320
376
|
const varPath = `variables.${varName}`;
|
|
321
377
|
|
|
378
|
+
// Names are referenced as {{vars.<name>}} — non-identifier
|
|
379
|
+
// names (dashes, dots, spaces) collide with the placeholder
|
|
380
|
+
// syntax and are silently never substituted.
|
|
381
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(varName)) {
|
|
382
|
+
errors.push({
|
|
383
|
+
path: varPath,
|
|
384
|
+
message: 'Variable names must be simple identifiers ([A-Za-z_][A-Za-z0-9_]*).'
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
322
388
|
if (!isRecord(varDef)) {
|
|
323
389
|
errors.push({ path: varPath, message: 'Variable definition must be an object.' });
|
|
324
390
|
continue;
|
|
@@ -5,8 +5,8 @@ import { pushWarning } from './warnings';
|
|
|
5
5
|
import { pickScale } from './viewportUtils';
|
|
6
6
|
import type { ResolvedPresets } from './presets';
|
|
7
7
|
import { getPathValue } from './renderUtils';
|
|
8
|
-
import { pointInPolygon, offsetPolygons, Polygon } from './clipUtils';
|
|
9
|
-
import {
|
|
8
|
+
import { pointInPolygon, offsetPolygons, unionPolygons, Polygon } from './clipUtils';
|
|
9
|
+
import { matchesFilter, normalizeClass } from './filters';
|
|
10
10
|
import { EPSILON_MIN_DIMENSION } from './geometryConstants';
|
|
11
11
|
|
|
12
12
|
export interface ViewportPrepResult {
|
|
@@ -20,7 +20,7 @@ export interface ViewportPrepResult {
|
|
|
20
20
|
viewportClipLabels: Record<string, boolean>;
|
|
21
21
|
viewportClipStyles: Record<
|
|
22
22
|
string,
|
|
23
|
-
{
|
|
23
|
+
{ color?: string; widthMm?: number; opacity?: number; dash?: string; fill?: string; fillOpacity?: number }
|
|
24
24
|
>;
|
|
25
25
|
viewportClipWhere: Record<string, WhereCondition | undefined>;
|
|
26
26
|
viewportHighlight: Record<
|
|
@@ -28,9 +28,10 @@ export interface ViewportPrepResult {
|
|
|
28
28
|
{
|
|
29
29
|
gids: Set<string> | null;
|
|
30
30
|
styles: {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
color?: string;
|
|
32
|
+
widthMm?: number;
|
|
33
|
+
opacity?: number;
|
|
34
|
+
dash?: string;
|
|
34
35
|
fill?: string;
|
|
35
36
|
fillOpacity?: number;
|
|
36
37
|
};
|
|
@@ -60,7 +61,7 @@ export const prepareViewports = (
|
|
|
60
61
|
const viewportClipLabels: Record<string, boolean> = {};
|
|
61
62
|
const viewportClipStyles: Record<
|
|
62
63
|
string,
|
|
63
|
-
{
|
|
64
|
+
{ color?: string; widthMm?: number; opacity?: number; dash?: string; fill?: string; fillOpacity?: number }
|
|
64
65
|
> = {};
|
|
65
66
|
const viewportClipWhere: Record<string, WhereCondition | undefined> = {};
|
|
66
67
|
const viewportHighlight: ViewportPrepResult['viewportHighlight'] = {};
|
|
@@ -77,7 +78,7 @@ export const prepareViewports = (
|
|
|
77
78
|
if (hlCfg && hlCfg.where) {
|
|
78
79
|
const sourceItems = getPathValue(data, `viewports.${vp.id}.items`);
|
|
79
80
|
if (Array.isArray(sourceItems) && sourceItems.length) {
|
|
80
|
-
const matching = sourceItems.filter((it) =>
|
|
81
|
+
const matching = sourceItems.filter((it) => matchesFilter(it, hlCfg.class, hlCfg.where));
|
|
81
82
|
if (matching.length) {
|
|
82
83
|
highlightGids = new Set(
|
|
83
84
|
matching
|
|
@@ -130,7 +131,7 @@ export const prepareViewports = (
|
|
|
130
131
|
viewportClipWhere[vp.id] = clipCfg.where;
|
|
131
132
|
const sourceItems = getPathValue(data, `viewports.${vp.id}.items`);
|
|
132
133
|
if (Array.isArray(sourceItems) && sourceItems.length) {
|
|
133
|
-
const matchingItems = sourceItems.filter((it) =>
|
|
134
|
+
const matchingItems = sourceItems.filter((it) => matchesFilter(it, clipCfg.class, clipCfg.where));
|
|
134
135
|
if (!matchingItems.length) {
|
|
135
136
|
pushWarning(
|
|
136
137
|
options.warnings,
|
|
@@ -167,13 +168,17 @@ export const prepareViewports = (
|
|
|
167
168
|
}
|
|
168
169
|
if (mapped.length >= 3) projectedLocal.push(mapped);
|
|
169
170
|
}
|
|
170
|
-
const
|
|
171
|
-
typeof clipCfg.
|
|
172
|
-
Number.isFinite(clipCfg.
|
|
173
|
-
clipCfg.
|
|
174
|
-
? clipCfg.
|
|
171
|
+
const expandBy =
|
|
172
|
+
typeof clipCfg.expandBy === 'number' &&
|
|
173
|
+
Number.isFinite(clipCfg.expandBy) &&
|
|
174
|
+
clipCfg.expandBy > 0
|
|
175
|
+
? clipCfg.expandBy
|
|
175
176
|
: 0;
|
|
176
|
-
const
|
|
177
|
+
const expanded = expandBy > 0 ? offsetPolygons(projectedLocal, expandBy) : projectedLocal;
|
|
178
|
+
// Merge overlapping polygons into single outlines (the
|
|
179
|
+
// result rings can include holes — everything downstream
|
|
180
|
+
// renders them with even-odd rules).
|
|
181
|
+
const buffered = unionPolygons(expanded);
|
|
177
182
|
|
|
178
183
|
if (buffered.length) {
|
|
179
184
|
// Calculate bounds from EXPANDED polygon for viewport sizing (so everything fits)
|
|
@@ -196,9 +201,10 @@ export const prepareViewports = (
|
|
|
196
201
|
viewportClipPolygonsLocal[vp.id] = buffered;
|
|
197
202
|
viewportClipLabels[vp.id] = !!clipCfg.clipLabels;
|
|
198
203
|
viewportClipStyles[vp.id] = {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
204
|
+
color: clipCfg.color ?? '#000000',
|
|
205
|
+
widthMm: clipCfg.widthMm ?? 0.5,
|
|
206
|
+
opacity: clipCfg.opacity ?? 1,
|
|
207
|
+
dash: clipCfg.dash,
|
|
202
208
|
fill: clipCfg.fill ?? 'none',
|
|
203
209
|
fillOpacity: clipCfg.fillOpacity ?? 0
|
|
204
210
|
};
|
|
@@ -219,6 +225,8 @@ export const prepareViewports = (
|
|
|
219
225
|
if (worldPolygons.length) {
|
|
220
226
|
viewportClipPolygonsWorld[vp.id] = worldPolygons;
|
|
221
227
|
clipAllowedGids = new Set<string>();
|
|
228
|
+
// pointInPolygon expects y — map polys ONCE, not per item.
|
|
229
|
+
const worldPolygons2D = worldPolygons.map((poly) => poly.map((p) => ({ x: p.x, y: p.z })));
|
|
222
230
|
for (const item of sourceItems) {
|
|
223
231
|
// Use item's direct x/z properties (same approach as labels)
|
|
224
232
|
const ix = getProp(item, 'x');
|
|
@@ -227,11 +235,9 @@ export const prepareViewports = (
|
|
|
227
235
|
if (!Number.isFinite(ix) || !Number.isFinite(iz)) {
|
|
228
236
|
continue;
|
|
229
237
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return pointInPolygon(polyWith2D, ix as number, iz as number);
|
|
234
|
-
});
|
|
238
|
+
const inside = worldPolygons2D.some((poly) =>
|
|
239
|
+
pointInPolygon(poly, ix as number, iz as number)
|
|
240
|
+
);
|
|
235
241
|
if (inside) {
|
|
236
242
|
const gid = getProp(item, 'gid') ?? getProp(item, 'id');
|
|
237
243
|
if (gid !== null && gid !== undefined) {
|
|
@@ -264,16 +270,18 @@ export const prepareViewports = (
|
|
|
264
270
|
|
|
265
271
|
const hlStyles = hlCfg
|
|
266
272
|
? {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
273
|
+
color: typeof hlCfg.color === 'string' ? hlCfg.color : '#ff0000',
|
|
274
|
+
widthMm: typeof hlCfg.widthMm === 'number' ? hlCfg.widthMm : 0.5,
|
|
275
|
+
opacity: typeof hlCfg.opacity === 'number' ? hlCfg.opacity : 1,
|
|
276
|
+
dash: typeof hlCfg.dash === 'string' ? hlCfg.dash : undefined,
|
|
270
277
|
fill: typeof hlCfg.fill === 'string' ? hlCfg.fill : '#ffff00',
|
|
271
278
|
fillOpacity: typeof hlCfg.fillOpacity === 'number' ? hlCfg.fillOpacity : 0.5
|
|
272
279
|
}
|
|
273
280
|
: {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
281
|
+
color: undefined,
|
|
282
|
+
widthMm: undefined,
|
|
283
|
+
opacity: undefined,
|
|
284
|
+
dash: undefined,
|
|
277
285
|
fill: undefined,
|
|
278
286
|
fillOpacity: undefined
|
|
279
287
|
};
|
|
@@ -291,6 +299,16 @@ export const prepareViewports = (
|
|
|
291
299
|
addFilterClasses(hlCfg?.class);
|
|
292
300
|
addFilterClasses(clipCfg?.class);
|
|
293
301
|
|
|
302
|
+
// gid-based features (highlight, clip include/exclude, includeClasses)
|
|
303
|
+
// require rawPaths with per-product meta — filterModelForViewport
|
|
304
|
+
// returns the model UNTOUCHED without them, so warn instead of
|
|
305
|
+
// silently rendering as if the config didn't exist.
|
|
306
|
+
if (baseModel && !baseModel.rawPaths?.length && (vp.highlight || vp.clip || vp.includeClasses)) {
|
|
307
|
+
pushWarning(
|
|
308
|
+
options.warnings,
|
|
309
|
+
`Viewport '${vp.id}' declares highlight/clip/includeClasses but its model has no rawPaths with per-product metadata — these settings will have no effect.`
|
|
310
|
+
);
|
|
311
|
+
}
|
|
294
312
|
const model = baseModel
|
|
295
313
|
? filterModelForViewport(
|
|
296
314
|
baseModel,
|