Compare commits

...

2 Commits

5 changed files with 166 additions and 3 deletions
+4 -3
View File
@@ -635,7 +635,7 @@ Kurz-Tasks (je gleiche Struktur: implementieren im passenden builtin-Paket + Wor
- [x] G2 LayoutBar UI + Viewport-Widget (zeigt Modellbereich skaliert, klickbar zoom-to) *(2026-08-29: LayoutBar-Komponente mit Minimap-Widget (computeMinimapRects, Modell-BBox + Viewport-Rechteck), ZoomPanController.setView(center,scale), CanvasArea-__v2LayoutBridge, App-Mount; Log G2)* - [x] G2 LayoutBar UI + Viewport-Widget (zeigt Modellbereich skaliert, klickbar zoom-to) *(2026-08-29: LayoutBar-Komponente mit Minimap-Widget (computeMinimapRects, Modell-BBox + Viewport-Rechteck), ZoomPanController.setView(center,scale), CanvasArea-__v2LayoutBridge, App-Mount; Log G2)*
- [x] G3 Titelblock-Block mit Attributen (Projekt/Datum/Maßstab/Autor) auto-insert *(2026-08-29: titleBlockService mit createTitleBlockElements (Rahmen + Titel + 4 attributierte Texte nach F5/F6-ATTDEF-Konvention tag/attdef/prompt), insertTitleBlockForLayout setzt frameBlockRef; CADDocument-Block-CRUD getBlock/addBlock; LayoutBar-Button 🏷; Log G3)* - [x] G3 Titelblock-Block mit Attributen (Projekt/Datum/Maßstab/Autor) auto-insert *(2026-08-29: titleBlockService mit createTitleBlockElements (Rahmen + Titel + 4 attributierte Texte nach F5/F6-ATTDEF-Konvention tag/attdef/prompt), insertTitleBlockForLayout setzt frameBlockRef; CADDocument-Block-CRUD getBlock/addBlock; LayoutBar-Button 🏷; Log G3)*
- [x] G4 PDF-Export je Layout im Ausgabemaßstab (pdf-lib; Linienbreiten aus Layern; monochrome Option) *(2026-08-29: exportLayoutPDF über elementToPrimitives (C2-Konventionen — korrekte Circle-Zentrum-Behandlung, im Gegensatz zum Legacy-exportPDF mit x+width/2-Bug), Viewport-skaliert A4 landscape, Layer-Dash-Patterns + Breiten, monochrome, Titelblock-Mitzeichnung; Log G4)* - [x] G4 PDF-Export je Layout im Ausgabemaßstab (pdf-lib; Linienbreiten aus Layern; monochrome Option) *(2026-08-29: exportLayoutPDF über elementToPrimitives (C2-Konventionen — korrekte Circle-Zentrum-Behandlung, im Gegensatz zum Legacy-exportPDF mit x+width/2-Bug), Viewport-skaliert A4 landscape, Layer-Dash-Patterns + Breiten, monochrome, Titelblock-Mitzeichnung; Log G4)*
- [ ] G5 Browser-Print stylesheet (@media print: nur Layout) - [x] G5 Browser-Print stylesheet (@media print: nur Layout) *(2026-08-29: @media print Block mit @page A4 landscape, .screen-only Chrome-Ausblendung, .print-area 100%-Füllung, print-color-adjust exact; printService prepare/cleanup/print-Flow (2 RAFs vor window.print); LayoutBar-Druck-Button 🖨; Log G5)*
## Phase H — Modi & Branchenneutralität ## Phase H — Modi & Branchenneutralität
- [ ] H1 simple/pro Mode (uiStore.mode, Ribbon-Filter tags basic, CommandLine hide, Settings persist) - [ ] H1 simple/pro Mode (uiStore.mode, Ribbon-Filter tags basic, CommandLine hide, Settings persist)
@@ -667,6 +667,7 @@ Format: `YYYY-MM-DD | TASK-ID | was getan | Tests Ergebnis | commit | Notizen/of
| Datum | Task | Zusammenfassung | Tests | Commit | Notizen | | Datum | Task | Zusammenfassung | Tests | Commit | Notizen |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| 2026-08-29 | G5 | Browser-Print stylesheet: (1) @media print Block in styles.css — @page size A4 landscape margin 10mm; .screen-only display none !important (UI-Chrome); .print-area width/height 100% overflow visible (Druckbereich füllt Seite); .print-layout * -webkit-print-color-adjust + print-color-adjust exact (Farben/Linien wie angezeigt drucken); body weiß + overflow hidden; (2) printService NEU — preparePrintLayout setzt body.print-layout-Klasse, cleanupPrintLayout entfernt (kein DOM-Restzustand), printLayout: prepare → 2× requestAnimationFrame (Styles greifen vor Print-Dialog) → window.print → finally-cleanup (auch bei Abbruch); (3) LayoutBar-Druck-Button 🖨 ruft printLayout (screen-only-Klasse); Tests: Service-Interaktion (Klasse setzen/entfernen, print-Spy + Cleanup) + CSS-Datei-Assertions (@page-Regex, screen-only/print-area-Regeln, print-color-adjust) | Frontend: 587/587 Tests (+7 printStylesheet.test.ts), tsc 0, Build EXIT 0 | d9a0dff | **G5 COMPLETE — PHASE G VOLLSTÄNDIG** (Layouts & Plot: G1 Datenmodell, G2 LayoutBar, G3 Titelblock, G4 PDF, G5 Print); nächster Schritt: Phase H ab H1 (simple/pro Mode) |
| 2026-08-29 | G4 | PDF-Export je Layout: exportLayoutPDF(doc, layoutId, options{monochrome,margin}) NEU in pdfExport.ts (additiv — Legacy-exportPDF unangetastet): A4 landscape 842×595pt; Ausgabemaßstab = layout.viewport.scale, sichtbarer Welt-Ausschnitt aus center berechnet; Y-Flip (PDF bottom-left vs CAD top-down) in toPage-Transform; Rendering über elementToPrimitives (C2) statt duplizierter Typ-Switches → garantiert identische Konventionen wie Pixi-Renderer (Legacy-exportPDF hätte Circle um width/2 verschoben gezeichnet — gleiche Bugklasse wie DXF-Writer vor F6, dort belassen und im Log vermerkt); Linienbreiten: properties.strokeWidth sonst Layer-lineType (solid 0.7pt/dashed 0.9pt/dotted 0.5pt) mit Dash-Patterns [4,3]/[0.5,2]; monochrome erzwingt rgb(0,0,0) + keine Dashes; unsichtbare Layer gefiltert; Arc→32 Segmente (Grad CCW); Titelblock aus frameBlockRef wird unskaliert unten-rechts mitgezeichnet (Block-Elemente um Ursprung an Ecken-Offset); drawPrimitive deckt alle 6 Primitive-Kinds | Frontend: 580/580 Tests (+6 pdfLayoutExport.test.ts — PDF via PDFDocument.load verifiziert: Seitenzahl 1, Größe 842×595, Frame-Block-Fall, monochrome, leerer Viewport, unknown-ID-throw, unsichtbare Layer), tsc 0, Build EXIT 0 | cb23638 | G4 COMPLETE; nächster Task G5 (Print-Stylesheet) | | 2026-08-29 | G4 | PDF-Export je Layout: exportLayoutPDF(doc, layoutId, options{monochrome,margin}) NEU in pdfExport.ts (additiv — Legacy-exportPDF unangetastet): A4 landscape 842×595pt; Ausgabemaßstab = layout.viewport.scale, sichtbarer Welt-Ausschnitt aus center berechnet; Y-Flip (PDF bottom-left vs CAD top-down) in toPage-Transform; Rendering über elementToPrimitives (C2) statt duplizierter Typ-Switches → garantiert identische Konventionen wie Pixi-Renderer (Legacy-exportPDF hätte Circle um width/2 verschoben gezeichnet — gleiche Bugklasse wie DXF-Writer vor F6, dort belassen und im Log vermerkt); Linienbreiten: properties.strokeWidth sonst Layer-lineType (solid 0.7pt/dashed 0.9pt/dotted 0.5pt) mit Dash-Patterns [4,3]/[0.5,2]; monochrome erzwingt rgb(0,0,0) + keine Dashes; unsichtbare Layer gefiltert; Arc→32 Segmente (Grad CCW); Titelblock aus frameBlockRef wird unskaliert unten-rechts mitgezeichnet (Block-Elemente um Ursprung an Ecken-Offset); drawPrimitive deckt alle 6 Primitive-Kinds | Frontend: 580/580 Tests (+6 pdfLayoutExport.test.ts — PDF via PDFDocument.load verifiziert: Seitenzahl 1, Größe 842×595, Frame-Block-Fall, monochrome, leerer Viewport, unknown-ID-throw, unsichtbare Layer), tsc 0, Build EXIT 0 | cb23638 | G4 COMPLETE; nächster Task G5 (Print-Stylesheet) |
| 2026-08-29 | G3 | Titelblock-Block mit Attributen: (1) titleBlockService NEU — createTitleBlockElements (reine Funktion): Rahmen rect 180×100 (App-Konvention x/y=Zentrum) + Titel-Label + 4 attributierte Texte PROJEKT/DATUM/MASSSTAB/AUTOR mit tag/attdef:true/prompt-Metadaten und text=TAG=Wert (exakt die F5-Parser- und F6-Writer-Konvention → DXF-ATTDEF-Roundtrip & D12-Formular-ready), leere Meta → Fallback —; (2) insertTitleBlockForLayout: Blockdefinition (category frame) via doc.addBlock + frameBlockRef im Layout via updateLayout, Re-Insert ersetzt Referenz (neue Block-ID), fehlendes Layout → throw; (3) CADDocument um getBlock/addBlock erweitert (bewusst NICHT undo-getrackt — konsistent zum UndoManager-Design-Kommentar); (4) LayoutBar-Button 🏷 je Layout: fügt/aktualisiert Titelblock mit Layout-Name als Projekt, heutiges Datum, Maßstab aus viewport.scale (1:1/scale gerundet), Autor WebCAD; Test-Zwischenfall: eigene Assertion prüfte attdef über alle 5 Texte inkl. Titel-Label (bewusst kein Attribut) — auf getaggte Texte gescoped | Frontend: 574/574 Tests (+6 titleBlock.test.ts), tsc 0, Build EXIT 0 | bbb469e | G3 COMPLETE; nächster Task G4 (PDF-Export je Layout) | | 2026-08-29 | G3 | Titelblock-Block mit Attributen: (1) titleBlockService NEU — createTitleBlockElements (reine Funktion): Rahmen rect 180×100 (App-Konvention x/y=Zentrum) + Titel-Label + 4 attributierte Texte PROJEKT/DATUM/MASSSTAB/AUTOR mit tag/attdef:true/prompt-Metadaten und text=TAG=Wert (exakt die F5-Parser- und F6-Writer-Konvention → DXF-ATTDEF-Roundtrip & D12-Formular-ready), leere Meta → Fallback —; (2) insertTitleBlockForLayout: Blockdefinition (category frame) via doc.addBlock + frameBlockRef im Layout via updateLayout, Re-Insert ersetzt Referenz (neue Block-ID), fehlendes Layout → throw; (3) CADDocument um getBlock/addBlock erweitert (bewusst NICHT undo-getrackt — konsistent zum UndoManager-Design-Kommentar); (4) LayoutBar-Button 🏷 je Layout: fügt/aktualisiert Titelblock mit Layout-Name als Projekt, heutiges Datum, Maßstab aus viewport.scale (1:1/scale gerundet), Autor WebCAD; Test-Zwischenfall: eigene Assertion prüfte attdef über alle 5 Texte inkl. Titel-Label (bewusst kein Attribut) — auf getaggte Texte gescoped | Frontend: 574/574 Tests (+6 titleBlock.test.ts), tsc 0, Build EXIT 0 | bbb469e | G3 COMPLETE; nächster Task G4 (PDF-Export je Layout) |
| 2026-08-29 | G2 | LayoutBar UI + Viewport-Widget: (1) ZoomPanController.setView(center, scale) NEU — Welt-Punkt exakt ins Canvas-Zentrum bei Scale, geclamped 0.01100; (2) LayoutBar-Komponente: listet Layouts des aktiven Docs (documentService-Fallback mit onActiveDocumentChanged-Subscription + onChanged-Live-Reload), je Eintrag SVG-Minimap (Modellbereich als Rechteck + Viewport-Ausschnitt blau — scale-abhängig sichtbar kleiner), Klick = zoom-to via Bridge, „+“ speichert aktuellen Viewport als Layout, 🗑 löscht; (3) computeMinimapRects als reine exportierte Funktion (Modell-BBox füllt Widget mit 6px Padding, ohne Elemente Volles-Widget-Fallback, Viewport = 800×600/scale nominal); (4) CanvasArea-__v2LayoutBridge nach E6b-Muster (zoomTo mit render-Trigger, getView aus getViewport+getScale); (5) App.tsx mountet LayoutBar über StatusBar; CSS angehängt; Zwischenfälle selbst gefixt: view-Parameter-Kollision in computeMinimapRects (Umbenennung viewRect) + Regex-Double-Escape im Test | Frontend: 568/568 Tests (+11 layoutBar.test.tsx), tsc 0, Build EXIT 0 | 7feed9e | G2 COMPLETE; nächster Task G3 (Titelblock-Block mit Attributen auto-insert) | | 2026-08-29 | G2 | LayoutBar UI + Viewport-Widget: (1) ZoomPanController.setView(center, scale) NEU — Welt-Punkt exakt ins Canvas-Zentrum bei Scale, geclamped 0.01100; (2) LayoutBar-Komponente: listet Layouts des aktiven Docs (documentService-Fallback mit onActiveDocumentChanged-Subscription + onChanged-Live-Reload), je Eintrag SVG-Minimap (Modellbereich als Rechteck + Viewport-Ausschnitt blau — scale-abhängig sichtbar kleiner), Klick = zoom-to via Bridge, „+“ speichert aktuellen Viewport als Layout, 🗑 löscht; (3) computeMinimapRects als reine exportierte Funktion (Modell-BBox füllt Widget mit 6px Padding, ohne Elemente Volles-Widget-Fallback, Viewport = 800×600/scale nominal); (4) CanvasArea-__v2LayoutBridge nach E6b-Muster (zoomTo mit render-Trigger, getView aus getViewport+getScale); (5) App.tsx mountet LayoutBar über StatusBar; CSS angehängt; Zwischenfälle selbst gefixt: view-Parameter-Kollision in computeMinimapRects (Umbenennung viewRect) + Regex-Double-Escape im Test | Frontend: 568/568 Tests (+11 layoutBar.test.tsx), tsc 0, Build EXIT 0 | 7feed9e | G2 COMPLETE; nächster Task G3 (Titelblock-Block mit Attributen auto-insert) |
@@ -736,5 +737,5 @@ Format: `YYYY-MM-DD | TASK-ID | was getan | Tests Ergebnis | commit | Notizen/of
- `frontend/src/utils/__tests__/format.test.ts` liegt in src statt tests/ — vereinheitlichen (kleiner Task in I2) - `frontend/src/utils/__tests__/format.test.ts` liegt in src statt tests/ — vereinheitlichen (kleiner Task in I2)
--- ---
*Nächster offener Schritt: **G5** (Browser-Print stylesheet: @media print nur Layout). Danach: Rest-D-Tools (D1D4/D9/D10-path/D11/D12), Phase H (Modi/Templates/Rollen/i18n/Flächen), Phase I (Release).* *Nächster offener Schritt: **H1** (simple/pro Mode: uiStore.mode, Ribbon-Filter tags basic, CommandLine hide, Settings persist). Danach: H2 Projekt-Templates, H3 Rollen, H4 i18n, H5 Flächen. Restlich: D1D4/D9/D10-path/D11/D12, Phase I (Release).*
*Status 2026-08-29: Phasen B+A+C+D+E+F komplett, G1G4 fertig (Layouts: Datenmodell, Bar, Titelblock, PDF-Export), BUG-1-Kollisionsklasse geschlossen. FE 580/580 Tests, BE 272/272, tsc/Build grün. Alle Commits auf Forgejo main.* *Status 2026-08-29: Phasen B+A+C+D+E+F+G komplett (G: Layouts Datenmodell/Bar/Titelblock/PDF/Print), BUG-1-Kollisionsklasse geschlossen. FE 587/587 Tests, BE 272/272, tsc/Build grün. Alle Commits auf Forgejo main.*
+11
View File
@@ -10,6 +10,7 @@ import React, { useCallback, useEffect, useState } from 'react';
import type { LayoutDefinition } from '../types/cad.types'; import type { LayoutDefinition } from '../types/cad.types';
import { getActiveDocument, onActiveDocumentChanged } from '../kernel/document/documentService'; import { getActiveDocument, onActiveDocumentChanged } from '../kernel/document/documentService';
import { insertTitleBlockForLayout } from '../services/titleBlockService'; import { insertTitleBlockForLayout } from '../services/titleBlockService';
import { printLayout } from '../services/printService';
export interface LayoutView { export interface LayoutView {
center: { x: number; y: number }; center: { x: number; y: number };
@@ -120,6 +121,10 @@ const LayoutBar: React.FC<LayoutBarProps> = ({ doc: docProp, bridge: bridgeProp
reload(); reload();
}; };
const handlePrint = () => {
void printLayout();
};
const handleZoomTo = (l: LayoutDefinition) => { const handleZoomTo = (l: LayoutDefinition) => {
bridge?.zoomTo(l.viewport.center, l.viewport.scale); bridge?.zoomTo(l.viewport.center, l.viewport.scale);
}; };
@@ -137,6 +142,12 @@ const LayoutBar: React.FC<LayoutBarProps> = ({ doc: docProp, bridge: bridgeProp
onClick={handleAdd} onClick={handleAdd}
disabled={!bridge || !doc} disabled={!bridge || !doc}
>+</button> >+</button>
<button
type="button"
className="layout-bar-print screen-only"
title="Layout drucken (A4 landscape)"
onClick={handlePrint}
>🖨</button>
</div> </div>
{layouts.length === 0 ? ( {layouts.length === 0 ? (
<div className="layout-bar-empty">Keine Layouts</div> <div className="layout-bar-empty">Keine Layouts</div>
+37
View File
@@ -0,0 +1,37 @@
/**
* Task G5 Browser-Print-Service.
*
* Bereitet die App für window.print() vor: Der Body erhält die Klasse
* `print-layout`, das @media print Stylesheet blendet das UI-Chrome aus
* und zeigt nur den Druckbereich (Canvas/Layout). Nach dem Druck wird
* die Klasse wieder entfernt (kein DOM-Mutation-Restzustand).
*/
const PRINT_CLASS = 'print-layout';
/** Setzt den Print-Modus (Body-Klasse; @media print regelt das Rendering). */
export function preparePrintLayout(): void {
document.body.classList.add(PRINT_CLASS);
}
/** Entfernt den Print-Modus (nach Druck oder bei Abbruch). */
export function cleanupPrintLayout(): void {
document.body.classList.remove(PRINT_CLASS);
}
/**
* Druck-Flow: Vorbereitung Rendern der nächsten Frames (Styles müssen
* greifen, bevor der Browser den Print-Dialog aufbaut) window.print
* Aufräumen. await-fähig für Tests (window.print wird gemockt).
*/
export async function printLayout(): Promise<void> {
preparePrintLayout();
// Zwei Frames warten: Style-Anwendung + Layout der Print-Ansicht
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
try {
window.print();
} finally {
cleanupPrintLayout();
}
}
+49
View File
@@ -2925,3 +2925,52 @@ body.drawer-open { overflow: hidden; }
border-radius: 4px; border-radius: 4px;
} }
.layout-bar-tb:hover { color: #4a90d9; background: rgba(74, 144, 217, 0.15); } .layout-bar-tb:hover { color: #4a90d9; background: rgba(74, 144, 217, 0.15); }
/*
Task G5 Browser-Print: @media print zeigt nur den Druckbereich
*/
@media print {
@page {
size: A4 landscape;
margin: 10mm;
}
/* UI-Chrome im Druck ausblenden */
.screen-only {
display: none !important;
}
/* Druckbereich füllt die Seite */
.print-area {
width: 100% !important;
height: auto !important;
overflow: visible !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
}
/* Farben/Linien so drucken wie angezeigt */
.print-layout * {
-webkit-print-color-adjust: exact !important;
print-color-adjust: exact !important;
}
/* Body: kein Scroll, weiße Fläche */
body {
background: #fff !important;
overflow: hidden !important;
}
}
.layout-bar-print {
background: transparent;
color: #6b7280;
border: none;
cursor: pointer;
font-size: 13px;
padding: 2px 4px;
border-radius: 4px;
}
.layout-bar-print:hover { color: #4a90d9; background: rgba(74, 144, 217, 0.15); }
+65
View File
@@ -0,0 +1,65 @@
/**
* Task G5 Browser-Print: @media print Stylesheet + Print-Service.
*
* printService bereitet den Body auf den Druck vor (print-layout-Klasse,
* Print-Modus ohne UI-Chrome), printLayout() triggert window.print nach
* Vorbereitung und räumt danach ab. Die CSS-Datei muss einen @media print
* Block mit den Kernregeln (UI ausblenden, Canvas füllen, @page)
* enthalten verifiziert als Datei-Assertion (jsdom kann kein print-CSS
* rendern).
*/
import { describe, it, expect, vi, afterEach } from 'vitest';
import { readFileSync } from 'fs';
import { join } from 'path';
import { preparePrintLayout, cleanupPrintLayout, printLayout } from '../src/services/printService';
describe('G5: printService', () => {
afterEach(() => {
cleanupPrintLayout();
});
it('preparePrintLayout setzt print-layout-Klasse am body', () => {
document.body.classList.remove('print-layout');
preparePrintLayout();
expect(document.body.classList.contains('print-layout')).toBe(true);
});
it('cleanupPrintLayout entfernt die Klasse wieder', () => {
preparePrintLayout();
cleanupPrintLayout();
expect(document.body.classList.contains('print-layout')).toBe(false);
});
it('printLayout ruft window.print nach Vorbereitung und räumt ab (async)', async () => {
const printSpy = vi.spyOn(window, 'print').mockImplementation(() => {});
await printLayout();
expect(printSpy).toHaveBeenCalledTimes(1);
// nach dem Druck ist wieder aufgeräumt
expect(document.body.classList.contains('print-layout')).toBe(false);
printSpy.mockRestore();
});
});
describe('G5: Print-Stylesheet-Regeln (Datei-Assertion)', () => {
const css = readFileSync(join(__dirname, '..', 'src', 'styles.css'), 'utf-8');
const printBlock = css.slice(css.indexOf('@media print'));
it('enthält einen @media print Block mit @page A4 landscape', () => {
expect(css).toContain('@media print');
expect(printBlock).toMatch(/@page\s*\{[^}]*size:\s*A4 landscape/);
});
it('blendet UI-Chrome im Druck aus (screen-only Regel)', () => {
expect(printBlock).toContain('.screen-only');
expect(printBlock).toMatch(/\.screen-only\s*\{[^}]*display:\s*none/);
});
it('Canvas/Druckbereich füllt die Seite (print-area Regel)', () => {
expect(printBlock).toContain('.print-area');
expect(printBlock).toMatch(/\.print-area\s*\{[^}]*width:\s*100%/);
});
it('print-layout-Modus erzwingt Farbdruck-Hintergründe (print-color-adjust)', () => {
expect(printBlock).toMatch(/-webkit-print-color-adjust:\s*exact/);
});
});