← Commit history

Schematic edits honour save:true; save defaults to false (each Fusion save is a cloud version), said in each describe

Colby Knox ·134b0f8f0b ·15d ago ·parent 1d657d6
4 files changed +43−15
CHANGELOG.md+7
@@ -3,6 +3,13 @@ All notable changes to the Adom Bridge Fusion 360 bridge, newest first. Canonical: https://wiki.adom.inc/adom/fusion-bridge ยท GitHub backup: `adom-inc/fusion-bridge` +## 1.12.3 (unreleased)++- Schematic edits honour `save: true`: after a read-back edit the design is saved in place (as+  `fusion_save_document`). `save` defaults to false, as the standard's conventions now allow, because each Fusion+  save is a new cloud version; every edit verb's describe says so. A never-saved design answers `saved: false` with+  the save's reason (with several open documents named Untitled, that reason can name the wrong one).+ ## 1.12.2 (2026-10-27, insiders)  The EDA Bridge Standard's schematic verbs (`sch_*`, agreed in standard 0.2.0): all 21, each read back on a live
contracts/describe.json+13
@@ -3578,6 +3578,7 @@         "expectedRevision": "the revision you last read (refused with stale_schematic otherwise)",         "label": "str",         "points": "[[x, y], ...] (required)",+        "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)",         "sheet": "int, default 1"       },       "longRunning": false,@@ -3608,6 +3609,7 @@         "entries": "[{at: [x, y] (the wire end), direction?: down-right|up-right|down-left|up-left, member?: str, sizeMm?: 2.54}] (required)",         "expectDocument": "optional",         "expectedRevision": "str",+        "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)",         "sheet": "int, default 1"       },       "longRunning": false,@@ -3634,6 +3636,7 @@         "expectDocument": "optional: the design's name",         "expectedRevision": "the revision you last read (refused with stale_schematic otherwise)",         "pairs": "[{from: \"U1.1\", to: \"R5.2\", first?: h|v, via?: [[x, y]]}] (required)",+        "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)",         "sheet": "int, default 1"       },       "longRunning": false,@@ -3668,6 +3671,7 @@         "members": "[str]",         "pins": "[Ref.Pin] in member order (required)",         "prefix": "str, default D",+        "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)",         "sheet": "int, default 1",         "start": "int, default 0"       },@@ -3688,6 +3692,7 @@       "input": {         "dryRun": "bool, default false",         "expectDocument": "optional",+        "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)",         "sheet": "int: must be the next number (default it)"       },       "longRunning": false,@@ -3716,6 +3721,7 @@         "expectedRevision": "the revision you last read (refused with stale_schematic otherwise)",         "from": "[x, y]",         "ids": "[wire id]",+        "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)",         "sheet": "int, default 1",         "to": "[x, y]"       },@@ -3748,6 +3754,7 @@         "edits": "[{id | at | net, moveTo?: [x, y], by?: [dx, dy], rotation?: deg, rename?: str}] (required)",         "expectDocument": "optional: the design's name",         "expectedRevision": "the revision you last read (refused with stale_schematic otherwise)",+        "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)",         "sheet": "int, default 1"       },       "longRunning": false,@@ -3769,6 +3776,7 @@         "dryRun": "bool, default TRUE",         "expectDocument": "optional",         "expectedRevision": "str",+        "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)",         "sheet": "int, default 1",         "toleranceMm": "default 0.5"       },@@ -3852,6 +3860,7 @@         "force": "bool, default false",         "points": "[[x, y]]",         "remove": "[id | [x, y]]",+        "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)",         "sheet": "int, default 1"       },       "longRunning": false,@@ -3886,6 +3895,7 @@         "dryRun": "bool, default false: answer the plan and change nothing",         "expectDocument": "optional: the design's name",         "expectedRevision": "the revision you last read (refused with stale_schematic otherwise)",+        "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)",         "sheet": "int, default 1"       },       "longRunning": false,@@ -3959,6 +3969,7 @@         "expectedRevision": "str",         "fields": "[designator, value] by default; other attribute names as Fusion names them",         "references": "[ref]",+        "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)",         "sheet": "int, default 1"       },       "longRunning": false,@@ -4008,6 +4019,7 @@         "expectDocument": "optional: the design's name",         "expectedRevision": "the revision you last read (refused with stale_schematic otherwise)",         "id": "wire id",+        "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)",         "sheet": "int, default 1"       },       "longRunning": false,@@ -4037,6 +4049,7 @@         "dryRun": "bool, default false: answer the plan and change nothing",         "expectDocument": "optional: the design's name",         "expectedRevision": "the revision you last read (refused with stale_schematic otherwise)",+        "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)",         "sheet": "int, default 1",         "texts": "[{text, at: [x, y], sizeMm?: 1.778, angle?: 0, align?: bottom-left|center|...}] (required)"       },
describe.py+13−13
@@ -136,47 +136,47 @@ _T = [      {"tree": "{name, sheets: [{page, description, children: [{name, module, path, at, sizeMm, pins, sheets}]}]}", "modules": "[str]", "problems": "[str]", "duplicatePages": "[]", "renumbered": "false"},      60, None, False, {}),     ('fusion_sch_text', 'Add free text to a schematic sheet (a note, a title): size, angle and alignment, on the Info layer. Read back exactly, or undone.',-     {"texts": "[{text, at: [x, y], sizeMm?: 1.778, angle?: 0, align?: bottom-left|center|...}] (required)", "sheet": "int, default 1", "expectedRevision": "the revision you last read (refused with stale_schematic otherwise)", "dryRun": "bool, default false: answer the plan and change nothing", "expectDocument": "optional: the design's name"},+     {"texts": "[{text, at: [x, y], sizeMm?: 1.778, angle?: 0, align?: bottom-left|center|...}] (required)", "sheet": "int, default 1", "expectedRevision": "the revision you last read (refused with stale_schematic otherwise)", "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)", "dryRun": "bool, default false: answer the plan and change nothing", "expectDocument": "optional: the design's name"},      {"added": "int", "texts": "[{id, text, at}]", "revision": "str"},      120, None, False, {'texts': [{'text': 'Power input', 'at': [20, 150]}]}),     ('fusion_sch_junction', 'Add junction dots at points, or remove dots by id or point. A point no wire passes is skipped; one where fewer than three items meet is listed as notNeeded and left (force:true adds it anyway). Read back, or undone.',-     {"points": "[[x, y]]", "remove": "[id | [x, y]]", "force": "bool, default false", "sheet": "int, default 1", "expectedRevision": "the revision you last read (refused with stale_schematic otherwise)", "dryRun": "bool, default false: answer the plan and change nothing", "expectDocument": "optional: the design's name"},+     {"points": "[[x, y]]", "remove": "[id | [x, y]]", "force": "bool, default false", "sheet": "int, default 1", "expectedRevision": "the revision you last read (refused with stale_schematic otherwise)", "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)", "dryRun": "bool, default false: answer the plan and change nothing", "expectDocument": "optional: the design's name"},      {"added": "[[x, y]]", "removed": "[[x, y]]", "skipped": "[{at, why}]", "notNeeded": "[[x, y]]", "revision": "str"},      120, None, False, {'points': [[40.64, 132.08]]}),     ('fusion_sch_delete_wire', 'Delete schematic wires by id, by their two ends, or every wire through a point; junctions left joining fewer than three items are removed too. Every other wire must read back unchanged, or the delete is undone.',-     {"ids": "[wire id]", "from": "[x, y]", "to": "[x, y]", "at": "[x, y]", "sheet": "int, default 1", "expectedRevision": "the revision you last read (refused with stale_schematic otherwise)", "dryRun": "bool, default false: answer the plan and change nothing", "expectDocument": "optional: the design's name"},+     {"ids": "[wire id]", "from": "[x, y]", "to": "[x, y]", "at": "[x, y]", "sheet": "int, default 1", "expectedRevision": "the revision you last read (refused with stale_schematic otherwise)", "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)", "dryRun": "bool, default false: answer the plan and change nothing", "expectDocument": "optional: the design's name"},      {"deleted": "[wire id]", "junctionsRemoved": "[[x, y]]", "notFound": "[str]", "revision": "str"},      120, None, False, {'ids': ['wire:40.64,127,40.64,132.08']}),     ('fusion_sch_connect_pins', "Wire pins to pins by Ref.Pin (matched by pad number, then pin name): one L-shaped route per pair (first leg along the from pin, or first:h|v, through via points), drawn as one net wire. Pins on two named nets join them (Fusion's 'Connect A and B?' is answered on the command line, and merged lists them). A route corner or end landing on another wire's middle gets a junction. Read back: each pair must end on one net, or everything is undone.",-     {"pairs": "[{from: \"U1.1\", to: \"R5.2\", first?: h|v, via?: [[x, y]]}] (required)", "sheet": "int, default 1", "expectedRevision": "the revision you last read (refused with stale_schematic otherwise)", "dryRun": "bool, default false: answer the plan and change nothing", "expectDocument": "optional: the design's name"},+     {"pairs": "[{from: \"U1.1\", to: \"R5.2\", first?: h|v, via?: [[x, y]]}] (required)", "sheet": "int, default 1", "expectedRevision": "the revision you last read (refused with stale_schematic otherwise)", "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)", "dryRun": "bool, default false: answer the plan and change nothing", "expectDocument": "optional: the design's name"},      {"wired": "int", "routes": "[{from, to, points, net, merged}]", "junctionsAdded": "[[x, y]]", "failed": "[{from, to, error}]", "revision": "str"},      120, None, False, {'pairs': [{'from': 'U1.1', 'to': 'R2.1'}]}),     ('fusion_sch_split_wire', "Split a schematic wire at a point into two wires with a junction dot at the cut (Fusion's way: a junction on a wire splits it), so a branch can end on a wire end. junction:false is refused.",-     {"at": "[x, y] (the cut; default the middle of id)", "id": "wire id", "sheet": "int, default 1", "expectedRevision": "the revision you last read (refused with stale_schematic otherwise)", "dryRun": "bool, default false: answer the plan and change nothing", "expectDocument": "optional: the design's name"},+     {"at": "[x, y] (the cut; default the middle of id)", "id": "wire id", "sheet": "int, default 1", "expectedRevision": "the revision you last read (refused with stale_schematic otherwise)", "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)", "dryRun": "bool, default false: answer the plan and change nothing", "expectDocument": "optional: the design's name"},      {"ids": "[two wire ids]", "junction": "id", "revision": "str"},      120, None, False, {'at': [30, 132.08]}),     ('fusion_sch_bus', 'Draw a bus as a polyline, optionally named by a bus label at its first point (D[0..7], or a group such as {SDA SCL}). Read back, or undone.',-     {"points": "[[x, y], ...] (required)", "label": "str", "sheet": "int, default 1", "expectedRevision": "the revision you last read (refused with stale_schematic otherwise)", "dryRun": "bool, default false: answer the plan and change nothing", "expectDocument": "optional: the design's name"},+     {"points": "[[x, y], ...] (required)", "label": "str", "sheet": "int, default 1", "expectedRevision": "the revision you last read (refused with stale_schematic otherwise)", "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)", "dryRun": "bool, default false: answer the plan and change nothing", "expectDocument": "optional: the design's name"},      {"ids": "[bus id]", "label": "str|null", "revision": "str"},      120, None, False, {'points': [[20, 100], [80, 100]], 'label': 'D[0..7]'}),     ('fusion_sch_edit_label', "Edit net labels by id, point or a whole net: move to a point or by a delta, set the rotation, or rename (a label shows its net's name, so a rename renames the label's wire segment; other segments of the old net keep their name; onto an existing name the two join, which Fusion asks about and the bridge answers on the command line). Read back, or undone.",-     {"edits": "[{id | at | net, moveTo?: [x, y], by?: [dx, dy], rotation?: deg, rename?: str}] (required)", "sheet": "int, default 1", "expectedRevision": "the revision you last read (refused with stale_schematic otherwise)", "dryRun": "bool, default false: answer the plan and change nothing", "expectDocument": "optional: the design's name"},+     {"edits": "[{id | at | net, moveTo?: [x, y], by?: [dx, dy], rotation?: deg, rename?: str}] (required)", "sheet": "int, default 1", "expectedRevision": "the revision you last read (refused with stale_schematic otherwise)", "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)", "dryRun": "bool, default false: answer the plan and change nothing", "expectDocument": "optional: the design's name"},      {"edited": "[{id, net, to, rotation, rename}]", "failed": "[{edit, error}]", "revision": "str"},      120, None, False, {'edits': [{'net': 'SDA', 'by': [0, 2.54]}]}),     ('fusion_sch_move_region', 'Move everything inside a box by (dx, dy) as one group: parts, wires, labels, junctions and texts; wires on the pins of a moved part follow it, and wires crossing the edge stretch. Fusion snaps the outline of a group to the sheet grid, so the box is snapped first (boxUsed) and refused with box_edge_off_grid, naming the parts, when the snapped box would take different parts than the asked one. Read back: parts, texts and labels moved exactly, nothing outside moved, every pin on the net it was on; otherwise undone.',-     {"box": "[x1, y1, x2, y2] mm (required)", "by": "[dx, dy] mm (required)", "sheet": "int, default 1", "expectedRevision": "the revision you last read (refused with stale_schematic otherwise)", "dryRun": "bool, default false: answer the plan and change nothing", "expectDocument": "optional: the design's name"},+     {"box": "[x1, y1, x2, y2] mm (required)", "by": "[dx, dy] mm (required)", "sheet": "int, default 1", "expectedRevision": "the revision you last read (refused with stale_schematic otherwise)", "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)", "dryRun": "bool, default false: answer the plan and change nothing", "expectDocument": "optional: the design's name"},      {"moved": "{parts, by}", "boxUsed": "[x1, y1, x2, y2]: the box on the sheet grid", "revision": "str"},      120, None, False, {'box': [0, 0, 50, 50], 'by': [10, 0]}),     ('fusion_sch_fix_connectivity', 'Find wire ends that almost touch a pin, label or other wire end (within toleranceMm) and, with dryRun:false, move each onto it. dryRun defaults to true: it lists the moves and changes nothing.',-     {"toleranceMm": "default 0.5", "sheet": "int, default 1", "dryRun": "bool, default TRUE", "expectedRevision": "str", "expectDocument": "optional"},+     {"toleranceMm": "default 0.5", "sheet": "int, default 1", "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)", "dryRun": "bool, default TRUE", "expectedRevision": "str", "expectDocument": "optional"},      {"moves": "[{net, from, to, distanceMm}]", "unresolved": "[{net, at, why}]", "revision": "str"},      120, None, False, {'toleranceMm': 0.5}),     ("fusion_sch_bus_entry", "Add 45-degree bus entries: the short diagonal from a member wire's end onto a bus. In Fusion an entry is a net wire named for its bus member (drawn from the bus side, so no member menu opens); member defaults to the net of the wire ending at at. Refused with entry_off_bus when the entry misses the bus, not_a_member when the net is not in the bus. Read back, or undone.",-     {"entries": "[{at: [x, y] (the wire end), direction?: down-right|up-right|down-left|up-left, member?: str, sizeMm?: 2.54}] (required)", "sheet": "int, default 1", "expectedRevision": "str", "dryRun": "bool, default false", "expectDocument": "optional"},+     {"entries": "[{at: [x, y] (the wire end), direction?: down-right|up-right|down-left|up-left, member?: str, sizeMm?: 2.54}] (required)", "sheet": "int, default 1", "expectedRevision": "str", "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)", "dryRun": "bool, default false", "expectDocument": "optional"},      {"added": "[{id, at, end, onBus, member}]", "revision": "str"},      150, None, False, {"entries": [{"at": [40, 102.54], "direction": "down-right"}]}),     ("fusion_sch_connect_to_bus", "Fan pins onto a bus: per pin, a wire from the pin to beside the bus, a 45-degree entry onto it and a member label, the net named for the member (members in pin order, or prefix plus start). Only an unconnected pin, or one already on that member net, is wired; one on another net is listed in failed (wiring it would join two nets). Read back: each pin on its member net, or undone.",-     {"pins": "[Ref.Pin] in member order (required)", "bus": "bus id or name (default the only bus)", "members": "[str]", "prefix": "str, default D", "start": "int, default 0", "entry": "up|down (vertical bus) or left|right (horizontal bus)", "busLabel": "bool|str: also label the bus", "sheet": "int, default 1", "expectedRevision": "str", "dryRun": "bool, default false", "expectDocument": "optional"},+     {"pins": "[Ref.Pin] in member order (required)", "bus": "bus id or name (default the only bus)", "members": "[str]", "prefix": "str, default D", "start": "int, default 0", "entry": "up|down (vertical bus) or left|right (horizontal bus)", "busLabel": "bool|str: also label the bus", "sheet": "int, default 1", "expectedRevision": "str", "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)", "dryRun": "bool, default false", "expectDocument": "optional"},      {"connected": "[{pin, member, wireId, entryId}]", "failed": "[{pin, error}]", "busLabel": "str|null", "revision": "str"},      180, None, False, {"pins": ["U1.1", "U1.2"], "prefix": "D", "start": 0}),     ("fusion_render_schematic", "Render a schematic sheet to a PNG without opening a window, through Fusion's own EXPORT IMAGE, and keep visual baselines: baseline set stores the render, baseline compare renders at the baseline's width and reports the pixel drift, the changed region (mm on the sheet) and a diff image with the changed pixels in red.",@@ -184,11 +184,11 @@ _T = [      {"png": "path", "width": "int", "height": "int", "dpi": "int", "extentMm": "[x0, y0, x1, y1]", "driftPercent": "number (compare)", "pass": "bool (compare)", "changedRegionMm": "[x1, y1, x2, y2] | null (compare)", "diffPng": "path | null (compare)", "rendererChanged": "bool (compare)", "sizeChanged": "bool (compare)", "baseline": "path"},      180, None, False, {"baseline": "compare"}),     ("fusion_sch_reset_fields", "Put parts' designator and value text (or other named fields) back where the library symbol places them, through each part's turn and mirror: position, rotation and alignment; size and visibility stay. A field that is not smashed is already there and is listed as untouched. Read back, or undone.",-     {"references": "[ref]", "all": "bool: every part on the sheet", "fields": "[designator, value] by default; other attribute names as Fusion names them", "sheet": "int, default 1", "expectedRevision": "str", "dryRun": "bool, default false", "expectDocument": "optional"},+     {"references": "[ref]", "all": "bool: every part on the sheet", "fields": "[designator, value] by default; other attribute names as Fusion names them", "sheet": "int, default 1", "expectedRevision": "str", "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)", "dryRun": "bool, default false", "expectDocument": "optional"},      {"reset": "[{ref, field}]", "untouched": "[{ref, field, why}]", "revision": "str"},      150, None, False, {"references": ["R5", "C3"]}),     ("fusion_sch_create", "Add an empty sheet after the last one (Fusion numbers a schematic's sheets; it has no sheet files). Page and child-sheet arguments are refused: Fusion sets a page through a frame part, and a child sheet is a module. Read back, or undone.",-     {"sheet": "int: must be the next number (default it)", "dryRun": "bool, default false", "expectDocument": "optional"},+     {"sheet": "int: must be the next number (default it)", "save": "bool, default FALSE: each Fusion save is a new cloud version; true saves the design after the edit (as fusion_save_document)", "dryRun": "bool, default false", "expectDocument": "optional"},      {"sheet": "int", "page": "int", "sheetSymbol": "null", "created": "bool", "revision": "str"},      150, None, False, {}),     ("fusion_sch_sheet_pins", "A sheet symbol's pins: in Fusion a sheet symbol is a module instance and its pins are the module's ports. Lists them with kind, side and whether a net reaches each; validate lists the unwired ones. Adding, renaming or deleting is refused (that is the module's PORT edit).",
rust/bridge/src/sch.rs+10−2
@@ -9,10 +9,18 @@ pub fn call(cmd: &str, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Val         serde_json::to_string(SOURCE).unwrap(), serde_json::to_string(&args.to_string()).unwrap(), serde_json::to_string(cmd).unwrap());     let r = pass("run_modeling_script", json!({"script": script}));     if r["success"].as_bool() == Some(false) { return r; }-    match r["data"]["result"].as_object() {+    let mut out = match r["data"]["result"].as_object() {         Some(_) => r["data"]["result"].clone(),-        None => json!({"success": false, "errorCode": "invalid_schematic_response", "error": "Fusion did not return a structured schematic result"}),+        None => return json!({"success": false, "errorCode": "invalid_schematic_response", "error": "Fusion did not return a structured schematic result"}),+    };+    // save defaults to false: each Fusion save is a new cloud version. save:true saves the design+    // in place after a successful edit, as fusion_save_document does.+    if args["save"].as_bool() == Some(true) && out["success"].as_bool() == Some(true) && out["mutated"].as_bool() == Some(true) {+        let sv = crate::savedoc::save_document(&json!({"documentName": out["documentName"], "comment": args["saveComment"].as_str().unwrap_or("fusion-bridge schematic edit")}), pass);+        out["saved"] = json!(sv["success"].as_bool() == Some(true) && sv["saved"].as_bool() != Some(false));+        if out["saved"] != json!(true) { out["saveResult"] = sv; }     }+    out }  /// `render_schematic`: the sheet as a PNG (Fusion's EXPORT IMAGE, in sch_script.py), and visual