task(CAD-14): phase 4a - tool options store + panel (optionsSchema-driven UI in right sidebar, dispatcher getOptions wired, registry defaults seeded) - truss options length/chordCount/angle now actually reach the tools
This commit is contained in:
@@ -1732,6 +1732,7 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
<RightSidebar
|
<RightSidebar
|
||||||
activePanel={activeRightPanel}
|
activePanel={activeRightPanel}
|
||||||
onPanelChange={setActiveRightPanel}
|
onPanelChange={setActiveRightPanel}
|
||||||
|
activeToolId={activeTool}
|
||||||
selectedElement={selectedElement}
|
selectedElement={selectedElement}
|
||||||
layers={layers}
|
layers={layers}
|
||||||
blocks={blocks}
|
blocks={blocks}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { setActiveDocument, getActiveDocument } from '../kernel/document/documen
|
|||||||
import { V2_TOOLS_ENABLED, USE_PIXI_RENDERER } from '../kernel/featureFlags';
|
import { V2_TOOLS_ENABLED, USE_PIXI_RENDERER } from '../kernel/featureFlags';
|
||||||
import { PixiRenderer } from '../render/pixi/PixiRenderer';
|
import { PixiRenderer } from '../render/pixi/PixiRenderer';
|
||||||
import { pluginRegistry } from '../plugins';
|
import { pluginRegistry } from '../plugins';
|
||||||
|
import { getToolOptions, setToolOptionsDefaults, defaultsFromSchema } from '../services/toolOptionsService';
|
||||||
|
|
||||||
const CanvasArea: React.FC<CanvasAreaProps> = ({
|
const CanvasArea: React.FC<CanvasAreaProps> = ({
|
||||||
cursorPos, viewMode, onViewChange, gridEnabled, orthoEnabled, snapEnabled,
|
cursorPos, viewMode, onViewChange, gridEnabled, orthoEnabled, snapEnabled,
|
||||||
@@ -169,6 +170,9 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
|||||||
},
|
},
|
||||||
setPreview: (el) => renderEngine.setPreviewElement(el as CADElement | null),
|
setPreview: (el) => renderEngine.setPreviewElement(el as CADElement | null),
|
||||||
getDoc: () => v2Doc,
|
getDoc: () => v2Doc,
|
||||||
|
// CAD-14 Phase 4a: Werkzeug-Optionen aus dem zentralen Store
|
||||||
|
// (Laenge/Breite/chordCount/Winkel... erreichen die V2-Werkzeuge).
|
||||||
|
getOptions: (toolId: string) => getToolOptions(toolId),
|
||||||
getSelectionBridge: () => ({
|
getSelectionBridge: () => ({
|
||||||
getIds: () => Array.from(selectionEngine.getSelectedIds()),
|
getIds: () => Array.from(selectionEngine.getSelectedIds()),
|
||||||
setIds: (ids) => selectionEngine.selectByIds(ids, false),
|
setIds: (ids) => selectionEngine.selectByIds(ids, false),
|
||||||
@@ -223,6 +227,14 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
|||||||
pixi!.setSelection({ selectedIds: ids, elements: selected });
|
pixi!.setSelection({ selectedIds: ids, elements: selected });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// CAD-14 Phase 4a: Defaults aus den optionsSchemas aller V2-Werkzeuge
|
||||||
|
// in den toolOptionsStore fuellen (Quelle fuer OptionsPanel + Dispatcher).
|
||||||
|
for (const tool of pluginRegistry.getToolsV2()) {
|
||||||
|
if (tool.optionsSchema?.length) {
|
||||||
|
setToolOptionsDefaults(tool.manifest.id, defaultsFromSchema(tool.optionsSchema as never));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.info('InteractionDispatcher v2 aktiviert (VITE_TOOL_DISPATCHER=v2)');
|
logger.info('InteractionDispatcher v2 aktiviert (VITE_TOOL_DISPATCHER=v2)');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { useCallback } from 'react';
|
|||||||
import type { RightSidebarProps, RightPanel } from '../types/ui.types';
|
import type { RightSidebarProps, RightPanel } from '../types/ui.types';
|
||||||
import type { LibraryProviderExtension } from '../plugins/types';
|
import type { LibraryProviderExtension } from '../plugins/types';
|
||||||
import PropertiesPanel from './PropertiesPanel';
|
import PropertiesPanel from './PropertiesPanel';
|
||||||
|
import ToolOptionsPanel from './ToolOptionsPanel';
|
||||||
import LayerPanel from './LayerPanel';
|
import LayerPanel from './LayerPanel';
|
||||||
import BlockLibrary from './BlockLibrary';
|
import BlockLibrary from './BlockLibrary';
|
||||||
import BlockLibraryTree from './BlockLibraryTree';
|
import BlockLibraryTree from './BlockLibraryTree';
|
||||||
@@ -23,6 +24,7 @@ const tabs: Array<{ id: RightPanel; label: string; svg: React.ReactNode; badge?:
|
|||||||
|
|
||||||
const RightSidebar: React.FC<RightSidebarProps> = ({
|
const RightSidebar: React.FC<RightSidebarProps> = ({
|
||||||
activePanel, onPanelChange, selectedElement, layers, blocks,
|
activePanel, onPanelChange, selectedElement, layers, blocks,
|
||||||
|
activeToolId,
|
||||||
className,
|
className,
|
||||||
activeLayerId, onSelectLayer, onAddLayer, onToggleLayer,
|
activeLayerId, onSelectLayer, onAddLayer, onToggleLayer,
|
||||||
onDeleteLayer, onRenameLayer, onDuplicateLayer, onToggleLock,
|
onDeleteLayer, onRenameLayer, onDuplicateLayer, onToggleLock,
|
||||||
@@ -116,6 +118,7 @@ const RightSidebar: React.FC<RightSidebarProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
<div className="rightbar-content">
|
<div className="rightbar-content">
|
||||||
<div className={`rightbar-panel${activePanel === 'tool' ? ' active' : ''}`} data-panel-content="tool">
|
<div className={`rightbar-panel${activePanel === 'tool' ? ' active' : ''}`} data-panel-content="tool">
|
||||||
|
<ToolOptionsPanel activeToolId={activeToolId} />
|
||||||
{activePanel === 'tool' && selectedElement && onUpdateElement && (
|
{activePanel === 'tool' && selectedElement && onUpdateElement && (
|
||||||
<PropertiesPanel selectedElement={selectedElement} layers={layers} unit={unit} scaleFactor={scaleFactor} onDelete={selectedElement && onElementsDeleted ? () => onElementsDeleted([selectedElement.id]) : undefined} onUpdateProperty={(key, value) => {
|
<PropertiesPanel selectedElement={selectedElement} layers={layers} unit={unit} scaleFactor={scaleFactor} onDelete={selectedElement && onElementsDeleted ? () => onElementsDeleted([selectedElement.id]) : undefined} onUpdateProperty={(key, value) => {
|
||||||
if (!selectedElement || !onUpdateElement) return;
|
if (!selectedElement || !onUpdateElement) return;
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* CAD-14 Phase 4a - Werkzeug-Optionen-Panel.
|
||||||
|
*
|
||||||
|
* Rendert das optionsSchema des aktiven V2-Werkzeugs und bindet
|
||||||
|
* die Werte an den toolOptionsService (Quelle fuer den Dispatcher).
|
||||||
|
*/
|
||||||
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
|
import { pluginRegistry } from '../plugins';
|
||||||
|
import { getToolOption, setToolOption, subscribeToolOptions } from '../services/toolOptionsService';
|
||||||
|
|
||||||
|
interface ToolOptionsPanelProps {
|
||||||
|
activeToolId: string | null | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ToolOptionsPanel: React.FC<ToolOptionsPanelProps> = ({ activeToolId }) => {
|
||||||
|
const [, forceRender] = useState(0);
|
||||||
|
|
||||||
|
// Store-Abonnement: Panel bei Optionsaenderung neu rendern
|
||||||
|
React.useEffect(() => {
|
||||||
|
return subscribeToolOptions(() => forceRender((n) => n + 1));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const tool = useMemo(() => {
|
||||||
|
if (!activeToolId) return null;
|
||||||
|
return pluginRegistry.getToolV2(activeToolId) ?? null;
|
||||||
|
}, [activeToolId]);
|
||||||
|
|
||||||
|
const schema = tool?.optionsSchema ?? [];
|
||||||
|
if (!tool || schema.length === 0) return null;
|
||||||
|
|
||||||
|
const renderField = (field: { key: string; label: string; type: string; min?: number; max?: number; options?: Array<{ value: string; label: string }> }) => {
|
||||||
|
const value = getToolOption(tool.manifest.id, field.key);
|
||||||
|
const onChange = (v: unknown) => setToolOption(tool.manifest.id, field.key, v);
|
||||||
|
if (field.type === 'number') {
|
||||||
|
return (
|
||||||
|
<label key={field.key} className="toolopt-row">
|
||||||
|
<span>{field.label}</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={typeof value === 'number' ? value : Number(value) || 0}
|
||||||
|
min={field.min}
|
||||||
|
onChange={(e) => onChange(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (field.type === 'color') {
|
||||||
|
return (
|
||||||
|
<label key={field.key} className="toolopt-row">
|
||||||
|
<span>{field.label}</span>
|
||||||
|
<input type="color" value={String(value ?? '#9aa0a6')} onChange={(e) => onChange(e.target.value)} />
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (field.type === 'select' && field.options) {
|
||||||
|
return (
|
||||||
|
<label key={field.key} className="toolopt-row">
|
||||||
|
<span>{field.label}</span>
|
||||||
|
<select value={String(value ?? '')} onChange={(e) => onChange(e.target.value)}>
|
||||||
|
{field.options.map((o) => (
|
||||||
|
<option key={o.value} value={o.value}>{o.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (field.type === 'boolean') {
|
||||||
|
return (
|
||||||
|
<label key={field.key} className="toolopt-row toolopt-check">
|
||||||
|
<input type="checkbox" checked={value === true} onChange={(e) => onChange(e.target.checked)} />
|
||||||
|
<span>{field.label}</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tool-options-panel">
|
||||||
|
<div className="toolopt-title">Werkzeug-Optionen: {tool.manifest.label}</div>
|
||||||
|
{schema.map((f) => renderField(f as never))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ToolOptionsPanel;
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* CAD-14 Phase 4a - toolOptionsService.
|
||||||
|
*
|
||||||
|
* Zentraler Store fuer Werkzeug-Optionen (Laenge, Breite, Gurtrohre,
|
||||||
|
* Winkel, Durchmesser...). Der InteractionDispatcher liest darueber
|
||||||
|
* getOptions; die UI (OptionsPanel) schreibt hier hinein. Defaults
|
||||||
|
* stammen aus dem optionsSchema der V2-Werkzeuge.
|
||||||
|
*/
|
||||||
|
|
||||||
|
type Listener = () => void;
|
||||||
|
|
||||||
|
/** Per-Tool gesetzte Werte (override Defaults). */
|
||||||
|
const values = new Map<string, Record<string, unknown>>();
|
||||||
|
/** Per-Tool Defaults (aus optionsSchema gefuellt von der Registry/UI). */
|
||||||
|
const defaults = new Map<string, Record<string, unknown>>();
|
||||||
|
/** Abonnenten (UI-Update bei Aenderung). */
|
||||||
|
const listeners = new Set<Listener>();
|
||||||
|
|
||||||
|
function notify(): void {
|
||||||
|
for (const l of Array.from(listeners)) l();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leitet sinnvolle Defaults aus einem optionsSchema ab:
|
||||||
|
* number -> default ?? min (geclamped), color -> '#9aa0a6', sonst undefined.
|
||||||
|
*/
|
||||||
|
export function defaultsFromSchema(schema: Array<{ key: string; type: string; default?: unknown; min?: number }>): Record<string, unknown> {
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
for (const f of schema) {
|
||||||
|
if (f.default !== undefined) { out[f.key] = f.default; continue; }
|
||||||
|
if (f.type === 'number') {
|
||||||
|
out[f.key] = typeof f.min === 'number' ? Math.max(f.min, f.min === 0 ? 0 : Math.round(f.min)) : 0;
|
||||||
|
} else if (f.type === 'color') {
|
||||||
|
out[f.key] = '#9aa0a6';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Registriert Defaults fuer ein Werkzeug (aus optionsSchema). */
|
||||||
|
export function setToolOptionsDefaults(toolId: string, defs: Record<string, unknown>): void {
|
||||||
|
defaults.set(toolId, { ...(defaults.get(toolId) ?? {}), ...defs });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getToolOption<T = unknown>(toolId: string, key: string, fallback?: T): T {
|
||||||
|
const v = values.get(toolId)?.[key];
|
||||||
|
if (v !== undefined) return v as T;
|
||||||
|
const d = defaults.get(toolId)?.[key];
|
||||||
|
if (d !== undefined) return d as T;
|
||||||
|
return fallback as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setToolOption(toolId: string, key: string, value: unknown): void {
|
||||||
|
const cur = values.get(toolId) ?? {};
|
||||||
|
cur[key] = value;
|
||||||
|
values.set(toolId, cur);
|
||||||
|
notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getToolOptions(toolId: string): Record<string, unknown> {
|
||||||
|
return { ...(defaults.get(toolId) ?? {}), ...(values.get(toolId) ?? {}) };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function subscribeToolOptions(fn: Listener): () => void {
|
||||||
|
listeners.add(fn);
|
||||||
|
return () => { listeners.delete(fn); };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetToolOptions(): void {
|
||||||
|
values.clear();
|
||||||
|
defaults.clear();
|
||||||
|
}
|
||||||
@@ -3125,3 +3125,15 @@ body.drawer-open { overflow: hidden; }
|
|||||||
.ribbon-v2-btn.active .ribbon-v2-label {
|
.ribbon-v2-btn.active .ribbon-v2-label {
|
||||||
color: #4a90d9;
|
color: #4a90d9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* CAD-14 Phase 4a: Werkzeug-Optionen */
|
||||||
|
.tool-options-panel { padding: 8px 4px; }
|
||||||
|
.toolopt-row {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
gap: 8px; padding: 4px 0; font-size: 12px; color: var(--color-text, #d0d0d0);
|
||||||
|
}
|
||||||
|
.toolopt-row input[type=number], .toolopt-row select {
|
||||||
|
width: 90px; padding: 3px 6px; background: rgba(255,255,255,0.06);
|
||||||
|
color: #d0d0d0; border: 1px solid rgba(255,255,255,0.15); border-radius: 4px;
|
||||||
|
}
|
||||||
|
.toolopt-row input[type=color] { width: 36px; height: 24px; border: none; background: transparent; cursor: pointer; }
|
||||||
|
|||||||
@@ -172,6 +172,8 @@ export interface RightSidebarProps {
|
|||||||
layers: CADLayer[];
|
layers: CADLayer[];
|
||||||
elements?: CADElement[];
|
elements?: CADElement[];
|
||||||
blocks: BlockDefinition[];
|
blocks: BlockDefinition[];
|
||||||
|
/** CAD-14 Phase 4a: Aktives V2-Werkzeug (fuer Werkzeug-Optionen-Panel). */
|
||||||
|
activeToolId?: string | null;
|
||||||
activeLayerId?: string;
|
activeLayerId?: string;
|
||||||
onSelectLayer?: (id: string) => void;
|
onSelectLayer?: (id: string) => void;
|
||||||
onAddLayer?: () => void;
|
onAddLayer?: () => void;
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* CAD-14 Phase 4a: toolOptionsService — zentraler Store fuer
|
||||||
|
* Werkzeug-Optionen (Laenge, Breite, chordCount, Winkel...).
|
||||||
|
* Quelle der Defaults: optionsSchema der V2-Werkzeuge.
|
||||||
|
* Der InteractionDispatcher liest darueber getOptions.
|
||||||
|
*/
|
||||||
|
import { describe, it, expect, beforeEach } from 'vitest';
|
||||||
|
import {
|
||||||
|
getToolOption,
|
||||||
|
setToolOption,
|
||||||
|
getToolOptions,
|
||||||
|
resetToolOptions,
|
||||||
|
subscribeToolOptions,
|
||||||
|
defaultsFromSchema,
|
||||||
|
setToolOptionsDefaults,
|
||||||
|
} from '../src/services/toolOptionsService';
|
||||||
|
|
||||||
|
describe('CAD-14 Phase 4a: toolOptionsService', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
resetToolOptions();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaultsFromSchema: leitet Defaults aus optionsSchema ab', () => {
|
||||||
|
const defaults = defaultsFromSchema([
|
||||||
|
{ key: 'length', label: 'Länge', type: 'number', min: 10, max: 1000 },
|
||||||
|
{ key: 'chordCount', label: 'Gurtrohre', type: 'number', min: 1, max: 4 },
|
||||||
|
] as never);
|
||||||
|
// number ohne expliziten default: min clamped auf sinnvollen Start
|
||||||
|
expect(defaults.length).toBeDefined();
|
||||||
|
expect(defaults.chordCount).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('set/get: Wert wird gespeichert und gelesen', () => {
|
||||||
|
setToolOption('truss', 'length', 175);
|
||||||
|
expect(getToolOption('truss', 'length')).toBe(175);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getToolOption ohne Set: default aus Schema (defaultsFromSchema)', () => {
|
||||||
|
setToolOptionsDefaults('truss', { length: 200, chordCount: 2, trussWidth: 29 });
|
||||||
|
expect(getToolOption('truss', 'length')).toBe(200);
|
||||||
|
expect(getToolOption('truss', 'chordCount')).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getToolOptions liefert Gesamtsatz (defaults + gesetzte Werte)', () => {
|
||||||
|
setToolOptionsDefaults('truss', { length: 200, trussWidth: 29 });
|
||||||
|
setToolOption('truss', 'length', 300);
|
||||||
|
const all = getToolOptions('truss');
|
||||||
|
expect(all.length).toBe(300);
|
||||||
|
expect(all.trussWidth).toBe(29);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('subscribe: Benachrichtigung bei Änderung (UI-Update)', () => {
|
||||||
|
let notified = 0;
|
||||||
|
const unsub = subscribeToolOptions(() => { notified++; });
|
||||||
|
setToolOption('truss', 'length', 250);
|
||||||
|
unsub();
|
||||||
|
setToolOption('truss', 'length', 260);
|
||||||
|
expect(notified).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resetToolOptions leert alles', () => {
|
||||||
|
setToolOptionsDefaults('truss', { length: 200 });
|
||||||
|
resetToolOptions();
|
||||||
|
expect(getToolOption('truss', 'length', 99)).toBe(99);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user