@twinfinity/printing
v6.2.0-beta
Published
SVG and PDF output
Readme
@twinfinity/printing
Template-driven page composition, section SVG building, and PDF/SVG export for BIM drawings.
This package consolidates layout composition (formerly @measurement-tools/print-layout), PDF/SVG export (formerly @measurement-tools/printing), and section SVG building (formerly @measurement-tools/sections) into a single package.
Quick Start
Build a section render state
import { createSectionSvg } from '@twinfinity/printing';
// entries = from computePlaneSectionsForProducts() in @twinfinity/geometry
const sectionState = createSectionSvg(entries);
// sectionState.paths — SVG path data with fill/stroke info
// sectionState.transform — viewport dimensions
// sectionState.labels — positioned text labelsCompose a drawing page from a template
import { renderDrawing } from '@twinfinity/printing';
const { svg, viewportScales } = await renderDrawing({
template,
sectionState,
});
// svg is a complete page SVG with title block, legend, scale bar, QR codes, etc.
// viewportScales['main-viewport'].denom tells you the scale (e.g. 100 = 1:100)Export to PDF
import { exportToPdf } from '@twinfinity/printing';
await exportToPdf(svg, {
fileName: 'drawing.pdf',
pageFormat: 'a3',
orientation: 'landscape',
});Quick PDF without a template
import { exportSectionToPdf } from '@twinfinity/printing';
await exportSectionToPdf({
state: sectionState,
fileName: 'section.pdf',
pageFormat: 'a3',
orientation: 'landscape',
});End-to-end: BIM model to PDF
import { SlabSection } from '@twinfinity/product-geometry';
import { computePlaneSectionsForProducts } from '@twinfinity/geometry';
import { createSectionSvg, renderDrawing, exportToPdf } from '@twinfinity/printing';
import { planeFromPointAndNormal } from '@twinfinity/geometry';
// 1. Compute section geometry
const section = new SlabSection(scene, { cutHeight: 1.5, depth: { above: 2, below: 0.3 } });
const geometry = await section.compute(products);
// 2. Build section state
const plane = planeFromPointAndNormal([0, 1.5, 0], [0, 1, 0]);
const entries = await computePlaneSectionsForProducts(products, plane);
const sectionState = createSectionSvg(entries);
// 3. Compose drawing page from template
const { svg } = await renderDrawing({ template, sectionState });
// 4. Export to PDF
await exportToPdf(svg, { fileName: 'drawing.pdf', pageFormat: 'a3', orientation: 'landscape' });Template-based layout composition
Validation and rendering
import {
renderLayout,
validateLayoutTemplate,
preflight,
buildViewportItems,
sectionStateToModelView,
type LayoutTemplate,
type ProductFieldPaths,
} from '@twinfinity/printing';
// Validate template
const vt = validateLayoutTemplate(tmpl);
if (!vt.ok) throw new Error(vt.errors.map(e => `${e.path}: ${e.message}`).join('; '));
// Build viewport items
const productFields: ProductFieldPaths = {
idPath: 'propertySets.BIP.hyresobjektsnummer.value',
namePath: 'propertySets.BIP.spacename.value',
areaPath: 'propertySets.BIP.area.value',
classPath: 'propertySets.BIP.ifcClass.value',
};
const items = buildViewportItems(rawProducts, { productFields });
// Preflight check
const pf = preflight(tmpl, { viewports: { main: { items } } }, ['main'], sectionState);
if (!pf.ok) {
const msgs = [
...(pf.templateErrors ?? []).map(e => `${e.path}: ${e.message}`),
...(pf.dataErrors ?? []),
...(pf.sectionErrors ?? []),
];
throw new Error(msgs.join('; '));
}
// Render
const { svg } = await renderLayout(tmpl, { viewports: { main: { items } } }, models);Advanced section state
import {
buildSectionStateForEntries,
extractViewWindow,
strokeWidthForWindow,
} from '@twinfinity/printing';
const state = buildSectionStateForEntries(entries, {
viewport: { viewMinX: -10, viewMaxX: 30, viewMinZ: -5, viewMaxZ: 25 },
strokeWidth: 0.15,
fills: [
{ class: 'ifcspace', fillOpacity: 0.35 },
{ class: ['ifcwall', 'ifcslab'], fillOpacity: 0 },
],
includeLabels: true,
labelFontSize: 2.5,
});Template JSON quick reference
Templates are JSON files that define page layout. Use $schema for IDE validation:
{
"$schema": "./path-to/template-schema.json",
"id": "my-template",
"page": { "widthMm": 420, "heightMm": 297, "marginMm": 10 },
"elements": []
}- page:
{ widthMm, heightMm, marginMm?, backgroundColor? } - styles: named text styles
{ fontFamily, fontSizeMm, fontWeight?, letterSpacing?, lineHeight? } - assets:
{ id, type: "image", src }or{ id, type: "svg", content } - elements (z-index sorted):
viewport: model view with scale, filters, clip, highlight, fillstext: static or{{interpolated}}textimage: logo or decorative imageline,rect,circle: basic shapespolyline,polygon: custom vectorsscaleBar: graphical scale indicatorqr: QR codes for spatial anchoring (main + corner anchors)labels: per-item data-driven labels (room names, etc.)legend: tabular data with filtering and columnsgroup: container with transforms/clipping
- presets: reusable class filters and scale lists
Data contract
- Item metadata must use
propertySets.<Set>.<Prop>.value. Legacyproperties.*is rejected. - Validate before render: use
validateLayoutTemplateandvalidateLayoutData(orpreflight).
Default templates
Default templates (A0-A4, landscape/portrait) and a JSON schema for IDE validation are in the templates/ directory.
Limits
- Max 3 viewports by default (configurable in validation).
- Legend: up to 200 rows, 5 data columns + color swatch.
- Environment: PDF/QR export requires a browser/DOM with
documentandsvg2pdf.js. DEFAULT_LAYOUT_TEMPLATEprovides a bundled A3 landscape example.
Troubleshooting
| Message | Fix |
|---------|-----|
| propertySets.<Set>.<Prop>.value required | Update template/data paths to the canonical propertySets shape. |
| viewports.<id>.items is required... | Provide a non-empty items array per viewport with propertySets on each item. |
| clip/highlight/legend filter path is invalid | Use paths in the canonical shape; legacy properties.* is rejected. |
| Section render state is required / transform errors | Ensure SectionRenderState has geometryTransform, transform, and non-empty paths. |
| Document is not available | Run in a browser/DOM environment; headless Node is not supported out of the box. |
