@twinfinity/printing 6.1.0-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/dist/types.js
CHANGED
|
@@ -2,7 +2,16 @@ export const DEFAULT_SVG_STYLES = {
|
|
|
2
2
|
cutLines: { enabled: true, stroke: '#dc2626', strokeWidth: 0.01, opacity: 1 },
|
|
3
3
|
visibleLines: { enabled: true, stroke: '#111827', strokeWidth: 0.005, opacity: 1 },
|
|
4
4
|
hiddenLines: { enabled: true, stroke: '#6b7280', strokeWidth: 0.004, strokeDasharray: '0.02,0.01', opacity: 1 },
|
|
5
|
-
|
|
5
|
+
silhouettesVisible: { enabled: true, stroke: '#000000', strokeWidth: 0.006, opacity: 1 },
|
|
6
|
+
// Hidden contours follow the hiddenLines drafting convention (dashed grey), not the
|
|
7
|
+
// visible outline's heavy solid stroke.
|
|
8
|
+
silhouettesHidden: {
|
|
9
|
+
enabled: true,
|
|
10
|
+
stroke: '#6b7280',
|
|
11
|
+
strokeWidth: 0.004,
|
|
12
|
+
strokeDasharray: '0.02,0.01',
|
|
13
|
+
opacity: 1
|
|
14
|
+
},
|
|
6
15
|
featureEdges: { enabled: true, stroke: '#374151', strokeWidth: 0.005, opacity: 1 },
|
|
7
16
|
stairArrows: { enabled: true, stroke: '#111827', strokeWidth: 0.005, opacity: 1 },
|
|
8
17
|
outlineVisible: { enabled: true, stroke: '#111827', strokeWidth: 0.004, opacity: 1 },
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"/","sources":["types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"/","sources":["types.ts"],"names":[],"mappings":"AAwCA,MAAM,CAAC,MAAM,kBAAkB,GAA8B;IACzD,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,EAAE;IAC7E,YAAY,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE;IAClF,WAAW,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,eAAe,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,EAAE;IAC/G,kBAAkB,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE;IACxF,oFAAoF;IACpF,wCAAwC;IACxC,iBAAiB,EAAE;QACf,OAAO,EAAE,IAAI;QACb,MAAM,EAAE,SAAS;QACjB,WAAW,EAAE,KAAK;QAClB,eAAe,EAAE,WAAW;QAC5B,OAAO,EAAE,CAAC;KACb;IACD,YAAY,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE;IAClF,WAAW,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE;IACjF,cAAc,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE;IACpF,aAAa,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC,EAAE;CACtH,CAAC","sourcesContent":["/** Line styling options for SVG rendering. */\nexport interface SvgLineStyle {\n enabled?: boolean;\n stroke?: string;\n strokeWidth?: number;\n strokeDasharray?: string;\n opacity?: number;\n}\n\n/** Style configuration for different line categories in SVG rendering. */\nexport interface SvgRenderStyles {\n cutLines?: SvgLineStyle;\n visibleLines?: SvgLineStyle;\n hiddenLines?: SvgLineStyle;\n silhouettesVisible?: SvgLineStyle;\n silhouettesHidden?: SvgLineStyle;\n featureEdges?: SvgLineStyle;\n stairArrows?: SvgLineStyle;\n outlineVisible?: SvgLineStyle;\n outlineHidden?: SvgLineStyle;\n}\n\n/** Plane basis for projecting 3D points to 2D. */\nexport interface SvgPlaneBasis {\n origin: { x: number; y: number; z: number };\n right: { x: number; y: number; z: number };\n up: { x: number; y: number; z: number };\n}\n\n/** Options for printToSvg. */\nexport interface PrintToSvgOptions {\n viewBox?: [number, number, number, number];\n styles?: SvgRenderStyles;\n includeXmlDeclaration?: boolean;\n backgroundColor?: string;\n planeBasis?: SvgPlaneBasis;\n bounds?: { minX: number; maxX: number; minY: number; maxY: number };\n boundsPadding?: number;\n}\n\nexport const DEFAULT_SVG_STYLES: Required<SvgRenderStyles> = {\n cutLines: { enabled: true, stroke: '#dc2626', strokeWidth: 0.01, opacity: 1 },\n visibleLines: { enabled: true, stroke: '#111827', strokeWidth: 0.005, opacity: 1 },\n hiddenLines: { enabled: true, stroke: '#6b7280', strokeWidth: 0.004, strokeDasharray: '0.02,0.01', opacity: 1 },\n silhouettesVisible: { enabled: true, stroke: '#000000', strokeWidth: 0.006, opacity: 1 },\n // Hidden contours follow the hiddenLines drafting convention (dashed grey), not the\n // visible outline's heavy solid stroke.\n silhouettesHidden: {\n enabled: true,\n stroke: '#6b7280',\n strokeWidth: 0.004,\n strokeDasharray: '0.02,0.01',\n opacity: 1\n },\n featureEdges: { enabled: true, stroke: '#374151', strokeWidth: 0.005, opacity: 1 },\n stairArrows: { enabled: true, stroke: '#111827', strokeWidth: 0.005, opacity: 1 },\n outlineVisible: { enabled: true, stroke: '#111827', strokeWidth: 0.004, opacity: 1 },\n outlineHidden: { enabled: true, stroke: '#9ca3af', strokeWidth: 0.003, strokeDasharray: '0.015,0.008', opacity: 1 }\n};\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinfinity/printing",
|
|
3
|
-
"version": "6.1.
|
|
3
|
+
"version": "6.1.2-ci.30738-beta",
|
|
4
4
|
"description": "SVG and PDF output",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
"setVersion": "bun run ../scripts/set-version.mts twinfinity-printing beta"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@twinfinity/geometry": "6.1.
|
|
27
|
+
"@twinfinity/geometry": "6.1.2-ci.30738-beta",
|
|
28
|
+
"@twinfinity/product-geometry": "6.1.2-ci.30738-beta",
|
|
29
|
+
"polygon-clipping": "^0.15.7"
|
|
28
30
|
},
|
|
29
31
|
"peerDependencies": {
|
|
30
32
|
"jspdf": "^2.5.1",
|
|
@@ -43,7 +45,10 @@
|
|
|
43
45
|
}
|
|
44
46
|
},
|
|
45
47
|
"devDependencies": {
|
|
48
|
+
"ajv": "^8.17.1",
|
|
46
49
|
"typescript": "^5.8.3",
|
|
47
|
-
"vitest": "^3.2.3"
|
|
50
|
+
"vitest": "^3.2.3",
|
|
51
|
+
"jspdf": "^2.5.1",
|
|
52
|
+
"svg2pdf.js": "^2.0.0"
|
|
48
53
|
}
|
|
49
54
|
}
|
package/src/index.ts
CHANGED
|
@@ -8,3 +8,7 @@ export * from './layout/index';
|
|
|
8
8
|
|
|
9
9
|
// Sections SVG viewport module
|
|
10
10
|
export * from './sections';
|
|
11
|
+
|
|
12
|
+
// Template-schema utilities (snippet-placeholder sanitizing for consumers
|
|
13
|
+
// that parse the shipped template-schema.json)
|
|
14
|
+
export { sanitizeTemplateSchemaSnippets } from './schemaUtils';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeClass } from './filters';
|
|
2
|
+
import { splitPlaceholder } from './renderUtils';
|
|
2
3
|
import type {
|
|
3
4
|
LayoutTemplate,
|
|
4
5
|
LayoutElement,
|
|
@@ -28,25 +29,26 @@ export interface TemplateRequirements {
|
|
|
28
29
|
/** Class filters referenced (e.g. "space", "walls"). Already lowercase. */
|
|
29
30
|
classFilters: string[];
|
|
30
31
|
|
|
31
|
-
/**
|
|
32
|
-
|
|
32
|
+
/** Resource IDs referenced by image/northArrow elements. */
|
|
33
|
+
resourceIds: string[];
|
|
33
34
|
|
|
34
35
|
/** Text interpolation variables (e.g. "viewport.main.scaleText"). */
|
|
35
36
|
interpolationVars: string[];
|
|
36
37
|
|
|
37
|
-
/** Whether the template references remote (http/https)
|
|
38
|
-
|
|
38
|
+
/** Whether the template references remote (http/https) resource URLs. */
|
|
39
|
+
usesRemoteResources: boolean;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
const INTERPOLATION_RE = /\{\{\s*([^}]+?)\s*\}\}/g;
|
|
42
43
|
|
|
43
|
-
/** Extract `{{path}}` tokens from a template string
|
|
44
|
+
/** Extract `{{path}}` tokens from a template string (format filters like
|
|
45
|
+
* `| round:1` are part of the placeholder, not the referenced path). */
|
|
44
46
|
function extractInterpolationVars(text: string): string[] {
|
|
45
47
|
const vars: string[] = [];
|
|
46
48
|
let m: RegExpExecArray | null;
|
|
47
49
|
INTERPOLATION_RE.lastIndex = 0;
|
|
48
50
|
while ((m = INTERPOLATION_RE.exec(text)) !== null) {
|
|
49
|
-
vars.push(m[1]);
|
|
51
|
+
vars.push(splitPlaceholder(m[1]).path);
|
|
50
52
|
}
|
|
51
53
|
return vars;
|
|
52
54
|
}
|
|
@@ -80,15 +82,15 @@ export function analyzeTemplate(template: LayoutTemplate): TemplateRequirements
|
|
|
80
82
|
const viewportIds = new Set<string>();
|
|
81
83
|
const propertyPaths = new Set<string>();
|
|
82
84
|
const classFilters = new Set<string>();
|
|
83
|
-
const
|
|
85
|
+
const resourceIds = new Set<string>();
|
|
84
86
|
const interpolationVars = new Set<string>();
|
|
85
|
-
let
|
|
87
|
+
let usesRemoteResources = false;
|
|
86
88
|
|
|
87
|
-
// Check
|
|
88
|
-
if (template.
|
|
89
|
-
for (const
|
|
90
|
-
if (
|
|
91
|
-
|
|
89
|
+
// Check resources for remote URLs
|
|
90
|
+
if (template.resources) {
|
|
91
|
+
for (const resource of template.resources) {
|
|
92
|
+
if (resource.src && /^https?:\/\//i.test(resource.src)) {
|
|
93
|
+
usesRemoteResources = true;
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
96
|
}
|
|
@@ -124,9 +126,9 @@ export function analyzeTemplate(template: LayoutTemplate): TemplateRequirements
|
|
|
124
126
|
// Preset references are resolved at render time, nothing to collect
|
|
125
127
|
}
|
|
126
128
|
|
|
127
|
-
// North arrow
|
|
128
|
-
if (vp.northArrow?.
|
|
129
|
-
|
|
129
|
+
// North arrow resource
|
|
130
|
+
if (vp.northArrow?.resourceId) {
|
|
131
|
+
resourceIds.add(vp.northArrow.resourceId);
|
|
130
132
|
}
|
|
131
133
|
break;
|
|
132
134
|
}
|
|
@@ -143,7 +145,7 @@ export function analyzeTemplate(template: LayoutTemplate): TemplateRequirements
|
|
|
143
145
|
|
|
144
146
|
case 'image': {
|
|
145
147
|
const img = el as ImageElement;
|
|
146
|
-
if (img.
|
|
148
|
+
if (img.resourceId) resourceIds.add(img.resourceId);
|
|
147
149
|
break;
|
|
148
150
|
}
|
|
149
151
|
|
|
@@ -211,8 +213,8 @@ export function analyzeTemplate(template: LayoutTemplate): TemplateRequirements
|
|
|
211
213
|
viewportIds: [...viewportIds],
|
|
212
214
|
propertyPaths: [...propertyPaths],
|
|
213
215
|
classFilters: [...classFilters],
|
|
214
|
-
|
|
216
|
+
resourceIds: [...resourceIds],
|
|
215
217
|
interpolationVars: [...interpolationVars],
|
|
216
|
-
|
|
218
|
+
usesRemoteResources
|
|
217
219
|
};
|
|
218
220
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-viewport compute-class closure (TWINS-3784 perf + semantics): the set of classes a
|
|
3
|
+
* single viewport's drawing generation may touch — its resolved includeClasses, its
|
|
4
|
+
* highlight/clip classes, its fill rules, and any labels/legend elements bound to it.
|
|
5
|
+
*
|
|
6
|
+
* The contract (decided with TWINS-3784): only the viewport's include classes take part
|
|
7
|
+
* in that viewport's drawing generation, HIDDEN LINES INCLUDED — geometry outside the
|
|
8
|
+
* closure neither draws nor occludes there (an undrawable pipe run must not hide the
|
|
9
|
+
* wall behind it), and does not pay compute cost. Sections are cached per
|
|
10
|
+
* (sectionDef, closure), so viewports sharing both share the geometry.
|
|
11
|
+
*
|
|
12
|
+
* Returns null when the viewport's closure cannot be bounded by class: no includeClasses
|
|
13
|
+
* (the viewport draws everything) or any feature filtered only by a where-condition. The
|
|
14
|
+
* caller then computes that viewport with all products — the previous behaviour.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { normalizeClass } from './filters';
|
|
18
|
+
import { resolveIncludeClasses } from './metadata';
|
|
19
|
+
import { isRecord, getProp } from './typeGuards';
|
|
20
|
+
import type { LayoutTemplate } from './types';
|
|
21
|
+
|
|
22
|
+
export function computeViewportClassClosure(vp: Record<string, unknown>, template: LayoutTemplate): Set<string> | null {
|
|
23
|
+
const presets = template.presets?.includeClasses ?? {};
|
|
24
|
+
const out = new Set<string>();
|
|
25
|
+
|
|
26
|
+
/** Add class name(s); false = value present but not class-expressible. */
|
|
27
|
+
const addClasses = (cls: unknown): boolean => {
|
|
28
|
+
if (typeof cls === 'string') {
|
|
29
|
+
const n = normalizeClass(cls);
|
|
30
|
+
if (n) out.add(n);
|
|
31
|
+
return n !== null;
|
|
32
|
+
}
|
|
33
|
+
if (Array.isArray(cls)) {
|
|
34
|
+
if (!cls.length) return false;
|
|
35
|
+
let ok = true;
|
|
36
|
+
for (const c of cls) ok = addClasses(c) && ok;
|
|
37
|
+
return ok;
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const resolved = resolveIncludeClasses(getProp(vp, 'includeClasses'), presets);
|
|
43
|
+
if (!resolved) return null; // no includeClasses ⇒ the viewport draws everything
|
|
44
|
+
for (const f of resolved) {
|
|
45
|
+
if (typeof f === 'string') {
|
|
46
|
+
if (!addClasses(f)) return null;
|
|
47
|
+
} else if (f.class !== undefined) {
|
|
48
|
+
// A where-condition narrows WITHIN the class — class superset is safe.
|
|
49
|
+
if (!addClasses(f.class)) return null;
|
|
50
|
+
} else {
|
|
51
|
+
return null; // where-only include filter — class-unbounded
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const key of ['highlight', 'clip']) {
|
|
55
|
+
const cfg = getProp(vp, key);
|
|
56
|
+
if (cfg == null) continue;
|
|
57
|
+
if (!isRecord(cfg) || !addClasses(getProp(cfg, 'class'))) return null;
|
|
58
|
+
}
|
|
59
|
+
const fills = getProp(vp, 'fills');
|
|
60
|
+
if (Array.isArray(fills)) {
|
|
61
|
+
for (const rule of fills) {
|
|
62
|
+
if (!isRecord(rule) || !addClasses(getProp(rule, 'class'))) return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Labels/legend elements bound to this viewport (recursing into groups). An element
|
|
67
|
+
// without a viewport binding is treated as bound to every viewport — conservative.
|
|
68
|
+
const vpId = getProp(vp, 'id');
|
|
69
|
+
let bounded = true;
|
|
70
|
+
const visit = (elements: unknown): void => {
|
|
71
|
+
if (!Array.isArray(elements) || !bounded) return;
|
|
72
|
+
for (const el of elements) {
|
|
73
|
+
if (!isRecord(el)) continue;
|
|
74
|
+
const kind = getProp(el, 'kind');
|
|
75
|
+
if (kind === 'labels' || kind === 'legend') {
|
|
76
|
+
const boundTo = getProp(el, 'viewport');
|
|
77
|
+
if (boundTo === undefined || boundTo === vpId) {
|
|
78
|
+
if (!addClasses(getProp(el, 'class'))) {
|
|
79
|
+
bounded = false;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const children = getProp(el, 'children');
|
|
85
|
+
if (children !== undefined) visit(children);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
visit(template.elements);
|
|
89
|
+
if (!bounded) return null;
|
|
90
|
+
|
|
91
|
+
return out.size > 0 ? out : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Per-class drawing-generation behavior extracted from includeClasses entries. */
|
|
95
|
+
export interface ClassBehaviorRule {
|
|
96
|
+
class: string | string[];
|
|
97
|
+
occludes?: boolean;
|
|
98
|
+
classify?: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Collects per-class behavior flags (TWINS-3784) from the viewport's includeClasses
|
|
103
|
+
* entries: `{ "class": "pipes", "occludes": false, "classify": false }`. Flags apply
|
|
104
|
+
* class-wide — a `where` on the same entry narrows what is DRAWN, not the behavior.
|
|
105
|
+
*/
|
|
106
|
+
export function collectClassBehavior(vp: Record<string, unknown>, template: LayoutTemplate): ClassBehaviorRule[] {
|
|
107
|
+
const presets = template.presets?.includeClasses ?? {};
|
|
108
|
+
const resolved = resolveIncludeClasses(getProp(vp, 'includeClasses'), presets);
|
|
109
|
+
if (!resolved) return [];
|
|
110
|
+
const rules: ClassBehaviorRule[] = [];
|
|
111
|
+
for (const f of resolved) {
|
|
112
|
+
if (typeof f === 'string' || f.class === undefined) continue;
|
|
113
|
+
if (f.occludes === false || f.classify === false) {
|
|
114
|
+
rules.push({
|
|
115
|
+
class: f.class,
|
|
116
|
+
...(f.occludes === false ? { occludes: false } : {}),
|
|
117
|
+
...(f.classify === false ? { classify: false } : {})
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return rules;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Filters products to the closure. Only products that can self-classify via `class.is`
|
|
126
|
+
* participate — adapter products without a class matcher always stay, because their raw
|
|
127
|
+
* class names may not use the friendly vocabulary templates speak (dropping them on a
|
|
128
|
+
* name mismatch would silently blank the sheet).
|
|
129
|
+
*/
|
|
130
|
+
export function filterProductsByClosure<TProduct extends { class?: { is(c: unknown): boolean } }>(
|
|
131
|
+
products: TProduct[],
|
|
132
|
+
closure: ReadonlySet<string>
|
|
133
|
+
): TProduct[] {
|
|
134
|
+
return products.filter((p) => {
|
|
135
|
+
const is = p.class?.is;
|
|
136
|
+
if (typeof is !== 'function') return true;
|
|
137
|
+
for (const c of closure) {
|
|
138
|
+
if (p.class!.is(c)) return true;
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
});
|
|
142
|
+
}
|
package/src/layout/clipUtils.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import polygonClipping from 'polygon-clipping';
|
|
1
2
|
import type { SvgModelView } from './types';
|
|
2
3
|
import { EPSILON_DIVISION_SAFETY, CENTER_FRACTION } from './geometryConstants';
|
|
3
4
|
|
|
@@ -85,6 +86,43 @@ export const filterEntriesByClip = (
|
|
|
85
86
|
});
|
|
86
87
|
};
|
|
87
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Union polygons so overlapping ones merge into a single outline (disjoint
|
|
91
|
+
* polygons pass through unchanged). Used on expanded clip polygons — two
|
|
92
|
+
* adjacent clip targets whose expansions overlap should read as one region,
|
|
93
|
+
* not as outlines crossing through each other.
|
|
94
|
+
*
|
|
95
|
+
* The result is a flat ring list that may include hole rings (e.g. several
|
|
96
|
+
* regions enclosing a courtyard) — consumers rendering fills or clip paths
|
|
97
|
+
* from these rings must use even-odd semantics.
|
|
98
|
+
*/
|
|
99
|
+
export const unionPolygons = (polys: Polygon[]): Polygon[] => {
|
|
100
|
+
if (!Array.isArray(polys) || polys.length < 2) return polys ?? [];
|
|
101
|
+
try {
|
|
102
|
+
const input = polys
|
|
103
|
+
.filter((p) => p.length >= 3)
|
|
104
|
+
.map((p): [number, number][][] => [[...p.map((pt): [number, number] => [pt.x, pt.y]), [p[0].x, p[0].y]]]);
|
|
105
|
+
if (input.length < 2) return polys;
|
|
106
|
+
const result = polygonClipping.union(input[0], ...input.slice(1));
|
|
107
|
+
const rings: Polygon[] = [];
|
|
108
|
+
for (const poly of result) {
|
|
109
|
+
for (const ring of poly) {
|
|
110
|
+
const r: Polygon = ring.map(([x, y]) => ({ x, y }));
|
|
111
|
+
// Drop the duplicated closing point the library emits.
|
|
112
|
+
if (r.length > 1 && r[0].x === r[r.length - 1].x && r[0].y === r[r.length - 1].y) {
|
|
113
|
+
r.pop();
|
|
114
|
+
}
|
|
115
|
+
if (r.length >= 3) rings.push(r);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return rings.length ? rings : polys;
|
|
119
|
+
} catch {
|
|
120
|
+
// Boolean ops can throw on degenerate input — fall back to the
|
|
121
|
+
// un-unioned polygons rather than losing the clip entirely.
|
|
122
|
+
return polys;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
88
126
|
export const offsetPolygons = (polys: Polygon[], delta: number): Polygon[] => {
|
|
89
127
|
if (!Array.isArray(polys) || !polys.length || !Number.isFinite(delta) || delta === 0) return polys ?? [];
|
|
90
128
|
const intersectLines = (p: Point, r: Point, q: Point, s: Point): Point | null => {
|
|
@@ -93,19 +131,40 @@ export const offsetPolygons = (polys: Polygon[], delta: number): Polygon[] => {
|
|
|
93
131
|
const t = ((q.x - p.x) * s.y - (q.y - p.y) * s.x) / rxs;
|
|
94
132
|
return { x: p.x + t * r.x, y: p.y + t * r.y };
|
|
95
133
|
};
|
|
134
|
+
// Vertices closer than this are treated as duplicates (coordinates are in
|
|
135
|
+
// model metres, so 0.1 mm is far below anything architectural).
|
|
136
|
+
const DUPLICATE_EPS = 1e-4;
|
|
96
137
|
const offsetPoly = (poly: Polygon): Polygon => {
|
|
97
|
-
|
|
138
|
+
// Sanitize the ring first: cut polylines usually repeat the first point
|
|
139
|
+
// as a closing point, and mesh cutting can emit consecutive
|
|
140
|
+
// near-duplicates. A zero-length edge has a zero normal, which
|
|
141
|
+
// degenerates the miter intersection and leaves that vertex unoffset —
|
|
142
|
+
// visible as a collapsed/distorted corner in the expanded outline.
|
|
143
|
+
const pts: Polygon = [];
|
|
144
|
+
for (const p of poly) {
|
|
145
|
+
const last = pts[pts.length - 1];
|
|
146
|
+
if (last && Math.abs(last.x - p.x) < DUPLICATE_EPS && Math.abs(last.y - p.y) < DUPLICATE_EPS) continue;
|
|
147
|
+
pts.push(p);
|
|
148
|
+
}
|
|
149
|
+
while (
|
|
150
|
+
pts.length > 1 &&
|
|
151
|
+
Math.abs(pts[0].x - pts[pts.length - 1].x) < DUPLICATE_EPS &&
|
|
152
|
+
Math.abs(pts[0].y - pts[pts.length - 1].y) < DUPLICATE_EPS
|
|
153
|
+
) {
|
|
154
|
+
pts.pop();
|
|
155
|
+
}
|
|
156
|
+
const n = pts.length;
|
|
98
157
|
if (n < 3) return poly;
|
|
99
|
-
const area =
|
|
100
|
-
const q =
|
|
158
|
+
const area = pts.reduce((sum, p, i) => {
|
|
159
|
+
const q = pts[(i + 1) % n];
|
|
101
160
|
return sum + p.x * q.y - q.x * p.y;
|
|
102
161
|
}, 0);
|
|
103
162
|
const orient = area >= 0 ? 1 : -1; // ccw => outward is right normal
|
|
104
163
|
const out: Polygon = [];
|
|
105
164
|
for (let i = 0; i < n; i++) {
|
|
106
|
-
const p0 =
|
|
107
|
-
const p1 =
|
|
108
|
-
const p2 =
|
|
165
|
+
const p0 = pts[(i + n - 1) % n];
|
|
166
|
+
const p1 = pts[i];
|
|
167
|
+
const p2 = pts[(i + 1) % n];
|
|
109
168
|
const e1x = p1.x - p0.x;
|
|
110
169
|
const e1y = p1.y - p0.y;
|
|
111
170
|
const e2x = p2.x - p1.x;
|
package/src/layout/filters.ts
CHANGED
|
@@ -33,6 +33,29 @@ const matchesClass = (itemClass: unknown, classFilter: string | string[] | undef
|
|
|
33
33
|
return classes.some((c) => classEquals(normalizeClass(c), normalized));
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Check whether a *live* product's class matches a filter, with the same semantics extracted items
|
|
38
|
+
* get from {@link matchesClass}: case-insensitive, `Ifc` prefix optional, arrays mean any-of.
|
|
39
|
+
*
|
|
40
|
+
* Live products carry a `ModelProductClass` rather than a string, and its `is()` compares with
|
|
41
|
+
* `===`. That rejects `SPACE` and `IFCWALL` — values every other filter in the same template
|
|
42
|
+
* accepts — and its own `Ifc` handling is case-sensitive too (`'IFCWALL'.startsWith('Ifc')` is
|
|
43
|
+
* false). Comparing the class's own `id`/`type` through `matchesClass` keeps product selection
|
|
44
|
+
* identical to the rest of the template rather than subtly stricter.
|
|
45
|
+
*/
|
|
46
|
+
const matchesProductClass = (product: unknown, classFilter: string | string[] | undefined): boolean => {
|
|
47
|
+
if (classFilter === undefined) return true;
|
|
48
|
+
if (!isRecord(product)) return false;
|
|
49
|
+
const cls = getProp(product, 'class');
|
|
50
|
+
const id = isRecord(cls) ? getProp(cls, 'id') : undefined;
|
|
51
|
+
const type = isRecord(cls) ? getProp(cls, 'type') : undefined;
|
|
52
|
+
return (
|
|
53
|
+
matchesClass(id, classFilter) ||
|
|
54
|
+
matchesClass(type, classFilter) ||
|
|
55
|
+
matchesClass(getProp(product, 'className'), classFilter)
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
36
59
|
/**
|
|
37
60
|
* Check whether an item matches a `where` condition.
|
|
38
61
|
*
|
|
@@ -55,13 +78,13 @@ const matchesWhere = (item: unknown, where: WhereCondition | undefined): boolean
|
|
|
55
78
|
};
|
|
56
79
|
|
|
57
80
|
/**
|
|
58
|
-
*
|
|
81
|
+
* Match items by class + where conditions.
|
|
59
82
|
*
|
|
60
83
|
* @param item - An object with ifcClass and propertySets
|
|
61
84
|
* @param classFilter - IFC class name(s) to match, or undefined for any
|
|
62
85
|
* @param where - Property conditions (short paths), or undefined for no conditions
|
|
63
86
|
*/
|
|
64
|
-
|
|
87
|
+
const matchesFilterImpl = (
|
|
65
88
|
item: unknown,
|
|
66
89
|
classFilter: string | string[] | undefined,
|
|
67
90
|
where: WhereCondition | undefined
|
|
@@ -77,4 +100,9 @@ export const matchesV2Filter = (
|
|
|
77
100
|
return true;
|
|
78
101
|
};
|
|
79
102
|
|
|
80
|
-
export
|
|
103
|
+
export const matchesFilter = matchesFilterImpl;
|
|
104
|
+
|
|
105
|
+
/** @deprecated Use {@link matchesFilter} — there never was a V1; the name was noise. */
|
|
106
|
+
export const matchesV2Filter = matchesFilterImpl;
|
|
107
|
+
|
|
108
|
+
export { normalizeClass, classEquals, normalizeValue, matchesClass, matchesProductClass, matchesWhere };
|