Port the sketch layer work from snaporca: offset chains, right-click, MCP verbs

Carries snaporca 971320e129, 6b049f0dc6, 4aae782029, 444d59f212, 74cf3d7e54 and the build
guards from 597557a6e4. Parity re-verified after every hunk: 17 files identical, 8 diverging by
their expected counts — DesignPanel.cpp still 32, DesignCanvas.cpp still 16, which is the proof
each hunk landed on the right side rather than being copied over a real divergence.

OFFSET OFFSETS THE CHAIN. Per-entity offsetting returned a closed rectangle as four parallel
segments that no longer touch, so entities_to_wires gave four OPEN wires and nothing could be
extruded. offset_entities now chains by shared endpoints and repairs each seam by mitering the
neighbours to their intersection. Second bug, invisible to any single-entity test: +d meant
"left of travel" for a line but "radius + d" for an arc regardless of sweep, so a slot outline
offset with its straights going one way and its caps the other. The convention is now written on
the declaration and pinned by a test.

tests/libslic3r/test_sketchprofile.cpp is new and asserts the LOOP rather than coordinates —
the property that decides whether a profile can be built, and the one the existing single-entity
[SketchEdit] cases cannot see. Its include is catch2/catch_all.hpp here: this fork ships Catch2
v3 while snaporca is on v2, which is why the test files are a tolerated divergence.

RIGHT-CLICK PICKS WHAT YOU POINTED AT, so a line's own verbs are offered instead of the
empty-selection vocabulary; sk_delete stops sharing btn:delete with the feature tree; and an
element's defining number (length / radius / diameter / angle / distance) can be typed, from the
menu or from V.

TWELVE MCP SKETCH VERBS. The socket had ~40 verbs and none touched a sketch, so the 2D layer
could only be exercised by driving a GUI with synthetic clicks. sketch_describe reports each
closed loop, the loops it encloses as voids, exact areas, and where a chain is still open;
sketch_validate/sketch_heal are FreeCAD's ValidateSketch — find vertices that overlap within a
tolerance but carry no coincidence, then weld them AND record the constraint, so a loop closed
by floating-point luck becomes one closed by construction. scripts/mcp-sketch-smoke.py is the
loop that asserts all of it.

Kernel suite on this fork: all tests passed, 2677 assertions in 230 test cases. The GUI target
links against the rebuilt deps image (the wxInspector blockage is gone) and the binary carries
the new verbs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tommaso Bianchi
2026-08-22 09:00:26 +02:00
co-authored by Claude Opus 5
parent df45edb13d
commit 5d7fc8c545
17 changed files with 1313 additions and 50 deletions
+25 -1
View File
@@ -38,7 +38,31 @@ echo "REPO=$REPO IMAGE=$IMAGE BUILD_VOL=$BUILD_VOL"
# scripts/ likewise: build_linux.sh's packaging step sources scripts/appimage_lib_policy.sh,
# which the baked snaporca tree does not have, so a fully successful link still exited
# non-zero with "missing AppImage helper" and the binary check never ran.
# ---- OOM guard (2026-08-21) -------------------------------------------------------------
# Two of these builds ran at once on 2026-08-21, each with ninja -j$(nproc)=16: ~36 cc1plus
# holding 42 GB of a 62 GB box -> global OOM at 21:05, a 2h28m kill storm, ssh unreachable,
# lightdm destroyed. Neither build produced a single object. scripts/rig-build.sh grew the
# bounds first; every script that starts a compile needs the same three, or the guard is only
# as strong as the script you happened not to use.
# flock — the lock path is SHARED with rig-build.sh and the other fork on purpose, so
# concurrent builds serialise instead of summing.
# -j — bounded parallelism; ~1.17 GB per cc1plus was the measured average.
# --memory — the actual guarantee: a runaway build dies in its own cgroup instead of taking
# the host down. --memory-swap equal to --memory forbids swap, which is what made
# ssh hang.
JOBS="${JOBS:-12}"
MEM="${MEM:-40g}"
LOCK=/tmp/orca-rig-build.lock
exec 9>"$LOCK"
if ! flock -n 9; then
echo "another build holds $LOCK — waiting (this is the OOM guard, not a hang)"
flock 9
fi
docker run --rm \
--memory="$MEM" --memory-swap="$MEM" \
-v "$REPO/src":/OrcaSlicer/src \
-v "$REPO/resources":/OrcaSlicer/resources \
-v "$REPO/CMakeLists.txt":/OrcaSlicer/CMakeLists.txt \
@@ -48,7 +72,7 @@ docker run --rm \
-v "$REPO/scripts":/OrcaSlicer/scripts \
-v "$BUILD_VOL":/OrcaSlicer/build \
"$IMAGE" \
bash -lc 'cd /OrcaSlicer && ./build_linux.sh -sr'
bash -lc "cd /OrcaSlicer && ./build_linux.sh -sr -j $JOBS"
# src/CMakeLists.txt:151 renames the OrcaSlicer target's output to "orca-slicer" — not
# "snapmaker-orca", which is the other fork's binary name.
+25 -1
View File
@@ -87,7 +87,31 @@ fi
# tests/ is mounted too -- unlike docker-iter-build.sh, this script exists precisely to
# compile tests being edited. CMakeLists.txt and cmake/ carry the SLIC3R_CAD gate; taking
# them from the baked image instead leaves the gate off and the CAD symbols vanish.
# ---- OOM guard (2026-08-21) -------------------------------------------------------------
# Two of these builds ran at once on 2026-08-21, each with ninja -j$(nproc)=16: ~36 cc1plus
# holding 42 GB of a 62 GB box -> global OOM at 21:05, a 2h28m kill storm, ssh unreachable,
# lightdm destroyed. Neither build produced a single object. scripts/rig-build.sh grew the
# bounds first; every script that starts a compile needs the same three, or the guard is only
# as strong as the script you happened not to use.
# flock — the lock path is SHARED with rig-build.sh and the other fork on purpose, so
# concurrent builds serialise instead of summing.
# -j — bounded parallelism; ~1.17 GB per cc1plus was the measured average.
# --memory — the actual guarantee: a runaway build dies in its own cgroup instead of taking
# the host down. --memory-swap equal to --memory forbids swap, which is what made
# ssh hang.
JOBS="${JOBS:-12}"
MEM="${MEM:-40g}"
LOCK=/tmp/orca-rig-build.lock
exec 9>"$LOCK"
if ! flock -n 9; then
echo "another build holds $LOCK — waiting (this is the OOM guard, not a hang)"
flock 9
fi
docker run --rm \
--memory="$MEM" --memory-swap="$MEM" \
-v "$REPO/src":/OrcaSlicer/src \
-v "$REPO/tests":/OrcaSlicer/tests \
-v "$REPO/resources":/OrcaSlicer/resources \
@@ -121,5 +145,5 @@ docker run --rm \
# src/CMakeLists.txt:92 while nothing about the kernel had changed. Turning the block off is
# not a workaround for that one dependency; it is the kernel suite finally declaring what it
# actually needs, so the next GUI-side dependency upstream adds cannot break it either.
cmake --build build --config Release --target libslic3r_tests
cmake --build build --config Release --target libslic3r_tests -- -j$JOBS
./build/tests/libslic3r/Release/libslic3r_tests '$TAGS' --order decl"
+114
View File
@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""Autonomous 2D-sketch loop: drive the Design tab's sketch layer over the MCP socket and
assert the things that decide whether a profile is buildable.
WHY THIS EXISTS. The 2D layer used to be reachable only by clicking, so every question about it
("is this loop closed?", "did the offset survive?", "is the circle a void or a second body?")
cost a GUI session and a human. The socket verbs make each one a call, and this script is the
loop: build a known profile, ask the app what it thinks it has, compare against arithmetic.
RUN IT AGAINST A RUNNING APP:
SNAPORCA_MCP=/tmp/mcp.sock <binary> # launch with the socket enabled
python3 scripts/mcp-sketch-smoke.py [socket] # default /tmp/mcp.sock
Exit 0 = every assertion held. Anything else prints the first mismatch and stops.
"""
import json, math, socket, sys
SOCK = sys.argv[1] if len(sys.argv) > 1 else "/tmp/mcp.sock"
_n = 0
def call(method, **params):
global _n
_n += 1
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.settimeout(30)
s.connect(SOCK)
s.sendall((json.dumps({"jsonrpc": "2.0", "id": _n, "method": method,
"params": params}) + "\n").encode())
buf = b""
while b"\n" not in buf:
d = s.recv(65536)
if not d:
break
buf += d
r = json.loads(buf.decode().strip())
if "error" in r:
raise RuntimeError(f"{method}: {r['error']}")
return r["result"]
def near(a, b, tol=1e-6):
return abs(a - b) < tol
def check(cond, what):
if not cond:
print(f"FAIL: {what}", file=sys.stderr)
sys.exit(1)
print(f" ok {what}")
def areas(rep):
return sorted(round(l["area"], 6) for l in rep["closed_loops"])
print("1. a rectangle is one closed loop of exactly its own area")
try:
call("sketch_cancel")
except Exception:
pass
call("sketch_begin", plane="XY")
call("sketch_add", rect=[0, 0, 80, 50])
r = call("sketch_describe")
check(r["buildable"], "buildable")
check(areas(r) == [4000.0], f"one loop of 4000 mm^2 (got {areas(r)})")
print("2. a circle inside it is a VOID, not a second profile")
call("sketch_add", type="circle", center=[40, 25], radius=10)
r = call("sketch_describe")
outer = [l for l in r["closed_loops"] if near(l["area"], 4000.0)][0]
check(len(outer["holes"]) == 1, "the rectangle encloses exactly one void")
hole = r["closed_loops"][outer["holes"][0]]
check(near(hole["area"], math.pi * 100), f"the void is pi*r^2 (got {hole['area']})")
print("3. offsetting the outer loop inward keeps it CLOSED and exact")
call("sketch_select", entities=[0, 1, 2, 3])
call("sketch_offset", distance=5)
r = call("sketch_describe")
check(r["open_ends"] == [], "no open ends after the offset")
check(any(near(l["area"], 70 * 40) for l in r["closed_loops"]),
f"the offset loop is 70x40 (got {areas(r)})")
print("4. a gap is REPORTED with its coordinates, then healed into a constraint")
call("sketch_cancel")
call("sketch_begin", plane="XY")
call("sketch_add", entities=[
{"type": "line", "p0": [0, 0], "p1": [60, 0]},
{"type": "line", "p0": [60, 0], "p1": [60, 40]},
{"type": "line", "p0": [60, 40], "p1": [0, 40]},
{"type": "line", "p0": [0, 40], "p1": [0.4, 0]}, # 0.4 mm short of closing
])
r = call("sketch_validate", tolerance=1.0)
check(not r["buildable"], "a 0.4 mm gap makes the profile unbuildable")
check(len(r["open_ends"]) == 2, f"both free ends are named (got {r['open_ends']})")
dof_before = r["dof"]
r = call("sketch_heal", tolerance=1.0)
check(r["welded"] == 1, f"one pair welded (got {r['welded']})")
check(r["buildable"] and r["open_ends"] == [], "healed profile is buildable")
check(areas(r) == [2400.0], f"healed loop is 60x40 (got {areas(r)})")
check(r["dof"] < dof_before,
f"the weld recorded a real constraint: DoF {dof_before} -> {r['dof']}")
print("5. construction geometry is excluded from the profile")
call("sketch_select", entities=[0])
call("sketch_construction")
r = call("sketch_describe")
check(not r["buildable"], "turning one side into a guide opens the profile again")
call("sketch_construction")
r = call("sketch_describe")
check(r["buildable"], "turning it back closes it again")
call("sketch_cancel")
print("\nall sketch assertions held")