fix(editor): via GetWidth layer fix + console tab/copy restore

- itemToJson: pass PADSTACK::ALL_LAYERS for vias — the layerless virtual
  PCB_VIA::GetWidth() is an assert trap since the padstack refactor, and the
  collab baseline/snapshot serializer hit it once per via per snapshot
  (big-board load = assert storm). Values were already correct; wire format
  unchanged (applyChanged's layerless SetWidth writes the same slot).
- console: closed state is a content-width bottom-left tab again (version
  badge + app bottom edge visible); opened footer panel unchanged.
- console: partial-selection copy works — wx's window-level keydown handler
  preventDefaults Ctrl/Cmd+C, so a capture-phase guard stops propagation to wx
  when the selection lives in the console; canvas mousedown collapses stale
  log selections so they can't steal the editor's own Ctrl+C.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HPtPBVLKQzaXTkirYgcVay
This commit is contained in:
Gergő Törcsvári 2026-08-17 12:33:21 +02:00
commit a0bbffe5c0
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
2 changed files with 76 additions and 25 deletions

View file

@ -999,6 +999,7 @@ export function WasmTool({
const [status, setStatus] = React.useState("Loading tool…");
const [logs, setLogs] = React.useState<string[]>([]);
const [showLog, setShowLog] = React.useState(false);
const consolePanelRef = React.useRef<HTMLDivElement>(null);
const [oomExhausted, setOomExhausted] = React.useState(false);
// Terminal failure, rendered INDEPENDENTLY of `ready`. The boot overlay only
// exists while `!ready`, so anything that killed the runtime after the editor
@ -1096,6 +1097,36 @@ export function WasmTool({
// exposes the style bridge, mounts the floating panel.
const [tunerMod, setTunerMod] = React.useState<TunerModule | null>(null);
// wx's window-level keydown handler forwards Ctrl/Cmd+C to the wasm app and
// preventDefaults it, so the browser's native "copy selection" never runs —
// log text could be selected but not copied. When the selection lives in the
// console panel, intercept the chord in the CAPTURE phase (ahead of wx's
// bubble-phase listener) and stop propagation; the default copy still fires.
// A canvas mousedown would normally collapse a selection, but wx
// preventDefaults that too — mirror it, or a stale log selection would keep
// stealing the editor's own Ctrl+C.
React.useEffect(() => {
const selectionInConsole = () => {
const sel = window.getSelection();
if (!sel || sel.isCollapsed || !sel.anchorNode) return false;
return consolePanelRef.current?.contains(sel.anchorNode) ?? false;
};
const onKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "c" && selectionInConsole())
e.stopPropagation();
};
const onPointerDown = (e: PointerEvent) => {
if (e.target instanceof HTMLCanvasElement && selectionInConsole())
window.getSelection()?.removeAllRanges();
};
window.addEventListener("keydown", onKeyDown, true);
window.addEventListener("pointerdown", onPointerDown, true);
return () => {
window.removeEventListener("keydown", onKeyDown, true);
window.removeEventListener("pointerdown", onPointerDown, true);
};
}, []);
const append = React.useCallback((msg: string) => {
// Mirror into the React-independent fatal-screen ring: if React ever
// unmounts itself on a crash, the DOM floor still has the full log.
@ -2481,33 +2512,47 @@ export function WasmTool({
must never end up underneath the thing reporting the failure. Forced
visible on a fatal even with chrome hidden, for the same reason. */}
{(!effectiveChromeHidden || fatal) && (
<div className="absolute bottom-0 left-0 right-0 z-40">
<div className="flex items-center bg-black/70">
/* Closed: a content-width tab pinned bottom-left (no right-0), so the
version badge and the app's bottom edge stay visible/clickable.
Open: the full-width footer panel. */
<div
ref={consolePanelRef}
className={
showLog ? "absolute bottom-0 left-0 right-0 z-40" : "absolute bottom-0 left-0 z-40"
}
>
{showLog ? (
<>
<div className="flex items-center bg-black/70">
<button
className="flex items-center gap-1 px-3 py-1 font-mono text-xs text-white"
onClick={() => setShowLog(false)}
>
<ChevronDown size={14} /> console ({logs.length})
</button>
<button
className="ml-auto px-3 py-1 font-mono text-xs text-white/70 hover:text-white"
onClick={() => {
void navigator.clipboard.writeText(logs.join("\n")).then(
() => append("[console] copied to clipboard"),
() => append("[console] clipboard copy failed"),
);
}}
>
copy
</button>
</div>
<pre className="max-h-64 select-text cursor-text overflow-auto bg-black/85 p-3 font-mono text-[11px] leading-tight text-green-300">
{logs.join("\n")}
</pre>
</>
) : (
<button
className="flex items-center gap-1 px-3 py-1 font-mono text-xs text-white"
onClick={() => setShowLog((s) => !s)}
className="flex items-center gap-1 bg-black/70 px-3 py-1 font-mono text-xs text-white"
onClick={() => setShowLog(true)}
>
{showLog ? <ChevronDown size={14} /> : <ChevronUp size={14} />} console
({logs.length})
<ChevronUp size={14} /> console ({logs.length})
</button>
{showLog && (
<button
className="ml-auto px-3 py-1 font-mono text-xs text-white/70 hover:text-white"
onClick={() => {
void navigator.clipboard.writeText(logs.join("\n")).then(
() => append("[console] copied to clipboard"),
() => append("[console] clipboard copy failed"),
);
}}
>
copy
</button>
)}
</div>
{showLog && (
<pre className="max-h-64 overflow-auto bg-black/85 p-3 font-mono text-[11px] leading-tight text-green-300">
{logs.join("\n")}
</pre>
)}
</div>
)}