task(H2): project templates - 6 branch presets (halle/openair/saal/messe/buero/garten) with grid/layers/plugins/library seed, replaceLayers

This commit is contained in:
Agent Zero
2026-08-29 02:57:59 +02:00
parent 2b9a3bef16
commit 131a922390
3 changed files with 237 additions and 0 deletions
@@ -111,6 +111,22 @@ export class CADDocument {
this.ydoc.data.blocks.set(block.id, block); 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 ──────────────────────────────────────────── // ── Schreiben ────────────────────────────────────────────
/** Fügt ein Element hinzu (einzelner Aufruf = eigener Undo-Schritt). */ /** Fügt ein Element hinzu (einzelner Aufruf = eigener Undo-Schritt). */
+136
View File
@@ -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);
}
+85
View File
@@ -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);
});
});