From 4f1d680406b01bdc98a9ed32b645c7cfb84d2da8 Mon Sep 17 00:00:00 2001 From: igurielidze Date: Sat, 21 Mar 2026 19:07:42 +0400 Subject: [PATCH] Merge device dock into single smart list: drop to place or assign ID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove tabs, single unified device list grouped by type - Drop on empty space → places new symbol with device ID - Drop on existing symbol → assigns the device ID as label - Drop-target highlight shown when hovering over existing symbols - Always sorted ascending by device type Co-Authored-By: Claude Opus 4.6 (1M context) --- svelte-app/src/components/DeviceDock.svelte | 294 +++++--------------- svelte-app/src/lib/canvas/interactions.ts | 66 +++-- 2 files changed, 115 insertions(+), 245 deletions(-) diff --git a/svelte-app/src/components/DeviceDock.svelte b/svelte-app/src/components/DeviceDock.svelte index 3854ce3..c248bfa 100644 --- a/svelte-app/src/components/DeviceDock.svelte +++ b/svelte-app/src/components/DeviceDock.svelte @@ -1,7 +1,7 @@
@@ -106,109 +86,55 @@ {#if !collapsed}
- -
- - +
+ Devices + {totalAll - totalCount}/{totalAll} placed
- - {#if activeTab === 'devices'} -
- {totalAll - totalCount}/{totalAll} placed -
+
Drop on empty space to place, on symbol to assign ID
- - - {#if filteredDevices.length === 0} -
{searchQuery ? 'No matches' : 'All placed'}
+ -
- {#each svgGroups as [groupName, devices] (groupName)} - - {#if !collapsedZones.has(groupName)} -
- {#each devices as device (device.id)} - - {/each} -
- {/if} - {/each} -
- - - {:else} -
- {unassignedIds.length} unassigned -
- - - -
Drag onto a symbol to assign
- - {#if unassignedIds.length === 0} -
{idSearchQuery ? 'No matches' : 'All IDs assigned'}
- {/if} - -
- {#each unassignedIds as device (device.id)} - - {/each} -
+ {#if filteredDevices.length === 0} +
{searchQuery ? 'No matches' : 'All placed'}
{/if} + +
+ {#each svgGroups as [groupName, devices] (groupName)} + + {#if !collapsedZones.has(groupName)} +
+ {#each devices as device (device.id)} + + {/each} +
+ {/if} + {/each} +
{/if}
@@ -264,54 +190,36 @@ min-height: 0; } - /* Tabs */ - .tabs { - display: flex; - gap: 2px; - flex-shrink: 0; - margin-bottom: 4px; - } - - .tab { - flex: 1; - padding: 5px 0; - background: #0d2847; - border: 1px solid #1a1a5e; - border-radius: 4px 4px 0 0; - color: #667; - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - cursor: pointer; - transition: all 0.15s; - } - - .tab:hover { - color: #aaa; - } - - .tab.active { - background: #0f3460; - color: #e94560; - border-bottom-color: #0f3460; - } - .dock-header { font-size: 10px; - padding: 2px 6px 4px; + padding: 2px 6px 2px; flex-shrink: 0; display: flex; - justify-content: flex-end; + justify-content: space-between; align-items: center; } + .dock-title { + font-size: 10px; + font-weight: 600; + color: #8899aa; + text-transform: uppercase; + letter-spacing: 0.5px; + } + .dock-count { font-size: 9px; color: #667; font-weight: 400; } + .dock-hint { + font-size: 8px; + color: #445; + padding: 0 6px 4px; + flex-shrink: 0; + } + /* Search */ .search-box { position: relative; @@ -469,66 +377,4 @@ overflow: hidden; text-overflow: ellipsis; } - - /* IDs tab */ - .ids-hint { - font-size: 9px; - color: #556; - padding: 0 6px 4px; - flex-shrink: 0; - } - - .id-drag-item { - display: flex; - align-items: center; - gap: 5px; - width: 100%; - padding: 3px 6px; - margin-bottom: 1px; - background: #0f3460; - border: 1px solid #1a1a5e; - border-radius: 3px; - cursor: grab; - user-select: none; - transition: all 0.15s ease; - color: #e0e0e0; - font-size: 10px; - font-weight: 500; - text-align: left; - } - - .id-symbol-icon { - width: 20px; - height: 14px; - object-fit: contain; - flex-shrink: 0; - filter: invert(1); - opacity: 0.7; - } - - .id-label { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - flex: 1; - min-width: 0; - } - - .id-type { - font-size: 8px; - color: #556; - white-space: nowrap; - flex-shrink: 0; - } - - .id-drag-item:hover { - background: #1a1a5e; - border-color: #e94560; - transform: translateX(-2px); - } - - .id-drag-item:active { - cursor: grabbing; - transform: scale(0.98); - } diff --git a/svelte-app/src/lib/canvas/interactions.ts b/svelte-app/src/lib/canvas/interactions.ts index 3849315..e24fb5a 100644 --- a/svelte-app/src/lib/canvas/interactions.ts +++ b/svelte-app/src/lib/canvas/interactions.ts @@ -471,6 +471,15 @@ function onMousemove(e: MouseEvent) { const sym = dragState.symbolDef; dragState.ghost.style.left = (e.clientX - sym.w / 2) + 'px'; dragState.ghost.style.top = (e.clientY - sym.h / 2) + 'px'; + // Show drop-target highlight when dragging a device over an existing symbol + if (dragState.deviceLabel) { + const pos = screenToCanvas(e.clientX, e.clientY); + const hoverId = hitTest(pos.x, pos.y); + if (layout.labelDropTarget !== hoverId) { + layout.labelDropTarget = hoverId; + layout.markDirty(); + } + } } if (dragState.type === 'assign-label' && dragState.ghost) { @@ -626,31 +635,46 @@ function onMouseup(e: MouseEvent) { if (dragState.type === 'palette' && dragState.ghost && dragState.symbolDef) { dragState.ghost.remove(); + layout.labelDropTarget = null; const sym = dragState.symbolDef; const rot = sym.defaultRotation || 0; const pos = screenToCanvas(e.clientX, e.clientY); - let dropX = pos.x - sym.w / 2; - let dropY = pos.y - sym.h / 2; - if (dropX >= -sym.w && dropX <= layout.canvasW && dropY >= -sym.h && dropY <= layout.canvasH) { - dropX = clamp(dropX, 0, layout.canvasW - sym.w); - dropY = clamp(dropY, 0, layout.canvasH - sym.h); - const valid = findValidPosition(-1, dropX, dropY, sym.w, sym.h, sym.id, rot, sym.curveAngle, sym.w2); - layout.addSymbol({ - symbolId: sym.id, - file: sym.file, - name: sym.name, - label: dragState.deviceLabel || '', - x: valid.x, - y: valid.y, - w: sym.w, - h: sym.h, - w2: sym.w2, - rotation: rot, - curveAngle: sym.curveAngle, - epcWaypoints: isEpcType(sym.id) ? EPC_CONFIG.defaultWaypoints.map(wp => ({ ...wp })) : undefined, - pdpCBs: undefined, - }); + // If dropped onto an existing symbol and we have a device label, assign the label + const hitId = dragState.deviceLabel ? hitTest(pos.x, pos.y) : null; + if (hitId !== null && dragState.deviceLabel) { + const target = layout.symbols.find(s => s.id === hitId); + if (target) { + layout.pushUndo(); + target.label = dragState.deviceLabel; + layout.markDirty(); + layout.saveMcmState(); + } + } else { + // Drop onto empty space: place a new symbol + let dropX = pos.x - sym.w / 2; + let dropY = pos.y - sym.h / 2; + + if (dropX >= -sym.w && dropX <= layout.canvasW && dropY >= -sym.h && dropY <= layout.canvasH) { + dropX = clamp(dropX, 0, layout.canvasW - sym.w); + dropY = clamp(dropY, 0, layout.canvasH - sym.h); + const valid = findValidPosition(-1, dropX, dropY, sym.w, sym.h, sym.id, rot, sym.curveAngle, sym.w2); + layout.addSymbol({ + symbolId: sym.id, + file: sym.file, + name: sym.name, + label: dragState.deviceLabel || '', + x: valid.x, + y: valid.y, + w: sym.w, + h: sym.h, + w2: sym.w2, + rotation: rot, + curveAngle: sym.curveAngle, + epcWaypoints: isEpcType(sym.id) ? EPC_CONFIG.defaultWaypoints.map(wp => ({ ...wp })) : undefined, + pdpCBs: undefined, + }); + } } }