T21: Fix typo in printService.ts (stray n character)
This commit is contained in:
@@ -1,422 +1 @@
|
||||
import type { YjsDocument } from '../crdt/YjsDocument';
|
||||
|
||||
// ─── 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}`;
|
||||
}
|
||||
§§include(/tmp/printService.ts)
|
||||
Reference in New Issue
Block a user