Compare commits
2 Commits
2b9a3bef16
...
e3fde5012e
| Author | SHA1 | Date | |
|---|---|---|---|
| e3fde5012e | |||
| 131a922390 |
+4
-3
@@ -639,7 +639,7 @@ Kurz-Tasks (je gleiche Struktur: implementieren im passenden builtin-Paket + Wor
|
||||
|
||||
## Phase H — Modi & Branchenneutralität
|
||||
- [x] H1 simple/pro Mode (uiStore.mode, Ribbon-Filter tags basic, CommandLine hide, Settings persist) *(2026-08-29: uiModeService statt uiStore — zustand war nie installiert (Q2 nie umgesetzt), Muster folgt documentService/featureFlags-Stil; localStorage webcad.uiMode persistiert, Default pro; RibbonBar filtert simple auf tags basic, CommandLine pro-only, Topbar-Toggle 😊/🛠; Log H1)*
|
||||
- [ ] H2 Projekt-Templates (Halle/OpenAir/Saal/Messe/Buero/Garten): JSON Presets (grid unit, layers, enabled plugins, library folders seed)
|
||||
- [x] H2 Projekt-Templates (Halle/OpenAir/Saal/Messe/Buero/Garten): JSON Presets (grid unit, layers, enabled plugins, library folders seed) *(2026-08-29: projectTemplates-Service mit 6 Presets je gridSize/unit/4 Layer/enabledPlugins/libraryFolderSeed + applyProjectTemplate über CADDocument.replaceLayers; Log H2)*
|
||||
- [ ] H3 Rollen: library-admin (global write) vs user (own folders) — an bestehendes auth anbinden, Backend guard
|
||||
- [ ] H4 i18n Grundlage: strings.ts zentrale Maps de/en, UI-Texte ersetzen (mechanisch, Teilautomatisierbar)
|
||||
- [ ] H5 Flächenberechnung Polygon (m²) Info im PropertiesPanel + measure-area Tool
|
||||
@@ -667,6 +667,7 @@ Format: `YYYY-MM-DD | TASK-ID | was getan | Tests Ergebnis | commit | Notizen/of
|
||||
|
||||
| Datum | Task | Zusammenfassung | Tests | Commit | Notizen |
|
||||
|---|---|---|---|---|---|
|
||||
| 2026-08-29 | H2 | Projekt-Templates: projectTemplates.ts NEU — ProjectTemplate-Typ (id/name/description/gridSize/unit/layers/enabledPlugins/libraryFolderSeed) + 6 Branchen-Presets als TS-Konstanten (JSON-äquivalent, ohne Datei-IO direkt tree-shakebar): halle (Veranstaltungshalle, 100cm-Raster, Wände/Bestuhlung/Bühne/Technik, event-tools), openair (Zonen/Bestuhlung/Absperrung/Wege, event-tools), saal (Bestuhlung/Bühne/Catering/Deko, 50cm), messe (Standlayout/Theken/Medien/Strom, 10mm), büro (Wände/Möbel/Netzwerk/Beschriftung, 10mm), garten (Bepflanzung/Wege/Wasser/Beleuchtung, 100cm) — je 4 Layer mit eindeutigen tpl-*-IDs, enabledPlugins inkl. core-*/builtin-library; applyProjectTemplate überträgt Layer-Set ERSETZEND (Template = Startzustand) via neuem CADDocument.replaceLayers (transaktionsgebündelt: alle deletes+sets in EINER Transaktion = 1 Undo-Schritt); gridSize/unit/pluginSeed sind UI-Einstellungen, die der Aufrufer aus dem Template liest (Dokumentation im Service) | Frontend: 602/602 Tests (+6 projectTemplates.test.ts), tsc 0, Build EXIT 0 | 131a922 | H2 COMPLETE; nächster Task H3 (Rollen library-admin vs. user, Backend-Guard) |
|
||||
| 2026-08-29 | H1 | simple/pro Mode: (1) uiModeService NEU — getUIMode/setUIMode/toggleUIMode/subscribeUIMode + useUIMode-React-Hook; localStorage-Persistenz (webcad.uiMode, Default pro, ungültige Werte → pro-Fallback, try/catch für nicht verfügbares localStorage); BEWUSST kein zustand-Store (Q2-Entscheidung wurde nie umgesetzt, kein stores/-Verzeichnis vorhanden) — Service folgt dem etablierten documentService/featureFlags-Muster des Projekts; (2) RibbonBar: V2-Tool-Sektion filtert im simple-Modus auf manifest.tags includes basic (useUIMode-Hook, Live-Update bei Toggle); (3) CommandLine: useUIMode → simple rendert null (pro-only); (4) Topbar: Toggle-Button 😊(→simple)/🛠(→pro) in topbar-right; Test-Zwischenfälle: CommandLine-Root-Klasse heißt .cmdline nicht .command-line (Selektor fixiert), Topbar braucht Pflicht-Props (Test-Render angepasst) | Frontend: 596/596 Tests (+9 uiMode.test.tsx), tsc 0, Build EXIT 0 | 8774fea | H1 COMPLETE; nächster Task H2 (Projekt-Templates) |
|
||||
| 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) |
|
||||
@@ -738,5 +739,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)
|
||||
|
||||
---
|
||||
*Nächster offener Schritt: **H2** (Projekt-Templates Halle/OpenAir/Saal/Messe/Buero/Garten: JSON Presets mit grid unit, layers, enabled plugins, library folders seed). Danach: H3 Rollen, H4 i18n, H5 Flächen. Restlich: D1–D4/D9/D10-path/D11/D12, Phase I (Release).*
|
||||
*Status 2026-08-29: Phasen B+A+C+D+E+F+G komplett, H1 fertig (simple/pro Mode), BUG-1-Kollisionsklasse geschlossen. FE 596/596 Tests, BE 272/272, tsc/Build grün. Alle Commits auf Forgejo main.*
|
||||
*Nächster offener Schritt: **H3** (Rollen: library-admin global write vs. user own folders — an bestehendes auth anbinden, Backend guard). Danach: H4 i18n, H5 Flächen. Restlich: D1–D4/D9/D10-path/D11/D12, Phase I (Release).*
|
||||
*Status 2026-08-29: Phasen B+A+C+D+E+F+G komplett, H1+H2 fertig (simple/pro Mode + Projekt-Templates), BUG-1-Kollisionsklasse geschlossen. FE 602/602 Tests, BE 272/272, tsc/Build grün. Alle Commits auf Forgejo main.*
|
||||
|
||||
@@ -111,6 +111,22 @@ export class CADDocument {
|
||||
this.ydoc.data.blocks.set(block.id, block);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ersetzt das komplette Layer-Set (Task H2: Projekt-Templates).
|
||||
* Bestehende Layer werden entfernt, Template-Layer übernommen.
|
||||
*/
|
||||
replaceLayers(layers: CADLayer[]): void {
|
||||
this.transact(() => {
|
||||
for (const id of Array.from(this.ydoc.data.layers.keys())) {
|
||||
this.ydoc.data.layers.delete(id);
|
||||
}
|
||||
for (const l of layers) {
|
||||
this.ydoc.data.layers.set(l.id, l);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Schreiben ────────────────────────────────────────────
|
||||
|
||||
/** Fügt ein Element hinzu (einzelner Aufruf = eigener Undo-Schritt). */
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Task H2 – Projekt-Templates: Branchen-Presets als Startzustand.
|
||||
*
|
||||
* Jedes Preset definiert: gridSize + unit (Raster), Layer-Set,
|
||||
* enabledPlugins (welche V2-Plugins aktiviert werden) und einen
|
||||
* libraryFolderSeed (Startordner der Block-Bibliothek).
|
||||
*/
|
||||
import type { CADLayer } from '../types/cad.types';
|
||||
import type { CADDocument } from '../kernel/document/CADDocument';
|
||||
|
||||
export type ProjectTemplateId = 'halle' | 'openair' | 'saal' | 'messe' | 'buero' | 'garten';
|
||||
|
||||
export interface ProjectTemplate {
|
||||
id: ProjectTemplateId;
|
||||
name: string;
|
||||
description: string;
|
||||
gridSize: number;
|
||||
unit: 'mm' | 'cm' | 'm';
|
||||
layers: CADLayer[];
|
||||
enabledPlugins: string[];
|
||||
libraryFolderSeed: string;
|
||||
}
|
||||
|
||||
function layer(id: string, name: string, color: string, sortOrder: number): CADLayer {
|
||||
return {
|
||||
id, name, visible: true, locked: false,
|
||||
color, lineType: 'solid', transparency: 0, sortOrder, parentId: null,
|
||||
};
|
||||
}
|
||||
|
||||
export const PROJECT_TEMPLATES: ProjectTemplate[] = [
|
||||
{
|
||||
id: 'halle',
|
||||
name: 'Halle',
|
||||
description: 'Veranstaltungshalle: Bestuhlung, Bühne, Technik, Wege',
|
||||
gridSize: 100,
|
||||
unit: 'cm',
|
||||
layers: [
|
||||
layer('tpl-h-wand', 'Wände', '#8a8f98', 0),
|
||||
layer('tpl-h-bestuhlung', 'Bestuhlung', '#4a90d9', 1),
|
||||
layer('tpl-h-buehne', 'Bühne', '#c0392b', 2),
|
||||
layer('tpl-h-technik', 'Technik', '#f39c12', 3),
|
||||
],
|
||||
enabledPlugins: ['event-tools', 'core-drawing', 'core-modify', 'core-annotate', 'builtin-library'],
|
||||
libraryFolderSeed: 'Veranstaltungstechnik',
|
||||
},
|
||||
{
|
||||
id: 'openair',
|
||||
name: 'OpenAir',
|
||||
description: 'Open-Air-Gelände: Zonen, Bestuhlung, Absperrungen, Wege',
|
||||
gridSize: 100,
|
||||
unit: 'cm',
|
||||
layers: [
|
||||
layer('tpl-o-zonen', 'Zonen', '#27ae60', 0),
|
||||
layer('tpl-o-bestuhlung', 'Bestuhlung', '#4a90d9', 1),
|
||||
layer('tpl-o-absperrung', 'Absperrung', '#c0392b', 2),
|
||||
layer('tpl-o-wege', 'Wege', '#b8a888', 3),
|
||||
],
|
||||
enabledPlugins: ['event-tools', 'core-drawing', 'core-modify', 'core-annotate', 'builtin-library'],
|
||||
libraryFolderSeed: 'OpenAir',
|
||||
},
|
||||
{
|
||||
id: 'saal',
|
||||
name: 'Saal',
|
||||
description: 'Festsaal: Bestuhlung, Bühne, Catering, Deko',
|
||||
gridSize: 50,
|
||||
unit: 'cm',
|
||||
layers: [
|
||||
layer('tpl-s-bestuhlung', 'Bestuhlung', '#4a90d9', 0),
|
||||
layer('tpl-s-buehne', 'Bühne', '#c0392b', 1),
|
||||
layer('tpl-s-catering', 'Catering', '#f39c12', 2),
|
||||
layer('tpl-s-deko', 'Deko', '#9b59b6', 3),
|
||||
],
|
||||
enabledPlugins: ['event-tools', 'core-drawing', 'core-modify', 'core-annotate', 'builtin-library'],
|
||||
libraryFolderSeed: 'Eventservice',
|
||||
},
|
||||
{
|
||||
id: 'messe',
|
||||
name: 'Messe',
|
||||
description: 'Messestand: Standlayout, Theken, Medien, Strom',
|
||||
gridSize: 10,
|
||||
unit: 'mm',
|
||||
layers: [
|
||||
layer('tpl-m-stand', 'Standlayout', '#e74c3c', 0),
|
||||
layer('tpl-m-theken', 'Theken', '#f39c12', 1),
|
||||
layer('tpl-m-medien', 'Medien', '#3498db', 2),
|
||||
layer('tpl-m-strom', 'Strom', '#f1c40f', 3),
|
||||
],
|
||||
enabledPlugins: ['core-drawing', 'core-modify', 'core-annotate', 'builtin-library'],
|
||||
libraryFolderSeed: 'Messebau',
|
||||
},
|
||||
{
|
||||
id: 'buero',
|
||||
name: 'Büro',
|
||||
description: 'Büroplanung: Möbel, Wände, Netzwerk, Beschriftung',
|
||||
gridSize: 10,
|
||||
unit: 'mm',
|
||||
layers: [
|
||||
layer('tpl-b-waende', 'Wände', '#8a8f98', 0),
|
||||
layer('tpl-b-moebel', 'Möbel', '#4a90d9', 1),
|
||||
layer('tpl-b-netzwerk', 'Netzwerk', '#27ae60', 2),
|
||||
layer('tpl-b-text', 'Beschriftung', '#9aa0a6', 3),
|
||||
],
|
||||
enabledPlugins: ['core-drawing', 'core-modify', 'core-annotate', 'builtin-library'],
|
||||
libraryFolderSeed: 'Büroeinrichtung',
|
||||
},
|
||||
{
|
||||
id: 'garten',
|
||||
name: 'Garten',
|
||||
description: 'Gartenplanung: Bepflanzung, Wege, Wasser, Beleuchtung',
|
||||
gridSize: 100,
|
||||
unit: 'cm',
|
||||
layers: [
|
||||
layer('tpl-g-bepflanzung', 'Bepflanzung', '#27ae60', 0),
|
||||
layer('tpl-g-wege', 'Wege', '#b8a888', 1),
|
||||
layer('tpl-g-wasser', 'Wasser', '#3498db', 2),
|
||||
layer('tpl-g-licht', 'Beleuchtung', '#f1c40f', 3),
|
||||
],
|
||||
enabledPlugins: ['core-drawing', 'core-modify', 'core-annotate', 'builtin-library'],
|
||||
libraryFolderSeed: 'Gartenlandschaft',
|
||||
},
|
||||
];
|
||||
|
||||
/** Template per ID (oder undefined). */
|
||||
export function getProjectTemplate(id: string): ProjectTemplate | undefined {
|
||||
return PROJECT_TEMPLATES.find((t) => t.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Überträgt ein Template auf ein Dokument: Layer-Set wird ERSETZT
|
||||
* (Template = Startzustand). Grid/unit/pluginSeed sind UI-Einstellungen,
|
||||
* die der Aufrufer aus dem Template liest.
|
||||
*/
|
||||
export function applyProjectTemplate(doc: CADDocument, tpl: ProjectTemplate): void {
|
||||
doc.replaceLayers(tpl.layers);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Task H2 – Projekt-Templates: JSON-Presets für Branchen-Szenarien.
|
||||
*
|
||||
* Sechs Presets (halle/openair/saal/messe/buero/garten), jedes mit
|
||||
* gridSize/unit, Layer-Set, enabled plugins und library folder seed.
|
||||
* applyProjectTemplate überträgt die Layer in ein CADDocument
|
||||
* (bestehende Layer werden ersetzt — Template = Startzustand).
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { CADDocument } from '../src/kernel/document/CADDocument';
|
||||
import { createInMemoryDoc } from './helpers/inMemoryDoc';
|
||||
import {
|
||||
PROJECT_TEMPLATES,
|
||||
getProjectTemplate,
|
||||
applyProjectTemplate,
|
||||
type ProjectTemplate,
|
||||
} from '../src/services/projectTemplates';
|
||||
|
||||
const EXPECTED_IDS = ['halle', 'openair', 'saal', 'messe', 'buero', 'garten'];
|
||||
|
||||
describe('H2: Projekt-Templates', () => {
|
||||
it('enthält genau die 6 Branchen-Presets mit eindeutigen IDs', () => {
|
||||
expect(PROJECT_TEMPLATES.map((t) => t.id).sort()).toEqual([...EXPECTED_IDS].sort());
|
||||
expect(new Set(PROJECT_TEMPLATES.map((t) => t.id)).size).toBe(6);
|
||||
});
|
||||
|
||||
it('jedes Preset hat Name, gridSize, unit, Layer (>=2), enabledPlugins und libraryFolderSeed', () => {
|
||||
for (const tpl of PROJECT_TEMPLATES) {
|
||||
expect(tpl.name.length).toBeGreaterThan(2);
|
||||
expect(tpl.gridSize).toBeGreaterThan(0);
|
||||
expect(['mm', 'cm', 'm']).toContain(tpl.unit);
|
||||
expect(tpl.layers.length).toBeGreaterThanOrEqual(2);
|
||||
expect(tpl.enabledPlugins.length).toBeGreaterThan(0);
|
||||
expect(tpl.libraryFolderSeed.length).toBeGreaterThan(2);
|
||||
// Layer haben gültige CADLayer-Struktur mit eindeutigen IDs
|
||||
const layerIds = new Set(tpl.layers.map((l) => l.id));
|
||||
expect(layerIds.size).toBe(tpl.layers.length);
|
||||
expect(tpl.layers.every((l) => l.name.length > 0 && typeof l.visible === 'boolean')).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('Szenario-spezifika: halle/openair/saal aktivieren event-tools, garten landscape', () => {
|
||||
const halle = getProjectTemplate('halle')!;
|
||||
expect(halle.enabledPlugins).toContain('event-tools');
|
||||
const openair = getProjectTemplate('openair')!;
|
||||
expect(openair.enabledPlugins).toContain('event-tools');
|
||||
const saal = getProjectTemplate('saal')!;
|
||||
expect(saal.enabledPlugins).toContain('event-tools');
|
||||
const garten = getProjectTemplate('garten')!;
|
||||
expect(garten.libraryFolderSeed.toLowerCase()).toContain('garten');
|
||||
const buero = getProjectTemplate('buero')!;
|
||||
expect(buero.unit).toBe('mm');
|
||||
});
|
||||
|
||||
it('getProjectTemplate liefert undefined für unbekannte ID', () => {
|
||||
expect(getProjectTemplate('gibts-nicht')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('H2: applyProjectTemplate', () => {
|
||||
it('setzt die Template-Layer im CADDocument (ersetzt bestehende)', () => {
|
||||
const { ydoc } = createInMemoryDoc();
|
||||
const doc = new CADDocument(ydoc);
|
||||
// Bestehenden Layer simulieren
|
||||
ydoc.data.layers.set('alt', {
|
||||
id: 'alt', name: 'Alt', visible: true, locked: false,
|
||||
color: '#fff', lineType: 'solid', transparency: 0, sortOrder: 0, parentId: null,
|
||||
});
|
||||
const tpl = getProjectTemplate('halle')!;
|
||||
applyProjectTemplate(doc, tpl);
|
||||
const layers = doc.getAllLayers();
|
||||
expect(layers.map((l) => l.id)).toEqual(tpl.layers.map((l) => l.id));
|
||||
expect(layers.find((l) => l.id === 'alt')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('Template-Layer sind danach über CADDocument lesbar', () => {
|
||||
const { ydoc } = createInMemoryDoc();
|
||||
const doc = new CADDocument(ydoc);
|
||||
const tpl = getProjectTemplate('buero')!;
|
||||
applyProjectTemplate(doc, tpl);
|
||||
const first = doc.getLayer(tpl.layers[0].id);
|
||||
expect(first).toBeDefined();
|
||||
expect(first!.name).toBe(tpl.layers[0].name);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user