diff --git a/crates/konnect-core/src/tools/sch_components.rs b/crates/konnect-core/src/tools/sch_components.rs index cc3dd2c5..9cda9946 100644 --- a/crates/konnect-core/src/tools/sch_components.rs +++ b/crates/konnect-core/src/tools/sch_components.rs @@ -7,9 +7,8 @@ use crate::mcp::protocol::CallToolResult; use crate::tool; use crate::tools::{ - find_all_symbol_instance_blocks, find_symbol_instance_block, get_path, opt_f64, opt_str, - reembed_lib_symbols, require_array, require_f64, require_str, ReembedOutcome, ToolContext, - ToolDef, + find_all_symbol_instance_blocks, get_path, opt_f64, opt_str, reembed_lib_symbols, + require_array, require_f64, require_str, ReembedOutcome, ToolContext, ToolDef, }; use konnect_schematic_editor as cse; use konnect_sexp::{ @@ -101,7 +100,7 @@ pub fn tools() -> Vec { ), tool!( "delete_schematic_component", - "Remove a symbol instance from the schematic by its reference designator.", + "Remove a component by reference designator, including every placed unit of a multi-unit symbol.", json!({ "type": "object", "properties": { @@ -114,7 +113,7 @@ pub fn tools() -> Vec { ), tool!( "edit_schematic_component", - "Update fields (Reference, Value, Footprint, custom properties) of a symbol instance.", + "Update fields (Reference, Value, Footprint, custom properties) consistently across every placed unit of a component.", json!({ "type": "object", "properties": { @@ -135,7 +134,7 @@ pub fn tools() -> Vec { ), tool!( "get_schematic_component", - "Get all properties, position, and pin locations for a symbol instance.", + "Get a component's shared properties and every placed unit's position. Use get_schematic_pin_locations for pins.", json!({ "type": "object", "properties": { @@ -161,7 +160,7 @@ pub fn tools() -> Vec { ), tool!( "move_schematic_component", - "Move a symbol to a new position. Does NOT adjust connected wires.", + "Move a component's lowest-numbered unit to a new position and translate every other placed unit by the same delta. Does NOT adjust connected wires.", json!({ "type": "object", "properties": { @@ -176,7 +175,7 @@ pub fn tools() -> Vec { ), tool!( "rotate_schematic_component", - "Rotate a symbol by setting its absolute rotation angle (0/90/180/270).", + "Set the lowest-numbered unit's absolute rotation and rotate every other placed unit by the same delta.", json!({ "type": "object", "properties": { @@ -234,7 +233,7 @@ pub fn tools() -> Vec { ), tool!( "get_schematic_pin_locations", - "Get the exact schematic-space (X,Y) coordinates of every pin on a symbol, \ + "Get the exact schematic-space (X,Y) coordinates of every pin on every placed unit of a component, \ accounting for rotation and mirroring. Uses the canonical pin transform. \ Each pin also reports 'orientation_degrees', the direction leading away \ from the symbol body (0 = east) — a net label at the pin must read that \ @@ -270,7 +269,7 @@ pub fn tools() -> Vec { ), tool!( "add_component_annotation", - "Add a custom property (annotation) to a symbol instance in the schematic.", + "Add or update a custom property consistently across every placed unit of a component.", json!({ "type": "object", "properties": { @@ -285,7 +284,7 @@ pub fn tools() -> Vec { ), tool!( "group_components", - "Add a group property to multiple components in the schematic.", + "Add or update a group property on every placed unit of multiple components.", json!({ "type": "object", "properties": { @@ -303,14 +302,14 @@ pub fn tools() -> Vec { ), tool!( "replace_component", - "Replace a component's lib_id with a new library symbol (swap the component type).", + "Replace every placed unit of a component with a new library symbol while preserving unit numbers. A unit override is accepted only for a single placement.", json!({ "type": "object", "properties": { "schematic": { "type": "string", "description": "Path to .kicad_sch file" }, "reference": { "type": "string", "description": "Component reference designator (e.g. 'U1')" }, "new_lib_id": { "type": "string", "description": "New Library:Symbol identifier (e.g. 'Device:C')" }, - "unit": { "type": "integer", "description": "Optional unit number for multi-unit symbols; validated against the new symbol's unit count. When omitted the existing unit is kept." } + "unit": { "type": "integer", "description": "Optional unit number for a single placed unit; rejected as ambiguous when the reference has multiple placements. When omitted, every existing unit number is preserved and validated against the new symbol." } }, "required": ["schematic", "reference", "new_lib_id"] }), @@ -690,15 +689,21 @@ async fn handle_delete_schematic_component( let mut sch = cse::Schematic::load(&sch_path)?; - match sch.symbols.remove_by_reference(&reference) { - Some(_) => { - sch.overwrite()?; - Ok(CallToolResult::json(&json!({ "deleted": reference }))) - } - None => Ok(CallToolResult::error(format!( + let before = sch.symbols.len(); + sch.symbols + .retain(|symbol| symbol.reference() != Some(reference.as_str())); + let deleted_units = before - sch.symbols.len(); + if deleted_units == 0 { + Ok(CallToolResult::error(format!( "Component '{}' not found in schematic", reference - ))), + ))) + } else { + sch.overwrite()?; + Ok(CallToolResult::json(&json!({ + "deleted": reference, + "deleted_units": deleted_units + }))) } } @@ -710,61 +715,48 @@ fn is_reserved_property(name: &str) -> bool { matches!(name, "Reference" | "Value" | "Footprint" | "Datasheet") } -/// Does `reference`'s symbol block already carry a `name` property? -fn property_exists(content: &str, reference: &str, name: &str) -> bool { - find_symbol_instance_block(content, reference).is_some_and(|(start, end)| { - content[start..end].contains(&format!(r#"(property "{name}" ""#)) - }) +#[derive(Debug, Clone, Copy)] +struct PropertyWriteCounts { + updated: usize, + added: usize, } -/// Update the value of an existing `(property "field" "…")` inside -/// `reference`'s symbol block, in place. Returns the reason on failure so the -/// caller can report it instead of silently claiming success. Shared by -/// `edit_schematic_component` and `add_component_annotation` (#203) — the -/// second used to append a duplicate instead. -fn update_property_value( - content: &str, - reference: &str, - field: &str, - new_val: &str, -) -> Result { - let (sym_start, sym_end) = find_symbol_instance_block(content, reference) - .ok_or_else(|| format!("symbol '{reference}' not found in this schematic"))?; - let sym_block = &content[sym_start..sym_end]; - let field_search = format!(r#"(property "{field}" ""#); - let field_offset = sym_block - .find(&field_search) - .map(|o| sym_start + o + field_search.len()) - .ok_or_else(|| format!("'{reference}' has no '{field}' property"))?; - // Find the closing quote of the current value - let val_end = content[field_offset..] - .find('"') - .map(|o| field_offset + o) - .ok_or_else(|| format!("'{field}' property on '{reference}' is malformed"))?; - Ok(format!( - "{}{}{}", - &content[..field_offset], - new_val, - &content[val_end..] - )) +fn escape_property_text(value: &str) -> String { + value + .replace('\\', "\\\\") + .replace('"', "\\\"") + .replace('\n', "\\n") + .replace('\r', "\\r") + .replace('\t', "\\t") } -/// Append a new `(property …)` to `reference`'s symbol block. +fn closing_quote(content: &str, value_start: usize) -> Option { + let mut escaped = false; + for (offset, ch) in content[value_start..].char_indices() { + if escaped { + escaped = false; + } else if ch == '\\' { + escaped = true; + } else if ch == '"' { + return Some(value_start + offset); + } + } + None +} + +/// Build the insertion for a custom property in one placed unit's block. /// -/// Anchored at the symbol's own `(at …)` and written hidden: a custom field is -/// data, not something to draw over the sheet, and KiCad 10's canonical -/// instance form puts `(hide yes)` as a sibling before `(effects …)` (#96). -/// The `(at …)` is mandatory — a property written without one is defaulted to -/// the sheet origin, which is how every `#PWR` reference once piled up in the -/// top-left corner (#95). -fn append_property( +/// The property is anchored at that unit's own placement and inherits its +/// indentation, so applying this to every block neither piles fields at the +/// origin nor rewrites an eeschema-formatted file wholesale. +fn property_insert_edit( content: &str, reference: &str, + start: usize, + end: usize, name: &str, value: &str, -) -> Result { - let (start, end) = find_symbol_instance_block(content, reference) - .ok_or_else(|| format!("symbol '{reference}' not found in this schematic"))?; +) -> Result { let block = &content[start..end]; // The symbol's placement, to anchor the new property on. @@ -791,22 +783,74 @@ fn append_property( }) .unwrap_or_else(|| "\t\t".to_string()); - let escaped = value.replace('\\', "\\\\").replace('"', "\\\""); + let escaped_name = escape_property_text(name); + let escaped_value = escape_property_text(value); let prop = format!( - "\n{indent}(property \"{name}\" \"{escaped}\"\n{indent}\t(at {x} {y} 0)\n\ + "\n{indent}(property \"{escaped_name}\" \"{escaped_value}\"\n{indent}\t(at {x} {y} 0)\n\ {indent}\t(hide yes)\n{indent}\t(effects\n{indent}\t\t(font\n{indent}\t\t\t\ (size 1.27 1.27)\n{indent}\t\t)\n{indent}\t)\n{indent})" ); // Insert before the block's closing paren so the property stays inside it. - let close = content[..end] + let close = block .rfind(')') + .map(|offset| start + offset) .ok_or_else(|| format!("symbol block for '{reference}' is malformed"))?; - Ok(format!( - "{}{}{}", - &content[..close], - prop, - &content[close..] + Ok(SexpEdit::insert(close, prop)) +} + +/// Set one shared component property in every placed unit. +/// +/// Built-in properties must already exist in every unit (`add_missing=false`). +/// Custom fields may be present on only some units in a legacy/broken sheet; +/// `add_missing=true` updates those copies and fills the missing ones in the +/// same atomic document command. +fn set_property_value( + content: &str, + reference: &str, + field: &str, + new_value: &str, + add_missing: bool, +) -> Result<(String, PropertyWriteCounts), String> { + let blocks = find_all_symbol_instance_blocks(content, reference); + if blocks.is_empty() { + return Err(format!("symbol '{reference}' not found in this schematic")); + } + + let escaped_field = escape_property_text(field); + let field_search = format!(r#"(property "{escaped_field}" ""#); + let escaped_value = escape_property_text(new_value); + let mut edits = Vec::new(); + let mut updated = 0; + let mut added = 0; + + for (start, end) in blocks { + let block = &content[start..end]; + if let Some(relative) = block.find(&field_search) { + let value_start = start + relative + field_search.len(); + let value_end = closing_quote(content, value_start) + .ok_or_else(|| format!("'{field}' property on '{reference}' is malformed"))?; + edits.push(SexpEdit::replace( + value_start, + value_end, + escaped_value.clone(), + )); + updated += 1; + } else if add_missing { + edits.push(property_insert_edit( + content, reference, start, end, field, new_value, + )?); + added += 1; + } else { + return Err(format!( + "'{reference}' is missing the shared '{field}' property on one of its placed units" + )); + } + } + + Ok(( + apply_edits(content.to_string(), edits), + PropertyWriteCounts { updated, added }, )) } @@ -872,10 +916,13 @@ async fn handle_edit_schematic_component( // and a closure capturing them mutably would lock both for its lifetime. macro_rules! apply { ($field:expr, $new_val:expr) => { - match update_property_value(&content, &reference, $field, $new_val) { - Ok(updated) => { + match set_property_value(&content, &reference, $field, $new_val, false) { + Ok((updated, counts)) => { content = updated; - changed.push(format!("{} → {}", $field, $new_val)); + changed.push(format!( + "{} → {} ({} unit(s))", + $field, $new_val, counts.updated + )); } Err(why) => errors.push(format!("{}: {}", $field, why)), } @@ -925,16 +972,15 @@ async fn handle_edit_schematic_component( )); continue; } - if property_exists(&content, &reference, name) { - apply!(name.as_str(), value); - } else { - match append_property(&content, &reference, name, value) { - Ok(updated) => { - content = updated; - changed.push(format!("{name} → {value} (added)")); - } - Err(why) => errors.push(format!("{name}: {why}")), + match set_property_value(&content, &reference, name, value, true) { + Ok((updated, counts)) => { + content = updated; + changed.push(format!( + "{name} → {value} ({} updated, {} added)", + counts.updated, counts.added + )); } + Err(why) => errors.push(format!("{name}: {why}")), } } } @@ -958,11 +1004,11 @@ async fn handle_edit_schematic_component( } if !changed.is_empty() { - let item_id = symbol_item_id(&expected, &reference)?; - let command = SchematicCommand::replace_item_from_document( + let item_ids = symbol_item_ids(&expected, &reference)?; + let command = SchematicCommand::replace_items_from_document( &expected, &content, - item_id, + item_ids, format!("Edit {reference}"), )?; commit_command(&sch_path, &command)?; @@ -990,29 +1036,50 @@ async fn handle_get_schematic_component( let sch = cse::Schematic::load(&sch_path)?; - match sch.symbols.by_reference(&reference) { - Some(sym) => { - let (x, y) = sym.position(); - let rotation = sym.at.rotation.unwrap_or(0.0); - let mirror = sym.mirror.as_deref().unwrap_or(""); - Ok(CallToolResult::json(&json!({ - "reference": sym.reference().unwrap_or("?"), - "value": sym.value_str().unwrap_or(""), - "footprint": sym.footprint().unwrap_or(""), - "lib_id": sym.lib_id, - "x": x, - "y": y, - "rotation": rotation, - "mirror_x": mirror.contains('x'), - "mirror_y": mirror.contains('y'), - "uuid": sym.uuid - }))) - } - None => Ok(CallToolResult::error(format!( + let placed: Vec<_> = sch + .symbols + .iter() + .filter(|symbol| symbol.reference() == Some(reference.as_str())) + .collect(); + let Some(anchor) = placed.iter().copied().min_by_key(|symbol| symbol.unit) else { + return Ok(CallToolResult::error(format!( "Component '{}' not found", reference - ))), - } + ))); + }; + let (x, y) = anchor.position(); + let rotation = anchor.at.rotation.unwrap_or(0.0); + let mirror = anchor.mirror.as_deref().unwrap_or(""); + let units: Vec<_> = placed + .iter() + .map(|symbol| { + let (unit_x, unit_y) = symbol.position(); + let unit_mirror = symbol.mirror.as_deref().unwrap_or(""); + json!({ + "unit": symbol.unit, + "x": unit_x, + "y": unit_y, + "rotation": symbol.at.rotation.unwrap_or(0.0), + "mirror_x": unit_mirror.contains('x'), + "mirror_y": unit_mirror.contains('y'), + "uuid": symbol.uuid + }) + }) + .collect(); + Ok(CallToolResult::json(&json!({ + "reference": anchor.reference().unwrap_or("?"), + "value": anchor.value_str().unwrap_or(""), + "footprint": anchor.footprint().unwrap_or(""), + "lib_id": anchor.lib_id, + "x": x, + "y": y, + "rotation": rotation, + "mirror_x": mirror.contains('x'), + "mirror_y": mirror.contains('y'), + "uuid": anchor.uuid, + "unit_count": units.len(), + "units": units + }))) } async fn handle_list_schematic_components( @@ -1070,16 +1137,37 @@ async fn handle_move_schematic_component( let mut sch = cse::Schematic::load(&sch_path)?; - match sch.symbols.by_reference_mut(&reference) { - Some(sym) => { - sym.move_to(new_x, new_y); - sch.overwrite()?; - Ok(CallToolResult::json( - &json!({ "moved": reference, "x": new_x, "y": new_y }), - )) - } - None => Err(anyhow::anyhow!("Component '{}' not found", reference)), + let Some(anchor) = sch + .symbols + .iter() + .filter(|symbol| symbol.reference() == Some(reference.as_str())) + .min_by_key(|symbol| symbol.unit) + else { + return Err(anyhow::anyhow!("Component '{}' not found", reference)); + }; + let (old_x, old_y) = anchor.position(); + let (dx, dy) = (new_x - old_x, new_y - old_y); + let mut placements = Vec::new(); + for symbol in sch + .symbols + .iter_mut() + .filter(|symbol| symbol.reference() == Some(reference.as_str())) + { + symbol.translate(dx, dy); + placements.push(json!({ + "unit": symbol.unit, + "x": symbol.at.x, + "y": symbol.at.y + })); } + sch.overwrite()?; + Ok(CallToolResult::json(&json!({ + "moved": reference, + "x": new_x, + "y": new_y, + "moved_units": placements.len(), + "placements": placements + }))) } async fn handle_rotate_schematic_component( @@ -1098,16 +1186,38 @@ async fn handle_rotate_schematic_component( let mut sch = cse::Schematic::load(&sch_path)?; - match sch.symbols.by_reference_mut(&reference) { - Some(sym) => { - sym.set_rotation(rotation); - sch.overwrite()?; - Ok(CallToolResult::json( - &json!({ "rotated": reference, "rotation": rotation }), - )) - } - None => Err(anyhow::anyhow!("Component '{}' not found", reference)), + let Some(anchor) = sch + .symbols + .iter() + .filter(|symbol| symbol.reference() == Some(reference.as_str())) + .min_by_key(|symbol| symbol.unit) + else { + return Err(anyhow::anyhow!("Component '{}' not found", reference)); + }; + let rotation_delta = rotation - anchor.at.rotation.unwrap_or(0.0); + let mut placements = Vec::new(); + for symbol in sch + .symbols + .iter_mut() + .filter(|symbol| symbol.reference() == Some(reference.as_str())) + { + // The delta lands each unit at its own angle, so a unit already at + // 270° asked to follow a +90° turn computes 360° — normalize into + // [0, 360) before writing; eeschema only ever stores 0/90/180/270 + // and re-saves anything else, so an unnormalized angle survives only + // until KiCad touches the file and then silently diverges from what + // this response reported. + let new_rotation = (symbol.at.rotation.unwrap_or(0.0) + rotation_delta).rem_euclid(360.0); + symbol.set_rotation(new_rotation); + placements.push(json!({ "unit": symbol.unit, "rotation": new_rotation })); } + sch.overwrite()?; + Ok(CallToolResult::json(&json!({ + "rotated": reference, + "rotation": rotation, + "rotated_units": placements.len(), + "placements": placements + }))) } async fn handle_move_connected( @@ -1156,29 +1266,44 @@ async fn handle_move_region( let mut sch = cse::Schematic::load(&sch_path)?; - // Collect references of symbols within the bounding box - let refs_to_move: Vec = sch + // Select placements by UUID, not reference. A multi-unit reference may + // have one unit inside the rectangle and another outside; resolving the + // selected reference back through `by_reference_mut` moved unit 1 every + // time, and could move it twice when both units were selected (#182). + let uuids_to_move: std::collections::HashSet = sch .symbols .within_rectangle(x1, y1, x2, y2) .iter() - .filter_map(|s| s.reference().map(String::from)) + .map(|symbol| symbol.uuid.clone()) .collect(); - let mut moved = Vec::new(); - for reference in &refs_to_move { - if let Some(sym) = sch.symbols.by_reference_mut(reference) { - let (ox, oy) = sym.position(); - let (nx, ny) = snap_point(ox + dx, oy + dy, 1.27); - sym.move_to(nx, ny); - moved.push(reference.clone()); + let mut moved_references = Vec::new(); + let mut placements = Vec::new(); + for symbol in sch.symbols.iter_mut() { + if uuids_to_move.contains(&symbol.uuid) { + let (old_x, old_y) = symbol.position(); + let (new_x, new_y) = snap_point(old_x + dx, old_y + dy, 1.27); + symbol.move_to(new_x, new_y); + let reference = symbol.reference().unwrap_or("?").to_string(); + if !moved_references.contains(&reference) { + moved_references.push(reference.clone()); + } + placements.push(json!({ + "reference": reference, + "unit": symbol.unit, + "x": new_x, + "y": new_y + })); } } sch.overwrite()?; Ok(CallToolResult::json(&json!({ - "moved_count": moved.len(), - "moved": moved + "moved_count": moved_references.len(), + "moved": moved_references, + "moved_unit_count": placements.len(), + "placements": placements }))) } @@ -1202,87 +1327,97 @@ async fn handle_get_schematic_pin_locations( }; let (_, tree) = read_schematic(&sch_path)?; - let instances = extract_symbol_instances(&tree); - let inst = match instances.iter().find(|i| i.reference == reference) { - Some(i) => i, - None => { - return Ok(CallToolResult::error(format!( - "Component '{}' not found", - reference - ))) - } - }; + match pin_locations_for_reference(&tree, &reference) { + Ok(result) => Ok(CallToolResult::json(&result)), + Err(error) => Ok(CallToolResult::error(error)), + } +} - // Find the library symbol definition within the schematic's lib_symbols section +fn pin_locations_for_reference( + tree: &konnect_sexp::SexpNode, + reference: &str, +) -> Result { + let instances = extract_symbol_instances(tree); + let placed: Vec<_> = instances + .iter() + .filter(|instance| instance.reference == reference) + .collect(); + let Some(anchor) = placed.iter().copied().min_by_key(|instance| instance.unit) else { + return Err(format!("Component '{reference}' not found")); + }; let lib_syms = tree .find("lib_symbols") - .map(|n| n.find_all("symbol")) + .map(|node| node.find_all("symbol")) .unwrap_or_default(); - let lib_sym = find_lib_symbol(&lib_syms, inst); - // A missing embedded definition is an error, not an empty pin list — - // silently returning [] hid every bad-lib_id component until wiring or - // netlisting failed much later (#34). - let Some(sym) = lib_sym else { - return Ok(CallToolResult::error(format!( - "Component '{}' has no embedded definition for '{}' in this \ - schematic's lib_symbols — it was likely added with a lib_id that \ - doesn't exist in the installed libraries, so it is invisible to \ - KiCAD's netlister. Re-add it with a valid lib_id \ - (delete_schematic_component + add_schematic_component).", - reference, - inst.lib_symbol_name() - ))); - }; - // Unit-aware: only this instance's unit (plus _0_1 commons), not every - // unit's pins superimposed (#35). - let lib_pins = extract_lib_pins_for_unit(sym, inst.unit); - // A definition that resolves but has ZERO pins is almost always an - // `(extends "Parent")` stub — kicad-cli can't resolve those either (the - // netlist shows a pinless part), so silent pins:[] hides real breakage. - // The #34 guard above only catches MISSING definitions. - if lib_pins.is_empty() { - if let Some(parent) = sym.find_str("extends") { - return Ok(CallToolResult::error(format!( - "Component '{}': the embedded definition for '{}' is an \ - (extends \"{}\") stub with no pins of its own. kicad-cli \ - cannot resolve extends stubs (the netlist gets a pinless \ - part). Re-add the component (delete_schematic_component + \ - add_schematic_component) so the definition is embedded in \ - full, or place the parent symbol '{}' directly.", + let mut all_pins = Vec::new(); + let mut units = Vec::new(); + for instance in placed.iter().copied() { + // A missing embedded definition is an error, not an empty pin list — + // silently returning [] hid bad lib_ids until netlisting (#34). + let Some(symbol) = find_lib_symbol(&lib_syms, instance) else { + return Err(format!( + "Component '{}' unit {} has no embedded definition for '{}' in this \ + schematic's lib_symbols — re-add it with a valid lib_id", reference, - inst.lib_symbol_name(), - parent, - parent - ))); + instance.unit, + instance.lib_symbol_name() + )); + }; + let lib_pins = extract_lib_pins_for_unit(symbol, instance.unit); + if lib_pins.is_empty() { + if let Some(parent) = symbol.find_str("extends") { + return Err(format!( + "Component '{}' unit {}: the embedded definition for '{}' is an \ + (extends \"{}\") stub with no pins — re-add the component so the \ + definition is embedded in full", + reference, + instance.unit, + instance.lib_symbol_name(), + parent + )); + } } - } - let t = inst.pin_transform(); - let pins: Vec = lib_pins - .iter() - .map(|p| { - let (sx, sy) = pin_endpoint(p, t); - json!({ - "number": p.number, - "name": p.name, - "x": sx, - "y": sy, - // Which way the pin faces away from the body (0 = east). A - // label here should read that way, or it runs back over the - // symbol's pin names. - "orientation_degrees": pin_outward_direction(p, t), - "length_mm": p.length + + let transform = instance.pin_transform(); + let pins: Vec = lib_pins + .iter() + .map(|pin| { + let (x, y) = pin_endpoint(pin, transform); + json!({ + "number": pin.number, + "name": pin.name, + "unit": instance.unit, + "x": x, + "y": y, + "orientation_degrees": pin_outward_direction(pin, transform), + "length_mm": pin.length + }) }) - }) - .collect(); + .collect(); + all_pins.extend(pins.iter().cloned()); + units.push(json!({ + "unit": instance.unit, + "x": instance.x, + "y": instance.y, + "rotation": instance.rotation, + "pins": pins + })); + } - Ok(CallToolResult::json(&json!({ + Ok(json!({ "reference": reference, - "component_x": inst.x, - "component_y": inst.y, - "rotation": inst.rotation, - "pins": pins - }))) + // Preserve the original single-placement fields as the logical + // component anchor while exposing every real placement below. + "component_x": anchor.x, + "component_y": anchor.y, + "x": anchor.x, + "y": anchor.y, + "rotation": anchor.rotation, + "unit_count": units.len(), + "units": units, + "pins": all_pins + })) } async fn handle_batch_get_pin_locations( @@ -1301,64 +1436,14 @@ async fn handle_batch_get_pin_locations( }; let (_, tree) = read_schematic(&sch_path)?; // single read - let instances = extract_symbol_instances(&tree); - let lib_syms = tree - .find("lib_symbols") - .map(|n| n.find_all("symbol")) - .unwrap_or_default(); - let results: Vec = refs .iter() - .map(|reference| { - let inst = match instances.iter().find(|i| &i.reference == reference) { - Some(i) => i, - None => return json!({ "reference": reference, "error": "not found" }), - }; - let lib_sym = find_lib_symbol(&lib_syms, inst); - // Per-entry error rather than a silent empty pin list (#34). - let Some(sym) = lib_sym else { - return json!({ - "reference": reference, - "error": format!( - "no embedded definition for '{}' in lib_symbols — \ - likely added with a nonexistent lib_id", - inst.lib_symbol_name() - ) - }); - }; - let lib_pins = extract_lib_pins_for_unit(sym, inst.unit); - // Zero pins from a resolving definition = extends stub (#35); - // mirror the single-component handler's structured error. - if lib_pins.is_empty() { - if let Some(parent) = sym.find_str("extends") { - return json!({ - "reference": reference, - "error": format!( - "embedded definition for '{}' is an (extends \"{}\") \ - stub with no pins — re-add the component so it is \ - embedded in full", - inst.lib_symbol_name(), parent - ) - }); - } - } - let t = inst.pin_transform(); - let pins: Vec = lib_pins - .iter() - .map(|p| { - let (sx, sy) = pin_endpoint(p, t); - json!({ - "number": p.number, - "name": p.name, - "x": sx, - "y": sy, - "orientation_degrees": pin_outward_direction(p, t), - "length_mm": p.length - }) - }) - .collect(); - json!({ "reference": reference, "x": inst.x, "y": inst.y, "pins": pins }) - }) + .map( + |reference| match pin_locations_for_reference(&tree, reference) { + Ok(component) => component, + Err(error) => json!({ "reference": reference, "error": error }), + }, + ) .collect(); Ok(CallToolResult::json(&json!({ "components": results }))) @@ -1438,35 +1523,21 @@ async fn handle_add_component_annotation( let content = read_consistent(&sch_path)?; let expected = content.clone(); - if find_symbol_instance_block(&content, &reference).is_none() { - return Ok(CallToolResult::error(format!( - "Component '{}' not found", - reference - ))); - } - // An existing key is updated in place; appending a second `(property // "KEY" …)` gives eeschema two fields with one name — it shows both, // edits the wrong one, and the duplicate survives save/reload (#203). - // A new key goes through append_property, which anchors at the symbol's - // own position and matches the block's indentation, rather than the - // hardcoded origin-anchored form this handler used to write. - let (new_content, updated_existing) = if property_exists(&content, &reference, &key) { - match update_property_value(&content, &reference, &key, &value) { - Ok(updated) => (updated, true), - Err(why) => return Ok(CallToolResult::error(format!("{key}: {why}"))), - } - } else { - match append_property(&content, &reference, &key, &value) { - Ok(updated) => (updated, false), - Err(why) => return Ok(CallToolResult::error(format!("{key}: {why}"))), - } + // A new key is anchored separately at every unit's own position and uses + // that block's indentation. A partially populated legacy component is + // repaired by updating existing copies and adding only the missing ones. + let (new_content, counts) = match set_property_value(&content, &reference, &key, &value, true) { + Ok(updated) => updated, + Err(why) => return Ok(CallToolResult::error(format!("{key}: {why}"))), }; - let item_id = symbol_item_id(&expected, &reference)?; - let command = SchematicCommand::replace_item_from_document( + let item_ids = symbol_item_ids(&expected, &reference)?; + let command = SchematicCommand::replace_items_from_document( &expected, &new_content, - item_id, + item_ids, format!("Add {key} property to {reference}"), )?; commit_command(&sch_path, &command)?; @@ -1475,18 +1546,27 @@ async fn handle_add_component_annotation( "reference": reference, "added_property": key, "value": value, - "updated_existing": updated_existing + "updated_existing": counts.updated > 0, + "updated_units": counts.updated, + "added_units": counts.added }))) } -fn symbol_item_id(content: &str, reference: &str) -> anyhow::Result { - let (start, end) = find_symbol_instance_block(content, reference) - .ok_or_else(|| anyhow::anyhow!("component '{reference}' not found"))?; - let symbol = parse_sexp(&content[start..end])?; - let uuid = symbol - .find_str("uuid") - .ok_or_else(|| anyhow::anyhow!("component '{reference}' has no UUID"))?; - Ok(ItemId::new(uuid.to_owned())?) +fn symbol_item_ids(content: &str, reference: &str) -> anyhow::Result> { + let blocks = find_all_symbol_instance_blocks(content, reference); + if blocks.is_empty() { + anyhow::bail!("component '{reference}' not found"); + } + blocks + .into_iter() + .map(|(start, end)| { + let symbol = parse_sexp(&content[start..end])?; + let uuid = symbol.find_str("uuid").ok_or_else(|| { + anyhow::anyhow!("component '{reference}' has a unit without UUID") + })?; + ItemId::new(uuid.to_owned()).map_err(Into::into) + }) + .collect() } async fn handle_group_components( @@ -1517,24 +1597,14 @@ async fn handle_group_components( let mut item_ids = Vec::new(); for reference in &refs { - let (sym_start, sym_end) = match find_symbol_instance_block(&content, reference) { - Some(r) => r, - None => continue, - }; - - let sym_block = &content[sym_start..sym_end]; - let insert_rel = sym_block - .find("(instances") - .unwrap_or(sym_block.rfind(')').unwrap_or(sym_block.len() - 1)); - let insert_abs = sym_start + insert_rel; - - let prop_sexp = format!( - " (property \"Group\" \"{group_name}\"\n (at 0 0 0)\n (effects (font (size 1.27 1.27)) (hide yes))\n )\n " - ); - - content = apply_edits(content, vec![SexpEdit::insert(insert_abs, prop_sexp)]); - item_ids.push(symbol_item_id(&expected, reference)?); - grouped.push(reference.clone()); + match set_property_value(&content, reference, "Group", &group_name, true) { + Ok((updated, _)) => { + content = updated; + item_ids.extend(symbol_item_ids(&expected, reference)?); + grouped.push(reference.clone()); + } + Err(_) => continue, + } } if !item_ids.is_empty() { @@ -1794,81 +1864,129 @@ async fn handle_replace_component( let mut content = read_consistent(&sch_path)?; let expected = content.clone(); - // Find the symbol block for this reference - let (sym_start, sym_end) = match find_symbol_instance_block(&content, &reference) { - Some(r) => r, - None => { - return Ok(CallToolResult::error(format!( - "Component '{}' not found", - reference - ))) - } - }; - - // Find the (lib_id "OLD") and replace it — searching only within this - // symbol's block, so a malformed instance can't reach into the next one. - let sym_block = &content[sym_start..sym_end]; - let lib_id_pat = "(lib_id \""; - let lib_id_rel = match sym_block.find(lib_id_pat) { - Some(o) => o, - None => { - return Ok(CallToolResult::error( - "Could not find lib_id in symbol block", - )) - } - }; - let lib_id_abs = sym_start + lib_id_rel + lib_id_pat.len(); - let lib_id_end = match content[lib_id_abs..].find('"') { - Some(o) => lib_id_abs + o, - None => return Ok(CallToolResult::error("Malformed lib_id")), - }; - - let old_lib_id = content[lib_id_abs..lib_id_end].to_string(); + let blocks = find_all_symbol_instance_blocks(&content, &reference); + if blocks.is_empty() { + return Ok(CallToolResult::error(format!( + "Component '{}' not found", + reference + ))); + } + if blocks.len() > 1 && new_unit.is_some() { + return Ok(CallToolResult::error(format!( + "Component '{}' has {} placed units; the 'unit' override is only \ + unambiguous for a single placement. Omit it to preserve each unit.", + reference, + blocks.len() + ))); + } - let new_content = apply_edits( - content, - vec![SexpEdit::replace( - lib_id_abs, - lib_id_end, - new_lib_id.clone(), - )], - ); - content = new_content; + let parsed = parse_sexp(&content)?; + let current_units: Vec = extract_symbol_instances(&parsed) + .into_iter() + .filter(|instance| instance.reference == reference) + .map(|instance| instance.unit) + .collect(); - // Optional unit change, validated against the NEW symbol's unit count - // (#35). Applied before the embed so all edits land in one write. let src = crate::tools::library::KiCadSymbolSource::for_file(&sch_path); + let embedded_unit_count = parsed + .find("lib_symbols") + .and_then(|libraries| { + libraries.find_all("symbol").into_iter().find(|symbol| { + symbol.get(1).and_then(|value| value.as_str()) == Some(new_lib_id.as_str()) + }) + }) + .map(|symbol| { + symbol + .find_all("symbol") + .into_iter() + .filter_map(|unit| { + unit.get(1) + .and_then(|value| value.as_str()) + .and_then(konnect_sexp::schematic::parse_subsymbol_unit) + }) + .max() + .unwrap_or(1) + .max(1) + }); + let unit_count = embedded_unit_count + .or_else(|| cse::library::symbol_unit_count(&new_lib_id, &src)) + .unwrap_or(1); if let Some(unit) = new_unit { - let unit_count = cse::library::symbol_unit_count(&new_lib_id, &src).unwrap_or(1); if unit < 1 || unit > unit_count { return Ok(CallToolResult::error(format!( "Invalid unit {} for '{}': the symbol has {} unit(s) (valid: 1..={}).", unit, new_lib_id, unit_count, unit_count ))); } - // Re-find the block (offsets moved with the lib_id edit), then update - // every `(unit N)` inside it — the symbol's own and the one in its - // (instances …) entry. - if let Some((s, e)) = find_symbol_instance_block(&content, &reference) { - let block = &content[s..e]; - let mut edits = Vec::new(); - let mut from = 0usize; - while let Some(rel) = block[from..].find("(unit ") { - let num_start = from + rel + "(unit ".len(); - let Some(close) = block[num_start..].find(')') else { - break; - }; - edits.push(SexpEdit::replace( - s + num_start, - s + num_start + close, - unit.to_string(), - )); - from = num_start + close; - } - content = apply_edits(content, edits); + } else if let Some(invalid) = current_units + .iter() + .find(|unit| **unit < 1 || **unit > unit_count) + { + return Ok(CallToolResult::error(format!( + "Cannot replace '{}' with '{}': placed unit {} does not exist in the \ + new {}-unit symbol. Delete and re-place the component deliberately.", + reference, new_lib_id, invalid, unit_count + ))); + } + + // Replace the library id in every unit block. Shared component identity + // must not leave one unit pointing at the old symbol (#182). + let lib_id_pat = "(lib_id \""; + let escaped_lib_id = escape_property_text(&new_lib_id); + let mut edits = Vec::new(); + let mut old_lib_ids = Vec::new(); + for (start, end) in &blocks { + let block = &content[*start..*end]; + let Some(relative) = block.find(lib_id_pat) else { + return Ok(CallToolResult::error(format!( + "A unit of '{}' has no lib_id", + reference + ))); + }; + let value_start = *start + relative + lib_id_pat.len(); + let Some(value_end) = closing_quote(&content, value_start) else { + return Ok(CallToolResult::error("Malformed lib_id")); + }; + old_lib_ids.push(content[value_start..value_end].to_string()); + edits.push(SexpEdit::replace( + value_start, + value_end, + escaped_lib_id.clone(), + )); + } + + // Add the optional unit edits without a second source read. The multi-unit + // guard above means this scan has at most one block. + if let Some(unit) = new_unit { + let (start, end) = blocks[0]; + let block = &content[start..end]; + let mut from = 0usize; + while let Some(relative) = block[from..].find("(unit ") { + let number_start = from + relative + "(unit ".len(); + let Some(close) = block[number_start..].find(')') else { + break; + }; + edits.push(SexpEdit::replace( + start + number_start, + start + number_start + close, + unit.to_string(), + )); + from = number_start + close; } } + old_lib_ids.sort(); + old_lib_ids.dedup(); + if old_lib_ids.len() != 1 { + return Ok(CallToolResult::error(format!( + "Component '{}' already has inconsistent library ids across its units: {}", + reference, + old_lib_ids.join(", ") + ))); + } + let old_lib_id = old_lib_ids.remove(0); + content = apply_edits(content, edits); + // Ensure the new library symbol definition is present. Bail BEFORE writing: // a replace that can't embed its definition would leave the component // netlist-invisible (#34). @@ -1881,7 +1999,8 @@ async fn handle_replace_component( "reference": reference, "old_lib_id": old_lib_id, "new_lib_id": new_lib_id, - "unit": new_unit + "unit": new_unit, + "units_replaced": blocks.len() }))) } @@ -3236,8 +3355,8 @@ mod tests { } /// The old path hardcoded (at 0 0 0) — the annotation rendered at the - /// sheet origin, far from its symbol. append_property anchors on the - /// symbol's own position. + /// sheet origin, far from its symbol. The shared property writer anchors + /// each property on its own placed unit. #[tokio::test] async fn add_component_annotation_anchors_at_the_symbol_not_the_origin() { let (_symdir, _env) = stub_symbol_dir(); @@ -3722,3 +3841,492 @@ mod move_connected_tests { ); } } + +#[cfg(test)] +mod multi_unit_component_tests { + use super::*; + use crate::mcp::protocol::ToolContent; + use crate::tools::ServerConfig; + use std::sync::Arc; + + const SCHEMATIC: &str = r#"(kicad_sch + (version 20260306) + (uuid "11111111-1111-4111-8111-111111111111") + (lib_symbols + (symbol "Test:DUAL" + (symbol "DUAL_1_1" + (pin input line (at 0 0 0) (length 0) (name "A") (number "1")) + ) + (symbol "DUAL_2_1" + (pin output line (at 0 0 0) (length 0) (name "Y") (number "2")) + ) + ) + (symbol "Test:DUAL_NEW" + (symbol "DUAL_NEW_1_1" + (pin input line (at 0 0 0) (length 0) (name "A") (number "1")) + ) + (symbol "DUAL_NEW_2_1" + (pin output line (at 0 0 0) (length 0) (name "Y") (number "2")) + ) + ) + ) + (symbol + (lib_id "Test:DUAL") + (at 100 100 0) + (unit 1) + (uuid "22222222-2222-4222-8222-222222222222") + (property "Reference" "U1" (at 100 98 0)) + (property "Value" "OLD" (at 100 102 0)) + (property "Footprint" "" (at 100 100 0)) + (property "Datasheet" "" (at 100 100 0)) + (property "Note" "OLD" (at 100 100 0) (hide yes)) + (instances + (project "multi" + (path "/11111111-1111-4111-8111-111111111111" + (reference "U1") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "Test:DUAL") + (at 100 120 180) + (unit 2) + (uuid "33333333-3333-4333-8333-333333333333") + (property "Reference" "U1" (at 100 118 0)) + (property "Value" "OLD" (at 100 122 0)) + (property "Footprint" "" (at 100 120 0)) + (property "Datasheet" "" (at 100 120 0)) + (instances + (project "multi" + (path "/11111111-1111-4111-8111-111111111111" + (reference "U1") + (unit 2) + ) + ) + ) + ) + (sheet_instances (path "/" (page "1"))) +) +"#; + + fn context() -> ToolContext { + ToolContext::new( + ServerConfig { + kicad_cli: String::new(), + kicad_binary: String::new(), + ipc_address: String::new(), + project_dir: None, + jlcpcb_db_path: None, + auto_load_toolsets: false, + eager_toolsets: false, + }, + Arc::new(crate::router::ToolRouter::new()), + ) + } + + fn fixture() -> (tempfile::TempDir, std::path::PathBuf) { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("multi.kicad_sch"); + std::fs::write(&path, SCHEMATIC).unwrap(); + (directory, path) + } + + /// A real eeschema save (KiCad's ecc83 demo): tabs, CRLF, and U1 placed + /// as units 2 and 3 of the embedded `ecc83-pp:ECC83` dual triode. The + /// hand-written `SCHEMATIC` above shares this module's own serialization + /// habits, so only this file exercises the indentation- and + /// dialect-matching branches against what KiCad actually writes. + fn eeschema_fixture() -> (tempfile::TempDir, std::path::PathBuf) { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("ecc83.kicad_sch"); + std::fs::write( + &path, + include_str!("../../tests/fixtures/ecc83_multiunit.kicad_sch"), + ) + .unwrap(); + (directory, path) + } + + #[tokio::test] + async fn a_real_eeschema_multi_unit_component_is_seen_whole() { + let (_directory, path) = eeschema_fixture(); + let result = body( + handle_get_schematic_component( + &json!({ "schematic": path, "reference": "U1" }), + &context(), + ) + .await + .unwrap(), + ); + assert_eq!( + result["unit_count"], 3, + "U1 is placed as both triodes plus the heater unit" + ); + let mut units: Vec = result["units"] + .as_array() + .unwrap() + .iter() + .map(|unit| unit["unit"].as_i64().unwrap()) + .collect(); + units.sort_unstable(); + assert_eq!(units, [1, 2, 3]); + } + + #[tokio::test] + async fn annotating_a_real_eeschema_file_reaches_every_unit_in_its_own_dialect() { + let (_directory, path) = eeschema_fixture(); + let result = body( + handle_add_component_annotation( + &json!({ + "schematic": path, + "reference": "U1", + "key": "MPN", + "value": "ECC83-JJ" + }), + &context(), + ) + .await + .unwrap(), + ); + assert_eq!(result["added_units"], 3, "{result}"); + let source = std::fs::read_to_string(&path).unwrap(); + assert_eq!( + source.matches("(property \"MPN\" \"ECC83-JJ\"").count(), + 3, + "the property lands in every unit block" + ); + // The inserted lines must follow the file's own indentation (tabs) — + // a 2-space insert in a tab-indented eeschema file is exactly the + // drift the KiCad-authored fixture exists to catch. + for line in source.lines().filter(|line| line.contains("\"MPN\"")) { + assert!( + line.starts_with('\t'), + "inserted property must be tab-indented like its file: {line:?}" + ); + } + } + + fn body(result: CallToolResult) -> serde_json::Value { + assert!(!result.is_error, "mutation unexpectedly failed"); + let ToolContent::Text { text } = &result.content[0] else { + panic!("expected text result"); + }; + serde_json::from_str(text).unwrap() + } + + fn instances(path: &std::path::Path) -> Vec { + let (_, tree) = read_schematic(path).unwrap(); + extract_symbol_instances(&tree) + .into_iter() + .filter(|instance| instance.reference == "U1" || instance.reference == "U9") + .collect() + } + + #[tokio::test] + async fn delete_removes_every_placed_unit() { + let (_directory, path) = fixture(); + let result = body( + handle_delete_schematic_component( + &json!({ "schematic": path, "reference": "U1" }), + &context(), + ) + .await + .unwrap(), + ); + assert_eq!(result["deleted_units"], 2); + assert!(instances(&path).is_empty()); + } + + #[tokio::test] + async fn move_translates_every_unit_by_one_shared_delta() { + let (_directory, path) = fixture(); + let result = body( + handle_move_schematic_component( + &json!({ + "schematic": path, + "reference": "U1", + "x": 110.0, + "y": 110.0 + }), + &context(), + ) + .await + .unwrap(), + ); + assert_eq!(result["moved_units"], 2); + let mut placed = instances(&path); + placed.sort_by_key(|instance| instance.unit); + assert!((placed[0].x - placed[1].x).abs() < 0.001); + assert!(((placed[1].y - placed[0].y) - 20.0).abs() < 0.001); + } + + #[tokio::test] + async fn rotate_preserves_the_units_relative_orientation() { + let (_directory, path) = fixture(); + let result = body( + handle_rotate_schematic_component( + &json!({ + "schematic": path, + "reference": "U1", + "rotation": 90.0 + }), + &context(), + ) + .await + .unwrap(), + ); + assert_eq!(result["rotated_units"], 2); + let mut placed = instances(&path); + placed.sort_by_key(|instance| instance.unit); + assert_eq!(placed[0].rotation, 90.0); + assert_eq!(placed[1].rotation, 270.0); + } + + /// The delta arithmetic can push a trailing unit past 360° — a unit at + /// 270° following a +90° turn computes 360°, which eeschema never writes + /// (it stores 0/90/180/270 and re-saves anything else). The stored and + /// reported angle must be the normalized one, or the response diverges + /// from the file the moment KiCad touches it. + #[tokio::test] + async fn rotation_past_a_full_turn_normalizes_instead_of_writing_360() { + let (_directory, path) = fixture(); + for target in [90.0, 180.0] { + body( + handle_rotate_schematic_component( + &json!({ + "schematic": path, + "reference": "U1", + "rotation": target + }), + &context(), + ) + .await + .unwrap(), + ); + } + let mut placed = instances(&path); + placed.sort_by_key(|instance| instance.unit); + assert_eq!(placed[0].rotation, 180.0); + assert_eq!( + placed[1].rotation, 0.0, + "270° + 90° must store 0°, not 360°" + ); + let source = std::fs::read_to_string(&path).unwrap(); + assert!( + !source.contains("(at 40 20 360)") && !source.contains(" 360)"), + "no unnormalized angle may reach the file" + ); + } + + #[tokio::test] + async fn edit_updates_shared_fields_on_every_unit() { + let (_directory, path) = fixture(); + body( + handle_edit_schematic_component( + &json!({ + "schematic": path, + "reference": "U1", + "value": "NEW", + "fields": { "MPN": "A\\\"B" } + }), + &context(), + ) + .await + .unwrap(), + ); + let source = std::fs::read_to_string(&path).unwrap(); + assert_eq!(source.matches("(property \"Value\" \"NEW\"").count(), 2); + assert_eq!( + source.matches("(property \"MPN\" \"A\\\\\\\"B\"").count(), + 2 + ); + } + + #[tokio::test] + async fn rename_updates_rendered_and_netlist_references_on_every_unit() { + let (_directory, path) = fixture(); + body( + handle_edit_schematic_component( + &json!({ + "schematic": path, + "reference": "U1", + "new_reference": "U9" + }), + &context(), + ) + .await + .unwrap(), + ); + let source = std::fs::read_to_string(&path).unwrap(); + assert_eq!(source.matches("(property \"Reference\" \"U9\"").count(), 2); + assert_eq!(source.matches("(reference \"U9\")").count(), 2); + assert_eq!(instances(&path).len(), 2); + } + + #[tokio::test] + async fn annotation_repairs_a_field_missing_from_one_unit() { + let (_directory, path) = fixture(); + let result = body( + handle_add_component_annotation( + &json!({ + "schematic": path, + "reference": "U1", + "key": "Note", + "value": "NEW" + }), + &context(), + ) + .await + .unwrap(), + ); + assert_eq!(result["updated_units"], 1); + assert_eq!(result["added_units"], 1); + let source = std::fs::read_to_string(&path).unwrap(); + assert_eq!(source.matches("(property \"Note\" \"NEW\"").count(), 2); + } + + #[tokio::test] + async fn grouping_adds_one_property_to_every_unit() { + let (_directory, path) = fixture(); + body( + handle_group_components( + &json!({ + "schematic": path, + "group_name": "Logic", + "references": ["U1"] + }), + &context(), + ) + .await + .unwrap(), + ); + let source = std::fs::read_to_string(&path).unwrap(); + assert_eq!(source.matches("(property \"Group\" \"Logic\"").count(), 2); + } + + #[tokio::test] + async fn pin_locations_include_every_units_real_placement() { + let (_directory, path) = fixture(); + let result = body( + handle_get_schematic_pin_locations( + &json!({ "schematic": path, "reference": "U1" }), + &context(), + ) + .await + .unwrap(), + ); + assert_eq!(result["unit_count"], 2); + assert_eq!(result["pins"].as_array().unwrap().len(), 2); + assert!(result["pins"] + .as_array() + .unwrap() + .iter() + .any(|pin| pin["number"] == "1" && pin["unit"] == 1 && pin["y"] == 100.0)); + assert!(result["pins"] + .as_array() + .unwrap() + .iter() + .any(|pin| pin["number"] == "2" && pin["unit"] == 2 && pin["y"] == 120.0)); + } + + #[tokio::test] + async fn component_summary_lists_every_placement() { + let (_directory, path) = fixture(); + let result = body( + handle_get_schematic_component( + &json!({ "schematic": path, "reference": "U1" }), + &context(), + ) + .await + .unwrap(), + ); + assert_eq!(result["unit_count"], 2); + assert_eq!( + result["unit_count"].as_u64().unwrap() as usize, + result["units"].as_array().unwrap().len() + ); + assert!(result["units"] + .as_array() + .unwrap() + .iter() + .any(|unit| unit["unit"] == 2 && unit["y"] == 120.0)); + } + + #[tokio::test] + async fn replace_changes_every_unit_and_preserves_unit_numbers() { + let (_directory, path) = fixture(); + let result = body( + handle_replace_component( + &json!({ + "schematic": path, + "reference": "U1", + "new_lib_id": "Test:DUAL_NEW" + }), + &context(), + ) + .await + .unwrap(), + ); + assert_eq!(result["units_replaced"], 2); + let mut placed = instances(&path); + placed.sort_by_key(|instance| instance.unit); + assert_eq!( + placed + .iter() + .map(|instance| instance.unit) + .collect::>(), + vec![1, 2] + ); + assert!(placed + .iter() + .all(|instance| instance.lib_id == "Test:DUAL_NEW")); + } + + #[tokio::test] + async fn replace_rejects_an_ambiguous_unit_override_without_writing() { + let (_directory, path) = fixture(); + let before = std::fs::read(&path).unwrap(); + let result = handle_replace_component( + &json!({ + "schematic": path, + "reference": "U1", + "new_lib_id": "Test:DUAL_NEW", + "unit": 1 + }), + &context(), + ) + .await + .unwrap(); + assert!(result.is_error); + assert_eq!(std::fs::read(&path).unwrap(), before); + } + + #[tokio::test] + async fn move_region_moves_the_selected_unit_not_unit_one() { + let (_directory, path) = fixture(); + let result = body( + handle_move_region( + &json!({ + "schematic": path, + "x1": 95.0, + "y1": 115.0, + "x2": 105.0, + "y2": 125.0, + "dx": 10.0, + "dy": 0.0 + }), + &context(), + ) + .await + .unwrap(), + ); + assert_eq!(result["moved_unit_count"], 1); + assert_eq!(result["placements"][0]["unit"], 2); + let mut placed = instances(&path); + placed.sort_by_key(|instance| instance.unit); + assert_eq!(placed[0].x, 100.0, "unit 1 must stay put"); + assert_ne!(placed[1].x, 100.0, "selected unit 2 must move"); + } +} diff --git a/crates/konnect-core/tests/fixtures/ecc83_multiunit.kicad_sch b/crates/konnect-core/tests/fixtures/ecc83_multiunit.kicad_sch new file mode 100644 index 00000000..97a3340e --- /dev/null +++ b/crates/konnect-core/tests/fixtures/ecc83_multiunit.kicad_sch @@ -0,0 +1,3545 @@ +(kicad_sch + (version 20250114) + (generator "eeschema") + (generator_version "9.0") + (uuid "28f865a0-4433-4a53-bbd7-b62f276848e4") + (paper "A4") + (title_block + (title "ECC Push-Pull") + (date "Sat 21 Mar 2015") + (rev "0.1") + ) + (lib_symbols + (symbol "ecc83-pp:C" + (pin_numbers + (hide yes) + ) + (pin_names + (offset 0.254) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "C" + (at 0.635 2.54 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "C" + (at 0.635 -2.54 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "" + (at 0.9652 -3.81 0) + (effects + (font + (size 0.762 0.762) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (effects + (font + (size 1.524 1.524) + ) + ) + ) + (property "Description" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_fp_filters" "C? C_????_* C_???? SMD*_c Capacitor*" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "C_0_1" + (polyline + (pts + (xy -2.032 0.762) (xy 2.032 0.762) + ) + (stroke + (width 0.508) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -2.032 -0.762) (xy 2.032 -0.762) + ) + (stroke + (width 0.508) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "C_1_1" + (pin passive line + (at 0 3.81 270) + (length 2.794) + (name "~" + (effects + (font + (size 1.016 1.016) + ) + ) + ) + (number "1" + (effects + (font + (size 1.016 1.016) + ) + ) + ) + ) + (pin passive line + (at 0 -3.81 90) + (length 2.794) + (name "~" + (effects + (font + (size 1.016 1.016) + ) + ) + ) + (number "2" + (effects + (font + (size 1.016 1.016) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "ecc83-pp:CONN_1" + (pin_numbers + (hide yes) + ) + (pin_names + (offset 0.762) + (hide yes) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "P" + (at 2.032 0 0) + (effects + (font + (size 1.016 1.016) + ) + (justify left) + ) + ) + (property "Value" "CONN_1" + (at 0 1.397 0) + (effects + (font + (size 0.762 0.762) + ) + (hide yes) + ) + ) + (property "Footprint" "MountingHole:MountingHole_3.2mm_M3_DIN965_Pad" + (at 0 -1.27 0) + (effects + (font + (size 0.254 0.254) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (effects + (font + (size 1.524 1.524) + ) + ) + ) + (property "Description" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "CONN_1_0_1" + (polyline + (pts + (xy -0.762 0) (xy -1.27 0) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (circle + (center 0 0) + (radius 0.7874) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "CONN_1_1_1" + (pin passive line + (at -3.81 0 0) + (length 2.54) + (name "1" + (effects + (font + (size 1.524 1.524) + ) + ) + ) + (number "1" + (effects + (font + (size 1.524 1.524) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "ecc83-pp:CONN_2" + (pin_names + (offset 1.016) + (hide yes) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "P" + (at -1.27 0 90) + (effects + (font + (size 1.016 1.016) + ) + ) + ) + (property "Value" "CONN_2" + (at 1.27 0 90) + (effects + (font + (size 1.016 1.016) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (effects + (font + (size 1.524 1.524) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (effects + (font + (size 1.524 1.524) + ) + ) + ) + (property "Description" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "CONN_2_0_1" + (rectangle + (start -2.54 3.81) + (end 2.54 -3.81) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "CONN_2_1_1" + (pin passive inverted + (at -8.89 2.54 0) + (length 6.35) + (name "P1" + (effects + (font + (size 1.524 1.524) + ) + ) + ) + (number "1" + (effects + (font + (size 1.524 1.524) + ) + ) + ) + ) + (pin passive inverted + (at -8.89 -2.54 0) + (length 6.35) + (name "PM" + (effects + (font + (size 1.524 1.524) + ) + ) + ) + (number "2" + (effects + (font + (size 1.524 1.524) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "ecc83-pp:CP" + (pin_numbers + (hide yes) + ) + (pin_names + (offset 0.254) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "C" + (at 0.635 2.54 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "CP" + (at 0.635 -2.54 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "" + (at 0.9652 -3.81 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Polarised capacitor" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_keywords" "cap capacitor" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_fp_filters" "CP_*" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "CP_0_1" + (rectangle + (start -2.286 0.508) + (end -2.286 1.016) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (rectangle + (start -2.286 0.508) + (end 2.286 0.508) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -1.778 2.286) (xy -0.762 2.286) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -1.27 2.794) (xy -1.27 1.778) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (rectangle + (start 2.286 1.016) + (end -2.286 1.016) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (rectangle + (start 2.286 1.016) + (end 2.286 0.508) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (rectangle + (start 2.286 -0.508) + (end -2.286 -1.016) + (stroke + (width 0) + (type default) + ) + (fill + (type outline) + ) + ) + ) + (symbol "CP_1_1" + (pin passive line + (at 0 3.81 270) + (length 2.794) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + (pin passive line + (at 0 -3.81 90) + (length 2.794) + (name "~" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "2" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "ecc83-pp:ECC83" + (pin_names + (offset 0) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "U" + (at 3.302 7.874 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "ECC83" + (at 8.89 -7.62 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "VALVE-NOVAL_P" + (at 10.16 -8.89 0) + (effects + (font + (size 0.762 0.762) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (effects + (font + (size 1.524 1.524) + ) + ) + ) + (property "Description" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_locked" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (symbol "ECC83_0_1" + (polyline + (pts + (xy -5.08 2.54) (xy -5.08 -2.54) (xy -5.08 -2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (arc + (start -5.08 2.54) + (mid 0 7.5979) + (end 5.08 2.54) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (arc + (start 5.08 -2.54) + (mid 0 -7.5979) + (end -5.08 -2.54) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 5.08 2.54) (xy 5.08 -2.54) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "ECC83_1_0" + (polyline + (pts + (xy -2.54 -5.08) (xy -2.54 -7.62) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 5.08) (xy 0 7.62) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "ECC83_1_1" + (polyline + (pts + (xy -5.08 0) (xy -3.175 0) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -2.54 5.08) (xy 2.794 5.08) (xy 2.794 5.08) + ) + (stroke + (width 0.254) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -1.905 0) (xy -3.175 0) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -0.635 0) (xy 0.635 0) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (arc + (start -2.54 -5.08) + (mid 0 -3.0968) + (end 2.54 -5.08) + (stroke + (width 0.254) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 1.905 0) (xy 3.175 0) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (pin input line + (at -7.62 0 0) + (length 2.54) + (name "G" + (effects + (font + (size 1.016 1.016) + ) + ) + ) + (number "7" + (effects + (font + (size 1.016 1.016) + ) + ) + ) + ) + (pin passive line + (at -2.54 -10.16 90) + (length 2.54) + (name "K" + (effects + (font + (size 1.016 1.016) + ) + ) + ) + (number "8" + (effects + (font + (size 1.016 1.016) + ) + ) + ) + ) + (pin passive line + (at 0 10.16 270) + (length 2.54) + (name "A" + (effects + (font + (size 1.016 1.016) + ) + ) + ) + (number "6" + (effects + (font + (size 1.016 1.016) + ) + ) + ) + ) + ) + (symbol "ECC83_2_0" + (polyline + (pts + (xy -2.54 -5.08) (xy -2.54 -7.62) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 0 5.08) (xy 0 7.62) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "ECC83_2_1" + (polyline + (pts + (xy -5.08 0) (xy -3.175 0) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -2.54 5.08) (xy 2.794 5.08) (xy 2.794 5.08) + ) + (stroke + (width 0.254) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -1.905 0) (xy -3.175 0) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy -0.635 0) (xy 0.635 0) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (arc + (start -2.54 -5.08) + (mid 0 -3.0968) + (end 2.54 -5.08) + (stroke + (width 0.254) + (type default) + ) + (fill + (type none) + ) + ) + (polyline + (pts + (xy 1.905 0) (xy 3.175 0) + ) + (stroke + (width 0.1524) + (type default) + ) + (fill + (type none) + ) + ) + (pin input line + (at -7.62 0 0) + (length 2.54) + (name "G" + (effects + (font + (size 1.016 1.016) + ) + ) + ) + (number "2" + (effects + (font + (size 1.016 1.016) + ) + ) + ) + ) + (pin passive line + (at -2.54 -10.16 90) + (length 2.54) + (name "K" + (effects + (font + (size 1.016 1.016) + ) + ) + ) + (number "3" + (effects + (font + (size 1.016 1.016) + ) + ) + ) + ) + (pin passive line + (at 0 10.16 270) + (length 2.54) + (name "A" + (effects + (font + (size 1.016 1.016) + ) + ) + ) + (number "1" + (effects + (font + (size 1.016 1.016) + ) + ) + ) + ) + ) + (symbol "ECC83_3_1" + (arc + (start -2.54 -6.35) + (mid -1.27 -5.5651) + (end 0 -6.35) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (arc + (start 0 -6.35) + (mid 1.27 -5.5651) + (end 2.54 -6.35) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + (pin input line + (at -2.54 -11.43 90) + (length 5.08) + (name "F1" + (effects + (font + (size 1.016 1.016) + ) + ) + ) + (number "4" + (effects + (font + (size 1.016 1.016) + ) + ) + ) + ) + (pin input line + (at 0 -11.43 90) + (length 5.08) + (name "F2" + (effects + (font + (size 1.016 1.016) + ) + ) + ) + (number "9" + (effects + (font + (size 1.016 1.016) + ) + ) + ) + ) + (pin input line + (at 2.54 -11.43 90) + (length 5.08) + (name "F1" + (effects + (font + (size 1.016 1.016) + ) + ) + ) + (number "5" + (effects + (font + (size 1.016 1.016) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "ecc83-pp:GND" + (power) + (pin_names + (offset 0) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "#PWR" + (at 0 -6.35 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 0 -3.81 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (effects + (font + (size 1.524 1.524) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (effects + (font + (size 1.524 1.524) + ) + ) + ) + (property "Description" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "GND_0_1" + (polyline + (pts + (xy 0 0) (xy 0 -1.27) (xy 1.27 -1.27) (xy 0 -2.54) (xy -1.27 -1.27) (xy 0 -1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "GND_1_1" + (pin power_in line + (at 0 0 270) + (length 0) + (hide yes) + (name "GND" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (number "1" + (effects + (font + (size 1.27 1.27) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "ecc83-pp:PWR_FLAG" + (power) + (pin_numbers + (hide yes) + ) + (pin_names + (offset 0) + (hide yes) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "#FLG" + (at 0 2.413 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Value" "PWR_FLAG" + (at 0 4.572 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at 0 0 0) + (effects + (font + (size 1.524 1.524) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (effects + (font + (size 1.524 1.524) + ) + ) + ) + (property "Description" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "PWR_FLAG_0_0" + (pin power_out line + (at 0 0 90) + (length 0) + (name "pwr" + (effects + (font + (size 0.508 0.508) + ) + ) + ) + (number "1" + (effects + (font + (size 0.508 0.508) + ) + ) + ) + ) + ) + (symbol "PWR_FLAG_0_1" + (polyline + (pts + (xy 0 0) (xy 0 1.27) (xy -1.905 2.54) (xy 0 3.81) (xy 1.905 2.54) (xy 0 1.27) + ) + (stroke + (width 0) + (type default) + ) + (fill + (type none) + ) + ) + ) + (embedded_fonts no) + ) + (symbol "ecc83-pp:R" + (pin_numbers + (hide yes) + ) + (pin_names + (offset 0) + ) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (property "Reference" "R" + (at 2.032 0 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "R" + (at 0 0 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "" + (at -1.778 0 90) + (effects + (font + (size 0.762 0.762) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (effects + (font + (size 0.762 0.762) + ) + ) + ) + (property "Description" "" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "ki_fp_filters" "R_* Resistor_*" + (at 0 0 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (symbol "R_0_1" + (rectangle + (start -1.016 -2.54) + (end 1.016 2.54) + (stroke + (width 0.254) + (type default) + ) + (fill + (type none) + ) + ) + ) + (symbol "R_1_1" + (pin passive line + (at 0 3.81 270) + (length 1.27) + (name "~" + (effects + (font + (size 1.524 1.524) + ) + ) + ) + (number "1" + (effects + (font + (size 1.524 1.524) + ) + ) + ) + ) + (pin passive line + (at 0 -3.81 90) + (length 1.27) + (name "~" + (effects + (font + (size 1.524 1.524) + ) + ) + ) + (number "2" + (effects + (font + (size 1.524 1.524) + ) + ) + ) + ) + ) + (embedded_fonts no) + ) + ) + (junction + (at 76.2 50.8) + (diameter 1.016) + (color 0 0 0 0) + (uuid "10ef5c17-5272-4e7f-89a5-ecf737e33e26") + ) + (junction + (at 185.42 76.2) + (diameter 1.016) + (color 0 0 0 0) + (uuid "28b3893d-db59-42a0-9c46-cef29fab5b3a") + ) + (junction + (at 86.36 50.8) + (diameter 1.016) + (color 0 0 0 0) + (uuid "5ff87081-c0a0-47ce-a04e-7b4396f38ef3") + ) + (junction + (at 60.96 88.9) + (diameter 1.016) + (color 0 0 0 0) + (uuid "75bb7da6-039f-40b8-ba94-99186b61d8b3") + ) + (junction + (at 144.78 107.95) + (diameter 1.016) + (color 0 0 0 0) + (uuid "da0797c6-d142-4a5b-b008-3d4d0d2550ff") + ) + (junction + (at 50.8 55.88) + (diameter 1.016) + (color 0 0 0 0) + (uuid "e98b95f3-e3ac-46a0-b2c5-310dc77f3348") + ) + (junction + (at 157.48 93.98) + (diameter 1.016) + (color 0 0 0 0) + (uuid "eb94e8ae-d252-44ad-9a8c-740b5c98d893") + ) + (junction + (at 157.48 76.2) + (diameter 1.016) + (color 0 0 0 0) + (uuid "ecb56094-6b3e-4511-801e-afbe298ca102") + ) + (no_connect + (at 154.94 177.8) + (uuid "07e6e92c-c8a8-42b9-a8a3-099796ec9615") + ) + (no_connect + (at 154.94 180.34) + (uuid "230887ce-f305-4883-86d7-72df3a1a2372") + ) + (no_connect + (at 154.94 185.42) + (uuid "4102cf29-cf4e-42c4-96cb-330b1b756d96") + ) + (no_connect + (at 154.94 182.88) + (uuid "517db1d3-5d33-4d54-a6be-6a1fc40799a4") + ) + (wire + (pts + (xy 185.42 76.2) (xy 198.12 76.2) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "06655b90-3a73-4865-8781-04208e869f47") + ) + (wire + (pts + (xy 198.12 81.28) (xy 195.58 81.28) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "10537025-ad57-4a68-a3ad-97783b6e906f") + ) + (wire + (pts + (xy 86.36 53.34) (xy 86.36 50.8) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "1a7e2049-1cef-423f-9044-882a24cd3080") + ) + (wire + (pts + (xy 195.58 81.28) (xy 195.58 83.82) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "1bfc9e1d-9730-4ad2-b53c-049bbdc4f2eb") + ) + (wire + (pts + (xy 138.43 116.84) (xy 138.43 113.03) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "31e24174-4181-446f-9f24-5cffc59c241d") + ) + (wire + (pts + (xy 144.78 107.95) (xy 144.78 123.19) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "3211000a-11bc-4174-b47e-920409d772ed") + ) + (wire + (pts + (xy 50.8 57.15) (xy 50.8 55.88) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "38870f5c-4bbe-464a-b4a4-7b080932d227") + ) + (wire + (pts + (xy 157.48 76.2) (xy 157.48 81.28) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "43ff79b9-e7f1-4ce8-8f5d-a3e328a0951b") + ) + (wire + (pts + (xy 149.86 64.77) (xy 149.86 93.98) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "492f5cda-ba4e-4ac1-9675-e6be96092062") + ) + (wire + (pts + (xy 185.42 76.2) (xy 185.42 81.28) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "58ca8983-4450-4fe2-8e2b-0e30d48402fa") + ) + (wire + (pts + (xy 86.36 50.8) (xy 160.02 50.8) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "5cb2363c-c774-4f15-8526-d847a6a0f944") + ) + (wire + (pts + (xy 50.8 50.8) (xy 76.2 50.8) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "67a136ce-9a43-4815-bf37-5a48e3c5ba88") + ) + (wire + (pts + (xy 171.45 76.2) (xy 157.48 76.2) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "6e244d34-c7f4-4b73-b2e4-7cd6a9e68746") + ) + (wire + (pts + (xy 138.43 113.03) (xy 135.89 113.03) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "7573f100-0025-455b-9269-cc3c743be43c") + ) + (wire + (pts + (xy 179.07 76.2) (xy 185.42 76.2) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "78e27532-fd91-4f5a-a2d8-e6fdf5d8048d") + ) + (wire + (pts + (xy 60.96 88.9) (xy 60.96 86.36) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "7b559062-4412-4d9e-9c3b-32723558239a") + ) + (wire + (pts + (xy 76.2 49.53) (xy 76.2 50.8) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "84963a83-b6da-4751-9910-f093c3a03bee") + ) + (wire + (pts + (xy 157.48 74.93) (xy 157.48 76.2) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "87a9b2c0-1c64-462f-bd1f-2bce76398152") + ) + (wire + (pts + (xy 63.5 93.98) (xy 50.8 93.98) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "8e3fe7d9-4894-46e6-9f30-60c30a67eb4d") + ) + (wire + (pts + (xy 86.36 62.23) (xy 86.36 60.96) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "93661ccf-845f-4e28-a2f3-792cd2d0f455") + ) + (wire + (pts + (xy 53.34 55.88) (xy 50.8 55.88) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "936bde2e-ed95-450c-9794-94948f30b67d") + ) + (wire + (pts + (xy 63.5 86.36) (xy 63.5 93.98) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "96d8a739-fd6e-4a71-af88-21b52a62647d") + ) + (wire + (pts + (xy 50.8 88.9) (xy 60.96 88.9) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "986dd788-f18e-4db3-a5a5-6a0a989a7f34") + ) + (wire + (pts + (xy 149.86 93.98) (xy 157.48 93.98) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "9b04edba-dede-4967-8f9b-38a56988a081") + ) + (wire + (pts + (xy 157.48 93.98) (xy 157.48 97.79) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "9edfba25-39d5-4b5e-a81e-c68bb365f583") + ) + (wire + (pts + (xy 135.89 107.95) (xy 144.78 107.95) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "ac0e9f32-a4a3-4405-b25f-0b2daa84fbf8") + ) + (wire + (pts + (xy 160.02 50.8) (xy 160.02 54.61) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "af9c8561-1608-47a7-8aa7-1d378b72d4c1") + ) + (wire + (pts + (xy 154.94 130.81) (xy 154.94 132.08) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "b7a0ea88-eaff-457a-afff-3c743cbd7651") + ) + (wire + (pts + (xy 152.4 64.77) (xy 149.86 64.77) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "b9ea24f5-9437-4489-8f03-fb33f32ab984") + ) + (wire + (pts + (xy 157.48 88.9) (xy 157.48 93.98) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "c3e30b89-963c-45be-9c7c-93532b58206c") + ) + (wire + (pts + (xy 144.78 130.81) (xy 144.78 132.08) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "dc9271ea-f970-4a19-a75f-7e6f5ed9c7b4") + ) + (wire + (pts + (xy 76.2 50.8) (xy 86.36 50.8) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "dd8fad86-da9c-4a52-b908-cd4a2e7bd87e") + ) + (wire + (pts + (xy 66.04 88.9) (xy 66.04 86.36) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "df86ea1e-527e-4c0f-b20e-3a0e986faf20") + ) + (wire + (pts + (xy 154.94 118.11) (xy 154.94 123.19) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "e35a19d0-80c7-4a83-8507-037b6833d50c") + ) + (wire + (pts + (xy 185.42 90.17) (xy 185.42 88.9) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "e9c31df6-6f42-45a2-8981-394de7ce2e5a") + ) + (wire + (pts + (xy 60.96 88.9) (xy 66.04 88.9) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "eb2a78d6-b4d5-4ef8-9a1a-3d9ed3db2fee") + ) + (wire + (pts + (xy 144.78 107.95) (xy 149.86 107.95) + ) + (stroke + (width 0) + (type solid) + ) + (uuid "eb6832ef-d0fe-479d-8f32-b253c6621b1a") + ) + (symbol + (lib_id "ecc83-pp:R") + (at 157.48 85.09 180) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-00004549f38a") + (property "Reference" "R1" + (at 154.94 85.09 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "1.5K" + (at 157.48 85.09 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal" + (at 159.512 85.0392 90) + (effects + (font + (size 0.254 0.254) + ) + ) + ) + (property "Datasheet" "" + (at 157.48 85.09 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Description" "" + (at 157.48 85.09 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "490a35ba-1ba2-44c5-adce-e27a045257ab") + ) + (pin "2" + (uuid "51355c82-d1cf-445b-b079-21c660dd8989") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "R1") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:R") + (at 154.94 127 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-00004549f39d") + (property "Reference" "R2" + (at 152.4 127 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "1.5K" + (at 154.94 127 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal" + (at 156.8196 127 90) + (effects + (font + (size 0.762 0.762) + ) + ) + ) + (property "Datasheet" "" + (at 154.94 127 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Description" "" + (at 154.94 127 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "d54c7402-b55e-42a6-b24e-c8231bb401fe") + ) + (pin "2" + (uuid "e7039a2b-a55a-45c3-8a0e-2abf3a015549") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "R2") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:R") + (at 144.78 127 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-00004549f3a2") + (property "Reference" "R4" + (at 142.24 125.73 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "47K" + (at 144.78 127 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal" + (at 146.7104 127.0508 90) + (effects + (font + (size 0.762 0.762) + ) + ) + ) + (property "Datasheet" "" + (at 144.78 127 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Description" "" + (at 144.78 127 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "c270f478-7e23-4604-bfdf-038c11b27bdf") + ) + (pin "2" + (uuid "0ad09d20-5006-493e-8278-f356e26f66f7") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "R4") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:R") + (at 185.42 85.09 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-00004549f3ad") + (property "Reference" "R3" + (at 182.88 85.09 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "100K" + (at 185.42 85.09 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P7.62mm_Horizontal" + (at 187.4266 85.09 90) + (effects + (font + (size 0.254 0.254) + ) + ) + ) + (property "Datasheet" "" + (at 185.42 85.09 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Description" "" + (at 185.42 85.09 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "39435a49-8935-4ddf-b360-6a015d358cfb") + ) + (pin "2" + (uuid "3f332333-6f91-4a13-8c53-eebb9573b799") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "R3") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:C") + (at 175.26 76.2 270) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-00004549f3be") + (property "Reference" "C2" + (at 175.26 72.39 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "680nF" + (at 175.26 80.01 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "Capacitor_THT:C_Disc_D4.7mm_W2.5mm_P5.00mm" + (at 175.26 81.28 90) + (effects + (font + (size 0.254 0.254) + ) + ) + ) + (property "Datasheet" "" + (at 175.26 76.2 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Description" "" + (at 175.26 76.2 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "eadc305d-a101-4c9c-94d9-1ecaf23352ac") + ) + (pin "2" + (uuid "44dabcab-6c7b-4e96-b86a-4fb6dd8f5b97") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "C2") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:CONN_2") + (at 127 110.49 180) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-00004549f464") + (property "Reference" "P1" + (at 127 105.41 0) + (effects + (font + (size 1.016 1.016) + ) + ) + ) + (property "Value" "IN" + (at 127 110.49 0) + (effects + (font + (size 1.016 1.016) + ) + ) + ) + (property "Footprint" "Footprints:Altech_AK300_1x02_P5.00mm_45-Degree" + (at 127 115.57 0) + (effects + (font + (size 0.381 0.381) + ) + ) + ) + (property "Datasheet" "" + (at 127 110.49 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Description" "" + (at 127 110.49 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "4e008eff-1d48-4641-80fd-f3bdec030729") + ) + (pin "2" + (uuid "7bc904bf-18da-4a24-be4f-4a6e9fd74076") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "P1") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:CONN_2") + (at 207.01 78.74 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-00004549f46c") + (property "Reference" "P2" + (at 207.01 73.66 0) + (effects + (font + (size 1.016 1.016) + ) + ) + ) + (property "Value" "OUT" + (at 208.28 78.74 90) + (effects + (font + (size 1.016 1.016) + ) + ) + ) + (property "Footprint" "Footprints:Altech_AK300_1x02_P5.00mm_45-Degree" + (at 207.01 83.82 0) + (effects + (font + (size 0.381 0.381) + ) + ) + ) + (property "Datasheet" "" + (at 207.01 78.74 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Description" "" + (at 207.01 78.74 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "5052135e-1b72-4faa-add1-da0cf01aa026") + ) + (pin "2" + (uuid "0a09657c-35de-4de7-9dfe-7d3a0d792dc3") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "P2") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:CONN_2") + (at 41.91 53.34 0) + (mirror y) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-00004549f4a5") + (property "Reference" "P3" + (at 41.91 48.26 0) + (effects + (font + (size 1.016 1.016) + ) + ) + ) + (property "Value" "POWER" + (at 40.64 53.34 90) + (effects + (font + (size 1.016 1.016) + ) + ) + ) + (property "Footprint" "Footprints:Altech_AK300_1x02_P5.00mm_45-Degree" + (at 41.91 58.42 0) + (effects + (font + (size 0.381 0.381) + ) + ) + ) + (property "Datasheet" "" + (at 41.91 53.34 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Description" "" + (at 41.91 53.34 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "54a114eb-3233-4517-9c5d-b1447c552b5c") + ) + (pin "2" + (uuid "357ce97b-c23c-459c-af19-1a501705cbb4") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "P3") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:CP") + (at 86.36 57.15 0) + (mirror y) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-00004549f4be") + (property "Reference" "C1" + (at 92.71 55.88 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Value" "10uF" + (at 93.98 58.42 0) + (effects + (font + (size 1.27 1.27) + ) + (justify left) + ) + ) + (property "Footprint" "Capacitor_THT:CP_Radial_D10.0mm_P5.00mm" + (at 91.44 59.69 0) + (effects + (font + (size 0.254 0.254) + ) + ) + ) + (property "Datasheet" "" + (at 86.36 57.15 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Description" "" + (at 86.36 57.15 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "6f18cb93-a67b-4a40-887d-5dbe82da9cf7") + ) + (pin "2" + (uuid "f905ad9c-2fd8-4770-9c67-d7f99b1c26af") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "C1") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:CONN_2") + (at 41.91 91.44 180) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-0000456a8acc") + (property "Reference" "P4" + (at 43.18 91.44 90) + (effects + (font + (size 1.016 1.016) + ) + ) + ) + (property "Value" "CONN_2" + (at 40.64 91.44 90) + (effects + (font + (size 1.016 1.016) + ) + ) + ) + (property "Footprint" "Footprints:Altech_AK300_1x02_P5.00mm_45-Degree" + (at 43.18 96.52 0) + (effects + (font + (size 0.381 0.381) + ) + ) + ) + (property "Datasheet" "" + (at 41.91 91.44 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Description" "" + (at 41.91 91.44 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "0b2cb6e8-aee2-426a-a772-436c4f8fd1de") + ) + (pin "2" + (uuid "f4c26ce8-3b51-4b5c-839b-1fa8af9cd764") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "P4") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:PWR_FLAG") + (at 53.34 55.88 270) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-0000457dbac0") + (property "Reference" "#FLG05" + (at 60.198 55.88 0) + (effects + (font + (size 0.762 0.762) + ) + (hide yes) + ) + ) + (property "Value" "PWR_FLAG" + (at 59.182 55.88 0) + (effects + (font + (size 0.762 0.762) + ) + ) + ) + (property "Footprint" "" + (at 53.34 55.88 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 53.34 55.88 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Description" "" + (at 53.34 55.88 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "8508cfae-2f39-4086-9b3c-8362f92ce148") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "#FLG05") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:GND") + (at 144.78 132.08 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-0000457dbaef") + (property "Reference" "#PWR04" + (at 144.78 132.08 0) + (effects + (font + (size 0.762 0.762) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 144.78 133.858 0) + (effects + (font + (size 0.762 0.762) + ) + (hide yes) + ) + ) + (property "Footprint" "" + (at 144.78 132.08 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 144.78 132.08 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Description" "" + (at 144.78 132.08 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "7b3ac387-2d8f-4fc2-98a0-74502639828d") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "#PWR04") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:GND") + (at 154.94 132.08 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-0000457dbaf1") + (property "Reference" "#PWR03" + (at 154.94 132.08 0) + (effects + (font + (size 0.762 0.762) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 154.94 133.858 0) + (effects + (font + (size 0.762 0.762) + ) + (hide yes) + ) + ) + (property "Footprint" "" + (at 154.94 132.08 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 154.94 132.08 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Description" "" + (at 154.94 132.08 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "1f35e760-d662-44a6-8970-5b1f4f0c5299") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "#PWR03") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:GND") + (at 185.42 90.17 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-0000457dbaf5") + (property "Reference" "#PWR02" + (at 185.42 90.17 0) + (effects + (font + (size 0.762 0.762) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 185.42 91.948 0) + (effects + (font + (size 0.762 0.762) + ) + (hide yes) + ) + ) + (property "Footprint" "" + (at 185.42 90.17 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 185.42 90.17 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Description" "" + (at 185.42 90.17 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "45409752-92eb-492b-8722-9657988f10c8") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "#PWR02") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:GND") + (at 195.58 83.82 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-0000457dbaf8") + (property "Reference" "#PWR01" + (at 195.58 83.82 0) + (effects + (font + (size 0.762 0.762) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 195.58 85.598 0) + (effects + (font + (size 0.762 0.762) + ) + (hide yes) + ) + ) + (property "Footprint" "" + (at 195.58 83.82 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 195.58 83.82 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Description" "" + (at 195.58 83.82 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "ffd8d9c0-407c-42f3-9854-4c5baebeb6bc") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "#PWR01") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:ECC83") + (at 160.02 64.77 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-000048b4f256") + (property "Reference" "U1" + (at 163.83 55.88 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "ECC83" + (at 153.67 72.39 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "Footprints:Valve_ECC-83-1" + (at 166.37 64.77 90) + (effects + (font + (size 0.762 0.762) + ) + ) + ) + (property "Datasheet" "" + (at 160.02 64.77 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Description" "" + (at 160.02 64.77 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "6" + (uuid "290267e7-4016-4193-aa9b-8e119f086579") + ) + (pin "7" + (uuid "57816012-691f-43c4-891a-0a9875701932") + ) + (pin "8" + (uuid "dfa2a830-6e8c-46c0-9986-7a866e30ad89") + ) + (pin "1" + (uuid "d071bc8d-0a42-4749-ab3b-bc91c0b12307") + ) + (pin "2" + (uuid "91389f1c-a07a-4e24-a880-42977189f2e3") + ) + (pin "3" + (uuid "5e94a278-17ab-494c-b84d-2518c4915ca9") + ) + (pin "4" + (uuid "d9fb4f9d-c0e3-4555-b79f-5f73479f12da") + ) + (pin "5" + (uuid "b87ff3e3-fb45-4272-93a9-dfdea2efee10") + ) + (pin "9" + (uuid "da18c681-21b7-4160-90d8-28ac0b07192c") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "U1") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:ECC83") + (at 157.48 107.95 0) + (unit 2) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-000048b4f263") + (property "Reference" "U1" + (at 161.29 99.06 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "ECC83" + (at 162.56 116.84 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "Footprints:Valve_ECC-83-1" + (at 162.56 118.11 0) + (effects + (font + (size 0.762 0.762) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 157.48 107.95 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Description" "" + (at 157.48 107.95 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "6" + (uuid "80ec201d-a52d-4fd2-ba38-1cdc1981bbfe") + ) + (pin "7" + (uuid "d54d8a7a-7cd2-40e2-af4a-a93696020627") + ) + (pin "8" + (uuid "8c3c98bc-57bf-4abd-86bc-7b589d000949") + ) + (pin "1" + (uuid "5ddcc9fb-67b8-4a28-ab44-71770d18fdc9") + ) + (pin "2" + (uuid "0644856d-0b21-4dec-9d81-e558c1bbc258") + ) + (pin "3" + (uuid "802841aa-9aa8-4683-9a8b-c79629d12511") + ) + (pin "4" + (uuid "137be269-0f98-42d1-91e1-2262f482b268") + ) + (pin "5" + (uuid "012226b2-d408-4aca-bdb4-5434ce0353d8") + ) + (pin "9" + (uuid "94a316aa-51cc-4d72-a077-b17481f80966") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "U1") + (unit 2) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:ECC83") + (at 63.5 74.93 0) + (unit 3) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-000048b4f266") + (property "Reference" "U1" + (at 63.5 63.5 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "ECC83" + (at 63.5 66.04 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "Footprints:Valve_ECC-83-1" + (at 57.15 74.93 90) + (effects + (font + (size 0.762 0.762) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 63.5 74.93 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Description" "" + (at 63.5 74.93 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "6" + (uuid "e1edffc8-a42d-4e92-819d-af672c5b448f") + ) + (pin "7" + (uuid "d47b30f2-1df2-4c12-a2e7-cd034702a671") + ) + (pin "8" + (uuid "d0c889d7-c38a-41ea-a23a-653d6aaec11d") + ) + (pin "1" + (uuid "6940fbb4-6bc6-403e-9a2e-59d49828a683") + ) + (pin "2" + (uuid "e7f8c376-bfb6-4458-bb95-ab4f9cd52db2") + ) + (pin "3" + (uuid "d780e109-c85e-4185-9956-dda4fde43edd") + ) + (pin "4" + (uuid "ff7c5f3b-943d-45e2-9097-0672973e1c9d") + ) + (pin "5" + (uuid "cd044e93-d16e-49c6-9c3f-ff2d37031cd6") + ) + (pin "9" + (uuid "9ca87f9c-db08-4bb2-b129-650dccc6a99d") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "U1") + (unit 3) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:GND") + (at 86.36 62.23 0) + (mirror y) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-000053b6f370") + (property "Reference" "#PWR06" + (at 86.36 62.23 0) + (effects + (font + (size 0.762 0.762) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 86.36 64.008 0) + (effects + (font + (size 0.762 0.762) + ) + (hide yes) + ) + ) + (property "Footprint" "" + (at 86.36 62.23 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 86.36 62.23 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Description" "" + (at 86.36 62.23 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "0925899f-4041-41ea-abb1-12963c2e5446") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "#PWR06") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:CONN_1") + (at 158.75 177.8 0) + (unit 1) + (exclude_from_sim yes) + (in_bom no) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-000054a5890a") + (property "Reference" "P5" + (at 160.782 177.8 0) + (effects + (font + (size 1.016 1.016) + ) + (justify left) + ) + ) + (property "Value" "MOUNTING_HOLE" + (at 158.75 176.403 0) + (effects + (font + (size 0.762 0.762) + ) + (hide yes) + ) + ) + (property "Footprint" "Footprints:MountingHole_3.2mm_M3_DIN965_Pad" + (at 158.75 177.8 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 158.75 177.8 0) + (effects + (font + (size 1.524 1.524) + ) + ) + ) + (property "Description" "" + (at 158.75 177.8 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "b47c1d61-902c-48ae-8802-734a54efb1a1") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "P5") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:CONN_1") + (at 158.75 180.34 0) + (unit 1) + (exclude_from_sim yes) + (in_bom no) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-000054a58c65") + (property "Reference" "P6" + (at 160.782 180.34 0) + (effects + (font + (size 1.016 1.016) + ) + (justify left) + ) + ) + (property "Value" "MOUNTING_HOLE" + (at 158.75 178.943 0) + (effects + (font + (size 0.762 0.762) + ) + (hide yes) + ) + ) + (property "Footprint" "Footprints:MountingHole_3.2mm_M3_DIN965_Pad" + (at 158.75 180.34 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 158.75 180.34 0) + (effects + (font + (size 1.524 1.524) + ) + ) + ) + (property "Description" "" + (at 158.75 180.34 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "f56a8913-ef96-4f3b-aa8b-1a13f8229c54") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "P6") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:CONN_1") + (at 158.75 182.88 0) + (unit 1) + (exclude_from_sim yes) + (in_bom no) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-000054a58c8a") + (property "Reference" "P7" + (at 160.782 182.88 0) + (effects + (font + (size 1.016 1.016) + ) + (justify left) + ) + ) + (property "Value" "MOUNTING_HOLE" + (at 158.75 181.483 0) + (effects + (font + (size 0.762 0.762) + ) + (hide yes) + ) + ) + (property "Footprint" "Footprints:MountingHole_3.2mm_M3_DIN965_Pad" + (at 158.75 182.88 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 158.75 182.88 0) + (effects + (font + (size 1.524 1.524) + ) + ) + ) + (property "Description" "" + (at 158.75 182.88 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "ca1e588f-0267-46c9-b6cd-66fd0b13056c") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "P7") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:CONN_1") + (at 158.75 185.42 0) + (unit 1) + (exclude_from_sim yes) + (in_bom no) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-000054a58ca3") + (property "Reference" "P8" + (at 160.782 185.42 0) + (effects + (font + (size 1.016 1.016) + ) + (justify left) + ) + ) + (property "Value" "MOUNTING_HOLE" + (at 158.75 184.023 0) + (effects + (font + (size 0.762 0.762) + ) + (hide yes) + ) + ) + (property "Footprint" "Footprints:MountingHole_3.2mm_M3_DIN965_Pad" + (at 158.75 185.42 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 158.75 185.42 0) + (effects + (font + (size 1.524 1.524) + ) + ) + ) + (property "Description" "" + (at 158.75 185.42 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "37f3c682-6c02-4995-a68d-ba2bc9fac1ab") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "P8") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:PWR_FLAG") + (at 76.2 49.53 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-0000550ea992") + (property "Reference" "#FLG07" + (at 76.2 42.672 0) + (effects + (font + (size 0.762 0.762) + ) + (hide yes) + ) + ) + (property "Value" "PWR_FLAG" + (at 76.2 43.688 0) + (effects + (font + (size 0.762 0.762) + ) + ) + ) + (property "Footprint" "" + (at 76.2 49.53 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 76.2 49.53 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Description" "" + (at 76.2 49.53 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "b1b98e40-b69b-4b60-8486-e01c25c7ae90") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "#FLG07") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:GND") + (at 50.8 57.15 0) + (mirror y) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-0000550eab37") + (property "Reference" "#PWR08" + (at 50.8 57.15 0) + (effects + (font + (size 0.762 0.762) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 50.8 58.928 0) + (effects + (font + (size 0.762 0.762) + ) + (hide yes) + ) + ) + (property "Footprint" "" + (at 50.8 57.15 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 50.8 57.15 0) + (effects + (font + (size 1.524 1.524) + ) + (hide yes) + ) + ) + (property "Description" "" + (at 50.8 57.15 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "2441dfd7-a2b0-441b-ab95-dc80d3bfb10c") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "#PWR08") + (unit 1) + ) + ) + ) + ) + (symbol + (lib_id "ecc83-pp:GND") + (at 138.43 116.84 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "00000000-0000-0000-0000-0000550eaf5a") + (property "Reference" "#PWR09" + (at 138.43 116.84 0) + (effects + (font + (size 0.762 0.762) + ) + (hide yes) + ) + ) + (property "Value" "GND" + (at 138.43 118.618 0) + (effects + (font + (size 0.762 0.762) + ) + (hide yes) + ) + ) + (property "Footprint" "" + (at 138.43 116.84 0) + (effects + (font + (size 1.524 1.524) + ) + ) + ) + (property "Datasheet" "" + (at 138.43 116.84 0) + (effects + (font + (size 1.524 1.524) + ) + ) + ) + (property "Description" "" + (at 138.43 116.84 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "a8c7d88f-5a85-486a-8858-7ed63775f7db") + ) + (instances + (project "ecc83-pp" + (path "/28f865a0-4433-4a53-bbd7-b62f276848e4" + (reference "#PWR09") + (unit 1) + ) + ) + ) + ) + (sheet_instances + (path "/" + (page "1") + ) + ) + (embedded_fonts no) +) diff --git a/tool-directory.md b/tool-directory.md index ccddf4a0..a278dedf 100644 --- a/tool-directory.md +++ b/tool-directory.md @@ -68,20 +68,20 @@ Six tools, grouped into *discovery/routing* and *observability*. | `create_schematic` | Create a new blank `.kicad_sch` schematic file, on A4 unless another paper size is given. Use `set_schematic_page` to change it later. | | `set_schematic_page` | Set the sheet's paper size (A0–A5, A–E, US Letter/Legal/Ledger) and orientation. Returns the size in mm — content outside the frame still exports and still nets up, so a too-small page is a silent defect. | | `add_schematic_component` | Add a symbol from a KiCAD library to the schematic. Snaps to the 1.27mm grid. | -| `delete_schematic_component` | Remove a symbol instance from the schematic by its reference designator. | -| `edit_schematic_component` | Update fields (Reference, Value, Footprint, custom properties) of a symbol instance. | -| `get_schematic_component` | Get all properties, position, and pin locations for a symbol instance. | +| `delete_schematic_component` | Remove a component and all of its placed units by reference designator. | +| `edit_schematic_component` | Update shared fields consistently across every placed unit of a component. | +| `get_schematic_component` | Get shared properties and every placed unit's position for a component. | | `list_schematic_components` | List all symbol instances with positions, values, footprints, and pin locations. | -| `move_schematic_component` | Move a symbol to a new position. Does NOT adjust connected wires. | -| `rotate_schematic_component` | Rotate a symbol by setting its absolute rotation angle (0/90/180/270). | +| `move_schematic_component` | Move the lowest-numbered unit to a new position and translate every other unit by the same delta. Does NOT adjust connected wires. | +| `rotate_schematic_component` | Set the lowest-numbered unit's absolute rotation and rotate every other unit by the same delta. | | `move_connected` | Move a symbol and stretch/shrink connected wire stubs to preserve connections. | | `move_region` | Move all symbols within a bounding box by a given offset. | | `annotate_schematic` | Run kicad-cli to auto-assign reference designators (`R?` → `R1`, `U?` → `U1`, etc.). | -| `get_schematic_pin_locations` | Get exact (X,Y) coordinates of every pin on a symbol, accounting for rotation/mirroring, plus each pin's `orientation_degrees` (the direction leading away from the body, 0 = east) and `length_mm`. | +| `get_schematic_pin_locations` | Get exact (X,Y) coordinates of every pin on every placed unit, accounting for rotation/mirroring, plus each pin's `orientation_degrees` and `length_mm`. | | `batch_get_schematic_pin_locations` | Get pin locations for multiple components in a single file read, with the same per-pin fields. | -| `add_component_annotation` | Add a custom property (annotation) to a symbol instance. | -| `group_components` | Add a group property to multiple components in the schematic. | -| `replace_component` | Replace a component's `lib_id` with a new library symbol (swap the component type). | +| `add_component_annotation` | Add or update a custom property across every placed unit of a component. | +| `group_components` | Add or update a group property across every placed unit of multiple components. | +| `replace_component` | Replace every placed unit's `lib_id` while preserving and validating its unit number. | | `update_symbols_from_library` | Re-embed placed symbols' definitions from their libraries, like KiCad's "Update Symbols from Library". Refuses a symbol whose pins moved or disappeared (wires attach at pin coordinates) unless `allow_pin_moves` is set. | | `reset_schematic_field_positions` | Move each symbol's Reference and Value text back to its library anchor, through the symbol's rotation — KiCad's "Reset field text positions". Repairs sheets whose fields sit at a uniform offset. | | `get_schematic_view` | Render a sheet with kicad-cli and return the path to the SVG it wrote. There is no PNG — KiCad has no schematic rasteriser. The file lands in a temp directory; use `export_schematic_svg` to choose the location. |