app
Adom Footprint
Public Made by Adomby adom
KiCad footprint creator with layer HUD viewer and delivery to KiCad/Fusion 360
← Commit history
Sync current source (open-source, up to date)
19 files changed
+2494−120
Cargo.lock+1849README.md+68−38VERSION+1bin/adom-footprintdocs/icon.svg+1docs/screenshot-fusion-footprint.pngdocs/screenshot-kicad-footprint.pngdocs/screenshot-viewer.pnginstall.sh+12−2package.json+6−19release.sh+87src/app.html+69−6src/fp_render.rs+229src/main.rs+42−1src/provenance.rs+73src/render.rs+24−52src/server.rs+4−1tests/golden/R_0402.kicad_mod+24uninstall.sh+5−1Cargo.lockadded+1849@@ -0,0 +1,1849 @@+# This file is automatically @generated by Cargo.+# It is not intended for manual editing.+version = 4++[[package]]+name = "adom-footprint"+version = "0.1.6"+dependencies = [+ "anyhow",+ "clap",+ "clap_complete",+ "regex",+ "reqwest",+ "serde",+ "serde_json",+ "tiny_http",+]++[[package]]+name = "aho-corasick"+version = "1.1.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"+dependencies = [+ "memchr",+]++[[package]]+name = "anstream"+version = "1.0.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"+dependencies = [+ "anstyle",+ "anstyle-parse",+ "anstyle-query",+ "anstyle-wincon",+ "colorchoice",+ "is_terminal_polyfill",+ "utf8parse",+]++[[package]]+name = "anstyle"+version = "1.0.14"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"++[[package]]+name = "anstyle-parse"+version = "1.0.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"+dependencies = [+ "utf8parse",+]++[[package]]+name = "anstyle-query"+version = "1.1.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"+dependencies = [+ "windows-sys 0.61.2",+]++[[package]]+name = "anstyle-wincon"+version = "3.0.11"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"+dependencies = [+ "anstyle",+ "once_cell_polyfill",+ "windows-sys 0.61.2",+]++[[package]]+name = "anyhow"+version = "1.0.102"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"++[[package]]+name = "ascii"+version = "1.1.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"++[[package]]+name = "atomic-waker"+version = "1.1.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"++[[package]]+name = "base64"+version = "0.22.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"++[[package]]+name = "bitflags"+version = "2.11.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"++[[package]]+name = "bumpalo"+version = "3.20.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"++[[package]]+name = "bytes"+version = "1.11.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"++[[package]]+name = "cc"+version = "1.2.62"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"+dependencies = [+ "find-msvc-tools",+ "shlex",+]++[[package]]+name = "cfg-if"+version = "1.0.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"++[[package]]+name = "chunked_transfer"+version = "1.5.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"++[[package]]+name = "clap"+version = "4.6.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"+dependencies = [+ "clap_builder",+ "clap_derive",+]++[[package]]+name = "clap_builder"+version = "4.6.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"+dependencies = [+ "anstream",+ "anstyle",+ "clap_lex",+ "strsim",+]++[[package]]+name = "clap_complete"+version = "4.6.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772"+dependencies = [+ "clap",+]++[[package]]+name = "clap_derive"+version = "4.6.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"+dependencies = [+ "heck",+ "proc-macro2",+ "quote",+ "syn",+]++[[package]]+name = "clap_lex"+version = "1.1.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"++[[package]]+name = "colorchoice"+version = "1.0.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"++[[package]]+name = "core-foundation"+version = "0.9.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"+dependencies = [+ "core-foundation-sys",+ "libc",+]++[[package]]+name = "core-foundation"+version = "0.10.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"+dependencies = [+ "core-foundation-sys",+ "libc",+]++[[package]]+name = "core-foundation-sys"+version = "0.8.7"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"++[[package]]+name = "displaydoc"+version = "0.2.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"+dependencies = [+ "proc-macro2",+ "quote",+ "syn",+]++[[package]]+name = "encoding_rs"+version = "0.8.35"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"+dependencies = [+ "cfg-if",+]++[[package]]+name = "equivalent"+version = "1.0.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"++[[package]]+name = "errno"+version = "0.3.14"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"+dependencies = [+ "libc",+ "windows-sys 0.61.2",+]++[[package]]+name = "fastrand"+version = "2.4.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"++[[package]]+name = "find-msvc-tools"+version = "0.1.9"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"++[[package]]+name = "fnv"+version = "1.0.7"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"++[[package]]+name = "foldhash"+version = "0.1.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"++[[package]]+name = "foreign-types"+version = "0.3.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"+dependencies = [+ "foreign-types-shared",+]++[[package]]+name = "foreign-types-shared"+version = "0.1.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"++[[package]]+name = "form_urlencoded"+version = "1.2.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"+dependencies = [+ "percent-encoding",+]++[[package]]+name = "futures-channel"+version = "0.3.32"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"+dependencies = [+ "futures-core",+ "futures-sink",+]++[[package]]+name = "futures-core"+version = "0.3.32"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"++[[package]]+name = "futures-io"+version = "0.3.32"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"++[[package]]+name = "futures-sink"+version = "0.3.32"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"++[[package]]+name = "futures-task"+version = "0.3.32"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"++[[package]]+name = "futures-util"+version = "0.3.32"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"+dependencies = [+ "futures-core",+ "futures-io",+ "futures-sink",+ "futures-task",+ "memchr",+ "pin-project-lite",+ "slab",+]++[[package]]+name = "getrandom"+version = "0.2.17"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"+dependencies = [+ "cfg-if",+ "libc",+ "wasi",+]++[[package]]+name = "getrandom"+version = "0.4.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"+dependencies = [+ "cfg-if",+ "libc",+ "r-efi",+ "wasip2",+ "wasip3",+]++[[package]]+name = "h2"+version = "0.4.14"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733"+dependencies = [+ "atomic-waker",+ "bytes",+ "fnv",+ "futures-core",+ "futures-sink",+ "http",+ "indexmap",+ "slab",+ "tokio",+ "tokio-util",+ "tracing",+]++[[package]]+name = "hashbrown"+version = "0.15.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"+dependencies = [+ "foldhash",+]++[[package]]+name = "hashbrown"+version = "0.17.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"++[[package]]+name = "heck"+version = "0.5.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"++[[package]]+name = "http"+version = "1.4.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"+dependencies = [+ "bytes",+ "itoa",+]++[[package]]+name = "http-body"+version = "1.0.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"+dependencies = [+ "bytes",+ "http",+]++[[package]]+name = "http-body-util"+version = "0.1.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"+dependencies = [+ "bytes",+ "futures-core",+ "http",+ "http-body",+ "pin-project-lite",+]++[[package]]+name = "httparse"+version = "1.10.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"++[[package]]+name = "httpdate"+version = "1.0.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"++[[package]]+name = "hyper"+version = "1.9.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"+dependencies = [+ "atomic-waker",+ "bytes",+ "futures-channel",+ "futures-core",+ "h2",+ "http",+ "http-body",+ "httparse",+ "itoa",+ "pin-project-lite",+ "smallvec",+ "tokio",+ "want",+]++[[package]]+name = "hyper-rustls"+version = "0.27.9"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"+dependencies = [+ "http",+ "hyper",+ "hyper-util",+ "rustls",+ "tokio",+ "tokio-rustls",+ "tower-service",+]++[[package]]+name = "hyper-tls"+version = "0.6.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"+dependencies = [+ "bytes",+ "http-body-util",+ "hyper",+ "hyper-util",+ "native-tls",+ "tokio",+ "tokio-native-tls",+ "tower-service",+]++[[package]]+name = "hyper-util"+version = "0.1.20"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"+dependencies = [+ "base64",+ "bytes",+ "futures-channel",+ "futures-util",+ "http",+ "http-body",+ "hyper",+ "ipnet",+ "libc",+ "percent-encoding",+ "pin-project-lite",+ "socket2",+ "system-configuration",+ "tokio",+ "tower-service",+ "tracing",+ "windows-registry",+]++[[package]]+name = "icu_collections"+version = "2.2.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"+dependencies = [+ "displaydoc",+ "potential_utf",+ "utf8_iter",+ "yoke",+ "zerofrom",+ "zerovec",+]++[[package]]+name = "icu_locale_core"+version = "2.2.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"+dependencies = [+ "displaydoc",+ "litemap",+ "tinystr",+ "writeable",+ "zerovec",+]++[[package]]+name = "icu_normalizer"+version = "2.2.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"+dependencies = [+ "icu_collections",+ "icu_normalizer_data",+ "icu_properties",+ "icu_provider",+ "smallvec",+ "zerovec",+]++[[package]]+name = "icu_normalizer_data"+version = "2.2.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"++[[package]]+name = "icu_properties"+version = "2.2.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"+dependencies = [+ "icu_collections",+ "icu_locale_core",+ "icu_properties_data",+ "icu_provider",+ "zerotrie",+ "zerovec",+]++[[package]]+name = "icu_properties_data"+version = "2.2.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"++[[package]]+name = "icu_provider"+version = "2.2.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"+dependencies = [+ "displaydoc",+ "icu_locale_core",+ "writeable",+ "yoke",+ "zerofrom",+ "zerotrie",+ "zerovec",+]++[[package]]+name = "id-arena"+version = "2.3.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"++[[package]]+name = "idna"+version = "1.1.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"+dependencies = [+ "idna_adapter",+ "smallvec",+ "utf8_iter",+]++[[package]]+name = "idna_adapter"+version = "1.2.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"+dependencies = [+ "icu_normalizer",+ "icu_properties",+]++[[package]]+name = "indexmap"+version = "2.14.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"+dependencies = [+ "equivalent",+ "hashbrown 0.17.1",+ "serde",+ "serde_core",+]++[[package]]+name = "ipnet"+version = "2.12.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"++[[package]]+name = "is_terminal_polyfill"+version = "1.70.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"++[[package]]+name = "itoa"+version = "1.0.18"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"++[[package]]+name = "js-sys"+version = "0.3.98"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"+dependencies = [+ "cfg-if",+ "futures-util",+ "once_cell",+ "wasm-bindgen",+]++[[package]]+name = "leb128fmt"+version = "0.1.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"++[[package]]+name = "libc"+version = "0.2.186"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"++[[package]]+name = "linux-raw-sys"+version = "0.12.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"++[[package]]+name = "litemap"+version = "0.8.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"++[[package]]+name = "log"+version = "0.4.29"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"++[[package]]+name = "memchr"+version = "2.8.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"++[[package]]+name = "mime"+version = "0.3.17"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"++[[package]]+name = "mio"+version = "1.2.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"+dependencies = [+ "libc",+ "wasi",+ "windows-sys 0.61.2",+]++[[package]]+name = "native-tls"+version = "0.2.18"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"+dependencies = [+ "libc",+ "log",+ "openssl",+ "openssl-probe",+ "openssl-sys",+ "schannel",+ "security-framework",+ "security-framework-sys",+ "tempfile",+]++[[package]]+name = "once_cell"+version = "1.21.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"++[[package]]+name = "once_cell_polyfill"+version = "1.70.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"++[[package]]+name = "openssl"+version = "0.10.80"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967"+dependencies = [+ "bitflags",+ "cfg-if",+ "foreign-types",+ "libc",+ "openssl-macros",+ "openssl-sys",+]++[[package]]+name = "openssl-macros"+version = "0.1.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"+dependencies = [+ "proc-macro2",+ "quote",+ "syn",+]++[[package]]+name = "openssl-probe"+version = "0.2.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"++[[package]]+name = "openssl-sys"+version = "0.9.116"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4"+dependencies = [+ "cc",+ "libc",+ "pkg-config",+ "vcpkg",+]++[[package]]+name = "percent-encoding"+version = "2.3.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"++[[package]]+name = "pin-project-lite"+version = "0.2.17"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"++[[package]]+name = "pkg-config"+version = "0.3.33"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"++[[package]]+name = "potential_utf"+version = "0.1.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"+dependencies = [+ "zerovec",+]++[[package]]+name = "prettyplease"+version = "0.2.37"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"+dependencies = [+ "proc-macro2",+ "syn",+]++[[package]]+name = "proc-macro2"+version = "1.0.106"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"+dependencies = [+ "unicode-ident",+]++[[package]]+name = "quote"+version = "1.0.45"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"+dependencies = [+ "proc-macro2",+]++[[package]]+name = "r-efi"+version = "6.0.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"++[[package]]+name = "regex"+version = "1.12.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"+dependencies = [+ "aho-corasick",+ "memchr",+ "regex-automata",+ "regex-syntax",+]++[[package]]+name = "regex-automata"+version = "0.4.14"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"+dependencies = [+ "aho-corasick",+ "memchr",+ "regex-syntax",+]++[[package]]+name = "regex-syntax"+version = "0.8.11"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"++[[package]]+name = "reqwest"+version = "0.12.28"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"+dependencies = [+ "base64",+ "bytes",+ "encoding_rs",+ "futures-channel",+ "futures-core",+ "futures-util",+ "h2",+ "http",+ "http-body",+ "http-body-util",+ "hyper",+ "hyper-rustls",+ "hyper-tls",+ "hyper-util",+ "js-sys",+ "log",+ "mime",+ "native-tls",+ "percent-encoding",+ "pin-project-lite",+ "rustls-pki-types",+ "serde",+ "serde_json",+ "serde_urlencoded",+ "sync_wrapper",+ "tokio",+ "tokio-native-tls",+ "tower",+ "tower-http",+ "tower-service",+ "url",+ "wasm-bindgen",+ "wasm-bindgen-futures",+ "web-sys",+]++[[package]]+name = "ring"+version = "0.17.14"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"+dependencies = [+ "cc",+ "cfg-if",+ "getrandom 0.2.17",+ "libc",+ "untrusted",+ "windows-sys 0.52.0",+]++[[package]]+name = "rustix"+version = "1.1.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"+dependencies = [+ "bitflags",+ "errno",+ "libc",+ "linux-raw-sys",+ "windows-sys 0.61.2",+]++[[package]]+name = "rustls"+version = "0.23.40"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"+dependencies = [+ "once_cell",+ "rustls-pki-types",+ "rustls-webpki",+ "subtle",+ "zeroize",+]++[[package]]+name = "rustls-pki-types"+version = "1.14.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"+dependencies = [+ "zeroize",+]++[[package]]+name = "rustls-webpki"+version = "0.103.13"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"+dependencies = [+ "ring",+ "rustls-pki-types",+ "untrusted",+]++[[package]]+name = "rustversion"+version = "1.0.22"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"++[[package]]+name = "ryu"+version = "1.0.23"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"++[[package]]+name = "schannel"+version = "0.1.29"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"+dependencies = [+ "windows-sys 0.61.2",+]++[[package]]+name = "security-framework"+version = "3.7.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"+dependencies = [+ "bitflags",+ "core-foundation 0.10.1",+ "core-foundation-sys",+ "libc",+ "security-framework-sys",+]++[[package]]+name = "security-framework-sys"+version = "2.17.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"+dependencies = [+ "core-foundation-sys",+ "libc",+]++[[package]]+name = "semver"+version = "1.0.28"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"++[[package]]+name = "serde"+version = "1.0.228"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"+dependencies = [+ "serde_core",+ "serde_derive",+]++[[package]]+name = "serde_core"+version = "1.0.228"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"+dependencies = [+ "serde_derive",+]++[[package]]+name = "serde_derive"+version = "1.0.228"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"+dependencies = [+ "proc-macro2",+ "quote",+ "syn",+]++[[package]]+name = "serde_json"+version = "1.0.149"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"+dependencies = [+ "itoa",+ "memchr",+ "serde",+ "serde_core",+ "zmij",+]++[[package]]+name = "serde_urlencoded"+version = "0.7.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"+dependencies = [+ "form_urlencoded",+ "itoa",+ "ryu",+ "serde",+]++[[package]]+name = "shlex"+version = "1.3.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"++[[package]]+name = "slab"+version = "0.4.12"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"++[[package]]+name = "smallvec"+version = "1.15.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"++[[package]]+name = "socket2"+version = "0.6.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"+dependencies = [+ "libc",+ "windows-sys 0.61.2",+]++[[package]]+name = "stable_deref_trait"+version = "1.2.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"++[[package]]+name = "strsim"+version = "0.11.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"++[[package]]+name = "subtle"+version = "2.6.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"++[[package]]+name = "syn"+version = "2.0.117"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"+dependencies = [+ "proc-macro2",+ "quote",+ "unicode-ident",+]++[[package]]+name = "sync_wrapper"+version = "1.0.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"+dependencies = [+ "futures-core",+]++[[package]]+name = "synstructure"+version = "0.13.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"+dependencies = [+ "proc-macro2",+ "quote",+ "syn",+]++[[package]]+name = "system-configuration"+version = "0.7.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"+dependencies = [+ "bitflags",+ "core-foundation 0.9.4",+ "system-configuration-sys",+]++[[package]]+name = "system-configuration-sys"+version = "0.6.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"+dependencies = [+ "core-foundation-sys",+ "libc",+]++[[package]]+name = "tempfile"+version = "3.27.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"+dependencies = [+ "fastrand",+ "getrandom 0.4.2",+ "once_cell",+ "rustix",+ "windows-sys 0.61.2",+]++[[package]]+name = "tiny_http"+version = "0.12.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82"+dependencies = [+ "ascii",+ "chunked_transfer",+ "httpdate",+ "log",+]++[[package]]+name = "tinystr"+version = "0.8.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"+dependencies = [+ "displaydoc",+ "zerovec",+]++[[package]]+name = "tokio"+version = "1.52.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"+dependencies = [+ "bytes",+ "libc",+ "mio",+ "pin-project-lite",+ "socket2",+ "windows-sys 0.61.2",+]++[[package]]+name = "tokio-native-tls"+version = "0.3.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"+dependencies = [+ "native-tls",+ "tokio",+]++[[package]]+name = "tokio-rustls"+version = "0.26.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"+dependencies = [+ "rustls",+ "tokio",+]++[[package]]+name = "tokio-util"+version = "0.7.18"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"+dependencies = [+ "bytes",+ "futures-core",+ "futures-sink",+ "pin-project-lite",+ "tokio",+]++[[package]]+name = "tower"+version = "0.5.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"+dependencies = [+ "futures-core",+ "futures-util",+ "pin-project-lite",+ "sync_wrapper",+ "tokio",+ "tower-layer",+ "tower-service",+]++[[package]]+name = "tower-http"+version = "0.6.10"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51"+dependencies = [+ "bitflags",+ "bytes",+ "futures-util",+ "http",+ "http-body",+ "pin-project-lite",+ "tower",+ "tower-layer",+ "tower-service",+ "url",+]++[[package]]+name = "tower-layer"+version = "0.3.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"++[[package]]+name = "tower-service"+version = "0.3.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"++[[package]]+name = "tracing"+version = "0.1.44"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"+dependencies = [+ "pin-project-lite",+ "tracing-core",+]++[[package]]+name = "tracing-core"+version = "0.1.36"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"+dependencies = [+ "once_cell",+]++[[package]]+name = "try-lock"+version = "0.2.5"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"++[[package]]+name = "unicode-ident"+version = "1.0.24"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"++[[package]]+name = "unicode-xid"+version = "0.2.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"++[[package]]+name = "untrusted"+version = "0.9.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"++[[package]]+name = "url"+version = "2.5.8"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"+dependencies = [+ "form_urlencoded",+ "idna",+ "percent-encoding",+ "serde",+]++[[package]]+name = "utf8_iter"+version = "1.0.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"++[[package]]+name = "utf8parse"+version = "0.2.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"++[[package]]+name = "vcpkg"+version = "0.2.15"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"++[[package]]+name = "want"+version = "0.3.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"+dependencies = [+ "try-lock",+]++[[package]]+name = "wasi"+version = "0.11.1+wasi-snapshot-preview1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"++[[package]]+name = "wasip2"+version = "1.0.3+wasi-0.2.9"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"+dependencies = [+ "wit-bindgen 0.57.1",+]++[[package]]+name = "wasip3"+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"+dependencies = [+ "wit-bindgen 0.51.0",+]++[[package]]+name = "wasm-bindgen"+version = "0.2.121"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790"+dependencies = [+ "cfg-if",+ "once_cell",+ "rustversion",+ "wasm-bindgen-macro",+ "wasm-bindgen-shared",+]++[[package]]+name = "wasm-bindgen-futures"+version = "0.4.71"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8"+dependencies = [+ "js-sys",+ "wasm-bindgen",+]++[[package]]+name = "wasm-bindgen-macro"+version = "0.2.121"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578"+dependencies = [+ "quote",+ "wasm-bindgen-macro-support",+]++[[package]]+name = "wasm-bindgen-macro-support"+version = "0.2.121"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2"+dependencies = [+ "bumpalo",+ "proc-macro2",+ "quote",+ "syn",+ "wasm-bindgen-shared",+]++[[package]]+name = "wasm-bindgen-shared"+version = "0.2.121"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441"+dependencies = [+ "unicode-ident",+]++[[package]]+name = "wasm-encoder"+version = "0.244.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"+dependencies = [+ "leb128fmt",+ "wasmparser",+]++[[package]]+name = "wasm-metadata"+version = "0.244.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"+dependencies = [+ "anyhow",+ "indexmap",+ "wasm-encoder",+ "wasmparser",+]++[[package]]+name = "wasmparser"+version = "0.244.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"+dependencies = [+ "bitflags",+ "hashbrown 0.15.5",+ "indexmap",+ "semver",+]++[[package]]+name = "web-sys"+version = "0.3.98"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa"+dependencies = [+ "js-sys",+ "wasm-bindgen",+]++[[package]]+name = "windows-link"+version = "0.2.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"++[[package]]+name = "windows-registry"+version = "0.6.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"+dependencies = [+ "windows-link",+ "windows-result",+ "windows-strings",+]++[[package]]+name = "windows-result"+version = "0.4.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"+dependencies = [+ "windows-link",+]++[[package]]+name = "windows-strings"+version = "0.5.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"+dependencies = [+ "windows-link",+]++[[package]]+name = "windows-sys"+version = "0.52.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"+dependencies = [+ "windows-targets",+]++[[package]]+name = "windows-sys"+version = "0.61.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"+dependencies = [+ "windows-link",+]++[[package]]+name = "windows-targets"+version = "0.52.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"+dependencies = [+ "windows_aarch64_gnullvm",+ "windows_aarch64_msvc",+ "windows_i686_gnu",+ "windows_i686_gnullvm",+ "windows_i686_msvc",+ "windows_x86_64_gnu",+ "windows_x86_64_gnullvm",+ "windows_x86_64_msvc",+]++[[package]]+name = "windows_aarch64_gnullvm"+version = "0.52.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"++[[package]]+name = "windows_aarch64_msvc"+version = "0.52.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"++[[package]]+name = "windows_i686_gnu"+version = "0.52.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"++[[package]]+name = "windows_i686_gnullvm"+version = "0.52.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"++[[package]]+name = "windows_i686_msvc"+version = "0.52.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"++[[package]]+name = "windows_x86_64_gnu"+version = "0.52.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"++[[package]]+name = "windows_x86_64_gnullvm"+version = "0.52.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"++[[package]]+name = "windows_x86_64_msvc"+version = "0.52.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"++[[package]]+name = "wit-bindgen"+version = "0.51.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"+dependencies = [+ "wit-bindgen-rust-macro",+]++[[package]]+name = "wit-bindgen"+version = "0.57.1"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"++[[package]]+name = "wit-bindgen-core"+version = "0.51.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"+dependencies = [+ "anyhow",+ "heck",+ "wit-parser",+]++[[package]]+name = "wit-bindgen-rust"+version = "0.51.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"+dependencies = [+ "anyhow",+ "heck",+ "indexmap",+ "prettyplease",+ "syn",+ "wasm-metadata",+ "wit-bindgen-core",+ "wit-component",+]++[[package]]+name = "wit-bindgen-rust-macro"+version = "0.51.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"+dependencies = [+ "anyhow",+ "prettyplease",+ "proc-macro2",+ "quote",+ "syn",+ "wit-bindgen-core",+ "wit-bindgen-rust",+]++[[package]]+name = "wit-component"+version = "0.244.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"+dependencies = [+ "anyhow",+ "bitflags",+ "indexmap",+ "log",+ "serde",+ "serde_derive",+ "serde_json",+ "wasm-encoder",+ "wasm-metadata",+ "wasmparser",+ "wit-parser",+]++[[package]]+name = "wit-parser"+version = "0.244.0"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"+dependencies = [+ "anyhow",+ "id-arena",+ "indexmap",+ "log",+ "semver",+ "serde",+ "serde_derive",+ "serde_json",+ "unicode-xid",+ "wasmparser",+]++[[package]]+name = "writeable"+version = "0.6.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"++[[package]]+name = "yoke"+version = "0.8.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"+dependencies = [+ "stable_deref_trait",+ "yoke-derive",+ "zerofrom",+]++[[package]]+name = "yoke-derive"+version = "0.8.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"+dependencies = [+ "proc-macro2",+ "quote",+ "syn",+ "synstructure",+]++[[package]]+name = "zerofrom"+version = "0.1.8"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"+dependencies = [+ "zerofrom-derive",+]++[[package]]+name = "zerofrom-derive"+version = "0.1.7"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"+dependencies = [+ "proc-macro2",+ "quote",+ "syn",+ "synstructure",+]++[[package]]+name = "zeroize"+version = "1.8.2"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"++[[package]]+name = "zerotrie"+version = "0.2.4"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"+dependencies = [+ "displaydoc",+ "yoke",+ "zerofrom",+]++[[package]]+name = "zerovec"+version = "0.11.6"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"+dependencies = [+ "yoke",+ "zerofrom",+ "zerovec-derive",+]++[[package]]+name = "zerovec-derive"+version = "0.11.3"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"+dependencies = [+ "proc-macro2",+ "quote",+ "syn",+]++[[package]]+name = "zmij"+version = "1.0.21"+source = "registry+https://github.com/rust-lang/crates.io-index"+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
README.md+68−38@@ -1,64 +1,94 @@ # adom-footprint -**A live PCB-footprint viewer with a floating Layers HUD — and the InstaPCB 300 µm solder-paste dispensing dots, jetted dot-by-dot, that no other PCB tool shows.**+Standalone Rust CLI + live app for **KiCad PCB footprints**. Generates+`.kicad_mod` files natively in Rust, renders them as dark-themed SVGs through the+shared **service-kicad** container, and serves an AI-drivable live viewer.+No Node, no gallia, no local KiCad install. -adom-footprint renders a `.kicad_mod` through the shared `service-kicad` (no local CAD install), crops tightly to the part, and themes it onto a dark PCB canvas. It reads legacy `(module …)` footprints as well as modern ones. Standalone Rust, no Node, no gallia.+ -## The Layers HUD+## What it does -+- **Generate** `.kicad_mod` natively in Rust from structured pad data — pads+ (rect/roundrect/circle, rotation, custom layers), body outline, and courtyard.+- **Render** the footprint to a cropped, dark-themed SVG via service-kicad+ (`POST /kicad/pcb/export/svg`) by wrapping it in a minimal one-footprint board+ at page center — the same render path chip-fetcher uses, so thumbnail and app+ match.+- **Serve** a live, AI-drivable app: the frontend stays connected to the Rust+ server (`GET /state` poller + `POST /eval` channel), so the AI can push a new+ footprint, hot-patch the UI, or read the console in real time. -A draggable, collapsible floating panel toggles each layer independently — **Copper**, **Assembly (F.Fab)**, **Courtyard**, **Paste apertures**, and the **InstaPCB paste dots**. The standard fab layers are on by default; the **InstaPCB paste-dots overlay is off by default** — it's an opt-in preview you flip on when you want it, so it never obscures the copper. Each row carries a live count (pads, openings, dots), and the HUD remembers its position and which layers you left on.+## Install -## The board+```+adompkg install adom-footprint+``` -+## Quick start -The rendered footprint on the dark canvas, in its **default state**: red copper pads with the central thermal pad, the yellow courtyard corners, and the pin-1 marker. Everything is cropped tight to the part — no CAD page frame, no worksheet title block — so the footprint fills the view. (The InstaPCB paste dots are off here by default — see below.)+```bash+# Create a footprint from pad JSON (stdin or --file)+echo '{"footprintName":"R_0402","pads":[...]}' | adom-footprint create --out R_0402 -## The signature feature: InstaPCB paste dots+# Preview an existing .kicad_mod as a themed viewer+adom-footprint preview --file R_0402/R_0402.kicad_mod -+# Run the live app (AI-drivable: /state, /load, /eval, /console)+adom-footprint serve --port 8783 --file R_0402/R_0402.kicad_mod+``` -Flip the **InstaPCB paste dots** layer on (it's off by default) and every pad fills with green dots — the **actual 300 µm solder-paste dispensing positions**, computed by Adom and overlaid on the copper. Adom's InstaPCB line **jets paste dot-by-dot instead of using a stencil**, so a footprint in the Adom ecosystem carries *where the paste will land* — the positions travel with the footprint, pad to pad. That's a first in PCB CAD: you're previewing the dispense program, not just a stencil aperture.+Open the served app through the Adom proxy URL+(`https://<slug>.adom.cloud/proxy/8783/`), never `localhost:8783` — the webview+renders outside the container. -+## CLI reference -## Interactive embed — drop the live viewer into any page+| Command | Description |+|---|---|+| `create --file <json> [--out <dir>]` | Generate `.kicad_mod` + SVG + viewer HTML from pad JSON |+| `preview --file <kicad_mod> [--out <dir>]` | Render an existing footprint to a themed viewer |+| `serve [--port 8783] [--file <kicad_mod>]` | Run the live AI-drivable app server | -+## Architecture -`adom-footprint embed` exports a **single self-contained `.html` file** — pan, zoom, and the full **Layers panel** (copper / fab / courtyard / paste apertures, and the opt-in InstaPCB paste-dot overlay), no server and no external assets — built to drop straight into an `<iframe>` on a **wiki component page**.+Pure Rust. Footprint generation (`src/fp_gen.rs`) is **verified against golden+files** in `tests/` (e.g. `R_0402.kicad_mod`, byte-for-byte). The only network+dependency is **service-kicad** for SVG rendering — a lightweight proxy to the+shared KiCad-10 container, so nothing installs KiCad locally. The live app+server (`src/server.rs`) follows the Adom app-creator contract: `GET /state`,+`POST /load`, `POST /eval` + `GET /eval/pending` + `POST /eval/:id/result`,+`GET/POST /console`, `POST /shutdown`, `GET /favicon.svg`, and best-effort HD+port registration. -```bash-adom-footprint embed --file part.kicad_mod # → part-embed.html-```-```html-<iframe src="…/part-embed.html" width="100%" height="520" style="border:0"></iframe>-```+## Build from source++The full source ships on the wiki app page — anyone in the community can build+it locally: -## Send to+```bash+# Fetch the source bundle from the wiki+adompkg source adom-footprint # downloads + unpacks the repo -KiCad (native footprint), Fusion 360 / Altium / OrCAD (`.lbr` via **adom-lbr**).+cargo build --release # standalone — only needs Rust + cargo+cargo test --release # runs the golden-file parity tests+cp target/release/adom-footprint ~/.local/bin/+``` -## Open source — check it out, vibe-code it, send it back+The only runtime dependency is network access to a service-kicad endpoint+(`KICAD_SERVICE_API`, defaults to the shared container). -adom-footprint is MIT and **fully open**. The complete Rust source is in this page's **Files** tab — browse it, or grab `src/` + `Cargo.toml`.+## Contribute back via pull request -1. **Build it.** `cargo build --release` drops a standalone binary (no Node, no gallia).-2. **Point your install at your checkout.** `adompkg link john/adom-footprint ./adom-footprint` so the `adom-footprint` you run is your local copy.-3. **Vibe-code a change.** Open the folder in Claude Code and describe what you want — a new layer, a different paste-dot pitch, another Send-to target. The legacy-KiCad normaliser and the paste-dot generator are each one small, readable module.-4. **Send it back.** Open a **pull request** on this page (the wiki reviews and merges per-repo PRs), or start a discussion thread. Good additions get folded into the shipped app.+adom-footprint is public, source included. To propose a change: -## Part of the Adom EDA family+1. `adompkg source adom-footprint` to get the repo.+2. Make your change and run `cargo test --release` (keep the golden tests green).+3. Open a pull request against the wiki page for `adom-footprint`. -Opened from **chip-fetcher**; pairs with **adom-symbol**, **adom-lbr**, and **ds2sf**.+> **Note:** real PR support on wiki.adom.inc is in progress. Until it lands,+> send patches or a branch link to the maintainers and they will apply it. -## Run it+## Wiki -```bash-adom-footprint serve --port 8783 # live app (AI-drivable: /state, /load, /deliver, /eval)-adom-footprint render --file part.kicad_mod --out part.svg-adom-footprint embed --file part.kicad_mod # interactive HTML for an iframe-```-MIT, contributions welcome.+https://wiki.adom.inc/apps/adom-footprint
VERSIONadded+1@@ -0,0 +1 @@+0.1.6
bin/adom-footprint⋯ 1 unchanged line ⋯
docs/icon.svgadded+1@@ -0,0 +1 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><rect x="6.5" y="9" width="3" height="6" rx="0.6"/><rect x="14.5" y="9" width="3" height="6" rx="0.6"/><path d="M6 6.2h1.2"/></svg>
docs/screenshot-fusion-footprint.pngadded⋯ 1 unchanged line ⋯
docs/screenshot-kicad-footprint.pngadded⋯ 1 unchanged line ⋯
docs/screenshot-viewer.pngadded⋯ 1 unchanged line ⋯
install.sh+12−2@@ -1,4 +1,14 @@ #!/bin/bash+# install.sh for adom-footprint (app).+# Runs from the extracted module directory (~/project/adom_modules/adom-footprint/). set -euo pipefail-DIR="$(cd "$(dirname "$0")" && pwd)"; BIN="${HOME}/.local/bin"; mkdir -p "$BIN"-install -m 0755 "$DIR/bin/adom-footprint" "$BIN/adom-footprint"; echo "Installed adom-footprint"++DIR="$(cd "$(dirname "$0")" && pwd)"+cd "$DIR"++# App install: link the binary onto PATH.+BIN_DIR="${HOME}/.local/bin"+mkdir -p "$BIN_DIR"+install -m 0755 "$DIR/bin/adom-footprint" "$BIN_DIR/adom-footprint"+echo "Installed adom-footprint -> $BIN_DIR/adom-footprint"+
package.json+6−19@@ -1,27 +1,14 @@ { "slug": "adom-footprint",- "version": "1.0.7",+ "version": "0.1.6", "type": "app",- "description": "Toggle copper, fab and the InstaPCB paste dots; export an adom-UI interactive embed for component pages.",- "dependencies": {},- "tags": [- "eda",- "kicad",- "footprint",- "pcb",- "instapcb"- ],- "hero": {- "type": "billboard",- "headline": "Footprints with a layers HUD.",- "subhead": "Toggle copper, fab and the InstaPCB paste dots; export an adom-UI interactive embed for component pages.",- "screenshot": "screenshots/shot.png",- "accent": "#00b8b1"+ "description": "KiCad footprint creator with layer HUD viewer and delivery to KiCad/Fusion 360",+ "dependencies": {+ "adom-lbr": "^0.1.0" }, "scripts": { "install": "./install.sh", "uninstall": "./uninstall.sh" },- "needs_sudo": false,- "license": "MIT"-}+ "needs_sudo": false+}
release.shadded+87@@ -0,0 +1,87 @@+#!/bin/bash+#+# Canonical release.sh for an adompkg source repo.+#+# Reads version from package.json, optionally bumps it (--bump patch|minor|major),+# runs build.sh if present, then runs `adompkg publish` and prints the resulting+# package URL.+#+# Usage:+# ADOMPKG_TOKEN=<carbon-api-key> ./release.sh # publish current version+# ADOMPKG_TOKEN=<carbon-api-key> ./release.sh --bump patch # bump + publish+#+set -euo pipefail++DIR="$(cd "$(dirname "$0")" && pwd)"+cd "$DIR"++if [ ! -f package.json ]; then+ echo "release.sh: no package.json found in $DIR" >&2+ exit 1+fi++REGISTRY="${ADOMPKG_REGISTRY:-https://git-wiki-ktqxite5iglh.adom.cloud}"++if [ -z "${ADOMPKG_TOKEN:-}" ]; then+ if [ -r /var/run/adom/api-key ]; then+ ADOMPKG_TOKEN="$(cat /var/run/adom/api-key)"+ export ADOMPKG_TOKEN+ else+ echo "release.sh: ADOMPKG_TOKEN is required (set ADOMPKG_TOKEN=<carbon-api-key>)" >&2+ exit 1+ fi+fi++# Read current version.+CUR_VERSION=$(node -e "console.log(require('./package.json').version)")+SLUG=$(node -e "console.log(require('./package.json').slug || require('./package.json').name)")++BUMP=""+for a in "$@"; do+ case "$a" in+ --bump)+ shift+ BUMP="${1:-patch}"+ shift || true+ ;;+ --bump=*)+ BUMP="${a#--bump=}"+ shift+ ;;+ esac+done++if [ -n "$BUMP" ]; then+ NEXT=$(node -e "+ const [maj, min, pat] = process.argv[1].split('.').map(n => parseInt(n, 10));+ const b = process.argv[2];+ let out;+ if (b === 'major') out = (maj + 1) + '.0.0';+ else if (b === 'minor') out = maj + '.' + (min + 1) + '.0';+ else out = maj + '.' + min + '.' + (pat + 1);+ console.log(out);+ " "$CUR_VERSION" "$BUMP")+ echo "release.sh: bumping $CUR_VERSION -> $NEXT ($BUMP)"+ node -e "+ const fs = require('fs');+ const p = JSON.parse(fs.readFileSync('package.json', 'utf8'));+ p.version = process.argv[1];+ fs.writeFileSync('package.json', JSON.stringify(p, null, 2) + '\n');+ " "$NEXT"+ CUR_VERSION="$NEXT"+fi++# Optional build step.+if [ -x ./build.sh ]; then+ echo "release.sh: running build.sh"+ ./build.sh+fi++# Publish.+echo "release.sh: publishing $SLUG@$CUR_VERSION to $REGISTRY"+adompkg publish .++# Print published URL.+echo+echo "Published: $REGISTRY/pages/$SLUG"+echo "Install: adompkg install $SLUG"
src/app.html+69−6@@ -82,8 +82,15 @@ .card .name { font-size:20px; font-weight:700; } .card .meta { color:var(--muted); font-size:13px; margin-top:2px; } .card .desc { color:#aeb6c2; font-size:13px; margin-top:6px; line-height:1.4; }- .svgbox { margin-top:16px; border:1px solid var(--border); border-radius:10px; background:#0d1117; overflow:hidden; min-height:200px; display:flex; align-items:center; justify-content:center; }- .svgbox svg { display:block; width:100%; height:auto; max-height:62vh; }+ .svgbox { margin-top:16px; border:1px solid var(--border); border-radius:10px; background:radial-gradient(circle at 50% 42%,#121a26 0%,#0d1117 72%); overflow:hidden; position:relative; height:64vh; cursor:grab; }+ .svgbox.grab { cursor:grabbing; }+ .svgbox svg { position:absolute; left:0; top:0; width:100%; height:100%; transform-origin:0 0; display:block; }+ .zoomtools { position:absolute; left:10px; top:10px; display:flex; gap:6px; z-index:5; }+ .zoomtools button { width:30px; height:30px; border-radius:8px; border:1px solid var(--border); background:var(--surface2,#1c2230); color:#cfd8e2; font-size:16px; cursor:pointer; display:grid; place-items:center; }+ .zoomtools button:hover { border-color:var(--accent2); color:var(--accent2); }+ .panhint { position:absolute; right:10px; bottom:9px; color:var(--text3); font-size:11px; z-index:5; pointer-events:none; }+ #svgbox [data-pad] { cursor:pointer; }+ #svgbox [data-pad]:hover > * { stroke:#fff !important; stroke-width:0.12 !important; } .empty { color:var(--text3); font-size:13px; padding:40px; } footer { flex:0 0 auto; text-align:center; color:var(--text3); font-size:11px; padding:8px; border-top:1px solid var(--border); } @@ -167,6 +174,12 @@ document.addEventListener('mouseover',e=>{ const el=e.target.closest('[data-tooltip]'); if(!el||el===cur) return; cur=el; clearTimeout(timer); timer=setTimeout(()=>{ tt.textContent=el.getAttribute('data-tooltip'); place(el); },500); }); document.addEventListener('mouseout',e=>{ if(e.target.closest('[data-tooltip]')===cur){ clearTimeout(timer); tt.classList.remove('show'); cur=null; } });+ // Pad hover — number / shape / size / mount, like the original footprint app.+ document.addEventListener('mouseover',e=>{ const g=e.target.closest('[data-pad]'); if(!g) return;+ tt.innerHTML='<div style="font-weight:700;font-size:14px;color:#37c8a6">Pad '+g.getAttribute('data-pad')+'</div>'+ +'<div style="font-size:11.5px;color:#aeb6c2;margin-top:3px">'+(g.getAttribute('data-shape')||'')+' · '+(g.getAttribute('data-size')||'')+' mm · '+(g.getAttribute('data-mount')||'')+'</div>';+ place(g); });+ document.addEventListener('mouseout',e=>{ if(e.target.closest('[data-pad]')) tt.classList.remove('show'); }); })(); let lastHash='';@@ -174,6 +187,7 @@ try{ const r=await fetch('state',{cache:'no-store'}); const s=await r.json(); setConn(true);+ window._lastState=s; const hash=JSON.stringify([s.footprintName,(s.svg||'').length]); if(hash!==lastHash){ lastHash=hash; render(s); } }catch(e){ setConn(false); }@@ -192,17 +206,49 @@ document.getElementById('svgbox').innerHTML=s.svg||''; document.getElementById('sendto').style.display=(has&&s.hasMod)?'inline-flex':'none'; setupLayers(s);+ initPanZoom(); } setInterval(pollState,1000); pollState(); + // ── Pan / zoom the footprint (drag to pan, scroll to zoom, +/-/fit) ──+ let _fpScale=1,_fpTx=0,_fpTy=0,_panZoomWired=false;+ function _fpApply(){ const sv=document.querySelector('#svgbox svg'); if(sv) sv.style.transform='translate('+_fpTx+'px,'+_fpTy+'px) scale('+_fpScale+')'; }+ function _fpZoom(cx,cy,f){ _fpTx=cx-(cx-_fpTx)*f; _fpTy=cy-(cy-_fpTy)*f; _fpScale*=f; _fpApply(); }+ function _fpFit(){ _fpScale=1;_fpTx=0;_fpTy=0;_fpApply(); }+ function initPanZoom(){+ const box=document.getElementById('svgbox'); if(!box) return;+ const sv=box.querySelector('svg'); if(sv){ sv.removeAttribute('width'); sv.removeAttribute('height'); }+ _fpFit();+ if(box.querySelector('.zoomtools')===null){+ const t=document.createElement('div'); t.className='zoomtools';+ t.innerHTML='<button id="fpZin" title="Zoom in">+</button><button id="fpZout" title="Zoom out">−</button><button id="fpFit" title="Fit">⤢</button>';+ box.appendChild(t);+ const h=document.createElement('div'); h.className='panhint'; h.textContent='drag to pan · scroll to zoom'; box.appendChild(h);+ t.querySelector('#fpZin').onclick=()=>{const r=box.getBoundingClientRect();_fpZoom(r.width/2,r.height/2,1.2)};+ t.querySelector('#fpZout').onclick=()=>{const r=box.getBoundingClientRect();_fpZoom(r.width/2,r.height/2,1/1.2)};+ t.querySelector('#fpFit').onclick=_fpFit;+ }+ if(_panZoomWired) return; _panZoomWired=true;+ box.addEventListener('wheel',e=>{ e.preventDefault(); const r=box.getBoundingClientRect(); _fpZoom(e.clientX-r.left,e.clientY-r.top,e.deltaY<0?1.12:1/1.12); },{passive:false});+ let drag=false,lx=0,ly=0;+ box.addEventListener('pointerdown',e=>{ if(e.target.closest('.zoomtools'))return; drag=true;lx=e.clientX;ly=e.clientY;box.classList.add('grab');box.setPointerCapture(e.pointerId); });+ box.addEventListener('pointermove',e=>{ if(!drag)return; _fpTx+=e.clientX-lx;_fpTy+=e.clientY-ly;lx=e.clientX;ly=e.clientY;_fpApply(); });+ box.addEventListener('pointerup',()=>{ drag=false;box.classList.remove('grab'); });+ }+ // ── Floaty Layers HUD ── // Layer definitions: KiCad-colored geometry is matched by color; the // Adom-computed paste overlays are matched by their group class.+ // Every layer is its own <g class="adom-layer-…"> in the self-rendered SVG,+ // in the original adom-footprint palette. const LAYER_DEFS=[- {key:'copper', name:'Copper', kind:'color', color:'#C83434', sw:'#C83434'},- {key:'assembly', name:'Assembly (F.Fab)', kind:'color', color:'#C872AB', sw:'#C872AB'},- {key:'courtyard',name:'Courtyard', kind:'color', color:'#F2EDA1', sw:'#F2EDA1', ring:true},- {key:'paste-ap', name:'Paste apertures', kind:'group', sel:'.adom-layer-paste-apertures', sw:'#8aa0b8', ring:true},+ {key:'copper', name:'Copper', kind:'group', sel:'.adom-layer-copper', sw:'#C83434'},+ {key:'padnum', name:'Pad numbers', kind:'group', sel:'.adom-layer-padnum', sw:'#ffffff', ring:true},+ {key:'silk', name:'Silkscreen', kind:'group', sel:'.adom-layer-silk', sw:'#00cccc'},+ {key:'fab', name:'Fab (F.Fab)', kind:'group', sel:'.adom-layer-fab', sw:'#6e7681', ring:true},+ {key:'courtyard', name:'Courtyard', kind:'group', sel:'.adom-layer-courtyard', sw:'#FF26E2', ring:true},+ {key:'drill', name:'Drill', kind:'group', sel:'.adom-layer-drill', sw:'#888888', ring:true},+ {key:'paste-ap', name:'Paste apertures', kind:'group', sel:'.adom-layer-paste-apertures', sw:'#8aa0b8', ring:true}, {key:'paste-dots',name:'InstaPCB paste dots', kind:'group', sel:'.adom-layer-paste-dots', sw:'#37c8a6', special:true}, ]; const LPREF='adomFpLayers';@@ -306,6 +352,23 @@ m.onclick=e=>{if(e.target===m)m.classList.remove('show')}; } + // ── AI-driving API (ai-driven-apps contract) — drive the real UI from the CLI ──+ window.ui = {+ layer(name, on){+ const def=LAYER_DEFS.find(d=>d.key===name||d.name.toLowerCase()===String(name).toLowerCase()||d.name.toLowerCase().indexOf(String(name).toLowerCase())>=0);+ if(!def) return 'no layer: '+name+' (have: '+LAYER_DEFS.map(d=>d.key).join(', ')+')';+ const want=(on!==false&&on!=='off'&&on!=='0'&&on!=='false');+ const row=[].slice.call(document.querySelectorAll('.layer-row')).find(r=>{const n=r.querySelector('.lname');return n&&n.textContent.indexOf(def.name)>=0;});+ if(row && row.classList.contains('on')!==want) row.click();+ return def.name+'='+want;+ },+ zoom(dir){ const b=document.getElementById(dir==='in'?'fpZin':dir==='out'?'fpZout':'fpFit'); if(b)b.click(); return 'zoom '+dir; },+ send(target){ const b=document.querySelector('.sbtn[data-target="'+target+'"]'); if(b){ b.click(); return 'send to '+target; } return 'no send target: '+target; },+ toast(m){ showToast(String(m)); return 'toast: '+m; },+ state(){ return window._lastState||{}; },+ list(){ return Object.keys(window.ui).filter(k=>typeof window.ui[k]==='function'); }+ };+ async function evalLoop(){ try{ const r=await fetch('eval/pending',{cache:'no-store'}); const j=await r.json();
src/fp_render.rsadded+229@@ -0,0 +1,229 @@+//! Self-render a `.kicad_mod` to a themed footprint SVG — every layer in its own+//! toggleable `<g>`, in the ORIGINAL adom-footprint palette (ported faithfully+//! from gallia `kicad-footprint-viewer.js`). No service-kicad dependency, so+//! silkscreen, drill holes and pad numbers come back and the colours are exact:+//! Courtyard #FF26E2 (magenta, dashed) · Fab #6e7681 (grey) · Silk #00cccc+//! (cyan) · Copper pads #C83434/#ef5350 (red) · Drill dark w/ #666 ring · white+//! pad numbers. Paste apertures + the InstaPCB dots layer on top of all of it.++use regex::Regex;+use std::fmt::Write as _;++#[derive(Clone)]+pub struct Pad {+ pub number: String,+ pub mount: String, // smd | thru_hole | np_thru_hole+ pub shape: String, // rect | circle | oval | roundrect | custom | trapezoid+ pub x: f64,+ pub y: f64,+ pub rot: f64,+ pub w: f64,+ pub h: f64,+ pub rratio: f64,+ pub drill: f64,+}+struct Seg { x1: f64, y1: f64, x2: f64, y2: f64 }+struct Rect { x1: f64, y1: f64, x2: f64, y2: f64 }+struct Poly { pts: Vec<(f64, f64)> }++pub struct Footprint {+ pub pads: Vec<Pad>,+ courtyard_lines: Vec<Seg>,+ courtyard_rects: Vec<Rect>,+ fab_lines: Vec<Seg>,+ fab_rects: Vec<Rect>,+ fab_polys: Vec<Poly>,+ silk_lines: Vec<Seg>,+ silk_polys: Vec<Poly>,+}++/// Balanced-paren slice of every `(<tag> …)` block.+fn blocks<'a>(s: &'a str, tag: &str) -> Vec<&'a str> {+ let bytes = s.as_bytes();+ let pat = format!("({} ", tag);+ let mut out = Vec::new();+ let mut search = 0;+ while let Some(rel) = s[search..].find(&pat) {+ let start = search + rel;+ let mut depth = 0i32;+ let mut end = start;+ for i in start..s.len() {+ match bytes[i] {+ b'(' => depth += 1,+ b')' => { depth -= 1; if depth == 0 { end = i; break; } }+ _ => {}+ }+ }+ out.push(&s[start..=end.min(s.len() - 1)]);+ search = end + 1;+ }+ out+}++fn layer_of(block: &str, re: &Regex) -> String {+ re.captures(block).map(|c| c[1].to_string()).unwrap_or_default()+}++pub fn parse(content: &str) -> Footprint {+ let at_re = Regex::new(r"\(at\s+(-?[\d.]+)\s+(-?[\d.]+)(?:\s+(-?[\d.]+))?\)").unwrap();+ let size_re = Regex::new(r"\(size\s+(-?[\d.]+)\s+(-?[\d.]+)\)").unwrap();+ let drill_re = Regex::new(r"\(drill\s+(-?[\d.]+)").unwrap();+ let rr_re = Regex::new(r"\(roundrect_rratio\s+([\d.]+)\)").unwrap();+ let layer_re = Regex::new(r##"\(layer\s+"([^"]+)"\)"##).unwrap();+ let se_re = Regex::new(r"\(start\s+(-?[\d.]+)\s+(-?[\d.]+)\)\s*\(end\s+(-?[\d.]+)\s+(-?[\d.]+)\)").unwrap();+ // Pad number may be quoted ("13") or bare (13, older KiCad) — accept both.+ let pad_head = Regex::new(r##"\(pad\s+"?([^"\s]+)"?\s+(\w+)\s+(\w+)"##).unwrap();+ let xy_re = Regex::new(r"\(xy\s+(-?[\d.]+)\s+(-?[\d.]+)\)").unwrap();++ let mut fp = Footprint {+ pads: vec![], courtyard_lines: vec![], courtyard_rects: vec![],+ fab_lines: vec![], fab_rects: vec![], fab_polys: vec![],+ silk_lines: vec![], silk_polys: vec![],+ };++ // ── Pads ──+ for b in blocks(content, "pad") {+ let head = match pad_head.captures(b) { Some(h) => h, None => continue };+ let number = head[1].to_string();+ if number.is_empty() { continue; }+ let (at, size) = match (at_re.captures(b), size_re.captures(b)) { (Some(a), Some(s)) => (a, s), _ => continue };+ fp.pads.push(Pad {+ number,+ mount: head[2].to_string(),+ shape: head[3].to_string(),+ x: at[1].parse().unwrap_or(0.0),+ y: at[2].parse().unwrap_or(0.0),+ rot: at.get(3).and_then(|m| m.as_str().parse().ok()).unwrap_or(0.0),+ w: size[1].parse().unwrap_or(0.0),+ h: size[2].parse().unwrap_or(0.0),+ rratio: rr_re.captures(b).and_then(|c| c[1].parse().ok()).unwrap_or(0.25),+ drill: drill_re.captures(b).and_then(|c| c[1].parse().ok()).unwrap_or(0.0),+ });+ }+ // ── fp_line by layer ──+ for b in blocks(content, "fp_line") {+ let cap = match se_re.captures(b) { Some(c) => c, None => continue };+ let seg = Seg { x1: cap[1].parse().unwrap_or(0.0), y1: cap[2].parse().unwrap_or(0.0), x2: cap[3].parse().unwrap_or(0.0), y2: cap[4].parse().unwrap_or(0.0) };+ match layer_of(b, &layer_re).as_str() {+ "F.CrtYd" => fp.courtyard_lines.push(seg),+ "F.Fab" => fp.fab_lines.push(seg),+ "F.SilkS" => fp.silk_lines.push(seg),+ _ => {}+ }+ }+ // ── fp_rect by layer ──+ for b in blocks(content, "fp_rect") {+ let cap = match se_re.captures(b) { Some(c) => c, None => continue };+ let r = Rect { x1: cap[1].parse().unwrap_or(0.0), y1: cap[2].parse().unwrap_or(0.0), x2: cap[3].parse().unwrap_or(0.0), y2: cap[4].parse().unwrap_or(0.0) };+ match layer_of(b, &layer_re).as_str() {+ "F.CrtYd" => fp.courtyard_rects.push(r),+ "F.Fab" => fp.fab_rects.push(r),+ _ => {}+ }+ }+ // ── fp_poly by layer ──+ for b in blocks(content, "fp_poly") {+ let pts: Vec<(f64, f64)> = xy_re.captures_iter(b).map(|c| (c[1].parse().unwrap_or(0.0), c[2].parse().unwrap_or(0.0))).collect();+ if pts.is_empty() { continue; }+ match layer_of(b, &layer_re).as_str() {+ "F.Fab" => fp.fab_polys.push(Poly { pts }),+ "F.SilkS" => fp.silk_polys.push(Poly { pts }),+ _ => {}+ }+ }+ fp+}++fn esc(s: &str) -> String { s.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """) }++/// Render the footprint to a themed SVG with per-layer `<g class="adom-layer-…">`+/// groups. `ox,oy` is unused here (geometry is in footprint-local mm, the viewBox+/// is computed from it); kept for signature parity with the paste overlay.+pub fn render(fp: &Footprint) -> (String, f64, f64) {+ // bounds over everything+ let mut pts: Vec<(f64, f64)> = vec![];+ for p in &fp.pads { pts.push((p.x - p.w / 2.0, p.y - p.h / 2.0)); pts.push((p.x + p.w / 2.0, p.y + p.h / 2.0)); }+ for s in fp.courtyard_lines.iter().chain(fp.fab_lines.iter()).chain(fp.silk_lines.iter()) { pts.push((s.x1, s.y1)); pts.push((s.x2, s.y2)); }+ for r in fp.courtyard_rects.iter().chain(fp.fab_rects.iter()) { pts.push((r.x1, r.y1)); pts.push((r.x2, r.y2)); }+ for poly in fp.fab_polys.iter().chain(fp.silk_polys.iter()) { for &pt in &poly.pts { pts.push(pt); } }+ if pts.is_empty() { pts.push((-5.0, -5.0)); pts.push((5.0, 5.0)); }+ let margin = 1.0;+ let min_x = pts.iter().map(|p| p.0).fold(f64::INFINITY, f64::min) - margin;+ let min_y = pts.iter().map(|p| p.1).fold(f64::INFINITY, f64::min) - margin;+ let max_x = pts.iter().map(|p| p.0).fold(f64::NEG_INFINITY, f64::max) + margin;+ let max_y = pts.iter().map(|p| p.1).fold(f64::NEG_INFINITY, f64::max) + margin;+ let (w, h) = (max_x - min_x, max_y - min_y);++ let mut s = String::new();+ let _ = write!(s, r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{:.3} {:.3} {:.3} {:.3}" preserveAspectRatio="xMidYMid meet">"##, min_x, min_y, w, h);++ // ── Courtyard (magenta, dashed) ──+ s.push_str(r##"<g class="adom-layer-courtyard">"##);+ for l in &fp.courtyard_lines { let _ = write!(s, r##"<line x1="{:.3}" y1="{:.3}" x2="{:.3}" y2="{:.3}" stroke="#FF26E2" stroke-width="0.05" stroke-dasharray="0.2,0.1"/>"##, l.x1, l.y1, l.x2, l.y2); }+ for r in &fp.courtyard_rects { let _ = write!(s, r##"<rect x="{:.3}" y="{:.3}" width="{:.3}" height="{:.3}" fill="none" stroke="#FF26E2" stroke-width="0.05" stroke-dasharray="0.2,0.1"/>"##, r.x1.min(r.x2), r.y1.min(r.y2), (r.x2 - r.x1).abs(), (r.y2 - r.y1).abs()); }+ s.push_str("</g>");++ // ── Fab outline (grey) ──+ s.push_str(r##"<g class="adom-layer-fab">"##);+ for l in &fp.fab_lines { let _ = write!(s, r##"<line x1="{:.3}" y1="{:.3}" x2="{:.3}" y2="{:.3}" stroke="#6e7681" stroke-width="0.1"/>"##, l.x1, l.y1, l.x2, l.y2); }+ for r in &fp.fab_rects { let _ = write!(s, r##"<rect x="{:.3}" y="{:.3}" width="{:.3}" height="{:.3}" fill="none" stroke="#6e7681" stroke-width="0.1"/>"##, r.x1.min(r.x2), r.y1.min(r.y2), (r.x2 - r.x1).abs(), (r.y2 - r.y1).abs()); }+ for poly in &fp.fab_polys { let d = poly_d(&poly.pts); let _ = write!(s, r##"<path d="{}" fill="none" stroke="#6e7681" stroke-width="0.1"/>"##, d); }+ s.push_str("</g>");++ // ── Copper pads (red) ──+ s.push_str(r##"<g class="adom-layer-copper">"##);+ for p in &fp.pads {+ let (hw, hh) = (p.w / 2.0, p.h / 2.0);+ let body = match p.shape.as_str() {+ "circle" => format!(r##"<circle cx="{:.3}" cy="{:.3}" r="{:.3}" fill="#C83434" stroke="#ef5350" stroke-width="0.04"/>"##, p.x, p.y, hw.min(hh)),+ "oval" => format!(r##"<rect x="{:.3}" y="{:.3}" width="{:.3}" height="{:.3}" rx="{:.3}" fill="#C83434" stroke="#ef5350" stroke-width="0.04"/>"##, p.x - hw, p.y - hh, p.w, p.h, hw.min(hh)),+ "roundrect" => format!(r##"<rect x="{:.3}" y="{:.3}" width="{:.3}" height="{:.3}" rx="{:.3}" fill="#C83434" stroke="#ef5350" stroke-width="0.04"/>"##, p.x - hw, p.y - hh, p.w, p.h, p.rratio * hw.min(hh) * 2.0),+ _ => format!(r##"<rect x="{:.3}" y="{:.3}" width="{:.3}" height="{:.3}" fill="#C83434" stroke="#ef5350" stroke-width="0.04"/>"##, p.x - hw, p.y - hh, p.w, p.h),+ };+ if p.rot.abs() > 0.01 {+ let _ = write!(s, r##"<g transform="rotate({:.3} {:.3} {:.3})" data-pad="{}" data-shape="{}" data-size="{:.3}x{:.3}" data-mount="{}">{}</g>"##, -p.rot, p.x, p.y, esc(&p.number), esc(&p.shape), p.w, p.h, esc(&p.mount), body);+ } else {+ let _ = write!(s, r##"<g data-pad="{}" data-shape="{}" data-size="{:.3}x{:.3}" data-mount="{}">{}</g>"##, esc(&p.number), esc(&p.shape), p.w, p.h, esc(&p.mount), body);+ }+ }+ s.push_str("</g>");++ // ── Drill holes (dark with grey ring) ──+ s.push_str(r##"<g class="adom-layer-drill">"##);+ for p in &fp.pads {+ if p.mount == "thru_hole" || p.mount == "np_thru_hole" {+ let r = if p.drill > 0.0 { p.drill / 2.0 } else { (p.w / 2.0).min(p.h / 2.0) * 0.5 };+ let _ = write!(s, r##"<circle cx="{:.3}" cy="{:.3}" r="{:.3}" fill="#0d1117" stroke="#666" stroke-width="0.05"/>"##, p.x, p.y, r);+ }+ }+ s.push_str("</g>");++ // ── Silkscreen (cyan) ──+ s.push_str(r##"<g class="adom-layer-silk">"##);+ for l in &fp.silk_lines { let _ = write!(s, r##"<line x1="{:.3}" y1="{:.3}" x2="{:.3}" y2="{:.3}" stroke="#00cccc" stroke-width="0.12" stroke-linecap="round"/>"##, l.x1, l.y1, l.x2, l.y2); }+ for poly in &fp.silk_polys { let d = poly_d(&poly.pts); let _ = write!(s, r##"<path d="{}" fill="#00cccc" stroke="#00cccc" stroke-width="0.1"/>"##, d); }+ s.push_str("</g>");++ // ── Pad numbers (white) ──+ s.push_str(r##"<g class="adom-layer-padnum">"##);+ for p in &fp.pads {+ let fs = p.w.min(p.h) * 0.42;+ if fs > 0.12 {+ let _ = write!(s, r##"<text x="{:.3}" y="{:.3}" text-anchor="middle" dominant-baseline="central" font-size="{:.3}" fill="#fff" font-family="sans-serif" style="pointer-events:none">{}</text>"##, p.x, p.y, fs, esc(&p.number));+ }+ }+ s.push_str("</g>");++ s.push_str("</svg>");+ // origin for the paste overlay: footprint-local (ax,ay) maps to (ax,ay) here+ (s, 0.0, 0.0)+}++fn poly_d(pts: &[(f64, f64)]) -> String {+ let mut d = String::new();+ for (i, &(x, y)) in pts.iter().enumerate() {+ let _ = write!(d, "{}{:.3},{:.3} ", if i == 0 { "M" } else { "L" }, x, y);+ }+ d.push('Z');+ d+}
src/main.rs+42−1@@ -2,9 +2,11 @@ //! No Node, no gallia. Generation is native Rust (`fp_gen`); SVG rendering goes //! through service-kicad (`render`); `serve` runs the AI-drivable live app. +mod provenance; mod deliver; mod fp_gen; mod paste;+mod fp_render; mod render; mod server; mod theme;@@ -63,6 +65,17 @@ enum Cmd { #[arg(long)] out: Option<PathBuf>, },+ /// Drive the RUNNING live app's UI from the CLI (ai-driven-apps contract).+ /// Examples: `ui layer silk off` · `ui layer paste-dots on` · `ui zoom fit` ·+ /// `ui send kicad` · `ui toast "hi"` · `ui state` · `ui list`.+ Ui {+ /// Action: layer | zoom | send | toast | state | list+ action: String,+ /// Arguments (e.g. `layer silk off`)+ args: Vec<String>,+ #[arg(long, default_value_t = 8783)]+ port: u16,+ }, /// Run the live app server (AI-drivable: /state, /eval, /console). Serve { #[arg(long, default_value_t = 8782)]@@ -72,6 +85,32 @@ enum Cmd { }, } +/// Drive the running app's `window.ui` API over the /eval channel.+fn cmd_ui(action: String, args: Vec<String>, port: u16) -> Result<()> {+ let arglist: Vec<String> = args.iter().map(|a| serde_json::to_string(a).unwrap_or_else(|_| format!("{a:?}"))).collect();+ let code = format!("(window.ui && window.ui.{action}) ? window.ui.{action}({}) : 'window.ui not ready — is a browser tab open?'", arglist.join(","));+ let base = format!("http://127.0.0.1:{port}");+ let client = reqwest::blocking::Client::builder().timeout(std::time::Duration::from_secs(10)).build()?;+ let resp = client.post(format!("{base}/eval")).json(&serde_json::json!({ "code": code })).send()+ .with_context(|| format!("POST {base}/eval — is the app running? (adom-footprint serve --port {port})"))?;+ let id = resp.json::<serde_json::Value>()?.get("id").and_then(|v| v.as_str()).map(String::from).ok_or_else(|| anyhow!("no eval id"))?;+ for _ in 0..60 {+ std::thread::sleep(std::time::Duration::from_millis(120));+ let v: serde_json::Value = client.get(format!("{base}/eval/{id}")).send()?.json().unwrap_or(serde_json::Value::Null);+ if v.get("done").and_then(|d| d.as_bool()) == Some(true) {+ let inner = v.get("result").cloned().unwrap_or(serde_json::Value::Null);+ if let Some(err) = inner.get("error") { if !err.is_null() { return Err(anyhow!("UI error: {}", err.as_str().unwrap_or(""))); } }+ match inner.get("result") {+ Some(r) if r.is_string() => println!("{}", r.as_str().unwrap_or("")),+ Some(r) if !r.is_null() => println!("{}", serde_json::to_string_pretty(r).unwrap_or_default()),+ _ => println!("ok"),+ }+ return Ok(());+ }+ }+ Err(anyhow!("timed out — open a browser tab on http://127.0.0.1:{port}/ so the UI can run the command"))+}+ #[derive(serde::Deserialize)] struct CreateInput { #[serde(rename = "footprintName")]@@ -120,7 +159,8 @@ fn main() { fn cmd_embed(file: PathBuf, out: Option<PathBuf>) -> Result<()> { let content = fs::read_to_string(&file).with_context(|| format!("reading {}", file.display()))?; let name = first_footprint_name(&content).unwrap_or_else(|| "footprint".to_string());- let svg = render::render_footprint_svg(&content, bbox_from_mod(&content))?;+ // The embed IS the InstaPCB layer panel — paste overlay belongs here.+ let svg = render::render_footprint_svg_with_paste(&content, bbox_from_mod(&content))?; let aps = paste::paste_apertures(&content); let dot_count = paste::paste_overlay_svg(&aps, 0.0, 0.0).2; let meta = serde_json::json!({ "pasteApertures": aps.len(), "pasteDots": dot_count });@@ -149,6 +189,7 @@ fn run() -> Result<()> { } Cmd::RenderDs2sf { file, out } => cmd_render_ds2sf(file, out), Cmd::Embed { file, out } => cmd_embed(file, out),+ Cmd::Ui { action, args, port } => cmd_ui(action, args, port), Cmd::Preview { file, out } => cmd_preview(file, out), Cmd::Serve { port, file } => { if let Some(f) = file {
src/provenance.rsadded+73@@ -0,0 +1,73 @@+//! Canonical source-provenance LABELING — the single rule for how every Adom+//! EDA app names where a symbol / footprint / 3D / datasheet actually came from.+//!+//! THE RULE (John, 2026-06-23): label by the DISCOVERY SOURCE — where the file+//! was actually downloaded — NOT by content_origin (who authored the model).+//!+//! - ONLY the maker's OWN website counts as "the manufacturer", and then we+//! NAME the site: "Texas Instruments (ti.com)", "Analog Devices (analog.com)".+//! - The moment the manufacturer hands you off to a 3rd-party library —+//! UltraLibrarian, SnapMagic/SnapEDA, Component Search Engine — that+//! AGGREGATOR is the source. NEVER label it "Manufacturer".+//! - A fuzzy bare "Manufacturer" is misleading and banned.+//!+//! This module is mirrored (same mapping) in adom-symbol, adom-footprint and+//! adom-lbr so the label is identical everywhere. See the `chip-provenance-labeling` skill.++/// The maker's canonical website domain, for "Texas Instruments (ti.com)" labels.+/// Matched case-insensitively on a substring of the manufacturer name.+pub fn mfr_domain(manufacturer: &str) -> Option<&'static str> {+ let m = manufacturer.to_lowercase();+ let table = [+ ("texas instr", "ti.com"), ("analog dev", "analog.com"),+ ("stmicro", "st.com"), ("nxp", "nxp.com"), ("microchip", "microchip.com"),+ ("infineon", "infineon.com"), ("on semi", "onsemi.com"), ("onsemi", "onsemi.com"),+ ("nordic", "nordicsemi.com"), ("espressif", "espressif.com"),+ ("renesas", "renesas.com"), ("diodes inc", "diodes.com"), ("vishay", "vishay.com"),+ ("bosch", "bosch-sensortec.com"), ("maxim", "analog.com"), ("dialog", "renesas.com"),+ ("rohm", "rohm.com"), ("toshiba", "toshiba.com"), ("skyworks", "skyworksinc.com"),+ ("qorvo", "qorvo.com"), ("micron", "micron.com"), ("cypress", "infineon.com"),+ ("silicon lab", "silabs.com"), ("silabs", "silabs.com"), ("lattice", "latticesemi.com"),+ ("winbond", "winbond.com"), ("ti ", "ti.com"),+ ];+ table.iter().find(|(k, _)| m.contains(k)).map(|(_, v)| *v)+}++/// The human label for a source, given the discovery-source slug + the+/// manufacturer name. THIS is the function every app calls.+pub fn source_label(discovery: &str, manufacturer: &str) -> String {+ match discovery.to_lowercase().as_str() {+ // Manufacturer-DIRECT: only when downloaded from the maker's own site.+ "manufacturer" | "mfr" | "vendor" => {+ if let Some(dom) = mfr_domain(manufacturer) {+ if manufacturer.is_empty() { format!("Manufacturer · {dom}") }+ else { format!("{manufacturer} ({dom})") }+ } else if !manufacturer.is_empty() {+ format!("{manufacturer} (direct)")+ } else {+ "Manufacturer (direct site)".to_string()+ }+ }+ // 3rd-party aggregators — NEVER "the manufacturer".+ "cse" | "componentsearch" | "cse_ul" | "samacsys" => "Component Search Engine".into(),+ "snapeda" | "snapmagic" => "SnapEDA".into(),+ "ultralibrarian" | "ul" => "Ultra Librarian".into(),+ "mouser" => "Mouser".into(),+ "digikey" => "DigiKey".into(),+ "arrow" => "Arrow".into(),+ "lcsc" => "LCSC".into(),+ "kicad_community" => "KiCad community".into(),+ other => other.to_string(),+ }+}++/// A short note for the "the manufacturer linked you to it, but it's a 3rd+/// party" middle-ground — appended in tooltips so the user knows the maker+/// pointed here even though the file came off an aggregator.+pub fn linked_from_mfr_note(manufacturer: &str) -> String {+ if manufacturer.is_empty() {+ "the manufacturer's page linked to this 3rd-party library".to_string()+ } else {+ format!("{manufacturer}'s own page links here, but the file is hosted by this 3rd-party library — not downloaded from {manufacturer} directly")+ }+}
src/render.rs+24−52@@ -21,64 +21,36 @@ const OY: f64 = 105.0; const MARGIN: f64 = 1.0; /// Render the footprint to a themed SVG, cropped to `bbox` (footprint-local mm).-pub fn render_footprint_svg(kicad_mod: &str, bbox: (f64, f64, f64, f64)) -> Result<String> {- let pcb = build_pcb_wrap(kicad_mod);- let url = format!("{}/kicad/pcb/export/svg?side=top", service_url());- let resp = reqwest::blocking::Client::builder()- .timeout(std::time::Duration::from_secs(60))- .build()?- .post(&url)- .header("Content-Type", "application/octet-stream")- .body(pcb)- .send()- .with_context(|| format!("POST {url}"))?;- if !resp.status().is_success() {- return Err(anyhow!("service-kicad {} — {}", resp.status(), resp.text().unwrap_or_default()));- }- let svg = resp.text()?;+/// CLEAN by default — NO InstaPCB paste-dot overlay. John's standing rule: the+/// default footprint render (cards, thumbnails, preview, the live app's initial+/// view) must show the bare footprint; the InstaPCB paste layer is opt-in. Use+/// `render_footprint_svg_with_paste` only where InstaPCB is the point (`embed`).+pub fn render_footprint_svg(kicad_mod: &str, _bbox: (f64, f64, f64, f64)) -> Result<String> {+ let normalized = normalize_legacy(kicad_mod);+ let fp = crate::fp_render::parse(&normalized);+ let (svg, _ox, _oy) = crate::fp_render::render(&fp);+ Ok(svg)+} - // Crop to the footprint by scanning the ACTUAL rendered geometry rather than- // assuming where KiCad placed it. Old-format mods (`(layer F.Cu)` unquoted,- // `tedit`) don't honor our injected `(at)`, so a placement-based viewBox- // misses the part entirely. Geometry-based cropping is format- and- // placement-agnostic. Fall back to the mod-local bbox if the scan finds- // nothing (e.g. an all-text footprint).- let (vbx, vby, vbw, vbh) = match svg_geometry_bbox(&svg, OX, OY) {- Some((minx, miny, maxx, maxy)) => (- minx - MARGIN,- miny - MARGIN,- (maxx - minx) + 2.0 * MARGIN,- (maxy - miny) + 2.0 * MARGIN,- ),- None => {- let (minx, miny, maxx, maxy) = bbox;- (OX + minx - MARGIN, OY + miny - MARGIN, (maxx - minx) + 2.0 * MARGIN, (maxy - miny) + 2.0 * MARGIN)- }- };- let re = Regex::new(r#"width="[^"]*mm" height="[^"]*mm" viewBox="[^"]*""#).unwrap();- let replacement = format!(- "width=\"{vbw:.3}mm\" height=\"{vbh:.3}mm\" viewBox=\"{vbx:.4} {vby:.4} {vbw:.4} {vbh:.4}\""- );- let svg = re.replace(&svg, replacement.as_str()).into_owned();- let themed = crate::theme::theme_footprint_svg(&svg);-- // Overlay the InstaPCB solder-paste dispensing positions (Adom-computed,- // page coords matching the placed footprint) as two toggleable layer groups.- let aps = crate::paste::paste_apertures(kicad_mod);- let (apert_g, dots_g, _n) = crate::paste::paste_overlay_svg(&aps, OX, OY);- let themed = if let Some(idx) = themed.rfind("</svg>") {- let mut s = String::with_capacity(themed.len() + apert_g.len() + dots_g.len() + 2);- s.push_str(&themed[..idx]);+/// Same render, PLUS the InstaPCB paste apertures + 300 µm jetted-dot overlay.+/// Only the `embed` flow (the interactive InstaPCB layer panel) uses this.+pub fn render_footprint_svg_with_paste(kicad_mod: &str, _bbox: (f64, f64, f64, f64)) -> Result<String> {+ let normalized = normalize_legacy(kicad_mod);+ let fp = crate::fp_render::parse(&normalized);+ let (svg, ox, oy) = crate::fp_render::render(&fp);+ let aps = crate::paste::paste_apertures(&normalized);+ let (apert_g, dots_g, _n) = crate::paste::paste_overlay_svg(&aps, ox, oy);+ let out = if let Some(idx) = svg.rfind("</svg>") {+ let mut s = String::with_capacity(svg.len() + apert_g.len() + dots_g.len());+ s.push_str(&svg[..idx]); s.push_str(&apert_g);- s.push('\n'); s.push_str(&dots_g);- s.push('\n');- s.push_str(&themed[idx..]);+ s.push_str(&svg[idx..]); s } else {- themed+ svg };- Ok(themed)+ Ok(out) } /// Bounding box of the real drawn geometry in a service-kicad PCB SVG, in the
src/server.rs+4−1@@ -124,7 +124,10 @@ fn route(method: &Method, url: &str, body: &str) -> Response<std::io::Cursor<Vec .map(String::from) .or_else(|| crate::first_footprint_name(km)) .unwrap_or_else(|| "footprint".to_string());- match crate::render::render_footprint_svg(km, crate::bbox_from_mod(km)) {+ // Bake the paste group so the live Layers panel's InstaPCB+ // toggle can show/hide it — but the client defaults it OFF+ // (app.html: paste-dots is opt-in), so the initial view is clean.+ match crate::render::render_footprint_svg_with_paste(km, crate::bbox_from_mod(km)) { Ok(svg) => set_state(AppState { footprint_name: name, svg,
tests/golden/R_0402.kicad_modadded+24@@ -0,0 +1,24 @@+(footprint "R_0402_1005Metric"+ (version 20240108)+ (generator "adom-footprint")+ (layer "F.Cu")+ (descr "0402 (1005 metric) resistor")+ (tags "resistor 0402")+ (attr smd)+ (fp_rect (start -0.93 -0.47) (end 0.93 0.47)+ (stroke (width 0.05) (type default)) (fill none) (layer "F.CrtYd"))+ (fp_rect (start -0.5 -0.27) (end 0.5 0.27)+ (stroke (width 0.1) (type default)) (fill none) (layer "F.Fab"))+ (fp_rect (start -0.62 -0.39) (end 0.62 0.39)+ (stroke (width 0.12) (type default)) (fill none) (layer "F.SilkS"))+ (fp_line (start -0.8 -0.5700000000000001) (end -0.2 -0.5700000000000001)+ (stroke (width 0.12) (type default)) (layer "F.SilkS"))+ (fp_text reference "REF**" (at 0 -1.27)+ (layer "F.SilkS") (effects (font (size 1 1) (thickness 0.15))))+ (fp_text value "R_0402_1005Metric" (at 0 1.27)+ (layer "F.Fab") (effects (font (size 1 1) (thickness 0.15))))+ (pad "1" smd roundrect (at -0.485 0) (size 0.6 0.7)+ (layers "F.Cu" "F.Paste" "F.Mask") (roundrect_rratio 0.25))+ (pad "2" smd roundrect (at 0.485 0) (size 0.6 0.7)+ (layers "F.Cu" "F.Paste" "F.Mask") (roundrect_rratio 0.25))+)
uninstall.sh+5−1@@ -1,3 +1,7 @@ #!/bin/bash+# uninstall.sh for adom-footprint (app). set -euo pipefail-rm -f "${HOME}/.local/bin/adom-footprint"; echo "Uninstalled adom-footprint"++rm -f "${HOME}/.local/bin/adom-footprint"+echo "Uninstalled adom-footprint"+