@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,96 @@
|
|
|
1
|
+
import type { SectionRenderState } from '@twinfinity/geometry';
|
|
2
|
+
import { validateLayoutTemplate } from './validation';
|
|
3
|
+
import { validateSectionState } from './sectionValidation';
|
|
4
|
+
import { isRecord, getProp } from './typeGuards';
|
|
5
|
+
import type { LayoutTemplate, ValidationIssue, ValidationOptions } from './types';
|
|
6
|
+
|
|
7
|
+
export type LayoutDataValidationResult =
|
|
8
|
+
| { ok: true }
|
|
9
|
+
| {
|
|
10
|
+
ok: false;
|
|
11
|
+
errors: string[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type PreflightResult =
|
|
15
|
+
| { ok: true }
|
|
16
|
+
| {
|
|
17
|
+
ok: false;
|
|
18
|
+
templateErrors?: ValidationIssue[];
|
|
19
|
+
dataErrors?: string[];
|
|
20
|
+
sectionErrors?: string[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validate template, layout data, and section state in a single call.
|
|
25
|
+
* Combines `validateLayoutTemplate`, `validateLayoutData`, and `validateSectionState`.
|
|
26
|
+
*/
|
|
27
|
+
export const preflight = (
|
|
28
|
+
template: LayoutTemplate,
|
|
29
|
+
data: unknown,
|
|
30
|
+
expectedViewportIds: string[],
|
|
31
|
+
sectionState?: SectionRenderState,
|
|
32
|
+
/**
|
|
33
|
+
* Forwarded to `validateLayoutTemplate`. Without it preflight always judged the template under
|
|
34
|
+
* default policy, so a caller that permits remote resources or data URLs was told its template
|
|
35
|
+
* was invalid by the very check meant to predict whether rendering would succeed.
|
|
36
|
+
*/
|
|
37
|
+
options?: ValidationOptions
|
|
38
|
+
): PreflightResult => {
|
|
39
|
+
const templateValidation = validateLayoutTemplate(template, options);
|
|
40
|
+
const dataValidation = validateLayoutData(data, expectedViewportIds);
|
|
41
|
+
const sectionValidation = sectionState ? validateSectionState(sectionState) : { ok: true as const };
|
|
42
|
+
|
|
43
|
+
const ok = templateValidation.ok && dataValidation.ok && sectionValidation.ok;
|
|
44
|
+
if (ok) return { ok: true };
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
ok: false,
|
|
48
|
+
templateErrors: templateValidation.ok ? undefined : templateValidation.errors,
|
|
49
|
+
dataErrors: dataValidation.ok ? undefined : dataValidation.errors,
|
|
50
|
+
sectionErrors: sectionValidation.ok ? undefined : sectionValidation.errors
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Validate layout data before rendering to catch missing propertySets or items arrays.
|
|
56
|
+
*/
|
|
57
|
+
export const validateLayoutData = (data: unknown, expectedViewportIds: string[]): LayoutDataValidationResult => {
|
|
58
|
+
const errors: string[] = [];
|
|
59
|
+
if (!isRecord(data)) {
|
|
60
|
+
return { ok: false, errors: ['layout data must be an object'] };
|
|
61
|
+
}
|
|
62
|
+
expectedViewportIds.forEach((vpId) => {
|
|
63
|
+
const viewports = getProp(data, 'viewports');
|
|
64
|
+
const viewport = isRecord(viewports) ? getProp(viewports, vpId) : undefined;
|
|
65
|
+
const items = getProp(viewport, 'items');
|
|
66
|
+
if (!Array.isArray(items)) {
|
|
67
|
+
errors.push(`viewports.${vpId}.items is required and must be an array.`);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
items.forEach((it: unknown, idx: number) => {
|
|
71
|
+
if (!isRecord(it)) {
|
|
72
|
+
errors.push(`viewports.${vpId}.items[${idx}] must be an object.`);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const sets = getProp(it, 'propertySets');
|
|
76
|
+
if (!isRecord(sets)) {
|
|
77
|
+
errors.push(`viewports.${vpId}.items[${idx}] is missing propertySets.<Set>.<Prop>.value.`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
Object.entries(sets).forEach(([setName, props]) => {
|
|
81
|
+
if (!isRecord(props)) {
|
|
82
|
+
errors.push(`viewports.${vpId}.items[${idx}].propertySets.${setName} must be an object.`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
Object.entries(props).forEach(([propName, payload]) => {
|
|
86
|
+
if (!isRecord(payload) || !('value' in payload)) {
|
|
87
|
+
errors.push(
|
|
88
|
+
`viewports.${vpId}.items[${idx}].propertySets.${setName}.${propName} must include a value (propertySets.<Set>.<Prop>.value).`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
return errors.length ? { ok: false, errors } : { ok: true };
|
|
96
|
+
};
|
package/src/layout/qrRender.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { escapeAttr } from './renderUtils';
|
|
2
|
-
import { interpolate } from './renderUtils';
|
|
2
|
+
import { interpolate, formatNumber } from './renderUtils';
|
|
3
|
+
import { resolveViewportSection } from './sectionDefs';
|
|
3
4
|
import { QrElement, SvgModelView } from './types';
|
|
4
5
|
|
|
5
6
|
async function getQRCode(): Promise<typeof import('qrcode')> {
|
|
@@ -58,13 +59,18 @@ export const renderQrElements = async (
|
|
|
58
59
|
const t = boundModel.transform;
|
|
59
60
|
const worldX = t.viewMinX + modelX;
|
|
60
61
|
const worldZ = t.viewMaxZ - modelY;
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
// Height: explicit qrContext wins; otherwise the bound viewport's
|
|
63
|
+
// own section definition (the template already knows its cut).
|
|
64
|
+
const sectionDef = resolveViewportSection(boundViewportDef);
|
|
65
|
+
const planeHeight = options?.qrContext?.planeHeight ?? (sectionDef?.plan ? sectionDef.position : 0);
|
|
66
|
+
coordinate = { x: formatNumber(worldX), y: formatNumber(planeHeight), z: formatNumber(worldZ) };
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
let payload: unknown = qr.dataTemplate ? interpolate(qr.dataTemplate, dataWithViewport) : null;
|
|
70
|
+
// Schema default: a QR with no role and no dataTemplate is role "main".
|
|
71
|
+
const role = qr.role ?? (qr.dataTemplate ? 'custom' : 'main');
|
|
66
72
|
if (!payload || typeof payload === 'string') {
|
|
67
|
-
if (
|
|
73
|
+
if (role === 'anchor' && coordinate) {
|
|
68
74
|
const corner =
|
|
69
75
|
qr.anchorCorner === 'bottom-left'
|
|
70
76
|
? 'bl'
|
|
@@ -76,7 +82,7 @@ export const renderQrElements = async (
|
|
|
76
82
|
? 'tr'
|
|
77
83
|
: 'na';
|
|
78
84
|
payload = { v: 1, c: corner, p: coordinate };
|
|
79
|
-
} else if (
|
|
85
|
+
} else if (role === 'main') {
|
|
80
86
|
const cornerShort =
|
|
81
87
|
qr.anchorCorner === 'bottom-left'
|
|
82
88
|
? 'bl'
|
|
@@ -119,8 +125,3 @@ export const renderQrElements = async (
|
|
|
119
125
|
|
|
120
126
|
return parts;
|
|
121
127
|
};
|
|
122
|
-
|
|
123
|
-
const formatNumber = (value: number): number => {
|
|
124
|
-
if (!Number.isFinite(value)) return 0;
|
|
125
|
-
return Number.parseFloat(value.toFixed(3));
|
|
126
|
-
};
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import type { SectionRenderState, SectionPath, SectionLabel } from '@twinfinity/geometry';
|
|
2
|
-
import { sectionStateToModelView, computePolylinesCenter } from './itemBuilders';
|
|
2
|
+
import { sectionStateToModelView, computePolylinesCenter, readXZ } from './itemBuilders';
|
|
3
3
|
import { buildMetadataMap, normalizePropertySets, resolveIfcClassName, resolveIfcClassId } from './metadata';
|
|
4
4
|
import { applyLineStyleToPaths } from './utils';
|
|
5
5
|
import { analyzeTemplate } from './analyzeTemplate';
|
|
6
|
-
import {
|
|
6
|
+
import { matchesFilter, matchesProductClass, normalizeValue } from './filters';
|
|
7
7
|
import type {
|
|
8
8
|
LayoutTemplate,
|
|
9
9
|
ViewportElement,
|
|
10
10
|
ViewportLineStyle,
|
|
11
|
+
ViewportLineStyles,
|
|
11
12
|
FillRule,
|
|
12
13
|
SvgModelView,
|
|
13
14
|
SectionResult,
|
|
14
15
|
RenderDrawingInput,
|
|
15
16
|
RenderDrawingResult,
|
|
16
17
|
NormalizedProductItem,
|
|
18
|
+
ProductPolylineEntry,
|
|
17
19
|
LabelStyle
|
|
18
20
|
} from './types';
|
|
19
21
|
import { getProp } from './typeGuards';
|
|
@@ -133,43 +135,29 @@ function getSampleColor(): (product: unknown) => SampledColor | undefined {
|
|
|
133
135
|
return _geometry.sampleProductColorRgba;
|
|
134
136
|
}
|
|
135
137
|
|
|
136
|
-
|
|
137
|
-
// renderDrawing is imported by index.ts which defines renderLayout.
|
|
138
|
-
// The binding is set by index.ts after definition via setRenderLayoutBinding().
|
|
139
|
-
let _renderLayout:
|
|
140
|
-
| ((
|
|
141
|
-
template: LayoutTemplate,
|
|
142
|
-
data: any,
|
|
143
|
-
models: Record<string, SvgModelView>,
|
|
144
|
-
options?: any
|
|
145
|
-
) => Promise<{ svg: string; viewportScales: any; warnings: string[] }>)
|
|
146
|
-
| null = null;
|
|
147
|
-
|
|
148
|
-
/** Called by index.ts to register renderLayout without a circular import. */
|
|
149
|
-
export function setRenderLayoutBinding(fn: NonNullable<typeof _renderLayout>): void {
|
|
150
|
-
_renderLayout = fn;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function getRenderLayout(): NonNullable<typeof _renderLayout> {
|
|
154
|
-
if (!_renderLayout) {
|
|
155
|
-
throw new Error(
|
|
156
|
-
'renderLayout not initialized. Ensure @twinfinity/printing index is imported before calling renderDrawing.'
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
return _renderLayout;
|
|
160
|
-
}
|
|
138
|
+
import { renderLayout } from './renderLayout';
|
|
161
139
|
|
|
162
140
|
// ---------------------------------------------------------------------------
|
|
163
141
|
// Public API
|
|
164
142
|
// ---------------------------------------------------------------------------
|
|
165
143
|
|
|
166
144
|
/**
|
|
167
|
-
* High-level entry point that
|
|
168
|
-
*
|
|
145
|
+
* High-level entry point that renders a layout template to a ready-to-print
|
|
146
|
+
* SVG string.
|
|
169
147
|
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
148
|
+
* The inputs form a 2×2 matrix — geometry × item data, each either raw or
|
|
149
|
+
* pre-built:
|
|
150
|
+
*
|
|
151
|
+
* | input | what it is |
|
|
152
|
+
* |---------------------------|--------------------------------------------------|
|
|
153
|
+
* | `sections` | RAW geometry: plane-section results (one shared or per viewport id). |
|
|
154
|
+
* | `models` | PRE-BUILT geometry: `SvgModelView`s per viewport id (e.g. from `slabSectionToModelView`). Needs `rawPaths` with `meta.gid` for highlight/clip/includeClasses. |
|
|
155
|
+
* | `options.productEntries` | RAW item data: per-product world-x/z polylines (`slabPerProductToEntries`); one flat list shared by all viewports or a per-viewport record. Feeds labels/legend/clip/highlight items. |
|
|
156
|
+
* | `options.viewportItems` | PRE-BUILT item data per viewport id (overrides `productEntries`). |
|
|
157
|
+
*
|
|
158
|
+
* Prerequisite: products must have property sets loaded
|
|
159
|
+
* (`await api.ifc.loadPropertySets()` on core BIM objects) — without them
|
|
160
|
+
* every `where` filter matches nothing.
|
|
173
161
|
*
|
|
174
162
|
* ```ts
|
|
175
163
|
* const section = computePlaneSectionsForProducts(products, plane, { includeInvisible: true });
|
|
@@ -177,9 +165,8 @@ function getRenderLayout(): NonNullable<typeof _renderLayout> {
|
|
|
177
165
|
* template,
|
|
178
166
|
* sections: { main: section },
|
|
179
167
|
* options: {
|
|
180
|
-
*
|
|
181
|
-
* qrContext: { host, containerName
|
|
182
|
-
* productFields: { idPath: "propertySets.BIP.spacenumber.value", ... },
|
|
168
|
+
* allowRemoteResources: true,
|
|
169
|
+
* qrContext: { host, containerName },
|
|
183
170
|
* roomLabels: { labelPath: "BIP.spacename", classFilter: "space" },
|
|
184
171
|
* },
|
|
185
172
|
* });
|
|
@@ -200,11 +187,9 @@ export async function renderDrawing(input: RenderDrawingInput): Promise<RenderDr
|
|
|
200
187
|
varWarnings.forEach((w) => warnings.push(`[Variables] ${w}`));
|
|
201
188
|
|
|
202
189
|
// Apply variables to template (substitute {{vars.*}} references)
|
|
203
|
-
template = applyTemplateVariables(template, values);
|
|
190
|
+
template = applyTemplateVariables(template, values, warnings);
|
|
204
191
|
}
|
|
205
192
|
|
|
206
|
-
const renderLayout = getRenderLayout();
|
|
207
|
-
|
|
208
193
|
// ---- Collect viewport elements from template ----
|
|
209
194
|
const viewports = template.elements.filter((el) => el.kind === 'viewport') as ViewportElement[];
|
|
210
195
|
if (!viewports.length) {
|
|
@@ -213,6 +198,17 @@ export async function renderDrawing(input: RenderDrawingInput): Promise<RenderDr
|
|
|
213
198
|
|
|
214
199
|
// ---- Fast path: pre-built models provided directly ----
|
|
215
200
|
if (input.models) {
|
|
201
|
+
if (options?.roomLabels) {
|
|
202
|
+
warnings.push('options.roomLabels is ignored on the models path — use a template labels element instead.');
|
|
203
|
+
}
|
|
204
|
+
// productEntries: one flat list for all viewports, or per-viewport record.
|
|
205
|
+
const entriesInput = options?.productEntries;
|
|
206
|
+
const entriesFor = (vpId: string): ProductPolylineEntry[] =>
|
|
207
|
+
Array.isArray(entriesInput) ? entriesInput : (entriesInput?.[vpId] ?? []);
|
|
208
|
+
const allEntries: ProductPolylineEntry[] = Array.isArray(entriesInput)
|
|
209
|
+
? entriesInput
|
|
210
|
+
: Object.values(entriesInput ?? {}).flat();
|
|
211
|
+
|
|
216
212
|
// Build items: explicit viewportItems take priority, then auto-build from productEntries
|
|
217
213
|
let viewportsData: Record<string, { items: NormalizedProductItem[] }>;
|
|
218
214
|
|
|
@@ -221,13 +217,13 @@ export async function renderDrawing(input: RenderDrawingInput): Promise<RenderDr
|
|
|
221
217
|
viewports.map((vp) => [vp.id, { items: [] as NormalizedProductItem[] }])
|
|
222
218
|
);
|
|
223
219
|
viewportsData = { ...emptyDefaults, ...options.viewportItems };
|
|
224
|
-
} else if (
|
|
220
|
+
} else if (allEntries.length) {
|
|
225
221
|
// Auto-build items per viewport from raw product entries
|
|
226
222
|
const sampleColor = getSampleColor();
|
|
227
223
|
viewportsData = {};
|
|
228
224
|
for (const vp of viewports) {
|
|
229
225
|
viewportsData[vp.id] = {
|
|
230
|
-
items: buildItemsFromEntries(
|
|
226
|
+
items: buildItemsFromEntries(entriesFor(vp.id), vp.fills, sampleColor)
|
|
231
227
|
};
|
|
232
228
|
}
|
|
233
229
|
} else {
|
|
@@ -258,18 +254,29 @@ export async function renderDrawing(input: RenderDrawingInput): Promise<RenderDr
|
|
|
258
254
|
warnings.push(`Template references property '${path}' but no items provide it.`);
|
|
259
255
|
}
|
|
260
256
|
}
|
|
257
|
+
} else if (
|
|
258
|
+
reqs.propertyPaths.length > 0 ||
|
|
259
|
+
template.elements.some((el) => el.kind === 'labels' || el.kind === 'legend')
|
|
260
|
+
) {
|
|
261
|
+
// Item-driven features with zero items is almost always a missing
|
|
262
|
+
// productEntries/viewportItems input, not an intentionally empty sheet.
|
|
263
|
+
warnings.push(
|
|
264
|
+
'Template uses item data (labels/legend/where-filters) but no items were provided — pass options.productEntries or options.viewportItems.'
|
|
265
|
+
);
|
|
261
266
|
}
|
|
262
267
|
|
|
263
|
-
if (reqs.
|
|
264
|
-
warnings.push('Template uses remote
|
|
268
|
+
if (reqs.usesRemoteResources && !template.allowRemoteResources && !options?.allowRemoteResources) {
|
|
269
|
+
warnings.push('Template uses remote resources but allowRemoteResources is not enabled.');
|
|
265
270
|
}
|
|
266
271
|
|
|
267
272
|
// Build metadata from product entries (same as standard path)
|
|
268
|
-
|
|
269
|
-
|
|
273
|
+
// Caller-supplied metadata first: entries only cover cut-bearing products, but
|
|
274
|
+
// include filtering must classify products whose geometry never crosses the cut
|
|
275
|
+
// (furniture below a plan cut) - renderTemplateSheet supplies the full map.
|
|
276
|
+
const metadataById = { ...(options?.metadataById ?? {}), ...buildMetadataMap(allEntries) };
|
|
270
277
|
|
|
271
278
|
const result = await renderLayout(template, layoutData, input.models, {
|
|
272
|
-
|
|
279
|
+
allowRemoteResources: template.allowRemoteResources ?? options?.allowRemoteResources,
|
|
273
280
|
qrContext: options?.qrContext,
|
|
274
281
|
metadataById
|
|
275
282
|
});
|
|
@@ -302,7 +309,6 @@ export async function renderDrawing(input: RenderDrawingInput): Promise<RenderDr
|
|
|
302
309
|
// ---- Build section state + model view per viewport ----
|
|
303
310
|
const models: Record<string, SvgModelView> = {};
|
|
304
311
|
const entriesByViewport: Record<string, SectionResult['perProduct']> = {};
|
|
305
|
-
const sectionStateCache = new Map<string, SectionRenderState | null>();
|
|
306
312
|
|
|
307
313
|
for (const vp of viewports) {
|
|
308
314
|
const sectionResult = sections[vp.id];
|
|
@@ -313,39 +319,44 @@ export async function renderDrawing(input: RenderDrawingInput): Promise<RenderDr
|
|
|
313
319
|
|
|
314
320
|
entriesByViewport[vp.id] = sectionResult.perProduct;
|
|
315
321
|
|
|
316
|
-
// Dedup: skip if already built for this viewport key
|
|
317
|
-
if (sectionStateCache.has(vp.id)) {
|
|
318
|
-
const cached = sectionStateCache.get(vp.id);
|
|
319
|
-
if (cached) {
|
|
320
|
-
models[vp.id] = sectionStateToModelView(cached);
|
|
321
|
-
}
|
|
322
|
-
continue;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
322
|
const resolved = resolveLineStyles(vp);
|
|
326
323
|
|
|
327
324
|
// Estimate label font size from template style and section bounds
|
|
328
325
|
const labelFontSize = estimateLabelFontSize(template, vp, sectionResult.perProduct);
|
|
329
326
|
|
|
330
|
-
// Extract clip polygon from section results if clip config is present
|
|
327
|
+
// Extract clip polygon from section results if clip config is present.
|
|
328
|
+
// Matching semantics MUST mirror matchesFilter (viewportPrep/labels/
|
|
329
|
+
// legend all use it on the same clip config): class optional,
|
|
330
|
+
// Ifc prefix optional, case-insensitive values, arrays mean any-of.
|
|
331
331
|
let clipPolygon: Array<Array<{ x: number; z: number }>> | undefined;
|
|
332
|
-
if (vp.clip?.where
|
|
333
|
-
const clipClass = Array.isArray(vp.clip.class) ? vp.clip.class : [vp.clip.class];
|
|
332
|
+
if (vp.clip?.where) {
|
|
333
|
+
const clipClass = vp.clip.class ? (Array.isArray(vp.clip.class) ? vp.clip.class : [vp.clip.class]) : null;
|
|
334
334
|
const clipEntries = sectionResult.perProduct.filter((entry: any) => {
|
|
335
335
|
const product = entry.product;
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
//
|
|
336
|
+
// Shared helper rather than product.class.is(): that compares with ===, so a clip
|
|
337
|
+
// filter of "SPACE" or "IFCWALL" selected nothing while labels and legend — reading
|
|
338
|
+
// the same clip config through matchesFilter — matched it. The clip silently did
|
|
339
|
+
// not apply, with no warning anywhere.
|
|
340
|
+
if (!matchesProductClass(product, clipClass ?? undefined)) return false;
|
|
339
341
|
for (const [path, expected] of Object.entries(vp.clip!.where!)) {
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
342
|
+
const actual = normalizeValue(resolveProductProperty(product, path));
|
|
343
|
+
const matches = Array.isArray(expected)
|
|
344
|
+
? expected.some((v) => normalizeValue(v) === actual)
|
|
345
|
+
: actual === normalizeValue(expected);
|
|
346
|
+
if (!matches) return false;
|
|
343
347
|
}
|
|
344
348
|
return true;
|
|
345
349
|
});
|
|
346
350
|
if (clipEntries.length) {
|
|
347
351
|
clipPolygon = clipEntries.flatMap((entry: any) =>
|
|
348
|
-
(entry.polylines || []).map((pl: any) =>
|
|
352
|
+
(entry.polylines || []).map((pl: any) =>
|
|
353
|
+
(pl.points || [])
|
|
354
|
+
.map((p: any) => readXZ(p))
|
|
355
|
+
.filter(
|
|
356
|
+
(p: { x: number | null; z: number | null }): p is { x: number; z: number } =>
|
|
357
|
+
p.x !== null && p.z !== null
|
|
358
|
+
)
|
|
359
|
+
)
|
|
349
360
|
);
|
|
350
361
|
}
|
|
351
362
|
}
|
|
@@ -374,8 +385,6 @@ export async function renderDrawing(input: RenderDrawingInput): Promise<RenderDr
|
|
|
374
385
|
planeNormal
|
|
375
386
|
});
|
|
376
387
|
|
|
377
|
-
sectionStateCache.set(vp.id, state);
|
|
378
|
-
|
|
379
388
|
if (!state) {
|
|
380
389
|
warnings.push(`No geometry for viewport '${vp.id}'.`);
|
|
381
390
|
continue;
|
|
@@ -423,7 +432,10 @@ export async function renderDrawing(input: RenderDrawingInput): Promise<RenderDr
|
|
|
423
432
|
|
|
424
433
|
// ---- Build metadata from all section entries ----
|
|
425
434
|
const allEntries = Object.values(entriesByViewport).flat();
|
|
426
|
-
|
|
435
|
+
// Caller-supplied metadata first: entries only cover cut-bearing products, but
|
|
436
|
+
// include filtering must classify products whose geometry never crosses the cut
|
|
437
|
+
// (furniture below a plan cut) - renderTemplateSheet supplies the full map.
|
|
438
|
+
const metadataById = { ...(options?.metadataById ?? {}), ...buildMetadataMap(allEntries) };
|
|
427
439
|
|
|
428
440
|
// ---- Assemble layout data ----
|
|
429
441
|
const layoutData = {
|
|
@@ -432,7 +444,7 @@ export async function renderDrawing(input: RenderDrawingInput): Promise<RenderDr
|
|
|
432
444
|
|
|
433
445
|
// ---- Render ----
|
|
434
446
|
const result = await renderLayout(template, layoutData, models, {
|
|
435
|
-
|
|
447
|
+
allowRemoteResources: template.allowRemoteResources ?? options?.allowRemoteResources,
|
|
436
448
|
qrContext: options?.qrContext,
|
|
437
449
|
metadataById
|
|
438
450
|
});
|
|
@@ -511,13 +523,12 @@ function estimateLabelFontSize(
|
|
|
511
523
|
for (const polyline of entry.polylines) {
|
|
512
524
|
if (!polyline.points) continue;
|
|
513
525
|
for (const pt of polyline.points) {
|
|
514
|
-
const px =
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
if (
|
|
518
|
-
if (
|
|
519
|
-
if (
|
|
520
|
-
if ((pz as number) > maxZ) maxZ = pz as number;
|
|
526
|
+
const { x: px, z: pz } = readXZ(pt);
|
|
527
|
+
if (px !== null && pz !== null) {
|
|
528
|
+
if (px < minX) minX = px;
|
|
529
|
+
if (px > maxX) maxX = px;
|
|
530
|
+
if (pz < minZ) minZ = pz;
|
|
531
|
+
if (pz > maxZ) maxZ = pz;
|
|
521
532
|
}
|
|
522
533
|
}
|
|
523
534
|
}
|
|
@@ -571,11 +582,12 @@ export const BUILTIN_HIDDEN_DEFAULTS: Required<ViewportLineStyle> = {
|
|
|
571
582
|
enabled: true
|
|
572
583
|
};
|
|
573
584
|
|
|
574
|
-
type LineCategory =
|
|
585
|
+
export type LineCategory =
|
|
575
586
|
| 'cutLines'
|
|
576
587
|
| 'visibleLines'
|
|
577
588
|
| 'hiddenLines'
|
|
578
|
-
| '
|
|
589
|
+
| 'silhouettesVisible'
|
|
590
|
+
| 'silhouettesHidden'
|
|
579
591
|
| 'featureEdges'
|
|
580
592
|
| 'stairArrows'
|
|
581
593
|
| 'outlineVisible'
|
|
@@ -585,28 +597,59 @@ type LineCategory =
|
|
|
585
597
|
export const CATEGORY_BUILTIN_OVERRIDES: Partial<Record<LineCategory, Partial<ViewportLineStyle>>> = {
|
|
586
598
|
cutLines: { widthMm: 0.02 },
|
|
587
599
|
hiddenLines: BUILTIN_HIDDEN_DEFAULTS,
|
|
600
|
+
// Occluded silhouettes follow the hidden drafting convention out of the box.
|
|
601
|
+
silhouettesHidden: BUILTIN_HIDDEN_DEFAULTS,
|
|
588
602
|
outlineHidden: BUILTIN_HIDDEN_DEFAULTS
|
|
589
603
|
};
|
|
590
604
|
|
|
605
|
+
/** Visibility group of each category; keyed to the `defaultVisibleLines`/`defaultHiddenLines` template keys. `stairArrows` is annotation, not geometry, and belongs to neither group. */
|
|
606
|
+
const CATEGORY_VISIBILITY_GROUP: Partial<Record<LineCategory, 'defaultVisibleLines' | 'defaultHiddenLines'>> = {
|
|
607
|
+
cutLines: 'defaultVisibleLines',
|
|
608
|
+
visibleLines: 'defaultVisibleLines',
|
|
609
|
+
silhouettesVisible: 'defaultVisibleLines',
|
|
610
|
+
silhouettesHidden: 'defaultHiddenLines',
|
|
611
|
+
featureEdges: 'defaultVisibleLines',
|
|
612
|
+
outlineVisible: 'defaultVisibleLines',
|
|
613
|
+
hiddenLines: 'defaultHiddenLines',
|
|
614
|
+
outlineHidden: 'defaultHiddenLines'
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Resolve the effective line style for one category — the single source of
|
|
619
|
+
* truth used by both the slab plan path (slabModelView) and the drawing path.
|
|
620
|
+
*
|
|
621
|
+
* Cascade, weakest first: built-in defaults → `default` → built-in category
|
|
622
|
+
* defaults → `defaultVisibleLines`/`defaultHiddenLines` (the category's
|
|
623
|
+
* visibility group) → the category's own key.
|
|
624
|
+
*/
|
|
625
|
+
export function resolveCategoryLineStyle(
|
|
626
|
+
category: LineCategory,
|
|
627
|
+
lineStyles?: ViewportLineStyles
|
|
628
|
+
): Required<ViewportLineStyle> {
|
|
629
|
+
const group = CATEGORY_VISIBILITY_GROUP[category];
|
|
630
|
+
return {
|
|
631
|
+
...BUILTIN_LINE_DEFAULTS,
|
|
632
|
+
...(lineStyles?.default ?? {}),
|
|
633
|
+
...(CATEGORY_BUILTIN_OVERRIDES[category] ?? {}),
|
|
634
|
+
...((group && lineStyles?.[group]) ?? {}),
|
|
635
|
+
...(lineStyles?.[category] ?? {})
|
|
636
|
+
} as Required<ViewportLineStyle>;
|
|
637
|
+
}
|
|
638
|
+
|
|
591
639
|
export interface ResolvedLineStyles {
|
|
592
640
|
default: Required<ViewportLineStyle>;
|
|
593
641
|
byCategory: Record<LineCategory, Required<ViewportLineStyle>>;
|
|
594
642
|
}
|
|
595
643
|
|
|
596
644
|
/**
|
|
597
|
-
* Resolve the effective line style for every category
|
|
598
|
-
*
|
|
599
|
-
* Priority: category entry in `lineStyles` → `lineStyles.default`
|
|
600
|
-
* → `lineWidthMm`/`lineColor` shorthand → built-in defaults.
|
|
645
|
+
* Resolve the effective line style for every category via
|
|
646
|
+
* {@link resolveCategoryLineStyle} (shared with the slab plan path).
|
|
601
647
|
*/
|
|
602
648
|
function resolveLineStyles(vp: ViewportElement): ResolvedLineStyles {
|
|
603
649
|
const ls = vp.lineStyles;
|
|
604
650
|
|
|
605
|
-
// Base default: lineStyles.default → shorthand → built-in
|
|
606
651
|
const base: Required<ViewportLineStyle> = {
|
|
607
652
|
...BUILTIN_LINE_DEFAULTS,
|
|
608
|
-
...(vp.lineWidthMm !== undefined ? { widthMm: vp.lineWidthMm } : {}),
|
|
609
|
-
...(vp.lineColor !== undefined ? { color: vp.lineColor } : {}),
|
|
610
653
|
...(ls?.default ?? {})
|
|
611
654
|
} as Required<ViewportLineStyle>;
|
|
612
655
|
|
|
@@ -614,38 +657,17 @@ function resolveLineStyles(vp: ViewportElement): ResolvedLineStyles {
|
|
|
614
657
|
'cutLines',
|
|
615
658
|
'visibleLines',
|
|
616
659
|
'hiddenLines',
|
|
617
|
-
'
|
|
660
|
+
'silhouettesVisible',
|
|
661
|
+
'silhouettesHidden',
|
|
618
662
|
'featureEdges',
|
|
619
663
|
'stairArrows',
|
|
620
664
|
'outlineVisible',
|
|
621
665
|
'outlineHidden'
|
|
622
666
|
];
|
|
623
667
|
|
|
624
|
-
// Category group mapping: individual categories inherit from their group style
|
|
625
|
-
const categoryGroups: Record<LineCategory, LineCategory> = {
|
|
626
|
-
cutLines: 'visibleLines',
|
|
627
|
-
visibleLines: 'visibleLines',
|
|
628
|
-
silhouettes: 'visibleLines',
|
|
629
|
-
featureEdges: 'visibleLines',
|
|
630
|
-
outlineVisible: 'visibleLines',
|
|
631
|
-
hiddenLines: 'hiddenLines',
|
|
632
|
-
outlineHidden: 'hiddenLines',
|
|
633
|
-
stairArrows: 'stairArrows'
|
|
634
|
-
};
|
|
635
|
-
|
|
636
668
|
const byCategory = {} as Record<LineCategory, Required<ViewportLineStyle>>;
|
|
637
669
|
for (const cat of categories) {
|
|
638
|
-
|
|
639
|
-
const groupName = categoryGroups[cat];
|
|
640
|
-
const groupStyle = ls?.[groupName]; // Group style (visibleLines, hiddenLines, stairArrows)
|
|
641
|
-
const categoryOverride = ls?.[cat]; // Individual category settings (enable/disable)
|
|
642
|
-
|
|
643
|
-
byCategory[cat] = {
|
|
644
|
-
...base,
|
|
645
|
-
...(builtinOverride ?? {}),
|
|
646
|
-
...(groupStyle ?? {}), // Apply group style first
|
|
647
|
-
...(categoryOverride ?? {}) // Then individual overrides (enable/disable)
|
|
648
|
-
} as Required<ViewportLineStyle>;
|
|
670
|
+
byCategory[cat] = resolveCategoryLineStyle(cat, ls);
|
|
649
671
|
}
|
|
650
672
|
|
|
651
673
|
return { default: base, byCategory };
|
|
@@ -667,7 +689,7 @@ export function resolveFillColor(
|
|
|
667
689
|
): [number, number, number, number] | undefined {
|
|
668
690
|
if (!fills?.length) return undefined;
|
|
669
691
|
for (const rule of fills) {
|
|
670
|
-
if (!
|
|
692
|
+
if (!matchesFilter(item, rule.class, rule.where)) continue;
|
|
671
693
|
if (rule.fill === 'sample') {
|
|
672
694
|
const sampled = sampleColor(product);
|
|
673
695
|
if (sampled) {
|