feat: initial commit web-cad-neu with docker-compose, frontend and backend
This commit is contained in:
@@ -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();
|
||||
@@ -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');
|
||||
},
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user