feat(drift-trio): phase B — action-catalog hooks + duplicate/lock wire fixes
17 kicadCollabTest* action primitives (eeschema: wire/junction/no-connect/ label/symbol/move/mirror/duplicate; pcbnew: track/via/text/zone/flip/ footprint-field/lock/move/duplicate), each a real SCH_/BOARD_COMMIT on the fiber so the listener → flushDiff emit runs as for UI edits; tool-unique names, merged-image safe. Fixes surfaced by the catalogs (0008 §10 #4–#8): eeschema adds SetParent before staging (Push silently skips listener notifications for unparented items), and pcbnew blobForItem now Formats non-footprints with the FILE writer + wrapInBoardEnvelope — SaveSelection's transfer copy cleared the locked flag, so (locked yes) never reached the doc. drift-trio.spec.ts gains full A/B-alternating catalogs with per-step landed gate + oracle sweep. Bumps kicad for the Duplicate child-uuid re-roll. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G5cAM9M6q34n5X4dbrfVvi
This commit is contained in:
parent
84cf459e16
commit
71a575d2d8
5 changed files with 570 additions and 14 deletions
2
kicad
2
kicad
|
|
@ -1 +1 @@
|
||||||
Subproject commit a005a34be0e0bc074c4d98bbaea51f59c0f7661f
|
Subproject commit 1ce0fb6ed09f01b180b1adec6f70bda36f8eae1d
|
||||||
|
|
@ -7,11 +7,13 @@ import {
|
||||||
TRIO_SCH,
|
TRIO_SCH,
|
||||||
type ToolCfg,
|
type ToolCfg,
|
||||||
SYM1,
|
SYM1,
|
||||||
|
WIRE1,
|
||||||
WIRE2,
|
WIRE2,
|
||||||
FP1,
|
FP1,
|
||||||
PAD1,
|
PAD1,
|
||||||
VIA1,
|
VIA1,
|
||||||
SEG1,
|
SEG1,
|
||||||
|
SEG2,
|
||||||
callHook,
|
callHook,
|
||||||
closeTrio,
|
closeTrio,
|
||||||
drift,
|
drift,
|
||||||
|
|
@ -245,3 +247,109 @@ test.describe("drift trio — pcbnew segment edit (envelope-parse change path)",
|
||||||
await closeTrio(trio);
|
await closeTrio(trio);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── S1 catalog: every phase-B action primitive, A/B alternating, sweep each ──
|
||||||
|
// One trio session per tool; each step runs on its actor, converges, and the
|
||||||
|
// full oracle sweep must stay silent. A step that returns a uuid parks it in
|
||||||
|
// `ctx` for later steps ("A adds a symbol → B moves THAT symbol").
|
||||||
|
|
||||||
|
type CatalogStep = {
|
||||||
|
name: string;
|
||||||
|
actor: "A" | "B";
|
||||||
|
run(page: import("@playwright/test").Page, ctx: Record<string, string>): Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function hookUuid(
|
||||||
|
page: import("@playwright/test").Page,
|
||||||
|
ctx: Record<string, string>,
|
||||||
|
key: string,
|
||||||
|
fn: string,
|
||||||
|
...args: (string | number)[]
|
||||||
|
): Promise<void> {
|
||||||
|
const uuid = await callHook<string>(page, fn, ...args);
|
||||||
|
expect(uuid, `${fn} must return a uuid`).toMatch(/[0-9a-f-]{36}/);
|
||||||
|
ctx[key] = uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hookOk(
|
||||||
|
page: import("@playwright/test").Page,
|
||||||
|
fn: string,
|
||||||
|
...args: (string | number | boolean)[]
|
||||||
|
): Promise<void> {
|
||||||
|
expect(await callHook<boolean>(page, fn, ...(args as (string | number)[])), `${fn} must hit`).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eeschema IU = 1e4/mm; pcbnew IU = 1e6/mm.
|
||||||
|
const SCH_CATALOG: CatalogStep[] = [
|
||||||
|
{ name: "A adds a wire", actor: "A", run: (p, c) => hookUuid(p, c, "wire", "kicadCollabTestAddWire", 600000, 600000, 700000, 600000) },
|
||||||
|
// A junction dropped mid-wire with no branch is REDUNDANT — eeschema's
|
||||||
|
// connectivity cleanup deletes it in the same commit (net-zero, the landed
|
||||||
|
// gate would time out). A branch wire off A's wire is the realistic edit;
|
||||||
|
// the cleanup auto-inserts the junction at the T, which then syncs.
|
||||||
|
{ name: "B branches a wire off A's wire", actor: "B", run: (p, c) => hookUuid(p, c, "branch", "kicadCollabTestAddWire", 650000, 600000, 650000, 650000) },
|
||||||
|
{ name: "A adds a local label", actor: "A", run: (p, c) => hookUuid(p, c, "label", "kicadCollabTestAddLabel", "label", "NET_A", 620000, 600000) },
|
||||||
|
{ name: "B adds a global label", actor: "B", run: (p, c) => hookUuid(p, c, "glabel", "kicadCollabTestAddLabel", "global", "VCC", 700000, 600000) },
|
||||||
|
{ name: "A adds a hier label", actor: "A", run: (p, c) => hookUuid(p, c, "hlabel", "kicadCollabTestAddLabel", "hier", "H1", 640000, 600000) },
|
||||||
|
{ name: "B adds a no-connect", actor: "B", run: (p, c) => hookUuid(p, c, "nc", "kicadCollabTestAddNoConnect", 710000, 600000) },
|
||||||
|
{ name: "A places a symbol from the doc lib", actor: "A", run: (p, c) => hookUuid(p, c, "sym2", "kicadCollabTestAddSymbol", "Device:R", 800000, 800000, "R2") },
|
||||||
|
{ name: "B moves that symbol", actor: "B", run: (p, c) => hookOk(p, "kicadCollabTestMoveSchItem", c.sym2!, 40000, 0) },
|
||||||
|
{ name: "A mirrors it", actor: "A", run: (p, c) => hookOk(p, "kicadCollabTestMirrorSchItem", c.sym2!, true) },
|
||||||
|
{ name: "B duplicates it", actor: "B", run: (p, c) => hookUuid(p, c, "sym3", "kicadCollabTestDuplicateSchItem", c.sym2!, 100000, 0) },
|
||||||
|
{ name: "A renames the duplicate's value", actor: "A", run: (p, c) => hookOk(p, "kicadCollabTestSetFieldText", c.sym3!, "dup-R") },
|
||||||
|
{ name: "B deletes the fixture wire", actor: "B", run: (p) => hookOk(p, "kicadCollabTestRemoveItem", WIRE1) },
|
||||||
|
];
|
||||||
|
|
||||||
|
const PCB_CATALOG: CatalogStep[] = [
|
||||||
|
{ name: "A adds a track", actor: "A", run: (p, c) => hookUuid(p, c, "track", "kicadCollabTestAddTrack", 50000000, 90000000, 60000000, 90000000, 300000, "F.Cu") },
|
||||||
|
{ name: "B adds a via", actor: "B", run: (p, c) => hookUuid(p, c, "via2", "kicadCollabTestAddVia", 55000000, 90000000, 800000, 400000) },
|
||||||
|
{ name: "A adds board text", actor: "A", run: (p, c) => hookUuid(p, c, "text", "kicadCollabTestAddBoardText", "T1", 70000000, 90000000, "F.SilkS") },
|
||||||
|
{ name: "B adds a zone", actor: "B", run: (p, c) => hookUuid(p, c, "zone", "kicadCollabTestAddZone", 30000000, 30000000, 40000000, 40000000, "F.Cu") },
|
||||||
|
{ name: "A flips the footprint", actor: "A", run: (p) => hookOk(p, "kicadCollabTestFlipBoardItem", FP1) },
|
||||||
|
{ name: "B edits the footprint value", actor: "B", run: (p) => hookOk(p, "kicadCollabTestSetFootprintField", FP1, "Value", "R-edited") },
|
||||||
|
{ name: "A locks a segment", actor: "A", run: (p) => hookOk(p, "kicadCollabTestSetBoardItemLocked", SEG2, true) },
|
||||||
|
{ name: "B moves the fixture via", actor: "B", run: (p) => hookOk(p, "kicadCollabTestMoveBoardItem", VIA1, 2000000, 0) },
|
||||||
|
{ name: "A duplicates the footprint", actor: "A", run: (p, c) => hookUuid(p, c, "fp2", "kicadCollabTestDuplicateBoardItem", FP1, 10000000, 0) },
|
||||||
|
{ name: "B deletes a fixture segment", actor: "B", run: (p) => hookOk(p, "kicadCollabTestRemoveItem", SEG1) },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [cfg, label, catalog] of [
|
||||||
|
[TRIO_SCH, "eeschema", SCH_CATALOG],
|
||||||
|
[TRIO_PCB, "pcbnew", PCB_CATALOG],
|
||||||
|
] as const) {
|
||||||
|
test.describe(`drift trio — ${label} S1 action catalog`, () => {
|
||||||
|
test.describe.configure({ timeout: 900000 });
|
||||||
|
|
||||||
|
test(`${label}: full catalog, A/B alternating, sweep after every step`, async ({
|
||||||
|
context,
|
||||||
|
testLogger,
|
||||||
|
}) => {
|
||||||
|
skipFirefox();
|
||||||
|
const room = `drift-trio-cat-${label}-${test.info().workerIndex}`;
|
||||||
|
const trio = await openTrio(context, cfg, room);
|
||||||
|
const ctx: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const step of catalog) {
|
||||||
|
await test.step(step.name, async () => {
|
||||||
|
const actor = step.actor === "A" ? trio.A : trio.B;
|
||||||
|
// The hooks commit on a fiber: settleConverged alone can pass on the
|
||||||
|
// PRE-action state (all tabs still equal) and the sweep then reads
|
||||||
|
// legitimate mid-propagation state as drift. Gate on the actor's own
|
||||||
|
// save changing first, so convergence is convergence ON the edit.
|
||||||
|
const before = await modelText(actor, cfg);
|
||||||
|
await step.run(actor, ctx);
|
||||||
|
await expect
|
||||||
|
.poll(() => modelText(actor, cfg), { timeout: 15000, intervals: [300] })
|
||||||
|
.not.toBe(before);
|
||||||
|
await settleConverged(trio, cfg);
|
||||||
|
await oracleSweep(trio, cfg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(await undoDepth(trio.C), "observer undo stack").toBe(0);
|
||||||
|
expect(hasAbort(testLogger), "no WASM abort").toBe(false);
|
||||||
|
await closeTrio(trio);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,15 @@ export const TRIO_SCH: ToolCfg = {
|
||||||
"kicadCollabTestRemoveItem",
|
"kicadCollabTestRemoveItem",
|
||||||
"kicadCollabTestSetFieldText",
|
"kicadCollabTestSetFieldText",
|
||||||
"kicadCollabTestUndoDepth",
|
"kicadCollabTestUndoDepth",
|
||||||
|
// drift-trio phase B action hooks
|
||||||
|
"kicadCollabTestAddWire",
|
||||||
|
"kicadCollabTestAddJunction",
|
||||||
|
"kicadCollabTestAddNoConnect",
|
||||||
|
"kicadCollabTestAddLabel",
|
||||||
|
"kicadCollabTestAddSymbol",
|
||||||
|
"kicadCollabTestMoveSchItem",
|
||||||
|
"kicadCollabTestMirrorSchItem",
|
||||||
|
"kicadCollabTestDuplicateSchItem",
|
||||||
],
|
],
|
||||||
fixture: `(kicad_sch
|
fixture: `(kicad_sch
|
||||||
\t(version 20250114)
|
\t(version 20250114)
|
||||||
|
|
@ -143,6 +152,16 @@ export const TRIO_PCB: ToolCfg = {
|
||||||
"kicadCollabTestSetPadSize",
|
"kicadCollabTestSetPadSize",
|
||||||
"kicadCollabTestMoveEndpoint",
|
"kicadCollabTestMoveEndpoint",
|
||||||
"kicadCollabTestUndoDepth",
|
"kicadCollabTestUndoDepth",
|
||||||
|
// drift-trio phase B action hooks
|
||||||
|
"kicadCollabTestAddTrack",
|
||||||
|
"kicadCollabTestAddVia",
|
||||||
|
"kicadCollabTestAddBoardText",
|
||||||
|
"kicadCollabTestAddZone",
|
||||||
|
"kicadCollabTestFlipBoardItem",
|
||||||
|
"kicadCollabTestSetFootprintField",
|
||||||
|
"kicadCollabTestSetBoardItemLocked",
|
||||||
|
"kicadCollabTestMoveBoardItem",
|
||||||
|
"kicadCollabTestDuplicateBoardItem",
|
||||||
],
|
],
|
||||||
fixture: `(kicad_pcb
|
fixture: `(kicad_pcb
|
||||||
\t(version 20241229)
|
\t(version 20241229)
|
||||||
|
|
|
||||||
|
|
@ -1390,6 +1390,208 @@ bool schCollabTestSetFieldText( std::string aId, std::string aText )
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ── drift-trio phase B action hooks (standalone-hardening 0008 §5) ───────────
|
||||||
|
// Creation/mutation primitives for the trio harness's action catalog. Each
|
||||||
|
// drives a REAL SCH_COMMIT on the fiber stack, so the SCHEMATIC_LISTENER →
|
||||||
|
// flushDiff emit path runs exactly as for a UI edit. Names are tool-unique
|
||||||
|
// (registered outside the KICAD_MERGED_EMBIND guard — same convention as
|
||||||
|
// kicadCollabTestSetFieldText), so the merged image needs no dispatcher.
|
||||||
|
|
||||||
|
// Commit a freshly-built item onto the current screen; returns its uuid.
|
||||||
|
static std::string schCollabTestCommitAdd( SCH_ITEM* aItem, const wxChar* aMsg )
|
||||||
|
{
|
||||||
|
SCH_EDIT_FRAME* fr = schFrame();
|
||||||
|
|
||||||
|
if( !fr )
|
||||||
|
{
|
||||||
|
delete aItem;
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
SCH_SCREEN* screen = fr->GetScreen();
|
||||||
|
|
||||||
|
// SCH_COMMIT::Push finds the SCHEMATIC via the ITEM's parent chain and
|
||||||
|
// silently skips every listener notification (OnItemsAdded → our emit)
|
||||||
|
// when it comes up null — a bare `new SCH_ITEM` has no parent yet, so
|
||||||
|
// parent it like the drawing tools do before staging.
|
||||||
|
aItem->SetParent( screen );
|
||||||
|
|
||||||
|
std::string id = toUtf8( aItem->m_Uuid.AsString() );
|
||||||
|
wxString msg( aMsg );
|
||||||
|
|
||||||
|
pcbjam_collab::runOnFiber( fr, [fr, aItem, screen, msg]() {
|
||||||
|
SCH_COMMIT commit( fr );
|
||||||
|
commit.Add( aItem, screen );
|
||||||
|
commit.Push( msg );
|
||||||
|
} );
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string schCollabTestAddWire( int aX1, int aY1, int aX2, int aY2 )
|
||||||
|
{
|
||||||
|
SCH_LINE* line = new SCH_LINE( VECTOR2I( aX1, aY1 ), LAYER_WIRE );
|
||||||
|
line->SetEndPoint( VECTOR2I( aX2, aY2 ) );
|
||||||
|
return schCollabTestCommitAdd( line, wxT( "Collab test add wire" ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string schCollabTestAddJunction( int aX, int aY )
|
||||||
|
{
|
||||||
|
return schCollabTestCommitAdd( new SCH_JUNCTION( VECTOR2I( aX, aY ) ),
|
||||||
|
wxT( "Collab test add junction" ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string schCollabTestAddNoConnect( int aX, int aY )
|
||||||
|
{
|
||||||
|
return schCollabTestCommitAdd( new SCH_NO_CONNECT( VECTOR2I( aX, aY ) ),
|
||||||
|
wxT( "Collab test add no-connect" ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// aKind: "label" | "global" | "hier".
|
||||||
|
std::string schCollabTestAddLabel( std::string aKind, std::string aText, int aX, int aY )
|
||||||
|
{
|
||||||
|
wxString txt = wxString::FromUTF8( aText.c_str() );
|
||||||
|
VECTOR2I pos( aX, aY );
|
||||||
|
SCH_LABEL_BASE* label;
|
||||||
|
|
||||||
|
if( aKind == "global" )
|
||||||
|
label = new SCH_GLOBALLABEL( pos, txt );
|
||||||
|
else if( aKind == "hier" )
|
||||||
|
label = new SCH_HIERLABEL( pos, txt );
|
||||||
|
else
|
||||||
|
label = new SCH_LABEL( pos, txt );
|
||||||
|
|
||||||
|
return schCollabTestCommitAdd( label, wxT( "Collab test add label" ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place another instance of a symbol ALREADY in the schematic's lib_symbols
|
||||||
|
// cache (the harness fixtures embed their defs — no external lib needed).
|
||||||
|
std::string schCollabTestAddSymbol( std::string aLibId, int aX, int aY, std::string aRef )
|
||||||
|
{
|
||||||
|
SCH_EDIT_FRAME* fr = schFrame();
|
||||||
|
|
||||||
|
if( !fr )
|
||||||
|
return "";
|
||||||
|
|
||||||
|
SCH_SCREEN* screen = fr->GetScreen();
|
||||||
|
wxString libIdStr = wxString::FromUTF8( aLibId.c_str() );
|
||||||
|
LIB_SYMBOL* libSym = nullptr;
|
||||||
|
|
||||||
|
for( const auto& [name, sym] : screen->GetLibSymbols() )
|
||||||
|
{
|
||||||
|
if( name == libIdStr )
|
||||||
|
{
|
||||||
|
libSym = sym;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if( !libSym )
|
||||||
|
return "";
|
||||||
|
|
||||||
|
LIB_ID libId;
|
||||||
|
|
||||||
|
if( libId.Parse( libIdStr ) >= 0 )
|
||||||
|
return "";
|
||||||
|
|
||||||
|
SCH_SHEET_PATH& sheet = fr->GetCurrentSheet();
|
||||||
|
SCH_SYMBOL* sym = new SCH_SYMBOL( *libSym, libId, &sheet, 1, 1, VECTOR2I( aX, aY ) );
|
||||||
|
sym->SetParent( screen ); // Push's SCHEMATIC lookup — see schCollabTestCommitAdd
|
||||||
|
sym->SetRef( &sheet, wxString::FromUTF8( aRef.c_str() ) );
|
||||||
|
|
||||||
|
std::string id = toUtf8( sym->m_Uuid.AsString() );
|
||||||
|
|
||||||
|
pcbjam_collab::runOnFiber( fr, [fr, sym, screen]() {
|
||||||
|
SCH_COMMIT commit( fr );
|
||||||
|
commit.Add( sym, screen );
|
||||||
|
commit.Push( wxT( "Collab test add symbol" ) );
|
||||||
|
} );
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// By-uuid variant of MoveFirst (same CallAfter + devirtualized move path).
|
||||||
|
bool schCollabTestMoveSchItem( std::string aId, int aDx, int aDy )
|
||||||
|
{
|
||||||
|
SCH_EDIT_FRAME* fr = schFrame();
|
||||||
|
|
||||||
|
if( !fr )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
SCH_SHEET_PATH path;
|
||||||
|
SCH_ITEM* item = fr->Schematic().ResolveItem( KIID( wxString::FromUTF8( aId.c_str() ) ),
|
||||||
|
&path, /*allowNull*/ true );
|
||||||
|
|
||||||
|
if( !item )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
SCH_SCREEN* screen = path.LastScreen();
|
||||||
|
fr->CallAfter( [fr, item, screen, aDx, aDy]() { collabTestMove( fr, item, screen, aDx, aDy ); } );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool schCollabTestMirrorSchItem( std::string aId, bool aHorizontal )
|
||||||
|
{
|
||||||
|
SCH_EDIT_FRAME* fr = schFrame();
|
||||||
|
|
||||||
|
if( !fr )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
SCH_SHEET_PATH path;
|
||||||
|
SCH_ITEM* item = fr->Schematic().ResolveItem( KIID( wxString::FromUTF8( aId.c_str() ) ),
|
||||||
|
&path, /*allowNull*/ true );
|
||||||
|
|
||||||
|
if( !item )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
SCH_SCREEN* screen = path.LastScreen();
|
||||||
|
|
||||||
|
pcbjam_collab::runOnFiber( fr, [fr, item, screen, aHorizontal]() {
|
||||||
|
SCH_COMMIT commit( fr );
|
||||||
|
commit.Modify( item, screen );
|
||||||
|
|
||||||
|
if( aHorizontal )
|
||||||
|
item->MirrorHorizontally( item->GetPosition().x );
|
||||||
|
else
|
||||||
|
item->MirrorVertically( item->GetPosition().y );
|
||||||
|
|
||||||
|
commit.Push( wxT( "Collab test mirror" ) );
|
||||||
|
} );
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duplicate (fresh uuids — exercises the Clone()/uuid-churn drift class).
|
||||||
|
std::string schCollabTestDuplicateSchItem( std::string aId, int aDx, int aDy )
|
||||||
|
{
|
||||||
|
SCH_EDIT_FRAME* fr = schFrame();
|
||||||
|
|
||||||
|
if( !fr )
|
||||||
|
return "";
|
||||||
|
|
||||||
|
SCH_SHEET_PATH path;
|
||||||
|
SCH_ITEM* item = fr->Schematic().ResolveItem( KIID( wxString::FromUTF8( aId.c_str() ) ),
|
||||||
|
&path, /*allowNull*/ true );
|
||||||
|
|
||||||
|
if( !item )
|
||||||
|
return "";
|
||||||
|
|
||||||
|
SCH_SCREEN* screen = path.LastScreen();
|
||||||
|
SCH_ITEM* dup = item->Duplicate( /*addToParentGroup*/ false );
|
||||||
|
dup->Move( VECTOR2I( aDx, aDy ) );
|
||||||
|
|
||||||
|
std::string id = toUtf8( dup->m_Uuid.AsString() );
|
||||||
|
|
||||||
|
pcbjam_collab::runOnFiber( fr, [fr, dup, screen]() {
|
||||||
|
SCH_COMMIT commit( fr );
|
||||||
|
commit.Add( dup, screen );
|
||||||
|
commit.Push( wxT( "Collab test duplicate" ) );
|
||||||
|
} );
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Programmatically save the in-memory schematic to a .kicad_sch file, without
|
// Programmatically save the in-memory schematic to a .kicad_sch file, without
|
||||||
// driving the Save As dialog — eeschema's analogue of pl_editor's
|
// driving the Save As dialog — eeschema's analogue of pl_editor's
|
||||||
// kicadSaveDrawingSheet. Serializes the root sheet via the same SCH_IO_KICAD_SEXPR
|
// kicadSaveDrawingSheet. Serializes the root sheet via the same SCH_IO_KICAD_SEXPR
|
||||||
|
|
@ -1768,6 +1970,15 @@ EMSCRIPTEN_BINDINGS(eeschema) {
|
||||||
function("kicadSaveSchematic", &kicadSaveSchematic);
|
function("kicadSaveSchematic", &kicadSaveSchematic);
|
||||||
// eeschema-only ysync-review repro hook (name not shared with pcbnew).
|
// eeschema-only ysync-review repro hook (name not shared with pcbnew).
|
||||||
function("kicadCollabTestSetFieldText", &schCollabTestSetFieldText);
|
function("kicadCollabTestSetFieldText", &schCollabTestSetFieldText);
|
||||||
|
// drift-trio phase B action hooks (tool-unique names, merged-image safe).
|
||||||
|
function("kicadCollabTestAddWire", &schCollabTestAddWire);
|
||||||
|
function("kicadCollabTestAddJunction", &schCollabTestAddJunction);
|
||||||
|
function("kicadCollabTestAddNoConnect", &schCollabTestAddNoConnect);
|
||||||
|
function("kicadCollabTestAddLabel", &schCollabTestAddLabel);
|
||||||
|
function("kicadCollabTestAddSymbol", &schCollabTestAddSymbol);
|
||||||
|
function("kicadCollabTestMoveSchItem", &schCollabTestMoveSchItem);
|
||||||
|
function("kicadCollabTestMirrorSchItem", &schCollabTestMirrorSchItem);
|
||||||
|
function("kicadCollabTestDuplicateSchItem", &schCollabTestDuplicateSchItem);
|
||||||
|
|
||||||
#ifndef KICAD_MERGED_EMBIND
|
#ifndef KICAD_MERGED_EMBIND
|
||||||
// JS names ALSO registered by pcbnew_embind.cpp — in the merged image these are
|
// JS names ALSO registered by pcbnew_embind.cpp — in the merged image these are
|
||||||
|
|
|
||||||
|
|
@ -331,10 +331,15 @@ public:
|
||||||
// differ by exactly CTL_OMIT_FOOTPRINT_VERSION (pcb_io_kicad_sexpr.h), so clipboard form emits
|
// differ by exactly CTL_OMIT_FOOTPRINT_VERSION (pcb_io_kicad_sexpr.h), so clipboard form emits
|
||||||
// (footprint "Lib:C1206" (version 20260206) (generator "pcbnew") (generator_version "10.0") …)
|
// (footprint "Lib:C1206" (version 20260206) (generator "pcbnew") (generator_version "10.0") …)
|
||||||
// — three tokens a board-embedded footprint never has (pcb_io_kicad_sexpr.cpp ~1201). Every
|
// — three tokens a board-embedded footprint never has (pcb_io_kicad_sexpr.cpp ~1201). Every
|
||||||
// footprint of every board drifted on those three lines. For non-footprints the two control
|
// footprint of every board drifted on those three lines.
|
||||||
// sets are identical (the bit only gates footprint output), so the SaveSelection path below
|
//
|
||||||
// is already file-equivalent and keeps its `(kicad_pcb … <layers> <item>)` envelope, which the
|
// Non-footprints DON'T use SaveSelection either (drift-trio finding, standalone-hardening
|
||||||
// parser requires — bare `(segment`/`(via`/`(zone` are not accepted at top level.
|
// 0008 §10): its "make safe to transfer" step clears the locked flag on the copy
|
||||||
|
// (kicad_clipboard.cpp ~416) — clipboard semantics; pastes are unlocked — so `(locked yes)`,
|
||||||
|
// real file content, vanished from every track/via/zone/text blob and a peer's lock never
|
||||||
|
// reached the doc. Instead the live item is Formatted directly with the FILE writer and
|
||||||
|
// wrapped in the same synthetic `(kicad_pcb … <layers> <item>)` envelope the apply path
|
||||||
|
// already parses (the parser rejects bare `(segment`/`(via`/`(zone` at top level).
|
||||||
//
|
//
|
||||||
// Footprints also DON'T go through SaveSelection: its "make safe to transfer" step copies the
|
// Footprints also DON'T go through SaveSelection: its "make safe to transfer" step copies the
|
||||||
// footprint, and FOOTPRINT's copy ctor ASSIGNS the mandatory fields into the new footprint's
|
// footprint, and FOOTPRINT's copy ctor ASSIGNS the mandatory fields into the new footprint's
|
||||||
|
|
@ -343,6 +348,8 @@ public:
|
||||||
// breaking the wire's identity-by-uuid (every emit would read as field remove+add, and round
|
// breaking the wire's identity-by-uuid (every emit would read as field remove+add, and round
|
||||||
// trips lose the field uuids). The copy ctor now restores those uuids itself (footprint.cpp),
|
// trips lose the field uuids). The copy ctor now restores those uuids itself (footprint.cpp),
|
||||||
// but we keep making the safety copy here so the live item is never mutated.
|
// but we keep making the safety copy here so the live item is never mutated.
|
||||||
|
std::string wrapInBoardEnvelope( BOARD& aBoard, const std::string& aItemSexpr );
|
||||||
|
|
||||||
std::string blobForItem( BOARD* aBoard, BOARD_ITEM* aItem )
|
std::string blobForItem( BOARD* aBoard, BOARD_ITEM* aItem )
|
||||||
{
|
{
|
||||||
if( aItem->Type() == PCB_FOOTPRINT_T )
|
if( aItem->Type() == PCB_FOOTPRINT_T )
|
||||||
|
|
@ -372,16 +379,16 @@ std::string blobForItem( BOARD* aBoard, BOARD_ITEM* aItem )
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
PCB_SELECTION sel;
|
// Direct file-writer Format of the LIVE item (no mutation, unlike
|
||||||
sel.Add( aItem ); // pointer-only insert; no mutation of the live item
|
// SaveSelection's transfer copy) + the apply path's own envelope.
|
||||||
|
WIRE_BOARD_IO io( aBoard );
|
||||||
|
STRING_FORMATTER fmt;
|
||||||
|
io.SetOutputFormatter( &fmt );
|
||||||
|
io.Format( aItem );
|
||||||
|
|
||||||
CLIPBOARD_IO io;
|
std::string body = fmt.GetString();
|
||||||
io.SetBoard( aBoard );
|
KICAD_FORMAT::Prettify( body, KICAD_FORMAT::FORMAT_MODE::COMPACT_TEXT_PROPERTIES );
|
||||||
|
return wrapInBoardEnvelope( *aBoard, body );
|
||||||
std::string out;
|
|
||||||
io.SetWriter( [&out]( const wxString& s ) { out = std::string( s.utf8_str() ); } );
|
|
||||||
io.SaveSelection( sel, /*isFootprintEditor*/ false );
|
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A bare `(footprint …)` blob carries no `(version …)`, but the parser NEEDS one: it starts at
|
// A bare `(footprint …)` blob carries no `(version …)`, but the parser NEEDS one: it starts at
|
||||||
|
|
@ -2006,6 +2013,207 @@ bool pcbCollabTestMoveEndpoint( std::string aId, int aDx, int aDy )
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── drift-trio phase B action hooks (standalone-hardening 0008 §5) ───────────
|
||||||
|
// Creation/mutation primitives for the trio harness's action catalog. Each
|
||||||
|
// drives a REAL BOARD_COMMIT on the fiber stack, so the BOARD_LISTENER →
|
||||||
|
// flushDiff emit path runs exactly as for a UI edit. Names are tool-unique
|
||||||
|
// (registered outside the KICAD_MERGED_EMBIND guard — same convention as
|
||||||
|
// kicadCollabTestSetPadSize), so the merged image needs no dispatcher.
|
||||||
|
|
||||||
|
// Commit a freshly-built board item; returns its uuid.
|
||||||
|
static std::string pcbCollabTestCommitAdd( BOARD_ITEM* aItem, const wxChar* aMsg )
|
||||||
|
{
|
||||||
|
PCB_EDIT_FRAME* fr = pcbFrame();
|
||||||
|
|
||||||
|
if( !fr )
|
||||||
|
{
|
||||||
|
delete aItem;
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string id = toUtf8( aItem->m_Uuid.AsString() );
|
||||||
|
wxString msg( aMsg );
|
||||||
|
|
||||||
|
pcbjam_collab::runOnFiber( fr, [fr, aItem, msg]() {
|
||||||
|
BOARD_COMMIT commit( fr );
|
||||||
|
commit.Add( aItem );
|
||||||
|
commit.Push( msg );
|
||||||
|
} );
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string pcbCollabTestAddTrack( int aX1, int aY1, int aX2, int aY2, int aWidth,
|
||||||
|
std::string aLayer )
|
||||||
|
{
|
||||||
|
PCB_EDIT_FRAME* fr = pcbFrame();
|
||||||
|
|
||||||
|
if( !fr )
|
||||||
|
return "";
|
||||||
|
|
||||||
|
BOARD* board = fr->GetBoard();
|
||||||
|
PCB_TRACK* track = new PCB_TRACK( board );
|
||||||
|
track->SetStart( VECTOR2I( aX1, aY1 ) );
|
||||||
|
track->SetEnd( VECTOR2I( aX2, aY2 ) );
|
||||||
|
track->SetWidth( aWidth );
|
||||||
|
track->SetLayer( board->GetLayerID( wxString::FromUTF8( aLayer.c_str() ) ) );
|
||||||
|
return pcbCollabTestCommitAdd( track, wxT( "Collab test add track" ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string pcbCollabTestAddVia( int aX, int aY, int aSize, int aDrill )
|
||||||
|
{
|
||||||
|
PCB_EDIT_FRAME* fr = pcbFrame();
|
||||||
|
|
||||||
|
if( !fr )
|
||||||
|
return "";
|
||||||
|
|
||||||
|
PCB_VIA* via = new PCB_VIA( fr->GetBoard() );
|
||||||
|
via->SetPosition( VECTOR2I( aX, aY ) );
|
||||||
|
via->SetWidth( aSize );
|
||||||
|
via->SetDrill( aDrill );
|
||||||
|
return pcbCollabTestCommitAdd( via, wxT( "Collab test add via" ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string pcbCollabTestAddBoardText( std::string aText, int aX, int aY, std::string aLayer )
|
||||||
|
{
|
||||||
|
PCB_EDIT_FRAME* fr = pcbFrame();
|
||||||
|
|
||||||
|
if( !fr )
|
||||||
|
return "";
|
||||||
|
|
||||||
|
BOARD* board = fr->GetBoard();
|
||||||
|
PCB_TEXT* text = new PCB_TEXT( board );
|
||||||
|
text->SetText( wxString::FromUTF8( aText.c_str() ) );
|
||||||
|
text->SetTextPos( VECTOR2I( aX, aY ) );
|
||||||
|
text->SetLayer( board->GetLayerID( wxString::FromUTF8( aLayer.c_str() ) ) );
|
||||||
|
return pcbCollabTestCommitAdd( text, wxT( "Collab test add text" ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rectangular unfilled zone outline on one layer.
|
||||||
|
std::string pcbCollabTestAddZone( int aX1, int aY1, int aX2, int aY2, std::string aLayer )
|
||||||
|
{
|
||||||
|
PCB_EDIT_FRAME* fr = pcbFrame();
|
||||||
|
|
||||||
|
if( !fr )
|
||||||
|
return "";
|
||||||
|
|
||||||
|
BOARD* board = fr->GetBoard();
|
||||||
|
ZONE* zone = new ZONE( board );
|
||||||
|
zone->SetLayer( board->GetLayerID( wxString::FromUTF8( aLayer.c_str() ) ) );
|
||||||
|
zone->Outline()->NewOutline();
|
||||||
|
zone->Outline()->Append( aX1, aY1 );
|
||||||
|
zone->Outline()->Append( aX2, aY1 );
|
||||||
|
zone->Outline()->Append( aX2, aY2 );
|
||||||
|
zone->Outline()->Append( aX1, aY2 );
|
||||||
|
return pcbCollabTestCommitAdd( zone, wxT( "Collab test add zone" ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pcbCollabTestFlipBoardItem( std::string aId )
|
||||||
|
{
|
||||||
|
PCB_EDIT_FRAME* fr = pcbFrame();
|
||||||
|
BOARD_ITEM* item = testResolve( fr, aId );
|
||||||
|
|
||||||
|
if( !item )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
pcbjam_collab::runOnFiber( fr, [fr, item]() {
|
||||||
|
BOARD_COMMIT commit( fr );
|
||||||
|
commit.Modify( item );
|
||||||
|
item->Flip( item->GetPosition(), FLIP_DIRECTION::LEFT_RIGHT );
|
||||||
|
commit.Push( wxT( "Collab test flip" ) );
|
||||||
|
} );
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// aField: "Reference" | "Value" (the two mandatory text fields).
|
||||||
|
bool pcbCollabTestSetFootprintField( std::string aId, std::string aField, std::string aText )
|
||||||
|
{
|
||||||
|
PCB_EDIT_FRAME* fr = pcbFrame();
|
||||||
|
BOARD_ITEM* item = testResolve( fr, aId );
|
||||||
|
|
||||||
|
if( !item || item->Type() != PCB_FOOTPRINT_T )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
FOOTPRINT* fp = static_cast<FOOTPRINT*>( item );
|
||||||
|
wxString text = wxString::FromUTF8( aText.c_str() );
|
||||||
|
bool isRef = ( aField == "Reference" );
|
||||||
|
|
||||||
|
if( !isRef && aField != "Value" )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
pcbjam_collab::runOnFiber( fr, [fr, fp, text, isRef]() {
|
||||||
|
BOARD_COMMIT commit( fr );
|
||||||
|
commit.Modify( fp );
|
||||||
|
|
||||||
|
if( isRef )
|
||||||
|
fp->SetReference( text );
|
||||||
|
else
|
||||||
|
fp->SetValue( text );
|
||||||
|
|
||||||
|
commit.Push( wxT( "Collab test footprint field" ) );
|
||||||
|
} );
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pcbCollabTestSetBoardItemLocked( std::string aId, bool aLocked )
|
||||||
|
{
|
||||||
|
PCB_EDIT_FRAME* fr = pcbFrame();
|
||||||
|
BOARD_ITEM* item = testResolve( fr, aId );
|
||||||
|
|
||||||
|
if( !item )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
pcbjam_collab::runOnFiber( fr, [fr, item, aLocked]() {
|
||||||
|
BOARD_COMMIT commit( fr );
|
||||||
|
commit.Modify( item );
|
||||||
|
item->SetLocked( aLocked );
|
||||||
|
commit.Push( wxT( "Collab test lock" ) );
|
||||||
|
} );
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// By-uuid variant of MoveFirst (same fiber + BOARD_COMMIT body).
|
||||||
|
bool pcbCollabTestMoveBoardItem( std::string aId, int aDx, int aDy )
|
||||||
|
{
|
||||||
|
PCB_EDIT_FRAME* fr = pcbFrame();
|
||||||
|
BOARD_ITEM* item = testResolve( fr, aId );
|
||||||
|
|
||||||
|
if( !item )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
pcbjam_collab::runOnFiber( fr, [fr, item, aDx, aDy]() { collabTestMove( fr, item, aDx, aDy ); } );
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duplicate (fresh uuids — exercises the FOOTPRINT copy-ctor uuid class).
|
||||||
|
std::string pcbCollabTestDuplicateBoardItem( std::string aId, int aDx, int aDy )
|
||||||
|
{
|
||||||
|
PCB_EDIT_FRAME* fr = pcbFrame();
|
||||||
|
BOARD_ITEM* item = testResolve( fr, aId );
|
||||||
|
|
||||||
|
if( !item )
|
||||||
|
return "";
|
||||||
|
|
||||||
|
if( item->GetParentFootprint() )
|
||||||
|
return ""; // duplicate roots only
|
||||||
|
|
||||||
|
BOARD_ITEM* dup = item->Duplicate( /*addToParentGroup*/ false );
|
||||||
|
dup->Move( VECTOR2I( aDx, aDy ) );
|
||||||
|
|
||||||
|
std::string id = toUtf8( dup->m_Uuid.AsString() );
|
||||||
|
|
||||||
|
pcbjam_collab::runOnFiber( fr, [fr, dup]() {
|
||||||
|
BOARD_COMMIT commit( fr );
|
||||||
|
commit.Add( dup );
|
||||||
|
commit.Push( wxT( "Collab test duplicate" ) );
|
||||||
|
} );
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
// Wrapper to return footprints as vector for JS iteration
|
// Wrapper to return footprints as vector for JS iteration
|
||||||
std::vector<FOOTPRINT*> Board_GetFootprints(BOARD* board) {
|
std::vector<FOOTPRINT*> Board_GetFootprints(BOARD* board) {
|
||||||
if (!board) return {};
|
if (!board) return {};
|
||||||
|
|
@ -2078,6 +2286,16 @@ EMSCRIPTEN_BINDINGS(pcbnew) {
|
||||||
// pcbnew-only ysync-review repro hooks (names not shared with eeschema).
|
// pcbnew-only ysync-review repro hooks (names not shared with eeschema).
|
||||||
function("kicadCollabTestSetPadSize", &pcbCollabTestSetPadSize);
|
function("kicadCollabTestSetPadSize", &pcbCollabTestSetPadSize);
|
||||||
function("kicadCollabTestMoveEndpoint", &pcbCollabTestMoveEndpoint);
|
function("kicadCollabTestMoveEndpoint", &pcbCollabTestMoveEndpoint);
|
||||||
|
// drift-trio phase B action hooks (tool-unique names, merged-image safe).
|
||||||
|
function("kicadCollabTestAddTrack", &pcbCollabTestAddTrack);
|
||||||
|
function("kicadCollabTestAddVia", &pcbCollabTestAddVia);
|
||||||
|
function("kicadCollabTestAddBoardText", &pcbCollabTestAddBoardText);
|
||||||
|
function("kicadCollabTestAddZone", &pcbCollabTestAddZone);
|
||||||
|
function("kicadCollabTestFlipBoardItem", &pcbCollabTestFlipBoardItem);
|
||||||
|
function("kicadCollabTestSetFootprintField", &pcbCollabTestSetFootprintField);
|
||||||
|
function("kicadCollabTestSetBoardItemLocked", &pcbCollabTestSetBoardItemLocked);
|
||||||
|
function("kicadCollabTestMoveBoardItem", &pcbCollabTestMoveBoardItem);
|
||||||
|
function("kicadCollabTestDuplicateBoardItem", &pcbCollabTestDuplicateBoardItem);
|
||||||
|
|
||||||
#ifndef KICAD_MERGED_EMBIND
|
#ifndef KICAD_MERGED_EMBIND
|
||||||
// JS names ALSO registered by eeschema_embind.cpp — in the merged image these are
|
// JS names ALSO registered by eeschema_embind.cpp — in the merged image these are
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue