app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.
← Commit history
Update 17 file(s)
17 files changed
+87−45
BRIDGE_VERSION+1−1CHANGELOG.md+8bridge.json+1−1dashboard/Cargo.lock+1−1dashboard/Cargo.toml+1−1install.sh+1−1package.json+1−1page.json+5−11rust/Cargo.lock+6−6rust/Cargo.toml+1−1rust/bridge/src/checks.rs+3−1rust/bridge/src/routing_script.py+4rust/vendor/adom-eda-checks/Cargo.toml+1−1rust/vendor/adom-eda-checks/README.md+3−3rust/vendor/adom-eda-checks/VENDOR.md+3−3rust/vendor/adom-eda-checks/src/report.rs+15−5rust/vendor/adom-eda-checks/src/review.rs+32−8BRIDGE_VERSION+1−1@@ -1 +1 @@-1.13.12+1.13.13
CHANGELOG.md+8@@ -3,6 +3,14 @@ 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.13.13 (2026-10-29)++- Engine 0.17.2 (standard 0.20.2). The checks get the board's minimum track width and drill (EAGLE's msWidth,+ msDrill) as the standard's width and hole-size rules, and review_board and board_report are told EAGLE's DRC always+ checks shorts: BQ25792's three missing_critical_rules errors were rules Fusion has.+- `fusion_board_report`: the JLCPCB assembly section gets the BOM lines with no LCSC number (as fusion_assembly_check+ does), and the DRC section reads Fusion's violation and airwire counts.+ ## 1.13.12 (2026-10-29) The standard's pair and length planners, the schematic checks and the design review and report from the shared engine 0.17.0 (standard 0.20.0). 326 verbs.
bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "fusion360", "displayName": "Autodesk Fusion 360",- "version": "1.13.12",+ "version": "1.13.13", "description": "Drive Autodesk Fusion 360 from the cloud: launch Fusion, electronics board layout, design rules, exports (STEP/IGES/STL/3MF/USDZ/OBJ/DXF/DWG/Gerbers/BOM/CPL), fast APS server-indexed cloud search plus browse/recent/file-info/versions, cloud file download/upload and folder creation, and in-app parametric modeling (fusion_run_modeling_script). Never-charge: APS calls are capped to the free tier.", "homepage": "https://wiki.adom.inc/adom/fusion-bridge", "author": "Adom Inc.",
dashboard/Cargo.lock+1−1@@ -4,7 +4,7 @@ version = 4 [[package]] name = "fusion-dashboard"-version = "1.13.12"+version = "1.13.13" dependencies = [ "serde_json", ]
dashboard/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "fusion-dashboard"-version = "1.13.12"+version = "1.13.13" edition = "2021" [dependencies]
install.sh+1−1@@ -21,7 +21,7 @@ echo "Installed fusion-bridge skills: $(ls "$PKG_DIR/skills" 2>/dev/null | tr '\ # binary (the three-artifact pattern: bridge zip -> ab, THIS pkg -> container skills + # this download, page media -> wiki). Per the wiki's PKG_VS_RELEASE policy, binaries are # platform-tagged RELEASE assets, never tarball content - the installer fetches them.-DASH_VERSION="1.13.12"+DASH_VERSION="1.13.13" DASH_URL="https://wiki.adom.inc/download/adom/fusion-bridge/${DASH_VERSION}/fusion-dashboard" DEST="" if [ -w /usr/local/bin ]; then DEST=/usr/local/bin
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.13.12",+ "version": "1.13.13", "type": "app", "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.", "dependencies": {
page.json+5−11@@ -1,6 +1,6 @@ { "slug": "fusion-bridge",- "version": "1.13.12",+ "version": "1.13.13", "type": "app", "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.", "dependencies": {@@ -17,9 +17,7 @@ "pcb", "exports", "aps",- "modeling",- "mechanical",- "assemblies"+ "modeling" ], "org": "adom", "title": "Fusion - the Fusion 360 Bridge",@@ -92,13 +90,9 @@ "pin electrical types from a schematic", "part heights on my fusion board", "which fusion parts have no 3d model",- "will my fusion board fit the enclosure",- "build a parametric part in fusion",- "assemble mechanical parts in fusion",- "insert a McMaster part in fusion",- "generate a mechanical assembly BOM"+ "will my fusion board fit the enclosure" ],- "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, author parametric mechanical parts and internal assemblies, ingest McMaster CAD and generate mechanical BOMs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).",+ "discovery_pitch": "Use the Adom Bridge Fusion 360 bridge when the user wants to DRIVE Autodesk Fusion 360 from the cloud: open/search their team's Fusion cloud designs, lay out a PCB or do electronics, export STEP/IGES/STL/Gerbers/BOM, or build a component library with real rendering 3D. It runs Fusion on the user's own machine via Adom Bridge. NOT for web/browser/login/form-filling (that's the native-browser extension) and NOT for KiCad (that's the kicad bridge).", "brief": "Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.", "sample_prompts": [ {@@ -149,4 +143,4 @@ "name": "John Lauer", "email": "[email protected]" }-}+}
rust/Cargo.lock+6−6@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adom-eda-checks"-version = "0.17.0"+version = "0.17.2" dependencies = [ "roxmltree", "serde_json",@@ -19,7 +19,7 @@ dependencies = [ [[package]] name = "adom-fusion-bridge"-version = "1.13.12"+version = "1.13.13" dependencies = [ "adom-eda-checks", "base64",@@ -299,7 +299,7 @@ dependencies = [ [[package]] name = "fusion-core"-version = "1.13.12"+version = "1.13.13" dependencies = [ "serde", "serde_json",@@ -310,7 +310,7 @@ dependencies = [ [[package]] name = "fusion-lbr"-version = "1.13.12"+version = "1.13.13" dependencies = [ "regex", "roxmltree",@@ -322,14 +322,14 @@ dependencies = [ [[package]] name = "fusion-os-mac"-version = "1.13.12"+version = "1.13.13" dependencies = [ "fusion-core", ] [[package]] name = "fusion-os-win"-version = "1.13.12"+version = "1.13.13" dependencies = [ "fusion-core", "windows-sys 0.59.0",
rust/Cargo.toml+1−1@@ -4,7 +4,7 @@ members = ["core", "lbr", "os-win", "os-mac", "bridge"] # One version for the whole bridge, in lockstep with BRIDGE_VERSION (tools/release.sh bumps both). [workspace.package]-version = "1.13.12"+version = "1.13.13" edition = "2021" license = "MIT"
rust/bridge/src/checks.rs+3−1@@ -47,7 +47,7 @@ pub fn call(cmd: &str, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Val if args[k].as_f64().map_or(true, |v| v < lim) && lim > 0.0 { bundle["args"][k] = json!(lim); } } }- if cmd == "assembly_check" && args["lcscMissing"].is_null() {+ if matches!(cmd, "assembly_check" | "board_report") && args["lcscMissing"].is_null() { // the BOM lines with no LCSC number, from the board's own part attributes (fusion_board_bom) let bom = crate::routing::call("board_bom", args, pass); if let Some(lines) = bom["lines"].as_array() {@@ -56,6 +56,8 @@ pub fn call(cmd: &str, args: &Value, pass: &dyn Fn(&str, Value) -> Value) -> Val bundle["args"]["lcscMissing"] = json!(missing); } }+ // EAGLE's DRC always checks shorts: there is no rule to find for it+ if matches!(cmd, "review_board" | "board_report") && args["alwaysChecked"].is_null() { bundle["args"]["alwaysChecked"] = json!(["short circuit"]); } if cmd == "silk_designators" { silk_boxes(&mut bundle["silk"]); } if cmd == "board_report" { // every companion this bridge has: the 3D bodies, a fab record (JLCPCB unless vendor names another),
rust/bridge/src/routing_script.py+4@@ -2960,6 +2960,10 @@ def check_inputs(app, root, board, meta, args): std.append({'kind': 'clearance', 'clearanceMm': round(v, 4), 'name': r_.get('name'), 'enabled': True}) dia, drill = _via_rule(board) std.append({'kind': 'via', 'sizeMm': round(dia, 4), 'drillMm': round(drill, 4), 'name': 'via (drill and restring rules)', 'enabled': True})+ # the board's minimum track width and drill (EAGLE's msWidth, msDrill) in the standard's form: a+ # minimum only, so a planner's default width is not taken from it+ std.append({'kind': 'width', 'minMm': round(_rule_len(board, 'msWidth', 0.2), 4), 'name': 'minimum width (msWidth)', 'enabled': True})+ std.append({'kind': 'hole size', 'minMm': round(_rule_len(board, 'msDrill', 0.3), 4), 'name': 'minimum drill (msDrill)', 'enabled': True}) return {'success': True, 'mutated': False, **meta, 'board': state(board, meta), 'stack': layer_stack(board), # only the copper clearances: Fusion's other rule kinds also say "Clearance" (Dimension, # Solder-Mask, Restrict, Keepout) and the engine takes any such kind's value
rust/vendor/adom-eda-checks/Cargo.toml+1−1@@ -1,6 +1,6 @@ [package] name = "adom-eda-checks"-version = "0.17.0"+version = "0.17.2" edition = "2021" description = "The EDA Bridge Standard's checks and board-file writers, computed from the board model JSON: no bridge calls, no tool." license = "MIT"
rust/vendor/adom-eda-checks/README.md+3−3@@ -13,7 +13,7 @@ boards the engine gives the same answers the Altium bridge 0.2.76 gives. |---|---|---| | `assembly_check` | board, stack? | the board against JLCPCB's two PCBA tiers as published (with the page and its read date): size (or `panelMm`), thickness and layers from the stack, one or two sides, the smallest two-pad part and the finest IC pitch, fiducials, and parts without an LCSC number (`lcscMissing` from the bridge's BOM). Pads sharing one centre are one land, and a component with no pads is no part. `service` is the cheaper tier every rule allows. Matches the Altium bridge's answers on its test boards. From its assembly.rs | | `auto_classes` | board | net classes from the names the nets already have (GND, Power, Clock, HighSpeed) and differential pairs from their suffixes (`_P`/`_N`, `DP`/`DN`, `+`/`-`), with `applyArgs`: the net_classes and diff_pairs calls that write them (Power with a `powerWidthMm` width rule). From the Altium bridge's autoclass.rs |-| `board_report` | board, and every companion it can use | every check the engine has, run on this bundle, as one self-contained HTML page (`text`), dark by default: fab limits (with `fab`), JLCPCB assembly, schematic review and symbols against footprints (with `sheetParts` and `netlist`), impedance, return paths and copper balance (with `stack`), 3D heights (with `bodies`), the design review, and the tool's own DRC (`drc`) and a base64 PNG of the board (`picture`) when the bridge passes them. A section whose input is missing says which to pass. From the Altium bridge's report.rs |+| `board_report` | board, and every companion it can use | every check the engine has, run on this bundle, as one self-contained HTML page (`text`), dark by default: fab limits (with `fab`), JLCPCB assembly, schematic review and symbols against footprints (with `sheetParts` and `netlist`), impedance, return paths and copper balance (with `stack`), 3D heights (with `bodies`), the design review, and the tool's own DRC (`drc`) and a base64 PNG of the board (`picture`) when the bridge passes them; `args` carries `lcscMissing`, `panelMm`, `gridMm` and `alwaysChecked` to those checks. A section whose input is missing says which to pass. From the Altium bridge's report.rs | | `check_clearance` | board, bodies? | pairs of parts closer than `thresholdMm`, closest first: `mode: courtyard` (default 0.25) measures part boxes (pads, and 3D bodies when given) on the same side; `mode: pads` (default 0.2) measures pad copper of different parts that shares a layer, by its real outline, skipping same-net pairs unless `includeSameNet`; `refs` keeps pairs involving those parts. A part under a raised one (pads apart, bodies apart in 3D) is listed in `stacked`, not `pairs`, as placement_check lists it. From the KiCad bridge's place_tools | | `check_orientation` | board | advisory: each two-pad part's quarter turns scored by airline length (MST of a routable net with the pad there, distance to the nearest pad of a plane-like net such as GND); turns saving `minImprovementMm` (0.6) or more are suggested, nothing moves. From the Altium bridge's design.rs | | `copper_balance` | board, stack, pours | copper per layer and how evenly mirrored layers match (bow and twist) |@@ -31,7 +31,7 @@ boards the engine gives the same answers the Altium bridge 0.2.76 gives. | `placement_check` | board, bodies? | courtyard overlaps (pads and bodies, grown by `marginMm`), parts off or over the edge, pads within `edgeMm` of the edge, parts on mounting holes; `stacked` lists plan-view overlaps where one part sits wholly above the other (a part under a raised molecule: pads apart, no bodies meeting in 3D); `bodiesUsed: false` when no bodies were given | | `plan_fanout` | board, stack?, rules? | the fanout of a BGA or QFN/QFP (`ref`): pitch and package from its pads; a BGA's outer ring escapes on the surface and each inner ball takes a dogbone via, or a via in the pad when the dogbone does not fit (`style` auto); a perimeter pad takes an outward stub, with a via at its end for ground or `viaPerimeter`, and an exposed pad a via grid. `calls` are route_net arguments, one per net; `conflicts` lists each via and stub closer than the clearance to another net's pad, track, arc or via anywhere on the board. A pin its net's copper already reaches is skipped (`includeRouted` plans it anyway), the exposed-pad grid keeps clear of holes already there, and `crossesPours` names other nets' pours the plan runs into (repour before DRC). From the KiCad bridge's place_tools | | `plan_route` | board, rules? | a path between two pads of a net (`from`, `to` as Ref.Pad) on any listed copper `layers`, with through vias between them: a bounded orthogonal A* on `gridMm`, width and via from the args, else the board's width and via rules (`planner.widthFrom`, `viaFrom`), keeping `widthMm/2` + the board's strictest board-wide copper clearance rule from other nets' copper (pads by their real outline), inside the outline, no via in a pad; a through via is checked on every copper layer of the board (`planner.viasCheckedOn`), not only the planned ones. `routeNet` is route_net's arguments, unchanged. Pours are not obstacles (repour after), but `crossesPours` names each other net's pour the route runs through, since that can split it; `avoidPours: true` keeps the route out of them (or a list of nets: out of theirs only), and `pourCost` makes a mm inside one cost 1 + pourCost mm, a preference where a hard block finds no path; a pour that holds the route's own end pad is exempt and named (`planner.avoidExempt`); with no path, `ends` names the avoided pours at each end pad and whether they are exempt, and `withoutAvoid` says whether a path exists through them. The grid path is smoothed to straight and 45-degree segments where they keep the same clearance (`smooth: false` keeps it) |-| `review_board` | board, sheetParts?, rules? | small design-review checks, errors first: placeholder values, single-pin nets, duplicate designators, parts with pads off the outline polygon or near the edge (`edgeMm`), parts with no footprint name, off-grid parts (with `gridMm`); from the sheet parts, missing Mouser/DigiKey/LCSC numbers and datasheets and placeholder comments; from the rules, disabled ones, missing clearance/width/hole-size/short-circuit rules and same-priority ties. `skipped` and `notImplemented` say what was not judged. From the Altium bridge's review.rs |+| `review_board` | board, sheetParts?, rules? | small design-review checks, errors first: placeholder values, single-pin nets, duplicate designators, parts with pads off the outline polygon or near the edge (`edgeMm`), parts with no footprint name, off-grid parts (with `gridMm`); from the sheet parts, missing Mouser/DigiKey/LCSC numbers and datasheets and placeholder comments; from the rules, disabled ones, missing clearance/width/hole-size/short-circuit rules and same-priority ties (rules classed from a standard kind, a descriptor, Altium's ordinal or the rule's own name; `alwaysChecked` names classes a tool always checks, like EAGLE's shorts). `skipped` and `notImplemented` say what was not judged. From the Altium bridge's review.rs | | `route_pair` | board, stack?, rules? | a differential pair planned as one centreline (pad midpoints through `via` waypoints) with each net offset half a pitch to its own side, mitred corners and a `fanMm` fan-out; width and gap given, or from `diffOhms` and the stack. `problems` names a crossing or other copper (pads by outline, routed tracks, vias) closer than the clearance; `routeNet` holds both nets' route_net arguments and `widthRule` the pair's add_rule. From the Altium bridge's pairroute.rs | | `schematic_review` | sheetParts, netlist | every placed pin, with its electrical type, against the compiled netlist: power pins on no net and outputs driving each other (errors); unconnected pins, one-pin nets, inputs nothing drives and IC supplies with no capacitor to ground (warnings; a regulator's switch node on an inductor is no missing capacitor). From the Altium bridge's schreview.rs | | `silk_designators` | board, silk | a place for each reference designator: one size (`size`, stepping down to `minSize`), beside its own part, clear of every pad by `padClearance`, of other parts' bodies and of other labels, inside the board. `applyArgs` is apply_silkscreen's arguments; parts with no clear spot are listed, not forced. From the Altium bridge's silkplace.rs |@@ -112,7 +112,7 @@ adom-eda-checks import_svg --svg logo.svg --args '{"at": [10, 10], "widthMm": 20 Replies keep the standard envelope (`success`, `errorCode`, `error`, `_hint`) and name verbs without a prefix (`apply_pours`, `add_rule`, `place_vias`): a bridge adds its own. Every reply also carries `engine`-(`"adom-eda-checks 0.17.0"`), so a bridge reports the engine it runs without writing the version by hand.+(`"adom-eda-checks 0.17.2"`), so a bridge reports the engine it runs without writing the version by hand. ## Tests
rust/vendor/adom-eda-checks/VENDOR.md+3−3@@ -3,9 +3,9 @@ The EDA Bridge Standard's shared checks engine, copied unchanged from adom/eda-bridge-standard `engine/` (the wiki has no git protocol, so Cargo cannot fetch it). -- Standard version: 0.20.0-- Standard commit: 38b5e58fbad4278de70f9e4cfe258056853810a3-- Crate version: 0.17.0+- Standard version: 0.20.2+- Standard commit: 3c0ef8ff5e5bc11c68ba23f8038dc8d2643eea75+- Crate version: 0.17.2 - Copied: 2026-10-28, by fusion-lane To update: clone the standard fresh (`adom-wiki repo clone adom/eda-bridge-standard`), copy `engine/src`,
rust/vendor/adom-eda-checks/src/report.rs+15−5@@ -24,8 +24,12 @@ pub fn sections(c: &Value) -> Vec<Section> { // design rules let d = &c["drc"]; out.push(if d.is_null() { Section { title: "Design rules (the tool's DRC)".into(), ok: None, summary: "not run: pass the tool's DRC reply (run_drc) as drc".into(), rows: vec![] } } else if failed(d) { err("Design rules (the tool's DRC)", d) } else {- Section { title: "Design rules (the tool's DRC)".into(), ok: Some(d["errors"].as_i64() == Some(0) && d["unconnected"].as_i64() == Some(0)),- summary: format!("{} violations, {} unconnected", txt(&d["errors"]), txt(&d["unconnected"])),+ // each tool counts in its own words: Altium errors/unconnected, Fusion violationCount/airwires+ let errs = d["errors"].as_i64().or(d["violationCount"].as_i64());+ let open = d["unconnected"].as_i64().or(d["airwires"].as_i64()).or(d["unrouted"].as_i64());+ let shown = |v: Option<i64>| v.map(|n| n.to_string()).unwrap_or_else(|| "?".into());+ Section { title: "Design rules (the tool's DRC)".into(), ok: match (errs, open) { (Some(e), Some(u)) => Some(e == 0 && u == 0), (Some(e), None) if e > 0 => Some(false), _ => None },+ summary: format!("{} violations, {} unconnected", shown(errs), shown(open)), rows: d["violations"].as_array().cloned().unwrap_or_default().iter().take(30).map(|v| (txt(&v["rule"]), txt(&v["objects"]))).collect() } }); let f = &c["fab"]; // the vendor as each finding's source names it, else the record's own name@@ -104,7 +108,7 @@ table{{width:100%;border-collapse:collapse;font-size:13px}} th,td{{text-align:le } /// board_report: input {board, stack?, pours?, rules?, bodies?, fab?, sheetParts?, netlist?, drc?, picture?, args: {title?,-/// date?}}. Every check the engine has, run on this bundle, as one self-contained HTML page (`text`), with the tool's+/// date?, lcscMissing?, panelMm?, gridMm?, alwaysChecked?}}; the last four go to assembly_check and review_board. Every check the engine has, run on this bundle, as one self-contained HTML page (`text`), with the tool's /// own DRC reply (`drc`) and a base64 PNG of the board (`picture`) when the bridge passes them. A section whose input /// is missing says so instead of passing. pub fn board_report(input: &Value) -> Value {@@ -115,12 +119,13 @@ pub fn board_report(input: &Value) -> Value { c["schematic"] = need("sheetParts", "the sheets' parts").or_else(|| need("netlist", "the compiled netlist")).unwrap_or_else(|| crate::run("schematic_review", &with(json!({})))); c["pinPad"] = need("sheetParts", "the sheets' parts").unwrap_or_else(|| crate::run("pin_pad_check", &with(json!({})))); c["fab"] = need("fab", "a pcb-design-rules record").unwrap_or_else(|| crate::run("fab_check", &with(json!({}))));- c["assembly"] = crate::run("assembly_check", &with(json!({})));+ // the bridge's BOM lines without an LCSC number, when it passes them (fusion-lane: without, the tier read economic)+ c["assembly"] = crate::run("assembly_check", &with(json!({"lcscMissing": input["args"]["lcscMissing"], "panelMm": input["args"]["panelMm"]}))); c["impedance"] = need("stack", "the stack-up").unwrap_or_else(|| crate::run("impedance", &with(json!({"z0": 50, "diff": 90})))); c["returnPath"] = need("stack", "the stack-up").unwrap_or_else(|| crate::run("return_path", &with(json!({})))); c["balance"] = need("stack", "the stack-up").unwrap_or_else(|| crate::run("copper_balance", &with(json!({})))); c["heights"] = need("bodies", "the 3D bodies").unwrap_or_else(|| crate::run("height_check", &with(json!({}))));- c["review"] = crate::run("review_board", &with(json!({})));+ c["review"] = crate::run("review_board", &with(json!({"gridMm": input["args"]["gridMm"], "alwaysChecked": input["args"]["alwaysChecked"]}))); c["drc"] = crate::input::part(input, "drc").unwrap_or(Value::Null); let secs = sections(&c); let args = &input["args"];@@ -166,5 +171,10 @@ mod tests { assert!(sec("Fab limits")["summary"].as_str().unwrap().contains("not run")); assert_eq!(sec("Design review")["ok"], json!(true), "{r}"); assert!(sec("JLCPCB assembly")["summary"].as_str().unwrap().starts_with("tier"));+ // Fusion's DRC counts in its own words; the bridge's LCSC gaps reach the assembly section+ let f = board_report(&json!({"board": st, "drc": {"violationCount": 3, "airwires": 0}, "args": {"lcscMissing": 2}}));+ let sec = |t: &str| f["sections"].as_array().unwrap().iter().find(|s| s["title"].as_str().unwrap_or("").starts_with(t)).cloned().unwrap();+ assert_eq!((sec("Design rules")["summary"].clone(), sec("Design rules")["ok"].clone()), (json!("3 violations, 0 unconnected"), json!(false)));+ assert_eq!(sec("JLCPCB assembly")["ok"], json!(false), "{f}"); } }
rust/vendor/adom-eda-checks/src/review.rs+32−8@@ -56,14 +56,25 @@ pub fn rule_class(r: &Value) -> String { if d.starts_with(prefix) { return c.into(); } } match r["kind"].as_i64() {- Some(0) => "clearance".into(), Some(2) => "width".into(), Some(42) => "hole size".into(), Some(15) => "short circuit".into(),- _ => d.split('(').next().unwrap_or("").trim().to_string(),+ Some(0) => return "clearance".into(), Some(2) => return "width".into(), Some(42) => return "hole size".into(), Some(15) => return "short circuit".into(),+ // any other Altium ordinal is no critical rule: its descriptor names it ("Routing Topology Rule (Shortest)" is+ // no short-circuit rule, "Polygon Connect (Conductor Width ...)" no width rule)+ Some(_) => return d.split('(').next().unwrap_or("").trim().to_string(),+ None => {} }+ // a tool's own rule names (Fusion's EAGLE rules: wire-wire, pad-via, minimum width, minimum drill, ...)+ let t = format!("{} {} {}", s(r, "kindName"), s(r, "name"), d).to_lowercase().replace(['_', '-'], " ");+ if t.contains("short circuit") || t.contains("shortcircuit") { return "short circuit".into(); }+ if (t.contains("width") || t.contains("minimum wire")) && !t.contains("annular") && !t.contains("conductor") { return "width".into(); }+ if t.contains("drill") || t.contains("hole size") || t.contains("holesize") { return "hole size".into(); }+ let copper = !["silk", "mask", "paste", "edge", "board", "courtyard", "component", "hole to hole", "plane"].iter().any(|w| t.contains(w));+ if copper && (t.contains("clearance") || t.contains("wire wire") || t.contains("pad via") || t.contains("pad pad")) { return "clearance".into(); }+ d.split('(').next().unwrap_or("").trim().to_string() } /// The checks over the board model, the sheet parts (read_sheet_file's components, or none) and the rules. Returns the /// findings (errors first), the checks run and those skipped with why. Pure.-pub fn run_checks(st: &Value, sheet: Option<&[Value]>, rules: &[Value], grid: f64, edge: f64, wanted: Option<&[String]>) -> (Vec<Value>, Vec<String>, Map<String, Value>) {+pub fn run_checks(st: &Value, sheet: Option<&[Value]>, rules: &[Value], grid: f64, edge: f64, wanted: Option<&[String]>, always: &[String]) -> (Vec<Value>, Vec<String>, Map<String, Value>) { let comps = st["components"].as_array().cloned().unwrap_or_default(); let pads = st["pads"].as_array().cloned().unwrap_or_default(); let ring = crate::coppermap::outline_ring(st);@@ -150,7 +161,9 @@ pub fn run_checks(st: &Value, sheet: Option<&[Value]>, rules: &[Value], grid: f6 finding("disabled_design_rules", sev, ("rule", s(r, "name")), format!("Design rule {:?} ({c}) is disabled: DRC passes without ever applying it.", s(r, "name"))) }).collect()); run("missing_critical_rules", no_rules, &mut out, &|| {- let have: BTreeSet<String> = rules.iter().filter(|r| r["enabled"] != json!(false)).map(rule_class).collect();+ // a tool that always checks a class (EAGLE's DRC always checks shorts) passes it in alwaysChecked+ let mut have: BTreeSet<String> = rules.iter().filter(|r| r["enabled"] != json!(false)).map(rule_class).collect();+ have.extend(always.iter().map(|a| a.to_lowercase().replace(['_', '-'], " "))); CRITICAL.iter().filter(|c| !have.contains(**c)).map(|c| finding("missing_critical_rules", "error", ("rule", c), format!("No enabled {c} rule on this board."))).collect() }); run("duplicate_rule_priority", no_rules, &mut out, &|| {@@ -165,13 +178,14 @@ pub fn run_checks(st: &Value, sheet: Option<&[Value]>, rules: &[Value], grid: f6 (out, ran, skipped) } -/// review_board: input {board, sheetParts?, rules?, args: {checks?, gridMm?, edgeMm?}}.+/// review_board: input {board, sheetParts?, rules?, args: {checks?, gridMm?, edgeMm?, alwaysChecked?: ["short circuit", ...]}}. pub fn review_board(input: &Value) -> Value { let st = match crate::input::board(input) { Ok(b) => b, Err(e) => return e }; let args = &input["args"]; let sheet: Option<Vec<Value>> = crate::input::part(input, "sheetParts").and_then(|p| p.get("components").or(Some(&p)).and_then(|x| x.as_array()).cloned()); let wanted: Option<Vec<String>> = args["checks"].as_array().map(|a| a.iter().filter_map(|x| x.as_str().map(String::from)).collect());- let (findings, ran, skipped) = run_checks(&st, sheet.as_deref(), &crate::input::rules(input), args["gridMm"].as_f64().unwrap_or(0.0), args["edgeMm"].as_f64().unwrap_or(0.5), wanted.as_deref());+ let always: Vec<String> = args["alwaysChecked"].as_array().map(|a| a.iter().filter_map(|x| x.as_str().map(String::from)).collect()).unwrap_or_default();+ let (findings, ran, skipped) = run_checks(&st, sheet.as_deref(), &crate::input::rules(input), args["gridMm"].as_f64().unwrap_or(0.0), args["edgeMm"].as_f64().unwrap_or(0.5), wanted.as_deref(), &always); let mut counts = json!({"error": 0, "warning": 0, "info": 0}); for x in &findings { let k = s(x, "severity").to_string(); counts[&k] = json!(counts[&k].as_i64().unwrap_or(0) + 1); } json!({"success": true, "findings": findings, "counts": counts, "checks": ran, "skipped": skipped,@@ -194,7 +208,7 @@ mod tests { let rules = vec![json!({"name": "Clearance", "kind": 0, "enabled": true, "priority": 1, "descriptor": "Clearance Constraint (Gap=0.1mm)"}), json!({"name": "Short", "kind": 15, "enabled": false, "priority": 1, "descriptor": "Short-Circuit Constraint (Allowed=No)"}), json!({"name": "W1", "kind": "width", "enabled": true, "priority": 2}), json!({"name": "W2", "kind": "width", "enabled": true, "priority": 2})];- let (f, ran, _) = run_checks(&st, Some(&sheet), &rules, 0.127, 0.5, None);+ let (f, ran, _) = run_checks(&st, Some(&sheet), &rules, 0.127, 0.5, None, &[]); let has = |c: &str, key: &str, v: &str| f.iter().any(|x| x["check"] == c && x[key] == v); assert!(has("placeholder_values", "designator", "U1")); assert!(has("single_pin_nets", "net", "LONE"));@@ -210,8 +224,11 @@ mod tests { assert!(f.iter().any(|x| x["check"] == "duplicate_rule_priority")); assert_eq!(f[0]["severity"], "error"); assert_eq!(ran.len(), 13);- let (_, _, skipped) = run_checks(&st, None, &[], 0.127, 0.5, None);+ let (_, _, skipped) = run_checks(&st, None, &[], 0.127, 0.5, None, &[]); assert!(skipped.contains_key("vendor_part_numbers") && skipped.contains_key("missing_critical_rules"));+ // EAGLE always checks shorts: passed in alwaysChecked, a missing short-circuit rule is no finding+ let (f2, _, _) = run_checks(&st, Some(&sheet), &rules, 0.0, 0.5, Some(&["missing_critical_rules".to_string()]), &["short_circuit".to_string()]);+ assert!(!f2.iter().any(|x| x["rule"] == "short circuit"), "{f2:?}"); } #[test]@@ -219,5 +236,12 @@ mod tests { assert_eq!(rule_class(&json!({"kind": 42})), "hole size"); assert_eq!(rule_class(&json!({"kind": 99, "descriptor": "Width Constraint (Min=0.1mm)"})), "width"); assert_eq!(rule_class(&json!({"kind": "short_circuit"})), "short circuit");+ // a tool's own names (Fusion's EAGLE rules), and a silk clearance is no copper clearance+ assert_eq!(rule_class(&json!({"name": "Wire-Wire", "value": "0.1mm"})), "clearance");+ assert_eq!(rule_class(&json!({"name": "Minimum Drill"})), "hole size");+ assert_eq!(rule_class(&json!({"name": "Silk Clearance"})), "");+ // Altium's other ordinals stay what their descriptor says, whatever words they hold+ assert_eq!(rule_class(&json!({"kind": 7, "name": "RoutingTopology", "descriptor": "Routing Topology Rule(Topology=Shortest) (All)"})), "routing topology rule");+ assert_eq!(rule_class(&json!({"kind": 6, "name": "PlaneConnect", "descriptor": "Power Plane Connect Rule(Relief Connect )(Conductor Width=0.254mm)"})), "power plane connect rule"); } }