Auto-generate schematics on mount, wire routing for grid layout
SPICE cells now auto-trigger schematic generation when they mount without an existing diagram, so the schematic leads the UI. Template and empty cells are skipped. Grid fallback layout replaced with wire-connected rendering: BFS-based node tier classification places components vertically between supply and ground rails, then routes wires by node type (power/ground bus rails, L-shaped signal wires, star topology for 3+ connections).
This commit is contained in:
parent
579f90487d
commit
b497d57890
@ -56,6 +56,16 @@ class ActiveLayout:
|
|||||||
unplaced: list[SpiceComponent]
|
unplaced: list[SpiceComponent]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlacedComponent:
|
||||||
|
"""A component placed in the grid with recorded terminal positions."""
|
||||||
|
|
||||||
|
comp: SpiceComponent
|
||||||
|
element: object # SchemDraw placed element
|
||||||
|
column: int
|
||||||
|
terminal_positions: dict[int, tuple[float, float]] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
# ── Ground / Supply Detection ───────────────────────────────────
|
# ── Ground / Supply Detection ───────────────────────────────────
|
||||||
|
|
||||||
GROUND_NAMES = {"0", "gnd"}
|
GROUND_NAMES = {"0", "gnd"}
|
||||||
@ -678,50 +688,396 @@ def _render_loop(parsed: ParsedNetlist, loop: list[SpiceComponent]) -> str:
|
|||||||
return d.get_imagedata("svg").decode()
|
return d.get_imagedata("svg").decode()
|
||||||
|
|
||||||
|
|
||||||
# ── Grid Layout Renderer ──────────────────────────────────────
|
# ── Grid Layout: Constants & Helpers ─────────────────────────
|
||||||
|
|
||||||
|
# Vertical tier positions (schemdraw y-axis: up is positive)
|
||||||
|
_GRID_SUPPLY_Y = 0.0
|
||||||
|
_GRID_UPPER_Y = -3.0
|
||||||
|
_GRID_MID_Y = -5.5
|
||||||
|
_GRID_LOWER_Y = -8.0
|
||||||
|
_GRID_GROUND_Y = -11.0
|
||||||
|
_GRID_X_SPACING = 6.0
|
||||||
|
_GRID_MIN_COMP_LEN = 2.0 # minimum component length to avoid zero-height placement
|
||||||
|
|
||||||
|
|
||||||
|
def _classify_node_tiers(
|
||||||
|
node_map: dict[str, list[tuple[SpiceComponent, int]]],
|
||||||
|
) -> dict[str, float]:
|
||||||
|
"""Assign vertical Y positions to nodes via BFS distance from ground/supply.
|
||||||
|
|
||||||
|
Ground and supply nodes are pinned to fixed tiers. Signal nodes get
|
||||||
|
Y-positions blended between supply (top) and ground (bottom) based on
|
||||||
|
their BFS hop distance from each rail, then snapped to the nearest
|
||||||
|
discrete tier for clean vertical alignment.
|
||||||
|
"""
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
tiers: dict[str, float] = {}
|
||||||
|
snap_levels = [_GRID_SUPPLY_Y, _GRID_UPPER_Y, _GRID_MID_Y, _GRID_LOWER_Y, _GRID_GROUND_Y]
|
||||||
|
|
||||||
|
# Pin ground and supply nodes
|
||||||
|
for node in node_map:
|
||||||
|
if _is_ground(node):
|
||||||
|
tiers[node] = _GRID_GROUND_Y
|
||||||
|
elif _is_supply(node):
|
||||||
|
tiers[node] = _GRID_SUPPLY_Y
|
||||||
|
|
||||||
|
# BFS from ground nodes
|
||||||
|
dist_from_gnd: dict[str, int] = {}
|
||||||
|
queue: deque[tuple[str, int]] = deque()
|
||||||
|
for node in node_map:
|
||||||
|
if _is_ground(node):
|
||||||
|
dist_from_gnd[node] = 0
|
||||||
|
queue.append((node, 0))
|
||||||
|
while queue:
|
||||||
|
current, dist = queue.popleft()
|
||||||
|
for comp, idx in node_map.get(current, []):
|
||||||
|
for i, other_node in enumerate(comp.nodes):
|
||||||
|
if i != idx and other_node not in dist_from_gnd:
|
||||||
|
dist_from_gnd[other_node] = dist + 1
|
||||||
|
queue.append((other_node, dist + 1))
|
||||||
|
|
||||||
|
# BFS from supply nodes
|
||||||
|
dist_from_sup: dict[str, int] = {}
|
||||||
|
queue = deque()
|
||||||
|
for node in node_map:
|
||||||
|
if _is_supply(node):
|
||||||
|
dist_from_sup[node] = 0
|
||||||
|
queue.append((node, 0))
|
||||||
|
while queue:
|
||||||
|
current, dist = queue.popleft()
|
||||||
|
for comp, idx in node_map.get(current, []):
|
||||||
|
for i, other_node in enumerate(comp.nodes):
|
||||||
|
if i != idx and other_node not in dist_from_sup:
|
||||||
|
dist_from_sup[other_node] = dist + 1
|
||||||
|
queue.append((other_node, dist + 1))
|
||||||
|
|
||||||
|
# Assign signal nodes: blend by relative BFS distance, snap to tier
|
||||||
|
for node in node_map:
|
||||||
|
if node in tiers:
|
||||||
|
continue
|
||||||
|
dg = dist_from_gnd.get(node, 999)
|
||||||
|
ds = dist_from_sup.get(node, 999)
|
||||||
|
if ds == 999 and dg == 999:
|
||||||
|
continuous_y = _GRID_MID_Y
|
||||||
|
elif ds == 999:
|
||||||
|
continuous_y = _GRID_LOWER_Y
|
||||||
|
elif dg == 999:
|
||||||
|
continuous_y = _GRID_UPPER_Y
|
||||||
|
else:
|
||||||
|
ratio = ds / (ds + dg)
|
||||||
|
continuous_y = _GRID_SUPPLY_Y + ratio * (_GRID_GROUND_Y - _GRID_SUPPLY_Y)
|
||||||
|
tiers[node] = min(snap_levels, key=lambda t: abs(t - continuous_y))
|
||||||
|
|
||||||
|
return tiers
|
||||||
|
|
||||||
|
|
||||||
|
def _assign_columns(
|
||||||
|
components: list[SpiceComponent],
|
||||||
|
node_map: dict[str, list[tuple[SpiceComponent, int]]],
|
||||||
|
) -> list[SpiceComponent]:
|
||||||
|
"""Order placeable components left-to-right via BFS through signal nodes.
|
||||||
|
|
||||||
|
Supply sources (DC V-source between supply rail and ground) are filtered
|
||||||
|
out — they become rail symbols instead of drawn components.
|
||||||
|
"""
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
|
placeable = [c for c in components if not _is_supply_source(c)]
|
||||||
|
if not placeable:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Build component adjacency through shared signal nodes
|
||||||
|
comp_names = {c.name for c in placeable}
|
||||||
|
comp_lookup = {c.name: c for c in placeable}
|
||||||
|
adj: dict[str, set[str]] = {c.name: set() for c in placeable}
|
||||||
|
|
||||||
|
for node, connections in node_map.items():
|
||||||
|
if _is_ground(node) or _is_supply(node):
|
||||||
|
continue
|
||||||
|
node_comps = [c.name for c, _ in connections if c.name in comp_names]
|
||||||
|
for i, a in enumerate(node_comps):
|
||||||
|
for b in node_comps[i + 1:]:
|
||||||
|
adj[a].add(b)
|
||||||
|
adj[b].add(a)
|
||||||
|
|
||||||
|
# BFS from first voltage source (or first component if none)
|
||||||
|
start = next((c for c in placeable if c.prefix == "V"), None)
|
||||||
|
if start is None:
|
||||||
|
start = placeable[0]
|
||||||
|
|
||||||
|
ordered: list[str] = []
|
||||||
|
visited: set[str] = set()
|
||||||
|
queue: deque[str] = deque([start.name])
|
||||||
|
visited.add(start.name)
|
||||||
|
|
||||||
|
while queue:
|
||||||
|
name = queue.popleft()
|
||||||
|
ordered.append(name)
|
||||||
|
for neighbor in sorted(adj.get(name, [])):
|
||||||
|
if neighbor not in visited:
|
||||||
|
visited.add(neighbor)
|
||||||
|
queue.append(neighbor)
|
||||||
|
|
||||||
|
# Append any disconnected components not reached by BFS
|
||||||
|
for c in placeable:
|
||||||
|
if c.name not in visited:
|
||||||
|
ordered.append(c.name)
|
||||||
|
|
||||||
|
return [comp_lookup[name] for name in ordered]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_terminal_positions(
|
||||||
|
placed_elem: object,
|
||||||
|
comp: SpiceComponent,
|
||||||
|
) -> dict[int, tuple[float, float]]:
|
||||||
|
"""Extract (x, y) terminal positions from a placed SchemDraw element."""
|
||||||
|
positions: dict[int, tuple[float, float]] = {}
|
||||||
|
|
||||||
|
if comp.prefix in ("Q", "M") and len(comp.nodes) >= 3:
|
||||||
|
anchors = (
|
||||||
|
["collector", "base", "emitter"]
|
||||||
|
if comp.prefix == "Q"
|
||||||
|
else ["drain", "gate", "source"]
|
||||||
|
)
|
||||||
|
for i, anchor_name in enumerate(anchors):
|
||||||
|
if hasattr(placed_elem, anchor_name):
|
||||||
|
pos = getattr(placed_elem, anchor_name)
|
||||||
|
positions[i] = (float(pos[0]), float(pos[1]))
|
||||||
|
else:
|
||||||
|
if hasattr(placed_elem, "start"):
|
||||||
|
positions[0] = (float(placed_elem.start[0]), float(placed_elem.start[1]))
|
||||||
|
if hasattr(placed_elem, "end"):
|
||||||
|
idx = min(1, len(comp.nodes) - 1)
|
||||||
|
positions[idx] = (float(placed_elem.end[0]), float(placed_elem.end[1]))
|
||||||
|
|
||||||
|
return positions
|
||||||
|
|
||||||
|
|
||||||
|
def _find_supply_value(
|
||||||
|
components: list[SpiceComponent],
|
||||||
|
supply_node: str,
|
||||||
|
) -> tuple[str, str]:
|
||||||
|
"""Find the DC voltage value for a supply rail. Returns (source_name, formatted_value)."""
|
||||||
|
for comp in components:
|
||||||
|
if comp.prefix == "V" and len(comp.nodes) == 2:
|
||||||
|
n0, n1 = comp.nodes
|
||||||
|
if (n0.lower() == supply_node.lower() and _is_ground(n1)) or \
|
||||||
|
(n1.lower() == supply_node.lower() and _is_ground(n0)):
|
||||||
|
return (comp.name, _format_value(comp.value))
|
||||||
|
return ("", "")
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_ground_rail(d, positions: list[tuple[float, float]]) -> None:
|
||||||
|
"""Draw horizontal ground bus with vertical stubs and Ground symbol."""
|
||||||
|
import schemdraw.elements as elm
|
||||||
|
|
||||||
|
if not positions:
|
||||||
|
return
|
||||||
|
|
||||||
|
sorted_pos = sorted(positions, key=lambda p: p[0])
|
||||||
|
|
||||||
|
# Horizontal bus line across all ground-connected terminals
|
||||||
|
if len(sorted_pos) > 1:
|
||||||
|
d.add(elm.Line().at(sorted_pos[0]).to(sorted_pos[-1]))
|
||||||
|
|
||||||
|
# Vertical stubs from each terminal down to the bus
|
||||||
|
for px, py in sorted_pos:
|
||||||
|
if abs(py - _GRID_GROUND_Y) > 0.1:
|
||||||
|
d.add(elm.Line().at((px, py)).to((px, _GRID_GROUND_Y)))
|
||||||
|
|
||||||
|
# Ground symbol at the bus midpoint
|
||||||
|
mid_x = sum(p[0] for p in sorted_pos) / len(sorted_pos)
|
||||||
|
d.add(elm.Ground().at((mid_x, _GRID_GROUND_Y)))
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_supply_rail(
|
||||||
|
d,
|
||||||
|
positions: list[tuple[float, float]],
|
||||||
|
label: str,
|
||||||
|
value: str,
|
||||||
|
) -> None:
|
||||||
|
"""Draw horizontal supply bus with vertical stubs and Vdd symbol."""
|
||||||
|
import schemdraw.elements as elm
|
||||||
|
|
||||||
|
if not positions:
|
||||||
|
return
|
||||||
|
|
||||||
|
sorted_pos = sorted(positions, key=lambda p: p[0])
|
||||||
|
|
||||||
|
if len(sorted_pos) > 1:
|
||||||
|
d.add(elm.Line().at(sorted_pos[0]).to(sorted_pos[-1]))
|
||||||
|
|
||||||
|
for px, py in sorted_pos:
|
||||||
|
if abs(py - _GRID_SUPPLY_Y) > 0.1:
|
||||||
|
d.add(elm.Line().at((px, py)).to((px, _GRID_SUPPLY_Y)))
|
||||||
|
|
||||||
|
mid_x = sum(p[0] for p in sorted_pos) / len(sorted_pos)
|
||||||
|
rail_label = label.upper()
|
||||||
|
if value:
|
||||||
|
rail_label += f" {value}"
|
||||||
|
d.add(elm.Vdd().at((mid_x, _GRID_SUPPLY_Y)).label(rail_label))
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_signal_wire(
|
||||||
|
d,
|
||||||
|
pos_a: tuple[float, float],
|
||||||
|
pos_b: tuple[float, float],
|
||||||
|
) -> None:
|
||||||
|
"""Draw an L-shaped or straight wire between two terminal positions."""
|
||||||
|
import schemdraw.elements as elm
|
||||||
|
|
||||||
|
x1, y1 = pos_a
|
||||||
|
x2, y2 = pos_b
|
||||||
|
|
||||||
|
# Skip near-zero-length wires
|
||||||
|
if abs(x1 - x2) < 0.1 and abs(y1 - y2) < 0.1:
|
||||||
|
return
|
||||||
|
|
||||||
|
if abs(x1 - x2) < 0.1 or abs(y1 - y2) < 0.1:
|
||||||
|
# Straight wire (aligned on one axis)
|
||||||
|
d.add(elm.Line().at((x1, y1)).to((x2, y2)))
|
||||||
|
else:
|
||||||
|
# L-shaped: horizontal first, then vertical
|
||||||
|
d.add(elm.Line().at((x1, y1)).to((x2, y1)))
|
||||||
|
d.add(elm.Line().at((x2, y1)).to((x2, y2)))
|
||||||
|
|
||||||
|
|
||||||
|
def _draw_star_wires(
|
||||||
|
d,
|
||||||
|
positions: list[tuple[float, float]],
|
||||||
|
) -> None:
|
||||||
|
"""Draw star-topology wiring with junction dot for 3+ connections."""
|
||||||
|
import schemdraw.elements as elm
|
||||||
|
|
||||||
|
if len(positions) < 2:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Junction at the median position (reduces total wire length)
|
||||||
|
xs = sorted(p[0] for p in positions)
|
||||||
|
ys = sorted(p[1] for p in positions)
|
||||||
|
jx = xs[len(xs) // 2]
|
||||||
|
jy = ys[len(ys) // 2]
|
||||||
|
|
||||||
|
for px, py in positions:
|
||||||
|
if abs(px - jx) < 0.1 and abs(py - jy) < 0.1:
|
||||||
|
continue
|
||||||
|
_draw_signal_wire(d, (px, py), (jx, jy))
|
||||||
|
|
||||||
|
d.add(elm.Dot().at((jx, jy)))
|
||||||
|
|
||||||
|
|
||||||
|
# ── Grid Layout Renderer ────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
def _render_grid(parsed: ParsedNetlist) -> str:
|
def _render_grid(parsed: ParsedNetlist) -> str:
|
||||||
"""Render components in a labeled grid layout.
|
"""Render components in a wire-connected grid layout.
|
||||||
|
|
||||||
Used for complex circuits where topological layout isn't feasible.
|
Places components vertically between BFS-classified node tiers, then
|
||||||
Components are arranged in columns by type with terminal node labels.
|
routes wires between terminals sharing the same net: power/ground get
|
||||||
|
horizontal bus rails with symbols, 2-connection signals get L-shaped
|
||||||
|
wires, and 3+ connections get star wiring with junction dots.
|
||||||
"""
|
"""
|
||||||
import schemdraw
|
import schemdraw
|
||||||
|
import schemdraw.elements as elm
|
||||||
|
|
||||||
|
node_map = _build_node_map(parsed.components)
|
||||||
|
tiers = _classify_node_tiers(node_map)
|
||||||
|
ordered = _assign_columns(parsed.components, node_map)
|
||||||
|
|
||||||
|
if not ordered:
|
||||||
|
d = schemdraw.Drawing(fontsize=11)
|
||||||
|
d.add(elm.Label().label("(empty circuit)"))
|
||||||
|
return d.get_imagedata("svg").decode()
|
||||||
|
|
||||||
d = schemdraw.Drawing(fontsize=11)
|
d = schemdraw.Drawing(fontsize=11)
|
||||||
|
|
||||||
# Group by role for logical ordering
|
# ── Phase 1: Place components vertically between node tiers ──
|
||||||
sources = [c for c in parsed.components if c.prefix in ("V", "I")]
|
placed_list: list[PlacedComponent] = []
|
||||||
passives = [c for c in parsed.components if c.prefix in ("R", "C", "L")]
|
|
||||||
active = [c for c in parsed.components if c.prefix in ("D", "Q", "M")]
|
|
||||||
other = [
|
|
||||||
c for c in parsed.components
|
|
||||||
if c.prefix not in ("V", "I", "R", "C", "L", "D", "Q", "M")
|
|
||||||
]
|
|
||||||
ordered = sources + passives + active + other
|
|
||||||
|
|
||||||
cols = min(4, max(2, len(ordered)))
|
|
||||||
x_spacing = 8
|
|
||||||
y_spacing = 5
|
|
||||||
|
|
||||||
for i, comp in enumerate(ordered):
|
|
||||||
row = i // cols
|
|
||||||
col = i % cols
|
|
||||||
x = col * x_spacing
|
|
||||||
y = -row * y_spacing
|
|
||||||
|
|
||||||
|
for col, comp in enumerate(ordered):
|
||||||
|
x = col * _GRID_X_SPACING
|
||||||
elem = _get_element(comp, parsed.models)
|
elem = _get_element(comp, parsed.models)
|
||||||
|
|
||||||
# 3+ terminal devices (BJT, MOSFET) need special handling
|
|
||||||
if comp.prefix in ("Q", "M") and len(comp.nodes) >= 3:
|
if comp.prefix in ("Q", "M") and len(comp.nodes) >= 3:
|
||||||
placed = d.add(elem.at((x, y)).label(_component_label(comp)))
|
# Multi-terminal device: center between top/bottom node tiers
|
||||||
_label_multiterminal(d, placed, comp)
|
node_ys = [tiers.get(n, _GRID_MID_Y) for n in comp.nodes[:3]]
|
||||||
else:
|
center_y = (max(node_ys) + min(node_ys)) / 2
|
||||||
placed = d.add(
|
placed_elem = d.add(
|
||||||
elem.at((x, y)).right().label(_component_label(comp))
|
elem.at((x, center_y)).label(_component_label(comp))
|
||||||
|
)
|
||||||
|
elif len(comp.nodes) >= 2:
|
||||||
|
# 2-terminal: orient vertically between the two node tiers
|
||||||
|
y0 = tiers.get(comp.nodes[0], _GRID_UPPER_Y)
|
||||||
|
y1 = tiers.get(comp.nodes[1], _GRID_LOWER_Y)
|
||||||
|
length = max(abs(y0 - y1), _GRID_MIN_COMP_LEN)
|
||||||
|
|
||||||
|
if y0 >= y1:
|
||||||
|
# node[0] higher → go down so start=node[0], end=node[1]
|
||||||
|
placed_elem = d.add(
|
||||||
|
elem.at((x, y0)).down().length(length)
|
||||||
|
.label(_component_label(comp), loc="right")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# node[0] lower → go up so start=node[0], end=node[1]
|
||||||
|
placed_elem = d.add(
|
||||||
|
elem.at((x, y0)).up().length(length)
|
||||||
|
.label(_component_label(comp), loc="right")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Fallback: single-node or unusual component
|
||||||
|
placed_elem = d.add(
|
||||||
|
elem.at((x, _GRID_MID_Y)).down().length(_GRID_MIN_COMP_LEN)
|
||||||
|
.label(_component_label(comp), loc="right")
|
||||||
|
)
|
||||||
|
|
||||||
|
positions = _get_terminal_positions(placed_elem, comp)
|
||||||
|
placed_list.append(PlacedComponent(
|
||||||
|
comp=comp, element=placed_elem, column=col,
|
||||||
|
terminal_positions=positions,
|
||||||
|
))
|
||||||
|
|
||||||
|
# ── Phase 2: Collect terminal positions grouped by node name ──
|
||||||
|
node_positions: dict[str, list[tuple[float, float]]] = {}
|
||||||
|
for pc in placed_list:
|
||||||
|
for i, node in enumerate(pc.comp.nodes):
|
||||||
|
if i in pc.terminal_positions:
|
||||||
|
node_positions.setdefault(node, []).append(
|
||||||
|
pc.terminal_positions[i]
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Phase 3: Route wires by node type ────────────────────────
|
||||||
|
for node, positions in node_positions.items():
|
||||||
|
if len(positions) < 2:
|
||||||
|
# Single connection: open dot with node label
|
||||||
|
if positions and not _is_ground(node) and not _is_supply(node):
|
||||||
|
px, py = positions[0]
|
||||||
|
d.add(
|
||||||
|
elm.Dot(open=True).at((px, py))
|
||||||
|
.label(node, loc="right", fontsize=9)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if _is_ground(node):
|
||||||
|
_draw_ground_rail(d, positions)
|
||||||
|
elif _is_supply(node):
|
||||||
|
_, src_val = _find_supply_value(parsed.components, node)
|
||||||
|
_draw_supply_rail(d, positions, node, src_val)
|
||||||
|
elif len(positions) == 2:
|
||||||
|
_draw_signal_wire(d, positions[0], positions[1])
|
||||||
|
# Label at the midpoint of the wire
|
||||||
|
mx = (positions[0][0] + positions[1][0]) / 2
|
||||||
|
my = (positions[0][1] + positions[1][1]) / 2
|
||||||
|
d.add(elm.Label().at((mx, my)).label(node, loc="right", fontsize=9))
|
||||||
|
else:
|
||||||
|
# 3+ connections: star wiring with junction dot
|
||||||
|
_draw_star_wires(d, positions)
|
||||||
|
xs = sorted(p[0] for p in positions)
|
||||||
|
ys = sorted(p[1] for p in positions)
|
||||||
|
d.add(
|
||||||
|
elm.Label().at((xs[len(xs) // 2], ys[len(ys) // 2]))
|
||||||
|
.label(node, loc="right", fontsize=9)
|
||||||
)
|
)
|
||||||
_label_two_terminal(d, placed, comp)
|
|
||||||
|
|
||||||
return d.get_imagedata("svg").decode()
|
return d.get_imagedata("svg").decode()
|
||||||
|
|
||||||
|
|||||||
@ -72,6 +72,15 @@ export function SpiceCell({ cell, isFirst, isLast }: SpiceCellProps) {
|
|||||||
[cell.id, cell.source, updateCellSource],
|
[cell.id, cell.source, updateCellSource],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Auto-generate schematic on first render if none exists
|
||||||
|
const hasAutoGenerated = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (schematicSvg || hasAutoGenerated.current) return;
|
||||||
|
if (!cell.source.trim() || cell.source.trim() === '* SPICE Netlist\n\nR1 in out 1k\nV1 in 0 DC 5\n\n.op\n.end') return;
|
||||||
|
hasAutoGenerated.current = true;
|
||||||
|
generateSchematic(cell.id);
|
||||||
|
}, [cell.id, cell.source, schematicSvg, generateSchematic]);
|
||||||
|
|
||||||
// Debounced auto-redraw: regenerate schematic 800ms after source changes.
|
// Debounced auto-redraw: regenerate schematic 800ms after source changes.
|
||||||
// Track the source at last generation to avoid retriggering from our own SVG updates.
|
// Track the source at last generation to avoid retriggering from our own SVG updates.
|
||||||
const lastGeneratedSource = useRef<string | null>(null);
|
const lastGeneratedSource = useRef<string | null>(null);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user