Sync cad-mainline with upstream main and carry the value-field + rename work on top

This commit is contained in:
Tommaso Bianchi
2026-09-10 11:01:24 +02:00
47 changed files with 1395 additions and 645 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ The verb in the name is the role:
| `check-sketch-engine-corpus.py` | The same ladder graded against a systematic sample of real drawings instead of shapes we chose. | Kernel + corpus |
| `check-gui-sketching.py` | The same profiles drawn the way a person draws them — synthetic mouse gestures and typed values. | Headless GUI |
| `check-gui-context-menu.py` | That right-click is the pivot of the design gesture, and adapts to what was clicked. | Headless GUI |
| `check-mcp-sketch.py` | The sketch layer driven over the MCP socket, asserting what decides whether a profile is buildable. | Headless GUI + `SNAPORCA_MCP` |
| `check-mcp-sketch.py` | The sketch layer driven over the MCP socket, asserting what decides whether a profile is buildable. | Headless GUI + `ORCA_CAD_MCP` |
**`run-kernel-tests.sh` is the only one CI can run.** The rest need a live
application with an OpenGL canvas and synthetic input, which hosted runners do not
+3 -3
View File
@@ -19,7 +19,7 @@
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# orcacad-deps, NOT snaporca-deps: see the note in run-kernel-tests.sh — the wrong image
# orcacad-deps, NOT snapmaker-deps: see the note in run-kernel-tests.sh — the wrong image
# fails at CMake configure, not at link time.
IMAGE="${IMAGE:-orcacad-deps}"
BUILD_VOL="${BUILD_VOL:-orcacad_buildcache}"
@@ -32,11 +32,11 @@ echo "REPO=$REPO IMAGE=$IMAGE BUILD_VOL=$BUILD_VOL"
# the build fails with "class GLCanvas3D has no member named set_design_sketch_tool".
#
# build_linux.sh must be mounted for the same reason, and here the stale copy is guaranteed
# wrong rather than merely risky: orcacad-deps is layered on snaporca-deps, so the baked script
# wrong rather than merely risky: orcacad-deps is layered on snapmaker-deps, so the baked script
# is the OTHER fork's and builds `--target Snapmaker_Orca`. This fork's target is `OrcaSlicer`,
# so without this mount configure succeeds and then ninja dies on "unknown target".
# 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
# which the baked Snapmaker 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) -------------------------------------------------------------
+2 -2
View File
@@ -2,7 +2,7 @@
# Rebuild the GUI binary the design rig launches — in a THROWAWAY container, writing into the
# same build-cache volume the rig's long-lived GUI container reads from.
#
# NEVER build inside the GUI container (snaporca-gui / orcacad-gui). Its baked /OrcaSlicer tree
# NEVER build inside the GUI container (snapmaker-gui / orcacad-gui). Its baked /OrcaSlicer tree
# is the Jun-13 Snapmaker-derived source, so a `cmake .` in there silently reconfigures the
# shared build dir as project(Snapmaker_Orca) and this fork's targets vanish. That is Trap 1 of
# five; all of them, with symptoms and exact recovery commands, are in docs/rig_build_traps.md.
@@ -21,7 +21,7 @@ REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# the wrong volume the two forks silently trade build artefacts.
PROJECT="$(sed -n 's/^project(\([A-Za-z_0-9]*\)).*/\1/p' "$REPO/CMakeLists.txt" | head -1)"
case "$PROJECT" in
Snapmaker_Orca) PREFIX=snaporca; BIN=snapmaker-orca ;;
Snapmaker_Orca) PREFIX=snapmaker; BIN=snapmaker-orca ;;
OrcaSlicer) PREFIX=orcacad; BIN=orca-slicer ;;
*) echo "FATAL: unrecognised project($PROJECT) in $REPO/CMakeLists.txt" >&2; exit 2 ;;
esac
+658
View File
@@ -0,0 +1,658 @@
#!/usr/bin/env python3
"""The click-edit contract: a value field that opens must accept what is TYPED into it.
WHY THIS EXISTS SEPARATELY FROM check-gui-sketching.py. That ladder draws geometry and grades the
result, and to make its values land it calls focus_field() — one synthetic click INTO the field
before typing. Its own docstring says why:
WITHOUT THIS THE TYPED VALUE IS SILENTLY DISCARDED. The field is shown and raised but the
window manager does not give it the keyboard, so xdotool's digits go to the canvas and Return
commits the value the field opened with — the pre-filled as-drawn number.
That click is a workaround for a defect, and a suite that performs it can never see the defect
again. A user cannot be told to click the field first; when they do not, they get the as-drawn
number and report "the label value is not editable". So this ladder types IMMEDIATELY after the
field opens, exactly as a person does, and fails if the prefill is what gets committed.
WHAT IT GRADES. The app emits one line per event under ORCA_CAD_UXTRACE=1:
[UX] open title=Length prefill=154.76
[UX] commit title=Length typed=80 value=80.0000
[UX] refused title=Length typed=8O
[UX] cancel title=Length
For every field the driver opens it asserts: a commit arrived, what the field received is what we
typed, the parsed value equals it, and it differs from the prefill. The last clause is the one
that matters — a field that is on screen but deaf commits its prefill, and every other signal
(the field is visible, a constraint is created, the solve succeeds) looks perfectly healthy.
scripts/CAD/check-gui-click-edit.py --display :10 --bin build/src/Release/orca-slicer
With --attach it drives an already-running app instead of launching one; the app must have been
started with ORCA_CAD_UXTRACE=1 and its stderr redirected to --trace.
Exit 0 = every field took what was typed.
"""
import argparse, json, os, re, shutil, signal, socket, subprocess, sys, tempfile, time
AP = argparse.ArgumentParser()
AP.add_argument("--display", default=os.environ.get("DISPLAY", ":10"))
AP.add_argument("--bin", default="build/src/Release/orca-slicer")
AP.add_argument("--datadir", default="")
AP.add_argument("--trace", default="")
AP.add_argument("--sock", default="/tmp/mcp-uxcheck.sock",
help="the app's MCP socket: the oracle for whether a sketch is really open")
AP.add_argument("--attach", action="store_true", help="drive a running app; do not launch one")
AP.add_argument("--keep", action="store_true", help="leave the app running afterwards")
AP.add_argument("--no-defocus", action="store_true",
help="do NOT take focus off the field before typing (weakens the gate; see below)")
AP.add_argument("--seed-from", default=os.path.expanduser("~/.config/OrcaCAD/OrcaSlicer.conf"),
help="an existing OrcaSlicer.conf to copy presets/settings from")
A = AP.parse_args()
DISP = A.display
TRACE = A.trace or os.path.join(tempfile.gettempdir(), "ux-click-edit.log")
_fail = 0
_checks = 0
_n = 0
def call(method, **params):
"""One MCP request over the app's unix socket. The socket is the only witness that cannot
lie about sketch state: the keytrace says a key ARRIVED, a screenshot says something is on
screen, and neither distinguishes an open sketch from sketch mode with the plane offer up."""
global _n
_n += 1
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.settimeout(30)
s.connect(A.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']['message']}")
return r["result"]
def try_call(method, **params):
try:
return call(method, **params)
except Exception:
return None
def sh(cmd):
# bash -c, NOT -lc: a login shell sources the profile on every xdotool call, and this driver
# makes hundreds. On a GNOME box that meant im-config running per call, thousands of journal
# lines, and a window poll slow enough to time out before the app had finished starting.
return subprocess.run(["bash", "-c", cmd], capture_output=True, text=True).stdout
def xdo(args):
sh(f"DISPLAY={DISP} xdotool {args}")
def key(k, pause=0.35, window=None):
xdo(f"key {'--window ' + str(window) + ' ' if window else ''}{k}")
time.sleep(pause)
def typ(s, pause=0.35, window=None):
# --clearmodifiers so a modifier left down by an earlier synthetic key cannot turn digits
# into something else; --delay 60 because ImGui reads one character per frame.
#
# `window` targets a specific window with XSendEvent instead of following the input focus.
# That is the whole gate: see type_into_open_field.
tgt = f"--window {window} " if window else ""
xdo(f"type {tgt}--clearmodifiers --delay 60 -- '{s}'")
time.sleep(pause)
def die(msg):
print(f"FATAL {msg}", file=sys.stderr)
sys.exit(2)
# ---------------------------------------------------------------- the app
_proc = None
def seed_datadir(datadir):
"""The Design tab does not exist unless enable_cad_feature is on, and it needs a RESTART.
A fresh datadir has it off, so a driver that just points the app at an empty directory gets
Prepare/Preview/Device/Project, no Design tab, and every rung fails for a reason that has
nothing to do with what is being tested. Seed the flag before the first launch.
"""
os.makedirs(datadir, exist_ok=True)
conf = os.path.join(datadir, "OrcaSlicer.conf")
data = {}
if os.path.exists(A.seed_from):
try:
with open(A.seed_from) as f:
data = json.load(f)
except Exception:
data = {}
app = data.setdefault("app", {})
app["enable_cad_feature"] = True
# Deterministic starting state for the rungs that follow: the bed drawn, loops welded as the
# ~90% case expects. A ladder whose result depends on the developer's own preferences is not
# a gate.
app["auto_close_sketch_loops"] = True
# SILENCE THE NETWORK PLUGIN PROMPT. Without this, GUI_App::post_init() re-raises "Bambu
# Network Plug-in Required" from an IDLE event — after any modal sweep this driver does at
# startup — and ShowModal() then runs a nested event loop. The app is alive, its window is
# there, and the MCP socket answers nothing: indistinguishable from a hang, and it was
# investigated as one, with gdb, twice. `installed_networking` false stops the whole
# networking-plugin path, so m_networking_need_update is never set and the dialog never
# exists to be swept.
app["installed_networking"] = False
with open(conf, "w") as f:
json.dump(data, f, indent=1)
for sub in ("user", "system", "presets", "vendor"):
src = os.path.join(os.path.dirname(A.seed_from), sub)
dst = os.path.join(datadir, sub)
if os.path.isdir(src) and not os.path.exists(dst):
shutil.copytree(src, dst)
def launch():
global _proc
datadir = A.datadir or os.path.join(tempfile.gettempdir(), "orcacad-uxcheck")
seed_datadir(datadir)
env = dict(os.environ)
# WAYLAND_DISPLAY MUST GO, and GDK_BACKEND must say x11. GTK prefers Wayland whenever
# WAYLAND_DISPLAY is set and ignores DISPLAY entirely, so a driver launched from a systemd
# user unit (which inherits it) started the app on the DESKTOP session instead of the rig:
# the process was alive, `xdotool search` on the rig display found nothing, and the window
# was sitting on the user's own screen. Silent, and it drives a stray app at someone's face.
env.pop("WAYLAND_DISPLAY", None)
env.update(DISPLAY=DISP, GDK_BACKEND="x11", ORCA_CAD_UXTRACE="1",
LIBGL_ALWAYS_SOFTWARE="1", GALLIUM_DRIVER="llvmpipe",
# The rig's Xvfb has no input-method daemon, and a dead ibus context makes a
# GtkEntry drop every character while the app looks fine. It cannot affect the
# in-canvas field (ImGui needs no IM) but the app has other text fields, and a
# display full of IBUS warnings has cost a whole misdiagnosis before.
GTK_IM_MODULE="gtk-im-context-simple", XMODIFIERS="@im=none",
# The key tracer is this driver's only positive signal that a keystroke reached
# the Design panel at all. Without it "the field never opened" is indistinguishable
# from "we never got into sketch mode", and the first run of this ladder reported
# seven product failures that were really one driver racing a still-loading app.
ORCA_CAD_KEYTRACE="1", ORCA_CAD_MCP=A.sock,
SSL_CERT_FILE="/etc/ssl/certs/ca-certificates.crt",
WEBKIT_DISABLE_DMABUF_RENDERER="1", WEBKIT_DISABLE_COMPOSITING_MODE="1")
if os.path.exists(A.sock):
os.unlink(A.sock) # a stale socket from a dead run answers nothing, slowly
log = open(TRACE, "wb")
_proc = subprocess.Popen([A.bin, "--datadir", datadir], env=env,
stdout=subprocess.DEVNULL, stderr=log)
for _ in range(120):
if win_id():
return
time.sleep(1)
die("the app never showed a window on " + DISP)
def window_pid(w):
"""_NET_WM_PID for a window, or 0. The property is how we tell a live app from its ghost."""
out = sh(f"DISPLAY={DISP} xprop -id {w} _NET_WM_PID 2>/dev/null")
m = re.search(r"= *(\d+)", out)
return int(m.group(1)) if m else 0
def pid_alive(pid):
return pid > 0 and os.path.isdir(f"/proc/{pid}")
def win_id():
"""The main window: OURS if we launched it, otherwise the biggest LIVE top-level.
Two rules here, each paid for.
By PID, not by size, whenever we launched the app. An X window outlives its client if the
connection is not torn down cleanly, and a killed OrcaSlicer can leave a full-screen ghost
mapped on the display. It answers geometry queries exactly like the real thing, it wins "the
biggest window" every time, and every synthetic keystroke sent to it goes nowhere. That is
indistinguishable, from the driver's side, from an app that ignores the keyboard — which is
the very defect this ladder exists to measure. One run reported the entire contract broken
while the real app sat beside the ghost, untouched.
Never by title: a saved project renames the main window.
"""
if _proc is not None:
for w in sh(f"DISPLAY={DISP} xdotool search --pid {_proc.pid} --onlyvisible --name '.'").split():
g = dict(l.split("=", 1) for l in
sh(f"DISPLAY={DISP} xdotool getwindowgeometry --shell {w}").strip().splitlines()
if "=" in l)
if "WIDTH" in g and int(g["WIDTH"]) * int(g["HEIGHT"]) > 400 * 400:
return (w, int(g["X"]), int(g["Y"]), int(g["WIDTH"]), int(g["HEIGHT"]))
return None
best = None
for w in sh(f"DISPLAY={DISP} xdotool search --onlyvisible --name '.'").split():
g = dict(l.split("=", 1) for l in
sh(f"DISPLAY={DISP} xdotool getwindowgeometry --shell {w}").strip().splitlines()
if "=" in l)
if "WIDTH" not in g:
continue
if not pid_alive(window_pid(w)): # a ghost: no client is behind it any more
continue
a = int(g["WIDTH"]) * int(g["HEIGHT"])
if a > 400 * 400 and (best is None or a > best[0]):
best = (a, w, int(g["X"]), int(g["Y"]), int(g["WIDTH"]), int(g["HEIGHT"]))
return best[1:] if best else None
_win = None
def win():
global _win
if _win is None:
w = win_id()
if w is None:
die("no app window on " + DISP)
sh(f"DISPLAY={DISP} xdotool windowactivate --sync {w[0]}")
sh(f"DISPLAY={DISP} xdotool windowsize {w[0]} 1920 1080")
sh(f"DISPLAY={DISP} xdotool windowmove {w[0]} 0 0")
time.sleep(1.0)
_win = (w[0], 0, 0, 1920, 1080)
return _win
def click(px, py, pause=0.5, btn=1):
_, X, Y, _, _ = win()
xdo(f"mousemove {X+int(px)} {Y+int(py)} click --delay 120 {btn}")
time.sleep(pause)
def visible_windows():
"""(id, name, w, h) for every MAPPED top-level, main window included.
`--onlyvisible` is what makes this usable. Without it xdotool also returns the app's unmapped
helper windows — a 10x10 and a 200x200 that exist for the whole session — and a caller that
tries to reason about "extra windows" from that list is reasoning about furniture.
"""
out = []
for w in sh(f"DISPLAY={DISP} xdotool search --onlyvisible --name '.'").split():
g = dict(l.split("=", 1) for l in
sh(f"DISPLAY={DISP} xdotool getwindowgeometry --shell {w}").strip().splitlines()
if "=" in l)
if "WIDTH" not in g:
continue
if not pid_alive(window_pid(w)): # see win_id(): a ghost cannot be closed, only ignored
continue
n = sh(f"DISPLAY={DISP} xdotool getwindowname {w}").strip()
out.append((w, n, int(g["WIDTH"]), int(g["HEIGHT"])))
return out
def dismiss_modals(timeout=30):
"""Close every modal over the main window, and PROVE none is left.
This is the rung that decides whether any of the others mean anything. A fresh datadir opens
"Bambu Network Plug-in Required" — 440x259, centred at 742,450 — which sits exactly on top of
the point every drawing gesture in TOOLS starts from. The whole ladder then reports eleven
product failures, all of them the driver clicking a dialog.
The old version pressed Escape and moved on. This dialog ignores Escape, so it "dismissed"
nothing and said so to no one; the run that found this was red for a reason that had nothing
to do with the contract under test. Escape is still tried first because it is the gentlest
thing that works on the wizard, then WM_DELETE_WINDOW, and then the function asserts what it
was supposed to have achieved instead of assuming it.
"""
# NEVER run without knowing which window to spare. The first version took `keep = main[0] if
# main else None`, so a win_id() that raced the app's mapping made keep None and every visible
# window a modal — this function then sent WM_DELETE to the app's own main window. The app
# survived as a process, printed "GdkWindow unexpectedly destroyed", and answered nothing
# afterwards; the ladder reported "no sketch opened" for 180s. Losing the main window is not a
# state to recover from silently.
deadline = time.time() + timeout
keep = None
while keep is None and time.time() < deadline:
main = win_id()
keep = main[0] if main else None
if keep is None:
time.sleep(0.5)
if keep is None:
die(f"no main window to protect after {timeout}s — refusing to close anything")
scr = sh(f"DISPLAY={DISP} xdotool getdisplaygeometry").split()
full = int(scr[0]) * int(scr[1]) if len(scr) == 2 else 1920 * 1080
while time.time() < deadline:
# A modal is small. Anything covering half the screen is the app, whatever id win_id()
# happened to return this instant — a second belt on the rule above, because the cost of
# being wrong here is an app that looks alive and answers nothing.
extra = [x for x in visible_windows() if x[0] != keep and x[2] * x[3] < full * 0.5]
if not extra:
return
for (w, n, _, _) in extra:
sh(f"DISPLAY={DISP} xdotool windowactivate {w}")
time.sleep(0.4)
key("Escape", 0.4)
if any(x[0] == w for x in visible_windows()):
sh(f"DISPLAY={DISP} xdotool windowclose {w}")
time.sleep(0.6)
time.sleep(0.5)
left = [f"{n!r} ({w}x{h})" for (i, n, w, h) in visible_windows()
if i != keep and w * h < full * 0.5]
die("a modal is still covering the canvas after " + str(timeout) + "s: " + ", ".join(left) +
" — every drawing gesture would land in it, so nothing below this line could be trusted")
def dismiss_first_run():
dismiss_modals()
# ---------------------------------------------------------------- the trace
def trace_lines():
try:
with open(TRACE, "r", errors="replace") as f:
return [l.strip() for l in f if l.startswith("[UX] ")]
except OSError:
return []
def trace_mark():
return len(trace_lines())
def parse(line):
m = re.match(r"\[UX\] (\w+) title=(.*?) (.*)$", line)
if not m:
return None
ev, title, rest = m.group(1), m.group(2), m.group(3)
kv = dict(re.findall(r"(\w+)=(\S*)", rest))
return ev, title, kv
# ---------------------------------------------------------------- grading
def check(cond, what):
"""Returns the verdict so a caller can abandon a rung whose precondition failed."""
global _fail, _checks
_checks += 1
if cond:
print(f" ok {what}")
else:
print(f" FAIL {what}", file=sys.stderr)
_fail += 1
return bool(cond)
def type_into_open_field(value, mark):
"""Type `value` into whatever field is open, WITHOUT clicking it first, and grade the pair.
No click: the click is the workaround this ladder exists to refuse. If the field cannot take
the keyboard on its own, `typed` will be the prefill and this fails — which is the report.
"""
# POLL for the field. It opens from a CallAfter that runs after a re-solve, so on llvmpipe it
# is simply not there yet when a fast driver looks — and "no field opened" is the same message
# whether the product never opened one or the driver asked too early. Wait, then decide.
opens = []
deadline = time.time() + 8.0
while time.time() < deadline:
opens = [e for e in (parse(l) for l in trace_lines()[mark:]) if e and e[0] == "open"]
if opens:
break
time.sleep(0.25)
if not opens:
check(False, f"a value field opened (nothing did; cannot type {value})")
return mark
title = opens[-1][1]
prefill = opens[-1][2].get("prefill", "")
m2 = trace_mark()
# TYPE NORMALLY. NOTHING TO DEFOCUS ANY MORE.
#
# The value field is drawn INSIDE the GL canvas by ImGui, so it is not a window: there is no
# second toplevel for a window manager to grant or refuse the keyboard, and the keystrokes go
# to the app's one window exactly as a person's would. That is the entire point of the design
# — the WM has no say — and it is why this ladder no longer tries to manufacture the failing
# condition.
#
# When the field WAS a floating wxFrame, this spot held two attempts to reproduce
# "field open, keyboard elsewhere", and both are recorded here so neither is tried again:
# - XSetInputFocus onto the main window (`xdotool windowfocus`): the field's own re-focus
# CallAfter wins the race every time; four retries all lost, and the ladder passed twice
# against a binary with the fix compiled out.
# - XSendEvent at the main window (`xdotool type --window`): GTK discards synthetic key
# events, so NEITHER build received anything and every run was red regardless of the code.
# A run that used the second of those is what produced "the app never saw a digit" — a
# property of xdotool, not of the product.
#
# For the in-canvas field the honest gate is simply: type, and see whether the value the app
# commits is the value that was typed.
diag = sh(f"DISPLAY={DISP} xdotool getwindowfocus").strip()
typ(str(value), 0.4)
key("Return", 0.9)
after, commits, refused, commit_at = [], [], [], None
deadline = time.time() + 5.0
while time.time() < deadline:
after = [parse(l) for l in trace_lines()[m2:]]
commits = [(i, e) for i, e in enumerate(after) if e and e[0] == "commit"]
refused = [e for e in after if e and e[0] == "refused"]
if commits:
commit_at = m2 + commits[-1][0]
commits = [e for _, e in commits]
if commits or refused:
break
time.sleep(0.25)
if refused and not commits:
check(False, f"{title}: field REFUSED {value!r} (typed={refused[-1][2].get('typed')!r})")
key("Escape", 0.5)
return trace_mark()
if not commits:
check(False, f"{title}: typed {value} but nothing committed — the field took no keys")
key("Escape", 0.5)
return trace_mark()
typed = commits[-1][2].get("typed", "")
got = commits[-1][2].get("value", "")
check(typed == str(value),
f"{title}: field received what was typed (typed={typed!r} wanted={value!r}"
f"{' <-- it committed its PREFILL, so it never got the keyboard' if typed == prefill else ''})")
# A value that will not parse is a FAILED CHECK, never an exception. An unguarded float()
# here met a locale-formatted "61,0000" and took the whole run down immediately after the
# first check in the ladder's history had passed — the seven rungs below it were never tried
# and the report read as a total failure.
try:
ok_val = abs(float(got) - float(value)) < 1e-6
except (TypeError, ValueError):
ok_val = False
check(ok_val, f"{title}: committed value is {got!r} (wanted {value})")
check(str(value) != prefill, f"{title}: the test value differs from the prefill {prefill!r}")
# RESUME JUST AFTER THE COMMIT, not at the end of the trace. A queued chain opens its next
# field from the commit callback, so by the time trace_mark() is read here that "open" line
# is already written — and the next call, searching only after this mark, never sees it. The
# rectangle's Height, the slot's Radius and the label reopen all failed as "nothing did"
# while the trace plainly showed the field open and waiting.
return (commit_at + 1) if commit_at is not None else trace_mark()
# ---------------------------------------------------------------- the ladder
def enter_sketch(timeout=180):
"""Open a real sketch on a real plane, and PROVE it with the socket before drawing anything.
THE SEQUENCE MATTERS AND IT IS NOT OBVIOUS. Shift+S enters sketch MODE and pops the plane
offer; the offer must be dismissed; and the plane itself is chosen by clicking it in the
viewport BEFORE Shift+S. check-gui-sketching.py has always done all four steps. This ladder
did two of them — Design tab, then Shift+S — and went straight to the tool letters.
That intermediate state is the trap. `is_sketching` reads 1, every tool key is accepted and
traced, and not one click draws anything, because there is no plane under them. The ladder
then reports eleven product failures, all of them "a value field opened (nothing did)", and
every one is the driver's. Two whole runs were spent on it.
So the gate is the ORACLE, not the keytrace: sketch_describe answers only when a sketch is
genuinely open. Waiting on a mode flag is what allowed the wrong state to pass for the right
one in the first place.
"""
deadline = time.time() + timeout
while time.time() < deadline:
click(132, 53) # Design tab
time.sleep(2.0)
dismiss_modals()
click(*PLANE_PX) # pick the plane IN THE VIEWPORT — before Shift+S
key("shift+s", 1.0)
key("Escape", 0.5) # entering sketch mode pops the offer; dismiss it
key("p", 0.6) # any sketch tool starts the session on that plane
if try_call("sketch_describe") is not None:
# NO Escape here. Every rung already opens with one to drop whatever tool the last
# one left armed, and Escape in the Design tab walks a LIFO: first press drops the
# armed tool, second LEAVES THE SKETCH. Pressing it here made that second press the
# rung's own, so the ladder exited the sketch before drawing anything and then
# reported all eleven checks failed with "nothing opened" — the tools were arming
# into an empty Feature-mode document.
return
die("no sketch opened after plane click + Shift+S within "
f"{timeout}s — sketch_describe never answered on {A.sock} (trace {TRACE})")
# tool key, the clicks that draw it, and one distinct value per queued field. The values are
# deliberately nothing like the as-drawn size, so a committed prefill cannot coincide with them.
# Where the plane label sits in the viewport before a sketch is open. Same constant the gesture
# ladder uses; it is a label on the 3D view, not a widget, so it moves only if the camera does.
PLANE_PX = (913, 359)
# Every coordinate below stays inside 1000..1400 x 500..760 — the box check-gui-sketching.py's
# calibration probes land four Points in, i.e. the region PROVEN to be live canvas on a 1920x1080
# window. Earlier values started at x=950, which is left of that box and also, on a fresh datadir,
# underneath the "Bambu Network Plug-in Required" modal.
TOOLS = [
("L", "Line", [(1030, 540), (1360, 540)], [61]),
("R", "Rectangle", [(1030, 540), (1360, 730)], [62, 43]),
("C", "Circle", [(1180, 620), (1330, 620)], [64]),
("S", "Slot", [(1030, 580), (1300, 580), (1300, 640)], [66]),
("G", "Polygon", [(1180, 620), (1320, 620)], [67]),
("E", "Ellipse", [(1180, 620), (1370, 620), (1180, 720)], [68]),
("A", "Arc", [(1040, 660), (1340, 660), (1190, 560)], [69]),
]
def rung_tool(k, name, clicks, values):
print(f" {name}")
key("Escape", 0.6) # back to Select, whatever the last tool left armed
# Every rung re-establishes that a sketch is STILL open. One stray Escape too many leaves it,
# and from then on every tool arms into a Feature-mode document that cannot open a value
# field — which the checks below report as eleven independent product failures.
if try_call("sketch_describe") is None:
die(f"{name}: the sketch is no longer open before this rung — an earlier rung left it")
key(k, 0.8)
# MARK BEFORE THE CLICKS, not after. The field is opened from a CallAfter scheduled by the
# render that follows the last click, so it can already be open by the time a mark taken
# afterwards is read — and type_into_open_field, which only looks at events AFTER its mark,
# then finds none and reports "a value field opened (nothing did)" for a field that is on
# screen, open, and waiting. That message accused the product of the exact defect the ladder
# exists to detect, from a bug in the ladder's own bookkeeping.
mark = trace_mark()
for (x, y) in clicks:
click(x, y)
for v in values:
mark = type_into_open_field(v, mark)
def rung_rounded_rect():
"""The shape the user actually reported: a ROUNDED rectangle, Width -> Height -> Radius.
It has no keyboard shortcut — the rectangle family binds R to CornerRect and leaves the other
modes in the toolbar flyout — so TOOLS above cannot reach it and the whole three-step chain
went untested. `run_verb` arms it the way the offer menu does.
NOTE the id: the OFFER verb is `sk_rect_rounded`; `design_rect_rounded` is the ACTION name and
run_verb throws on it, leaving the tool as Select. A run that misses that draws nothing and
still reaches its assertions, so arm-and-verify rather than arm-and-hope.
"""
print(" Rounded rectangle")
key("Escape", 0.6)
tool = None
for _ in range(8):
try_call("run_verb", verb="sk_rect_rounded")
time.sleep(0.8)
tool = (try_call("sketch_describe") or {}).get("tool")
if tool == "rect_rounded":
break
if not check(tool == "rect_rounded", f"the rounded-rectangle tool armed (tool={tool!r})"):
return
mark = trace_mark()
click(1030, 540); click(1330, 700); click(1300, 660) # corners, then the radius point
for v in (63, 41, 7):
mark = type_into_open_field(v, mark)
def rung_label_click():
"""The user's own report: click an existing dimension label and type a new value into it.
KEEP THE SHAPE AS DRAWN. An earlier version committed 55 and 47 into the queued chain first,
which resized the rectangle — and then clicked the pixel where the label had been before the
resize. It missed, every time, and reported the reopen broken. The shape's on-screen position
is only predictable if nothing has moved it, so Escape the chain instead: the rectangle stays
exactly between the two corners we clicked.
FIND THE LABEL, do not assume its offset. A dimension label is drawn beside its edge at an
offset that depends on zoom and text metrics, so a single hardcoded pixel is a guess that
silently becomes wrong. Walk a short band across the top edge instead and stop at the first
click that opens a field; if none of them does, that is a real failure and it says so.
"""
print(" label click-to-edit")
key("Escape", 0.6) # Select mode
key("R", 0.8)
click(1020, 530)
click(1350, 740)
time.sleep(1.5)
key("Escape", 0.8) # keep as drawn: abandon the queued value chain
time.sleep(0.8)
key("Escape", 0.6) # back to Select so a click picks rather than draws
mid_x, top_y = (1020 + 1350) // 2, 530
candidates = [(mid_x, top_y + dy) for dy in (-26, -20, -14, -8, 0, 8, 14)]
for (cx, cy) in candidates:
mark = trace_mark()
click(cx, cy)
deadline = time.time() + 2.0
while time.time() < deadline:
if [e for e in (parse(l) for l in trace_lines()[mark:]) if e and e[0] == "open"]:
check(True, f"clicking a dimension label reopened its value field (at {cx},{cy})")
type_into_open_field(71, mark)
return
time.sleep(0.2)
check(False, "clicking a dimension label reopened its value field "
f"(tried {len(candidates)} points across the top edge at x={mid_x})")
def main():
if not A.attach:
if not os.path.exists(A.bin):
die(f"no binary at {A.bin}")
open(TRACE, "w").close()
launch()
dismiss_first_run()
win()
print(f"click-edit ladder on {DISP}, trace {TRACE}")
enter_sketch()
for (k, name, clicks, values) in TOOLS:
rung_tool(k, name, clicks, values)
rung_rounded_rect()
rung_label_click()
print()
if _fail:
print(f"CLICK-EDIT LADDER FAILED — {_fail} of {_checks} checks", file=sys.stderr)
else:
print(f"CLICK-EDIT LADDER HELD — {_checks} checks")
if _proc is not None and not A.keep:
_proc.send_signal(signal.SIGTERM)
try:
_proc.wait(20)
except subprocess.TimeoutExpired:
_proc.kill()
return 1 if _fail else 0
if __name__ == "__main__":
sys.exit(main())
+10 -10
View File
@@ -11,16 +11,16 @@ action and no shortcut, so a key-driven ladder cannot reach them at all.
This ladder drives the menu. Nothing here is asserted from pixels:
WHAT WAS CLICKED -> the offer's own [OFFER] trace, emitted by show_offer_menu from the same
loop that builds the rows (SNAPORCA_KEYTRACE). It cannot drift from what
loop that builds the rows (ORCA_CAD_KEYTRACE). It cannot drift from what
the user is shown, which a hand-written expectation list would.
WHAT IS OFFERED -> the same trace, compared against DesignOffer.hpp parsed independently.
"The menu shows exactly the verbs the table says apply here" is a
property; a copied list of row names is a transcription.
WHAT IT PRODUCED -> the MCP socket, read-only, exactly as in the gesture ladder.
Run inside the rig container, with the app launched under SNAPORCA_KEYTRACE=1:
Run inside the rig container, with the app launched under ORCA_CAD_KEYTRACE=1:
docker exec snaporca-gui python3 /OrcaSlicer/scripts/CAD/check-gui-context-menu.py [rung ...]
docker exec orcacad-gui python3 /OrcaSlicer/scripts/CAD/check-gui-context-menu.py [rung ...]
"""
import importlib.util
import math
@@ -38,13 +38,13 @@ _spec = importlib.util.spec_from_file_location("gui_ladder", os.path.join(HERE,
G = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(G)
LOG = os.environ.get("SNAPORCA_GUI_LOG", "/tmp/gui-session.log")
LOG = os.environ.get("ORCA_CAD_GUI_LOG", "/tmp/gui-session.log")
# The rig container's /OrcaSlicer is the image's own baked source tree, not this checkout, so the
# generated header is not where a repo-relative path expects it. Look in both places and say which
# one was read — a ladder that silently graded against the WRONG table would be worse than one
# that refuses to start.
HEADER_CANDIDATES = [os.environ.get("SNAPORCA_OFFER_HPP", ""),
HEADER_CANDIDATES = [os.environ.get("ORCA_CAD_OFFER_HPP", ""),
os.path.join(HERE, "..", "src", "slic3r", "GUI", "CAD", "DesignOffer.hpp"),
os.path.join(HERE, "DesignOffer.hpp")]
@@ -61,12 +61,12 @@ def load_table():
"""Every verb in DesignOffer.hpp, as dicts. The independent half of the comparison.
Parsed from the generated header rather than from tool_atlas.json on purpose: the header is
what the binary was compiled from, and the two have been out of step before (snaporca-ziam,
what the binary was compiled from, and the two have been out of step before (ziam,
where regenerating the header silently dropped the Constrain row).
"""
path = next((p for p in HEADER_CANDIDATES if p and os.path.exists(p)), None)
if path is None:
raise SystemExit("no DesignOffer.hpp found; set SNAPORCA_OFFER_HPP or copy it beside "
raise SystemExit("no DesignOffer.hpp found; set ORCA_CAD_OFFER_HPP or copy it beside "
"this script (tried: " + ", ".join(filter(None, HEADER_CANDIDATES)) + ")")
print(f"offer table: {os.path.realpath(path)}")
src = open(path).read()
@@ -301,7 +301,7 @@ def rung_kinds():
# Empty space first: nothing is selected, so the sketch vocabulary's no-selection row set.
# Right-click has two jobs on a draw tool, and which one it does depends on whether an
# anchor is down. Both are asserted here: the version that consumed EVERY right-click made
# the offer unreachable from any armed tool (snaporca-ghcz), which is the goal's own
# the offer unreachable from any armed tool (ghcz), which is the goal's own
# mechanism failing silently.
hi = y1 - (y1 - y0) * 0.12
o = open_offer(cx, hi)
@@ -766,7 +766,7 @@ def rung_curves():
f"and it takes a typed radius exactly: {got:.9f} (asked 30.0)")
# The DoF of ONE CIRCLE is three. Asserted here because it is where the lie showed:
# after a delete the solver was never re-run, so this reported the DoF of the
# geometry that had just been erased. snaporca-ua9g.
# geometry that had just been erased. ua9g.
G.check("VERTEX", G.describe()["dof"] == 2,
f"and the sketch reports the DoF of what is actually in it: {G.describe()['dof']}")
@@ -1198,7 +1198,7 @@ def main():
if not os.path.exists(LOG):
G.die(f"no {LOG} — launch the app through scripts/CAD/start-headless-gui.sh")
if "[OFFER]" not in open(LOG, errors="replace").read()[-400000:]:
print(f"note: no [OFFER] lines in {LOG} yet — the app must run with SNAPORCA_KEYTRACE=1")
print(f"note: no [OFFER] lines in {LOG} yet — the app must run with ORCA_CAD_KEYTRACE=1")
want = sys.argv[1:] or list(RUNGS)
# TWICE. From a cold launch the app shows the Home page over the Design tab, and the first
# click only selects the tab — the second is what brings the viewport forward. A ladder that
+28 -39
View File
@@ -9,16 +9,16 @@ cannot say the Design tab meets its goal. This one draws with synthetic clicks a
values into the in-canvas field, then reads the result back through the socket, which is used
here ONLY as an instrument, never as an author.
Runs INSIDE the headless rig container (Xvfb :10 + openbox + the app with SNAPORCA_MCP set):
Runs INSIDE the headless rig container (Xvfb :10 + openbox + the app with ORCA_CAD_MCP set):
docker cp scripts/CAD/check-gui-sketching.py snaporca-gui:/tmp/ && \
docker exec snaporca-gui python3 /tmp/check-gui-sketching.py [rung ...]
docker cp scripts/CAD/check-gui-sketching.py orcacad-gui:/tmp/ && \
docker exec orcacad-gui python3 /tmp/check-gui-sketching.py [rung ...]
With no arguments every rung runs. Exit 0 = every property held.
"""
import json, math, os, re, socket, subprocess, sys, time
SOCK = os.environ.get("SNAPORCA_MCP", "/tmp/mcp.sock")
SOCK = os.environ.get("ORCA_CAD_MCP", "/tmp/mcp.sock")
DISP = os.environ.get("DISPLAY", ":10")
_n = 0
_fail = 0
@@ -225,7 +225,7 @@ def leave_sketch():
# "no sketch opened after plane click + Shift+S"; the unshifted Construction checkbox reported
# "0 construction axis". Both name the wrong subsystem. Canvas coordinates are immune because
# clickmm() derives them from the live canvas geometry — only the chrome constants need this.
CHROME_DY = int(os.environ.get("SNAPORCA_CHROME_DY", "26"))
CHROME_DY = int(os.environ.get("ORCA_CAD_CHROME_DY", "26"))
DESIGN_TAB = (128, 29 + CHROME_DY)
@@ -324,45 +324,34 @@ def calibrate_here():
PACE = 1.0
def field_win():
"""The open in-canvas value field as (x, y, w, h) in SCREEN pixels, or None.
def field_open():
"""Is a sketch value field open? Asked of the APP, not of the window list.
It is a top-level window of its own, not a child of the canvas (a native child cannot be
composited over the double-buffered wxGLCanvas), so it is found by enumerating windows rather
than by looking inside the app's frame. Two other small top-levels exist: the status chip,
which lives on the bottom edge, and 1x1/10x10 helpers.
It used to be answered by hunting for a small top-level window, because the field WAS one.
It is not any more — it is drawn by ImGui inside the GL canvas precisely so that no window
manager gets a vote on whether it may hold the keyboard. Enumerating windows now always
answers "no field", which turns every check built on it into one that cannot fail.
sketch_describe's `editing` is DesignSketchTool::value_field_open(), i.e. the app's own
answer to the same question.
"""
_, X, Y, W, H = win()
for w in sh(f"DISPLAY={DISP} xdotool search --onlyvisible --class '.'").split():
g = dict(l.split("=", 1) for l in
sh(f"DISPLAY={DISP} xdotool getwindowgeometry --shell {w}").strip().splitlines()
if "=" in l)
if "WIDTH" not in g:
continue
x, y, ww, hh = int(g["X"]), int(g["Y"]), int(g["WIDTH"]), int(g["HEIGHT"])
if ww >= W or hh < 24 or hh > 120 or ww < 40:
continue
if y > Y + H - 80: # the status chip, pinned to the bottom edge
continue
return (x, y, ww, hh)
return None
d = try_call("sketch_describe")
return bool(d and d.get("editing"))
def focus_field():
"""Put the keyboard in the value field, by clicking it.
"""Deliberately nothing.
WITHOUT THIS THE TYPED VALUE IS SILENTLY DISCARDED. The field is shown and raised but the
window manager does not give it the keyboard, so xdotool's digits go to the canvas and Return
commits the value the field opened with — the pre-filled as-drawn number. The failure is
invisible from the outside: a constraint IS created, the solve succeeds, and the sketch simply
holds the dimension you did not ask for (typed 40, got 54.94). One click fixes it.
This used to click into the value field before typing, and its old docstring explained why:
"WITHOUT THIS THE TYPED VALUE IS SILENTLY DISCARDED ... the window manager does not give it
the keyboard, so xdotool's digits go to the canvas". That was a workaround for the field
being a separate top-level window, and it is also what made this ladder blind to the very
defect the user reported — a suite that clicks the field first can never see that typing
without clicking is broken.
The field is now inside the canvas and the canvas has the keyboard, so typing just works and
there is nothing to click. Kept as a no-op so the call sites still read in order.
"""
r = field_win()
if r is None:
return False
x, y, w, h = r
xdo(f"mousemove {x + w // 2} {y + h // 2} click --delay 120 1")
time.sleep(0.3)
return True
@@ -1732,7 +1721,7 @@ def rung_type_guards():
clickmm(*rim(cs[0])); clickmm(*rim(cs[1]))
click(*CON_BTN["angle"])
time.sleep(1.0)
check("ANGLE", field_win() is None,
check("ANGLE", not field_open(),
"Angle on two circles opened no value field")
key("Escape", 0.6)
d2 = confirm_and_reopen()
@@ -1884,7 +1873,7 @@ def rung_scale():
f"every cut-out is exactly {side:.6f} squared")
# Now the part that matters: draw ONE more entity by hand, on top of all that.
#
# No Escape here, deliberately: this rung is the regression test for snaporca-j7gc, where a
# No Escape here, deliberately: this rung is the regression test for j7gc, where a
# bulk sketch_add made while a creation tool is armed was read as a drawn gesture, opened that
# tool's value field and swallowed the next key and click until one Escape dismissed it. The
# gesture below has to land on the FIRST try. Fixed by resyncing m_autoedit_seen in
+1 -1
View File
@@ -8,7 +8,7 @@ cost a GUI session and a human. The socket verbs make each one a call, and this
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
ORCA_CAD_MCP=/tmp/mcp.sock <binary> # launch with the socket enabled
python3 scripts/CAD/check-mcp-sketch.py [socket] # default /tmp/mcp.sock
Exit 0 = every assertion held. Anything else prints the first mismatch and stops.
+2 -2
View File
@@ -35,7 +35,7 @@ import sys
import tempfile
import time
SOCK = os.environ.get("SNAPORCA_MCP", "/tmp/mcp.sock")
SOCK = os.environ.get("ORCA_CAD_MCP", "/tmp/mcp.sock")
TOL = 1e-6 # exact-comparison tolerance (all inputs are lines)
WELD = 0.05 # endpoint-coincidence tolerance, in PDF units
@@ -295,7 +295,7 @@ def grade(pdf, name, report):
# same rule the engine now uses (DesignSketchTool::region_loops). A vertex is exactly
# where two loops touch in a real drawing, and a ray cast from a point lying ON the
# polygon under test answers by rounding: that alone accounted for every one of the 6
# sheets where the two attributions used to disagree. snaporca-5hvl.
# sheets where the two attributions used to disagree. 5hvl.
probes = [interior_point(r) for r in rings]
mine_parent = {}
for i, r in enumerate(rings):
+1 -1
View File
@@ -15,7 +15,7 @@ Every rung asserts those. Area appears only as a cross-check, never as the verdi
Entirely 2D: sketch entities only, no extrude, revolve or any solid feature.
SNAPORCA_MCP=/tmp/mcp.sock <binary>
ORCA_CAD_MCP=/tmp/mcp.sock <binary>
python3 scripts/CAD/check-sketch-engine.py [socket]
Exit 0 = every rung held. Otherwise the first broken property is named and the run stops.
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# One turn of the keyboard-focus convergence loop, start to verdict, with no human in it.
#
# scripts/CAD/focus-loop.sh # full turn: sync -> build -> restart -> assert
# SKIP_BUILD=1 scripts/CAD/focus-loop.sh # re-assert against the binary already on the host
#
# Exit 0 only when every gate holds. Any other exit is a failing gate and names which.
#
# WHY THIS EXISTS. The focus defects in the Design tab were chased for days by hand: build, launch
# the GUI, drive it with xdotool, read a screenshot, guess, repeat. That needs a person at every
# step and it is where the days went. This does not: behemoth carries an agent-owned Xvfb :10 with
# openbox, the app, xdotool and an MCP socket that reports sketch state as JSON, so a turn is
# sync -> build -> restart -> assert, and the ASSERTION is the verdict, not my reading of a picture.
#
# WHY BEHEMOTH AND NOT THE orcacad-gui RIG CONTAINER. The rig was the obvious host and it does not
# work for this: its image pins a dependency set 216 non-CAD source files behind cad-mainline
# (assimp among them), so today's CAD sources call GUI_App::is_auto_close_sketch_loops and
# MainFrame::ensure_design_panel, which that tree has never heard of. Syncing all of src/ to fix
# that needs a deps rebuild measured in hours. behemoth already builds this exact tree, already
# runs a WM on :10, and is the machine the user actually runs the product on — so the loop asserts
# against the shipping artefact rather than a stale twin. Reviving the rig means rebuilding its
# deps image first; until then it cannot adjudicate anything about this code.
set -uo pipefail
HOST="${HOST:-tommaso@100.103.234.2}"
DISP="${DISP:-:10}"
SRC="${SRC:-\$HOME/projects/orca/orcacad-native/src}"
TRACE="${TRACE:-/tmp/ux-focus-loop.log}"
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
BIN="build/src/Release/orca-slicer"
say() { printf '\n=== %s\n' "$*"; }
die() { printf 'GATE FAILED: %s\n' "$*" >&2; exit 1; }
ssh -o ConnectTimeout=10 "$HOST" true || die "cannot reach $HOST"
# ---------------------------------------------------------------- S1 sync
# Only the CAD paths and the ladders. behemoth's tree is a full cad-mainline checkout kept in step
# by its own realign; pushing unrelated files from here would make the build host disagree with
# git for reasons no later session could reconstruct.
say "S1 sync"
rsync -q "$REPO"/src/slic3r/GUI/CAD/*.{cpp,hpp} "$HOST:$SRC/src/slic3r/GUI/CAD/" || die "sync GUI/CAD"
rsync -q "$REPO"/src/libslic3r/CAD/*.{cpp,hpp} "$HOST:$SRC/src/libslic3r/CAD/" || die "sync libslic3r/CAD"
rsync -q "$REPO"/scripts/CAD/check-gui-click-edit.py "$REPO"/scripts/CAD/check-gui-sketching.py \
"$HOST:/tmp/" || die "sync ladders"
echo " sources + ladders in place"
# ---------------------------------------------------------------- S2 build
# flock: two concurrent Orca builds once OOM'd this machine for 2h28m. Every build script on the
# fleet takes this same lock.
#
# Grade the BINARY'S TIMESTAMP, never the build command's exit code. This is a Ninja Multi-Config
# tree whose default rules are Debug while the artefact under test is Release, so a wrong-config
# invocation returns success in seconds having touched nothing — it cost a wasted cycle here
# before anyone thought to look at the file.
if [ -z "${SKIP_BUILD:-}" ]; then
say "S2 build"
before=$(ssh "$HOST" "stat -c %Y $SRC/$BIN 2>/dev/null || echo 0")
ssh "$HOST" "flock /tmp/orca-rig-build.lock \$HOME/projects/orca/orcacad-native/rebuild.sh > /tmp/focus-build.log 2>&1"
rc=$?
after=$(ssh "$HOST" "stat -c %Y $SRC/$BIN 2>/dev/null || echo 0")
if [ "$rc" != 0 ] || [ "$after" = "$before" ]; then
ssh "$HOST" "grep -m5 -B2 'error:' /tmp/focus-build.log; tail -5 /tmp/focus-build.log"
die "S2 build (exit $rc, binary $( [ "$after" = "$before" ] && echo unchanged || echo rebuilt ))"
fi
echo " built"
fi
# ---------------------------------------------------------------- S3 F2P
# The ladder launches and tears down the app itself, in its own datadir, so nothing here has to
# manage a process. It types WITHOUT clicking the field first, which is the whole contract.
say "S3 fail-to-pass: type without clicking the field"
ssh "$HOST" "cd $SRC && DISPLAY=$DISP python3 /tmp/check-gui-click-edit.py \
--display $DISP --bin $BIN --trace $TRACE"
f2p=$?
# ---------------------------------------------------------------- S4 P2P
say "S4 pass-to-pass: the existing gesture ladder"
ssh "$HOST" "cd $SRC && DISPLAY=$DISP python3 /tmp/check-gui-sketching.py 2>&1 | tail -3"
p2p=$?
say "VERDICT"
[ "$f2p" = 0 ] || die "F2P: a tool did not take the typed value (exit $f2p)"
[ "$p2p" = 0 ] || die "P2P: the gesture ladder regressed (exit $p2p)"
echo "ALL GATES HELD"
+5 -5
View File
@@ -10,7 +10,7 @@
# FULL=1 scripts/CAD/run-all-checks.sh # corpus over ALL 997 sheets (~25 min)
# SKIP_GUI=1 scripts/CAD/run-all-checks.sh # kernel only, for a machine with no rig
#
# The rig container is expected to be up with the app running and SNAPORCA_MCP set; bring it up
# The rig container is expected to be up with the app running and ORCA_CAD_MCP set; bring it up
# with scripts/CAD/start-headless-gui.sh inside it. The corpus lives at /corpus in that container.
set -uo pipefail
# ../.. -- this script lives in scripts/CAD/, so one level up is scripts/, not the repo
@@ -19,7 +19,7 @@ set -uo pipefail
# scripts/scripts/ and reporting instant failures that were all the same typo.
cd "$(dirname "${BASH_SOURCE[0]}")/../.." || exit 1
# orcacad-gui, NOT snaporca-gui: that is the other fork's rig, and defaulting to it makes
# orcacad-gui, NOT snapmaker-gui: that is the other fork's rig, and defaulting to it makes
# this gate verify the wrong fork's binary while reporting green. run-kernel-tests.sh
# carries the same warning about the build volume, where the defect was found first.
C="${C:-orcacad-gui}"
@@ -53,13 +53,13 @@ run_in_rig() { # copy the script in fresh, then run it ther
# FIRST, and it needs no rig: the offer table the menu is compiled from must be what the atlas
# says. The header calls itself GENERATED and had been hand-edited anyway — which cost four rows
# that existed only in the header, one row wired to the wrong action, and a count of 91 for a
# 92-row array, so the last verb was unreachable (snaporca-z8rs, snaporca-ziam).
# 92-row array, so the last verb was unreachable (z8rs, ziam).
# docs/CAD/, not docs/: SoftFever moved the design docs into the CAD subfolder
# (bbd1989e1e) and this line kept the old path, so the rung failed on a missing file
# rather than on anything about the table. The other fork still has docs/ux/.
step "offer table matches the atlas" python3 docs/CAD/ux/mockups/gen_offer_table.py --check
step "kernel suite" scripts/CAD/run-kernel-tests.sh --vol "${KVOL:-snaporca_kerneltest}"
step "kernel suite" scripts/CAD/run-kernel-tests.sh --vol "${KVOL:-orcacad_kerneltest}"
if [ -z "${SKIP_GUI:-}" ]; then
step "engine ladder (rungs 1-8, scripted geometry)" \
@@ -71,7 +71,7 @@ if [ -z "${SKIP_GUI:-}" ]; then
step "gesture ladder (mouse and keyboard)" \
run_in_rig scripts/CAD/check-gui-sketching.py /tmp/check-gui-sketching.py
# The offer ladder needs TWO extra things the others do not: the app must have been launched
# with SNAPORCA_KEYTRACE=1 (its [OFFER] lines are the whole instrument), and it reads the
# with ORCA_CAD_KEYTRACE=1 (its [OFFER] lines are the whole instrument), and it reads the
# generated offer table to predict what each selection should show — which is not in the
# container's own baked source tree, so it is copied in beside the script — /tmp, where
# run_in_rig puts the script, is one of the paths the ladder looks in.
+5 -5
View File
@@ -23,18 +23,18 @@
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
# orcacad-deps, NOT snaporca-deps: this fork is mainline-based and needs Eigen 5.0.1,
# CGAL 5.6.3, wx 3.3.2 and Python 3.12 Development.Embed, none of which snaporca-deps has.
# orcacad-deps, NOT snapmaker-deps: this fork is mainline-based and needs Eigen 5.0.1,
# CGAL 5.6.3, wx 3.3.2 and Python 3.12 Development.Embed, none of which snapmaker-deps has.
# With the wrong image CMake dies at configure, which is exactly why this fork went
# M1-M8 without ever compiling (see commit 1633005bba).
IMAGE="${IMAGE:-orcacad-deps}"
# Must NOT default to snaporca_buildcache: that is the other fork's volume, and pointing
# Must NOT default to snapmaker_buildcache: that is the other fork's volume, and pointing
# this fork at it makes the two silently trade build artefacts. build-gui-incremental.sh had the
# identical defect and was fixed to orcacad_buildcache; this script was missed.
VOL="${BUILD_VOL:-orcacad_kerneltest}"
# No exclusions. Both cases that used to be quarantined now run: the solver SIGABRT on
# circle-line tangency is fixed (snaporca-tkz), and the internal-thread case turned out to have
# correct geometry and a wrong reference in the test (snaporca-kzy). A green run here now means
# circle-line tangency is fixed (tkz), and the internal-thread case turned out to have
# correct geometry and a wrong reference in the test (kzy). A green run here now means
# the whole CAD suite passed, not "everything except the two we gave up on".
#
# ...and that claim was still not true, because the default tag was [CadDocument] alone while
+2 -2
View File
@@ -36,8 +36,8 @@ export LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe
# them comes up looking perfectly healthy: the window is there, status says app up, and every
# ladder then dies on "Connection refused" — which reads as a dead app rather than a rig that was
# started without its instrument. The script that launches the rig is where they belong.
export SNAPORCA_MCP="${SNAPORCA_MCP:-/tmp/mcp.sock}"
export SNAPORCA_KEYTRACE="${SNAPORCA_KEYTRACE:-1}"
export ORCA_CAD_MCP="${ORCA_CAD_MCP:-/tmp/mcp.sock}"
export ORCA_CAD_KEYTRACE="${ORCA_CAD_KEYTRACE:-1}"
export LD_LIBRARY_PATH="$LIBPY:$LIBPY2:${LD_LIBRARY_PATH:-}"
mkdir -p /root/.config # startup dies in boost::filesystem::create_directory without this
+5 -5
View File
@@ -1,13 +1,13 @@
# Deps-only base image for fast iteration on SnapOrca.
# Deps-only base image for fast iteration on Orca.
# Identical system+pinned-dependency setup to scripts/Dockerfile, but STOPS after
# `build_linux.sh -dr` (no slicer/AppImage build). Produces an image with the pinned
# deps baked at /OrcaSlicer/deps/build/destdir, so the slicer can be rebuilt
# incrementally via scripts/CAD/build-gui-incremental.sh without re-running the long deps build.
#
# Build once (rebuild only when deps/ changes, e.g. OCCT module flags):
# docker build -t snaporca-deps -f scripts/Dockerfile.deps .
# docker build -t snapmaker-deps -f scripts/Dockerfile.deps .
FROM docker.io/ubuntu:24.04
LABEL maintainer="SnapOrca CAD iteration base"
LABEL maintainer="Orca CAD iteration base"
# Disable interactive package configuration
RUN apt-get update && \
@@ -80,11 +80,11 @@ RUN ./build_linux.sh -dr -j 12
RUN ln -sfn /OrcaSlicer/deps/build/OrcaSlicer_dep /OrcaSlicer/deps/build/destdir
# The rig's GUI runtime. This used to arrive for free because orcacad-deps was layered on
# snaporca-deps; that lineage is Trap 1 in docs/rig_build_traps.md (a baked project(Snapmaker_Orca)
# snapmaker-deps; that lineage is Trap 1 in docs/rig_build_traps.md (a baked project(Snapmaker_Orca)
# tree) and building from this Dockerfile is what removes it — along with the X stack the rig
# needs. scripts/CAD/start-headless-gui.sh requires Xvfb and openbox (without a window manager `xdotool
# windowactivate` aborts with "windowmanager claims not to support..."), drives the UI with
# xdotool, and captures to /shots with scrot/ImageMagick. Same set snaporca-deps carries.
# xdotool, and captures to /shots with scrot/ImageMagick. Same set snapmaker-deps carries.
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
imagemagick \
openbox \
+2 -2
View File
@@ -9,8 +9,8 @@
#
# The consequence was that scripts/CAD/run-kernel-tests.sh failed at CMake CONFIGURE time,
# before a single source file compiled, so THIS FORK'S KERNEL SUITE COULD NOT RUN AT ALL.
# Every kernel change ported here was parity-checked against snaporca and never independently
# tested (snaporca-w80c). The Snapmaker fork does not hit this: its base requires neither
# Every kernel change ported here was parity-checked against Snapmaker and never independently
# tested (w80c). The Snapmaker fork does not hit this: its base requires neither
# assimp nor OpenCV.
#
# WHY A LAYER AND NOT A FULL DEPS REBUILD. Rebuilding every dependency takes hours and would