Repository navigation
feat(schematic): place symbols mirrored - #485
Conversation
|
Ask first: make this PR's issue accounting explicit, but do not rebase yet. The placement-only increment is an acceptable focused first step. It does not need to add
This branch overlaps #483 and #489 in the schematic mutation/placement files and predates current |
|
Done, and no rebase — the branch is untouched, waiting on the refresh base and order you set after #442.
One thing in that plan worth your eye before I write it: the reason the mutation tool is not here is that |
|
#442 has landed as Nothing is needed from you yet: hold the rebase until #489 merges, then reconstruct once onto the @neusse — same note as on #489; override if you sequenced these differently. |
|
Queue status remains dependency-blocked; please do not rebase yet. #489 must land first because both PRs modify For the eventual reconstruction, please also correct the PR body so its dependency/series section reflects the actual #489 ordering, and add durable
|
|
#489 has landed as Refresh base:
Both of George's asks above stand; nothing further is requested before substantive review. |
Konnect models mirroring completely on the read side and could never write it. The model carries `mirror`, the parser reads `(mirror x|y)`, the serializer emits it in eeschema's own field order, geometry.rs implements the transform with eeschema's exact semantics, and list_schematic_components and get_schematic_pin_locations both report through it. What was missing was any assignment: `Symbol::mirror` was initialised to None and never set outside the parser, so a schematic authored through Konnect could not contain a mirrored symbol (mixelpixx#450). add_schematic_component and batch_place_components take an optional `mirror` of "x", "y" or "none", using the file format's own vocabulary rather than inventing one. An unknown axis is refused through invalid_arg instead of being dropped: a caller that misspells the axis is asking for a reflection, and placing the symbol unmirrored while reporting success is the failure the argument exists to end. In the batch, a bad axis refuses only its own entry, as every other per-item problem there already does. The placement's PinTransform takes the requested mirror rather than the hardcoded `mirror_x: false, mirror_y: false`, so Reference and Value follow a reflected body the way they already follow a rotated one (mixelpixx#101). Rotation 180 is not a substitute. It is exact for a single-input symbol but reverses the visual order of a multi-pin symbol's pins: a 4-input NAND drawn output-left renders its inputs 5,4,3,2 instead of 2,3,4,5. Pin numbers and the netlist stay correct; only the drawing is wrong, which matters when the deliverable is the reproduced drawing. Verified against KiCad 10.0.6, not only in-process: a 74HC00 placed with (mirror y) reports its output at the origin's left and its inputs at the right, (mirror x) swaps pins 1 and 2 in screen-Y, kicad-cli exports a netlist listing all three placements, and `kicad-cli sch upgrade` round-trips both tokens unchanged through KiCad's own writer. No new tool: sch_components is at MAX_TOOLS_PER_TOOLSET, and that constant asks for a conversation before it moves. Mirroring an already-placed symbol therefore still needs a delete and re-place; the pull request raises where such a tool should live. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two placement tests resolved Device:R through the installed KiCad
symbol libraries, so they passed on a developer machine with KiCad and
failed on every CI runner without one ("Library 'Device' not found").
The tests were the platform-dependent part, not the change.
They now place ecc83-pp:R, which the real eeschema fixture already
embeds; ensure_lib_symbol returns on an embedded symbol before it
consults any library source, so nothing outside the repository is
touched. Verified by running them with HOME and KICAD10_SYMBOL_DIR
pointed at an empty directory.
Placement preflights the project name against the file's instance paths,
which name ecc83-pp, so the fixture is written under that stem rather
than the shared helper's ecc83. The assertions are scoped to the symbol
each test places, since that fixture carries mirrored symbols of its own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The mirror reached the response but was never bound as placement intent, so nothing proved it landed. ComponentTargetUnit binds unit, lib_id, x, y, rotation, fields and instance paths, and verify_component_expectations checks each against the committed readback; mirror was reported from the file but not compared. A reflection that failed to write would still have returned success, which is the class of defect GOVERNANCE.md's house rule exists to prevent. ComponentTargetUnit now carries the axis. The placement constructor takes it as intent from the request. The from-source constructor records what the file already carries, so rotate, move and delete assert they left the reflection alone rather than failing on symbols that have one -- verified by moving and rotating mirrored symbols end to end. add_power_symbol binds None: a power symbol is placed upright and now says so. native_readback_intent_mismatch gains a "mirror" arm alongside rotation and the rest, and the guard was neutered to watch the new test fail. Not added: a "mirror" case in native_placement_readback_requires_requested_values. That matrix asserts only that the readback returns kind "stale_target", and its fixture trips "component UUID ... has a conflicting instance reference" before any field comparison runs -- a control arm where nothing differs at all still passes. Adding a mirror case there would have looked like coverage while proving nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Konnect's tool schemas are public API and API_MIGRATIONS.md is where an argument's contract is recorded; the PR body and the tool-directory row are not that durable record. Asked for on mixelpixx#489 and on mixelpixx#514's design acceptance in the same words, so this branch carries its own entry rather than waiting to be asked a third time. The entry covers what a caller needs: that `mirror` is optional on add_schematic_component and on each batch_place_components entry, that it speaks the file format's own "x"/"y"/"none", that omitting it keeps the previous behaviour exactly, why there is no both-axes spelling, that an unknown axis refuses the call (or only its own batch entry) rather than placing the symbol upright, that the field anchors follow the reflection, and that mirror_x/mirror_y are read from the reparsed committed schematic beside x, y and rotation and bound as intent, so a reflection that did not reach the file refuses with stale_target. No code change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
aa3514e to
16c9a0b
Compare
|
Pushed, reconstructed once onto the base you named. Head Four commits: the three mirror-placement commits the branch already carried when you set the sequencing, plus The migration entry is The
Dropping #489's Body corrected. The dependency/series section now records the real ordering — base All ten required checks are green on |
neusse
left a comment
There was a problem hiding this comment.
Exact-head review complete on 16c9a0b52e14efaa721cdaf0bbe6c117a519907a.
The reconstructed branch contains only the mirrored-placement increment on current main; the #489 conflict resolution preserves both field-placement and mirror readback. The optional mirror contract, serialized axis, transformed field anchors, committed-file intent verification, compatibility/migration record, real-KiCad evidence, and negative controls satisfy this PR's assigned half of #450. All ten required checks are green and there are no unresolved review threads.
Part of #450 is correct: the named successor owns mutation of an already-placed symbol and remains the terminal Closes #450 change. This exact head is ready for the user's merge authorization.
Summary
Symbols can be placed mirrored.
add_schematic_componentandbatch_place_componentstake an optionalmirrorof"x","y"or"none".Konnect already modelled mirroring completely on the read side and could never write it. The model carries
mirror, the parser reads(mirror x|y), the serializer emits it in eeschema's own field order,geometry.rsimplements the transform with eeschema's exact semantics, and bothlist_schematic_componentsandget_schematic_pin_locationsreport through it. What was missing was any assignment:Symbol::mirrorwas initialised toNoneand never set toSome(...)outside the parser, so a mirrored schematic round-tripped losslessly while one authored through Konnect could never contain a mirror.Part of #450
Acceptance mapping
mirrorargument onadd_schematic_componentandbatch_place_components, the axis written to the file, and the placement'sPinTransformcarrying it so field anchors follow a reflected body.feat/450-mirror-schematic-componentmirror_schematic_componentalongsiderotate_schematic_component— and thesch_componentstoolset-capacity question that adding a tool raises. That PR is the terminal change and will carryCloses #450.The successor plan is recorded on #450 itself so the issue stays the source of truth.
Approach
Three changes, no new geometry:
Symbol::set_mirror(Option<&str>)—Noneremoves the token, because eeschema records an unmirrored symbol by omitting it and has no(mirror none).mirror_argreads the argument in the file format's own vocabulary rather than inventing one. An unknown axis is refused throughinvalid_arg, not dropped: a caller that misspells the axis is asking for a reflection, and placing the symbol unmirrored while reporting success is exactly the failure this argument exists to end. In the batch a bad axis refuses only its own entry, as every other per-item problem in that loop already does.PinTransformtakes the requested mirror instead of the hardcodedmirror_x: false, mirror_y: false, so Reference and Value follow a reflected body the way they already follow a rotated one (Schematic property placement: use library anchors instead of hardcoded ±3.81, account for rotation #101).ComponentTargetUnitbinds the axis as placement intent alongsidex,yandrotation, andverify_component_expectationscompares it against the committed readback, so a reflection that failed to write cannot return success. The from-source constructor records the axis the file already carries, which makesrotate,moveanddeleteassert they left an existing reflection alone rather than fail on any symbol that has one.add_power_symbolbindsNone: a power symbol is placed upright and now says so.The axes are mutually exclusive, which is why the argument is one enum rather than two booleans: eeschema stores at most one
SYM_MIRROR_*flag, and mirroring about both axes is rotation by 180°, which belongs inat. The response keeps the existingmirror_x/mirror_ybooleans and now reports them from the committed readback, per unit and at the top level.Why rotation 180° is not a substitute. It is exact for a single-input symbol, but it reverses the visual order of a multi-pin symbol's pins: a 4-input NAND drawn output-left renders its inputs 5,4,3,2 instead of 2,3,4,5. Pin numbers and the netlist stay correct — only the drawing is wrong. That matters when the deliverable is the reproduced drawing, which is where this came from: transcribing hand-drawn sheets whose gates appear in all four rotations and mirrored.
One thing left out, and a question
I did not add a
mirror_schematic_componentalongsiderotate_schematic_component, although the issue proposes one and mirroring an already-placed symbol currently needs a delete and re-place.sch_componentsis atMAX_TOOLS_PER_TOOLSET(20), and that constant says "don't raise this number without a conversation". Rather than raise it, split the toolset, or file the tool somewhere it does not belong, this PR stops at the placement arguments — one reviewable outcome, no tool-count churn, notool-directory.mdor registry changes.Where would you like it to live? I am happy to follow up with the mutation tool once that is settled.
Branch and dependencies
Base branch:
main. Reconstructed once onto4a5dd41e470c9b2a12ce56457d7d2240fe7e80d8, the merge of #489 and currentmainat the time of the push.Depends on: #489, now merged. This PR was held behind it on the maintainers' sequencing because both change
sch_components.rs. It carries none of #489's commits — only its own mirror-placement work on top of them.Series order: #489 → this PR → #499, the order set on 2026-09-09. After this one, the successor
feat/450-mirror-schematic-componentowns mutation of an already-placed symbol and is the terminalCloses #450.Unique commits: four.
feat(schematic): place symbols mirroredmirrorargument,Symbol::set_mirror, and the placementPinTransformcarrying the axistest(schematic): place mirror fixtures from the file's own lib_symbolsfix(schematic): bind the placement mirror as verified intentdocs(api): record the placement mirror contractdocs/API_MIGRATIONS.mdentry asked for on 2026-09-10Next PR to promote after this one: #499, which writes pre-commit validation against the final shape of these handlers.
Compatibility and safety
mirrorproperty on two existing tool schemas. Omitting it is exactly the previous behaviour, and every existing call keeps its meaning.mirror_x/mirror_ynow appear on the component mutation readback, per unit and at the top level. Both names already exist elsewhere in the API (list_schematic_components,get_schematic_component) and carry the same meaning here. Nothing was renamed or removed.(mirror x|y)token on the placed symbol, in the position the serializer already emitted it from a parsed file.docs/API_MIGRATIONS.mdgains## Unreleased: mirrored schematic placement (minor release), inserted above the existing entries and preserving every one now onmain— the file is byte-identical tomainonce the new entry is removed. It records the accepted values, that omittingmirroris exactly the previous behaviour, why there is no both-axes spelling, what an unknown axis refuses, and thatmirror_x/mirror_yare read from the reparsed committed file and bound as intent.tool_count,tool-directory.md, DEV.md or README count changes.cargo xtask fix-doc-counts --checkreports documentation already current.Validation
The exact head under review is
16c9a0b52e14efaa721cdaf0bbe6c117a519907a, on base4a5dd41. The four commands above were also re-run underenv -iwith an emptyHOMEandKICAD10_SYMBOL_DIR/KICAD10_FOOTPRINT_DIRunset — a bare runner in one command — and pass there too.All ten required checks pass on the exact head under review,
16c9a0b52e14efaa721cdaf0bbe6c117a519907a, on macOS, Ubuntu and Windows.Every new guard was neutered and watched to fail before being trusted:
mirror_argaccepts any axisplacing_with_an_unknown_mirror_axis_refuses_without_writingset_mirrorplacing_mirrored_reflects_the_body_and_its_field_anchorsPinTransformback to hardcodedfalseplacing_mirrored_reflects_the_body_and_its_field_anchorsmirrorbatch_place_components_mirrors_per_component_and_refuses_a_bad_axisbatch_place_components_mirrors_per_component_and_refuses_a_bad_axisnative_readback_intent_mirrorRe-neutered after the reconstruction, because the merge with #489 rewrote the block both changes touch:
mirror_x/mirror_yforced tofalseincomponent_mutation_readback_from_schematicnative_readback_intent_mirror,placing_mirrored_reflects_the_body_and_its_field_anchors,batch_place_components_mirrors_per_component_and_refuses_a_bad_axis, and #489'sfield_placement_moves_the_text_and_keeps_its_value,field_placement_hides_then_shows_a_field,field_placement_never_rewrites_a_value_that_looks_like_a_placementpost-write mirror differs from bound intent for UUID …field_placementsinsert dropped from the same blockfield_placement_*testsThe second row is there on purpose: it proves the conflict resolution kept #489's half live rather than merely compiling beside it. The first row shows the binding reaches beyond the mirror tests — with the observed axis lost, every mutation on a symbol the fixture already mirrors refuses, which is the guard working.
Two things worth flagging rather than burying.
A first push failed CI on Linux and Windows and the tests were the platform-dependent part, not the change. They resolved
Device:Rthrough the installed KiCad symbol libraries, so they passed on a machine with KiCad and failed on every runner without one. They now placeecc83-pp:R, which the real eeschema fixture already embeds —ensure_lib_symbolreturns on an embedded symbol before consulting any library source. Confirmed by running them withHOMEandKICAD10_SYMBOL_DIRpointed at an empty directory.I did not add a
"mirror"case tonative_placement_readback_requires_requested_values. I tried, then neutered the guard and watched the test pass anyway. That matrix asserts only that the readback returnskind: "stale_target", and its fixture tripscomponent UUID … has a conflicting instance referencebefore any field comparison runs — a control arm where nothing differs also passes. So it is currently vacuous forunit,lib_id,x,y,rotationandValuetoo. I left it alone rather than widen this PR, and did not add a case that would have looked like coverage while proving nothing. Happy to open a separate issue if that is useful.The field-anchor test was wrong on its first pass and is worth flagging: it compared the two placements' raw Reference coordinates against the 40 mm the caller asked for, but the origins snap to the 1.27 mm grid and land 39.37 mm apart, so the assertion passed with the transform neutered. It now compares each anchor against its own origin and asserts the reflection exactly negates it —
+2.032unmirrored,−2.032with(mirror y).Verified against real KiCad (10.0.6), not only in-process. A
74xx:74HC00placed three ways, then read back:mirror: "y"mirror: "x"which is
SYM_MIRROR_Ynegating screen-X andSYM_MIRROR_Xnegating screen-Y, asgeometry.rsdocuments. Then:kicad-cli sch export netlistloads the file and lists all three placements;kicad-cli sch upgrade --force— KiCad's own writer — round-trips both(mirror x)and(mirror y)unchanged;mirror: "none"writes no token at all.The clearing path was also exercised against
tests/fixtures/ecc83_multiunit.kicad_sch, a real eeschema save whoseP3andC1are genuinely mirrored, during development of the mutation tool that this PR ultimately left out.Not run: Windows and Linux; no environment here. Nothing in the change is platform-dependent — it is one token in the schematic writer — and hosted CI covers the build and test matrix.
Review checklist
upstream/main, has no merge conflicts, and CI passed on this exact head (all ten checks green on16c9a0b).upstream/main, not a release tag.docs/NAMING_CONVENTIONS.md; no public renames.docs/API_MIGRATIONS.md.