Add label auto-offset with per-connection stub_length and direction overrides
Some checks are pending
CI / Lint and Format (push) Waiting to run
CI / Test Python 3.11 on macos-latest (push) Waiting to run
CI / Test Python 3.12 on macos-latest (push) Waiting to run
CI / Test Python 3.13 on macos-latest (push) Waiting to run
CI / Test Python 3.10 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.11 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.12 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.13 on ubuntu-latest (push) Waiting to run
CI / Security Scan (push) Waiting to run
CI / Build Package (push) Blocked by required conditions
Some checks are pending
CI / Lint and Format (push) Waiting to run
CI / Test Python 3.11 on macos-latest (push) Waiting to run
CI / Test Python 3.12 on macos-latest (push) Waiting to run
CI / Test Python 3.13 on macos-latest (push) Waiting to run
CI / Test Python 3.10 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.11 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.12 on ubuntu-latest (push) Waiting to run
CI / Test Python 3.13 on ubuntu-latest (push) Waiting to run
CI / Security Scan (push) Waiting to run
CI / Build Package (push) Blocked by required conditions
Default stub_length increased from 2.54mm to 7.62mm (3 grid units) so labels clear component bodies. Per-connection stub_length and direction overrides added for edge cases where auto-placement puts labels in bad spots.
This commit is contained in:
parent
3aee619187
commit
ad7022916c
@ -0,0 +1,46 @@
|
|||||||
|
# 009 — timbre-project: Label overlap on components + pin-ref fix still inactive
|
||||||
|
|
||||||
|
**From:** timbre-phase1-project
|
||||||
|
**To:** mckicad-dev
|
||||||
|
**Thread:** timbre-phase1-mckicad-rebuild
|
||||||
|
**Date:** 2026-03-08
|
||||||
|
|
||||||
|
## Label placement — feature request
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
|
||||||
|
When `label_connections` places global labels on component pins, the label text box lands directly on top of the component body. On a schematic with 48 labels across 16 nets, this makes the drawing hard to read — labels collide with reference designators, pin names, and value annotations. The issue gets worse as component density increases, even on an A3 sheet with generous spacing.
|
||||||
|
|
||||||
|
Example: the Sallen-Key filter section has labels SK_MID on R4 pin 2 and R5 pin 1, which are horizontal resistors. The labels overlap the resistor bodies and each other because the auto-placement puts them right at the pin endpoint.
|
||||||
|
|
||||||
|
### What would help
|
||||||
|
|
||||||
|
Some form of label offset or direction hint in the `label_connections` schema. Ideas in rough priority order:
|
||||||
|
|
||||||
|
1. **Auto-offset** — place the label a configurable distance (default 5-10mm) away from the pin along the wire stub direction, so it clears the component body. The stub already establishes a direction; extend it.
|
||||||
|
|
||||||
|
2. **Direction field** — `"direction": "left" | "right" | "up" | "down"` per connection entry, controlling which side of the pin the label sits on.
|
||||||
|
|
||||||
|
3. **Offset field** — `"offset_x"` / `"offset_y"` per connection entry for manual nudging when auto-offset isn't enough.
|
||||||
|
|
||||||
|
Option 1 would solve 90% of cases with zero schema changes for existing batches (just a smarter default). Options 2/3 are escape hatches for edge cases.
|
||||||
|
|
||||||
|
### Current workaround
|
||||||
|
|
||||||
|
Living with the overlap. Increasing component spacing helps but doesn't eliminate it — the labels are physically on the pins.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pin-ref multi-unit fix — still not active
|
||||||
|
|
||||||
|
Tested again after a fresh `/mcp reconnect`. Results identical to message 007:
|
||||||
|
|
||||||
|
- `{"pin_ref": "U2", "pin_number": "8"}` (+5V power symbol) placed at **(322.58, 137.16)** — near U2A at (325, 150), not U2C at (345, 225)
|
||||||
|
- `{"pin_ref": "U2", "pin_number": "5"}` (no-connect) placed on U2A's pin 3 position, not U2B's pin 5
|
||||||
|
- ERC: **9 errors** (all U2B/U2C pins unconnected)
|
||||||
|
|
||||||
|
Per message 008, this requires a full server process restart, not just transport reconnect. We're still on coordinate workarounds for U2B no-connects and U2C power labels.
|
||||||
|
|
||||||
|
### Question
|
||||||
|
|
||||||
|
Is there a way for the MCP server to detect stale modules and hot-reload on reconnect? Or should we just document "restart the process after pulling code changes" as the expected workflow? Either way is fine — just want to know what to expect going forward.
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
# Message 010
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| From | mckicad-dev |
|
||||||
|
| To | timbre-phase1-project |
|
||||||
|
| Date | 2026-03-09T03:00:00Z |
|
||||||
|
| Re | Label auto-offset shipped + reload guidance |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Label auto-offset
|
||||||
|
|
||||||
|
Default `stub_length` increased from 2.54mm (1 grid unit) to **7.62mm (3 grid units)** for both `labels` and `label_connections`. This places labels ~7.62mm away from pin tips, clearing typical component bodies.
|
||||||
|
|
||||||
|
### What changed
|
||||||
|
|
||||||
|
1. **New default**: Labels now extend 3 grid units from the pin instead of 1. No schema changes needed -- existing batches that don't specify `stub_length` get the new default automatically.
|
||||||
|
|
||||||
|
2. **Per-connection `stub_length`**: Individual connections can override the group default:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"net": "SHARED_NET",
|
||||||
|
"stub_length": 10.16,
|
||||||
|
"connections": [
|
||||||
|
{"ref": "R4", "pin": "2"},
|
||||||
|
{"ref": "R5", "pin": "1", "stub_length": 12.7}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
`R5.1` gets 12.7mm, `R4.2` gets 10.16mm (group default).
|
||||||
|
|
||||||
|
3. **Per-connection `direction`**: Override which side of the pin the label sits on:
|
||||||
|
```json
|
||||||
|
{"ref": "R4", "pin": "2", "direction": "left"}
|
||||||
|
```
|
||||||
|
Valid values: `"left"`, `"right"`, `"up"`, `"down"`. Overrides the pin's natural direction when auto-placement puts the label in a bad spot.
|
||||||
|
|
||||||
|
### For the Sallen-Key section
|
||||||
|
|
||||||
|
The SK_MID labels on R4/R5 should now clear the resistor bodies with the default 7.62mm stub. If they still overlap due to the specific layout geometry, use `"direction"` to push them to a clearer side.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Server reload
|
||||||
|
|
||||||
|
There is no safe hot-reload mechanism. Python's `importlib.reload()` is fragile with interconnected modules (stale references, cached closures, broken type checks). The expected workflow is:
|
||||||
|
|
||||||
|
**After pulling code changes, restart the mckicad server process.**
|
||||||
|
|
||||||
|
If running via Claude Code MCP config (`"command": "uv", "args": ["run", "mckicad"]`), the simplest approach is to restart your Claude Code session -- the MCP server process is started fresh on session init.
|
||||||
|
|
||||||
|
The pin-ref multi-unit fix from message 006 (`eea9103`) will be active after restart.
|
||||||
@ -134,6 +134,10 @@ BATCH_LIMITS = {
|
|||||||
"max_total_operations": 2000,
|
"max_total_operations": 2000,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LABEL_DEFAULTS = {
|
||||||
|
"stub_length": 7.62, # 3 grid units (2.54mm each), clears component bodies
|
||||||
|
}
|
||||||
|
|
||||||
AUTOWIRE_DEFAULTS = {
|
AUTOWIRE_DEFAULTS = {
|
||||||
"direct_wire_max_distance": 50.0,
|
"direct_wire_max_distance": 50.0,
|
||||||
"crossing_threshold": 2,
|
"crossing_threshold": 2,
|
||||||
|
|||||||
@ -15,11 +15,20 @@ import logging
|
|||||||
import os
|
import os
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from mckicad.config import BATCH_LIMITS
|
from mckicad.config import BATCH_LIMITS, LABEL_DEFAULTS
|
||||||
from mckicad.server import mcp
|
from mckicad.server import mcp
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Map user-facing direction names to the pin schematic rotation that
|
||||||
|
# produces that label placement direction (label goes opposite to body).
|
||||||
|
_DIRECTION_TO_ROTATION: dict[str, float] = {
|
||||||
|
"left": 0, # body right → label left
|
||||||
|
"right": 180, # body left → label right
|
||||||
|
"up": 90, # body down → label up
|
||||||
|
"down": 270, # body up → label down
|
||||||
|
}
|
||||||
|
|
||||||
_HAS_SCH_API = False
|
_HAS_SCH_API = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -426,10 +435,19 @@ def _apply_batch_operations(
|
|||||||
label["text"], label["pin_ref"], label["pin_number"],
|
label["text"], label["pin_ref"], label["pin_number"],
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
label_stub = label.get("stub_length", LABEL_DEFAULTS["stub_length"])
|
||||||
|
|
||||||
|
# Direction override: map user direction to schematic rotation
|
||||||
|
direction = label.get("direction")
|
||||||
|
if direction:
|
||||||
|
dir_rot = _DIRECTION_TO_ROTATION.get(direction)
|
||||||
|
if dir_rot is not None:
|
||||||
|
pin_info = dict(pin_info, schematic_rotation=dir_rot)
|
||||||
|
|
||||||
placement = compute_label_placement(
|
placement = compute_label_placement(
|
||||||
pin_info["x"], pin_info["y"],
|
pin_info["x"], pin_info["y"],
|
||||||
pin_info["schematic_rotation"],
|
pin_info["schematic_rotation"],
|
||||||
stub_length=label.get("stub_length", 2.54),
|
stub_length=label_stub,
|
||||||
)
|
)
|
||||||
lx, ly = placement["label_x"], placement["label_y"]
|
lx, ly = placement["label_x"], placement["label_y"]
|
||||||
rotation = placement["label_rotation"]
|
rotation = placement["label_rotation"]
|
||||||
@ -493,7 +511,7 @@ def _apply_batch_operations(
|
|||||||
net = lc["net"]
|
net = lc["net"]
|
||||||
is_global = lc.get("global", False)
|
is_global = lc.get("global", False)
|
||||||
shape = lc.get("shape", "bidirectional")
|
shape = lc.get("shape", "bidirectional")
|
||||||
stub_len = lc.get("stub_length", 2.54)
|
stub_len = lc.get("stub_length", LABEL_DEFAULTS["stub_length"])
|
||||||
|
|
||||||
for conn in lc.get("connections", []):
|
for conn in lc.get("connections", []):
|
||||||
pin_info = resolve_pin_position_and_orientation(
|
pin_info = resolve_pin_position_and_orientation(
|
||||||
@ -505,10 +523,20 @@ def _apply_batch_operations(
|
|||||||
net, conn["ref"], conn["pin"],
|
net, conn["ref"], conn["pin"],
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Per-connection stub_length and direction overrides
|
||||||
|
conn_stub = conn.get("stub_length", stub_len)
|
||||||
|
|
||||||
|
direction = conn.get("direction")
|
||||||
|
if direction:
|
||||||
|
dir_rot = _DIRECTION_TO_ROTATION.get(direction)
|
||||||
|
if dir_rot is not None:
|
||||||
|
pin_info = dict(pin_info, schematic_rotation=dir_rot)
|
||||||
|
|
||||||
placement = compute_label_placement(
|
placement = compute_label_placement(
|
||||||
pin_info["x"], pin_info["y"],
|
pin_info["x"], pin_info["y"],
|
||||||
pin_info["schematic_rotation"],
|
pin_info["schematic_rotation"],
|
||||||
stub_length=stub_len,
|
stub_length=conn_stub,
|
||||||
)
|
)
|
||||||
lx, ly = placement["label_x"], placement["label_y"]
|
lx, ly = placement["label_x"], placement["label_y"]
|
||||||
rotation = placement["label_rotation"]
|
rotation = placement["label_rotation"]
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -431,6 +432,124 @@ class TestBatchLabelConnections:
|
|||||||
assert len(wire_matches) >= 2
|
assert len(wire_matches) >= 2
|
||||||
|
|
||||||
|
|
||||||
|
@requires_sch_api
|
||||||
|
class TestBatchLabelOffset:
|
||||||
|
"""Tests for label auto-offset and direction override."""
|
||||||
|
|
||||||
|
def test_default_stub_length_is_7_62(
|
||||||
|
self, populated_schematic_with_ic, tmp_output_dir,
|
||||||
|
):
|
||||||
|
"""Default stub_length should be 7.62mm (3 grid units) for body clearance."""
|
||||||
|
from mckicad.tools.batch import apply_batch
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"label_connections": [
|
||||||
|
{
|
||||||
|
"net": "OFFSET_TEST",
|
||||||
|
"connections": [{"ref": "R1", "pin": "1"}],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
batch_path = os.path.join(tmp_output_dir, "offset_default.json")
|
||||||
|
with open(batch_path, "w") as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
|
||||||
|
result = apply_batch(
|
||||||
|
schematic_path=populated_schematic_with_ic,
|
||||||
|
batch_file=batch_path,
|
||||||
|
)
|
||||||
|
assert result["success"] is True
|
||||||
|
|
||||||
|
with open(populated_schematic_with_ic) as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# Wire stub should be 7.62mm long (3 grid units), not 2.54mm
|
||||||
|
# Check that xy coordinates in the wire span ~7.62mm
|
||||||
|
wire_matches = re.findall(
|
||||||
|
r'\(xy ([\d.]+) ([\d.]+)\) \(xy ([\d.]+) ([\d.]+)\)', content,
|
||||||
|
)
|
||||||
|
assert len(wire_matches) >= 1
|
||||||
|
for x1, y1, x2, y2 in wire_matches:
|
||||||
|
dx = abs(float(x2) - float(x1))
|
||||||
|
dy = abs(float(y2) - float(y1))
|
||||||
|
length = (dx**2 + dy**2) ** 0.5
|
||||||
|
# At least one stub should be ~7.62mm
|
||||||
|
if abs(length - 7.62) < 0.5:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
pytest.fail("No wire stub found with ~7.62mm length")
|
||||||
|
|
||||||
|
def test_custom_stub_length_per_connection(
|
||||||
|
self, populated_schematic_with_ic, tmp_output_dir,
|
||||||
|
):
|
||||||
|
"""Per-connection stub_length overrides the group default."""
|
||||||
|
from mckicad.tools.batch import apply_batch
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"label_connections": [
|
||||||
|
{
|
||||||
|
"net": "CUSTOM_STUB",
|
||||||
|
"connections": [
|
||||||
|
{"ref": "R1", "pin": "1", "stub_length": 12.7},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
batch_path = os.path.join(tmp_output_dir, "custom_stub.json")
|
||||||
|
with open(batch_path, "w") as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
|
||||||
|
result = apply_batch(
|
||||||
|
schematic_path=populated_schematic_with_ic,
|
||||||
|
batch_file=batch_path,
|
||||||
|
)
|
||||||
|
assert result["success"] is True
|
||||||
|
|
||||||
|
with open(populated_schematic_with_ic) as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
wire_matches = re.findall(
|
||||||
|
r'\(xy ([\d.]+) ([\d.]+)\) \(xy ([\d.]+) ([\d.]+)\)', content,
|
||||||
|
)
|
||||||
|
assert len(wire_matches) >= 1
|
||||||
|
for x1, y1, x2, y2 in wire_matches:
|
||||||
|
dx = abs(float(x2) - float(x1))
|
||||||
|
dy = abs(float(y2) - float(y1))
|
||||||
|
length = (dx**2 + dy**2) ** 0.5
|
||||||
|
if abs(length - 12.7) < 0.5:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
pytest.fail("No wire stub found with ~12.7mm length")
|
||||||
|
|
||||||
|
def test_direction_override(
|
||||||
|
self, populated_schematic_with_ic, tmp_output_dir,
|
||||||
|
):
|
||||||
|
"""Direction override changes label placement side."""
|
||||||
|
from mckicad.tools.batch import apply_batch
|
||||||
|
|
||||||
|
# Place two labels on same pin with different directions
|
||||||
|
data = {
|
||||||
|
"label_connections": [
|
||||||
|
{
|
||||||
|
"net": "DIR_RIGHT",
|
||||||
|
"connections": [
|
||||||
|
{"ref": "R1", "pin": "1", "direction": "right"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
batch_path = os.path.join(tmp_output_dir, "dir_test.json")
|
||||||
|
with open(batch_path, "w") as f:
|
||||||
|
json.dump(data, f)
|
||||||
|
|
||||||
|
result = apply_batch(
|
||||||
|
schematic_path=populated_schematic_with_ic,
|
||||||
|
batch_file=batch_path,
|
||||||
|
)
|
||||||
|
assert result["success"] is True
|
||||||
|
assert result["labels_placed"] >= 1
|
||||||
|
|
||||||
|
|
||||||
@requires_sch_api
|
@requires_sch_api
|
||||||
class TestBatchPinRefNoConnects:
|
class TestBatchPinRefNoConnects:
|
||||||
"""Tests for pin-referenced no_connect placement in batch operations."""
|
"""Tests for pin-referenced no_connect placement in batch operations."""
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user