app
KiCad - the KiCad Bridge
Public Made by Adomby adom
Drive your own KiCad from an AI, in the background: open every editor, read the whole board as data (the EDA Bridge Standard board model), stack-up, part heights, netlists, place, route, pour, DRC and exports.
master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
//! Standalone silkscreen graphics, with stable UUIDs and matching DRC/IPC geometry.
use crate::pcb::RoutingError;
use crate::placement::{distance_bytes, parse_fields, vector2_bytes, wire_message, wire_varint};
use crate::schematic::{new_uuid, parse_node, Document, Node};
use serde_json::{json, Value};
use std::collections::HashSet;
pub const TYPE_URL: &str = "type.googleapis.com/kiapi.board.types.BoardGraphicShape";
fn fail(s: impl Into<String>) -> RoutingError {
RoutingError::new("silk_graphics_refused", s).with("mutated", json!(false))
}
#[derive(Clone, Debug)]
pub struct Graphic {
pub id: String,
pub layer: String,
pub kind: String,
pub points: Vec<(f64, f64)>,
pub stroke: f64,
}
impl Graphic {
fn parse(v: &Value, id: String) -> Result<Self, RoutingError> {
let o = v
.as_object()
.ok_or_else(|| fail("graphic must be an object"))?;
if o.keys()
.any(|k| !["id", "layer", "kind", "points", "stroke"].contains(&k.as_str()))
{
return Err(fail("unsupported graphic property"));
}
let layer = v["layer"]
.as_str()
.filter(|s| matches!(*s, "F.SilkS" | "B.SilkS"))
.ok_or_else(|| fail("silkscreen layer required"))?;
let kind = v["kind"].as_str().ok_or_else(|| fail("kind required"))?;
let count = match kind {
"line" | "rect" => 2,
"arc" => 3,
"bezier" => 4,
_ => return Err(fail("kind must be line, rect, arc or bezier")),
};
let input = v["points"]
.as_array()
.filter(|a| a.len() == count)
.ok_or_else(|| fail("wrong point count"))?;
let mut points = Vec::new();
for p in input {
let a = p
.as_array()
.filter(|a| a.len() == 2)
.ok_or_else(|| fail("point requires x,y"))?;
let coord = |v: &Value| {
v.as_f64()
.filter(|n| n.is_finite() && n.abs() <= 10000.)
.ok_or_else(|| fail("invalid coordinate"))
};
points.push((coord(&a[0])?, coord(&a[1])?));
}
let stroke = v["stroke"]
.as_f64()
.filter(|n| n.is_finite() && *n > 0. && *n <= 5.)
.ok_or_else(|| fail("stroke must be positive mm, at most 5"))?;
if points.first() == points.last() {
return Err(fail("degenerate geometry"));
}
if kind == "rect" && (points[0].0 == points[1].0 || points[0].1 == points[1].1) {
return Err(fail("degenerate rectangle"));
}
if kind == "arc" {
let (a, b, c) = (points[0], points[1], points[2]);
if ((b.0 - a.0) * (c.1 - a.1) - (b.1 - a.1) * (c.0 - a.0)).abs() < 1e-12 {
return Err(fail("collinear arc"));
}
}
Ok(Self {
id,
layer: layer.into(),
kind: kind.into(),
points,
stroke,
})
}
pub fn node(&self) -> Node {
let point = |tag: &str, p: (f64, f64)| format!("({tag} {} {})", p.0, p.1);
let geometry = match self.kind.as_str() {
"bezier" => format!(
"(pts {})",
self.points
.iter()
.map(|p| point("xy", *p))
.collect::<Vec<_>>()
.join(" ")
),
"arc" => format!(
"{} {} {}",
point("start", self.points[0]),
point("mid", self.points[1]),
point("end", self.points[2])
),
_ => format!(
"{} {}",
point("start", self.points[0]),
point("end", self.points[1])
),
};
let kind = if self.kind == "bezier" {
"curve"
} else {
&self.kind
};
parse_node(&format!(
"(gr_{kind} {geometry} (stroke (width {}) (type solid)) {} (layer {}) (uuid {}))",
self.stroke,
if self.kind == "rect" {
"(fill none)"
} else {
""
},
serde_json::to_string(&self.layer).unwrap(),
serde_json::to_string(&self.id).unwrap()
))
.unwrap()
}
pub fn bytes(&self, layer: i32) -> Vec<u8> {
let mut stroke = Vec::new();
wire_message(
&mut stroke,
1,
&distance_bytes((self.stroke * 1e6).round() as i64),
);
wire_varint(&mut stroke, 2, 2);
let mut fill = Vec::new();
wire_varint(&mut fill, 1, 1);
let mut attrs = Vec::new();
wire_message(&mut attrs, 1, &stroke);
wire_message(&mut attrs, 2, &fill);
let mut geometry = Vec::new();
for (i, p) in self.points.iter().enumerate() {
wire_message(
&mut geometry,
(i + 1) as u32,
&vector2_bytes((p.0 * 1e6).round() as i64, (p.1 * 1e6).round() as i64),
);
}
let mut shape = Vec::new();
wire_message(&mut shape, 3, &attrs);
wire_message(
&mut shape,
match self.kind.as_str() {
"line" => 4,
"rect" => 5,
"arc" => 6,
_ => 9,
},
&geometry,
);
let mut id = Vec::new();
wire_message(&mut id, 1, self.id.as_bytes());
let mut out = Vec::new();
wire_message(&mut out, 1, &shape);
wire_varint(&mut out, 2, layer as u64);
wire_message(&mut out, 4, &id);
wire_varint(&mut out, 5, 1);
out
}
}
pub fn id(bytes: &[u8]) -> Option<String> {
let fields = parse_fields(bytes).ok()?;
let id = fields.iter().find(|f| f.number == 4)?;
let nested = parse_fields(id.data).ok()?;
String::from_utf8(nested.iter().find(|f| f.number == 1)?.data.to_vec()).ok()
}
pub struct Plan {
pub candidate: String,
pub create: Vec<Graphic>,
pub update: Vec<Graphic>,
pub remove: Vec<String>,
}
pub fn prepare(text: &str, args: &Value) -> Result<Plan, RoutingError> {
let mut doc = Document::parse(text).map_err(fail)?;
let mut touched = HashSet::new();
let editable = |id: &str| -> Result<(), RoutingError> {
let n = doc
.root
.children()
.iter()
.find(|n| n.child_value("uuid").as_deref() == Some(id))
.ok_or_else(|| fail("unknown standalone graphic ID"))?;
if !matches!(
n.head(),
Some("gr_line" | "gr_rect" | "gr_arc" | "gr_curve")
) || !matches!(
n.child_value("layer").as_deref(),
Some("F.SilkS" | "B.SilkS")
) || n
.child("locked")
.map(|c| c.arg(0).as_deref() != Some("no"))
.unwrap_or(false)
{
return Err(fail(
"ID is locked or is not a supported standalone silkscreen graphic",
));
}
Ok(())
};
let rows = |key: &str| -> Result<Vec<Value>, RoutingError> {
match args.get(key) {
None => Ok(vec![]),
Some(v) => v
.as_array()
.filter(|a| a.len() <= 2000)
.cloned()
.ok_or_else(|| fail("batch must be an array of at most 2000 items")),
}
};
let mut create = Vec::new();
let mut update = Vec::new();
let mut remove = Vec::new();
for v in rows("create")? {
if v.get("id").is_some() {
return Err(fail("create IDs are bridge-assigned"));
}
create.push(Graphic::parse(&v, new_uuid())?);
}
for v in rows("update")? {
let id = v["id"].as_str().ok_or_else(|| fail("update needs id"))?;
editable(id)?;
if !touched.insert(id.to_string()) {
return Err(fail("duplicate ID"));
}
update.push(Graphic::parse(&v, id.into())?);
}
for v in rows("remove")? {
let id = v.as_str().ok_or_else(|| fail("remove needs IDs"))?;
editable(id)?;
if !touched.insert(id.to_string()) {
return Err(fail("duplicate ID"));
}
remove.push(id.into());
}
if create.is_empty() && update.is_empty() && remove.is_empty() {
return Err(fail("empty batch"));
}
let children = doc.root.children_mut().unwrap();
children.retain(|n| {
!n.child_value("uuid")
.map(|id| touched.contains(&id))
.unwrap_or(false)
});
children.extend(create.iter().chain(&update).map(Graphic::node));
Ok(Plan {
candidate: doc.to_text(),
create,
update,
remove,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn curves_and_borders_preserve_ids_and_coordinates() {
for (kind, points) in [
("line", json!([[-2, 3], [4, 5]])),
("rect", json!([[-2, 3], [4, 5]])),
("arc", json!([[-2, 3], [0, 5], [4, 5]])),
("bezier", json!([[-2, 3], [0, 5], [3, 6], [4, 5]])),
] {
let g = Graphic::parse(
&json!({"kind":kind,"layer":"B.SilkS","points":points,"stroke":0.1}),
"same-id".into(),
)
.unwrap();
assert_eq!(id(&g.bytes(38)).as_deref(), Some("same-id"));
assert_eq!(g.node().child_value("uuid").as_deref(), Some("same-id"));
assert!(g.node().to_text().contains("-2 3"));
}
}
#[test]
fn refuses_foreign_locked_and_degenerate_shapes() {
let text="(kicad_pcb (gr_line (uuid a) (layer F.Cu)) (gr_rect (uuid b) (layer F.SilkS) (locked yes)))";
for id in ["a", "b", "unknown"] {
assert!(prepare(text, &json!({"remove":[id]})).is_err());
}
assert!(Graphic::parse(
&json!({"kind":"arc","layer":"F.SilkS","points":[[0,0],[1,1],[2,2]],"stroke":0.1}),
"x".into()
)
.is_err());
}
}