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}
-
-
-
-
+
-
- {#if activeTab === 'devices'}
-
+
Drop on empty space to place, on symbol to assign ID
-
-
- {#if searchQuery}
-
- {/if}
-
-
- {#if filteredDevices.length === 0}
-
{searchQuery ? 'No matches' : 'All placed'}
+
+
+ {#if searchQuery}
+
{/if}
+
-
- {#each svgGroups as [groupName, devices] (groupName)}
-
- {#if !collapsedZones.has(groupName)}
-
- {#each devices as device (device.id)}
-
- {/each}
-
- {/if}
- {/each}
-
-
-
- {:else}
-
-
-
-
- {#if idSearchQuery}
-
- {/if}
-
-
-
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,
+ });
+ }
}
}