From efb85e91977f9d2a7f5671b2d94ca02c4164c8c8 Mon Sep 17 00:00:00 2001 From: Aqil-Ahmad Date: Sat, 25 Oct 2025 12:09:45 +0500 Subject: [PATCH 1/3] new approach: modify render --- ...onent_doInitialPcbFootprintStringRender.ts | 9 --- lib/components/base-components/Renderable.ts | 8 ++- ...ad-footprint-pcbpath-selector-pcb.snap.svg | 1 + ...-kicad-footprint-pcbpath-selector.test.tsx | 58 +++++++++++++++++++ 4 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 tests/repros/__snapshots__/repro-kicad-footprint-pcbpath-selector-pcb.snap.svg create mode 100644 tests/repros/repro-kicad-footprint-pcbpath-selector.test.tsx diff --git a/lib/components/base-components/NormalComponent/NormalComponent_doInitialPcbFootprintStringRender.ts b/lib/components/base-components/NormalComponent/NormalComponent_doInitialPcbFootprintStringRender.ts index 550a9245c..0f27761df 100644 --- a/lib/components/base-components/NormalComponent/NormalComponent_doInitialPcbFootprintStringRender.ts +++ b/lib/components/base-components/NormalComponent/NormalComponent_doInitialPcbFootprintStringRender.ts @@ -56,7 +56,6 @@ export function NormalComponent_doInitialPcbFootprintStringRender( result.footprintCircuitJson, ) component.addAll(fpComponents) - component._markDirty("InitializePortsFromChildren") } catch (err) { const db = component.root?.db if (db && component.source_component_id && component.pcb_component_id) { @@ -102,7 +101,6 @@ export function NormalComponent_doInitialPcbFootprintStringRender( soup as any, ) component.addAll(fpComponents) - component._markDirty("InitializePortsFromChildren") } catch (err) { const db = component.root?.db if (db && component.source_component_id && component.pcb_component_id) { @@ -174,13 +172,6 @@ export function NormalComponent_doInitialPcbFootprintStringRender( if (!Array.isArray(result) && result.cadModel) { component._asyncFootprintCadModel = result.cadModel } - // Ensure existing Ports re-run PcbPortRender now that pads exist - for (const child of component.children) { - if (child.componentName === "Port") { - child._markDirty?.("PcbPortRender") - } - } - component._markDirty("InitializePortsFromChildren") } catch (err) { const db = component.root?.db if (db && component.source_component_id && component.pcb_component_id) { diff --git a/lib/components/base-components/Renderable.ts b/lib/components/base-components/Renderable.ts index 63fd9fd17..c27cef815 100644 --- a/lib/components/base-components/Renderable.ts +++ b/lib/components/base-components/Renderable.ts @@ -307,10 +307,14 @@ export abstract class Renderable implements IRenderable { if (hasIncompleteEffects) return } - // Check declared async dependencies for this phase within subtree const deps = asyncPhaseDependencies[phase] || [] for (const depPhase of deps) { - if (this._hasIncompleteAsyncEffectsInSubtreeForPhase(depPhase)) return + const root = (this as any).root + const boardComponent = root?._getBoard?.() || root?.children?.[0] || this + if ( + boardComponent._hasIncompleteAsyncEffectsInSubtreeForPhase?.(depPhase) + ) + return } this._emitRenderLifecycleEvent(phase, "start") diff --git a/tests/repros/__snapshots__/repro-kicad-footprint-pcbpath-selector-pcb.snap.svg b/tests/repros/__snapshots__/repro-kicad-footprint-pcbpath-selector-pcb.snap.svg new file mode 100644 index 000000000..cd5b93607 --- /dev/null +++ b/tests/repros/__snapshots__/repro-kicad-footprint-pcbpath-selector-pcb.snap.svg @@ -0,0 +1 @@ +R1R1R2R2 \ No newline at end of file diff --git a/tests/repros/repro-kicad-footprint-pcbpath-selector.test.tsx b/tests/repros/repro-kicad-footprint-pcbpath-selector.test.tsx new file mode 100644 index 000000000..6487a091a --- /dev/null +++ b/tests/repros/repro-kicad-footprint-pcbpath-selector.test.tsx @@ -0,0 +1,58 @@ +import { test, expect } from "bun:test" +import { getTestFixture } from "tests/fixtures/get-test-fixture" +import kicadModJson from "tests/fixtures/assets/R_0402_1005Metric.json" with { + type: "json", +} + +test("trace pcbPath selectors work with kicad footprints", async () => { + const { circuit } = getTestFixture() + + circuit.platform = { + footprintLibraryMap: { + kicad: async (footprintName: string) => { + return { + footprintCircuitJson: kicadModJson, + } + }, + }, + } + + circuit.add( + + + + + , + ) + + await circuit.renderUntilSettled() + + const selectorErrors = circuit.db.pcb_trace_error + .list() + .filter((e) => e.message?.includes("Could not resolve pcbPath selector")) + expect(selectorErrors.length).toBe(0) + + const pcbTrace = circuit.db.pcb_trace.list()[0] + expect(pcbTrace).toBeDefined() + expect(pcbTrace.route.length).toBeGreaterThanOrEqual(3) + + await expect(circuit).toMatchPcbSnapshot(import.meta.path) +}) From abe8d2ba717623db5e31df6e2a5201f1dba07873 Mon Sep 17 00:00:00 2001 From: Aqil-Ahmad Date: Sat, 25 Oct 2025 13:08:34 +0500 Subject: [PATCH 2/3] add helper for clean checks --- lib/components/base-components/Renderable.ts | 24 +++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/components/base-components/Renderable.ts b/lib/components/base-components/Renderable.ts index c27cef815..a4930949e 100644 --- a/lib/components/base-components/Renderable.ts +++ b/lib/components/base-components/Renderable.ts @@ -233,7 +233,7 @@ export abstract class Renderable implements IRenderable { return this._asyncEffects.some((effect) => !effect.complete) } - private _hasIncompleteAsyncEffectsInSubtreeForPhase( + protected _hasIncompleteAsyncEffectsInSubtreeForPhase( phase: RenderPhase, ): boolean { // Check self @@ -249,6 +249,14 @@ export abstract class Renderable implements IRenderable { return false } + private _getRootRenderable(): Renderable { + let node: Renderable = this + while (node.parent) { + node = node.parent as Renderable + } + return node + } + getCurrentRenderPhase(): RenderPhase | null { return this._currentRenderPhase } @@ -308,13 +316,13 @@ export abstract class Renderable implements IRenderable { } const deps = asyncPhaseDependencies[phase] || [] - for (const depPhase of deps) { - const root = (this as any).root - const boardComponent = root?._getBoard?.() || root?.children?.[0] || this - if ( - boardComponent._hasIncompleteAsyncEffectsInSubtreeForPhase?.(depPhase) - ) - return + if (deps.length > 0) { + const root = this._getRootRenderable() + const checkNode = (root.children?.[0] as Renderable) || root + for (const depPhase of deps) { + if (checkNode._hasIncompleteAsyncEffectsInSubtreeForPhase?.(depPhase)) + return + } } this._emitRenderLifecycleEvent(phase, "start") From 842623715fbdc61e67b471b49fed9fc6d355449b Mon Sep 17 00:00:00 2001 From: Aqil-Ahmad Date: Sat, 25 Oct 2025 14:59:19 +0500 Subject: [PATCH 3/3] port match fix --- lib/components/base-components/Renderable.ts | 6 +++--- lib/components/primitive-components/Port/Port.ts | 3 +++ .../repro-kicad-footprint-pcbpath-selector-pcb.snap.svg | 1 - .../repro-kicad-footprint-pcbpath-selector1-pcb.snap.svg | 1 + ...tsx => repro-kicad-footprint-pcbpath-selector1.test.tsx} | 0 5 files changed, 7 insertions(+), 4 deletions(-) delete mode 100644 tests/repros/__snapshots__/repro-kicad-footprint-pcbpath-selector-pcb.snap.svg create mode 100644 tests/repros/__snapshots__/repro-kicad-footprint-pcbpath-selector1-pcb.snap.svg rename tests/repros/{repro-kicad-footprint-pcbpath-selector.test.tsx => repro-kicad-footprint-pcbpath-selector1.test.tsx} (100%) diff --git a/lib/components/base-components/Renderable.ts b/lib/components/base-components/Renderable.ts index a4930949e..8ab458396 100644 --- a/lib/components/base-components/Renderable.ts +++ b/lib/components/base-components/Renderable.ts @@ -251,8 +251,8 @@ export abstract class Renderable implements IRenderable { private _getRootRenderable(): Renderable { let node: Renderable = this - while (node.parent) { - node = node.parent as Renderable + while (node.parent && node.parent instanceof Renderable) { + node = node.parent } return node } @@ -318,7 +318,7 @@ export abstract class Renderable implements IRenderable { const deps = asyncPhaseDependencies[phase] || [] if (deps.length > 0) { const root = this._getRootRenderable() - const checkNode = (root.children?.[0] as Renderable) || root + const checkNode = (root.children?.[0] as any) || root for (const depPhase of deps) { if (checkNode._hasIncompleteAsyncEffectsInSubtreeForPhase?.(depPhase)) return diff --git a/lib/components/primitive-components/Port/Port.ts b/lib/components/primitive-components/Port/Port.ts index f62f5f495..96a24d342 100644 --- a/lib/components/primitive-components/Port/Port.ts +++ b/lib/components/primitive-components/Port/Port.ts @@ -245,6 +245,9 @@ export class Port extends PrimitiveComponent { */ registerMatch(component: PrimitiveComponent) { this.matchedComponents.push(component) + if (this.renderPhaseStates.PcbPortRender.initialized && !this.pcb_port_id) { + this._markDirty("PcbPortRender") + } } getNameAndAliases() { const { _parsedProps: props } = this diff --git a/tests/repros/__snapshots__/repro-kicad-footprint-pcbpath-selector-pcb.snap.svg b/tests/repros/__snapshots__/repro-kicad-footprint-pcbpath-selector-pcb.snap.svg deleted file mode 100644 index cd5b93607..000000000 --- a/tests/repros/__snapshots__/repro-kicad-footprint-pcbpath-selector-pcb.snap.svg +++ /dev/null @@ -1 +0,0 @@ -R1R1R2R2 \ No newline at end of file diff --git a/tests/repros/__snapshots__/repro-kicad-footprint-pcbpath-selector1-pcb.snap.svg b/tests/repros/__snapshots__/repro-kicad-footprint-pcbpath-selector1-pcb.snap.svg new file mode 100644 index 000000000..57225ba00 --- /dev/null +++ b/tests/repros/__snapshots__/repro-kicad-footprint-pcbpath-selector1-pcb.snap.svg @@ -0,0 +1 @@ +R1R1R2R2 \ No newline at end of file diff --git a/tests/repros/repro-kicad-footprint-pcbpath-selector.test.tsx b/tests/repros/repro-kicad-footprint-pcbpath-selector1.test.tsx similarity index 100% rename from tests/repros/repro-kicad-footprint-pcbpath-selector.test.tsx rename to tests/repros/repro-kicad-footprint-pcbpath-selector1.test.tsx