fix(pcbnew): collab syncs real item layer — devirtualize Get/SetLayer; collab on by default
This commit is contained in:
parent
1bfa562a61
commit
b672c54189
3 changed files with 39 additions and 13 deletions
|
|
@ -191,6 +191,11 @@ test.describe("pcbnew collab bridge — single page", () => {
|
|||
expect((byId.get(TEXT1) as { text?: string }).text, "board text string emitted").toBe(
|
||||
"BOARDTEXT",
|
||||
);
|
||||
// The text is on F.SilkS (=5). Asserting the ACTUAL layer (not just "round-trips") catches the
|
||||
// GetLayer() emit bug, where every item reported layer 0 (F.Cu) and added items landed on the
|
||||
// wrong copper layer on the peer.
|
||||
expect((byId.get(TEXT1) as { layer?: number }).layer, "board text on F.SilkS (not stuck at 0)").toBe(5);
|
||||
expect((byId.get(SEG1) as { layer?: number }).layer, "segment on F.Cu").toBe(0);
|
||||
expect(hasAbort(testLogger), "no WASM abort").toBe(false);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
#include <kiway.h>
|
||||
#include <kiid.h>
|
||||
#include <layer_ids.h>
|
||||
#include <lset.h>
|
||||
#include <tool/coroutine.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <map>
|
||||
|
|
@ -107,6 +108,20 @@ bool isTrackType( KICAD_T t )
|
|||
return t == PCB_TRACE_T || t == PCB_ARC_T || t == PCB_VIA_T;
|
||||
}
|
||||
|
||||
// Read an item's layer WITHOUT the virtual GetLayer(). That virtual mis-dispatches in the
|
||||
// non-coroutine emit/snapshot context — it returns 0 (F_Cu) for EVERY item (the same asyncify
|
||||
// call_indirect class as eeschema's Move()), which silently put every collab-added item/track/
|
||||
// text on the top copper layer on the peer. A class-qualified `BOARD_ITEM::GetLayer()` is a
|
||||
// statically-bound (direct) call that just reads m_layer, bypassing call_indirect. Zones keep
|
||||
// their layer in m_layerSet (not m_layer), so use their non-virtual GetFirstLayer().
|
||||
int itemLayer( BOARD_ITEM* aItem )
|
||||
{
|
||||
if( aItem->Type() == PCB_ZONE_T )
|
||||
return (int) static_cast<ZONE*>( aItem )->GetFirstLayer();
|
||||
|
||||
return (int) aItem->BOARD_ITEM::GetLayer();
|
||||
}
|
||||
|
||||
// Iterate every board item the bridge syncs: the top-level items (tracks, footprints, drawings,
|
||||
// zones, groups) PLUS each footprint's TEXT children (fields = reference/value/user, and graphic
|
||||
// PCB_TEXT). The text children are visited by their OWN uuid because a silkscreen reference/value
|
||||
|
|
@ -155,7 +170,7 @@ json itemToJson( BOARD_ITEM* aItem )
|
|||
{ "type", toUtf8( aItem->GetClass() ) },
|
||||
{ "x", p.x }, // internal units (nm); integral, no quantization needed
|
||||
{ "y", p.y },
|
||||
{ "layer", (int) aItem->GetLayer() },
|
||||
{ "layer", itemLayer( aItem ) }, // devirtualized — aItem->GetLayer() mis-dispatches here
|
||||
};
|
||||
|
||||
if( isTrackType( aItem->Type() ) )
|
||||
|
|
@ -313,7 +328,9 @@ BOARD_ITEM* makeItem( BOARD& aBoard, const json& j )
|
|||
tr->SetStart( VECTOR2I( j.value( "sx", 0 ), j.value( "sy", 0 ) ) );
|
||||
tr->SetEnd( VECTOR2I( j.value( "ex", 0 ), j.value( "ey", 0 ) ) );
|
||||
tr->SetWidth( j.value( "width", 0 ) );
|
||||
tr->SetLayer( (PCB_LAYER_ID) j.value( "layer", (int) F_Cu ) );
|
||||
// SetLayer is virtual and (like GetLayer) mis-dispatches in this apply context → it no-ops,
|
||||
// leaving the item on the default layer. Class-qualify to a direct m_layer write.
|
||||
tr->BOARD_ITEM::SetLayer( (PCB_LAYER_ID) j.value( "layer", (int) F_Cu ) );
|
||||
item = tr;
|
||||
}
|
||||
// Via / zone: reconstruct natively from emitted geometry (the envelope-blob parse is
|
||||
|
|
@ -340,7 +357,9 @@ BOARD_ITEM* makeItem( BOARD& aBoard, const json& j )
|
|||
outline.emplace_back( p[0].get<int>(), p[1].get<int>() );
|
||||
}
|
||||
|
||||
zone->SetLayer( (PCB_LAYER_ID) j.value( "layer", (int) F_Cu ) );
|
||||
// Zones keep their layer in m_layerSet via SetLayer→SetLayerSet (both virtual, both no-op
|
||||
// here). Class-qualify ZONE::SetLayerSet to set it directly (else GetFirstLayer == -1).
|
||||
zone->ZONE::SetLayerSet( LSET( { (PCB_LAYER_ID) j.value( "layer", (int) F_Cu ) } ) );
|
||||
|
||||
if( outline.size() >= 3 )
|
||||
zone->AddPolygon( outline );
|
||||
|
|
@ -352,7 +371,7 @@ BOARD_ITEM* makeItem( BOARD& aBoard, const json& j )
|
|||
auto* txt = new PCB_TEXT( &aBoard );
|
||||
txt->SetText( wxString::FromUTF8( j.value( "text", "" ).c_str() ) );
|
||||
txt->SetPosition( VECTOR2I( j.value( "x", 0 ), j.value( "y", 0 ) ) );
|
||||
txt->SetLayer( (PCB_LAYER_ID) j.value( "layer", (int) F_SilkS ) );
|
||||
txt->BOARD_ITEM::SetLayer( (PCB_LAYER_ID) j.value( "layer", (int) F_SilkS ) ); // devirt
|
||||
|
||||
if( j.contains( "tw" ) )
|
||||
txt->SetTextSize( VECTOR2I( j.value( "tw", 0 ), j.value( "th", 0 ) ) );
|
||||
|
|
|
|||
|
|
@ -12,11 +12,12 @@ import { clog, cwarn } from "@/wasm/collab/debug";
|
|||
const COLLAB_TOOLS = new Set<Tool>(["pl_editor", "eeschema", "pcbnew"]);
|
||||
|
||||
/**
|
||||
* Opt-in collaborative editing (features/yjs-bridge). Enabled when the URL carries
|
||||
* `?collab=1` and the tool has the collab bridge. Open the same project URL in two tabs
|
||||
* with `?collab=1` to edit together: the channel is keyed to project+file, so both tabs
|
||||
* share one Y.Doc over BroadcastChannel. Editor edits (add/move items) fire the tool's
|
||||
* change hook → the bridge → the peer tab.
|
||||
* Collaborative editing (features/yjs-bridge), ON BY DEFAULT for any tool that has the
|
||||
* collab bridge. Open the same project URL in two tabs to edit together: the channel is
|
||||
* keyed to project+file, so both tabs share one Y.Doc over BroadcastChannel. Editor edits
|
||||
* (add/move items) fire the tool's change hook → the bridge → the peer tab.
|
||||
*
|
||||
* Opt OUT with `?collab=0` (or `collab=false`). Tools without a bridge are skipped anyway.
|
||||
*/
|
||||
async function maybeStartCollab(
|
||||
win: ToolWindow,
|
||||
|
|
@ -28,10 +29,10 @@ async function maybeStartCollab(
|
|||
onStatus: (t: string) => void;
|
||||
},
|
||||
): Promise<void> {
|
||||
const enabled = new URLSearchParams(win.location.search).get("collab");
|
||||
const collabParam = new URLSearchParams(win.location.search).get("collab");
|
||||
const mod = win.Module;
|
||||
clog("maybeStartCollab gate:", {
|
||||
collabParam: enabled,
|
||||
collabParam,
|
||||
tool: opts.tool,
|
||||
hasModule: !!mod,
|
||||
hasSnapshot: typeof mod?.kicadCollabSnapshot,
|
||||
|
|
@ -39,8 +40,9 @@ async function maybeStartCollab(
|
|||
url: win.location.href,
|
||||
});
|
||||
|
||||
if (!enabled || enabled === "0" || enabled === "false") {
|
||||
clog("disabled (no ?collab=1) — skipping");
|
||||
// On by default; only an explicit opt-out disables it.
|
||||
if (collabParam === "0" || collabParam === "false") {
|
||||
clog("disabled (?collab=0) — skipping");
|
||||
return;
|
||||
}
|
||||
if (!COLLAB_TOOLS.has(opts.tool)) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue