"use strict"; // Resizable grid-based combinational logic board. Internal wiring is a graph of // straight segments. Junctions are explicit movable nodes and terminals may // fan out to any number of connections. (function (global) { const SIDES = Object.freeze(["left", "right", "top", "bottom"]); const GATE_TYPES = Object.freeze(["and", "not", "rectifier", "on"]); const GRID = 40; const DEFAULT_COLS = 10; const DEFAULT_ROWS = 10; const MIN_COLS = 6; const MAX_COLS = 20; const MIN_ROWS = 4; const MAX_ROWS = 20; const HALF_W = 32; const HALF_H = 32; const CANVAS_W = DEFAULT_COLS * GRID; const CANVAS_H = DEFAULT_ROWS * GRID; const MAX_NODES = 36; const MAX_WIRES = 384; const clone = value => { try { return JSON.parse(JSON.stringify(value)); } catch (_) { return null; } }; const clamp = global.TarinaiCoreHelpers?.clampNumber || ((v, a, b) => Math.max(a, Math.min(b, Number(v) || 0))); const safeRotation = value => ((Math.round(Number(value) || 0) % 4) + 4) % 4; const safeSide = value => SIDES.includes(value) ? value : "left"; const nodeType = value => GATE_TYPES.includes(value) ? value : "and"; const nodeInputCount = type => type === "and" ? 2 : ((type === "not" || type === "rectifier") ? 1 : 0); const safeCols = value => Math.round(clamp(value ?? DEFAULT_COLS, MIN_COLS, MAX_COLS)); const safeRows = value => Math.round(clamp(value ?? DEFAULT_ROWS, MIN_ROWS, MAX_ROWS)); const canvasWidth = config => safeCols(config?.cols) * GRID; const canvasHeight = config => safeRows(config?.rows) * GRID; const snap = value => Math.round((Number(value) || 0) / GRID) * GRID; const isVerticalSide = side => side === "left" || side === "right"; const sideExtent = (config, side) => isVerticalSide(side) ? canvasHeight(config) : canvasWidth(config); const safeOffset = (config, side, value) => { const extent = sideExtent(config, side); return clamp(snap(value), GRID, Math.max(GRID, extent - GRID)); }; const portLimit = config => Math.max(1, safeRows(config?.rows) - 1); const pointToken = (config, x, y) => `p:${Math.round(clamp(snap(x), GRID, canvasWidth(config) - GRID))}:${Math.round(clamp(snap(y), GRID, canvasHeight(config) - GRID))}`; function boardHalfWidth(boardOrConfig) { const config = boardOrConfig?.type === "circuit_board" ? ensureConfig(boardOrConfig) : (boardOrConfig || defaultConfig()); return 64 * safeCols(config.cols) / 20; } function boardHalfHeight(boardOrConfig) { const config = boardOrConfig?.type === "circuit_board" ? ensureConfig(boardOrConfig) : (boardOrConfig || defaultConfig()); return 64 * safeRows(config.rows) / 20; } function updateBoardPhysicalSize(board) { if (!board || board.type !== "circuit_board") return; const hw = boardHalfWidth(board.circuitConfig || defaultConfig()); const hh = boardHalfHeight(board.circuitConfig || defaultConfig()); board.r = Math.max(18, Math.hypot(hw, hh)); board.radius = board.r; } function parsePointToken(config, token) { const match = /^p:(-?\d+):(-?\d+)$/.exec(String(token || "")); if (!match) return null; return { x: clamp(snap(match[1]), GRID, canvasWidth(config) - GRID), y: clamp(snap(match[2]), GRID, canvasHeight(config) - GRID), }; } function spacedOffsets(config, side, count) { const extent = sideExtent(config, side); const usable = Math.max(GRID, extent - GRID * 2); const total = Math.max(1, count); if (total === 1) return [safeOffset(config, side, extent * 0.5)]; return Array.from({ length: total }, (_, index) => safeOffset(config, side, GRID + usable * index / (total - 1))); } function defaultPorts(config, direction) { const count = direction === "input" ? 3 : 2; const side = direction === "input" ? "left" : "right"; const prefix = direction === "input" ? "I" : "O"; return spacedOffsets(config, side, Math.min(count, portLimit(config))).map((offset, index) => ({ id: `${prefix}${index + 1}`, side, offset })); } function defaultConfig() { const config = { v: 4, cols: DEFAULT_COLS, rows: DEFAULT_ROWS, inputs: [], outputs: [], nodes: [], wires: [] }; config.inputs = defaultPorts(config, "input"); config.outputs = defaultPorts(config, "output"); return config; } function normalizeNodeId(value, used, index) { let id = /^[a-z0-9_-]{1,24}$/i.test(String(value || "")) ? String(value) : `g${index + 1}`; if (!used.has(id)) { used.add(id); return id; } let suffix = 2; while (used.has(`${id}_${suffix}`)) suffix += 1; id = `${id}_${suffix}`; used.add(id); return id; } function normalizePorts(config, rawPorts, direction) { const source = Array.isArray(rawPorts) ? rawPorts : []; const maximum = portLimit(config); const prefix = direction === "input" ? "I" : "O"; const fallbackSide = direction === "input" ? "left" : "right"; const used = new Set(); const result = []; for (let index = 0; index < source.length && result.length < maximum; index += 1) { const raw = source[index] || {}; let id = /^[A-Za-z][A-Za-z0-9_-]{0,11}$/.test(String(raw.id || "")) ? String(raw.id) : `${prefix}${index + 1}`; if (used.has(id)) { let n = 1; while (used.has(`${prefix}${n}`)) n += 1; id = `${prefix}${n}`; } used.add(id); const side = safeSide(raw.side || fallbackSide); result.push({ id, side, offset: safeOffset(config, side, raw.offset ?? ((index + 1) * GRID * 2)) }); } return result; } function rotateOffset(x, y, rotation) { const r = safeRotation(rotation); if (r === 1) return { x: -y, y: x }; if (r === 2) return { x: -x, y: -y }; if (r === 3) return { x: y, y: -x }; return { x, y }; } function nodeBodySize(type) { if (type === "not" || type === "rectifier") return { width: 44, height: 30 }; if (type === "on") return { width: 38, height: 38 }; return { width: 80, height: 80 }; } function nodePlacementBounds(config, type, rotation = 0) { const size = nodeBodySize(type); const r = safeRotation(rotation); const bodyHalfW = (r % 2) ? size.height * 0.5 : size.width * 0.5; const bodyHalfH = (r % 2) ? size.width * 0.5 : size.height * 0.5; const probe = { id: "__probe__", type, x: 0, y: 0, rotation: r }; const pins = nodePins(probe); let minPinX = 0, maxPinX = 0, minPinY = 0, maxPinY = 0; for (const pin of pins) { minPinX = Math.min(minPinX, pin.x); maxPinX = Math.max(maxPinX, pin.x); minPinY = Math.min(minPinY, pin.y); maxPinY = Math.max(maxPinY, pin.y); } const width = canvasWidth(config), height = canvasHeight(config); const minX = Math.max(bodyHalfW, GRID - minPinX); const maxX = Math.min(width - bodyHalfW, width - GRID - maxPinX); const minY = Math.max(bodyHalfH, GRID - minPinY); const maxY = Math.min(height - bodyHalfH, height - GRID - maxPinY); return { minX: Math.ceil(minX / GRID) * GRID, maxX: Math.floor(maxX / GRID) * GRID, minY: Math.ceil(minY / GRID) * GRID, maxY: Math.floor(maxY / GRID) * GRID, }; } function clampNodePosition(config, type, rotation, x, y) { const bounds = nodePlacementBounds(config, type, rotation); return { x: clamp(snap(x), bounds.minX, Math.max(bounds.minX, bounds.maxX)), y: clamp(snap(y), bounds.minY, Math.max(bounds.minY, bounds.maxY)), }; } function canvasPortPosition(config, port) { const side = safeSide(port?.side); const offset = safeOffset(config, side, port?.offset ?? sideExtent(config, side) * 0.5); if (side === "left") return { x: GRID, y: offset }; if (side === "right") return { x: canvasWidth(config) - GRID, y: offset }; if (side === "top") return { x: offset, y: GRID }; return { x: offset, y: canvasHeight(config) - GRID }; } function externalPins(config) { return [ ...config.inputs.map(port => ({ token: `i:${port.id}`, role: "source", direction: "input", kind: "external", label: port.id, port, ...canvasPortPosition(config, port) })), ...config.outputs.map(port => ({ token: `o:${port.id}`, role: "target", direction: "output", kind: "external", label: port.id, port, ...canvasPortPosition(config, port) })), ]; } function nodePins(node) { const pins = []; const count = nodeInputCount(node.type); const pushPin = (pin, role, ox, oy) => { const rotated = rotateOffset(ox, oy, node.rotation); pins.push({ token: `n:${node.id}:${pin}`, role, kind: "node", node, pin, x: snap(node.x + rotated.x), y: snap(node.y + rotated.y) }); }; if (count === 1) pushPin("a", "target", -GRID, 0); if (count === 2) { pushPin("a", "target", -GRID, -GRID); pushPin("b", "target", -GRID, GRID); } pushPin("o", "source", GRID, 0); return pins; } function allPins(config) { return [...externalPins(config), ...config.nodes.flatMap(nodePins)]; } function pinByToken(config, token) { return allPins(config).find(pin => pin.token === token) || null; } function endpointPosition(config, token) { const pin = pinByToken(config, token); if (pin) return { x: pin.x, y: pin.y, pin }; const point = parsePointToken(config, token); return point ? { ...point, pin: null } : null; } function validEndpoint(config, token) { return Boolean(endpointPosition(config, token)); } function wireKey(a, b) { return a < b ? `${a}|${b}` : `${b}|${a}`; } function remapPointTokens(config, wires) { return (wires || []).map(wire => { const map = token => { const match = /^p:(-?\d+):(-?\d+)$/.exec(String(token || "")); return match ? pointToken(config, Number(match[1]), Number(match[2])) : String(token || ""); }; return { a: map(wire?.a), b: map(wire?.b) }; }); } function normalizeConfig(value) { // Save compatibility is intentionally not provided for older board formats. if (!value || typeof value !== "object" || Number(value.v) !== 4) return defaultConfig(); const config = { v: 4, cols: safeCols(value.cols), rows: safeRows(value.rows), inputs: [], outputs: [], nodes: [], wires: [], }; config.inputs = normalizePorts(config, value.inputs, "input"); config.outputs = normalizePorts(config, value.outputs, "output"); const used = new Set(); config.nodes = (Array.isArray(value.nodes) ? value.nodes : []).slice(0, MAX_NODES).map((node, index) => { const type = nodeType(node?.type); const rotation = safeRotation(node?.rotation); const pos = clampNodePosition(config, type, rotation, node?.x ?? (GRID * 3 + (index % 4) * GRID * 2), node?.y ?? (GRID * 2 + Math.floor(index / 4) * GRID * 2)); return { id: normalizeNodeId(node?.id, used, index), type, x: pos.x, y: pos.y, rotation }; }); const seen = new Set(); for (const wire of remapPointTokens(config, Array.isArray(value.wires) ? value.wires : []).slice(0, MAX_WIRES)) { const a = String(wire?.a || ""); const b = String(wire?.b || ""); if (!validEndpoint(config, a) || !validEndpoint(config, b) || a === b) continue; const key = wireKey(a, b); if (seen.has(key)) continue; seen.add(key); config.wires.push({ a, b }); } return config; } function ensureConfig(board) { if (!board) return defaultConfig(); board.circuitConfig = normalizeConfig(board.circuitConfig); updateBoardPhysicalSize(board); return board.circuitConfig; } function serializeConfig(boardOrConfig) { const value = boardOrConfig?.type === "circuit_board" ? ensureConfig(boardOrConfig) : normalizeConfig(boardOrConfig); return clone(value) || defaultConfig(); } function applySerializedConfig(board, value) { if (!board) return false; board.circuitConfig = normalizeConfig(value); board._circuitTerminals = null; board._circuitInputState = Object.create(null); board._circuitNodeState = Object.create(null); board._circuitOutputState = Object.create(null); updateBoardPhysicalSize(board); return true; } function ports(board) { const config = ensureConfig(board); return [ ...config.inputs.map(port => ({ ...port, direction: "input", portId: `i:${port.id}`, label: `\u5165\u529b ${port.id}` })), ...config.outputs.map(port => ({ ...port, direction: "output", portId: `o:${port.id}`, label: `\u51fa\u529b ${port.id}` })), ]; } function localPortPosition(port, boardOrConfig = null) { const config = boardOrConfig?.type === "circuit_board" ? ensureConfig(boardOrConfig) : (boardOrConfig || defaultConfig()); const side = safeSide(port?.side); const extent = sideExtent(config, side); const offset = safeOffset(config, side, port?.offset ?? extent * 0.5); const t = clamp(offset / Math.max(1, extent), 0, 1); const halfW = boardHalfWidth(config); const halfH = boardHalfHeight(config); if (side === "left") return { x: -halfW, y: -halfH + t * halfH * 2 }; if (side === "right") return { x: halfW, y: -halfH + t * halfH * 2 }; if (side === "top") return { x: -halfW + t * halfW * 2, y: -halfH }; return { x: -halfW + t * halfW * 2, y: halfH }; } function portById(board, portId) { return ports(board).find(port => port.portId === portId) || null; } function portWorld(board, portOrId) { const port = typeof portOrId === "string" ? portById(board, portOrId) : portOrId; if (!board || !port) return null; const local = localPortPosition(port, board); const angle = Number(board.angle || 0) || 0; const c = Math.cos(angle), s = Math.sin(angle); return { x: board.x + local.x * c - local.y * s, y: board.y + local.x * s + local.y * c, localX: local.x, localY: local.y, port }; } function nearestPort(board, x, y, maxDistance = 25) { let best = null; let bestD = Number(maxDistance) || 25; for (const port of ports(board)) { const point = portWorld(board, port); const d = Math.hypot((Number(x) || 0) - point.x, (Number(y) || 0) - point.y); if (d <= bestD) { bestD = d; best = { ...point, distance: d }; } } return best; } function isPortExternallyConnected(board, portId, worldRef = board?.world || global.world) { if (!board || !portId || !worldRef?.items) return false; for (const link of worldRef.items) { if (!link || link.dead || !["wire", "insulated_wire"].includes(link.type)) continue; for (let index = 0; index < 2; index += 1) { const endpoint = global.TarinaiPhysicsBodySystem?.endpoint?.(link, index) || link.physicsConstraint?.endpoints?.[index] || null; if (endpoint?.id === board.id && endpoint?.portId === portId) return true; } } return false; } function remapAttachedWireEndpoints(board, worldRef = board?.world || global.world) { if (!board || !worldRef?.items) return 0; let changed = 0; for (const link of worldRef.items) { if (!link || link.dead || !["wire", "insulated_wire"].includes(link.type)) continue; for (let index = 0; index < 2; index += 1) { const endpoint = global.TarinaiPhysicsBodySystem?.endpoint?.(link, index) || link.physicsConstraint?.endpoints?.[index] || null; if (!endpoint || endpoint.id !== board.id || !endpoint.portId) continue; const point = portWorld(board, endpoint.portId); if (!point) continue; endpoint.localX = point.localX; endpoint.localY = point.localY; endpoint.x = point.x; endpoint.y = point.y; endpoint.center = false; changed += 1; } } const pending = worldRef.pendingLinkEndpoint; if (pending?.id === board.id && pending.portId) { const point = portWorld(board, pending.portId); if (point) { pending.localX = point.localX; pending.localY = point.localY; pending.x = point.x; pending.y = point.y; pending.center = false; changed += 1; } } if (changed) worldRef.markSpatialDirty?.("circuit-board-port-remap"); return changed; } function terminal(board, portId) { if (!board) return null; const port = portById(board, portId); if (!port) return null; if (!board._circuitTerminals || typeof board._circuitTerminals !== "object") board._circuitTerminals = Object.create(null); let result = board._circuitTerminals[portId]; if (!result) result = board._circuitTerminals[portId] = { kind: "circuit_terminal", type: "circuit_terminal", board, boardId: board.id || "", portId, direction: port.direction, signalActive: false }; result.board = board; result.boardId = board.id || ""; result.direction = port.direction; result.signalActive = port.direction === "output" ? Boolean(board._circuitOutputState?.[port.id]) : Boolean(board._circuitInputState?.[port.id]); return result; } function evaluate(board, inputValues = null) { const config = ensureConfig(board); const inputs = Object.create(null); for (const port of config.inputs) inputs[port.id] = Boolean(inputValues ? inputValues[port.id] : board?._circuitInputState?.[port.id]); const parent = new Map(); const rank = new Map(); const add = token => { if (!parent.has(token)) { parent.set(token, token); rank.set(token, 0); } }; const find = token => { add(token); let root = token; while (parent.get(root) !== root) root = parent.get(root); let current = token; while (parent.get(current) !== current) { const next = parent.get(current); parent.set(current, root); current = next; } return root; }; const union = (a, b) => { let ra = find(a), rb = find(b); if (ra === rb) return; const ar = rank.get(ra) || 0, br = rank.get(rb) || 0; if (ar < br) { const t = ra; ra = rb; rb = t; } parent.set(rb, ra); if (ar === br) rank.set(ra, ar + 1); }; for (const pin of allPins(config)) add(pin.token); for (const wire of config.wires) { add(wire.a); add(wire.b); union(wire.a, wire.b); } const members = new Map(); for (const token of parent.keys()) { const root = find(token); if (!members.has(root)) members.set(root, []); members.get(root).push(token); } const nodeMap = new Map(config.nodes.map(node => [node.id, node])); const dependencies = new Map(config.nodes.map(node => [node.id, new Set()])); for (const node of config.nodes) { for (const pinName of nodeInputCount(node.type) === 2 ? ["a", "b"] : (nodeInputCount(node.type) === 1 ? ["a"] : [])) { const root = find(`n:${node.id}:${pinName}`); for (const member of members.get(root) || []) { const match = /^n:([^:]+):o$/.exec(member); if (match && dependencies.has(match[1])) dependencies.get(node.id).add(match[1]); } } } const unstableNodes = new Set(); { let index = 0; const indices = new Map(), low = new Map(), stack = [], onStack = new Set(); const visit = id => { indices.set(id, index); low.set(id, index); index += 1; stack.push(id); onStack.add(id); for (const dep of dependencies.get(id) || []) { if (!indices.has(dep)) { visit(dep); low.set(id, Math.min(low.get(id), low.get(dep))); } else if (onStack.has(dep)) low.set(id, Math.min(low.get(id), indices.get(dep))); } if (low.get(id) !== indices.get(id)) return; const component = []; while (stack.length) { const value = stack.pop(); onStack.delete(value); component.push(value); if (value === id) break; } if (component.length > 1 || (component.length === 1 && dependencies.get(component[0])?.has(component[0]))) { for (const value of component) unstableNodes.add(value); } }; for (const node of config.nodes) if (!indices.has(node.id)) visit(node.id); } const nodeValues = Object.create(null); const netValues = new Map(); const visitingNodes = new Set(); const visitingNets = new Set(); function driverValue(token) { if (token.startsWith("i:")) return Boolean(inputs[token.slice(2)]); const match = /^n:([^:]+):o$/.exec(token); return match ? nodeValue(match[1]) : false; } function netValue(token) { const root = find(token); if (netValues.has(root)) return Boolean(netValues.get(root)); if (visitingNets.has(root)) return false; visitingNets.add(root); let value = false; for (const member of members.get(root) || [token]) { if (member.startsWith("i:") || /^n:[^:]+:o$/.test(member)) { if (driverValue(member)) { value = true; break; } } } visitingNets.delete(root); netValues.set(root, Boolean(value)); return Boolean(value); } function nodeValue(id) { if (Object.prototype.hasOwnProperty.call(nodeValues, id)) return Boolean(nodeValues[id]); if (visitingNodes.has(id)) return false; const node = nodeMap.get(id); if (!node) return false; if (unstableNodes.has(id)) { nodeValues[id] = false; return false; } visitingNodes.add(id); let value = false; if (node.type === "on") value = true; else { const a = netValue(`n:${id}:a`); if (node.type === "not") value = !a; else if (node.type === "rectifier") value = a; else value = a && netValue(`n:${id}:b`); } visitingNodes.delete(id); nodeValues[id] = Boolean(value); return Boolean(value); } const outputs = Object.create(null); for (const port of config.outputs) outputs[port.id] = netValue(`o:${port.id}`); if (board) { board._circuitInputState = inputs; board._circuitNodeState = nodeValues; board._circuitOutputState = outputs; for (const port of config.outputs) { const term = terminal(board, `o:${port.id}`); if (term) term.signalActive = Boolean(outputs[port.id]); } } return { inputs, nodes: nodeValues, outputs }; } function nearestPin(config, x, y, maxDistance = 17) { let best = null; let bestD = maxDistance; for (const pin of allPins(config)) { const d = Math.hypot(x - pin.x, y - pin.y); if (d <= bestD) { bestD = d; best = { ...pin, distance: d }; } } return best; } function junctionTokens(config) { const degree = new Map(); for (const wire of config.wires) { for (const token of [wire.a, wire.b]) if (parsePointToken(config, token)) degree.set(token, (degree.get(token) || 0) + 1); } return [...degree.entries()].map(([token, count]) => ({ token, count, ...parsePointToken(config, token) })); } function nearestJunction(config, x, y, maxDistance = 16) { let best = null; let bestD = maxDistance; for (const junction of junctionTokens(config)) { const d = Math.hypot(x - junction.x, y - junction.y); if (d <= bestD) { bestD = d; best = { ...junction, distance: d }; } } return best; } function nodeAt(config, x, y) { for (let i = config.nodes.length - 1; i >= 0; i -= 1) { const node = config.nodes[i]; const rotation = safeRotation(node.rotation); const dx = x - node.x, dy = y - node.y; const local = rotateOffset(dx, dy, (4 - rotation) % 4); const size = nodeBodySize(node.type); if (Math.abs(local.x) <= size.width * 0.5 && Math.abs(local.y) <= size.height * 0.5) return node; } return null; } const distanceToSegment = (px, py, ax, ay, bx, by) => global.TarinaiGeometry.pointSegmentDistance(px, py, ax, ay, bx, by); function wirePath(config, wire) { const a = endpointPosition(config, wire.a); const b = endpointPosition(config, wire.b); return a && b ? [a, b] : []; } function wireAt(config, x, y, maxDistance = 9) { let best = null; let bestD = maxDistance; for (let i = config.wires.length - 1; i >= 0; i -= 1) { const path = wirePath(config, config.wires[i]); if (path.length < 2) continue; const d = distanceToSegment(x, y, path[0].x, path[0].y, path[1].x, path[1].y); if (d <= bestD) { bestD = d; best = { wire: config.wires[i], index: i, distance: d }; } } return best; } function gcd(a, b) { a = Math.abs(Math.round(a)); b = Math.abs(Math.round(b)); while (b) { const t = a % b; a = b; b = t; } return a || 1; } function nearestGridPointOnWire(config, wire, x, y, maxDistance = 18) { const path = wirePath(config, wire); if (path.length < 2) return null; const a = path[0], b = path[1]; const ux = Math.round((b.x - a.x) / GRID), uy = Math.round((b.y - a.y) / GRID); const steps = gcd(ux, uy); let best = null, bestD = maxDistance; for (let i = 0; i <= steps; i += 1) { const px = snap(a.x + (b.x - a.x) * i / steps); const py = snap(a.y + (b.y - a.y) * i / steps); const d = Math.hypot(x - px, y - py); if (d <= bestD) { bestD = d; best = { x: px, y: py, distance: d }; } } return best; } function pointOnSegment(point, a, b) { return distanceToSegment(point.x, point.y, a.x, a.y, b.x, b.y) < 0.6 && point.x >= Math.min(a.x, b.x) - 0.6 && point.x <= Math.max(a.x, b.x) + 0.6 && point.y >= Math.min(a.y, b.y) - 0.6 && point.y <= Math.max(a.y, b.y) + 0.6; } function splitWiresAtPoint(config, x, y) { const token = pointToken(config, x, y); const point = parsePointToken(config, token); const next = []; let changed = false; for (const wire of config.wires) { const path = wirePath(config, wire); if (path.length < 2 || !pointOnSegment(point, path[0], path[1]) || wire.a === token || wire.b === token) { next.push(wire); continue; } const aPos = endpointPosition(config, wire.a), bPos = endpointPosition(config, wire.b); if ((aPos.x === point.x && aPos.y === point.y) || (bPos.x === point.x && bPos.y === point.y)) { next.push(wire); continue; } next.push({ a: wire.a, b: token }, { a: token, b: wire.b }); changed = true; } if (changed) config.wires = next.slice(0, MAX_WIRES); return token; } function moveJunction(config, oldToken, x, y) { const nextToken = pointToken(config, x, y); if (!parsePointToken(config, oldToken) || nextToken === oldToken) return nextToken; for (const wire of config.wires) { if (wire.a === oldToken) wire.a = nextToken; if (wire.b === oldToken) wire.b = nextToken; } const seen = new Set(); config.wires = config.wires.filter(wire => { if (wire.a === wire.b) return false; const key = wireKey(wire.a, wire.b); if (seen.has(key)) return false; seen.add(key); return true; }); return nextToken; } function anchorAt(config, x, y, allowEmpty = false) { const pin = nearestPin(config, x, y, 18); if (pin) return { token: pin.token, x: pin.x, y: pin.y, kind: "pin" }; const junction = nearestJunction(config, x, y, 18); if (junction) return { token: junction.token, x: junction.x, y: junction.y, kind: "junction" }; let best = null; for (const wire of config.wires) { const candidate = nearestGridPointOnWire(config, wire, x, y, 18); if (candidate && (!best || candidate.distance < best.distance)) best = { ...candidate, wire }; } if (best) return { token: pointToken(config, best.x, best.y), x: best.x, y: best.y, kind: "wire" }; if (!allowEmpty) return null; const px = clamp(snap(x), GRID, canvasWidth(config) - GRID), py = clamp(snap(y), GRID, canvasHeight(config) - GRID); return { token: pointToken(config, px, py), x: px, y: py, kind: "empty" }; } function canvasPoint(canvas, event) { const rect = canvas.getBoundingClientRect(); return { x: (event.clientX - rect.left) * canvas.width / Math.max(1, rect.width), y: (event.clientY - rect.top) * canvas.height / Math.max(1, rect.height) }; } function terminalPlacement(config, x, y) { const width = canvasWidth(config), height = canvasHeight(config); const distances = [ { side: "left", d: x }, { side: "right", d: width - x }, { side: "top", d: y }, { side: "bottom", d: height - y }, ].sort((a, b) => a.d - b.d); const side = distances[0].side; const axis = isVerticalSide(side) ? y : x; return { side, offset: safeOffset(config, side, axis) }; } function nextPortId(config, direction) { const prefix = direction === "input" ? "I" : "O"; const portsList = direction === "input" ? config.inputs : config.outputs; const used = new Set(portsList.map(port => port.id)); let index = 1; while (used.has(`${prefix}${index}`)) index += 1; return `${prefix}${index}`; } function ensureDialog() { if (typeof document === "undefined") return null; let dialog = document.getElementById("circuitBoardEditor"); if (dialog) return dialog; dialog = document.createElement("div"); dialog.id = "circuitBoardEditor"; dialog.className = "rotator-editor circuit-grid-editor hidden"; dialog.innerHTML = `
IN/OUT\u7aef\u5b50\u30c4\u30fc\u30eb\u3067\u57fa\u76e4\u5916\u5468\u306b\u76f4\u63a5\u7aef\u5b50\u3092\u7f6e\u304d\u3001\u79fb\u52d5\u30c4\u30fc\u30eb\u3067\u56db\u8fba\u306e\u4efb\u610f\u4f4d\u7f6e\u3078\u79fb\u52d5\u3067\u304d\u307e\u3059\u3002\u6d88\u3057\u30b4\u30e0\u3067\u672a\u63a5\u7d9a\u7aef\u5b50\u3092\u524a\u9664\u3067\u304d\u307e\u3059\u3002\u914d\u7dda\u306f2\u70b9\u3092\u9078\u3093\u3067\u76f4\u7dda\u3067\u5f15\u304d\u3001\u65e2\u5b58\u7dda\u4e0a\u304b\u3089\u5206\u5c90\u3067\u304d\u307e\u3059\u3002NOT\u3092\u542b\u3080\u5faa\u74b0\u56de\u8def\u306f\u72b6\u614b\u304c\u5b89\u5b9a\u3057\u306a\u3044\u305f\u3081OFF\u3068\u3057\u3066\u6271\u3044\u307e\u3059\u3002\u56de\u8def\u90e8\u54c1\u306e\u8a2d\u7f6e\u524d\u306fQ/E\u306790\u5ea6\u305a\u3064\u56de\u8ee2\u3067\u304d\u307e\u3059\u3002ON\u306f\u5e38\u6642ON\u306e\u5b9a\u6570\u4fe1\u53f7\u6e90\u3067\u3059\u3002