@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.
Files changed (157) hide show
  1. package/dist/BuildInfo.js +3 -3
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +3 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/layout/analyzeTemplate.d.ts +4 -4
  7. package/dist/layout/analyzeTemplate.d.ts.map +1 -1
  8. package/dist/layout/analyzeTemplate.js +18 -16
  9. package/dist/layout/analyzeTemplate.js.map +1 -1
  10. package/dist/layout/classClosure.d.ts +41 -0
  11. package/dist/layout/classClosure.d.ts.map +1 -0
  12. package/dist/layout/classClosure.js +143 -0
  13. package/dist/layout/classClosure.js.map +1 -0
  14. package/dist/layout/clipUtils.d.ts +11 -0
  15. package/dist/layout/clipUtils.d.ts.map +1 -1
  16. package/dist/layout/clipUtils.js +67 -6
  17. package/dist/layout/clipUtils.js.map +1 -1
  18. package/dist/layout/filters.d.ts +14 -8
  19. package/dist/layout/filters.d.ts.map +1 -1
  20. package/dist/layout/filters.js +28 -3
  21. package/dist/layout/filters.js.map +1 -1
  22. package/dist/layout/index.d.ts +24 -57
  23. package/dist/layout/index.d.ts.map +1 -1
  24. package/dist/layout/index.js +20 -337
  25. package/dist/layout/index.js.map +1 -1
  26. package/dist/layout/itemBuilders.d.ts +11 -0
  27. package/dist/layout/itemBuilders.d.ts.map +1 -1
  28. package/dist/layout/itemBuilders.js +17 -3
  29. package/dist/layout/itemBuilders.js.map +1 -1
  30. package/dist/layout/labelsRender.d.ts.map +1 -1
  31. package/dist/layout/labelsRender.js +15 -27
  32. package/dist/layout/labelsRender.js.map +1 -1
  33. package/dist/layout/legendRender.d.ts +18 -6
  34. package/dist/layout/legendRender.d.ts.map +1 -1
  35. package/dist/layout/legendRender.js +49 -79
  36. package/dist/layout/legendRender.js.map +1 -1
  37. package/dist/layout/metadata.d.ts.map +1 -1
  38. package/dist/layout/metadata.js +26 -5
  39. package/dist/layout/metadata.js.map +1 -1
  40. package/dist/layout/model.d.ts +4 -3
  41. package/dist/layout/model.d.ts.map +1 -1
  42. package/dist/layout/model.js +17 -57
  43. package/dist/layout/model.js.map +1 -1
  44. package/dist/layout/options.d.ts +1 -2
  45. package/dist/layout/options.d.ts.map +1 -1
  46. package/dist/layout/options.js.map +1 -1
  47. package/dist/layout/preflight.d.ts +32 -0
  48. package/dist/layout/preflight.d.ts.map +1 -0
  49. package/dist/layout/preflight.js +69 -0
  50. package/dist/layout/preflight.js.map +1 -0
  51. package/dist/layout/qrRender.d.ts.map +1 -1
  52. package/dist/layout/qrRender.js +11 -10
  53. package/dist/layout/qrRender.js.map +1 -1
  54. package/dist/layout/renderDrawing.d.ts +28 -17
  55. package/dist/layout/renderDrawing.d.ts.map +1 -1
  56. package/dist/layout/renderDrawing.js +107 -88
  57. package/dist/layout/renderDrawing.js.map +1 -1
  58. package/dist/layout/renderLayout.d.ts +25 -0
  59. package/dist/layout/renderLayout.d.ts.map +1 -0
  60. package/dist/layout/renderLayout.js +315 -0
  61. package/dist/layout/renderLayout.js.map +1 -0
  62. package/dist/layout/renderTemplateSheet.d.ts +99 -0
  63. package/dist/layout/renderTemplateSheet.d.ts.map +1 -0
  64. package/dist/layout/renderTemplateSheet.js +296 -0
  65. package/dist/layout/renderTemplateSheet.js.map +1 -0
  66. package/dist/layout/renderUtils.d.ts +19 -0
  67. package/dist/layout/renderUtils.d.ts.map +1 -1
  68. package/dist/layout/renderUtils.js +47 -4
  69. package/dist/layout/renderUtils.js.map +1 -1
  70. package/dist/layout/sectionDefs.d.ts +65 -0
  71. package/dist/layout/sectionDefs.d.ts.map +1 -0
  72. package/dist/layout/sectionDefs.js +88 -0
  73. package/dist/layout/sectionDefs.js.map +1 -0
  74. package/dist/layout/slabModelView.d.ts +34 -0
  75. package/dist/layout/slabModelView.d.ts.map +1 -0
  76. package/dist/layout/slabModelView.js +175 -0
  77. package/dist/layout/slabModelView.js.map +1 -0
  78. package/dist/layout/templateVariables.d.ts +1 -1
  79. package/dist/layout/templateVariables.d.ts.map +1 -1
  80. package/dist/layout/templateVariables.js +21 -9
  81. package/dist/layout/templateVariables.js.map +1 -1
  82. package/dist/layout/types.d.ts +164 -83
  83. package/dist/layout/types.d.ts.map +1 -1
  84. package/dist/layout/types.js.map +1 -1
  85. package/dist/layout/validation.d.ts.map +1 -1
  86. package/dist/layout/validation.js +78 -33
  87. package/dist/layout/validation.js.map +1 -1
  88. package/dist/layout/viewportPrep.d.ts +8 -6
  89. package/dist/layout/viewportPrep.d.ts.map +1 -1
  90. package/dist/layout/viewportPrep.js +35 -23
  91. package/dist/layout/viewportPrep.js.map +1 -1
  92. package/dist/layout/viewportRender.d.ts +9 -7
  93. package/dist/layout/viewportRender.d.ts.map +1 -1
  94. package/dist/layout/viewportRender.js +93 -126
  95. package/dist/layout/viewportRender.js.map +1 -1
  96. package/dist/layout/viewportUtils.d.ts +20 -0
  97. package/dist/layout/viewportUtils.d.ts.map +1 -1
  98. package/dist/layout/viewportUtils.js +107 -4
  99. package/dist/layout/viewportUtils.js.map +1 -1
  100. package/dist/printToPdf.d.ts +7 -1
  101. package/dist/printToPdf.d.ts.map +1 -1
  102. package/dist/printToPdf.js +81 -14
  103. package/dist/printToPdf.js.map +1 -1
  104. package/dist/printToSvg.d.ts.map +1 -1
  105. package/dist/printToSvg.js +7 -3
  106. package/dist/printToSvg.js.map +1 -1
  107. package/dist/schemaUtils.d.ts +9 -0
  108. package/dist/schemaUtils.d.ts.map +1 -0
  109. package/dist/schemaUtils.js +14 -0
  110. package/dist/schemaUtils.js.map +1 -0
  111. package/dist/templates/default-template.json +30 -8
  112. package/dist/types.d.ts +2 -1
  113. package/dist/types.d.ts.map +1 -1
  114. package/dist/types.js +10 -1
  115. package/dist/types.js.map +1 -1
  116. package/package.json +8 -3
  117. package/src/index.ts +4 -0
  118. package/src/layout/analyzeTemplate.ts +21 -19
  119. package/src/layout/classClosure.ts +142 -0
  120. package/src/layout/clipUtils.ts +65 -6
  121. package/src/layout/filters.ts +31 -3
  122. package/src/layout/index.ts +61 -571
  123. package/src/layout/itemBuilders.ts +20 -5
  124. package/src/layout/labelsRender.ts +23 -40
  125. package/src/layout/legendRender.ts +64 -84
  126. package/src/layout/metadata.ts +26 -5
  127. package/src/layout/model.ts +21 -66
  128. package/src/layout/options.ts +1 -2
  129. package/src/layout/preflight.ts +96 -0
  130. package/src/layout/qrRender.ts +11 -10
  131. package/src/layout/renderDrawing.ts +132 -110
  132. package/src/layout/renderLayout.ts +483 -0
  133. package/src/layout/renderTemplateSheet.ts +435 -0
  134. package/src/layout/renderUtils.ts +53 -4
  135. package/src/layout/sectionDefs.ts +131 -0
  136. package/src/layout/slabModelView.ts +196 -0
  137. package/src/layout/templateVariables.ts +25 -9
  138. package/src/layout/types.ts +160 -70
  139. package/src/layout/validation.ts +98 -32
  140. package/src/layout/viewportPrep.ts +47 -29
  141. package/src/layout/viewportRender.ts +114 -141
  142. package/src/layout/viewportUtils.ts +118 -5
  143. package/src/printToPdf.ts +84 -15
  144. package/src/printToSvg.ts +7 -3
  145. package/src/schemaUtils.ts +13 -0
  146. package/src/templates/default-template.json +30 -8
  147. package/src/types.ts +12 -2
  148. package/templates/default-a0-landscape.json +171 -66
  149. package/templates/default-a0-portrait.json +159 -60
  150. package/templates/default-a1-landscape.json +135 -48
  151. package/templates/default-a1-portrait.json +123 -42
  152. package/templates/default-a3-landscape.json +87 -24
  153. package/templates/default-a3-portrait.json +99 -30
  154. package/templates/default-a4-landscape.json +75 -18
  155. package/templates/default-a4-portrait.json +75 -18
  156. package/templates/llm-authoring-guide.md +247 -0
  157. 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
- silhouettes: { enabled: true, stroke: '#000000', strokeWidth: 0.006, opacity: 1 },
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":"AAuCA,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,WAAW,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE;IACjF,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 silhouettes?: 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 silhouettes: { enabled: true, stroke: '#000000', strokeWidth: 0.006, opacity: 1 },\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"]}
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.1-beta",
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.1-beta"
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
- /** Asset IDs referenced by image/northArrow elements. */
32
- assetIds: string[];
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) asset URLs. */
38
- usesRemoteAssets: boolean;
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 assetIds = new Set<string>();
85
+ const resourceIds = new Set<string>();
84
86
  const interpolationVars = new Set<string>();
85
- let usesRemoteAssets = false;
87
+ let usesRemoteResources = false;
86
88
 
87
- // Check assets for remote URLs
88
- if (template.assets) {
89
- for (const asset of template.assets) {
90
- if (asset.src && /^https?:\/\//i.test(asset.src)) {
91
- usesRemoteAssets = true;
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 asset
128
- if (vp.northArrow?.assetId) {
129
- assetIds.add(vp.northArrow.assetId);
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.assetId) assetIds.add(img.assetId);
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
- assetIds: [...assetIds],
216
+ resourceIds: [...resourceIds],
215
217
  interpolationVars: [...interpolationVars],
216
- usesRemoteAssets
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
+ }
@@ -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
- const n = poly.length;
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 = poly.reduce((sum, p, i) => {
100
- const q = poly[(i + 1) % n];
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 = poly[(i + n - 1) % n];
107
- const p1 = poly[i];
108
- const p2 = poly[(i + 1) % n];
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;
@@ -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
- * V2 filter: match items by class + where conditions.
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
- export const matchesV2Filter = (
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 { normalizeClass, classEquals, normalizeValue, matchesClass, matchesWhere };
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 };