Add device visibility controls: right-click hide and top bar type toggles
- Right-click context menu: "Hide" option to hide individual symbols - "Show All Hidden" appears in context menu when anything is hidden - Top visibility bar with toggle chips for each device group - Hidden symbols are excluded from rendering, hit testing, and SVG export Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cc91481c98
commit
51794cb9ae
@ -84,6 +84,21 @@
|
||||
contextMenu = null;
|
||||
}
|
||||
|
||||
function handleHide() {
|
||||
if (!contextMenu) return;
|
||||
if (layout.selectedIds.has(contextMenu.symId) && layout.selectedIds.size > 1) {
|
||||
layout.hideSelected();
|
||||
} else {
|
||||
layout.toggleSymbolHidden(contextMenu.symId);
|
||||
}
|
||||
contextMenu = null;
|
||||
}
|
||||
|
||||
function handleShowAll() {
|
||||
layout.showAllHidden();
|
||||
contextMenu = null;
|
||||
}
|
||||
|
||||
function handleSetLabel() {
|
||||
if (!contextMenu) return;
|
||||
const sym = layout.symbols.find(s => s.id === contextMenu!.symId);
|
||||
@ -142,7 +157,12 @@
|
||||
<button class="context-menu-item" onclick={handleSetLabel} role="menuitem">Set ID</button>
|
||||
<button class="context-menu-item" onclick={handleMirror} role="menuitem">Mirror</button>
|
||||
<button class="context-menu-item" onclick={handleDuplicate} role="menuitem">Duplicate</button>
|
||||
<button class="context-menu-item" onclick={handleHide} role="menuitem">Hide</button>
|
||||
<button class="context-menu-item" onclick={handleDelete} role="menuitem">Delete</button>
|
||||
{#if layout.symbols.some(s => s.hidden) || layout.hiddenGroups.size > 0}
|
||||
<div class="context-menu-divider"></div>
|
||||
<button class="context-menu-item" onclick={handleShowAll} role="menuitem">Show All Hidden</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@ -222,4 +242,10 @@
|
||||
.context-menu-item:hover {
|
||||
background: #e94560;
|
||||
}
|
||||
|
||||
.context-menu-divider {
|
||||
height: 1px;
|
||||
background: #0f3460;
|
||||
margin: 4px 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
80
svelte-app/src/components/VisibilityBar.svelte
Normal file
80
svelte-app/src/components/VisibilityBar.svelte
Normal file
@ -0,0 +1,80 @@
|
||||
<script lang="ts">
|
||||
import { layout } from '$lib/stores/layout.svelte.js';
|
||||
import { SYMBOL_GROUPS } from '$lib/symbols.js';
|
||||
|
||||
function toggle(group: string) {
|
||||
layout.toggleGroupHidden(group);
|
||||
}
|
||||
|
||||
let hasHidden = $derived(layout.symbols.some(s => s.hidden));
|
||||
</script>
|
||||
|
||||
<div class="visibility-bar">
|
||||
{#each SYMBOL_GROUPS as group}
|
||||
<button
|
||||
class="group-chip"
|
||||
class:hidden-group={layout.hiddenGroups.has(group)}
|
||||
onclick={() => toggle(group)}
|
||||
title={layout.hiddenGroups.has(group) ? `Show ${group}` : `Hide ${group}`}
|
||||
>
|
||||
{group}
|
||||
</button>
|
||||
{/each}
|
||||
{#if hasHidden}
|
||||
<button class="show-all-btn" onclick={() => layout.showAllHidden()}>
|
||||
Show Hidden
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.visibility-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
background: #1a1a2e;
|
||||
border-bottom: 1px solid #0f3460;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.group-chip {
|
||||
padding: 3px 10px;
|
||||
font-size: 11px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #0f3460;
|
||||
background: #16213e;
|
||||
color: #e0e0e0;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.group-chip:hover {
|
||||
border-color: #e94560;
|
||||
}
|
||||
|
||||
.group-chip.hidden-group {
|
||||
background: transparent;
|
||||
color: #555;
|
||||
border-color: #333;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.show-all-btn {
|
||||
margin-left: auto;
|
||||
padding: 3px 10px;
|
||||
font-size: 11px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e94560;
|
||||
background: transparent;
|
||||
color: #e94560;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.show-all-btn:hover {
|
||||
background: #e94560;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
@ -1,6 +1,7 @@
|
||||
/** Pure hit-testing functions — no module state, no side effects */
|
||||
|
||||
import { isResizable, isCurvedType, isSpurType, isEpcType, isInductionType, EPC_CONFIG, INDUCTION_CONFIG, getCurveGeometry } from '../symbols.js';
|
||||
import { isResizable, isCurvedType, isSpurType, isEpcType, isInductionType, getSymbolGroup, EPC_CONFIG, INDUCTION_CONFIG, getCurveGeometry } from '../symbols.js';
|
||||
import { layout } from '../stores/layout.svelte.js';
|
||||
import { THEME } from './render-theme.js';
|
||||
import type { PlacedSymbol } from '../types.js';
|
||||
|
||||
@ -160,6 +161,7 @@ export function hitTestSymbols(
|
||||
): number | null {
|
||||
for (let i = symbols.length - 1; i >= 0; i--) {
|
||||
const sym = symbols[i];
|
||||
if (sym.hidden || layout.hiddenGroups.has(getSymbolGroup(sym.symbolId))) continue;
|
||||
const local = toSymbolLocal(cx, cy, sym);
|
||||
if (pointInSymbol(local.x, local.y, sym)) return sym.id;
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { layout } from '../stores/layout.svelte.js';
|
||||
import { getSymbolImage, isResizable, isCurvedType, isSpurType, isEpcType, isInductionType, isPhotoeyeType, getCurveGeometry, SPACING_EXEMPT, EPC_CONFIG, INDUCTION_CONFIG, PHOTOEYE_CONFIG } from '../symbols.js';
|
||||
import { getSymbolImage, isResizable, isCurvedType, isSpurType, isEpcType, isInductionType, isPhotoeyeType, getCurveGeometry, getSymbolGroup, SPACING_EXEMPT, EPC_CONFIG, INDUCTION_CONFIG, PHOTOEYE_CONFIG } from '../symbols.js';
|
||||
import { checkSpacingViolation } from './collision.js';
|
||||
import { THEME } from './render-theme.js';
|
||||
import type { PlacedSymbol } from '../types.js';
|
||||
@ -68,11 +68,14 @@ export function render() {
|
||||
drawGrid(ctx);
|
||||
}
|
||||
|
||||
// Draw non-overlay symbols first, then overlay symbols (photoeyes) on top
|
||||
// Draw non-overlay symbols first, then overlay symbols (photoeyes/FIOs) on top
|
||||
// Skip hidden symbols (individually hidden or group hidden)
|
||||
for (const sym of layout.symbols) {
|
||||
if (sym.hidden || layout.hiddenGroups.has(getSymbolGroup(sym.symbolId))) continue;
|
||||
if (!SPACING_EXEMPT.has(sym.symbolId)) drawSymbol(ctx, sym as PlacedSymbol);
|
||||
}
|
||||
for (const sym of layout.symbols) {
|
||||
if (sym.hidden || layout.hiddenGroups.has(getSymbolGroup(sym.symbolId))) continue;
|
||||
if (SPACING_EXEMPT.has(sym.symbolId)) drawSymbol(ctx, sym as PlacedSymbol);
|
||||
}
|
||||
|
||||
@ -487,14 +490,22 @@ function drawSymbolOverlays(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) {
|
||||
strokeOutline(ctx, sym, 0);
|
||||
}
|
||||
|
||||
// Label above symbol
|
||||
// Label: centered inside symbol if large enough, above if too small
|
||||
if (sym.label) {
|
||||
ctx.fillStyle = THEME.label.color;
|
||||
ctx.font = THEME.label.font;
|
||||
const metrics = ctx.measureText(sym.label);
|
||||
const textH = 10; // approximate font height
|
||||
if (sym.w >= metrics.width + 4 && sym.h >= textH + 4) {
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(sym.label, cx, sym.y + sym.h / 2);
|
||||
} else {
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'bottom';
|
||||
ctx.fillText(sym.label, cx, sym.y + THEME.label.offsetY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawSymbol(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { layout } from './stores/layout.svelte.js';
|
||||
import { isEpcType, isInductionType, isSpurType, isCurvedType, EPC_CONFIG, INDUCTION_CONFIG, getCurveGeometry } from './symbols.js';
|
||||
import { isEpcType, isInductionType, isSpurType, isCurvedType, getSymbolGroup, EPC_CONFIG, INDUCTION_CONFIG, getCurveGeometry } from './symbols.js';
|
||||
import { deserializeSymbol } from './serialization.js';
|
||||
import type { PlacedSymbol } from './types.js';
|
||||
|
||||
@ -30,6 +30,7 @@ export async function exportSVG() {
|
||||
];
|
||||
|
||||
for (const sym of layout.symbols) {
|
||||
if (sym.hidden || layout.hiddenGroups.has(getSymbolGroup(sym.symbolId))) continue;
|
||||
const rot = sym.rotation || 0;
|
||||
const mirrored = sym.mirrored || false;
|
||||
const cx = sym.x + sym.w / 2;
|
||||
|
||||
@ -35,6 +35,9 @@ class LayoutStore {
|
||||
// Label-drag drop target highlight (symbol id or null)
|
||||
labelDropTarget = $state<number | null>(null);
|
||||
|
||||
// Visibility: hidden device type groups and count of individually hidden symbols
|
||||
hiddenGroups = $state<Set<string>>(new Set());
|
||||
|
||||
// Dirty flag for canvas re-render
|
||||
dirty = $state(0);
|
||||
|
||||
@ -204,6 +207,58 @@ class LayoutStore {
|
||||
this.saveMcmState();
|
||||
}
|
||||
|
||||
toggleSymbolHidden(id: number) {
|
||||
const sym = this.symbols.find(s => s.id === id);
|
||||
if (!sym) return;
|
||||
sym.hidden = !sym.hidden;
|
||||
if (this.selectedIds.has(id)) {
|
||||
const newSet = new Set(this.selectedIds);
|
||||
newSet.delete(id);
|
||||
this.selectedIds = newSet;
|
||||
}
|
||||
this.markDirty();
|
||||
this.saveMcmState();
|
||||
}
|
||||
|
||||
hideSelected() {
|
||||
if (this.selectedIds.size === 0) return;
|
||||
for (const id of this.selectedIds) {
|
||||
const sym = this.symbols.find(s => s.id === id);
|
||||
if (sym) sym.hidden = true;
|
||||
}
|
||||
this.selectedIds = new Set();
|
||||
this.markDirty();
|
||||
this.saveMcmState();
|
||||
}
|
||||
|
||||
showAllHidden() {
|
||||
let changed = false;
|
||||
for (const sym of this.symbols) {
|
||||
if (sym.hidden) {
|
||||
sym.hidden = false;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (this.hiddenGroups.size > 0) {
|
||||
this.hiddenGroups = new Set();
|
||||
changed = true;
|
||||
}
|
||||
if (changed) {
|
||||
this.markDirty();
|
||||
}
|
||||
}
|
||||
|
||||
toggleGroupHidden(group: string) {
|
||||
const newSet = new Set(this.hiddenGroups);
|
||||
if (newSet.has(group)) {
|
||||
newSet.delete(group);
|
||||
} else {
|
||||
newSet.add(group);
|
||||
}
|
||||
this.hiddenGroups = newSet;
|
||||
this.markDirty();
|
||||
}
|
||||
|
||||
clearAll() {
|
||||
this.pushUndo();
|
||||
this.symbols = [];
|
||||
|
||||
@ -72,6 +72,11 @@ export const SYMBOLS: SymbolDef[] = [
|
||||
|
||||
export const SYMBOL_GROUPS = [...new Set(SYMBOLS.map(s => s.group))];
|
||||
|
||||
const SYMBOL_GROUP_MAP = new Map(SYMBOLS.map(s => [s.id, s.group]));
|
||||
export function getSymbolGroup(symbolId: string): string {
|
||||
return SYMBOL_GROUP_MAP.get(symbolId) || '';
|
||||
}
|
||||
|
||||
export const PRIORITY_TYPES = new Set([
|
||||
'conveyor', 'conveyor_v', 'chute', 'chute_v',
|
||||
'tipper', 'tipper_v', 'extendo', 'extendo_v',
|
||||
|
||||
@ -29,6 +29,7 @@ export interface PlacedSymbol {
|
||||
w2?: number; // Spur: top base width
|
||||
rotation: number;
|
||||
mirrored?: boolean; // Horizontal flip
|
||||
hidden?: boolean; // Individually hidden via context menu
|
||||
curveAngle?: number; // For curved conveyors/chutes
|
||||
epcWaypoints?: EpcWaypoint[]; // EPC editable line waypoints (local coords)
|
||||
pdpCBs?: number[]; // PDP visible circuit breaker numbers (1-26)
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { preloadSymbolImages } from '$lib/symbols.js';
|
||||
import Toolbar from '../components/Toolbar.svelte';
|
||||
import VisibilityBar from '../components/VisibilityBar.svelte';
|
||||
import Canvas from '../components/Canvas.svelte';
|
||||
import DeviceDock from '../components/DeviceDock.svelte';
|
||||
|
||||
@ -20,7 +21,10 @@
|
||||
{#if ready}
|
||||
<div class="app">
|
||||
<Toolbar />
|
||||
<div class="main-area">
|
||||
<VisibilityBar />
|
||||
<Canvas />
|
||||
</div>
|
||||
<DeviceDock />
|
||||
</div>
|
||||
{:else}
|
||||
@ -37,6 +41,13 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user