import { useMemo, useState, useSyncExternalStore } from "react"; import type { KeyboardEvent as ReactKeyboardEvent, PointerEvent as ReactPointerEvent } from "react"; import { createSchemetaStore } from "../state/store.js"; import type { SchemetaInstance, SchemetaModel, SchemetaStore } from "../state/store.js"; const MIN_SCALE = 0.4; const MAX_SCALE = 2.5; const SCALE_STEP = 0.1; const AUTO_COLUMNS = 4; const AUTO_X_START = 60; const AUTO_Y_START = 60; const AUTO_X_GAP = 210; const AUTO_Y_GAP = 150; const NUDGE_STEP = 10; const NUDGE_STEP_FAST = 20; const BOOTSTRAP_MODEL: SchemetaModel = { symbols: { mcu: { body: { width: 180, height: 140 }, pins: [ { name: "3V3", number: "1", side: "left", offset: 28 }, { name: "GND", number: "2", side: "left", offset: 56 }, { name: "GPIO1", number: "6", side: "right", offset: 40 }, { name: "GPIO2", number: "7", side: "right", offset: 72 } ] }, sensor: { body: { width: 150, height: 110 }, pins: [ { name: "3V3", number: "1", side: "left", offset: 30 }, { name: "GND", number: "2", side: "left", offset: 58 }, { name: "SCL", number: "3", side: "right", offset: 40 }, { name: "SDA", number: "4", side: "right", offset: 68 } ] }, conn: { body: { width: 130, height: 90 }, pins: [ { name: "TX", number: "1", side: "right", offset: 30 }, { name: "RX", number: "2", side: "right", offset: 56 } ] } }, instances: [ { ref: "U1", symbol: "mcu", properties: { value: "Control MCU" }, placement: { x: 120, y: 120, rotation: 0 } }, { ref: "U2", symbol: "sensor", properties: { value: "IMU" }, placement: { x: 420, y: 130, rotation: 0 } }, { ref: "U3", symbol: "sensor", properties: { value: "Temp Sensor" }, placement: { x: null, y: null, rotation: 0 } }, { ref: "J1", symbol: "conn", properties: { value: "Debug Header" }, placement: { x: 180, y: 320, rotation: 0 } } ], nets: [] }; type SymbolPin = { name?: string; number?: string; side?: string; offset?: number; }; type NodePlacement = { x: number; y: number; width: number; height: number; rotation: number; locked: boolean; leftPins: SymbolPin[]; rightPins: SymbolPin[]; }; type CanvasPoint = { x: number; y: number }; type CanvasWireSegment = { a: CanvasPoint; b: CanvasPoint; }; type CanvasWire = { id: string; label: string; netClass: string | null; segments: CanvasWireSegment[]; labelPoint: CanvasPoint | null; highlight: boolean; source: "routed" | "fallback"; }; type DragState = { refs: string[]; pointerId: number; startClientX: number; startClientY: number; starts: Map; }; type BoxSelectState = { pointerId: number; start: CanvasPoint; current: CanvasPoint; }; const fallbackStore = createSchemetaStore({ model: BOOTSTRAP_MODEL, viewport: { scale: 1, panX: 30, panY: 30 }, selection: { selectedRefs: [], selectedNet: null, selectedPin: null } }); function useStoreState(activeStore: SchemetaStore): ReturnType { return useSyncExternalStore( (listener: () => void) => activeStore.subscribe(() => listener()), () => activeStore.getState(), () => activeStore.getState() ); } function toFinite(value: unknown, fallback: number) { return typeof value === "number" && Number.isFinite(value) ? value : fallback; } function toDimension(value: unknown, fallback: number, min = 80, max = 320) { const parsed = toFinite(value, fallback); return Math.max(min, Math.min(max, Math.round(parsed))); } function asRecord(value: unknown): Record | null { if (typeof value !== "object" || value === null) { return null; } return value as Record; } function asPoint(value: unknown): CanvasPoint | null { if (Array.isArray(value) && value.length >= 2) { const x = Number(value[0]); const y = Number(value[1]); if (Number.isFinite(x) && Number.isFinite(y)) { return { x, y }; } } const record = asRecord(value); if (!record) { return null; } const x = Number(record.x); const y = Number(record.y); if (Number.isFinite(x) && Number.isFinite(y)) { return { x, y }; } return null; } function rotatePoint(point: CanvasPoint, center: CanvasPoint, rotationDeg: number): CanvasPoint { if (!Number.isFinite(rotationDeg) || rotationDeg === 0) { return point; } const radians = (rotationDeg * Math.PI) / 180; const cos = Math.cos(radians); const sin = Math.sin(radians); const dx = point.x - center.x; const dy = point.y - center.y; return { x: center.x + dx * cos - dy * sin, y: center.y + dx * sin + dy * cos }; } function pointsToSegments(points: CanvasPoint[]): CanvasWireSegment[] { const segments: CanvasWireSegment[] = []; for (let index = 1; index < points.length; index += 1) { const a = points[index - 1]; const b = points[index]; if (a.x === b.x && a.y === b.y) { continue; } segments.push({ a, b }); } return segments; } function asSegment(value: unknown): CanvasWireSegment | null { const record = asRecord(value); if (!record) { return null; } const a = asPoint(record.a ?? record.start ?? record.from); const b = asPoint(record.b ?? record.end ?? record.to); if (a && b) { return { a, b }; } const x1 = Number(record.x1); const y1 = Number(record.y1); const x2 = Number(record.x2); const y2 = Number(record.y2); if (Number.isFinite(x1) && Number.isFinite(y1) && Number.isFinite(x2) && Number.isFinite(y2)) { return { a: { x: x1, y: y1 }, b: { x: x2, y: y2 } }; } return null; } function getSegmentsFromUnknown(value: unknown): CanvasWireSegment[] { if (!Array.isArray(value)) { return []; } const points = value.map((entry) => asPoint(entry)).filter((entry): entry is CanvasPoint => entry !== null); if (points.length >= 2 && points.length === value.length) { return pointsToSegments(points); } const segments: CanvasWireSegment[] = []; value.forEach((entry) => { const segment = asSegment(entry); if (segment) { segments.push(segment); return; } const nested = asRecord(entry); if (!nested) { return; } const nestedSegments = [ ...getSegmentsFromUnknown(nested.segments), ...getSegmentsFromUnknown(nested.path), ...getSegmentsFromUnknown(nested.polyline), ...getSegmentsFromUnknown(nested.points) ]; segments.push(...nestedSegments); }); return segments; } function segmentLengthSquared(segment: CanvasWireSegment): number { const dx = segment.b.x - segment.a.x; const dy = segment.b.y - segment.a.y; return dx * dx + dy * dy; } function segmentMidpoint(segment: CanvasWireSegment): CanvasPoint { return { x: (segment.a.x + segment.b.x) / 2, y: (segment.a.y + segment.b.y) / 2 }; } function pickLabelPoint(segments: CanvasWireSegment[], preferred: CanvasPoint | null): CanvasPoint | null { if (preferred) { return preferred; } if (segments.length === 0) { return null; } let longest = segments[0]; let bestLength = segmentLengthSquared(segments[0]); for (let index = 1; index < segments.length; index += 1) { const candidateLength = segmentLengthSquared(segments[index]); if (candidateLength > bestLength) { bestLength = candidateLength; longest = segments[index]; } } return segmentMidpoint(longest); } function getSymbolNode(symbols: SchemetaModel["symbols"], instance: SchemetaInstance) { if (!instance.symbol || !symbols || typeof symbols !== "object") { return null; } const candidate = symbols[instance.symbol]; return candidate && typeof candidate === "object" ? candidate : null; } function getNodePlacement(instance: SchemetaInstance, index: number, symbols: SchemetaModel["symbols"]): NodePlacement { const col = index % AUTO_COLUMNS; const row = Math.floor(index / AUTO_COLUMNS); const symbol = getSymbolNode(symbols, instance) as | { body?: { width?: number; height?: number }; pins?: SymbolPin[] } | null; const width = toDimension(symbol?.body?.width, 150); const height = toDimension(symbol?.body?.height, 100); const x = toFinite(instance.placement?.x, AUTO_X_START + col * AUTO_X_GAP); const y = toFinite(instance.placement?.y, AUTO_Y_START + row * AUTO_Y_GAP); const rotation = toFinite(instance.placement?.rotation, 0); const locked = Boolean(instance.placement?.locked); const pins = Array.isArray(symbol?.pins) ? symbol.pins : []; const leftPins = pins.filter((pin) => pin?.side === "left").slice(0, 6); const rightPins = pins.filter((pin) => pin?.side === "right").slice(0, 6); return { x, y, width, height, rotation, locked, leftPins, rightPins }; } function toNetIdentity(netRecord: Record, index: number) { const named = typeof netRecord.name === "string" && netRecord.name.trim().length > 0 ? netRecord.name.trim() : null; return { id: named ?? `net-${index + 1}`, label: named ?? `Net ${index + 1}` }; } function getPinAnchor( placement: NodePlacement, instance: SchemetaInstance, symbols: SchemetaModel["symbols"], pinName: string | null ): CanvasPoint { const symbol = getSymbolNode(symbols, instance) as { pins?: SymbolPin[] } | null; const pins = Array.isArray(symbol?.pins) ? symbol.pins : []; const normalizedPin = pinName?.trim() ?? ""; const pin = pins.find((candidate) => { const candidateName = typeof candidate?.name === "string" ? candidate.name : ""; const candidateNumber = typeof candidate?.number === "string" ? candidate.number : ""; return normalizedPin.length > 0 && (candidateName === normalizedPin || candidateNumber === normalizedPin); }); const offset = Math.max(0, Math.min(placement.height, toFinite(pin?.offset, placement.height * 0.5))); let anchor: CanvasPoint; switch (pin?.side) { case "left": anchor = { x: placement.x, y: placement.y + offset }; break; case "top": anchor = { x: placement.x + placement.width * 0.5, y: placement.y }; break; case "bottom": anchor = { x: placement.x + placement.width * 0.5, y: placement.y + placement.height }; break; case "right": default: anchor = { x: placement.x + placement.width, y: placement.y + offset }; break; } const center = { x: placement.x + placement.width / 2, y: placement.y + placement.height / 2 }; return rotatePoint(anchor, center, placement.rotation); } function extractRoutedWires(compileResult: unknown, selectedNet: string | null): CanvasWire[] { const compile = asRecord(compileResult); if (!compile) { return []; } const layout = asRecord(compile.layout); const topology = asRecord(compile.topology); const candidates: unknown[] = [layout?.routed, layout?.routes, compile.routed, compile.routes, topology?.routed]; for (const candidate of candidates) { if (!Array.isArray(candidate) || candidate.length === 0) { continue; } const parsed: CanvasWire[] = []; candidate.forEach((entry, index) => { const route = asRecord(entry); if (!route) { return; } const netRecord = asRecord(route.net); const netNameRaw = (typeof netRecord?.name === "string" && netRecord.name.trim().length > 0 ? netRecord.name.trim() : null) ?? (typeof route.net_name === "string" && route.net_name.trim().length > 0 ? route.net_name.trim() : null) ?? (typeof route.net === "string" && route.net.trim().length > 0 ? route.net.trim() : null) ?? (typeof route.name === "string" && route.name.trim().length > 0 ? route.name.trim() : null); const id = netNameRaw ?? `net-${index + 1}`; const label = netNameRaw ?? `Net ${index + 1}`; const netClass = (typeof netRecord?.class === "string" && netRecord.class.trim().length > 0 ? netRecord.class.trim() : null) ?? (typeof route.class === "string" && route.class.trim().length > 0 ? route.class.trim() : null); const segments = [ ...getSegmentsFromUnknown(route.routes), ...getSegmentsFromUnknown(route.segments), ...getSegmentsFromUnknown(route.path), ...getSegmentsFromUnknown(route.polyline), ...getSegmentsFromUnknown(route.points) ]; if (segments.length === 0) { return; } const preferredPoint = asPoint(Array.isArray(route.labelPoints) ? route.labelPoints[0] : null) ?? asPoint(Array.isArray(route.tiePoints) ? route.tiePoints[0] : null) ?? asPoint(Array.isArray(route.junctionPoints) ? route.junctionPoints[0] : null); parsed.push({ id, label, netClass, segments, labelPoint: pickLabelPoint(segments, preferredPoint), highlight: selectedNet !== null && selectedNet === id, source: "routed" }); }); if (parsed.length > 0) { return parsed; } } return []; } function buildFallbackWires( nets: Record[], placements: Map, instancesByRef: Map, symbols: SchemetaModel["symbols"], selectedNet: string | null ): CanvasWire[] { const wires: CanvasWire[] = []; nets.forEach((net, index) => { const { id, label } = toNetIdentity(net, index); const netClass = typeof net.class === "string" && net.class.trim().length > 0 ? net.class.trim() : null; const nodesRaw = Array.isArray(net.nodes) ? net.nodes : []; const points = nodesRaw .map((node) => { const endpoint = asRecord(node); if (!endpoint || typeof endpoint.ref !== "string") { return null; } const placement = placements.get(endpoint.ref); const instance = instancesByRef.get(endpoint.ref); if (!placement || !instance) { return null; } const pinName = typeof endpoint.pin === "string" && endpoint.pin.trim().length > 0 ? endpoint.pin : null; return getPinAnchor(placement, instance, symbols, pinName); }) .filter((value): value is CanvasPoint => value !== null); if (points.length < 2) { return; } const root = points[0]; const segments = points.slice(1).map((point) => ({ a: root, b: point })); if (segments.length === 0) { return; } wires.push({ id, label, netClass, segments, labelPoint: pickLabelPoint(segments, null), highlight: selectedNet !== null && selectedNet === id, source: "fallback" }); }); return wires; } function formatZoom(scale: number) { return `${Math.round(scale * 100)}%`; } type CanvasAreaProps = { store?: SchemetaStore; }; function shouldIgnoreKeyboardTarget(target: EventTarget | null): boolean { if (!(target instanceof HTMLElement)) { return false; } const tag = target.tagName.toLowerCase(); return tag === "input" || tag === "textarea" || target.isContentEditable; } function toggleRefSelection(selectedRefs: string[], ref: string): string[] { const set = new Set(selectedRefs); if (set.has(ref)) { set.delete(ref); } else { set.add(ref); } return Array.from(set).sort((a, b) => a.localeCompare(b)); } function normalizeRotation(value: unknown): number { const numeric = Number(value); if (!Number.isFinite(numeric)) { return 0; } const normalized = ((Math.round(numeric / 90) * 90) % 360 + 360) % 360; return normalized; } function removeRefsFromModel(model: SchemetaModel, selectedRefs: string[]): SchemetaModel { if (!Array.isArray(model.instances) || selectedRefs.length === 0) { return model; } const refSet = new Set(selectedRefs); const instances = model.instances.filter((instance) => !refSet.has(instance.ref)); const nets = Array.isArray(model.nets) ? model.nets .map((net) => { if (!Array.isArray(net.nodes)) { return net; } const nodes = net.nodes.filter((node) => { const endpoint = asRecord(node); return !(endpoint && typeof endpoint.ref === "string" && refSet.has(endpoint.ref)); }); return { ...net, nodes }; }) .filter((net) => Array.isArray(net.nodes) && net.nodes.length > 1) : model.nets; return { ...model, instances, nets }; } export function CanvasArea({ store: providedStore }: CanvasAreaProps) { const store = providedStore ?? fallbackStore; const state = useStoreState(store); const [drag, setDrag] = useState(null); const [boxSelect, setBoxSelect] = useState(null); const instances = state.model?.instances ?? []; const symbols = state.model?.symbols ?? {}; const selectedRefs = state.selection.selectedRefs; const selectedSet = useMemo(() => new Set(selectedRefs), [selectedRefs]); const selectedNet = state.selection.selectedNet; const placements = useMemo(() => { const byRef = new Map(); instances.forEach((instance, index) => { byRef.set(instance.ref, getNodePlacement(instance, index, symbols)); }); return byRef; }, [instances, symbols]); const instancesByRef = useMemo(() => { return new Map(instances.map((instance) => [instance.ref, instance])); }, [instances]); const modelNets = useMemo( () => (state.model?.nets ?? []).filter((item): item is Record => typeof item === "object" && item !== null), [state.model?.nets] ); const routedWires = useMemo(() => extractRoutedWires(state.compileResult, selectedNet), [state.compileResult, selectedNet]); const fallbackWires = useMemo( () => buildFallbackWires(modelNets, placements, instancesByRef, symbols, selectedNet), [instancesByRef, modelNets, placements, selectedNet, symbols] ); const hasRoutedWires = routedWires.length > 0; const visibleWires = hasRoutedWires ? routedWires : fallbackWires; const hasSelectedNet = selectedNet !== null; function clearSelection() { store.actions.setSelection({ selectedRefs: [], selectedNet: null, selectedPin: null }); store.actions.setUiFlags({ isolateComponent: false, isolateNet: false }); } function updateScale(nextScale: number) { const clamped = Math.max(MIN_SCALE, Math.min(MAX_SCALE, nextScale)); store.actions.setViewport({ scale: clamped }); } function eventToCanvasPoint(event: ReactPointerEvent): CanvasPoint { const surface = event.currentTarget; const rect = surface.getBoundingClientRect(); const localX = event.clientX - rect.left + surface.scrollLeft; const localY = event.clientY - rect.top + surface.scrollTop; return { x: (localX - state.viewport.panX) / state.viewport.scale, y: (localY - state.viewport.panY) / state.viewport.scale }; } function beginDrag(refs: string[], event: ReactPointerEvent) { const starts = new Map(); refs.forEach((ref) => { const placement = placements.get(ref); if (!placement || placement.locked) { return; } starts.set(ref, { x: placement.x, y: placement.y }); }); if (starts.size === 0) { return; } store.actions.beginTransaction(starts.size > 1 ? "drag components" : "drag component"); event.currentTarget.setPointerCapture(event.pointerId); setDrag({ refs: Array.from(starts.keys()), pointerId: event.pointerId, startClientX: event.clientX, startClientY: event.clientY, starts }); } function handleCanvasPointerDown(event: ReactPointerEvent) { if (event.button !== 0 || event.target !== event.currentTarget) { return; } event.currentTarget.focus(); const additive = event.shiftKey || event.metaKey || event.ctrlKey; if (!additive) { clearSelection(); } const point = eventToCanvasPoint(event); event.currentTarget.setPointerCapture(event.pointerId); setBoxSelect({ pointerId: event.pointerId, start: point, current: point }); } function handleCanvasPointerMove(event: ReactPointerEvent) { if (!boxSelect || boxSelect.pointerId !== event.pointerId) { return; } event.preventDefault(); const point = eventToCanvasPoint(event); setBoxSelect((prev) => { if (!prev) { return null; } return { ...prev, current: point }; }); } function handleCanvasPointerUpOrCancel(event: ReactPointerEvent) { if (!boxSelect || boxSelect.pointerId !== event.pointerId) { return; } if (event.currentTarget.hasPointerCapture(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId); } const additive = event.shiftKey || event.metaKey || event.ctrlKey; const minX = Math.min(boxSelect.start.x, boxSelect.current.x); const minY = Math.min(boxSelect.start.y, boxSelect.current.y); const maxX = Math.max(boxSelect.start.x, boxSelect.current.x); const maxY = Math.max(boxSelect.start.y, boxSelect.current.y); const width = maxX - minX; const height = maxY - minY; const nextSelected = width < 4 && height < 4 ? [] : instances .map((instance, index) => ({ ref: instance.ref, placement: placements.get(instance.ref) ?? getNodePlacement(instance, index, symbols) })) .filter((entry) => { const nodeMinX = entry.placement.x; const nodeMinY = entry.placement.y; const nodeMaxX = entry.placement.x + entry.placement.width; const nodeMaxY = entry.placement.y + entry.placement.height; return !(nodeMaxX < minX || nodeMinX > maxX || nodeMaxY < minY || nodeMinY > maxY); }) .map((entry) => entry.ref) .sort((a, b) => a.localeCompare(b)); if (nextSelected.length > 0 || !additive) { const merged = additive ? Array.from(new Set([...state.selection.selectedRefs, ...nextSelected])) : nextSelected; store.actions.setSelection({ selectedRefs: merged.sort((a, b) => a.localeCompare(b)), selectedNet: null, selectedPin: null }); if (merged.length > 0) { store.actions.setUiFlags({ isolateComponent: false, isolateNet: false }); } } setBoxSelect(null); } function handleNodePointerDown(event: ReactPointerEvent, ref: string) { if (event.button !== 0) { return; } event.stopPropagation(); event.currentTarget.focus(); const additive = event.shiftKey || event.metaKey || event.ctrlKey; if (additive) { store.actions.setSelection({ selectedRefs: toggleRefSelection(state.selection.selectedRefs, ref), selectedNet: null, selectedPin: null }); store.actions.setUiFlags({ isolateComponent: false, isolateNet: false }); return; } const isAlreadySelected = selectedSet.has(ref); const dragCandidates = isAlreadySelected && selectedRefs.length > 1 ? selectedRefs : [ref]; if (!isAlreadySelected || selectedRefs.length !== dragCandidates.length) { store.actions.setSelection({ selectedRefs: dragCandidates, selectedNet: null, selectedPin: null }); store.actions.setUiFlags({ isolateComponent: false, isolateNet: false }); } beginDrag(dragCandidates, event); } function handleNodePointerMove(event: ReactPointerEvent) { if (!drag || drag.pointerId !== event.pointerId) { return; } event.preventDefault(); const dx = (event.clientX - drag.startClientX) / state.viewport.scale; const dy = (event.clientY - drag.startClientY) / state.viewport.scale; drag.refs.forEach((ref) => { const origin = drag.starts.get(ref); if (!origin) { return; } store.actions.moveComponent(ref, { x: origin.x + dx, y: origin.y + dy }); }); } function handleNodePointerUpOrCancel(event: ReactPointerEvent) { if (!drag || drag.pointerId !== event.pointerId) { return; } if (event.currentTarget.hasPointerCapture(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId); } setDrag(null); store.actions.commitTransaction(); } function rotateSelected() { if (selectedRefs.length === 0) { return; } const refs = selectedRefs.filter((ref) => { const placement = placements.get(ref); return Boolean(placement && !placement.locked); }); if (refs.length === 0) { return; } store.actions.beginTransaction(refs.length > 1 ? "rotate components" : "rotate component"); refs.forEach((ref) => { const instance = instancesByRef.get(ref); const current = normalizeRotation(instance?.placement?.rotation ?? 0); store.actions.moveComponent(ref, { rotation: normalizeRotation(current + 90) }); }); store.actions.commitTransaction(); } function nudgeSelected(dx: number, dy: number) { if (selectedRefs.length === 0) { return; } const refs = selectedRefs.filter((ref) => { const placement = placements.get(ref); return Boolean(placement && !placement.locked); }); if (refs.length === 0) { return; } store.actions.beginTransaction(refs.length > 1 ? "nudge components" : "nudge component"); refs.forEach((ref) => { const placement = placements.get(ref); if (!placement) { return; } store.actions.moveComponent(ref, { x: placement.x + dx, y: placement.y + dy }); }); store.actions.commitTransaction(); } function deleteSelected() { if (!state.model || selectedRefs.length === 0) { return; } const nextModel = removeRefsFromModel(state.model, selectedRefs); store.actions.beginTransaction(selectedRefs.length > 1 ? "delete components" : "delete component"); store.actions.setModel(nextModel); clearSelection(); store.actions.commitTransaction(); } function handleCanvasKeyDown(event: ReactKeyboardEvent) { if (shouldIgnoreKeyboardTarget(event.target)) { return; } const hasCommand = event.ctrlKey || event.metaKey; if (hasCommand && event.key.toLowerCase() === "a") { event.preventDefault(); store.actions.setSelection({ selectedRefs: instances.map((instance) => instance.ref).sort((a, b) => a.localeCompare(b)), selectedNet: null, selectedPin: null }); store.actions.setUiFlags({ isolateComponent: false, isolateNet: false }); return; } if (event.key === "Escape") { event.preventDefault(); clearSelection(); return; } if (event.key === "Delete" || event.key === "Backspace") { event.preventDefault(); deleteSelected(); return; } if (event.code === "Space") { event.preventDefault(); rotateSelected(); return; } const step = event.shiftKey ? NUDGE_STEP_FAST : NUDGE_STEP; switch (event.key) { case "ArrowUp": event.preventDefault(); nudgeSelected(0, -step); break; case "ArrowDown": event.preventDefault(); nudgeSelected(0, step); break; case "ArrowLeft": event.preventDefault(); nudgeSelected(-step, 0); break; case "ArrowRight": event.preventDefault(); nudgeSelected(step, 0); break; default: break; } } function selectNet(netId: string | null) { store.actions.setSelection({ selectedRefs: [], selectedNet: netId, selectedPin: null }); store.actions.setUiFlags({ isolateComponent: false, isolateNet: false }); } const selectionBoxStyle = useMemo(() => { if (!boxSelect) { return null; } const left = Math.min(boxSelect.start.x, boxSelect.current.x); const top = Math.min(boxSelect.start.y, boxSelect.current.y); const width = Math.max(1, Math.abs(boxSelect.current.x - boxSelect.start.x)); const height = Math.max(1, Math.abs(boxSelect.current.y - boxSelect.start.y)); return { left: `${left}px`, top: `${top}px`, width: `${width}px`, height: `${height}px` }; }, [boxSelect]); return (
Zoom {formatZoom(state.viewport.scale)}
{state.uiFlags.showLabels ? visibleWires.map((wire) => { if (!wire.labelPoint) { return null; } const isDimmed = hasSelectedNet && !wire.highlight; return ( ); }) : null} {selectionBoxStyle ?
: null} {instances.map((instance, index) => { const placement = placements.get(instance.ref) ?? getNodePlacement(instance, index, symbols); const isSelected = selectedSet.has(instance.ref); const valueLabel = typeof instance.properties?.value === "string" && instance.properties.value.length > 0 ? instance.properties.value : instance.symbol ?? instance.part ?? "Component"; return (
handleNodePointerDown(event, instance.ref)} onPointerMove={handleNodePointerMove} onPointerUp={handleNodePointerUpOrCancel} onPointerCancel={handleNodePointerUpOrCancel} tabIndex={-1} >
{instance.ref}
{valueLabel}
{placement.leftPins.map((pin) => (
{pin.number ?? "-"} {pin.name ?? "PIN"}
))}
{placement.rightPins.map((pin) => (
{pin.name ?? "PIN"} {pin.number ?? "-"}
))}
{placement.locked ?
Locked
: null}
); })}
); }