fix(wires): close both halves of the load-time endpoint race
The whenDefined recalc was necessary but not sufficient — measured on staging, the example route loses BOTH ways: - the element side: every settle timer can fire before the part is findable in the DOM. The pinInfo-ready poll in DynamicComponent now also re-derives wire endpoints the first time the part's pinInfo is actually readable, and waits up to 10s (the 2s cap was the corner- wire bug again on slow connections). - the wire side: loadExample stores wires with zeroed endpoints BEHIND awaits (library installs), potentially after every recalc has come and gone. A canvas effect now spots endpoints still at (0,0) and runs one deferred recalc; real coordinates land, the condition clears, no loop.
This commit is contained in:
parent
4b9fa3f0d0
commit
f791536007
|
|
@ -446,42 +446,41 @@ export const DynamicComponent: React.FC<DynamicComponentProps> = ({
|
|||
}
|
||||
return false;
|
||||
};
|
||||
// Wires resolve through the same pinInfo. The canvas re-derives wire
|
||||
// endpoints on load-settle timers (100/300/500ms), but an overlay-defined
|
||||
// custom element can upgrade LATER than the last timer — its chunk is
|
||||
// large — and then every wire to it stays snapped to the part's corner
|
||||
// until something moves. Recalculate when the tag actually defines,
|
||||
// however late that is (resolves immediately when already defined).
|
||||
let cancelled = false;
|
||||
customElements
|
||||
.whenDefined(metadata.tagName)
|
||||
.then(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (cancelled) return;
|
||||
try {
|
||||
useSimulatorStore.getState().recalculateAllWirePositions();
|
||||
} catch {
|
||||
// headless tests
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// invalid tag name: nothing will ever mount, nothing to recalc
|
||||
});
|
||||
// Wires resolve through the same pinInfo, and the canvas' load-settle
|
||||
// timers (100/300/500ms) can ALL fire before this element is findable —
|
||||
// an overlay-defined custom element upgrades when its (large) chunk
|
||||
// lands, and on the example route the wires themselves are stored after
|
||||
// further awaits. So: the first time THIS part's pinInfo is actually
|
||||
// readable from the DOM, re-derive every wire endpoint. Measured on
|
||||
// staging: without this, all four wires to the part sat on its corner
|
||||
// until the user nudged something.
|
||||
const tryWires = () => {
|
||||
try {
|
||||
const el = document.getElementById(id) as (HTMLElement & { pinInfo?: unknown[] }) | null;
|
||||
if (el && Array.isArray(el.pinInfo) && el.pinInfo.length > 0) {
|
||||
useSimulatorStore.getState().recalculateAllWirePositions();
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// headless tests
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (tryReseat()) {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}
|
||||
// Same cadence as the pinInfo-ready poll above: the custom element may
|
||||
// upgrade a few frames after React commits.
|
||||
const reseated = tryReseat();
|
||||
const wired = tryWires();
|
||||
if (reseated && wired) return;
|
||||
// Poll until both settle. 10s, not 2s: the overlay chunk that defines
|
||||
// the element can take that long on a slow connection, and giving up
|
||||
// early is exactly the corner-wire bug again.
|
||||
let done = { reseat: reseated, wires: wired };
|
||||
const interval = setInterval(() => {
|
||||
if (tryReseat()) clearInterval(interval);
|
||||
if (!done.reseat) done.reseat = tryReseat();
|
||||
if (!done.wires) done.wires = tryWires();
|
||||
if (done.reseat && done.wires) clearInterval(interval);
|
||||
}, 100);
|
||||
const timeout = setTimeout(() => clearInterval(interval), 2000);
|
||||
const timeout = setTimeout(() => clearInterval(interval), 10000);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearInterval(interval);
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2287,6 +2287,21 @@ export const SimulatorCanvas = ({ headerSlot }: SimulatorCanvasProps = {}) => {
|
|||
return () => timers.forEach((t) => clearTimeout(t));
|
||||
}, [components, recalculateAllWirePositions]);
|
||||
|
||||
// A wire endpoint still at (0,0) was STORED after every timer above had
|
||||
// already fired — the example loader writes wires with zeroed endpoints
|
||||
// behind several awaits (library installs), so on that route the timers
|
||||
// race and lose. One deferred recalc converges it; recalc writes real
|
||||
// coordinates (a pin position, or the part-corner fallback), so this
|
||||
// effect self-quiesces instead of looping.
|
||||
useEffect(() => {
|
||||
const unresolved = wires.some(
|
||||
(w) => (w.start.x === 0 && w.start.y === 0) || (w.end.x === 0 && w.end.y === 0),
|
||||
);
|
||||
if (!unresolved) return;
|
||||
const t = setTimeout(() => recalculateAllWirePositions(), 150);
|
||||
return () => clearTimeout(t);
|
||||
}, [wires, recalculateAllWirePositions]);
|
||||
|
||||
// Auto-pan/zoom to keep the board and all components visible after a project
|
||||
// import/load. We track the previous component count and only re-center when
|
||||
// the count jumps (indicating the user loaded a new circuit, not just added
|
||||
|
|
|
|||
Loading…
Reference in New Issue