feat: layer-tree selection sync + group folders in tree + ribbonbar readability
This commit is contained in:
@@ -1088,6 +1088,12 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
|
|
||||||
// Layer handlers
|
// Layer handlers
|
||||||
const handleSelectLayer = useCallback((id: string) => setActiveLayerId(id), []);
|
const handleSelectLayer = useCallback((id: string) => setActiveLayerId(id), []);
|
||||||
|
|
||||||
|
const handleSelectElement = useCallback((id: string) => {
|
||||||
|
setSelectedElementIds([id]);
|
||||||
|
const el = elements.find(e => e.id === id);
|
||||||
|
setSelectedElement(el ?? null);
|
||||||
|
}, [elements]);
|
||||||
const handleAddLayer = useCallback(() => {
|
const handleAddLayer = useCallback(() => {
|
||||||
setLayers((prev) => {
|
setLayers((prev) => {
|
||||||
const newId = `layer-${Date.now()}`;
|
const newId = `layer-${Date.now()}`;
|
||||||
@@ -1521,6 +1527,7 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
blocks={blocks}
|
blocks={blocks}
|
||||||
onBlockDrop={handleBlockDrop}
|
onBlockDrop={handleBlockDrop}
|
||||||
onSelectionChange={handleSelectionChange}
|
onSelectionChange={handleSelectionChange}
|
||||||
|
externalSelectionIds={selectedElementIds}
|
||||||
selectedTemplate={selectedTemplate}
|
selectedTemplate={selectedTemplate}
|
||||||
bgConfig={bgConfig}
|
bgConfig={bgConfig}
|
||||||
remoteCursors={collab.cursors}
|
remoteCursors={collab.cursors}
|
||||||
@@ -1563,6 +1570,9 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
onReorder={handleReorderLayer}
|
onReorder={handleReorderLayer}
|
||||||
onAddSubLayer={handleAddSubLayer}
|
onAddSubLayer={handleAddSubLayer}
|
||||||
onUpdateElement={handleUpdateElement}
|
onUpdateElement={handleUpdateElement}
|
||||||
|
selectedElementIds={selectedElementIds}
|
||||||
|
groups={groups}
|
||||||
|
onSelectElement={handleSelectElement}
|
||||||
token={token}
|
token={token}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
|||||||
cursorPos, viewMode, onViewChange, gridEnabled, orthoEnabled, snapEnabled,
|
cursorPos, viewMode, onViewChange, gridEnabled, orthoEnabled, snapEnabled,
|
||||||
polarEnabled,
|
polarEnabled,
|
||||||
activeTool, elements, layers, activeLayerId, onElementCreated, onElementsDeleted, onElementsModified, onCursorMoved, onToolStateChanged,
|
activeTool, elements, layers, activeLayerId, onElementCreated, onElementsDeleted, onElementsModified, onCursorMoved, onToolStateChanged,
|
||||||
onToggleGrid, onToggleOrtho, onToggleSnap, onZoomIn, onZoomOut, onZoomFit, onTextEdit, onCommandTrigger, blocks, onBlockDrop, onSelectionChange, selectedTemplate, bgConfig, remoteCursors, zoomCommand,
|
onToggleGrid, onToggleOrtho, onToggleSnap, onZoomIn, onZoomOut, onZoomFit, onTextEdit, onCommandTrigger, blocks, onBlockDrop, onSelectionChange, externalSelectionIds, selectedTemplate, bgConfig, remoteCursors, zoomCommand,
|
||||||
groups,
|
groups,
|
||||||
}) => {
|
}) => {
|
||||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||||
@@ -25,6 +25,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
|||||||
const interactionRef = useRef<InteractionEngine | null>(null);
|
const interactionRef = useRef<InteractionEngine | null>(null);
|
||||||
const spatialIndexRef = useRef<SpatialIndex | null>(null);
|
const spatialIndexRef = useRef<SpatialIndex | null>(null);
|
||||||
const layerManagerRef = useRef<LayerManager | null>(null);
|
const layerManagerRef = useRef<LayerManager | null>(null);
|
||||||
|
const selectionEngineRef = useRef<SelectionEngine | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
@@ -45,6 +46,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
|||||||
interactionRef.current = interaction;
|
interactionRef.current = interaction;
|
||||||
spatialIndexRef.current = spatialIndex;
|
spatialIndexRef.current = spatialIndex;
|
||||||
layerManagerRef.current = layerManager;
|
layerManagerRef.current = layerManager;
|
||||||
|
selectionEngineRef.current = selectionEngine;
|
||||||
|
|
||||||
interaction.setCallbacks({
|
interaction.setCallbacks({
|
||||||
onElementCreated,
|
onElementCreated,
|
||||||
@@ -66,6 +68,7 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
|||||||
interactionRef.current = null;
|
interactionRef.current = null;
|
||||||
spatialIndexRef.current = null;
|
spatialIndexRef.current = null;
|
||||||
layerManagerRef.current = null;
|
layerManagerRef.current = null;
|
||||||
|
selectionEngineRef.current = null;
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
@@ -169,6 +172,28 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [groups]);
|
}, [groups]);
|
||||||
|
|
||||||
|
// Sync external selection (from tree panel) to SelectionEngine
|
||||||
|
const prevExternalSelectionRef = useRef<string[]>([]);
|
||||||
|
useEffect(() => {
|
||||||
|
const selectionEngine = selectionEngineRef.current;
|
||||||
|
const renderEngine = renderEngineRef.current;
|
||||||
|
if (!selectionEngine || !renderEngine) return;
|
||||||
|
if (!externalSelectionIds || externalSelectionIds.length === 0) return;
|
||||||
|
// Avoid feedback loop: skip if selection is already identical
|
||||||
|
const currentIds = Array.from(selectionEngine.getSelectedIds()).sort();
|
||||||
|
const incomingIds = [...externalSelectionIds].sort();
|
||||||
|
const same = currentIds.length === incomingIds.length && currentIds.every((id, i) => id === incomingIds[i]);
|
||||||
|
if (same) return;
|
||||||
|
// Avoid re-processing the same external selection we just applied
|
||||||
|
const prevIds = prevExternalSelectionRef.current;
|
||||||
|
const sameAsPrev = prevIds.length === incomingIds.length && prevIds.every((id, i) => id === incomingIds[i]);
|
||||||
|
if (sameAsPrev) return;
|
||||||
|
prevExternalSelectionRef.current = incomingIds;
|
||||||
|
selectionEngine.selectByIds(externalSelectionIds, false);
|
||||||
|
renderEngine.render();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [externalSelectionIds]);
|
||||||
|
|
||||||
// Sync active layer to LayerManager
|
// Sync active layer to LayerManager
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const layerManager = layerManagerRef.current;
|
const layerManager = layerManagerRef.current;
|
||||||
|
|||||||
@@ -32,6 +32,17 @@ const layerIcon = (
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const groupIcon = (
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<path d="M3 7a2 2 0 0 1 2-2h4l2 2h7a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
||||||
|
<rect x="7" y="11" width="4" height="4" rx="0.5" />
|
||||||
|
<rect x="13" y="11" width="4" height="4" rx="0.5" />
|
||||||
|
<line x1="9" y1="15" x2="9" y2="17" />
|
||||||
|
<line x1="15" y1="15" x2="15" y2="17" />
|
||||||
|
<line x1="9" y1="17" x2="15" y2="17" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
const elementIcons: Record<string, React.ReactNode> = {
|
const elementIcons: Record<string, React.ReactNode> = {
|
||||||
line: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="4" y1="20" x2="20" y2="4"/></svg>,
|
line: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="4" y1="20" x2="20" y2="4"/></svg>,
|
||||||
rect: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="4" y="4" width="16" height="16"/></svg>,
|
rect: <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="4" y="4" width="16" height="16"/></svg>,
|
||||||
@@ -58,7 +69,10 @@ const LayerPanel: React.FC<LayerPanelProps> = ({
|
|||||||
layers,
|
layers,
|
||||||
elements = [],
|
elements = [],
|
||||||
activeLayerId,
|
activeLayerId,
|
||||||
|
selectedElementIds = [],
|
||||||
|
groups = [],
|
||||||
onSelectLayer,
|
onSelectLayer,
|
||||||
|
onSelectElement,
|
||||||
onAddLayer,
|
onAddLayer,
|
||||||
onToggleLayer,
|
onToggleLayer,
|
||||||
onDeleteLayer,
|
onDeleteLayer,
|
||||||
@@ -76,22 +90,71 @@ const LayerPanel: React.FC<LayerPanelProps> = ({
|
|||||||
.sort((a, b) => a.sortOrder - b.sortOrder)
|
.sort((a, b) => a.sortOrder - b.sortOrder)
|
||||||
.map((l) => {
|
.map((l) => {
|
||||||
const layerElements = elements.filter((e) => e.layerId === l.id);
|
const layerElements = elements.filter((e) => e.layerId === l.id);
|
||||||
const elementNodes: TreeNode[] = layerElements.map((e) => ({
|
|
||||||
|
// Build group nodes for this layer
|
||||||
|
const layerGroups = groups.filter((g) =>
|
||||||
|
g.elementIds.some((eid) => layerElements.some((e) => e.id === eid))
|
||||||
|
);
|
||||||
|
const groupNodes: TreeNode[] = layerGroups.map((g) => {
|
||||||
|
const groupElementIds = g.elementIds.filter((eid) =>
|
||||||
|
layerElements.some((e) => e.id === eid)
|
||||||
|
);
|
||||||
|
const groupElementsList = groupElementIds
|
||||||
|
.map((eid) => elements.find((e) => e.id === eid))
|
||||||
|
.filter((e): e is CADElement => e !== undefined);
|
||||||
|
|
||||||
|
const groupChildNodes: TreeNode[] = groupElementsList.map((e) => ({
|
||||||
|
id: e.id,
|
||||||
|
name: elementTypeLabels[e.type] || e.type,
|
||||||
|
icon: elementIcons[e.type] || defaultIcon,
|
||||||
|
expanded: false,
|
||||||
|
active: selectedElementIds.includes(e.id),
|
||||||
|
children: [],
|
||||||
|
}));
|
||||||
|
|
||||||
|
// A group is "active" if any of its elements are selected
|
||||||
|
const groupActive = groupElementIds.some((eid) => selectedElementIds.includes(eid));
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: g.id,
|
||||||
|
name: g.name,
|
||||||
|
icon: groupIcon,
|
||||||
|
expanded: false,
|
||||||
|
active: groupActive,
|
||||||
|
children: groupChildNodes,
|
||||||
|
count: groupElementIds.length > 0 ? groupElementIds.length : undefined,
|
||||||
|
} as TreeNode;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build element nodes — only elements NOT in any group go directly under layer
|
||||||
|
const groupedElementIds = new Set<string>();
|
||||||
|
for (const g of layerGroups) {
|
||||||
|
for (const eid of g.elementIds) {
|
||||||
|
if (layerElements.some((e) => e.id === eid)) {
|
||||||
|
groupedElementIds.add(eid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const ungroupedElements = layerElements.filter((e) => !groupedElementIds.has(e.id));
|
||||||
|
const elementNodes: TreeNode[] = ungroupedElements.map((e) => ({
|
||||||
id: e.id,
|
id: e.id,
|
||||||
name: elementTypeLabels[e.type] || e.type,
|
name: elementTypeLabels[e.type] || e.type,
|
||||||
icon: elementIcons[e.type] || defaultIcon,
|
icon: elementIcons[e.type] || defaultIcon,
|
||||||
expanded: false,
|
expanded: false,
|
||||||
active: false,
|
active: selectedElementIds.includes(e.id),
|
||||||
children: [],
|
children: [],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const subLayerNodes = buildTree(l.id);
|
const subLayerNodes = buildTree(l.id);
|
||||||
|
|
||||||
|
// Groups appear above elements, sublayers appear first
|
||||||
return {
|
return {
|
||||||
id: l.id,
|
id: l.id,
|
||||||
name: l.name,
|
name: l.name,
|
||||||
icon: layerIcon,
|
icon: layerIcon,
|
||||||
expanded: false,
|
expanded: false,
|
||||||
active: l.id === activeLayerId,
|
active: l.id === activeLayerId,
|
||||||
children: [...subLayerNodes, ...elementNodes],
|
children: [...subLayerNodes, ...groupNodes, ...elementNodes],
|
||||||
count: layerElements.length > 0 ? layerElements.length : undefined,
|
count: layerElements.length > 0 ? layerElements.length : undefined,
|
||||||
} as TreeNode;
|
} as TreeNode;
|
||||||
});
|
});
|
||||||
@@ -99,6 +162,24 @@ const LayerPanel: React.FC<LayerPanelProps> = ({
|
|||||||
|
|
||||||
const tree = buildTree(null);
|
const tree = buildTree(null);
|
||||||
|
|
||||||
|
// Determine which IDs are layer IDs vs element IDs vs group IDs for dispatch
|
||||||
|
const layerIds = new Set(layers.map((l) => l.id));
|
||||||
|
const groupIds = new Set(groups.map((g) => g.id));
|
||||||
|
|
||||||
|
const handleSelect = (id: string) => {
|
||||||
|
if (layerIds.has(id)) {
|
||||||
|
onSelectLayer(id);
|
||||||
|
} else if (groupIds.has(id)) {
|
||||||
|
const group = groups.find((g) => g.id === id);
|
||||||
|
if (group && group.elementIds.length > 0) {
|
||||||
|
onSelectElement?.(group.elementIds[0]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// It's an element ID
|
||||||
|
onSelectElement?.(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddSubLayer = (parentId: string) => {
|
const handleAddSubLayer = (parentId: string) => {
|
||||||
if (onAddSubLayer) {
|
if (onAddSubLayer) {
|
||||||
onAddSubLayer(parentId);
|
onAddSubLayer(parentId);
|
||||||
@@ -131,7 +212,8 @@ const LayerPanel: React.FC<LayerPanelProps> = ({
|
|||||||
<TreeView
|
<TreeView
|
||||||
nodes={tree}
|
nodes={tree}
|
||||||
selectedId={activeLayerId}
|
selectedId={activeLayerId}
|
||||||
onSelect={onSelectLayer}
|
selectedIds={selectedElementIds}
|
||||||
|
onSelect={handleSelect}
|
||||||
onReorder={onReorder}
|
onReorder={onReorder}
|
||||||
draggable={true}
|
draggable={true}
|
||||||
renderIcon={(node) => node.icon}
|
renderIcon={(node) => node.icon}
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ const RightSidebar: React.FC<RightSidebarProps> = ({
|
|||||||
onRenameBlock, onDuplicateBlock, onDeleteBlock, onSvgImport, onSaveGroupAsBlock,
|
onRenameBlock, onDuplicateBlock, onDeleteBlock, onSvgImport, onSaveGroupAsBlock,
|
||||||
onBlockCategoryChange, onBlockSearch, onDragBlock,
|
onBlockCategoryChange, onBlockSearch, onDragBlock,
|
||||||
kiMessages, kiSuggestions, onKISend, onKISuggestionClick, kiLoading, onUpdateElement,
|
kiMessages, kiSuggestions, onKISend, onKISuggestionClick, kiLoading, onUpdateElement,
|
||||||
|
selectedElementIds,
|
||||||
|
groups,
|
||||||
|
onSelectElement,
|
||||||
onCollapse,
|
onCollapse,
|
||||||
collapsed,
|
collapsed,
|
||||||
onToggleCollapse,
|
onToggleCollapse,
|
||||||
@@ -79,7 +82,7 @@ const RightSidebar: React.FC<RightSidebarProps> = ({
|
|||||||
}} />}
|
}} />}
|
||||||
</div>
|
</div>
|
||||||
<div className={`rightbar-panel${activePanel === 'layer' ? ' active' : ''}`} data-panel-content="layer">
|
<div className={`rightbar-panel${activePanel === 'layer' ? ' active' : ''}`} data-panel-content="layer">
|
||||||
{activePanel === 'layer' && <LayerPanel layers={layers} elements={elements} onElementsDeleted={onElementsDeleted} onToggleElementVisible={onToggleElementVisible} activeLayerId={activeLayerId} onSelectLayer={onSelectLayer ?? (() => {})} onAddLayer={onAddLayer ?? (() => {})} onToggleLayer={onToggleLayer ?? (() => {})} onDeleteLayer={onDeleteLayer} onRenameLayer={onRenameLayer} onDuplicateLayer={onDuplicateLayer} onToggleLock={onToggleLock} onReorder={onReorder} onAddSubLayer={onAddSubLayer} />}
|
{activePanel === 'layer' && <LayerPanel layers={layers} elements={elements} onElementsDeleted={onElementsDeleted} onToggleElementVisible={onToggleElementVisible} activeLayerId={activeLayerId} onSelectLayer={onSelectLayer ?? (() => {})} onSelectElement={onSelectElement} selectedElementIds={selectedElementIds} groups={groups} onAddLayer={onAddLayer ?? (() => {})} onToggleLayer={onToggleLayer ?? (() => {})} onDeleteLayer={onDeleteLayer} onRenameLayer={onRenameLayer} onDuplicateLayer={onDuplicateLayer} onToggleLock={onToggleLock} onReorder={onReorder} onAddSubLayer={onAddSubLayer} />}
|
||||||
</div>
|
</div>
|
||||||
<div className={`rightbar-panel${activePanel === 'library' ? ' active' : ''}`} data-panel-content="library">
|
<div className={`rightbar-panel${activePanel === 'library' ? ' active' : ''}`} data-panel-content="library">
|
||||||
{activePanel === 'library' && (
|
{activePanel === 'library' && (
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { TreeNode } from '../types/ui.types';
|
|||||||
interface TreeViewProps {
|
interface TreeViewProps {
|
||||||
nodes: TreeNode[];
|
nodes: TreeNode[];
|
||||||
selectedId?: string | null;
|
selectedId?: string | null;
|
||||||
|
selectedIds?: string[];
|
||||||
onSelect: (id: string) => void;
|
onSelect: (id: string) => void;
|
||||||
onToggle?: (id: string) => void;
|
onToggle?: (id: string) => void;
|
||||||
onReorder?: (draggedId: string, targetId: string, position: 'before' | 'after' | 'inside') => void;
|
onReorder?: (draggedId: string, targetId: string, position: 'before' | 'after' | 'inside') => void;
|
||||||
@@ -16,6 +17,7 @@ interface TreeViewProps {
|
|||||||
const TreeView: React.FC<TreeViewProps> = ({
|
const TreeView: React.FC<TreeViewProps> = ({
|
||||||
nodes,
|
nodes,
|
||||||
selectedId,
|
selectedId,
|
||||||
|
selectedIds,
|
||||||
onSelect,
|
onSelect,
|
||||||
onToggle,
|
onToggle,
|
||||||
onReorder,
|
onReorder,
|
||||||
@@ -87,7 +89,7 @@ const TreeView: React.FC<TreeViewProps> = ({
|
|||||||
const renderNode = (node: TreeNode, level: number): React.ReactNode => {
|
const renderNode = (node: TreeNode, level: number): React.ReactNode => {
|
||||||
const hasChildren = node.children && node.children.length > 0;
|
const hasChildren = node.children && node.children.length > 0;
|
||||||
const isExpanded = expandedSet.has(node.id) || node.expanded;
|
const isExpanded = expandedSet.has(node.id) || node.expanded;
|
||||||
const isSelected = selectedId === node.id;
|
const isSelected = selectedId === node.id || (selectedIds !== undefined && selectedIds.includes(node.id));
|
||||||
const isDragOver = dragOverId === node.id;
|
const isDragOver = dragOverId === node.id;
|
||||||
const isDragging = draggedId === node.id;
|
const isDragging = draggedId === node.id;
|
||||||
|
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ a:hover { text-decoration: underline; }
|
|||||||
.project-name:hover { background: var(--color-surface-2); }
|
.project-name:hover { background: var(--color-surface-2); }
|
||||||
.project-name .caret { color: var(--color-text-faint); }
|
.project-name .caret { color: var(--color-text-faint); }
|
||||||
.saved-badge {
|
.saved-badge {
|
||||||
font-size: var(--fs-xs);
|
font-size: var(--fs-sm);
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
background: var(--color-surface-2);
|
background: var(--color-surface-2);
|
||||||
@@ -355,7 +355,7 @@ a:hover { text-decoration: underline; }
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-size: var(--fs-sm);
|
font-size: var(--fs-md);
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
border-bottom: 2px solid transparent;
|
border-bottom: 2px solid transparent;
|
||||||
margin-bottom: -1px;
|
margin-bottom: -1px;
|
||||||
@@ -385,8 +385,8 @@ a:hover { text-decoration: underline; }
|
|||||||
}
|
}
|
||||||
.ribbon-group-btns { display: flex; align-items: center; gap: 2px; flex: 1; }
|
.ribbon-group-btns { display: flex; align-items: center; gap: 2px; flex: 1; }
|
||||||
.ribbon-group-label {
|
.ribbon-group-label {
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
color: var(--color-text-faint);
|
color: var(--color-text-muted);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.4px;
|
letter-spacing: 0.4px;
|
||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--color-border);
|
||||||
@@ -414,7 +414,7 @@ a:hover { text-decoration: underline; }
|
|||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
font-size: 10px;
|
font-size: 12px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: all var(--t-fast);
|
transition: all var(--t-fast);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ export interface CanvasAreaProps {
|
|||||||
blocks?: BlockDefinition[];
|
blocks?: BlockDefinition[];
|
||||||
onBlockDrop?: (blockId: string, x: number, y: number) => void;
|
onBlockDrop?: (blockId: string, x: number, y: number) => void;
|
||||||
onSelectionChange?: (selectedIds: string[]) => void;
|
onSelectionChange?: (selectedIds: string[]) => void;
|
||||||
|
externalSelectionIds?: string[];
|
||||||
selectedTemplate?: string | null;
|
selectedTemplate?: string | null;
|
||||||
bgConfig?: BackgroundConfig | null;
|
bgConfig?: BackgroundConfig | null;
|
||||||
remoteCursors?: UserCursor[];
|
remoteCursors?: UserCursor[];
|
||||||
@@ -174,6 +175,9 @@ export interface RightSidebarProps {
|
|||||||
onKISuggestionClick?: (suggestion: KISuggestion) => void;
|
onKISuggestionClick?: (suggestion: KISuggestion) => void;
|
||||||
kiLoading?: boolean;
|
kiLoading?: boolean;
|
||||||
onUpdateElement?: (el: CADElement) => void;
|
onUpdateElement?: (el: CADElement) => void;
|
||||||
|
selectedElementIds?: string[];
|
||||||
|
groups?: Array<{ id: string; name: string; elementIds: string[]; parentGroupId: string | null }>;
|
||||||
|
onSelectElement?: (id: string) => void;
|
||||||
token?: string;
|
token?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
onCollapse?: () => void;
|
onCollapse?: () => void;
|
||||||
@@ -192,7 +196,10 @@ export interface LayerPanelProps {
|
|||||||
layers: CADLayer[];
|
layers: CADLayer[];
|
||||||
elements?: CADElement[];
|
elements?: CADElement[];
|
||||||
activeLayerId?: string;
|
activeLayerId?: string;
|
||||||
|
selectedElementIds?: string[];
|
||||||
|
groups?: Array<{ id: string; name: string; elementIds: string[]; parentGroupId: string | null }>;
|
||||||
onSelectLayer: (id: string) => void;
|
onSelectLayer: (id: string) => void;
|
||||||
|
onSelectElement?: (id: string) => void;
|
||||||
onAddLayer: () => void;
|
onAddLayer: () => void;
|
||||||
onToggleLayer: (id: string) => void;
|
onToggleLayer: (id: string) => void;
|
||||||
onDeleteLayer?: (id: string) => void;
|
onDeleteLayer?: (id: string) => void;
|
||||||
|
|||||||
@@ -238,6 +238,112 @@ describe('LayerPanel', () => {
|
|||||||
fireEvent.click(toggleBtn);
|
fireEvent.click(toggleBtn);
|
||||||
expect(onToggleLayer).toHaveBeenCalled();
|
expect(onToggleLayer).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
// Helper: expand a tree node by clicking its toggle button
|
||||||
|
function expandLayer(label: string) {
|
||||||
|
const layerNode = screen.getByText(label).closest('.tree-node');
|
||||||
|
expect(layerNode).toBeTruthy();
|
||||||
|
const toggle = layerNode?.querySelector('.tree-toggle');
|
||||||
|
expect(toggle).toBeTruthy();
|
||||||
|
fireEvent.click(toggle!);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should highlight selected element in tree when selectedElementIds is provided', () => {
|
||||||
|
const layers = [makeLayer({ id: 'layer-1', name: 'Layer 1' })];
|
||||||
|
const elements = [
|
||||||
|
makeElement({ id: 'elem-1', type: 'rect', layerId: 'layer-1' }),
|
||||||
|
makeElement({ id: 'elem-2', type: 'circle', layerId: 'layer-1' }),
|
||||||
|
];
|
||||||
|
const { container } = render(
|
||||||
|
<LayerPanel
|
||||||
|
layers={layers}
|
||||||
|
elements={elements}
|
||||||
|
selectedElementIds={['elem-2']}
|
||||||
|
activeLayerId="layer-1"
|
||||||
|
onSelectLayer={() => {}}
|
||||||
|
onAddLayer={() => {}}
|
||||||
|
onToggleLayer={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expandLayer('Layer 1');
|
||||||
|
// The element node for elem-2 should have the 'active' class
|
||||||
|
const treeNodes = container.querySelectorAll('.tree-node');
|
||||||
|
const elemNode = Array.from(treeNodes).find((n) => n.textContent?.includes('Kreis'));
|
||||||
|
expect(elemNode).toBeTruthy();
|
||||||
|
expect(elemNode?.classList.contains('active')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onSelectElement when clicking an element in the tree', () => {
|
||||||
|
const layers = [makeLayer({ id: 'layer-1', name: 'Layer 1' })];
|
||||||
|
const elements = [
|
||||||
|
makeElement({ id: 'elem-1', type: 'rect', layerId: 'layer-1' }),
|
||||||
|
];
|
||||||
|
const onSelectElement = vi.fn();
|
||||||
|
render(
|
||||||
|
<LayerPanel
|
||||||
|
layers={layers}
|
||||||
|
elements={elements}
|
||||||
|
activeLayerId="layer-1"
|
||||||
|
onSelectLayer={() => {}}
|
||||||
|
onSelectElement={onSelectElement}
|
||||||
|
onAddLayer={() => {}}
|
||||||
|
onToggleLayer={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expandLayer('Layer 1');
|
||||||
|
fireEvent.click(screen.getByText('Rechteck'));
|
||||||
|
expect(onSelectElement).toHaveBeenCalledWith('elem-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render groups as tree nodes with their elements as children', () => {
|
||||||
|
const layers = [makeLayer({ id: 'layer-1', name: 'Layer 1' })];
|
||||||
|
const elements = [
|
||||||
|
makeElement({ id: 'elem-1', type: 'rect', layerId: 'layer-1' }),
|
||||||
|
makeElement({ id: 'elem-2', type: 'circle', layerId: 'layer-1' }),
|
||||||
|
];
|
||||||
|
const groups = [
|
||||||
|
{ id: 'group-1', name: 'Gruppe A', elementIds: ['elem-1', 'elem-2'], parentGroupId: null },
|
||||||
|
];
|
||||||
|
render(
|
||||||
|
<LayerPanel
|
||||||
|
layers={layers}
|
||||||
|
elements={elements}
|
||||||
|
groups={groups}
|
||||||
|
activeLayerId="layer-1"
|
||||||
|
onSelectLayer={() => {}}
|
||||||
|
onAddLayer={() => {}}
|
||||||
|
onToggleLayer={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expandLayer('Layer 1');
|
||||||
|
expect(screen.getByText('Gruppe A')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select all group elements when clicking a group node', () => {
|
||||||
|
const layers = [makeLayer({ id: 'layer-1', name: 'Layer 1' })];
|
||||||
|
const elements = [
|
||||||
|
makeElement({ id: 'elem-1', type: 'rect', layerId: 'layer-1' }),
|
||||||
|
makeElement({ id: 'elem-2', type: 'circle', layerId: 'layer-1' }),
|
||||||
|
];
|
||||||
|
const groups = [
|
||||||
|
{ id: 'group-1', name: 'Gruppe A', elementIds: ['elem-1', 'elem-2'], parentGroupId: null },
|
||||||
|
];
|
||||||
|
const onSelectElement = vi.fn();
|
||||||
|
render(
|
||||||
|
<LayerPanel
|
||||||
|
layers={layers}
|
||||||
|
elements={elements}
|
||||||
|
groups={groups}
|
||||||
|
activeLayerId="layer-1"
|
||||||
|
onSelectLayer={() => {}}
|
||||||
|
onSelectElement={onSelectElement}
|
||||||
|
onAddLayer={() => {}}
|
||||||
|
onToggleLayer={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expandLayer('Layer 1');
|
||||||
|
fireEvent.click(screen.getByText('Gruppe A'));
|
||||||
|
expect(onSelectElement).toHaveBeenCalledWith('elem-1');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── PropertiesPanel ─────────────────────────────────
|
// ─── PropertiesPanel ─────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user