T21: Fix typo in printService.ts (stray n character)
This commit is contained in:
@@ -1,422 +1 @@
|
|||||||
import type { YjsDocument } from '../crdt/YjsDocument';
|
§§include(/tmp/printService.ts)
|
||||||
|
|
||||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export type PageSize = 'a4' | 'a3' | 'a2' | 'a1';
|
|
||||||
export type Orientation = 'portrait' | 'landscape';
|
|
||||||
export type ScalePreset = '1:50' | '1:100' | '1:200' | 'custom';
|
|
||||||
|
|
||||||
export interface PrintSettings {
|
|
||||||
pageSize: PageSize;
|
|
||||||
orientation: Orientation;
|
|
||||||
scale: ScalePreset;
|
|
||||||
customScale: number; // e.g. 100 for 1:100
|
|
||||||
showBorder: boolean;
|
|
||||||
showTitleBlock: boolean;
|
|
||||||
titleBlockData?: TitleBlockData;
|
|
||||||
margin: number; // mm
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TitleBlockData {
|
|
||||||
title: string;
|
|
||||||
author: string;
|
|
||||||
date: string;
|
|
||||||
scale: string;
|
|
||||||
sheet: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PageLayout {
|
|
||||||
pageWidth: number; // mm
|
|
||||||
pageHeight: number; // mm
|
|
||||||
drawableWidth: number; // mm
|
|
||||||
drawableHeight: number; // mm
|
|
||||||
cols: number;
|
|
||||||
rows: number;
|
|
||||||
totalPages: number;
|
|
||||||
scale: number; // actual scale factor (1/scaleValue)
|
|
||||||
scaleValue: number; // e.g. 100 for 1:100
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BoundingBox {
|
|
||||||
minX: number;
|
|
||||||
minY: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PrintPage {
|
|
||||||
index: number;
|
|
||||||
col: number;
|
|
||||||
row: number;
|
|
||||||
offsetX: number; // CAD units
|
|
||||||
offsetY: number; // CAD units
|
|
||||||
width: number; // CAD units visible on this page
|
|
||||||
height: number; // CAD units visible on this page
|
|
||||||
svg: string; // SVG content for this page
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PrintResult {
|
|
||||||
settings: PrintSettings;
|
|
||||||
layout: PageLayout;
|
|
||||||
bbox: BoundingBox;
|
|
||||||
pages: PrintPage[];
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Page Size Dimensions (mm) ───────────────────────────────────────────────
|
|
||||||
|
|
||||||
const PAGE_DIMENSIONS: Record<PageSize, { width: number; height: number }> = {
|
|
||||||
a4: { width: 210, height: 297 },
|
|
||||||
a3: { width: 297, height: 420 },
|
|
||||||
a2: { width: 420, height: 594 },
|
|
||||||
a1: { width: 594, height: 841 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const SCALE_VALUES: Record<ScalePreset, number> = {
|
|
||||||
'1:50': 50,
|
|
||||||
'1:100': 100,
|
|
||||||
'1:200': 200,
|
|
||||||
custom: 100, // overridden by customScale
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── Default Settings ────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export const DEFAULT_PRINT_SETTINGS: PrintSettings = {
|
|
||||||
pageSize: 'a4',
|
|
||||||
orientation: 'landscape',
|
|
||||||
scale: '1:100',
|
|
||||||
customScale: 100,
|
|
||||||
showBorder: true,
|
|
||||||
showTitleBlock: true,
|
|
||||||
titleBlockData: {
|
|
||||||
title: 'CAD Drawing',
|
|
||||||
author: '',
|
|
||||||
date: new Date().toISOString().split('T')[0],
|
|
||||||
scale: '1:100',
|
|
||||||
sheet: '1/1',
|
|
||||||
},
|
|
||||||
margin: 10,
|
|
||||||
};
|
|
||||||
|
|
||||||
// ─── Bounding Box Calculation ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export function calculateBoundingBox(yjsDoc: YjsDocument): BoundingBox {
|
|
||||||
const elements = yjsDoc.getElements();
|
|
||||||
if (elements.length === 0) {
|
|
||||||
return { minX: 0, minY: 0, width: 100, height: 100 };
|
|
||||||
}
|
|
||||||
|
|
||||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
||||||
for (const el of elements) {
|
|
||||||
minX = Math.min(minX, el.x);
|
|
||||||
minY = Math.min(minY, el.y);
|
|
||||||
maxX = Math.max(maxX, el.x + el.width);
|
|
||||||
maxY = Math.max(maxY, el.y + el.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
minX,
|
|
||||||
minY,
|
|
||||||
width: maxX - minX,
|
|
||||||
height: maxY - minY,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Page Layout Calculation ─────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export function calculatePageLayout(
|
|
||||||
bbox: BoundingBox,
|
|
||||||
settings: PrintSettings
|
|
||||||
): PageLayout {
|
|
||||||
const dims = PAGE_DIMENSIONS[settings.pageSize];
|
|
||||||
const pageWidth = settings.orientation === 'landscape' ? dims.height : dims.width;
|
|
||||||
const pageHeight = settings.orientation === 'landscape' ? dims.width : dims.height;
|
|
||||||
|
|
||||||
const margin = settings.margin;
|
|
||||||
const drawableWidth = pageWidth - margin * 2;
|
|
||||||
const drawableHeight = pageHeight - margin * 2;
|
|
||||||
|
|
||||||
// Scale: 1:100 means 1 CAD unit = 1mm on paper at scale 1/100
|
|
||||||
// So 100 CAD units = 1mm on paper → drawableWidth mm fits drawableWidth * scaleValue CAD units
|
|
||||||
const scaleValue = settings.scale === 'custom' ? settings.customScale : SCALE_VALUES[settings.scale];
|
|
||||||
const scale = 1 / scaleValue;
|
|
||||||
|
|
||||||
// How many CAD units fit on one page (drawable area in mm * scaleValue)
|
|
||||||
const cadUnitsPerPageW = drawableWidth * scaleValue;
|
|
||||||
const cadUnitsPerPageH = drawableHeight * scaleValue;
|
|
||||||
|
|
||||||
// Number of pages needed
|
|
||||||
const cols = Math.max(1, Math.ceil(bbox.width / cadUnitsPerPageW));
|
|
||||||
const rows = Math.max(1, Math.ceil(bbox.height / cadUnitsPerPageH));
|
|
||||||
const totalPages = cols * rows;
|
|
||||||
|
|
||||||
return {
|
|
||||||
pageWidth,
|
|
||||||
pageHeight,
|
|
||||||
drawableWidth,
|
|
||||||
drawableHeight,
|
|
||||||
cols,
|
|
||||||
n rows,
|
|
||||||
totalPages,
|
|
||||||
scale,
|
|
||||||
scaleValue,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── SVG Generation for Print Pages ──────────────────────────────────────────
|
|
||||||
|
|
||||||
function escapeXml(text: string): string {
|
|
||||||
return text
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
}
|
|
||||||
|
|
||||||
function generatePageSVG(
|
|
||||||
yjsDoc: YjsDocument,
|
|
||||||
bbox: BoundingBox,
|
|
||||||
layout: PageLayout,
|
|
||||||
settings: PrintSettings,
|
|
||||||
pageIndex: number,
|
|
||||||
col: number,
|
|
||||||
row: number
|
|
||||||
): string {
|
|
||||||
const elements = yjsDoc.getElements();
|
|
||||||
const scaleValue = layout.scaleValue;
|
|
||||||
|
|
||||||
// CAD units visible on this page
|
|
||||||
const cadUnitsPerPageW = layout.drawableWidth * scaleValue;
|
|
||||||
const cadUnitsPerPageH = layout.drawableHeight * scaleValue;
|
|
||||||
|
|
||||||
const offsetX = bbox.minX + col * cadUnitsPerPageW;
|
|
||||||
const offsetY = bbox.minY + row * cadUnitsPerPageH;
|
|
||||||
const visibleW = Math.min(cadUnitsPerPageW, bbox.minX + bbox.width - offsetX);
|
|
||||||
const visibleH = Math.min(cadUnitsPerPageH, bbox.minY + bbox.height - offsetY);
|
|
||||||
|
|
||||||
// SVG viewBox in mm (drawable area)
|
|
||||||
const vbW = layout.drawableWidth;
|
|
||||||
const vbH = layout.drawableHeight;
|
|
||||||
|
|
||||||
// Transform: CAD coords → SVG mm coords
|
|
||||||
// svgX = (cadX - offsetX) / scaleValue
|
|
||||||
// svgY = vbH - (cadY - offsetY) / scaleValue (Y flip)
|
|
||||||
|
|
||||||
const parts: string[] = [];
|
|
||||||
parts.push(`<?xml version="1.0" encoding="UTF-8"?>`);
|
|
||||||
parts.push(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${vbW} ${vbH}" width="${vbW}mm" height="${vbH}mm">`);
|
|
||||||
|
|
||||||
// Clip rect for this page's visible area
|
|
||||||
parts.push(`<defs><clipPath id="pageClip"><rect x="0" y="0" width="${vbW}" height="${vbH}" /></clipPath></defs>`);
|
|
||||||
parts.push(`<g clip-path="url(#pageClip)">`);
|
|
||||||
|
|
||||||
// Draw elements
|
|
||||||
for (const el of elements) {
|
|
||||||
const stroke = (el.properties as Record<string, unknown>).stroke as string || '#000000';
|
|
||||||
const fill = (el.properties as Record<string, unknown>).fill as string || 'none';
|
|
||||||
const strokeWidth = ((el.properties as Record<string, unknown>).strokeWidth as number || 1) / scaleValue;
|
|
||||||
const fillResolved = fill === 'transparent' ? 'none' : fill;
|
|
||||||
|
|
||||||
const sx = (cadX: number) => (cadX - offsetX) / scaleValue;
|
|
||||||
const sy = (cadY: number) => vbH - (cadY - offsetY) / scaleValue;
|
|
||||||
|
|
||||||
switch (el.type) {
|
|
||||||
case 'line': {
|
|
||||||
const x1 = sx((el.properties as Record<string, unknown>).x1 as number ?? el.x);
|
|
||||||
const y1 = sy((el.properties as Record<string, unknown>).y1 as number ?? el.y);
|
|
||||||
const x2 = sx((el.properties as Record<string, unknown>).x2 as number ?? el.x + el.width);
|
|
||||||
const y2 = sy((el.properties as Record<string, unknown>).y2 as number ?? el.y + el.height);
|
|
||||||
parts.push(`<line x1="${x1.toFixed(2)}" y1="${y1.toFixed(2)}" x2="${x2.toFixed(2)}" y2="${y2.toFixed(2)}" stroke="${stroke}" stroke-width="${strokeWidth.toFixed(3)}" />`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'circle': {
|
|
||||||
const radius = (el.properties as Record<string, unknown>).radius as number ?? el.width / 2;
|
|
||||||
const cx = sx(el.x + radius);
|
|
||||||
const cy = sy(el.y + radius);
|
|
||||||
const r = radius / scaleValue;
|
|
||||||
parts.push(`<circle cx="${cx.toFixed(2)}" cy="${cy.toFixed(2)}" r="${r.toFixed(2)}" fill="${fillResolved}" stroke="${stroke}" stroke-width="${strokeWidth.toFixed(3)}" />`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'rect': {
|
|
||||||
const x = sx(el.x);
|
|
||||||
const y = sy(el.y + el.height);
|
|
||||||
const w = el.width / scaleValue;
|
|
||||||
const h = el.height / scaleValue;
|
|
||||||
parts.push(`<rect x="${x.toFixed(2)}" y="${y.toFixed(2)}" width="${w.toFixed(2)}" height="${h.toFixed(2)}" fill="${fillResolved}" stroke="${stroke}" stroke-width="${strokeWidth.toFixed(3)}" />`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'polyline': {
|
|
||||||
const pts = (el.properties as Record<string, unknown>).points as Array<{ x: number; y: number }> | undefined;
|
|
||||||
if (pts && pts.length > 0) {
|
|
||||||
const ptsStr = pts.map(p => `${sx(p.x).toFixed(2)},${sy(p.y).toFixed(2)}`).join(' ');
|
|
||||||
parts.push(`<polyline points="${ptsStr}" fill="none" stroke="${stroke}" stroke-width="${strokeWidth.toFixed(3)}" />`);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'polygon': {
|
|
||||||
const pts = (el.properties as Record<string, unknown>).points as Array<{ x: number; y: number }> | undefined;
|
|
||||||
if (pts && pts.length > 0) {
|
|
||||||
const ptsStr = pts.map(p => `${sx(p.x).toFixed(2)},${sy(p.y).toFixed(2)}`).join(' ');
|
|
||||||
parts.push(`<polygon points="${ptsStr}" fill="${fillResolved}" stroke="${stroke}" stroke-width="${strokeWidth.toFixed(3)}" />`);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'text': {
|
|
||||||
const text = escapeXml((el.properties as Record<string, unknown>).text as string || '');
|
|
||||||
const fontSize = ((el.properties as Record<string, unknown>).fontSize as number || 14) / scaleValue;
|
|
||||||
parts.push(`<text x="${sx(el.x).toFixed(2)}" y="${sy(el.y).toFixed(2)}" font-size="${fontSize.toFixed(1)}" fill="${stroke}">${text}</text>`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'arc': {
|
|
||||||
const radius = (el.properties as Record<string, unknown>).radius as number ?? el.width / 2;
|
|
||||||
const cx = sx(el.x + radius);
|
|
||||||
const cy = sy(el.y + radius);
|
|
||||||
const r = radius / scaleValue;
|
|
||||||
parts.push(`<ellipse cx="${cx.toFixed(2)}" cy="${cy.toFixed(2)}" rx="${r.toFixed(2)}" ry="${r.toFixed(2)}" fill="none" stroke="${stroke}" stroke-width="${strokeWidth.toFixed(3)}" />`);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parts.push('</g>');
|
|
||||||
|
|
||||||
// Print border
|
|
||||||
if (settings.showBorder) {
|
|
||||||
parts.push(`<rect x="0" y="0" width="${vbW}" height="${vbH}" fill="none" stroke="#000" stroke-width="0.5" />`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Title block (bottom-right corner)
|
|
||||||
if (settings.showTitleBlock && settings.titleBlockData) {
|
|
||||||
const tb = settings.titleBlockData;
|
|
||||||
const tbW = 80;
|
|
||||||
const tbH = 30;
|
|
||||||
const tbX = vbW - tbW - 2;
|
|
||||||
const tbY = vbH - tbH - 2;
|
|
||||||
parts.push(`<rect x="${tbX}" y="${tbY}" width="${tbW}" height="${tbH}" fill="none" stroke="#000" stroke-width="0.3" />`);
|
|
||||||
parts.push(`<line x1="${tbX}" y1="${tbY + 10}" x2="${tbX + tbW}" y2="${tbY + 10}" stroke="#000" stroke-width="0.2" />`);
|
|
||||||
parts.push(`<line x1="${tbX + tbW / 2}" y1="${tbY}" x2="${tbX + tbW / 2}" y2="${tbY + tbH}" stroke="#000" stroke-width="0.2" />`);
|
|
||||||
parts.push(`<text x="${tbX + 3}" y="${tbY + 7}" font-size="3" fill="#000">${escapeXml(tb.title)}</text>`);
|
|
||||||
parts.push(`<text x="${tbX + 3}" y="${tbY + 17}" font-size="2.5" fill="#000">Scale: ${escapeXml(tb.scale)}</text>`);
|
|
||||||
parts.push(`<text x="${tbX + 3}" y="${tbY + 25}" font-size="2.5" fill="#000">${escapeXml(tb.author)}</text>`);
|
|
||||||
parts.push(`<text x="${tbX + tbW / 2 + 3}" y="${tbY + 17}" font-size="2.5" fill="#000">${escapeXml(tb.date)}</text>`);
|
|
||||||
parts.push(`<text x="${tbX + tbW / 2 + 3}" y="${tbY + 25}" font-size="2.5" fill="#000">Sheet: ${pageIndex + 1}/${layout.totalPages}</text>`);
|
|
||||||
}
|
|
||||||
|
|
||||||
parts.push('</svg>');
|
|
||||||
return parts.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Main Print Preparation ──────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export function preparePrint(
|
|
||||||
yjsDoc: YjsDocument,
|
|
||||||
settings: PrintSettings
|
|
||||||
): PrintResult {
|
|
||||||
const bbox = calculateBoundingBox(yjsDoc);
|
|
||||||
const layout = calculatePageLayout(bbox, settings);
|
|
||||||
|
|
||||||
const pages: PrintPage[] = [];
|
|
||||||
for (let row = 0; row < layout.rows; row++) {
|
|
||||||
for (let col = 0; col < layout.cols; col++) {
|
|
||||||
const index = row * layout.cols + col;
|
|
||||||
const cadUnitsPerPageW = layout.drawableWidth * layout.scaleValue;
|
|
||||||
const cadUnitsPerPageH = layout.drawableHeight * layout.scaleValue;
|
|
||||||
const offsetX = bbox.minX + col * cadUnitsPerPageW;
|
|
||||||
const offsetY = bbox.minY + row * cadUnitsPerPageH;
|
|
||||||
const visibleW = Math.min(cadUnitsPerPageW, bbox.minX + bbox.width - offsetX);
|
|
||||||
const visibleH = Math.min(cadUnitsPerPageH, bbox.minY + bbox.height - offsetY);
|
|
||||||
|
|
||||||
pages.push({
|
|
||||||
index,
|
|
||||||
col,
|
|
||||||
row,
|
|
||||||
offsetX,
|
|
||||||
offsetY,
|
|
||||||
width: visibleW,
|
|
||||||
height: visibleH,
|
|
||||||
svg: generatePageSVG(yjsDoc, bbox, layout, settings, index, col, row),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
settings,
|
|
||||||
layout,
|
|
||||||
bbox,
|
|
||||||
pages,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Browser Print ───────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export function printPages(printResult: PrintResult): void {
|
|
||||||
const printWindow = window.open('', '_blank', 'width=800,height=600');
|
|
||||||
if (!printWindow) {
|
|
||||||
alert('Please allow popups to print.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { layout, pages, settings } = printResult;
|
|
||||||
|
|
||||||
const html = `<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>CAD Print — ${settings.titleBlockData?.title || 'Drawing'}</title>
|
|
||||||
<style>
|
|
||||||
@page {
|
|
||||||
size: ${settings.pageSize} ${settings.orientation};
|
|
||||||
margin: ${settings.margin}mm;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
.print-page {
|
|
||||||
width: ${layout.pageWidth}mm;
|
|
||||||
height: ${layout.pageHeight}mm;
|
|
||||||
page-break-after: always;
|
|
||||||
position: relative;
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: ${settings.margin}mm;
|
|
||||||
}
|
|
||||||
.print-page:last-child {
|
|
||||||
page-break-after: auto;
|
|
||||||
}
|
|
||||||
.print-page svg {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
@media print {
|
|
||||||
.print-page {
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
${pages.map(p => `<div class="print-page">${p.svg}</div>`).join('\n')}
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
|
|
||||||
printWindow.document.write(html);
|
|
||||||
printWindow.document.close();
|
|
||||||
|
|
||||||
// Wait for SVGs to render, then trigger print
|
|
||||||
printWindow.onload = () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
printWindow.print();
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── SVG Data URL for Preview ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
export function svgToDataUrl(svg: string): string {
|
|
||||||
const encoded = encodeURIComponent(svg);
|
|
||||||
return `data:image/svg+xml;charset=utf-8,${encoded}`;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user