task(E7): event elements polish - curtain (wavy polyline), spotlight (cone polygon with angle/color), barrier (chain circles), stage height attribute

This commit is contained in:
Agent Zero
2026-08-29 02:36:20 +02:00
parent f4dbb8caaa
commit 9176d7c9a4
5 changed files with 313 additions and 4 deletions
@@ -280,15 +280,156 @@ export const arcRowTool: ToolExtensionV2 = {
};
/** V2-Plugin: Event-Domain (Tasks A4.9 + E1). */
// ─────────────────────────────────────────────────────────────
// Task E7: Event-Elemente-Polish — curtain, spotlight, barrier
// ─────────────────────────────────────────────────────────────
/** Vorhang: wellige Polyline (Standardbreite 400) über der Bühne. */
export const curtainTool: ToolExtensionV2 = {
manifest: {
id: 'curtain',
label: 'Vorhang',
icon: '╭',
ribbonTab: 'insert',
tags: ['basic'],
description: 'Vorhang entlang der Bühnen-Oberkante platzieren',
},
optionsSchema: [
{ key: 'stageHeight', label: 'Höhe (cm)', type: 'number' },
],
handlers: {
down(e, ctx) {
const c = ctx as FullToolContext;
if (!c.doc) return;
const { layerId } = optsOf(ctx);
const stageHeight = (ctx.options.stageHeight as number | undefined) ?? 120;
const width = 400;
// Wellenform: 24 Punkte, Sinus gedämpft an den Enden
const pts: Pt[] = [];
const SEG = 24;
for (let i = 0; i <= SEG; i++) {
const t = i / SEG;
const x = e.world.x - width / 2 + width * t;
const y = e.world.y + Math.sin(t * Math.PI) * (stageHeight / 4);
pts.push({ x, y });
}
const el: CADElement = {
id: `curtain-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
type: 'curtain',
layerId,
x: e.world.x,
y: e.world.y,
width,
height: stageHeight,
properties: { points: pts, stageHeight, stroke: '#7a4a8f', strokeWidth: 2 },
};
c.doc.transact(() => { c.doc!.addElement(el); });
ctx.setStatus(`Vorhang platziert (${width} breit, ${stageHeight} cm hoch)`);
},
},
};
/** Scheinwerfer: Lichtkegel in angle-Richtung mit Farbe. */
export const spotlightTool: ToolExtensionV2 = {
manifest: {
id: 'spotlight',
label: 'Scheinwerfer',
icon: '▶',
ribbonTab: 'insert',
tags: ['basic'],
description: 'Scheinwerfer-Kegel platzieren (Winkel + Farbe aus Optionen)',
},
optionsSchema: [
{ key: 'angle', label: 'Winkel (°)', type: 'number' },
{ key: 'color', label: 'Lichtfarbe', type: 'color' },
],
handlers: {
down(e, ctx) {
const c = ctx as FullToolContext;
if (!c.doc) return;
const { layerId } = optsOf(ctx);
const angle = (ctx.options.angle as number | undefined) ?? 0;
const color = (ctx.options.color as string | undefined) ?? '#ffcc00';
const length = 300;
const halfAngle = 20;
const rad = (angle * Math.PI) / 180;
const radHalf = (halfAngle * Math.PI) / 180;
const p1 = { x: e.world.x + length * Math.cos(rad - radHalf), y: e.world.y + length * Math.sin(rad - radHalf) };
const p2 = { x: e.world.x + length * Math.cos(rad + radHalf), y: e.world.y + length * Math.sin(rad + radHalf) };
const pts: Pt[] = [
{ x: e.world.x, y: e.world.y },
p1,
p2,
{ x: e.world.x, y: e.world.y },
];
const el: CADElement = {
id: `spotlight-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
type: 'spotlight',
layerId,
x: e.world.x,
y: e.world.y,
width: length,
height: length,
properties: { points: pts, angle, color },
};
c.doc.transact(() => { c.doc!.addElement(el); });
ctx.setStatus(`Scheinwerfer platziert (${angle}°, ${color})`);
},
},
};
/** Absperrung: Kette aus Kreis-Paaren entlang Standardlänge 200. */
export const barrierTool: ToolExtensionV2 = {
manifest: {
id: 'barrier',
label: 'Absperrung',
icon: '⛴',
ribbonTab: 'insert',
tags: ['basic'],
description: 'Absperr-Kette (Kreis-Paare) platzieren',
},
optionsSchema: [
{ key: 'length', label: 'Länge', type: 'number' },
],
handlers: {
down(e, ctx) {
const c = ctx as FullToolContext;
if (!c.doc) return;
const { layerId } = optsOf(ctx);
const length = (ctx.options.length as number | undefined) ?? 200;
const circles: Array<{ x: number; y: number; r: number }> = [];
const pairs = 6;
for (let i = 0; i < pairs; i++) {
const t = (i + 0.5) / pairs;
const cx = e.world.x - length / 2 + length * t;
circles.push({ x: cx, y: e.world.y - 10, r: 8 });
circles.push({ x: cx, y: e.world.y + 10, r: 8 });
}
const el: CADElement = {
id: `barrier-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
type: 'barrier',
layerId,
x: e.world.x,
y: e.world.y,
width: length,
height: 20,
properties: { circles, color: '#c0392b' },
};
c.doc.transact(() => { c.doc!.addElement(el); });
ctx.setStatus(`Absperrung platziert (${length} lang, ${pairs} Kettenglieder)`);
},
},
};
export const eventToolsV2Plugin: PluginV2 = {
manifest: {
id: 'event-tools',
name: 'Event-Bestuhlung',
version: '2.1.0',
version: '2.2.0',
author: 'web-cad team',
description: 'Stühle, Reihen (Drag), Blöcke (Drag), Tische, Bühne (V2).',
description: 'Stühle, Reihen (Drag), Blöcke (Drag), Tische, Bühne, Vorhang, Scheinwerfer, Absperrung (V2).',
category: 'elements',
enabledByDefault: true,
},
tools: [chairTool, seatingRowToolDrag, seatingBlockToolDrag, tableTool, stageTool, arcRowTool, kinoTemplateTool, banquetTemplateTool, standingTableTemplateTool, podiumTemplateTool],
tools: [chairTool, seatingRowToolDrag, seatingBlockToolDrag, tableTool, stageTool, arcRowTool, kinoTemplateTool, banquetTemplateTool, standingTableTemplateTool, podiumTemplateTool, curtainTool, spotlightTool, barrierTool],
};
@@ -236,6 +236,66 @@ export function elementToPrimitives(el: CADElement): ShapePrimitive[] {
}];
}
case 'curtain': {
// Vorhang (Task E7): wellige Polyline entlang der Breite.
const pts = Array.isArray(props.points)
? (props.points as unknown[]).map(asPoint)
: [];
if (pts.length >= 2) {
return [{ kind: 'polyline', points: pts, close: false, stroke, strokeWidth: sw }];
}
return [{
kind: 'line',
from: { x: el.x - el.width / 2, y: el.y },
to: { x: el.x + el.width / 2, y: el.y },
stroke,
strokeWidth: sw,
}];
}
case 'spotlight': {
// Scheinwerfer (Task E7): Kegel-Polygon in properties.color.
const pts = Array.isArray(props.points)
? (props.points as unknown[]).map(asPoint)
: [];
if (pts.length >= 3) {
const col = (props.color as string | undefined) ?? '#ffcc00';
return [{ kind: 'polyline', points: pts, close: true, stroke: col, strokeWidth: sw }];
}
return [];
}
case 'barrier': {
// Absperrung (Task E7): Kette aus Kreis-Paaren (properties.circles).
const circles = Array.isArray(props.circles)
? (props.circles as Array<{ x: number; y: number; r: number }>)
: [];
const col = (props.color as string | undefined) ?? '#c0392b';
if (circles.length > 0) {
return circles.map((c) => ({
kind: 'circle' as const,
center: { x: c.x, y: c.y },
radius: c.r,
fill: null,
stroke: col,
strokeWidth: 1,
}));
}
const out: ShapePrimitive[] = [];
const N = 5;
for (let i = 0; i < N; i++) {
out.push({
kind: 'circle',
center: { x: el.x - el.width / 2 + (el.width * (i + 0.5)) / N, y: el.y },
radius: Math.min(6, el.width / (N * 2.5)),
fill: null,
stroke: col,
strokeWidth: 1,
});
}
return out;
}
default:
return [];
}
+1
View File
@@ -359,6 +359,7 @@ export class SeatingService {
stroke: '#1a2e3f',
strokeWidth: 2,
label: cfg.label,
height: cfg.height, // Bühnenhöhe in cm (Task E7: stage height attr)
},
};
}
+2 -1
View File
@@ -6,7 +6,8 @@ export type ElementType =
| 'line' | 'circle' | 'arc' | 'rect' | 'polygon'
| 'polyline' | 'text' | 'dimension' | 'block_instance' | 'chair'
| 'seating-row' | 'seating-block' | 'table' | 'stage'
| 'leader' | 'revcloud' | 'image';
| 'leader' | 'revcloud' | 'image'
| 'curtain' | 'spotlight' | 'barrier';
export type LineType = 'solid' | 'dashed' | 'dotted';