From 131a922390110323d4a7d7844ec6741d55d2b768 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Sat, 29 Aug 2026 02:57:59 +0200 Subject: [PATCH] task(H2): project templates - 6 branch presets (halle/openair/saal/messe/buero/garten) with grid/layers/plugins/library seed, replaceLayers --- frontend/src/kernel/document/CADDocument.ts | 16 +++ frontend/src/services/projectTemplates.ts | 136 ++++++++++++++++++++ frontend/tests/projectTemplates.test.ts | 85 ++++++++++++ 3 files changed, 237 insertions(+) create mode 100644 frontend/src/services/projectTemplates.ts create mode 100644 frontend/tests/projectTemplates.test.ts diff --git a/frontend/src/kernel/document/CADDocument.ts b/frontend/src/kernel/document/CADDocument.ts index 6833620..d04ba2c 100644 --- a/frontend/src/kernel/document/CADDocument.ts +++ b/frontend/src/kernel/document/CADDocument.ts @@ -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). */ diff --git a/frontend/src/services/projectTemplates.ts b/frontend/src/services/projectTemplates.ts new file mode 100644 index 0000000..1300fae --- /dev/null +++ b/frontend/src/services/projectTemplates.ts @@ -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); +} diff --git a/frontend/tests/projectTemplates.test.ts b/frontend/tests/projectTemplates.test.ts new file mode 100644 index 0000000..398fdc5 --- /dev/null +++ b/frontend/tests/projectTemplates.test.ts @@ -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); + }); +});