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,
|
||||
}
|
||||
|
||||
LABEL_DEFAULTS = {
|
||||
"stub_length": 7.62, # 3 grid units (2.54mm each), clears component bodies
|
||||
}
|
||||
|
||||
AUTOWIRE_DEFAULTS = {
|
||||
"direct_wire_max_distance": 50.0,
|
||||
"crossing_threshold": 2,
|
||||
|
||||
@ -15,11 +15,20 @@ import logging
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from mckicad.config import BATCH_LIMITS
|
||||
from mckicad.config import BATCH_LIMITS, LABEL_DEFAULTS
|
||||
from mckicad.server import mcp
|
||||
|
||||
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
|
||||
|
||||
try:
|
||||
@ -426,10 +435,19 @@ def _apply_batch_operations(
|
||||
label["text"], label["pin_ref"], label["pin_number"],
|
||||
)
|
||||
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(
|
||||
pin_info["x"], pin_info["y"],
|
||||
pin_info["schematic_rotation"],
|
||||
stub_length=label.get("stub_length", 2.54),
|
||||
stub_length=label_stub,
|
||||
)
|
||||
lx, ly = placement["label_x"], placement["label_y"]
|
||||
rotation = placement["label_rotation"]
|
||||
@ -493,7 +511,7 @@ def _apply_batch_operations(
|
||||
net = lc["net"]
|
||||
is_global = lc.get("global", False)
|
||||
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", []):
|
||||
pin_info = resolve_pin_position_and_orientation(
|
||||
@ -505,10 +523,20 @@ def _apply_batch_operations(
|
||||
net, conn["ref"], conn["pin"],
|
||||
)
|
||||
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(
|
||||
pin_info["x"], pin_info["y"],
|
||||
pin_info["schematic_rotation"],
|
||||
stub_length=stub_len,
|
||||
stub_length=conn_stub,
|
||||
)
|
||||
lx, ly = placement["label_x"], placement["label_y"]
|
||||
rotation = placement["label_rotation"]
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
@ -431,6 +432,124 @@ class TestBatchLabelConnections:
|
||||
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
|
||||
class TestBatchPinRefNoConnects:
|
||||
"""Tests for pin-referenced no_connect placement in batch operations."""
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user