master
John Lauer 1.0.54: 1.0.54. Wiki #119: back-side footprints written by other tools are read the way KiCad reads them. A footprint o… 4f34c0c 1mo ago
use super::*;
use crate::silk_graphics as graphics;

pub fn silk_graphics_batch(ctx: &Ctx, args: &Value) -> RResult<Value> {
    if args["expectedRevision"]
        .as_str()
        .filter(|s| !s.is_empty())
        .is_none()
    {
        return Err(RoutingError::new(
            "revision_required",
            "Read the board revision before editing silk graphics",
        ));
    }
    let s = connect(ctx, args)?;
    let (text, _, revision) = s.snapshot()?;
    check_revision(args, &revision)?;
    let plan = graphics::prepare(&text, args)?;
    let before = drc_snapshot(ctx.kicad_cli.as_deref(), &s.source_path(), &text)?;
    let after = drc_snapshot(ctx.kicad_cli.as_deref(), &s.source_path(), &plan.candidate)?;
    let all = |mut v: Value| {
        if let Some(rows) = v["violations"].as_array_mut() {
            for row in rows {
                row["severity"] = json!("error");
            }
        }
        v
    };
    let added = new_errors(&all(before), &all(after.clone()))?;
    if !added.is_empty() {
        return Err(RoutingError::new(
            "drc_rejected",
            "Silk graphics add native DRC findings; no live edit",
        )
        .with("_hint", json!(crate::ipc::silk_drc_hint(&added)))
        .with("violations", json!(added))
        .with("mutated", json!(false)));
    }
    if args["dryRun"] == true {
        return Ok(json!({"success":true,"mutated":false,"drc":after,"revision":revision}));
    }
    let encode = |g: &graphics::Graphic| prost_types::Any {
        type_url: graphics::TYPE_URL.into(),
        value: g.bytes(BoardLayerInfo::id_from_name(&g.layer).unwrap()),
    };
    let created: Vec<_> = plan.create.iter().map(|g| g.id.clone()).collect();
    let updated: Vec<_> = plan.update.iter().map(|g| g.id.clone()).collect();
    let verify = |rows: Vec<prost_types::Any>, wanted: &[String]| -> Result<(), IpcFailure> {
        let actual: HashSet<_> = rows.iter().filter_map(|a| graphics::id(&a.value)).collect();
        if actual != wanted.iter().cloned().collect() {
            return Err(IpcFailure::from(
                "Native graphics reply did not preserve requested UUIDs".to_string(),
            ));
        }
        Ok(())
    };
    check_revision(args, &s.revision()?)?;
    transaction(&s, "Adom silkscreen graphics", || {
        if !plan.remove.is_empty() {
            s.client.delete_items(plan.remove.clone())?;
        }
        if !plan.update.is_empty() {
            verify(
                s.client
                    .update_items(plan.update.iter().map(&encode).collect())?,
                &updated,
            )?;
        }
        if !plan.create.is_empty() {
            verify(
                s.client
                    .create_items(plan.create.iter().map(&encode).collect(), None)?,
                &created,
            )?;
        }
        Ok(())
    })?;
    let mut out = json!({"success":true,"committed":true,"mutated":true,"undoSteps":1,"createdIds":created,"updatedIds":updated,"removedIds":plan.remove,"drc":after,"saved":false,"source":"live-editor","_hint":"Inspect every postCommit error before retrying. Refresh the linked 3D viewer after edits. Coordinates are board-frame on either side, not mirrored screen coordinates."});
    match s
        .client
        .get_items_raw_by_type_codes(vec![PcbObjectTypeCode::new_shape().code])
    {
        Ok(rows) => {
            let live: HashSet<_> = rows.iter().filter_map(|a| graphics::id(&a.value)).collect();
            let remaining: Vec<_> = plan.remove.iter().filter(|id| live.contains(*id)).collect();
            let missing: Vec<_> = created
                .iter()
                .chain(&updated)
                .filter(|id| !live.contains(*id))
                .collect();
            out["deletionVerified"] = json!(remaining.is_empty());
            if !remaining.is_empty() || !missing.is_empty() {
                out["success"] = json!(false);
                out["postCommitReadbackError"] =
                    json!({"remainingRemovedIds":remaining,"missingLiveIds":missing});
            }
        }
        Err(e) => {
            out["success"] = json!(false);
            out["postCommitReadbackError"] = json!(e.to_string());
        }
    }
    match s.revision() {
        Ok(rev) => out["revision"] = json!(rev),
        Err(e) => {
            out["success"] = json!(false);
            out["postCommitRevisionError"] = json!(e.message);
        }
    }
    let ids = created.iter().chain(&updated).cloned().collect::<Vec<_>>();
    if !ids.is_empty() {
        match s.client.get_item_bounding_boxes(ids,false){Ok(rows)=>out["bounds"]=json!(rows.iter().map(|b|json!({"id":b.item_id,"boxMm":[b.x_nm as f64/1e6,b.y_nm as f64/1e6,(b.x_nm+b.width_nm) as f64/1e6,(b.y_nm+b.height_nm) as f64/1e6]})).collect::<Vec<_>>()),Err(e)=>{out["success"]=json!(false);out["postCommitBoundsError"]=json!(e.to_string());}}
    }
    if args["save"] == true {
        match s.client.save_document() {
            Ok(()) => out["saved"] = json!(true),
            Err(e) => {
                out["success"] = json!(false);
                out["postCommitSaveError"] = json!(e.to_string());
            }
        }
    }
    Ok(out)
}