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;
|
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() {
|
function handleSetLabel() {
|
||||||
if (!contextMenu) return;
|
if (!contextMenu) return;
|
||||||
const sym = layout.symbols.find(s => s.id === contextMenu!.symId);
|
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={handleSetLabel} role="menuitem">Set ID</button>
|
||||||
<button class="context-menu-item" onclick={handleMirror} role="menuitem">Mirror</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={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>
|
<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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@ -222,4 +242,10 @@
|
|||||||
.context-menu-item:hover {
|
.context-menu-item:hover {
|
||||||
background: #e94560;
|
background: #e94560;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.context-menu-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #0f3460;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
</style>
|
</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 */
|
/** 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 { THEME } from './render-theme.js';
|
||||||
import type { PlacedSymbol } from '../types.js';
|
import type { PlacedSymbol } from '../types.js';
|
||||||
|
|
||||||
@ -160,6 +161,7 @@ export function hitTestSymbols(
|
|||||||
): number | null {
|
): number | null {
|
||||||
for (let i = symbols.length - 1; i >= 0; i--) {
|
for (let i = symbols.length - 1; i >= 0; i--) {
|
||||||
const sym = symbols[i];
|
const sym = symbols[i];
|
||||||
|
if (sym.hidden || layout.hiddenGroups.has(getSymbolGroup(sym.symbolId))) continue;
|
||||||
const local = toSymbolLocal(cx, cy, sym);
|
const local = toSymbolLocal(cx, cy, sym);
|
||||||
if (pointInSymbol(local.x, local.y, sym)) return sym.id;
|
if (pointInSymbol(local.x, local.y, sym)) return sym.id;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { layout } from '../stores/layout.svelte.js';
|
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 { checkSpacingViolation } from './collision.js';
|
||||||
import { THEME } from './render-theme.js';
|
import { THEME } from './render-theme.js';
|
||||||
import type { PlacedSymbol } from '../types.js';
|
import type { PlacedSymbol } from '../types.js';
|
||||||
@ -68,11 +68,14 @@ export function render() {
|
|||||||
drawGrid(ctx);
|
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) {
|
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);
|
if (!SPACING_EXEMPT.has(sym.symbolId)) drawSymbol(ctx, sym as PlacedSymbol);
|
||||||
}
|
}
|
||||||
for (const sym of layout.symbols) {
|
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);
|
if (SPACING_EXEMPT.has(sym.symbolId)) drawSymbol(ctx, sym as PlacedSymbol);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -487,13 +490,21 @@ function drawSymbolOverlays(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) {
|
|||||||
strokeOutline(ctx, sym, 0);
|
strokeOutline(ctx, sym, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Label above symbol
|
// Label: centered inside symbol if large enough, above if too small
|
||||||
if (sym.label) {
|
if (sym.label) {
|
||||||
ctx.fillStyle = THEME.label.color;
|
ctx.fillStyle = THEME.label.color;
|
||||||
ctx.font = THEME.label.font;
|
ctx.font = THEME.label.font;
|
||||||
ctx.textAlign = 'center';
|
const metrics = ctx.measureText(sym.label);
|
||||||
ctx.textBaseline = 'bottom';
|
const textH = 10; // approximate font height
|
||||||
ctx.fillText(sym.label, cx, sym.y + THEME.label.offsetY);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { layout } from './stores/layout.svelte.js';
|
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 { deserializeSymbol } from './serialization.js';
|
||||||
import type { PlacedSymbol } from './types.js';
|
import type { PlacedSymbol } from './types.js';
|
||||||
|
|
||||||
@ -30,6 +30,7 @@ export async function exportSVG() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for (const sym of layout.symbols) {
|
for (const sym of layout.symbols) {
|
||||||
|
if (sym.hidden || layout.hiddenGroups.has(getSymbolGroup(sym.symbolId))) continue;
|
||||||
const rot = sym.rotation || 0;
|
const rot = sym.rotation || 0;
|
||||||
const mirrored = sym.mirrored || false;
|
const mirrored = sym.mirrored || false;
|
||||||
const cx = sym.x + sym.w / 2;
|
const cx = sym.x + sym.w / 2;
|
||||||
|
|||||||
@ -35,6 +35,9 @@ class LayoutStore {
|
|||||||
// Label-drag drop target highlight (symbol id or null)
|
// Label-drag drop target highlight (symbol id or null)
|
||||||
labelDropTarget = $state<number | null>(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 flag for canvas re-render
|
||||||
dirty = $state(0);
|
dirty = $state(0);
|
||||||
|
|
||||||
@ -204,6 +207,58 @@ class LayoutStore {
|
|||||||
this.saveMcmState();
|
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() {
|
clearAll() {
|
||||||
this.pushUndo();
|
this.pushUndo();
|
||||||
this.symbols = [];
|
this.symbols = [];
|
||||||
|
|||||||
@ -72,6 +72,11 @@ export const SYMBOLS: SymbolDef[] = [
|
|||||||
|
|
||||||
export const SYMBOL_GROUPS = [...new Set(SYMBOLS.map(s => s.group))];
|
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([
|
export const PRIORITY_TYPES = new Set([
|
||||||
'conveyor', 'conveyor_v', 'chute', 'chute_v',
|
'conveyor', 'conveyor_v', 'chute', 'chute_v',
|
||||||
'tipper', 'tipper_v', 'extendo', 'extendo_v',
|
'tipper', 'tipper_v', 'extendo', 'extendo_v',
|
||||||
|
|||||||
@ -29,6 +29,7 @@ export interface PlacedSymbol {
|
|||||||
w2?: number; // Spur: top base width
|
w2?: number; // Spur: top base width
|
||||||
rotation: number;
|
rotation: number;
|
||||||
mirrored?: boolean; // Horizontal flip
|
mirrored?: boolean; // Horizontal flip
|
||||||
|
hidden?: boolean; // Individually hidden via context menu
|
||||||
curveAngle?: number; // For curved conveyors/chutes
|
curveAngle?: number; // For curved conveyors/chutes
|
||||||
epcWaypoints?: EpcWaypoint[]; // EPC editable line waypoints (local coords)
|
epcWaypoints?: EpcWaypoint[]; // EPC editable line waypoints (local coords)
|
||||||
pdpCBs?: number[]; // PDP visible circuit breaker numbers (1-26)
|
pdpCBs?: number[]; // PDP visible circuit breaker numbers (1-26)
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { preloadSymbolImages } from '$lib/symbols.js';
|
import { preloadSymbolImages } from '$lib/symbols.js';
|
||||||
import Toolbar from '../components/Toolbar.svelte';
|
import Toolbar from '../components/Toolbar.svelte';
|
||||||
|
import VisibilityBar from '../components/VisibilityBar.svelte';
|
||||||
import Canvas from '../components/Canvas.svelte';
|
import Canvas from '../components/Canvas.svelte';
|
||||||
import DeviceDock from '../components/DeviceDock.svelte';
|
import DeviceDock from '../components/DeviceDock.svelte';
|
||||||
|
|
||||||
@ -20,7 +21,10 @@
|
|||||||
{#if ready}
|
{#if ready}
|
||||||
<div class="app">
|
<div class="app">
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
<Canvas />
|
<div class="main-area">
|
||||||
|
<VisibilityBar />
|
||||||
|
<Canvas />
|
||||||
|
</div>
|
||||||
<DeviceDock />
|
<DeviceDock />
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
@ -37,6 +41,13 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-area {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user