@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
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { escapeAttr, getPathValue, resolvePath } from './renderUtils';
|
|
2
|
-
import { rotateBounds, pickScale } from './viewportUtils';
|
|
1
|
+
import { escapeAttr, getPathValue, resolvePath, formatNumber } from './renderUtils';
|
|
2
|
+
import { rotateBounds, pickScale, rotatedContentExtents } from './viewportUtils';
|
|
3
3
|
import { ViewportRenderInfo } from './labelsRender';
|
|
4
|
-
import {
|
|
5
|
-
import { ComputedViewportScale, SvgModelView, ViewportElement,
|
|
4
|
+
import { matchesFilter } from './filters';
|
|
5
|
+
import { ComputedViewportScale, SvgModelView, ViewportElement, ResourceSpec, ResolvedIncludeFilter } from './types';
|
|
6
6
|
import { isRecord, getProp } from './typeGuards';
|
|
7
|
+
import type { LegendColorAssignment } from './legendRender';
|
|
7
8
|
|
|
8
9
|
type PolyPoint = { x: number; y: number };
|
|
9
10
|
// Debug overlay for clip polygons. Keep false for normal builds.
|
|
@@ -41,7 +42,7 @@ export const renderViewportElement = (
|
|
|
41
42
|
dataWithViewport: any,
|
|
42
43
|
preferredScales: number[],
|
|
43
44
|
scales: Record<string, { preferredScales?: number[] }>,
|
|
44
|
-
|
|
45
|
+
resourceMap: Map<string, ResourceSpec>,
|
|
45
46
|
viewportScales: Record<string, ComputedViewportScale>,
|
|
46
47
|
viewportClipPolygonsLocal: Record<string, Array<Array<PolyPoint>>>,
|
|
47
48
|
viewportClipBounds: Record<
|
|
@@ -51,12 +52,12 @@ export const renderViewportElement = (
|
|
|
51
52
|
viewportClipLabels: Record<string, boolean>,
|
|
52
53
|
viewportClipStyles: Record<
|
|
53
54
|
string,
|
|
54
|
-
{
|
|
55
|
+
{ color?: string; widthMm?: number; opacity?: number; dash?: string; fill?: string; fillOpacity?: number }
|
|
55
56
|
>,
|
|
56
57
|
viewportHighlight: Record<string, { gids: Set<string> | null; styles: any }>,
|
|
57
58
|
viewportRenderInfo: Record<string, ViewportRenderInfo>,
|
|
58
59
|
warnings: string[],
|
|
59
|
-
legendColorMaps?: Map<string,
|
|
60
|
+
legendColorMaps?: Map<string, LegendColorAssignment>,
|
|
60
61
|
precomputed?: {
|
|
61
62
|
effectiveWidthModel?: number;
|
|
62
63
|
effectiveHeightModel?: number;
|
|
@@ -65,7 +66,8 @@ export const renderViewportElement = (
|
|
|
65
66
|
clipBounds?: { minX: number; maxX: number; minY: number; maxY: number; width: number; height: number };
|
|
66
67
|
renderCenterModelX?: number;
|
|
67
68
|
renderCenterModelY?: number;
|
|
68
|
-
}
|
|
69
|
+
},
|
|
70
|
+
includeFilters?: ResolvedIncludeFilter[] | null
|
|
69
71
|
): string[] => {
|
|
70
72
|
const parts: string[] = [];
|
|
71
73
|
if (!baseModel) {
|
|
@@ -101,7 +103,19 @@ export const renderViewportElement = (
|
|
|
101
103
|
const mmPerModel = scaleInfo.mmPerModelUnit;
|
|
102
104
|
const contentWidthMm = renderWidthModel * mmPerModel;
|
|
103
105
|
const contentHeightMm = renderHeightModel * mmPerModel;
|
|
104
|
-
|
|
106
|
+
// Tight bounds of the rotated geometry (see rotatedContentExtents): sizes the
|
|
107
|
+
// content box and re-centers the rotation pivot so diagonally-oriented models
|
|
108
|
+
// straightened via rotationDeg fill the viewport instead of shrinking to the
|
|
109
|
+
// inflated AABB-of-AABB. Clip keeps the AABB path — its bounds are the clip
|
|
110
|
+
// region, not the full geometry.
|
|
111
|
+
const tight = hasClip ? null : rotatedContentExtents(model, rotationDeg);
|
|
112
|
+
if (tight && precomputed?.renderCenterModelX == null && precomputed?.renderCenterModelY == null) {
|
|
113
|
+
renderCenterModelX = tight.centerViewX;
|
|
114
|
+
renderCenterModelY = tight.centerViewY;
|
|
115
|
+
}
|
|
116
|
+
const rotated = tight
|
|
117
|
+
? { width: tight.width * mmPerModel, height: tight.height * mmPerModel }
|
|
118
|
+
: rotateBounds(contentWidthMm, contentHeightMm, rotationDeg);
|
|
105
119
|
|
|
106
120
|
const centerX = vp.align === 'topLeft' ? vp.xMm + rotated.width / 2 : vp.xMm + vp.widthMm / 2;
|
|
107
121
|
const centerY = vp.align === 'topLeft' ? vp.yMm + rotated.height / 2 : vp.yMm + vp.heightMm / 2;
|
|
@@ -113,37 +127,13 @@ export const renderViewportElement = (
|
|
|
113
127
|
let clipId: string | null = null;
|
|
114
128
|
const clipDebugPolys: string[] = [];
|
|
115
129
|
const clipStyle = viewportClipStyles[vp.id];
|
|
116
|
-
const clipStroke = clipStyle?.
|
|
117
|
-
const clipStrokeWidth = clipStyle?.
|
|
118
|
-
const clipStrokeOpacity = clipStyle?.
|
|
130
|
+
const clipStroke = clipStyle?.color;
|
|
131
|
+
const clipStrokeWidth = clipStyle?.widthMm;
|
|
132
|
+
const clipStrokeOpacity = clipStyle?.opacity;
|
|
133
|
+
const clipDash = clipStyle?.dash;
|
|
119
134
|
const clipFill = clipStyle?.fill;
|
|
120
135
|
const clipFillOpacity = clipStyle?.fillOpacity;
|
|
121
|
-
const transformAttr = `translate(${centerX} ${centerY}) rotate(${rotationDeg}) scale(${mmPerModel}) translate(${-renderCenterModelX} ${-renderCenterModelY})`;
|
|
122
|
-
|
|
123
|
-
// Debug: log model bounds and path breakdown
|
|
124
|
-
// if (model.transform) {
|
|
125
|
-
// console.log(`[DEBUG MODEL vp="${vp.id}"] Model bounds: viewMinX=${model.transform.viewMinX.toFixed(2)} viewMaxX=${model.transform.viewMaxX?.toFixed(2)} viewMinZ=${model.transform.viewMinZ.toFixed(2)} viewMaxZ=${model.transform.viewMaxZ.toFixed(2)} viewWidth=${model.transform.viewWidth.toFixed(2)} viewHeight=${model.transform.viewHeight.toFixed(2)}`);
|
|
126
|
-
|
|
127
|
-
// if (model.rawPaths) {
|
|
128
|
-
// const fills = model.rawPaths.filter(p => p.meta?.isFill);
|
|
129
|
-
// const segments = model.rawPaths.filter(p => p.meta?.isSegment);
|
|
130
|
-
// const other = model.rawPaths.filter(p => !p.meta?.isFill && !p.meta?.isSegment);
|
|
131
|
-
// console.log(`[DEBUG MODEL vp="${vp.id}"] Path breakdown: ${model.rawPaths.length} total (${fills.length} fills, ${segments.length} segments, ${other.length} other)`);
|
|
132
|
-
|
|
133
|
-
// // Sample a few segment paths
|
|
134
|
-
// const sampleSegments = segments.slice(0, 3);
|
|
135
|
-
// sampleSegments.forEach((seg, idx) => {
|
|
136
|
-
// console.log(`[DEBUG MODEL vp="${vp.id}"] Segment ${idx}: class=${seg.meta?.class} fill=${seg.fill} stroke=${seg.stroke} strokeWidth=${seg.strokeWidth} d=${seg.d.substring(0, 50)}...`);
|
|
137
|
-
// });
|
|
138
|
-
|
|
139
|
-
// // Check if geometryTransform is present
|
|
140
|
-
// if (model.geometryTransform) {
|
|
141
|
-
// console.log(`[DEBUG MODEL vp="${vp.id}"] geometryTransform="${model.geometryTransform}"`);
|
|
142
|
-
// } else {
|
|
143
|
-
// console.log(`[DEBUG MODEL vp="${vp.id}"] WARNING: geometryTransform is MISSING!`);
|
|
144
|
-
// }
|
|
145
|
-
// }
|
|
146
|
-
// }
|
|
136
|
+
const transformAttr = `translate(${formatNumber(centerX)} ${formatNumber(centerY)}) rotate(${rotationDeg}) scale(${mmPerModel}) translate(${formatNumber(-renderCenterModelX)} ${formatNumber(-renderCenterModelY)})`;
|
|
147
137
|
|
|
148
138
|
// Guard: detect NaN/Infinity in computed transform values
|
|
149
139
|
if (
|
|
@@ -159,14 +149,11 @@ export const renderViewportElement = (
|
|
|
159
149
|
return parts;
|
|
160
150
|
}
|
|
161
151
|
|
|
162
|
-
// console.log(`[DEBUG CLIP APPLY vp="${vp.id}"] clipPolysLocal exists: ${!!clipPolysLocal}, length: ${clipPolysLocal?.length ?? 0}`);
|
|
163
152
|
if (clipPolysLocal && clipPolysLocal.length) {
|
|
164
|
-
// console.log(`[DEBUG CLIP APPLY vp="${vp.id}"] *** APPLYING CLIP-PATH *** with ${clipPolysLocal.length} polygons`);
|
|
165
153
|
if (clipPolysLocal.every((poly) => !poly.length)) {
|
|
166
154
|
warnings.push(`Viewport '${vp.id}' clip has no points after prep; skipping clip.`);
|
|
167
155
|
}
|
|
168
156
|
clipId = `clip-${vp.id}-${parts.length}`;
|
|
169
|
-
const polyPartsPage: string[] = [];
|
|
170
157
|
// Clip polygon points are in model-view coordinates (not SVG-internal),
|
|
171
158
|
// so transform them with outer only (model-view → page). Do NOT apply
|
|
172
159
|
// geometryTransform — that maps SVG-internal → model-view, which is only
|
|
@@ -179,33 +166,21 @@ export const renderViewportElement = (
|
|
|
179
166
|
)
|
|
180
167
|
);
|
|
181
168
|
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
clipMaxY = -Infinity;
|
|
187
|
-
clipPolysLocal.forEach((poly) => {
|
|
188
|
-
poly.forEach((p) => {
|
|
189
|
-
if (p.x < clipMinX) clipMinX = p.x;
|
|
190
|
-
if (p.x > clipMaxX) clipMaxX = p.x;
|
|
191
|
-
if (p.y < clipMinY) clipMinY = p.y;
|
|
192
|
-
if (p.y > clipMaxY) clipMaxY = p.y;
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
// console.log(`[DEBUG CLIP vp="${vp.id}"] Local bounds: x=${clipMinX.toFixed(2)}..${clipMaxX.toFixed(2)} y=${clipMinY.toFixed(2)}..${clipMaxY.toFixed(2)} (w=${(clipMaxX - clipMinX).toFixed(2)} h=${(clipMaxY - clipMinY).toFixed(2)})`);
|
|
196
|
-
|
|
169
|
+
// Rings from unioned clips can include holes — a single even-odd path
|
|
170
|
+
// (rather than one polygon per ring) makes holes subtract correctly in
|
|
171
|
+
// both the clip mask and any fill.
|
|
172
|
+
const ringDs: string[] = [];
|
|
197
173
|
clipPolysLocal.forEach((poly) => {
|
|
198
174
|
const pagePtsArr = poly.map((p) => matApply(outer, p));
|
|
199
|
-
const pagePts = pagePtsArr.map((p) => `${p.x},${p.y}`).join(' ');
|
|
200
|
-
|
|
201
|
-
if (clipStroke
|
|
175
|
+
const pagePts = pagePtsArr.map((p) => `${formatNumber(p.x)},${formatNumber(p.y)}`).join(' ');
|
|
176
|
+
ringDs.push('M ' + pagePtsArr.map((p) => `${formatNumber(p.x)} ${formatNumber(p.y)}`).join(' L ') + ' Z');
|
|
177
|
+
if (clipStroke) {
|
|
202
178
|
clipDebugPolys.push(
|
|
203
|
-
`<polygon points="${pagePts}"` +
|
|
204
|
-
|
|
179
|
+
`<polygon points="${pagePts}" fill="none"` +
|
|
180
|
+
` stroke="${escapeAttr(clipStroke)}"` +
|
|
205
181
|
(clipStrokeWidth ? ` stroke-width="${clipStrokeWidth}"` : '') +
|
|
206
182
|
(clipStrokeOpacity !== undefined ? ` stroke-opacity="${clipStrokeOpacity}"` : '') +
|
|
207
|
-
(
|
|
208
|
-
(clipFillOpacity !== undefined ? ` fill-opacity="${clipFillOpacity}"` : '') +
|
|
183
|
+
(clipDash ? ` stroke-dasharray="${escapeAttr(clipDash)}"` : '') +
|
|
209
184
|
` />`
|
|
210
185
|
);
|
|
211
186
|
}
|
|
@@ -215,8 +190,15 @@ export const renderViewportElement = (
|
|
|
215
190
|
);
|
|
216
191
|
}
|
|
217
192
|
});
|
|
193
|
+
if (clipFill && clipFill !== 'none') {
|
|
194
|
+
clipDebugPolys.unshift(
|
|
195
|
+
`<path d="${ringDs.join(' ')}" fill-rule="evenodd" fill="${escapeAttr(clipFill)}"` +
|
|
196
|
+
(clipFillOpacity !== undefined ? ` fill-opacity="${clipFillOpacity}"` : '') +
|
|
197
|
+
` stroke="none" />`
|
|
198
|
+
);
|
|
199
|
+
}
|
|
218
200
|
parts.push(
|
|
219
|
-
`<defs><clipPath id="${clipId}" clipPathUnits="userSpaceOnUse"
|
|
201
|
+
`<defs><clipPath id="${clipId}" clipPathUnits="userSpaceOnUse"><path d="${ringDs.join(' ')}" clip-rule="evenodd" /></clipPath></defs>`
|
|
220
202
|
);
|
|
221
203
|
}
|
|
222
204
|
|
|
@@ -225,35 +207,25 @@ export const renderViewportElement = (
|
|
|
225
207
|
|
|
226
208
|
// Render fill polygons below structural lines
|
|
227
209
|
const allowedGids = viewportHighlight[vp.id]?.gids ?? null;
|
|
228
|
-
const fillSvg = buildFillSvg(vp, dataWithViewport, model, allowedGids, legendColorMaps);
|
|
210
|
+
const fillSvg = buildFillSvg(vp, dataWithViewport, model, allowedGids, legendColorMaps, warnings, includeFilters);
|
|
229
211
|
if (fillSvg) parts.push(fillSvg);
|
|
230
212
|
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Compensate stroke-widths for the viewport scale transform.
|
|
250
|
-
// Paths have stroke-width in model units; the scale(mmPerModel) transform
|
|
251
|
-
// would make them thinner/thicker depending on zoom. Dividing by mmPerModel
|
|
252
|
-
// ensures strokes render at their intended mm size regardless of viewport scale.
|
|
253
|
-
const compensatedSvg = model.groupSvg.replace(
|
|
254
|
-
/stroke-width="([0-9.eE+-]+)"/g,
|
|
255
|
-
(_, w) => `stroke-width="${parseFloat(w) / mmPerModel}"`
|
|
256
|
-
);
|
|
213
|
+
// Compensate stroke-widths AND dash arrays for the viewport scale
|
|
214
|
+
// transform. Path styling is authored in mm; the scale(mmPerModel)
|
|
215
|
+
// transform would multiply both, so divide them out — otherwise
|
|
216
|
+
// stroke-dasharray "4 2" means 4 model units (metres), not 4 mm, and the
|
|
217
|
+
// dash period changes with the viewport scale.
|
|
218
|
+
const compensatedSvg = model.groupSvg
|
|
219
|
+
.replace(/stroke-width="([0-9.eE+-]+)"/g, (_, w) => `stroke-width="${parseFloat(w) / mmPerModel}"`)
|
|
220
|
+
.replace(
|
|
221
|
+
/stroke-dasharray="([0-9.eE+\-, ]+)"/g,
|
|
222
|
+
(_, dashes: string) =>
|
|
223
|
+
`stroke-dasharray="${dashes
|
|
224
|
+
.split(/[\s,]+/)
|
|
225
|
+
.filter(Boolean)
|
|
226
|
+
.map((d) => parseFloat(d) / mmPerModel)
|
|
227
|
+
.join(' ')}"`
|
|
228
|
+
);
|
|
257
229
|
parts.push(compensatedSvg);
|
|
258
230
|
parts.push('</g>');
|
|
259
231
|
parts.push('</g>');
|
|
@@ -282,30 +254,30 @@ export const renderViewportElement = (
|
|
|
282
254
|
parts.push(
|
|
283
255
|
`<rect x="${vp.xMm}" y="${vp.yMm}" width="${vp.widthMm}" height="${vp.heightMm}" stroke="${escapeAttr(
|
|
284
256
|
borderStroke
|
|
285
|
-
)}" stroke-width="${borderWidth}" fill="none"${vp.border.
|
|
257
|
+
)}" stroke-width="${borderWidth}" fill="none"${vp.border.dash ? ` stroke-dasharray="${escapeAttr(vp.border.dash)}"` : ''} />`
|
|
286
258
|
);
|
|
287
259
|
}
|
|
288
260
|
|
|
289
261
|
if (vp.northArrow) {
|
|
290
262
|
const arrow = vp.northArrow;
|
|
291
|
-
const
|
|
263
|
+
const resource = resourceMap.get(arrow.resourceId);
|
|
292
264
|
const size = arrow.sizeMm ?? 12;
|
|
293
265
|
const offsetX = arrow.offsetXMm ?? 8;
|
|
294
266
|
const offsetY = arrow.offsetYMm ?? 8;
|
|
295
267
|
const arrowX = vp.xMm + vp.widthMm - offsetX - size;
|
|
296
268
|
const arrowY = vp.yMm + offsetY;
|
|
297
269
|
const arrowRotation = arrow.rotationDeg ?? 0;
|
|
298
|
-
if (!
|
|
299
|
-
warnings.push(`Missing
|
|
300
|
-
} else if (
|
|
270
|
+
if (!resource) {
|
|
271
|
+
warnings.push(`Missing resource '${arrow.resourceId}' for north arrow on viewport '${vp.id}'.`);
|
|
272
|
+
} else if (resource.src) {
|
|
301
273
|
parts.push(
|
|
302
|
-
`<image href="${escapeAttr(
|
|
274
|
+
`<image href="${escapeAttr(resource.src)}" x="${arrowX}" y="${arrowY}" width="${size}" height="${size}" transform="rotate(${arrowRotation}, ${arrowX + size / 2}, ${
|
|
303
275
|
arrowY + size / 2
|
|
304
276
|
})" />`
|
|
305
277
|
);
|
|
306
|
-
} else if (
|
|
278
|
+
} else if (resource.content) {
|
|
307
279
|
parts.push(
|
|
308
|
-
`<g transform="translate(${arrowX} ${arrowY}) rotate(${arrowRotation} ${size / 2} ${size / 2})">${
|
|
280
|
+
`<g transform="translate(${arrowX} ${arrowY}) rotate(${arrowRotation} ${size / 2} ${size / 2})">${resource.content}</g>`
|
|
309
281
|
);
|
|
310
282
|
}
|
|
311
283
|
}
|
|
@@ -327,7 +299,9 @@ function buildFillSvg(
|
|
|
327
299
|
data: any,
|
|
328
300
|
model: SvgModelView,
|
|
329
301
|
allowedGids: Set<string> | null,
|
|
330
|
-
legendColorMaps
|
|
302
|
+
legendColorMaps: Map<string, LegendColorAssignment> | undefined,
|
|
303
|
+
warnings: string[],
|
|
304
|
+
includeFilters?: ResolvedIncludeFilter[] | null
|
|
331
305
|
): string | null {
|
|
332
306
|
const fills = vp.fills;
|
|
333
307
|
if (!fills?.length || !model.transform) return null;
|
|
@@ -342,7 +316,7 @@ function buildFillSvg(
|
|
|
342
316
|
const opacity = rule.fillOpacity ?? 0.35;
|
|
343
317
|
// Find items matching this fill rule
|
|
344
318
|
let matching = items.filter((item: unknown) =>
|
|
345
|
-
isRecord(item) ?
|
|
319
|
+
isRecord(item) ? matchesFilter(item, rule.class, rule.where) : false
|
|
346
320
|
);
|
|
347
321
|
|
|
348
322
|
// Filter by allowed GIDs (from clip polygon center point test)
|
|
@@ -352,20 +326,23 @@ function buildFillSvg(
|
|
|
352
326
|
const gid = getProp(item, 'gid') ?? getProp(item, 'id');
|
|
353
327
|
return gid !== null && gid !== undefined && allowedGids.has(String(gid));
|
|
354
328
|
});
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
//
|
|
329
|
+
} else if (includeFilters?.length) {
|
|
330
|
+
// The viewport's includeClasses gates generic fills exactly like it gates
|
|
331
|
+
// line paths — the include list is authoritative for what geometry appears.
|
|
332
|
+
// GID-gated rules (matchClipFilter with an active clip/highlight set, the
|
|
333
|
+
// branch above) are exempt, mirroring highlight/clip classes in
|
|
334
|
+
// filterModelForViewport: a clipped apartment's space fills must render
|
|
335
|
+
// even in a view whose includeClasses excludes spaces.
|
|
336
|
+
matching = matching.filter(
|
|
337
|
+
(item: unknown) =>
|
|
338
|
+
isRecord(item) &&
|
|
339
|
+
includeFilters.some((f) =>
|
|
340
|
+
typeof f === 'string'
|
|
341
|
+
? matchesFilter(item, f, undefined)
|
|
342
|
+
: matchesFilter(item, f.class, f.where)
|
|
343
|
+
)
|
|
344
|
+
);
|
|
358
345
|
}
|
|
359
|
-
// console.log(`[DEBUG FILL vp="${vp.id}"] Fill rule (class="${rule.class}", where=${JSON.stringify(rule.where)}) matched ${matching.length} items`);
|
|
360
|
-
// if (matching.length > 0) {
|
|
361
|
-
// console.log(`[DEBUG FILL vp="${vp.id}"] First matched item:`, {
|
|
362
|
-
// gid: getProp(matching[0], 'gid'),
|
|
363
|
-
// name: getProp(matching[0], 'name'),
|
|
364
|
-
// ifcClass: getProp(matching[0], 'ifcClass'),
|
|
365
|
-
// propertySets: getProp(matching[0], 'propertySets'),
|
|
366
|
-
// polylines: Array.isArray(getProp(matching[0], 'polylines')) ? (getProp(matching[0], 'polylines') as any[]).length : 0
|
|
367
|
-
// });
|
|
368
|
-
// }
|
|
369
346
|
if (!matching.length) continue;
|
|
370
347
|
|
|
371
348
|
for (const item of matching) {
|
|
@@ -379,40 +356,33 @@ function buildFillSvg(
|
|
|
379
356
|
if (!color) continue; // no sampled color available
|
|
380
357
|
fillColor = `rgb(${color[0]},${color[1]},${color[2]})`;
|
|
381
358
|
} else if (rule.fill === 'legend') {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
`[FILL] fill="legend" specified but no legend color map found for viewport "${vp.id}". Available viewports:`,
|
|
387
|
-
Array.from(legendColorMaps?.keys() ?? [])
|
|
359
|
+
const legendColors = legendColorMaps?.get(vp.id);
|
|
360
|
+
if (!legendColors) {
|
|
361
|
+
warnings.push(
|
|
362
|
+
`Viewport '${vp.id}' has fill: "legend" but no legend with assignColors targets it (legends exist for: ${Array.from(legendColorMaps?.keys() ?? []).join(', ') || 'none'}).`
|
|
388
363
|
);
|
|
389
|
-
|
|
364
|
+
break;
|
|
365
|
+
}
|
|
366
|
+
// Resolve the item's colour: by item identity first (covers
|
|
367
|
+
// ungrouped legends — every item its own colour), then by the
|
|
368
|
+
// legend's groupBy property value (shared across a group).
|
|
369
|
+
let resolved: string | undefined;
|
|
370
|
+
const gid = getProp(item, 'gid') ?? getProp(item, 'id');
|
|
371
|
+
if (gid !== null && gid !== undefined) {
|
|
372
|
+
resolved = legendColors.byGid.get(String(gid));
|
|
390
373
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
let legendKey: string | undefined;
|
|
394
|
-
const possibleKeys = ['spacename', 'BIP.spacename', 'name'];
|
|
395
|
-
for (const key of possibleKeys) {
|
|
396
|
-
const value = resolvePath(item, key);
|
|
374
|
+
if (!resolved && legendColors.groupBy) {
|
|
375
|
+
const value = resolvePath(item, legendColors.groupBy);
|
|
397
376
|
if (value !== undefined && value !== null) {
|
|
398
|
-
|
|
399
|
-
break;
|
|
377
|
+
resolved = legendColors.byValue.get(String(value));
|
|
400
378
|
}
|
|
401
379
|
}
|
|
402
|
-
if (!
|
|
403
|
-
|
|
404
|
-
// if (legendKey) {
|
|
405
|
-
// console.log(`[FILL] No color mapping for "${legendKey}" (property: ${matchedProperty}). Available keys:`, Array.from(legendColorMap.keys()));
|
|
406
|
-
// }
|
|
407
|
-
continue;
|
|
408
|
-
}
|
|
409
|
-
fillColor = legendColorMap.get(legendKey)!;
|
|
410
|
-
// console.log(`[FILL] ✓ Matched "${legendKey}" -> ${fillColor}`);
|
|
380
|
+
if (!resolved) continue; // item not represented in the legend
|
|
381
|
+
fillColor = resolved;
|
|
411
382
|
} else {
|
|
412
383
|
fillColor = rule.fill;
|
|
413
384
|
}
|
|
414
385
|
|
|
415
|
-
// console.log(`[FILL] Processing ${polylines.length} polyline(s) for item with color ${fillColor}`);
|
|
416
386
|
for (const polyline of polylines) {
|
|
417
387
|
const points = isRecord(polyline) ? getProp(polyline, 'points') : null;
|
|
418
388
|
if (!Array.isArray(points) || points.length < 3) continue;
|
|
@@ -426,7 +396,11 @@ function buildFillSvg(
|
|
|
426
396
|
if (!Number.isFinite(px) || !Number.isFinite(pz)) continue;
|
|
427
397
|
const mx = (px as number) - viewMinX;
|
|
428
398
|
const my = viewMaxZ - (pz as number);
|
|
429
|
-
pathParts.push(
|
|
399
|
+
pathParts.push(
|
|
400
|
+
i === 0
|
|
401
|
+
? `M ${formatNumber(mx)} ${formatNumber(my)}`
|
|
402
|
+
: `L ${formatNumber(mx)} ${formatNumber(my)}`
|
|
403
|
+
);
|
|
430
404
|
}
|
|
431
405
|
if (pathParts.length < 3) continue;
|
|
432
406
|
pathParts.push('Z');
|
|
@@ -437,6 +411,5 @@ function buildFillSvg(
|
|
|
437
411
|
}
|
|
438
412
|
}
|
|
439
413
|
|
|
440
|
-
// console.log(`[FILL] Generated ${parts.length} fill polygon(s) for viewport "${vp.id}"`);
|
|
441
414
|
return parts.length ? `<g>${parts.join('')}</g>` : null;
|
|
442
415
|
}
|
|
@@ -11,29 +11,142 @@ export const rotateBounds = (width: number, height: number, rotationDeg: number)
|
|
|
11
11
|
return { width: rotatedWidth, height: rotatedHeight };
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
export interface RotatedContentExtents {
|
|
15
|
+
/** Tight extents of the rotated content, model units (rotated/sheet axes). */
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
/** Tight-bounds center mapped back to view space — the rotation pivot that centers the content. */
|
|
19
|
+
centerViewX: number;
|
|
20
|
+
centerViewY: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Absolute move/line sequences only (what our section renderers emit). Anything
|
|
24
|
+
// else — relative commands, curves, arcs — falls back to AABB-based rotation.
|
|
25
|
+
const ABSOLUTE_POLYLINE_D = /^[MLZ\s,0-9eE+\-.]*$/;
|
|
26
|
+
const NUMBER_IN_D = /-?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
|
|
27
|
+
|
|
28
|
+
const extentsCache = new WeakMap<object, Map<string, RotatedContentExtents | null>>();
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Tight bounds of the model's actual geometry under viewport rotation.
|
|
32
|
+
*
|
|
33
|
+
* Rotating the model's AABB *rectangle* only ever inflates it — for content that
|
|
34
|
+
* sits diagonally in model space (the main reason to set rotationDeg: straighten
|
|
35
|
+
* a building on the sheet), the true rotated bounds SHRINK instead, and fitting
|
|
36
|
+
* against the inflated rectangle picks a scale several preset steps too small.
|
|
37
|
+
* Returns null when tight bounds can't be derived (no rotation, no rawPaths /
|
|
38
|
+
* transform, or path data that isn't plain absolute polylines) — callers then
|
|
39
|
+
* keep the AABB-based rotateBounds behavior.
|
|
40
|
+
*/
|
|
41
|
+
export const rotatedContentExtents = (model: SvgModelView, rotationDeg: number): RotatedContentExtents | null => {
|
|
42
|
+
const norm = ((rotationDeg % 360) + 360) % 360;
|
|
43
|
+
const paths = model.rawPaths;
|
|
44
|
+
const t = model.transform;
|
|
45
|
+
if (!norm || !paths?.length || !t) return null;
|
|
46
|
+
// Keyed on the transform origin as well as the rotation. The extents below are computed through
|
|
47
|
+
// t.viewMinX / t.viewMaxZ, so two models sharing a rawPaths array but placed with different
|
|
48
|
+
// origins have different answers — keying on rotation alone handed the second the first's.
|
|
49
|
+
const cacheKey = `${norm}|${t.viewMinX}|${t.viewMaxZ}`;
|
|
50
|
+
let perRotation = extentsCache.get(paths);
|
|
51
|
+
const cached = perRotation?.get(cacheKey);
|
|
52
|
+
if (cached !== undefined) return cached;
|
|
53
|
+
if (!perRotation) {
|
|
54
|
+
perRotation = new Map();
|
|
55
|
+
extentsCache.set(paths, perRotation);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const rad = (norm * Math.PI) / 180;
|
|
59
|
+
const cos = Math.cos(rad);
|
|
60
|
+
const sin = Math.sin(rad);
|
|
61
|
+
let minX = Infinity;
|
|
62
|
+
let minY = Infinity;
|
|
63
|
+
let maxX = -Infinity;
|
|
64
|
+
let maxY = -Infinity;
|
|
65
|
+
for (const p of paths) {
|
|
66
|
+
if (!p.d) continue;
|
|
67
|
+
if (!ABSOLUTE_POLYLINE_D.test(p.d)) {
|
|
68
|
+
perRotation.set(cacheKey, null);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const nums = p.d.match(NUMBER_IN_D);
|
|
72
|
+
if (!nums) continue;
|
|
73
|
+
for (let i = 0; i + 1 < nums.length; i += 2) {
|
|
74
|
+
// Raw coords are projected y-up space; geometryTransform maps them to
|
|
75
|
+
// view space (y-down, origin at the model's min corner) — mirror that
|
|
76
|
+
// here so extents/center land in the space the render transform uses.
|
|
77
|
+
const vx = Number(nums[i]) - t.viewMinX;
|
|
78
|
+
const vy = t.viewMaxZ - Number(nums[i + 1]);
|
|
79
|
+
const rx = vx * cos - vy * sin;
|
|
80
|
+
const ry = vx * sin + vy * cos;
|
|
81
|
+
if (rx < minX) minX = rx;
|
|
82
|
+
if (rx > maxX) maxX = rx;
|
|
83
|
+
if (ry < minY) minY = ry;
|
|
84
|
+
if (ry > maxY) maxY = ry;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (!Number.isFinite(minX) || !Number.isFinite(minY) || maxX - minX <= 0 || maxY - minY <= 0) {
|
|
88
|
+
perRotation.set(cacheKey, null);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const cx = (minX + maxX) / 2;
|
|
92
|
+
const cy = (minY + maxY) / 2;
|
|
93
|
+
const result: RotatedContentExtents = {
|
|
94
|
+
width: maxX - minX,
|
|
95
|
+
height: maxY - minY,
|
|
96
|
+
// Inverse-rotate the rotated-space center: using this as the transform
|
|
97
|
+
// pivot makes the tight content box land centered in the viewport.
|
|
98
|
+
centerViewX: cx * cos + cy * sin,
|
|
99
|
+
centerViewY: -cx * sin + cy * cos
|
|
100
|
+
};
|
|
101
|
+
perRotation.set(cacheKey, result);
|
|
102
|
+
return result;
|
|
103
|
+
};
|
|
104
|
+
|
|
14
105
|
export const pickScale = (
|
|
15
106
|
viewport: ViewportElement,
|
|
16
107
|
model: SvgModelView,
|
|
17
108
|
preferred: number[],
|
|
18
109
|
scalePresets: Record<string, { preferredScales?: number[] }>
|
|
19
110
|
): ComputedViewportScale => {
|
|
111
|
+
const rotationDeg = viewport.rotationDeg ?? 0;
|
|
112
|
+
// Callers substitute clip-region dims into widthModel/heightModel; tight
|
|
113
|
+
// rotated bounds of the FULL geometry would be wrong there, so only use
|
|
114
|
+
// them when the passed dims still match the model's own view bounds.
|
|
115
|
+
const clipAdjusted =
|
|
116
|
+
!!model.transform &&
|
|
117
|
+
(Math.abs(model.widthModel - model.transform.viewWidth) > 1e-9 ||
|
|
118
|
+
Math.abs(model.heightModel - model.transform.viewHeight) > 1e-9);
|
|
119
|
+
const tight = clipAdjusted ? null : rotatedContentExtents(model, rotationDeg);
|
|
120
|
+
|
|
121
|
+
// 'fit': exact fill of the viewport — the denominator is whatever ratio
|
|
122
|
+
// makes the (rotated) model touch the viewport bounds; no rounding to a
|
|
123
|
+
// preferred-scale list, so the scale text can be odd (e.g. 1:73.4).
|
|
124
|
+
if (viewport.scaleMode === 'fit') {
|
|
125
|
+
const rotatedModel = tight ?? rotateBounds(model.widthModel, model.heightModel, rotationDeg);
|
|
126
|
+
const mmPerModel = Math.min(
|
|
127
|
+
viewport.widthMm / (rotatedModel.width || 1),
|
|
128
|
+
viewport.heightMm / (rotatedModel.height || 1)
|
|
129
|
+
);
|
|
130
|
+
if (Number.isFinite(mmPerModel) && mmPerModel > 0) {
|
|
131
|
+
return { denom: Math.round((1000 / mmPerModel) * 10) / 10, mmPerModelUnit: mmPerModel, fits: true };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
20
135
|
const scalePresetName = viewport.scalePreset;
|
|
21
136
|
const scalePreset = scalePresetName && scalePresets[scalePresetName] ? scalePresets[scalePresetName] : null;
|
|
22
137
|
const denoms =
|
|
23
138
|
viewport.scaleMode === 'fixed' && viewport.fixedScaleDenom
|
|
24
139
|
? [viewport.fixedScaleDenom]
|
|
25
140
|
: uniquePositiveDenoms(viewport.preferredScales ?? scalePreset?.preferredScales ?? preferred);
|
|
26
|
-
|
|
27
|
-
const rotationDeg = viewport.rotationDeg ?? 0;
|
|
28
141
|
let selectedDenom = denoms[denoms.length - 1];
|
|
29
142
|
let selectedMmPerModel = 1000 / selectedDenom;
|
|
30
143
|
let fits = false;
|
|
31
144
|
|
|
32
145
|
for (const denom of denoms) {
|
|
33
146
|
const mmPerModel = 1000 / denom;
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
147
|
+
const rotated = tight
|
|
148
|
+
? { width: tight.width * mmPerModel, height: tight.height * mmPerModel }
|
|
149
|
+
: rotateBounds(model.widthModel * mmPerModel, model.heightModel * mmPerModel, rotationDeg);
|
|
37
150
|
if (rotated.width <= viewport.widthMm && rotated.height <= viewport.heightMm) {
|
|
38
151
|
selectedDenom = denom;
|
|
39
152
|
selectedMmPerModel = mmPerModel;
|