diff --git a/svelte-app/src/components/Canvas.svelte b/svelte-app/src/components/Canvas.svelte index 15e9a54..dbcf9b8 100644 --- a/svelte-app/src/components/Canvas.svelte +++ b/svelte-app/src/components/Canvas.svelte @@ -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 @@ + + {#if layout.symbols.some(s => s.hidden) || layout.hiddenGroups.size > 0} +
+ + {/if} {/if} @@ -222,4 +242,10 @@ .context-menu-item:hover { background: #e94560; } + + .context-menu-divider { + height: 1px; + background: #0f3460; + margin: 4px 0; + } diff --git a/svelte-app/src/components/VisibilityBar.svelte b/svelte-app/src/components/VisibilityBar.svelte new file mode 100644 index 0000000..e68a5da --- /dev/null +++ b/svelte-app/src/components/VisibilityBar.svelte @@ -0,0 +1,80 @@ + + + + + diff --git a/svelte-app/src/lib/canvas/hit-testing.ts b/svelte-app/src/lib/canvas/hit-testing.ts index 32c875b..9efcb97 100644 --- a/svelte-app/src/lib/canvas/hit-testing.ts +++ b/svelte-app/src/lib/canvas/hit-testing.ts @@ -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; } diff --git a/svelte-app/src/lib/canvas/renderer.ts b/svelte-app/src/lib/canvas/renderer.ts index e263975..549a78a 100644 --- a/svelte-app/src/lib/canvas/renderer.ts +++ b/svelte-app/src/lib/canvas/renderer.ts @@ -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,13 +490,21 @@ 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; - ctx.textAlign = 'center'; - ctx.textBaseline = 'bottom'; - ctx.fillText(sym.label, cx, sym.y + THEME.label.offsetY); + 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); + } } } diff --git a/svelte-app/src/lib/export.ts b/svelte-app/src/lib/export.ts index 945bc98..c341051 100644 --- a/svelte-app/src/lib/export.ts +++ b/svelte-app/src/lib/export.ts @@ -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; diff --git a/svelte-app/src/lib/stores/layout.svelte.ts b/svelte-app/src/lib/stores/layout.svelte.ts index 2352bed..d8d99ae 100644 --- a/svelte-app/src/lib/stores/layout.svelte.ts +++ b/svelte-app/src/lib/stores/layout.svelte.ts @@ -35,6 +35,9 @@ class LayoutStore { // Label-drag drop target highlight (symbol id or null) labelDropTarget = $state