Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions examples/example32-stale-edit-event-trace-dash.fixture.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { useMemo, useState } from "react"
import { su } from "@tscircuit/soup-util"
import { renderToCircuitJson } from "lib/dev/render-to-circuit-json"
import type { ManualEditEvent } from "lib/types/edit-events"
import { SchematicViewer } from "lib/index"
import type { CircuitJson } from "circuit-json"

const buildCircuit = (includeC2: boolean) =>
renderToCircuitJson(
<board width="14mm" height="10mm">
<resistor name="R1" resistance={1000} schX={-4} />
<capacitor name="C1" capacitance="1uF" schX={0} />
{includeC2 && <capacitor name="C2" capacitance="1uF" schX={4} />}
<trace from=".R1 .pin2" to=".C1 .pin1" />
{includeC2 && <trace from=".C1 .pin2" to=".C2 .pin1" />}
</board>,
) as CircuitJson

const findSchematicComponentIdByName = (
circuitJson: CircuitJson,
name: string,
) => {
const sourceComponent = su(circuitJson)
.source_component.list()
.find((c) => c.name === name)
if (!sourceComponent) return undefined
return su(circuitJson)
.schematic_component.list()
.find((c) => c.source_component_id === sourceComponent.source_component_id)
?.schematic_component_id
}

/**
* Regression fixture for the "stale edit event stops trace dashing" bug:
* once an edit event references a schematic_component_id that no longer
* exists in circuitJson, every edit event *after* it (including the one for
* the component you're actively dragging) used to silently stop getting its
* dashed-trace styling.
*/
export default () => {
const initialCircuitJson = useMemo(() => buildCircuit(true), [])
const [circuitJson, setCircuitJson] =
useState<CircuitJson>(initialCircuitJson)
const [editEvents, setEditEvents] = useState<ManualEditEvent[]>([])

const simulateStaleEditEvent = () => {
const staleComponentId = findSchematicComponentIdByName(
initialCircuitJson,
"C2",
)
if (!staleComponentId) return

setEditEvents([
{
edit_event_id: "stale-c2-edit",
edit_event_type: "edit_schematic_component_location",
schematic_component_id: staleComponentId,
original_center: { x: 4, y: 0 },
new_center: { x: 5, y: 1 },
in_progress: false,
created_at: Date.now(),
},
])
// Remove C2 from the rendered circuit so the edit event above is stale --
// it references a schematic_component_id that no longer exists.
setCircuitJson(buildCircuit(false))
}

return (
<div style={{ position: "relative", height: "100%" }}>
<div
style={{
position: "absolute",
top: "16px",
left: "16px",
zIndex: 1001,
maxWidth: "360px",
backgroundColor: "#fff",
padding: "12px",
borderRadius: "4px",
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
fontFamily: "sans-serif",
fontSize: "13px",
lineHeight: 1.5,
}}
>
<strong>Regression check: stale edit event trace dashing</strong>
<ol style={{ paddingLeft: "18px", margin: "8px 0" }}>
<li>
Click "Simulate stale edit + remove C2" — this queues an edit event
for C2, then removes C2 from circuitJson (mirroring a consumer
swapping in a new circuit while an old edit event is still around).
</li>
<li>
Drag R1 or C1. Their connected trace should turn dashed while
dragging. Before the fix, the stale C2 edit event stopped processing
early and no trace would dash.
</li>
</ol>
<button
type="button"
onClick={simulateStaleEditEvent}
style={{
padding: "8px 12px",
borderRadius: "4px",
border: "none",
backgroundColor: "#f44336",
color: "#fff",
cursor: "pointer",
}}
>
Simulate stale edit + remove C2
</button>
</div>
<SchematicViewer
circuitJson={circuitJson}
editEvents={editEvents}
onEditEvent={(event) => setEditEvents([...editEvents, event])}
containerStyle={{ height: "100%" }}
debugGrid
editingEnabled
/>
</div>
)
}
94 changes: 33 additions & 61 deletions lib/hooks/useChangeSchematicTracesForMovedComponents.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useEffect, useRef } from "react"
import { su } from "@tscircuit/soup-util"
import type { ManualEditEvent } from "../types/edit-events"
import type { CircuitJson } from "circuit-json"
import { getTraceIdsToDash } from "../utils/get-trace-ids-to-dash"

/**
* This hook makes traces dashed when their connected components are being moved
Expand Down Expand Up @@ -36,69 +36,41 @@ export const useChangeSchematicTracesForMovedComponents = ({
;(trace as any).style.animation = ""
}

// If there's an active edit event, make connected traces dashed
for (const editEvent of [
...editEvents,
...(activeEditEvent ? [activeEditEvent] : []),
]) {
if (
"schematic_component_id" in editEvent &&
editEvent.edit_event_type === "edit_schematic_component_location"
) {
const sch_component = su(circuitJson).schematic_component.get(
editEvent.schematic_component_id,
)
if (!sch_component) return
// Make traces connected to a moved (or moving) component dashed
const traceIdsToDash = getTraceIdsToDash({
circuitJson,
editEvents,
activeEditEvent,
})

const src_ports = su(circuitJson).source_port.list({
source_component_id: sch_component.source_component_id,
})
const src_port_ids = new Set(src_ports.map((sp) => sp.source_port_id))
const src_traces = su(circuitJson)
.source_trace.list()
.filter((st) =>
st.connected_source_port_ids?.some((spi: string) =>
src_port_ids.has(spi),
),
)
const src_trace_ids = new Set(
src_traces.map((st) => st.source_trace_id),
)
const schematic_traces = su(circuitJson)
.schematic_trace.list()
.filter((st) => src_trace_ids.has(st.source_trace_id!))
for (const schematicTraceId of traceIdsToDash) {
const traceElements = svg.querySelectorAll(
`[data-schematic-trace-id="${schematicTraceId}"] path`,
)
for (const traceElement of Array.from(traceElements)) {
if (traceElement.getAttribute("class")?.includes("invisible"))
continue
traceElement.setAttribute("stroke-dasharray", "20,20")
;(traceElement as any).style.animation =
"dash-animation 350ms linear infinite, pulse-animation 900ms linear infinite"

// Make the connected traces dashed
schematic_traces.forEach((trace) => {
const traceElements = svg.querySelectorAll(
`[data-schematic-trace-id="${trace.schematic_trace_id}"] path`,
)
for (const traceElement of Array.from(traceElements)) {
if (traceElement.getAttribute("class")?.includes("invisible"))
continue
traceElement.setAttribute("stroke-dasharray", "20,20")
;(traceElement as any).style.animation =
"dash-animation 350ms linear infinite, pulse-animation 900ms linear infinite"

if (!svg.querySelector("style#dash-animation")) {
const style = document.createElement("style")
style.id = "dash-animation"
style.textContent = `
@keyframes dash-animation {
to {
stroke-dashoffset: -40;
}
}
@keyframes pulse-animation {
0% { opacity: 0.6; }
50% { opacity: 0.2; }
100% { opacity: 0.6; }
}
`
svg.appendChild(style)
if (!svg.querySelector("style#dash-animation")) {
const style = document.createElement("style")
style.id = "dash-animation"
style.textContent = `
@keyframes dash-animation {
to {
stroke-dashoffset: -40;
}
}
@keyframes pulse-animation {
0% { opacity: 0.6; }
50% { opacity: 0.2; }
100% { opacity: 0.6; }
}
}
})
`
svg.appendChild(style)
}
}
}
}
Expand Down
78 changes: 78 additions & 0 deletions lib/utils/get-trace-ids-to-dash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { su } from "@tscircuit/soup-util"
import type { CircuitJson } from "circuit-json"
import type { ManualEditEvent } from "../types/edit-events"

/**
* Returns the schematic_trace_ids that should render dashed because they're
* connected to a component currently being moved (an active drag, or a
* queued edit event).
*
* A stale edit event -- one whose schematic_component_id no longer resolves
* against circuitJson, e.g. because the component was removed -- is skipped
* rather than aborting, so it doesn't prevent later, still-valid edit events
* from being processed.
*/
export const getTraceIdsToDash = ({
circuitJson,
editEvents,
activeEditEvent,
}: {
circuitJson: CircuitJson
editEvents: ManualEditEvent[]
activeEditEvent: ManualEditEvent | null
}): Set<string> => {
const traceIds = new Set<string>()

for (const editEvent of [
...editEvents,
...(activeEditEvent ? [activeEditEvent] : []),
]) {
if (
!("schematic_component_id" in editEvent) ||
editEvent.edit_event_type !== "edit_schematic_component_location"
) {
continue
}

const sch_component = su(circuitJson).schematic_component.get(
editEvent.schematic_component_id,
)
if (!sch_component) continue

const src_ports = su(circuitJson).source_port.list({
source_component_id: sch_component.source_component_id,
})
const src_port_ids = new Set(src_ports.map((sp) => sp.source_port_id))

// schematic_trace only reliably links back to source_trace via
// subcircuit_connectivity_map_key -- source_trace_id on schematic_trace
// is a display-style label (e.g. "R1.2-C1.1"), not a real
// source_trace_id, so it can't be used to join the two. This is the same
// key useSchematicNetHover uses to relate traces to nets.
const connectivityKeys = new Set(
su(circuitJson)
.source_trace.list()
.filter((st) =>
st.connected_source_port_ids?.some((spi: string) =>
src_port_ids.has(spi),
),
)
.map((st) => st.subcircuit_connectivity_map_key)
.filter((key): key is string => Boolean(key)),
)

const schematic_traces = su(circuitJson)
.schematic_trace.list()
.filter(
(st) =>
st.subcircuit_connectivity_map_key &&
connectivityKeys.has(st.subcircuit_connectivity_map_key),
)

for (const trace of schematic_traces) {
traceIds.add(trace.schematic_trace_id!)
}
}

return traceIds
}
Loading
Loading