feat: initial commit web-cad-neu with docker-compose, frontend and backend

This commit is contained in:
2026-06-26 10:50:24 +02:00
commit 4ec76fe406
102 changed files with 25722 additions and 0 deletions
+170
View File
@@ -0,0 +1,170 @@
/**
* PluginRegistry Manages plugin registration, lifecycle, and extension lookups.
*/
import type {
Plugin,
PluginContext,
PluginState,
ElementTypeExtension,
ToolExtension,
CommandExtension,
ImportExportExtension,
} from './types';
class PluginRegistryClass {
private plugins = new Map<string, Plugin>();
private states = new Map<string, PluginState>();
private context: PluginContext | null = null;
private listeners = new Set<() => void>();
/** Set the plugin context (called once on app init) */
setContext(ctx: PluginContext) {
this.context = ctx;
}
/** Register a plugin */
register(plugin: Plugin) {
const { id } = plugin.manifest;
if (this.plugins.has(id)) {
console.warn(`[PluginRegistry] Plugin '${id}' already registered`);
return;
}
this.plugins.set(id, plugin);
this.states.set(id, {
manifest: plugin.manifest,
enabled: plugin.manifest.enabledByDefault ?? false,
loaded: false,
});
this.notify();
}
/** Enable and activate a plugin */
enable(pluginId: string) {
const plugin = this.plugins.get(pluginId);
const state = this.states.get(pluginId);
if (!plugin || !state || !this.context) return;
state.enabled = true;
if (!state.loaded) {
plugin.onInit?.(this.context);
state.loaded = true;
}
plugin.onActivate?.(this.context);
this.notify();
}
/** Disable and deactivate a plugin */
disable(pluginId: string) {
const plugin = this.plugins.get(pluginId);
const state = this.states.get(pluginId);
if (!plugin || !state) return;
state.enabled = false;
plugin.onDeactivate?.();
this.notify();
}
/** Toggle plugin enabled state */
toggle(pluginId: string) {
const state = this.states.get(pluginId);
if (!state) return;
if (state.enabled) {
this.disable(pluginId);
} else {
this.enable(pluginId);
}
}
/** Unregister a plugin */
unregister(pluginId: string) {
const plugin = this.plugins.get(pluginId);
if (plugin) {
plugin.onDestroy?.();
}
this.plugins.delete(pluginId);
this.states.delete(pluginId);
this.notify();
}
/** Get all plugin states */
getStates(): PluginState[] {
return Array.from(this.states.values());
}
/** Get a specific plugin */
getPlugin(pluginId: string): Plugin | undefined {
return this.plugins.get(pluginId);
}
/** Get all enabled plugins */
getEnabledPlugins(): Plugin[] {
const result: Plugin[] = [];
for (const [id, plugin] of this.plugins) {
const state = this.states.get(id);
if (state?.enabled) result.push(plugin);
}
return result;
}
/** Get all element type extensions from enabled plugins */
getElementTypeExtensions(): ElementTypeExtension[] {
const extensions: ElementTypeExtension[] = [];
for (const plugin of this.getEnabledPlugins()) {
if (plugin.elementTypes) extensions.push(...plugin.elementTypes);
}
return extensions;
}
/** Get all tool extensions from enabled plugins */
getToolExtensions(): ToolExtension[] {
const extensions: ToolExtension[] = [];
for (const plugin of this.getEnabledPlugins()) {
if (plugin.tools) extensions.push(...plugin.tools);
}
return extensions;
}
/** Get all command extensions from enabled plugins */
getCommandExtensions(): CommandExtension[] {
const extensions: CommandExtension[] = [];
for (const plugin of this.getEnabledPlugins()) {
if (plugin.commands) extensions.push(...plugin.commands);
}
return extensions;
}
/** Get all import/export extensions from enabled plugins */
getImportExportExtensions(): ImportExportExtension[] {
const extensions: ImportExportExtension[] = [];
for (const plugin of this.getEnabledPlugins()) {
if (plugin.importExport) extensions.push(...plugin.importExport);
}
return extensions;
}
/** Find an element type extension by type name */
getElementType(typeName: string): ElementTypeExtension | undefined {
return this.getElementTypeExtensions().find((e) => e.typeName === typeName);
}
/** Subscribe to state changes */
subscribe(listener: () => void): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
private notify() {
this.listeners.forEach((l) => l());
}
/** Initialize all plugins that are enabled by default */
initDefaults() {
for (const [id, plugin] of this.plugins) {
if (plugin.manifest.enabledByDefault) {
this.enable(id);
}
}
}
}
export const pluginRegistry = new PluginRegistryClass();
+221
View File
@@ -0,0 +1,221 @@
/**
* Event-Tools Plugin Built-in example plugin
* Adds custom element types: stage-curtain, spotlight, barrier
* Adds command: EVENT_SEATING (generates seating rows)
*/
import type { Plugin, PluginContext, ElementTypeExtension, CommandExtension } from '../types';
import type { CADElement } from '../../types/cad.types';
function uid(prefix: string): string {
return `${prefix}-${Date.now()}-${Math.floor(Math.random() * 10000)}`;
}
// ─── Element Type: Stage Curtain ────────────────────────
const stageCurtain: ElementTypeExtension = {
typeName: 'stage-curtain',
displayName: 'Bühnenvorhang',
defaultWidth: 6,
defaultHeight: 0.3,
defaultProperties: {
fill: '#8B0000',
stroke: '#5C0000',
strokeWidth: 2,
curtainStyle: 'pleated',
},
render(ctx, element, scale) {
const { x, y, width, height, properties } = element;
ctx.save();
ctx.fillStyle = (properties.fill as string) || '#8B0000';
ctx.strokeStyle = (properties.stroke as string) || '#5C0000';
ctx.lineWidth = (properties.strokeWidth as number) || 2;
// Draw pleated curtain
const pleats = Math.max(6, Math.floor(width / 0.5));
const pleatWidth = width / pleats;
ctx.beginPath();
ctx.moveTo(x, y);
for (let i = 0; i <= pleats; i++) {
const px = x + i * pleatWidth;
const py = y + (i % 2 === 0 ? 0 : height * 0.15);
ctx.lineTo(px, py);
}
ctx.lineTo(x + width, y + height);
ctx.lineTo(x, y + height);
ctx.closePath();
ctx.fill();
ctx.stroke();
ctx.restore();
return true;
},
hitTest(element, hx, hy, tolerance) {
const { x, y, width, height } = element;
return hx >= x - tolerance && hx <= x + width + tolerance &&
hy >= y - tolerance && hy <= y + height + tolerance;
},
propertyFields: [
{ key: 'fill', label: 'Farbe', type: 'color' },
{ key: 'curtainStyle', label: 'Stil', type: 'select', options: [
{ value: 'pleated', label: 'Gefaltet' },
{ value: 'flat', label: 'Glatt' },
]},
],
};
// ─── Element Type: Spotlight ────────────────────────────
const spotlight: ElementTypeExtension = {
typeName: 'spotlight',
displayName: 'Scheinwerfer',
defaultWidth: 2,
defaultHeight: 2,
defaultProperties: {
fill: 'rgba(255, 220, 100, 0.3)',
stroke: '#FFD700',
strokeWidth: 1.5,
beamAngle: 45,
},
render(ctx, element, scale) {
const { x, y, width, height, properties } = element;
ctx.save();
const cx = x + width / 2;
const cy = y + height / 2;
const radius = Math.max(width, height) / 2;
// Draw beam cone
const beamAngle = ((properties.beamAngle as number) || 45) * Math.PI / 180;
const gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius);
gradient.addColorStop(0, 'rgba(255, 220, 100, 0.5)');
gradient.addColorStop(1, 'rgba(255, 220, 100, 0.05)');
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.arc(cx, cy, radius, -Math.PI / 2 - beamAngle / 2, -Math.PI / 2 + beamAngle / 2);
ctx.closePath();
ctx.fill();
// Draw fixture circle
ctx.fillStyle = '#333';
ctx.strokeStyle = (properties.stroke as string) || '#FFD700';
ctx.lineWidth = (properties.strokeWidth as number) || 1.5;
ctx.beginPath();
ctx.arc(cx, cy, 0.15 * scale, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.restore();
return true;
},
hitTest(element, hx, hy, tolerance) {
const cx = element.x + element.width / 2;
const cy = element.y + element.height / 2;
const radius = Math.max(element.width, element.height) / 2 + tolerance;
const dx = hx - cx;
const dy = hy - cy;
return dx * dx + dy * dy <= radius * radius;
},
propertyFields: [
{ key: 'beamAngle', label: 'Strahlwinkel°', type: 'number', min: 10, max: 180, step: 5 },
{ key: 'stroke', label: 'Farbe', type: 'color' },
],
};
// ─── Element Type: Barrier ──────────────────────────────
const barrier: ElementTypeExtension = {
typeName: 'barrier',
displayName: 'Absperrung',
defaultWidth: 3,
defaultHeight: 0.1,
defaultProperties: {
fill: '#FFA500',
stroke: '#CC8400',
strokeWidth: 1.5,
pattern: 'striped',
},
render(ctx, element, scale) {
const { x, y, width, height, properties } = element;
ctx.save();
ctx.fillStyle = (properties.fill as string) || '#FFA500';
ctx.strokeStyle = (properties.stroke as string) || '#CC8400';
ctx.lineWidth = (properties.strokeWidth as number) || 1.5;
// Draw striped barrier
const stripeWidth = 0.3;
const stripes = Math.floor(width / stripeWidth);
for (let i = 0; i < stripes; i++) {
ctx.fillStyle = i % 2 === 0 ? '#FFA500' : '#000000';
ctx.fillRect(x + i * stripeWidth, y, stripeWidth, height);
}
ctx.strokeStyle = (properties.stroke as string) || '#CC8400';
ctx.strokeRect(x, y, width, height);
ctx.restore();
return true;
},
hitTest(element, hx, hy, tolerance) {
const { x, y, width, height } = element;
return hx >= x - tolerance && hx <= x + width + tolerance &&
hy >= y - tolerance && hy <= y + height + tolerance;
},
propertyFields: [
{ key: 'fill', label: 'Farbe', type: 'color' },
{ key: 'pattern', label: 'Muster', type: 'select', options: [
{ value: 'striped', label: 'Gestreift' },
{ value: 'solid', label: 'Einfarbig' },
]},
],
};
// ─── Command: EVENT_SEATING ─────────────────────────────
const eventSeatingCommand: CommandExtension = {
name: 'EVENT_SEATING',
description: 'Erzeugt Bestuhlung in Reihen',
usage: 'EVENT_SEATING <rows> <cols> [gap] [rowGap]',
execute(args, context) {
const rows = parseInt(args[0] || '5', 10);
const cols = parseInt(args[1] || '10', 10);
const gap = parseFloat(args[2] || '0.6');
const rowGap = parseFloat(args[3] || '1.0');
const layerId = context.getActiveLayerId();
const chairWidth = 0.5;
const chairHeight = 0.5;
const startX = 2;
const startY = 2;
let count = 0;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const el: CADElement = {
id: uid('chair'),
type: 'chair',
layerId,
x: startX + c * (chairWidth + gap),
y: startY + r * (chairHeight + rowGap),
width: chairWidth,
height: chairHeight,
properties: { fill: '#4A90D9', stroke: '#2A70B9', strokeWidth: 1, rotation: 0 },
};
context.addElement(el);
count++;
}
}
context.showToast(`${count} Stühle in ${rows} Reihen erstellt`, 'success');
},
};
// ─── Plugin Definition ──────────────────────────────────
export const eventToolsPlugin: Plugin = {
manifest: {
id: 'event-tools',
name: 'Event-Tools',
version: '1.0.0',
author: 'Web CAD Team',
description: 'Erweitert Web CAD um Bühnenvorhänge, Scheinwerfer, Absperrungen und Bestuhlungs-Befehle.',
category: 'elements',
enabledByDefault: true,
},
elementTypes: [stageCurtain, spotlight, barrier],
commands: [eventSeatingCommand],
onInit(context) {
context.log('Event-Tools Plugin initialisiert');
},
onActivate(context) {
context.log('Event-Tools Plugin aktiviert');
},
};
+26
View File
@@ -0,0 +1,26 @@
/**
* Plugin System Public API
*/
export { pluginRegistry } from './PluginRegistry';
export type {
Plugin,
PluginManifest,
PluginContext,
PluginState,
ElementTypeExtension,
ToolExtension,
CommandExtension,
ImportExportExtension,
PropertyField,
} from './types';
// Built-in plugins
export { eventToolsPlugin } from './builtin/eventTools';
import { pluginRegistry } from './PluginRegistry';
import { eventToolsPlugin } from './builtin/eventTools';
/** Register all built-in plugins */
export function registerBuiltinPlugins() {
pluginRegistry.register(eventToolsPlugin);
}
+118
View File
@@ -0,0 +1,118 @@
/**
* Plugin System Types Manifest, Extension Points, Lifecycle
*/
import type { CADElement, CADLayer } from '../types/cad.types';
// ─── Plugin Manifest ────────────────────────────────────
export interface PluginManifest {
id: string;
name: string;
version: string;
author: string;
description: string;
icon?: string;
category: 'tools' | 'elements' | 'import-export' | 'theme' | 'other';
enabledByDefault?: boolean;
}
// ─── Extension Points ───────────────────────────────────
/** Custom element type with renderer */
export interface ElementTypeExtension {
typeName: string;
displayName: string;
icon?: string;
defaultWidth: number;
defaultHeight: number;
defaultProperties: Record<string, unknown>;
/** Render element on canvas context. Return true if handled. */
render?: (ctx: CanvasRenderingContext2D, element: CADElement, scale: number) => boolean;
/** Optional hit-test for selection */
hitTest?: (element: CADElement, x: number, y: number, tolerance: number) => boolean;
/** Optional property panel fields */
propertyFields?: PropertyField[];
}
/** Custom tool that appears in ribbon bar */
export interface ToolExtension {
id: string;
label: string;
icon: string;
ribbonTab: string;
tooltip?: string;
shortcut?: string;
onActivate: (context: PluginContext) => void;
}
/** Property panel field definition */
export interface PropertyField {
key: string;
label: string;
type: 'text' | 'number' | 'color' | 'select' | 'checkbox';
options?: Array<{ value: string; label: string }>;
min?: number;
max?: number;
step?: number;
}
/** Custom command-line command */
export interface CommandExtension {
name: string;
description: string;
usage: string;
execute: (args: string[], context: PluginContext) => void;
}
/** Custom import/export format */
export interface ImportExportExtension {
format: string;
extension: string;
label: string;
import?: (data: string, context: PluginContext) => CADElement[];
export?: (elements: CADElement[], layers: CADLayer[], context: PluginContext) => string;
}
// ─── Plugin Context (API for plugins) ───────────────────
export interface PluginContext {
/** Add element to current drawing */
addElement: (element: CADElement) => void;
/** Remove element by ID */
removeElement: (id: string) => void;
/** Update element properties */
updateElement: (id: string, properties: Partial<CADElement>) => void;
/** Get all elements */
getElements: () => CADElement[];
/** Get all layers */
getLayers: () => CADLayer[];
/** Get active layer ID */
getActiveLayerId: () => string;
/** Show status message */
showToast: (message: string, type?: 'info' | 'success' | 'warning' | 'error') => void;
/** Log to console with plugin prefix */
log: (message: string) => void;
}
// ─── Plugin Interface ───────────────────────────────────
export interface Plugin {
manifest: PluginManifest;
/** Called when plugin is loaded */
onInit?: (context: PluginContext) => void;
/** Called when plugin is activated */
onActivate?: (context: PluginContext) => void;
/** Called when plugin is deactivated */
onDeactivate?: () => void;
/** Called on plugin unload */
onDestroy?: () => void;
/** Extension points */
elementTypes?: ElementTypeExtension[];
tools?: ToolExtension[];
commands?: CommandExtension[];
importExport?: ImportExportExtension[];
}
// ─── Plugin State ───────────────────────────────────────
export interface PluginState {
manifest: PluginManifest;
enabled: boolean;
loaded: boolean;
}