diff --git a/.claude/loopspec/sketch-focus-arbiter.spec.md b/.claude/loopspec/sketch-focus-arbiter.spec.md new file mode 100644 index 0000000000..f0c1287307 --- /dev/null +++ b/.claude/loopspec/sketch-focus-arbiter.spec.md @@ -0,0 +1,86 @@ +# DELEGATION SPECIFICATION: HARNESS-DRIVEN VALIDATION LOOP +slug: sketch-focus-arbiter · repo: /home/tommaso/projects/apps/orca_cad · branch: cad-mainline + +## 1. TARGET GOAL + +**Functional Objective.** Keyboard input in the Design tab is routed by WHAT THE KEY IS, not by +which widget the window manager decided to focus. Adopted from FreeCAD's +`DrawSketchKeyboardManager::detectKeyboardEventHandlingMode` +(src/Mod/Sketcher/Gui/DrawSketchKeyboardManager.cpp), which never queries focus at all: + + - digit, `-`, `.`, `,` -> the open value field + - Backspace / Delete -> the open value field (when one is open) + - Enter / Return / Tab -> commit the field, control returns to the view + - a letter -> the sketch-tool shortcut map, as today + - Esc -> the existing CadLevel LIFO (DesignInteraction.hpp), unchanged + - anything else -> sticky: whoever had it keeps it + +Observable postcondition: for EVERY sketch tool that opens a value field, a value typed +immediately after the field appears — with NO click into the field — is the value committed. +Today the prefill is committed instead whenever the WM withholds focus. + +**Target Files / Scope (writable).** + src/slic3r/GUI/CAD/DesignPanel.cpp (the arbiter lives in the existing wxEVT_CHAR_HOOK) + src/slic3r/GUI/CAD/DesignCanvas.cpp/.hpp (forwarding entry points only) + src/slic3r/GUI/CAD/SketchInlineEditor.cpp/.hpp (accept a programmatically delivered character) + scripts/CAD/check-gui-click-edit.py (F2P oracle — authoring exception, see §4) +Everything else read-only. No dependency additions, no reformatting. + +**Open Bindings.** + - The in-canvas ImGui field on wip/in-canvas-value-field is NOT in scope. Default: the arbiter + is implemented against the CURRENT wxFrame field on cad-mainline, because content-based + routing makes the window's focus irrelevant either way. If it later moves in-canvas the + arbiter is unchanged. + - Tools whose field is opened by a toolbar button rather than a gesture (Constrain path) are + covered by the same arbiter but are not in the F2P tool list. Default: assert them in P2P only. + +## 2. HARNESS ENVIRONMENT & GROUND TRUTH + +The rig container `orcacad-gui` on nativedev IS the harness. Xvfb `:11` + openbox, the app under +test, `xdotool` for synthetic input, and an MCP socket at `/tmp/mcp.sock` that reports sketch +state as JSON. It is a closed loop: drive input, read geometry back, assert. No window manager +politics, no human. + + Harness interface (ordered; each slot one invocation, one exit code): + S1 sync docker cp orcacad-gui:/OrcaSlicer/ + S2 build docker exec orcacad-gui ninja -C /OrcaSlicer/build orca-slicer + S3 restart docker exec orcacad-gui /OrcaSlicer/scripts/CAD/start-headless-gui.sh + S4 F2P docker exec -e DISPLAY=:11 orcacad-gui python3 /tmp/check-gui-click-edit.py --attach + S5 P2P docker exec -e DISPLAY=:11 orcacad-gui python3 /tmp/check-gui-sketching.py + +**F2P.** `scripts/CAD/check-gui-click-edit.py`. For each of Line, Rectangle, Circle, Slot, +Polygon, Ellipse and Rounded rectangle: arm the tool, draw it, and type a value that differs +from the prefill WITHOUT clicking the field. Assert the committed value equals the typed value. +The ladder must FAIL against unmodified cad-mainline — that is what proves it asserts something. + +**P2P.** `scripts/CAD/check-gui-sketching.py`, the existing gesture ladder, minus anything red at +baseline. NOTE: it calls `focus_field()` — one click into the field before typing — which is the +workaround this whole task removes. It stays green as a regression guard; it is NOT evidence. + +**Test Integrity Constraint.** `focus_field()` in check-gui-sketching.py must NOT be deleted to +make things pass, and check-gui-click-edit.py must NOT be weakened. Either invalidates the run. + +## 3. VERIFICATION COMMANDS +1. Static: `docker exec orcacad-gui ninja -C /OrcaSlicer/build orca-slicer` (warnings delta only; + this repo configures no linter — the compiler is the static gate. Absolute-zero is NOT the gate.) +2. Harness: `docker exec -e DISPLAY=:11 orcacad-gui python3 /tmp/check-gui-click-edit.py --attach` +3. Regression: `docker exec -e DISPLAY=:11 orcacad-gui python3 /tmp/check-gui-sketching.py` + +## 4. CONVERGENCE LOOP — ceiling 8 iterations +EDIT (scoped) -> EXECUTE S1..S5 -> PARSE the ladder's per-tool assertions and the [UX]/[KEYTRACE] +lines -> PATCH from the parsed cause. On ceiling without convergence: stop, report the last diff +and the unresolved failure set. Do not report success. + +F2P authoring exception: check-gui-click-edit.py is writable, and must be shown RED against +unmodified source before any source edit counts. + +## 5. TERMINATION CRITERIA +- [ ] S2 exits 0, and introduces no compiler warning absent from the baseline. +- [ ] S4 ALL_PASSED — every tool commits the typed value, no click into the field. +- [ ] S5 shows zero regressions against its recorded baseline pass count. +- [ ] F2P proven red without the fix (source stashed, ladder re-run, must FAIL). + +## 6. GUARDRAILS +Zero-assumption: no completion claim without captured stdout and exit codes. Oracle supremacy: +the ladder's verdict overrides my judgement. Blast radius: §1 files only. Baseline obligation: +run §3 once before the first edit and record it. diff --git a/docs/CAD/GAP_ANALYSIS_vs_ONSHAPE.md b/docs/CAD/GAP_ANALYSIS_vs_ONSHAPE.md index 05b8351c34..bf603cf2c1 100644 --- a/docs/CAD/GAP_ANALYSIS_vs_ONSHAPE.md +++ b/docs/CAD/GAP_ANALYSIS_vs_ONSHAPE.md @@ -1,4 +1,4 @@ -# SnapOrca-CAD vs Onshape — capability gap analysis +# Orca-CAD vs Onshape — capability gap analysis Generated 2026-07-22 by enumerating the source, not from recollection: `CadFeatureType` and `add_*` in `src/libslic3r/CAD/CadDocument.hpp`, `Tool` in @@ -6,19 +6,19 @@ Generated 2026-07-22 by enumerating the source, not from recollection: `SketchConstraintType` + `SketchEntity::Type` in `src/libslic3r/CAD/SketchEngine.hpp`, and the JSON-RPC dispatch in `src/slic3r/GUI/CAD/McpControl.cpp`. -**Scope note.** Onshape is a cloud PLM platform; SnapOrca is a Design tab inside a +**Scope note.** Onshape is a cloud PLM platform; Orca is a Design tab inside a slicer. A large share of Onshape's surface (release management, branching, real-time collaboration, FEA, rendering, PDM) is out of scope by construction and is listed separately at the bottom rather than counted as a "missing tool". --- -## 1. What SnapOrca already has +## 1. What Orca already has ### 2D sketcher — near parity with Onshape This is the strongest area. Very little is missing. -| Category | SnapOrca | +| Category | Orca | |---|---| | Entities | Line, Polyline, Arc (3-point / tangent / center), Circle (center / 2-point / 3-point), Point, Ellipse, Elliptical arc, B-spline | | Shapes | Rectangle (corner / center / oblique / rounded), Slot, Arc-slot, Polygon | @@ -88,7 +88,7 @@ Studio + configurations are a core differentiator, and this is the cheapest Tier item to close for the size of the payoff. **4. Surface modelling.** Absent. No surface extrude/revolve/loft/sweep, no fill, -knit, trim/extend surface, offset surface, or thicken. SnapOrca is solid-only. +knit, trim/extend surface, offset surface, or thicken. Orca is solid-only. *Impact:* organic/complex shapes and repair of imported junk geometry are impossible. OCCT already provides all of it (`TKOffset`, `TKBRep`), so the kernel is not the blocker — only UI and feature plumbing. @@ -106,7 +106,7 @@ blocker — only UI and feature plumbing. | **Split body** | Cut removes material; splitting one body into two independently-usable bodies is absent. Very relevant for print-in-parts. | Medium | | **Thicken** | Solid from a surface/face offset. | Needs surfaces | | **Rib** | Standard structural feature. | Medium | -| **Delete face / move face / replace face** | Direct/dumb-solid editing — the main tool for fixing imported STEP. Given SnapOrca imports STEP *and* meshes, its absence is felt. | Medium | +| **Delete face / move face / replace face** | Direct/dumb-solid editing — the main tool for fixing imported STEP. Given Orca imports STEP *and* meshes, its absence is felt. | Medium | | **Datum axis, coordinate system** | Only datum *planes* exist. Axes are needed for revolve/pattern references. | Yes | | **Mass properties** | `GeometryEngine` computes a volume internally, but there is no volume/mass/COM/inertia readout. For print cost/time estimation this is nearly free to expose. | Yes — trivial | | **Measure tool in the GUI** | `measure` exists over MCP but there is no interactive measure in the UI. | Yes | @@ -123,7 +123,7 @@ blocker — only UI and feature plumbing. Version control with branching/merging, release management, real-time multi-user collaboration, cloud PDM, FeatureScript custom-feature authoring, simulation/FEA, photorealistic rendering, app store/integrations. These are Onshape-the-platform, -not Onshape-the-modeller. Not defects in SnapOrca. +not Onshape-the-modeller. Not defects in Orca. --- @@ -140,7 +140,7 @@ the most capability per unit of work: 5. **Split body** — high value for print-in-parts workflows. 6. **Project edges into sketch** — the sketcher's most conspicuous hole. 7. **Surface modelling** — large, but OCCT already ships the algorithms. -8. **Assemblies** — largest effort; only worth it if SnapOrca targets multi-part products. +8. **Assemblies** — largest effort; only worth it if Orca targets multi-part products. Deliberately last: drawings and sheet metal — high cost, low relevance to an FDM-oriented tool. @@ -149,7 +149,7 @@ FDM-oriented tool. ## 4. Honest summary -SnapOrca's **sketcher is at or near Onshape parity**, and its **solid feature set +Orca's **sketcher is at or near Onshape parity**, and its **solid feature set covers the mainstream modelling path** (sketch → extrude/revolve/sweep/loft → dress-up → boolean/pattern). What is absent is *breadth*: assemblies, surfaces, sheet metal, drawings, and — most importantly for a tool calling itself parametric — diff --git a/docs/CAD/cad_dependency_weight.md b/docs/CAD/cad_dependency_weight.md index 04528aa2a6..0b64ac8097 100644 --- a/docs/CAD/cad_dependency_weight.md +++ b/docs/CAD/cad_dependency_weight.md @@ -4,7 +4,7 @@ What the Design tab actually costs a maintainer who merges it. Written to be che every number below is reproducible with the command that produced it, and the places where a number is still missing say so instead of guessing. -Measured on Linux x86_64, OCCT V7_6_0, in the `snaporca-deps` build image. +Measured on Linux x86_64, OCCT V7_6_0, in the `snapmaker-deps` build image. ## Summary @@ -75,7 +75,7 @@ producing them honestly needs a build this machine cannot do: 1. **Windows DLL delta.** OCCT builds shared on Windows, so the shipped cost there is real DLL bytes rather than linker-selected objects. That needs a Windows build to size — - tracked as the cross-platform build proof (`snaporca-gix`). + tracked as the cross-platform build proof (`gix`). 2. **Clean-build time delta.** Measuring it means building the deps prefix twice, with the flag ON and OFF, on the same machine. The incremental figures from day-to-day work do not answer the question and are not offered as if they did. diff --git a/docs/CAD/design/mate-connectors/DESIGN_MATE_CONNECTORS.md b/docs/CAD/design/mate-connectors/DESIGN_MATE_CONNECTORS.md index 1644ccbad6..fe1a7a5fee 100644 --- a/docs/CAD/design/mate-connectors/DESIGN_MATE_CONNECTORS.md +++ b/docs/CAD/design/mate-connectors/DESIGN_MATE_CONNECTORS.md @@ -1,6 +1,6 @@ # Mate connectors: aligning with the mainstream CAD systems -Research date: 2026-08-05. Written against `orca_cad` / `snaporca` at the M8 state +Research date: 2026-08-05. Written against `orca_cad` / `Snapmaker` at the M8 state (`CadDocument.{hpp,cpp}`, `apply_mate`, `datum_frame`, the `Mate` card in `DesignPanel.cpp`). **Brief:** align with the mate-connector concept as the main CAD programs actually implement it, @@ -300,7 +300,7 @@ times. **C2 — The roll is unspecified.** Aligning Z leaves one rotation about Z undetermined. Something must pin it, and if that something is world-derived, the frame does not rotate with its part. **This -codebase shipped exactly this bug** (`snaporca-en4`): a face-only connector took Z from the face +codebase shipped exactly this bug** (`en4`): a face-only connector took Z from the face normal but X from `coordsys_x_hint`, a world constant, so Fastened and Slider claimed to lock an orientation the frame could not see. Fixed 2026-07-26 by deriving X from the face's own first usable edge — but note the fix's own caveat: *"replaying an older document whose face-only connector fed a @@ -512,7 +512,7 @@ Source of record: `CadDocument.hpp:26,247-252,298-310`; `CadDocument.cpp:1669` ( **Already aligned — do not "fix" these:** the five types and their DOF; the frame definition (A1); Z as the joint axis (A2); superimpose-then-relax (A3); the fixed/moving asymmetry in the data model (A5); DOF wording in the type list (A7); free-DOF preservation (A8); right-handed frames under mirror -(R14); and `snaporca-en4`'s fix, which put roll derivation on the body where it belongs (C2). +(R14); and `en4`'s fix, which put roll derivation on the body where it belongs (C2). **The pattern worth naming: the kernel is in good shape and the concept is under-explained.** Half the requirements here are wording and drawing, not geometry. The two real engineering items are R9 (origin @@ -668,7 +668,7 @@ is a symbol, not a part — it must not shrink with the model. Nothing about tha The glyph was therefore implemented and driven on the rig. Screenshots: `g-0*.png`, left in the workspace `artifacts/shots/` and not moved into the repo. Five findings, none of which a mock could have produced: -**F1 — Three axis arms lose to one.** Rendered side by side (`SNAPORCA_GLYPH=A` vs default), the +**F1 — Three axis arms lose to one.** Rendered side by side (`ORCA_CAD_GLYPH=A` vs default), the Onshape-style RGB trio crowds a 22 px disc: the arrowheads are as large as the disc, they bury the gold quadrant, and at an oblique angle the three heads pile into a coloured smudge. Worse, **it is indistinguishable from the move gizmo and the bed triad**, which are already RGB arrow trios in this @@ -827,7 +827,7 @@ conversation it arrived in points at "glyph". It changes the meaning of every stored document containing a mate. Options: (a) invert and migrate, writing `direction=Aligned` where `mate_flip` was false; (b) invert only for new mates and store `direction` explicitly from now on. (b) is safer and costs one field. Note this project has taken one -such semantic hit knowingly before — the `snaporca-en4` fix — and the golden fixture survived, so the +such semantic hit knowingly before — the `en4` fix — and the golden fixture survived, so the migration path is a known quantity. **If G3 (live preview) lands first, this matters much less.** **D2 — How far to take origin candidates?** [R9] @@ -911,7 +911,7 @@ R18's loud refusals carrying the honesty. [Joint kinematics — the six lower pairs and their DOF](https://erc-bpgc.github.io/handbook/mechanical/Joint%20Kinematics/) · [ISO 10303-105 — Kinematics (STEP integrated resource)](https://www.iso.org/standard/78589.html) -**Internal** — `snaporca-en4` (closed 2026-07-26, fixes C2 here) · `CadDocument.cpp:1669` +**Internal** — `en4` (closed 2026-07-26, fixes C2 here) · `CadDocument.cpp:1669` `datum_frame` · `CadDocument.cpp:2961` `apply_mate` · `CadDocument.cpp:1302` `add_mate` **Second opinion** — an independent review by Kimi Code (2026-08-05) contributed the diff --git a/docs/CAD/design/mate-connectors/bear_glyph_table.h b/docs/CAD/design/mate-connectors/bear_glyph_table.h index 2ff5ed5996..c26631e7e0 100644 --- a/docs/CAD/design/mate-connectors/bear_glyph_table.h +++ b/docs/CAD/design/mate-connectors/bear_glyph_table.h @@ -9,7 +9,7 @@ static const Vec2d kBearOutline[] = { // 12 verts, RDP eps 0.030, CCW static const Vec2d kBearChin[] = { // the CHIN BAR, flat. The muzzle is relief — see kBearCrest. {-0.2682, -0.3578}, {+0.2628, -0.3578}, {+0.2237, -0.1786}, }; -// {cx, cy, r}: two eyes, then the cheek dot that carries handedness (snaporca-wi3z). +// {cx, cy, r}: two eyes, then the cheek dot that carries handedness (wi3z). static const Vec3d kBearMarks[] = { {-0.1997, +0.1760, +0.0590}, {+0.1947, +0.1760, +0.0590}, diff --git a/docs/CAD/design/mate-connectors/connector-glyph-proposal.html b/docs/CAD/design/mate-connectors/connector-glyph-proposal.html index 6f1bcff518..2a8475ee24 100644 --- a/docs/CAD/design/mate-connectors/connector-glyph-proposal.html +++ b/docs/CAD/design/mate-connectors/connector-glyph-proposal.html @@ -96,7 +96,7 @@
-
SnapOrca Design · assembly
+
Orca Design · assembly

Mate connector glyph — polarity and verse

Onshape's core (disc + roll quadrant + Z arrow) is adopted unchanged because it is proven and diff --git a/docs/CAD/design/mate-connectors/emit_glyph_table.py b/docs/CAD/design/mate-connectors/emit_glyph_table.py index 400263516c..6e0fd29e32 100644 --- a/docs/CAD/design/mate-connectors/emit_glyph_table.py +++ b/docs/CAD/design/mate-connectors/emit_glyph_table.py @@ -1,4 +1,4 @@ -"""Emit the simplified bear as a C++ table for the viewport glyph — snaporca-wi3z. +"""Emit the simplified bear as a C++ table for the viewport glyph — wi3z. Everything is normalised to the part's own bounding span and centred, so the renderer scales by one radius R in screen pixels and nothing here carries millimetres. Emitting rather than @@ -79,7 +79,7 @@ L.append("};") L.append(f"static const Vec2d kBearChin[] = {{ // the CHIN BAR, flat. The muzzle is relief — see kBearCrest.") L.append(" "+", ".join(f"{{{fmt(x)}, {fmt(y)}}}" for x,y in TRI)+",") L.append("};") -L.append("// {cx, cy, r}: two eyes, then the cheek dot that carries handedness (snaporca-wi3z).") +L.append("// {cx, cy, r}: two eyes, then the cheek dot that carries handedness (wi3z).") L.append("static const Vec3d kBearMarks[] = {") for cx,cy,r in E: L.append(f" {{{fmt(cx)}, {fmt(cy)}, {fmt(r)}}},") L.append(f" {{{fmt(DOT[0])}, {fmt(DOT[1])}, {fmt(DOT[2])}}},") diff --git a/docs/CAD/design/mate-connectors/glyph_preview.py b/docs/CAD/design/mate-connectors/glyph_preview.py index 254ca7e80b..e339e6bdab 100644 --- a/docs/CAD/design/mate-connectors/glyph_preview.py +++ b/docs/CAD/design/mate-connectors/glyph_preview.py @@ -1,4 +1,4 @@ -"""Render the SIMPLIFIED glyph exactly as render_mate_face() draws it — snaporca-x0kd. +"""Render the SIMPLIFIED glyph exactly as render_mate_face() draws it — x0kd. This is the panel the study was missing. simplify_study.py measured a FLAT outline and relief_sheet.py measured the FULL 1508-facet part; neither showed the simplified glyph WITH its diff --git a/docs/CAD/design/mate-connectors/handedness.py b/docs/CAD/design/mate-connectors/handedness.py index 3af54b3161..68fa07bb55 100644 --- a/docs/CAD/design/mate-connectors/handedness.py +++ b/docs/CAD/design/mate-connectors/handedness.py @@ -1,4 +1,4 @@ -"""Give the bear a handedness mark that survives rasterisation — snaporca-wi3z, Tommaso's call 2. +"""Give the bear a handedness mark that survives rasterisation — wi3z, Tommaso's call 2. The study showed the left/right cue lives in sub-millimetre corner radii and is therefore invisible at glyph size: one pixel is 2.6 mm at 32 px. Roll and verse are safe; handedness is not. diff --git a/docs/CAD/design/mate-connectors/muzzle_variants.py b/docs/CAD/design/mate-connectors/muzzle_variants.py index 06b18c5a7c..f4c9ab33b5 100644 --- a/docs/CAD/design/mate-connectors/muzzle_variants.py +++ b/docs/CAD/design/mate-connectors/muzzle_variants.py @@ -1,4 +1,4 @@ -"""The muzzle has to READ, not just be present — snaporca-wi3z. +"""The muzzle has to READ, not just be present — wi3z. Faithfully scaled, the part's ridge is 11.3 mm on an 83 mm face: 13.6 % of the width. At glyph size that is a scratch. A glyph is a symbol, not a scale model, so the question is how much diff --git a/docs/CAD/design/mate-connectors/relief_sheet.py b/docs/CAD/design/mate-connectors/relief_sheet.py index c7e204129c..4b8bc6aa74 100644 --- a/docs/CAD/design/mate-connectors/relief_sheet.py +++ b/docs/CAD/design/mate-connectors/relief_sheet.py @@ -1,4 +1,4 @@ -"""Flat glyph vs 3D relief, at the elevations that killed the disc — snaporca-wi3z. +"""Flat glyph vs 3D relief, at the elevations that killed the disc — wi3z. The flat study collapsed at 16 deg because anything drawn IN the connector's plane foreshortens by sin(elevation). This renders the SAME bear as its real relief (1508 facets off the supplied male) diff --git a/docs/CAD/design/mate-connectors/simplify_study.py b/docs/CAD/design/mate-connectors/simplify_study.py index 3e1b15923d..c0bbcf63ab 100644 --- a/docs/CAD/design/mate-connectors/simplify_study.py +++ b/docs/CAD/design/mate-connectors/simplify_study.py @@ -1,4 +1,4 @@ -"""Reduce the bear face to the fewest marks that still read at glyph size — snaporca-wi3z. +"""Reduce the bear face to the fewest marks that still read at glyph size — wi3z. Geometry comes from bear_outline.json, which extract_outline.py pulled off the supplied male B-rep's back plate: the outer wire IS the silhouette, the inner wires are the two eyes and the diff --git a/docs/CAD/rig_build_traps.md b/docs/CAD/rig_build_traps.md index ac7829a1a8..b620b244d9 100644 --- a/docs/CAD/rig_build_traps.md +++ b/docs/CAD/rig_build_traps.md @@ -1,13 +1,13 @@ # Rig build traps -The build rig is two long-lived containers, `snaporca-gui` and `orcacad-gui`, one per fork. Each -mounts only its fork's build volume (`snaporca_buildcache` / `orcacad_buildcache`) at +The build rig is two long-lived containers, `snapmaker-gui` and `orcacad-gui`, one per fork. Each +mounts only its fork's build volume (`snapmaker_buildcache` / `orcacad_buildcache`) at `/OrcaSlicer/build`, its fork's `resources/`, and a shots directory — nothing else. They run the binary; they do not build it. Rebuild with `scripts/CAD/build-gui.sh`. | fork repo | project() | deps image | build volume | GUI container | binary | |---|---|---|---|---|---| -| `snaporca` | `Snapmaker_Orca` | `snaporca-deps` | `snaporca_buildcache` | `snaporca-gui` | `snapmaker-orca` | +| `Snapmaker` | `Snapmaker_Orca` | `snapmaker-deps` | `snapmaker_buildcache` | `snapmaker-gui` | `snapmaker-orca` | | `orca_cad` | `OrcaSlicer` | `orcacad-deps` | `orcacad_buildcache` | `orcacad-gui` | `orca-slicer` | `scripts/CAD/build-gui.sh` exists alongside `scripts/CAD/build-gui-incremental.sh` for one reason: it does a @@ -29,7 +29,7 @@ reports an unknown target, and `orca-slicer` / `OrcaSlicer` have been replaced b **Cause.** The GUI image's baked `/OrcaSlicer` tree is the Jun-13 Snapmaker-derived source (`project(Snapmaker_Orca)`, executable `snapmaker-orca`). `orcacad-deps` is layered on -`snaporca-deps`, so even on the mainline fork the baked tree is the other fork's. A `cmake .` +`snapmaker-deps`, so even on the mainline fork the baked tree is the other fork's. A `cmake .` there reconfigures the shared build dir under the wrong project name. **Fix.** Build only via `scripts/CAD/build-gui.sh`, which starts a throwaway container from the deps @@ -64,10 +64,10 @@ Did you initialize submodules?` (the `FATAL_ERROR` guarding `PYBIND11_SOURCE_DIR fork's root `CMakeLists.txt`, near line 948). **Cause.** The deps image predates that requirement. Only the mainline (`orca_cad`) fork has -`deps_src/pybind11` and the requirement; snaporca has neither. +`deps_src/pybind11` and the requirement; Snapmaker has neither. **Fix.** Mount `deps_src` over the baked tree — `scripts/CAD/build-gui.sh` does. Corollary: mounting a -snaporca tree into an `orcacad-deps` build reproduces this error exactly. +Snapmaker tree into an `orcacad-deps` build reproduces this error exactly. --- @@ -96,7 +96,7 @@ you ever configure by hand, run it twice. **Cause.** The cache carries `SLIC3R_CAD=ON`, but the root `CMakeLists.txt` actually configured is a stale baked copy that predates the gate and never runs `add_definitions(-DSLIC3R_CAD)` (the gate is `if (SLIC3R_CAD)` / `add_definitions(-DSLIC3R_CAD)` in the root list — line 179/180 in -snaporca, 319/320 in orca_cad). Every `#ifdef SLIC3R_CAD` block therefore compiles out while the +Snapmaker, 319/320 in orca_cad). Every `#ifdef SLIC3R_CAD` block therefore compiles out while the option still reads ON. **Fix.** Always mount the live `CMakeLists.txt` and `cmake/` — never inherit them from the image. @@ -109,12 +109,12 @@ all mount both. `ninja ` writes `/OrcaSlicer/build/src/Release/`; only `build_linux.sh` additionally packages to `/OrcaSlicer/build/package/bin/`. `orca_cad`'s -`scripts/CAD/start-headless-gui.sh` defaults `BIN` to `src/Release/orca-slicer`, but snaporca's defaults to -`package/bin/snapmaker-orca`. So after a target-only rebuild on snaporca, launching +`scripts/CAD/start-headless-gui.sh` defaults `BIN` to `src/Release/orca-slicer`, but Snapmaker's defaults to +`package/bin/snapmaker-orca`. So after a target-only rebuild on Snapmaker, launching `start-headless-gui.sh` with its default runs the **stale packaged** binary — the change under test is invisible and the session hunts a phantom. Pass `BIN` explicitly: - docker exec -e BIN=/OrcaSlicer/build/src/Release/snapmaker-orca snaporca-gui /OrcaSlicer/scripts/CAD/start-headless-gui.sh + docker exec -e BIN=/OrcaSlicer/build/src/Release/snapmaker-orca snapmaker-gui /OrcaSlicer/scripts/CAD/start-headless-gui.sh `scripts/CAD/build-gui.sh` prints the correct line for the current fork when it finishes. diff --git a/docs/CAD/ux/mockups/gen_offer_table.py b/docs/CAD/ux/mockups/gen_offer_table.py index 38e1ad9c66..9744f9288b 100644 --- a/docs/CAD/ux/mockups/gen_offer_table.py +++ b/docs/CAD/ux/mockups/gen_offer_table.py @@ -121,7 +121,7 @@ def main(): # A verb may carry a NOTE: the reason it exists, emitted as a C++ comment above its row. # Without somewhere to put it, a rationale written into the generated header is deleted by # the next regeneration — which is how the model-mode "Constrain sketch" row came to exist - # in the header and not in the atlas at all (snaporca-ziam). The map exists once; so does + # in the header and not in the atlas at all (ziam). The map exists once; so does # the explanation. for ln in ([v["note"]] if isinstance(v.get("note"), str) else v.get("note") or []): lines.append(f" // {ln}") diff --git a/scripts/CAD/README.md b/scripts/CAD/README.md index d881b6f1db..861b77a976 100644 --- a/scripts/CAD/README.md +++ b/scripts/CAD/README.md @@ -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 diff --git a/scripts/CAD/build-gui-incremental.sh b/scripts/CAD/build-gui-incremental.sh index 1b6d2f2b0b..539a21c973 100755 --- a/scripts/CAD/build-gui-incremental.sh +++ b/scripts/CAD/build-gui-incremental.sh @@ -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) ------------------------------------------------------------- diff --git a/scripts/CAD/build-gui.sh b/scripts/CAD/build-gui.sh index 1858ef5dc7..0a1a86dd5c 100755 --- a/scripts/CAD/build-gui.sh +++ b/scripts/CAD/build-gui.sh @@ -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 diff --git a/scripts/CAD/check-gui-click-edit.py b/scripts/CAD/check-gui-click-edit.py new file mode 100755 index 0000000000..159bdb19f1 --- /dev/null +++ b/scripts/CAD/check-gui-click-edit.py @@ -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()) diff --git a/scripts/CAD/check-gui-context-menu.py b/scripts/CAD/check-gui-context-menu.py index cb16bd6fb6..28e404ecf0 100644 --- a/scripts/CAD/check-gui-context-menu.py +++ b/scripts/CAD/check-gui-context-menu.py @@ -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 diff --git a/scripts/CAD/check-gui-sketching.py b/scripts/CAD/check-gui-sketching.py index bee48f0680..d4ebd1f1ca 100644 --- a/scripts/CAD/check-gui-sketching.py +++ b/scripts/CAD/check-gui-sketching.py @@ -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 diff --git a/scripts/CAD/check-mcp-sketch.py b/scripts/CAD/check-mcp-sketch.py index b0d7ea70e3..2e700921f5 100755 --- a/scripts/CAD/check-mcp-sketch.py +++ b/scripts/CAD/check-mcp-sketch.py @@ -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 # launch with the socket enabled + ORCA_CAD_MCP=/tmp/mcp.sock # 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. diff --git a/scripts/CAD/check-sketch-engine-corpus.py b/scripts/CAD/check-sketch-engine-corpus.py index d9237ca2d0..20eca5d381 100644 --- a/scripts/CAD/check-sketch-engine-corpus.py +++ b/scripts/CAD/check-sketch-engine-corpus.py @@ -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): diff --git a/scripts/CAD/check-sketch-engine.py b/scripts/CAD/check-sketch-engine.py index 2a2ec9d7c9..56fee44568 100755 --- a/scripts/CAD/check-sketch-engine.py +++ b/scripts/CAD/check-sketch-engine.py @@ -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 + ORCA_CAD_MCP=/tmp/mcp.sock python3 scripts/CAD/check-sketch-engine.py [socket] Exit 0 = every rung held. Otherwise the first broken property is named and the run stops. diff --git a/scripts/CAD/focus-loop.sh b/scripts/CAD/focus-loop.sh new file mode 100755 index 0000000000..28cfec37ab --- /dev/null +++ b/scripts/CAD/focus-loop.sh @@ -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" diff --git a/scripts/CAD/run-all-checks.sh b/scripts/CAD/run-all-checks.sh index e2f9664fd0..7928652051 100755 --- a/scripts/CAD/run-all-checks.sh +++ b/scripts/CAD/run-all-checks.sh @@ -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. diff --git a/scripts/CAD/run-kernel-tests.sh b/scripts/CAD/run-kernel-tests.sh index 56a60e8636..ef4356cb65 100755 --- a/scripts/CAD/run-kernel-tests.sh +++ b/scripts/CAD/run-kernel-tests.sh @@ -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 diff --git a/scripts/CAD/start-headless-gui.sh b/scripts/CAD/start-headless-gui.sh index 8ed4c3019b..d9944da8cf 100755 --- a/scripts/CAD/start-headless-gui.sh +++ b/scripts/CAD/start-headless-gui.sh @@ -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 diff --git a/scripts/Dockerfile.deps b/scripts/Dockerfile.deps index f0ca4e4735..295b72028d 100644 --- a/scripts/Dockerfile.deps +++ b/scripts/Dockerfile.deps @@ -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 \ diff --git a/scripts/Dockerfile.deps-assimp b/scripts/Dockerfile.deps-assimp index 6e280e999f..40566885c0 100644 --- a/scripts/Dockerfile.deps-assimp +++ b/scripts/Dockerfile.deps-assimp @@ -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 diff --git a/src/libslic3r/AppConfig.cpp b/src/libslic3r/AppConfig.cpp index 4a6280d3ae..3919c7b578 100644 --- a/src/libslic3r/AppConfig.cpp +++ b/src/libslic3r/AppConfig.cpp @@ -325,7 +325,7 @@ void AppConfig::set_defaults() // Design tab: draw a mate connector as a face rather than as the abstract disc + roll // quadrant. Defaults ON — face orientation is hardwired perception, so the roll and the // verse read without being learned, which no abstract glyph achieves. Turning it off - // restores the conventional CAD representation for users who expect it (snaporca-x0kd). + // restores the conventional CAD representation for users who expect it (x0kd). if (get("design_connector_face_glyph").empty()) set_bool("design_connector_face_glyph", true); #endif diff --git a/src/libslic3r/CAD/CadDocument.cpp b/src/libslic3r/CAD/CadDocument.cpp index fad2c6b827..c20b636ae7 100644 --- a/src/libslic3r/CAD/CadDocument.cpp +++ b/src/libslic3r/CAD/CadDocument.cpp @@ -2142,7 +2142,7 @@ TopoDS_Wire CadDocument::build_sketch_wire(const CadFeature& sketch, bool closed // legacy tail of this function ends in a default rectangle built from width/height, // which for an entity sketch are whatever they happened to be initialised to — so a // sketch entities_to_wire cannot handle (a circle coexisting with a line, two circles: - // snaporca-88v) used to extrude into a box the user never drew, silently. Failing here + // 88v) used to extrude into a box the user never drew, silently. Failing here // costs the caller an error message; falling through cost them wrong geometry that // looked deliberate. The legacy profile/shape paths below are still reached by sketches // that legitimately carry no entities at all. @@ -2751,7 +2751,7 @@ void CadDocument::apply_feature(TopoDS_Shape& result, bool& have_body, // Neutral plane = horizontal plane through the body's bbox bottom, pull direction +Z. // The face pivots about the line where it meets the neutral plane and tilts by the angle. // ponytail: neutral plane / pull direction fixed to world up; pick-based neutral plane - // deferred (same as the datum-plane pick types, snaporca-dgv). + // deferred (same as the datum-plane pick types, dgv). Bnd_Box bb; BRepBndLib::Add(result, bb); Standard_Real xmin, ymin, zmin, xmax, ymax, zmax; bb.Get(xmin, ymin, zmin, xmax, ymax, zmax); @@ -3537,7 +3537,7 @@ void CadDocument::apply_mate(std::vector& bodies, const CadFeature& f) } // Volume of a shape, 0 for anything that isn't a solid we can measure. Used to catch a -// subtraction that removed nothing (snaporca-daf). +// subtraction that removed nothing (daf). static double solid_volume(const TopoDS_Shape& s) { if (s.IsNull()) return 0.0; @@ -3591,7 +3591,7 @@ void CadDocument::route_feature(std::vector& bodies, const CadFeature& // an agent especially — then has no signal at all that the hole it asked for was never // drilled: same body, same volume, ok:true. Measure the volume across the op and refuse // the no-op. Only for removals: every other feature type may legitimately leave the volume - // alone (a Transform certainly does). snaporca-daf. + // alone (a Transform certainly does). daf. const bool removes = f.type == CadFeatureType::Hole || f.type == CadFeatureType::Thread || ((f.type == CadFeatureType::Extrude || f.type == CadFeatureType::Revolve @@ -3625,7 +3625,7 @@ bool CadDocument::recompute() // made a sketch-only design unsaveable AND unopenable: DesignPanel::recompute_guarded syncs // the 3MF recipe only "on success", so nothing was written, and deserialize_recipe ends with // `return recompute()`, so a project that did carry a recipe was refused on load with - // "Could not restore the CAD model" while its features sat correctly in the list. snaporca-mtav. + // "Could not restore the CAD model" while its features sat correctly in the list. mtav. bool any_solid_feature = false; try { // Parametric pass: evaluate document variables, then each feature's expression bindings, diff --git a/src/libslic3r/CAD/SketchSolver.cpp b/src/libslic3r/CAD/SketchSolver.cpp index e206b4bfdd..952f9de16f 100644 --- a/src/libslic3r/CAD/SketchSolver.cpp +++ b/src/libslic3r/CAD/SketchSolver.cpp @@ -403,7 +403,7 @@ static SketchSolveResult solve_system(std::vector& entities, // from the corpse. The fillet degrade ladder hit this on every corner — rung 1 (a // tangent on each leg) is legitimately over-constrained against the legs' own H/V, and // its wreckage then failed rungs 2 and 3, which solve cleanly on their own. The arc - // ended up with no constraints at all and the solver snapped the corner shut. snaporca-pl5. + // ended up with no constraints at all and the solver snapped the corner shut. pl5. if (!out.ok) return out; for (size_t i = 0; i < entities.size(); ++i) { SketchEntity& e = entities[i]; diff --git a/src/slic3r/GUI/CAD/DesignCanvas.cpp b/src/slic3r/GUI/CAD/DesignCanvas.cpp index ef37cb055a..8db2afbbc2 100644 --- a/src/slic3r/GUI/CAD/DesignCanvas.cpp +++ b/src/slic3r/GUI/CAD/DesignCanvas.cpp @@ -83,21 +83,55 @@ DesignCanvas::DesignCanvas(wxWindow* parent) // Onshape-style in-canvas value editor, floating over the GL canvas. The tool hands // us a screen pixel (device px) + a commit/cancel pair; we convert to logical client // px and wrap the callbacks so each one re-solves and re-renders the viewport. - m_inline_editor = std::make_unique(m_canvas_widget); + m_inline_editor = std::make_unique(); + // The tool draws it: it owns the frame's ImGui pass and the render scale. Handing it a raw + // pointer rather than the unique_ptr keeps the ownership where it was. + m_sketch_tool.inline_editor = m_inline_editor.get(); + // SCHEDULE a paint, do not render one. request_repaint() renders SYNCHRONOUSLY on software + // GL, and this callback runs from inside DesignSketchTool::render() — so using it here asks + // for a render from within a render. The frames stopped after nine, which is what a + // re-entrancy guard giving up looks like. Refresh() posts a paint event instead: the current + // frame finishes, the event loop runs (which is where ImGui's queued characters are consumed), + // and the next frame starts clean. + // MEASUREMENT: does a typed character reach the GL canvas at all? Everything downstream of + // this point is known good (ImGui reports want_text=1 and our InputText active), so if these + // lines do not appear the character never got past the panel's CHAR_HOOK / the focus chain, + // and no amount of work inside the field will help. Skips always: a pure observer. + if (m_canvas_widget != nullptr && std::getenv("ORCA_CAD_UXTRACE")) { + m_canvas_widget->Bind(wxEVT_CHAR, [](wxKeyEvent& e) { + fprintf(stderr, "[UX] canvas_char key=%d\n", e.GetKeyCode()); + fflush(stderr); + e.Skip(); + }); + } + m_inline_editor->request_frame = [this] { + // BOTH halves, and the dirty flag first: GLCanvas3D's paint handler returns without + // rendering when the canvas is not marked dirty, so a bare Refresh() posts an event that + // draws nothing and the frames still stop. request_repaint() does exactly this pair on + // the hardware path; what it must NOT do here is its software path, which renders + // synchronously — and this callback runs from inside render(). + if (m_canvas) m_canvas->set_as_dirty(); + if (m_canvas_widget) m_canvas_widget->Refresh(false); + }; m_sketch_tool.on_inline_edit = [this](wxPoint screen_px, double current, const std::string& title, std::function commit, std::function cancel) { if (!m_inline_editor) { if (cancel) cancel(); return; } - // The tool hands us canvas device px; convert to logical client px, then to - // absolute screen coords for the floating editor frame. - const double s = m_canvas_widget ? m_canvas_widget->GetContentScaleFactor() : 1.0; - const wxPoint client_pt(int(screen_px.x / s), int(screen_px.y / s)); - const wxPoint scr = m_canvas_widget ? m_canvas_widget->ClientToScreen(client_pt) : client_pt; + // The tool hands us canvas device px and the field is now drawn IN the canvas, so this + // is already the coordinate space it wants — no conversion to screen coordinates, and no + // window to place there. // Freeze the sketch tool while the field is open so a stray click/move on the GL - // canvas can't draw under the floating editor; released on commit or cancel. + // canvas can't draw under the field; released on commit or cancel. m_sketch_tool.set_inline_busy(true); - m_inline_editor->open(scr, current, title, + // AND PUT THE KEYBOARD ON THE CANVAS. The field is drawn by ImGui, and ImGui is fed from + // GLCanvas3D's own key handler, so a key only reaches it if the canvas is the focused + // widget. That is a focus move WITHIN one window — the toolkit's business, not the window + // manager's, which is the whole point of not being a window any more — but it still has + // to be asked for: after a toolbar click or a tree selection the focus is elsewhere in + // the panel, and the field would sit there taking nothing. + if (m_canvas_widget) m_canvas_widget->SetFocus(); + m_inline_editor->open(screen_px, current, title, [this, commit](double v) { m_sketch_tool.set_inline_busy(false); if (commit) commit(v); @@ -1206,7 +1240,7 @@ void DesignCanvas::set_status_text(const wxString& text, const wxColour& colour) } // SetLabel + Wrap + Fit, in that order and always together. Moving the status out of the panel -// removed the clipping of snaporca-8cc but not the underlying problem: the chip is a top-level +// removed the clipping of 8cc but not the underlying problem: the chip is a top-level // popup that Fit()s to its text, so a long sentence simply grew past the right edge of the canvas // and hung over the window. Wrapping to the room actually available is what makes the earlier // promise — "a sentence can be a sentence" — true at every window width, including the charter's @@ -1240,7 +1274,7 @@ void DesignCanvas::place_status_hud() const wxPoint bl = m_canvas_widget->ClientToScreen( wxPoint(kLeftInset, cs.GetHeight() - hs.GetHeight() - 12)); // No Raise() and no focus juggling: a popup neither takes focus nor falls behind. This was - // caught with SNAPORCA_KEYTRACE — shift+S logged a line, the following R logged nothing, and + // caught with ORCA_CAD_KEYTRACE — shift+S logged a line, the following R logged nothing, and // the only thing between them was the first status update showing this window. if (!m_status_hud->IsShown()) m_status_hud->Show(); // Show before Move (GTK ignores pre-map Move) m_status_hud->Move(bl); @@ -1326,12 +1360,12 @@ void DesignCanvas::delete_selected_sketch_entities() bool DesignCanvas::inline_busy() const { - // The TOOL's flag says a value is pending; the FRAME being mapped says a window is on screen - // holding the keyboard. Either one means "a field is up", and only the union of the two is - // safe to route Esc by: the flag alone went false while the frame was still mapped, which is - // the orphan that swallowed every key with nothing able to close it. + // Two sources, still: the TOOL's flag says a value is pending, the editor says a field is + // drawn. They agree now that the field is not a window — the orphan state (logically closed, + // still on screen, still eating keys) cannot be represented when there is nothing to leave + // mapped — but the union costs nothing and is the honest question to ask. return m_sketch_tool.inline_busy() - || (m_inline_editor && m_inline_editor->is_mapped()); + || (m_inline_editor && m_inline_editor->is_open()); } bool DesignCanvas::inline_has_focus() const @@ -1402,25 +1436,17 @@ void DesignCanvas::open_inline_value(double current, std::function if (!m_inline_editor || !m_canvas_widget) { if (cancel) cancel(); return; } // Host-driven value entry (committed-feature Constrain path): the trigger is a toolbar // button. Anchor the field OVER the picked geometry (same as the draw-then-edit tools) when - // the tool can project it; else fall back to the viewport centre, where the sketch is in - // view. GetScreenRect collapses GetClientSize()+ClientToScreen() into one call; if the GL - // canvas reports degenerate geometry (transiently, right after a re-layout), fall back to the - // always-realised top-level window so the editor never lands in the top-left corner. - wxRect r = m_canvas_widget->GetScreenRect(); - if (r.GetWidth() <= 1 || r.GetHeight() <= 1) { - if (wxWindow* top = wxGetTopLevelParent(m_canvas_widget)) - r = top->GetScreenRect(); - } - wxPoint scr(r.GetLeft() + r.GetWidth() / 2, r.GetTop() + r.GetHeight() / 2); - wxPoint anchor; - if (m_sketch_tool.constrain_value_anchor(anchor)) { // device px in the canvas viewport - const double s = m_canvas_widget->GetContentScaleFactor(); - scr = m_canvas_widget->ClientToScreen(wxPoint(int(anchor.x / s), int(anchor.y / s))); - } - // Freeze the canvas so focus-follows-mouse can't steal keyboard focus off the field — the - // same fix the draw-then-edit path uses (cursor focus stays on the field, no pre-click). + // the tool can project it; else fall back to the middle of the canvas, where the sketch is + // in view. Everything here is canvas DEVICE px, the space the field is drawn in. + const wxSize cs = m_canvas_widget->GetClientSize(); + const double sf = m_canvas_widget->GetContentScaleFactor(); + wxPoint anchor(int(cs.GetWidth() * sf) / 2, int(cs.GetHeight() * sf) / 2); + m_sketch_tool.constrain_value_anchor(anchor); // device px in the canvas viewport m_sketch_tool.set_inline_busy(true); - m_inline_editor->open(scr, current, "", + m_canvas_widget->SetFocus(); // same reason as the draw-then-edit path: ImGui reads the + // canvas's key events, so the canvas must be the focused widget + + m_inline_editor->open(anchor, current, "", [this, commit](double v) { m_sketch_tool.set_inline_busy(false); if (commit) commit(v); diff --git a/src/slic3r/GUI/CAD/DesignCanvas.hpp b/src/slic3r/GUI/CAD/DesignCanvas.hpp index 1da8a17cf4..fab2e556e3 100644 --- a/src/slic3r/GUI/CAD/DesignCanvas.hpp +++ b/src/slic3r/GUI/CAD/DesignCanvas.hpp @@ -94,7 +94,7 @@ public: void set_on_segment_drawn(std::function cb); void set_on_cursor_metrics(std::function cb); void set_on_solve_state(std::function cb); // dof, ok, has_constraints - // Live per-step guidance from the armed sketch tool (mode, step, picks). snaporca-1c0c. + // Live per-step guidance from the armed sketch tool (mode, step, picks). 1c0c. void set_on_sketch_step(std::function cb); void apply_segment_length(double len); // exact length, then commit & repaint void keep_segment_as_drawn(); // commit as-drawn & repaint @@ -219,12 +219,12 @@ public: void set_highlight_sketches(std::vector> hl); void set_datum_planes(std::vector planes, std::vector sizes = {}); // draw datum/reference planes (u/v extents) - // Mate connectors, drawn as frames so their verse and polarity are visible (snaporca-wgsc). + // Mate connectors, drawn as frames so their verse and polarity are visible (wgsc). void set_mate_connectors(std::vector g); void set_mate_links(std::vector> l); void set_body_highlight(bool on); // tint the solid when its feature is tree-selected // The status line, shown along the BASE OF THE VIEWPORT rather than in the side panel: - // the panel clips it at ~73 characters with no warning (snaporca-8cc), the viewport's + // the panel clips it at ~73 characters with no warning (8cc), the viewport's // bottom margin has the whole window width to spare. Empty text hides it. void set_status_text(const wxString& text, const wxColour& colour); // Take the status line down / bring it back when the Design page leaves and re-enters view. diff --git a/src/slic3r/GUI/CAD/DesignPanel.cpp b/src/slic3r/GUI/CAD/DesignPanel.cpp index 4165d752db..33326fd5ca 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.cpp +++ b/src/slic3r/GUI/CAD/DesignPanel.cpp @@ -62,7 +62,7 @@ #include "slic3r/GUI/MainFrame.hpp" #include "slic3r/GUI/GUI_ObjectList.hpp" -// English-only pin for the Design tab (see snaporca-design-ux-contract): one lever +// English-only pin for the Design tab (see design-ux-contract): one lever // de-translates this whole TU so our strings never half-translate against the host's // localized chrome. Host UI still follows the app locale; only this tab is pinned EN. // GOTCHA: every _L(...) in this file must take a STRING LITERAL (FromUTF8 wants const char*). @@ -452,7 +452,7 @@ DesignPanel::DesignPanel(wxWindow* parent) } }; m_keys_sketch['Q'] = [this] { - // With geometry selected, Q converts THAT geometry (snaporca-6zic) — the reading + // With geometry selected, Q converts THAT geometry (6zic) — the reading // everyone arrives with from other sketchers. With nothing selected it keeps its // old meaning: arm construction for whatever you draw next. if (m_viewport && m_viewport->is_sketching() && @@ -547,7 +547,7 @@ DesignPanel::DesignPanel(wxWindow* parent) // still BUILT — that is what registers its "fly:#" address and its Shift+key — // but it is never placed on the bar. Hiding rather than skipping construction is deliberate: // the addresses are created inside the widget-building loops, so not building would silently - // delete 42 verbs from the offer while they still rendered. snaporca-7ih records the cleanup + // delete 42 verbs from the offer while they still rendered. 7ih records the cleanup // that lets the construction go away too. // What stays: the two doc-row imports (consumed by add_doc below) and the view controls, // which are chrome_only in the atlas and so have no offer row to fall back on. @@ -577,7 +577,7 @@ DesignPanel::DesignPanel(wxWindow* parent) // outright for a family the bar no longer carries. It used to sit INSIDE the build // loop, so a retired family still had to be constructed and then Hide()n: skipping it // would have deleted 42 verbs from the offer while their rows still rendered and did - // nothing when picked. snaporca-7ih. + // nothing when picked. 7ih. // Keyed on "fly:#" so the generated table can name a variant without the // item struct growing a field at 26 call sites. for (size_t i = 0; i < vars.size(); ++i) { @@ -746,7 +746,7 @@ DesignPanel::DesignPanel(wxWindow* parent) // Thicken and were then asked to point at something. Reached from the offer the // verb is invoked ON a face, so discarding it opened the card reading "(pick a // solid face)" over an immediate "thicken: face not found" — the user pointed at - // the face and the card said it could not find one. snaporca-kgx. + // the face and the card said it could not find one. kgx. // The index is per-body, so it only survives if the body combo landed on the body // it came from; selected_body_default() above returns exactly that when valid. if (m_thicken_body->GetSelection() != m_sel_solid_body) @@ -1267,7 +1267,7 @@ DesignPanel::DesignPanel(wxWindow* parent) // The offer reaches each tool by its ratified address; without these the offer could // name a family but only ever arm its FIRST tool: picking "Rectangle" ran key:R and // gave you a corner rectangle, with oblique and rounded unreachable. Keyed on the icon - // id (already unique per family) so no call site grows an argument. snaporca-6vs. + // id (already unique per family) so no call site grows an argument. 6vs. for (size_t i = 0; i < vars.size(); ++i) { const DesignSketchTool::Mode mode = vars[i].mode; const wxString hint = vars[i].hint; @@ -1423,7 +1423,7 @@ DesignPanel::DesignPanel(wxWindow* parent) // the offer's Create > Polygon submenu. They used to sit inline in this row, then in a // sidebar card; both put the choice somewhere you had to leave the geometry to reach, // and the count cannot be recovered afterwards (a drawn polygon's inline editor offers - // Side and Angle, never the count). snaporca-e1p. + // Side and Angle, never the count). e1p. auto arm_polygon = [this, select_tool] { push_polygon_params(); select_tool(DesignSketchTool::Mode::Polygon, @@ -1444,7 +1444,7 @@ DesignPanel::DesignPanel(wxWindow* parent) // where you chose the tool — not behind a card you must open to discover it existed. // Each address opens the tool exactly as its shortcut does, then says which one. // The members are read at INVOCATION, not capture: the cards are built after this row. - // snaporca-e1p. + // e1p. auto open_feature = [this](int key) { auto it = m_keys_feature.find(key); if (it != m_keys_feature.end() && it->second) it->second(); @@ -1745,7 +1745,7 @@ DesignPanel::DesignPanel(wxWindow* parent) // NO plane row. A sketch takes its plane from what is picked in the VIEWPORT — a planar face // on a solid, or one of the reference-plane ghosts clicked in 3D — resolved by // sketch_plane_from_selection(). A three-row XY/XZ/YZ combo could not express either of those - // targets, so it displayed a value that was at best redundant and at worst false. snaporca-e1p. + // targets, so it displayed a value that was at best redundant and at worst false. e1p. m_width = make_spin(m_cards, 20); form->Add(new wxStaticText(m_cards, wxID_ANY, _L("Width / X")), 0, wxALIGN_CENTER_VERTICAL); @@ -1808,7 +1808,7 @@ DesignPanel::DesignPanel(wxWindow* parent) // tool — the offer's Create > Polygon submenu names the common side counts and the two // fits, and arming from there sets both. A spin field on the left could not be reached // without leaving the geometry, and the count is unrecoverable afterwards: the inline - // editor a drawn polygon opens offers Side and Angle, never the count. snaporca-e1p. + // editor a drawn polygon opens offers Side and Angle, never the count. e1p. } // --- Extrude dialog (consumes the selected sketch) --- @@ -3027,7 +3027,7 @@ DesignPanel::DesignPanel(wxWindow* parent) // on a solid, or one of the reference-plane ghosts — because that is where the user is // looking and pointing. A combo duplicated that decision somewhere the geometry could not // see it, and once a face could be picked it went further and displayed a stale row that - // contradicted the real target. snaporca-e1p. + // contradicted the real target. e1p. // Kept as a member, not a local: the card has to be able to STOP saying this. It asked // for a plane even when one had just been picked, directly contradicting the status line // two inches below it, which by then read "Sketching on XZ". @@ -3138,7 +3138,7 @@ DesignPanel::DesignPanel(wxWindow* parent) // "slow double-click renames" the old comment promised does not survive wxGTK, which fires // ITEM_ACTIVATED first), none of the seven header icons renames, and F2 is a function key // nothing announces. A user who wants to name a sketch tries the row, and now the row - // answers. snaporca-rename. + // answers. rename. m_tree->Bind(wxEVT_TREE_ITEM_RIGHT_CLICK, [this](wxTreeEvent& e) { m_tree->SelectItem(e.GetItem()); // right-click targets what it points at const int sel = tree_selection(); @@ -3622,7 +3622,7 @@ DesignPanel::DesignPanel(wxWindow* parent) // Selection no longer writes the status line: on_sketch_step owns it, says the same thing // for Select mode and — unlike this callback, which also fired while an edit-op mirrored its // picks into the selection — never claims "N selected, Delete removes them" in the middle of - // a Mirror gesture, where Delete does nothing of the sort. snaporca-1c0c. + // a Mirror gesture, where Delete does nothing of the sort. 1c0c. // Onshape flow: clicking inside a closed-loop face commits the sketch and opens // the Extrude dialog (with a ghost preview) targeting that sketch. @@ -3667,7 +3667,7 @@ DesignPanel::DesignPanel(wxWindow* parent) // exists to remove. Only a stroke hit carries an entity (an interior click is a region, // not a line), so a click inside a loop deliberately leaves the field alone rather than // resetting it to something arbitrary. The sketch picker follows the same pick, so - // pointing at a line in a different sketch retargets both together. snaporca-3648. + // pointing at a line in a different sketch retargets both together. 3648. if (m_active == Tool::Rib && entity >= 0) { if (m_rib_sketch != nullptr) for (unsigned i = 0; i < m_rib_sketch->GetCount(); ++i) @@ -3741,7 +3741,7 @@ DesignPanel::DesignPanel(wxWindow* parent) m_sel_solid_edge = (level == 3) ? edge : -1; m_sel_solid_vertex = (level == 4); // Keep the hit face even at whole-body level: the cycle's first click means "this body", - // but the user pointed AT a face and a sketch should be able to use it. snaporca-3a2. + // but the user pointed AT a face and a sketch should be able to use it. 3a2. m_pick_face_body = (level >= 1) ? body : -1; m_pick_face = (level >= 1) ? face : -1; // Last pick wins: selecting a solid drops any stale committed-sketch loop selection. @@ -3762,7 +3762,7 @@ DesignPanel::DesignPanel(wxWindow* parent) if (m_active == Tool::Dressup) { sync_dressup_target(); update_fillet_gizmo(); refresh_preview(); } // Boolean card open: the VIEWPORT is how you choose the two operands. Until now they // could only come from two combos — the one control the charter names for this tool - // (e1p item 4), and the same pair snaporca-7xx caught silently resolving every row to + // (e1p item 4), and the same pair 7xx caught silently resolving every row to // index 0. The highlight already flowed card -> viewport; this closes the loop the // other way. First pick is the target (kept), second is the tool (consumed); the // combos mirror both, so the typed half of L2 still works and still round-trips. @@ -3787,7 +3787,7 @@ DesignPanel::DesignPanel(wxWindow* parent) } } // Mirror card open: the body you point at is the body that gets mirrored. Same one-way - // flow Boolean had (snaporca-310o) and the same fix — the combo stays as the typed half. + // flow Boolean had (310o) and the same fix — the combo stays as the typed half. // Only one operand here, so there is no slot to alternate and no swap to do. if (m_active == Tool::Mirror && m_sel_solid_body >= 0 && m_mirror_body != nullptr && m_sel_solid_body < int(m_mirror_body->GetCount())) { @@ -3914,12 +3914,12 @@ DesignPanel::DesignPanel(wxWindow* parent) m_status->SetForegroundColour(wxNullColour); const int nb = int(m_doc.bodies.size()); const wxString bodytag = (nb > 1) ? wxString::Format(_L("Body %d "), body + 1) : wxString(); - // Each sub-element line ends by naming the NEXT click (snaporca-gem). Escalation to the + // Each sub-element line ends by naming the NEXT click (gem). Escalation to the // whole body is a gesture nothing on screen would otherwise reveal, and the status line // is the only surface that can teach it at the moment it applies. It REPLACES the old // per-level verb hints ("right-click to push/pull it", "Fillet/Chamfer to dress it") // rather than joining them: the line is clipped at the panel edge past ~55 characters - // (set_status's Wrap() does not take effect — snaporca-8cc), and those verbs are shown + // (set_status's Wrap() does not take effect — 8cc), and those verbs are shown // with their icons in the offer anyway, while this gesture is shown nowhere else. // Both clauses fit now that the line is drawn over the viewport instead of squeezed // into the panel. Say "what applies to it", never "verbs" — that is this codebase's @@ -4173,18 +4173,25 @@ DesignPanel::DesignPanel(wxWindow* parent) // select_tool() is what the sketch keys call — so the first letter after entering // sketch mode fell through to the feature map, matched nothing (feature keys are // Shift+letter), and did nothing. The mouse worked only because the toolbar flyout - // reaches select_tool() directly. That is why all 17 keys read as dead. snaporca-0ud. + // reaches select_tool() directly. That is why all 17 keys read as dead. 0ud. const bool sketch_mode = (m_ui_mode == UiMode::Sketch); // Never steal editing keys from a focused text field or an open in-canvas value field — // Delete/Ctrl+Z there must edit the text, not the model. const bool in_text = (dynamic_cast(wxWindow::FindFocus()) != nullptr) || (m_viewport && m_viewport->inline_busy()); - if (getenv("SNAPORCA_KEYTRACE")) { + if (getenv("ORCA_CAD_KEYTRACE")) { wxWindow* fw = wxWindow::FindFocus(); fprintf(stderr, "[KEYTRACE] key=%d ui_mode=%d is_sketching=%d in_text=%d inline_busy=%d focus=%s\n", key, int(m_ui_mode), (m_viewport && m_viewport->is_sketching()) ? 1 : 0, in_text ? 1 : 0, (m_viewport && m_viewport->inline_busy()) ? 1 : 0, - fw ? (const char*) fw->GetClassInfo()->GetClassName() : "(none)"); + // wxString, not a cast: GetClassName() returns const wxChar* — wchar_t* in + // this build — and casting THAT to const char* and printing it with %s emits + // the first byte and stops at the padding NUL. Every focus= field this tracer + // has ever printed was a single letter: "wxGLCanvas" came out as "w", and so + // did "wxWindow". An instrument that silently truncates its most important + // field is worse than no instrument, and this one was trusted for a whole + // day's diagnosis. + fw ? wxString(fw->GetClassInfo()->GetClassName()).utf8_str().data() : "(none)"); fflush(stderr); } @@ -4205,6 +4212,11 @@ DesignPanel::DesignPanel(wxWindow* parent) m_viewport->inline_commit(); return; } + // NO forwarding here any more. The field is drawn INSIDE the GL canvas now, so it + // is fed the way every other ImGui widget in this app is fed: GLCanvas3D::on_char -> + // ImGuiWrapper::update_key_data -> io.AddInputCharacter. Re-adding a panel-side + // forwarder would also mask whether that path works, which is exactly what is being + // measured. // Esc is NOT special-cased here any more: escape() routes it, and the open field is // exactly what CadLevel::Transient means, so it closes the field and stops there. } @@ -4244,7 +4256,7 @@ DesignPanel::DesignPanel(wxWindow* parent) // Delete — the selected sketch entities (or the last drawn one if none is selected), or the // selected feature in Feature mode. Focus-independent, same reason as undo above. // WXK_BACK too: on a keyboard whose Del is a chord (every laptop this runs on), Del is - // the one destructive key nobody can reach, and Backspace is what users press. snaporca-oql1. + // the one destructive key nobody can reach, and Backspace is what users press. oql1. if (!in_text && (key == WXK_DELETE || (key == WXK_BACK && sketching))) { if (sketching) { m_viewport->delete_selected_or_last_sketch_entity(); return; } if (m_ui_mode == UiMode::Feature && m_active == Tool::None @@ -4439,7 +4451,7 @@ void DesignPanel::set_ui_mode(UiMode m) if (m != UiMode::Sketch) m_sketch_on.clear(); // no stale "on the picked face" on the next hint // The DoF readout describes a SKETCH's constraint state, so it means nothing back in Feature // mode — where it nonetheless stayed on screen after every Confirm, Cancel and Escape - // (snaporca-752). Cleared here rather than at those three exits because this is the one place + // (752). Cleared here rather than at those three exits because this is the one place // all of them pass through, and a fourth exit added later would otherwise reintroduce it. // Constrain mode keeps it: that is where the number is the whole point. if (m == UiMode::Feature && m_dof_status != nullptr) { @@ -4679,7 +4691,7 @@ static void run_off_ui_thread(wxWindow* parent, const wxString& message, const s // // This used to be written in exactly ONE place — on_commit(), as a side effect of Commit to // Plate — so a user who modelled for an hour and pressed Ctrl+S saved a project containing no -// feature history at all, and the app reported success (snaporca-vjk5). The 3MF exporter was +// feature history at all, and the app reported success (vjk5). The 3MF exporter was // never at fault: nothing had handed it a recipe. // // Every save path (Ctrl+S, Save As, autosave, crash recovery) reads model.cad_recipe, so @@ -5068,7 +5080,7 @@ void DesignPanel::on_add_extrude() } else if (extrude_uses_loop()) { // Extrude just the selected loop (its entity subset), leaving the source sketch's // other loops intact and still selectable. - if (::getenv("SNAPORCA_PICK_TRACE")) + if (::getenv("ORCA_CAD_PICK_TRACE")) std::fprintf(stderr, "[pick] on_add_extrude: feat=%d reg=%d ents=%zu\n", m_extrude_sketch_ref, m_sel_sketch_region, m_viewport->selected_loop_entities().size()); @@ -5140,11 +5152,11 @@ void DesignPanel::on_add_dressup() // Hole and Thread LATCH the geometry they were opened or picked on; Thicken / Shell / Draft read // the live selection instead. Both models are right for what they are — a placement tool with its // own plane state, versus an operation whose operand IS the selected face — and the latch is the -// kinder of the two now that a click on empty canvas clears the selection (snaporca-od0): a stray +// kinder of the two now that a click on empty canvas clears the selection (od0): a stray // click costs a Thicken pick, and costs a Hole nothing. What was missing is that nothing in the // Hole/Thread card NAMED the latched face, so after such a click the only words on screen were // the viewport's "Nothing selected" — over a ghost still drawn on the face Confirm would drill. -// That reads as a contradiction and was filed as one (snaporca-200). The card now says what it +// That reads as a contradiction and was filed as one (200). The card now says what it // holds, the way the other three cards already do. Pass -1 for "none, using the plane dropdown". void DesignPanel::set_hole_target_label(int face) { @@ -6013,7 +6025,7 @@ SketchPlane DesignPanel::plane_from_choice(int row) const // clicking one of the ghost planes in 3D (on_datum_base_picked) rather than by opening the combo. // Before this, a picked face was ignored and the only way onto it was to build a Coincident datum // plane first and then find it in a dropdown — three steps and a junk feature in the tree for the -// most common gesture in solid modelling. snaporca-3a2. +// most common gesture in solid modelling. 3a2. SketchPlane DesignPanel::sketch_plane_from_selection(wxString& what) const { SketchPlane p; @@ -6068,7 +6080,7 @@ bool DesignPanel::sketch_map_applies() const // actually REACHED, so the menu describes what is highlighted — a header that names a face while // the whole body is lit would be lying, and this menu's whole value is that it tells the truth // about the selection. (Sketching on the face you merely clicked is unaffected: that path is -// sketch_plane_from_selection, which deliberately uses m_pick_face. snaporca-3a2.) +// sketch_plane_from_selection, which deliberately uses m_pick_face. 3a2.) int DesignPanel::offer_selection_kind() const { if (sketch_map_applies()) { @@ -6191,7 +6203,7 @@ void DesignPanel::set_status(const wxString& text) // sets the colour on it just before calling here, so this stays the one place that knows // both. What the user reads is drawn along the BASE OF THE VIEWPORT: in the panel the line // was clipped at ~73 characters with no warning and no wrap (Wrap() never took effect — - // snaporca-8cc), which silently length-limited every hint in the tab. The viewport's bottom + // 8cc), which silently length-limited every hint in the tab. The viewport's bottom // margin has the whole window width, so a sentence can be a sentence. if (m_viewport != nullptr) { // wxNullColour means "no opinion", and the dark default text colour is nearly invisible @@ -6206,7 +6218,7 @@ void DesignPanel::set_status(const wxString& text) } -// The sentence for the step the armed sketch tool is on (snaporca-1c0c). One table, so a tool's +// The sentence for the step the armed sketch tool is on (1c0c). One table, so a tool's // gesture is described in one place and the description cannot drift from the code that reads the // clicks: the step counts here are the ones DesignSketchTool::render previews and on_mouse // consumes. `step` = anchors already placed (edit-ops: 0 none, 1 first pick down, 2 ready to @@ -6402,7 +6414,7 @@ wxMenuItem* DesignPanel::append_offer_item(wxMenu* menu, int id, const wxString& // honest source for that is the loop that builds the rows. static void offer_trace(const char* fmt, ...) { - static const bool on = std::getenv("SNAPORCA_KEYTRACE") != nullptr; + static const bool on = std::getenv("ORCA_CAD_KEYTRACE") != nullptr; if (!on) return; va_list ap; va_start(ap, fmt); @@ -6418,13 +6430,13 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos) const int kind = offer_selection_kind(); const uint32_t bit = offer_bit(OfferSel(kind)); // Which verb MAP applies is a question about the MODE, not about whether a session is - // running — the same distinction the keyboard already had to learn (snaporca-0ud). Gated on + // running — the same distinction the keyboard already had to learn (0ud). Gated on // is_sketching() the offer opened on entering a sketch showing the FEATURE rows, every one // of them refusing the sketch selection, so it read as a menu of nine dead entries. const bool sketching = sketch_map_applies(); // The offer ladder reads THIS, not the pixels: the trace is emitted from the same loop that // builds the menu, so it cannot drift from what the user is shown. Gated on the existing - // SNAPORCA_KEYTRACE so a rig run needs one env var, not two. snaporca-. + // ORCA_CAD_KEYTRACE so a rig run needs one env var, not two. . offer_trace("open kind=%d sketching=%d bodies=%d", kind, sketching ? 1 : 0, int(m_doc.bodies.size())); @@ -6539,7 +6551,7 @@ void DesignPanel::show_offer_menu(const wxPoint& screen_pos) } } - // --- Mate palette section (snaporca-lukg part B) --- + // --- Mate palette section (lukg part B) --- // Fed by CadDocument::mate_options() so the offer can never disagree with the kernel about // which assembly mates a connector pair admits. Shown only when the document holds at least // two ENABLED CoordSys features: below that the whole section would be one permanently dead @@ -7008,7 +7020,7 @@ wxString DesignPanel::idle_hint() const : _L("No solid yet — select a sketch and right-click it to Extrude."); } -// The Design tab is no longer the visible page (snaporca-dlj). The status line is a popup floating +// The Design tab is no longer the visible page (dlj). The status line is a popup floating // over the GL canvas, so it does NOT go away when this page does — it stayed up over Prepare and // over the home screen, still reading like a live Design selection ("selected (whole body) — // right-click for what applies to it") on a tab that has no such selection and no such menu. @@ -7111,7 +7123,7 @@ void DesignPanel::refresh_tree() // recompute() returns FALSE for a document that has no solid ("no solid-producing features", // CadDocument.cpp) — which is precisely a document the user has only drawn sketches in. So // drawing a profile, pressing Confirm and saving wrote a 3MF with no orca_cad.bin in it - // at all, and the app reported success: the whole design was gone on reopen (snaporca-mtav). + // at all, and the app reported success: the whole design was gone on reopen (mtav). // The three sites that say "a lone sketch yields an empty body; that is expected" call // m_doc.recompute() directly and so never reached the sync either. One hook here covers all // of them, including the live sketch tool's own commit path. @@ -9080,7 +9092,7 @@ void DesignPanel::load_feature_into_dialog(const CadFeature& f) m_hole_through->SetValue(f.hole_through); m_hole_x->SetValue(f.hole_x); m_hole_y->SetValue(f.hole_y); - // Re-latch the on-face state FROM THE STORED FEATURE (snaporca-uif9). m_hole_on_face is + // Re-latch the on-face state FROM THE STORED FEATURE (uif9). m_hole_on_face is // only ever cleared by the Hole flyout and by the plane combo, so after any on-face hole // it stays true — and a re-edit then drilled on whatever face was latched last, which may // be a different face, a different body, or a body since rebuilt. f is the only source @@ -9111,7 +9123,7 @@ void DesignPanel::load_feature_into_dialog(const CadFeature& f) m_thread_x->SetValue(f.thread_x); m_thread_y->SetValue(f.thread_y); if (m_thread_std) m_thread_std->SetSelection(0); // Custom: spins reflect the stored feature - // Same latch, same failure, same fix as Hole above (snaporca-uif9). + // Same latch, same failure, same fix as Hole above (uif9). m_thread_on_face = !is_base_plane(f.plane, m_doc.modeling_origin); m_thread_face_plane = f.plane; m_thread_face_body = m_thread_on_face ? f.target_body : -1; @@ -9690,7 +9702,7 @@ CadFeature DesignPanel::build_candidate(Tool t) const // The plane is STRUCTURAL, like Extrude's profile source. While EDITING it is preserved // from the seeded original — the card carries no plane control and the old combo silently // collapsed a face plane to a base plane through the modeling origin. While ADDING it - // comes from what is picked in the viewport. snaporca-e1p. + // comes from what is picked in the viewport. e1p. if (!editing) { wxString where; f.plane = sketch_plane_from_selection(where); } f.width = m_width->GetValue(); f.height = m_height->GetValue(); @@ -10020,7 +10032,7 @@ void DesignPanel::update_fillet_gizmo() } // Push the active Hole card's plane + position + diameter/depth to the viewport gizmo. -// Grey the FEATURE buttons whose tool cannot run yet, and say why in the tooltip (snaporca-o9j). +// Grey the FEATURE buttons whose tool cannot run yet, and say why in the tooltip (o9j). // Tommaso reported the array controls as MISSING; they were not, but Pattern with no body // accepted the click, opened nothing, and wrote its refusal somewhere other than where the click // happened — from the user's seat that is indistinguishable from a dead button. A control that diff --git a/src/slic3r/GUI/CAD/DesignPanel.hpp b/src/slic3r/GUI/CAD/DesignPanel.hpp index 60003ff50c..da812f3c2e 100644 --- a/src/slic3r/GUI/CAD/DesignPanel.hpp +++ b/src/slic3r/GUI/CAD/DesignPanel.hpp @@ -57,7 +57,7 @@ public: void clear_document(); // New Project / Open Project: drop the document with the project // Rebuild off the UI thread (progress dialog only if it turns out to be slow), so a feature // op on a heavy imported solid does not freeze the window. Returns m_doc.recompute()'s result. - // Push the document's recipe into the Model so ANY save path persists it (snaporca-vjk5). + // Push the document's recipe into the Model so ANY save path persists it (vjk5). void sync_recipe_to_model(); bool recompute_guarded(const wxString& message); @@ -173,7 +173,7 @@ private: // Which body a tool should act on when it opens: the one picked in the VIEWPORT, else // the first. Selection comes first and the tool consumes it — every body combo used to // default to index 0, so picking body 3 and opening Mirror silently mirrored body 1. - // Clamped to the list, so it is safe to hand straight to SetSelection. snaporca-e1p. + // Clamped to the list, so it is safe to hand straight to SetSelection. e1p. int selected_body_default() const; void populate_body_choices(int as_of_feature = -1); // Fill `c` with the bodies as they existed just before `as_of_feature` and select @@ -279,7 +279,7 @@ private: // The plane the Thread tool builds on: a picked cylindrical face (axis) or the dropdown. SketchPlane thread_plane() const; // Name the geometry the card has LATCHED, so it never has to be inferred from the viewport. - // Pass -1 for "none, falling back to the plane dropdown". See snaporca-200. + // Pass -1 for "none, falling back to the plane dropdown". See 200. void set_hole_target_label(int face); void set_thread_target_label(int face, int edge); CadFeature build_candidate(Tool t) const; @@ -489,13 +489,13 @@ private: // Polygon's two parameters are chosen FROM THE TOOL, in the offer's Polygon submenu, not // from a card on the left: the side count cannot be edited after drawing (the inline editor // offers Side and Angle only), so it has to be settled at the moment the tool is armed — - // which is exactly where the offer already is. snaporca-e1p. + // which is exactly where the offer already is. e1p. int m_poly_sides{6}; // 3..64; the submenu names the common ones bool m_poly_circumscribed{false}; // Which reference plane a sketch falls back to when no face is picked: 0/1/2 = XY/XZ/YZ, // >=3 indexes resolve_datum_planes(). Set by CLICKING a ghost plane in the viewport — there is - // deliberately no dropdown for it. snaporca-e1p. + // deliberately no dropdown for it. e1p. int m_ref_plane{0}; // m_ref_plane is always a VALID plane, so it cannot itself distinguish "the user chose XY" // from "nobody has chosen anything yet". This does. @@ -703,7 +703,7 @@ private: // The face actually under the last solid click, INDEPENDENT of the whole/face/edge cycle level. // The first click on a solid selects the WHOLE body, but the ray has already resolved which face // it hit and the callback passes it. "Sketch on the face I clicked" must not require discovering - // that a second click refines the selection, so keep it instead of throwing it away. snaporca-3a2. + // that a second click refines the selection, so keep it instead of throwing it away. 3a2. int m_pick_face_body{-1}; int m_pick_face{-1}; // What the live sketch was actually opened on ("the picked face", "XY", a datum's name), so the @@ -767,7 +767,7 @@ private: double m_hole_umin{0}, m_hole_umax{0}, m_hole_vmin{0}, m_hole_vmax{0}; // Says which face the latch above is holding. Thicken/Shell/Draft show theirs because their // face IS the live selection; this one has to be shown precisely BECAUSE it is not, and the - // status line goes on saying "Nothing selected" while the ghost keeps drilling. snaporca-200. + // status line goes on saying "Nothing selected" while the ghost keeps drilling. 200. wxStaticText* m_hole_target_label{nullptr}; ComboBox* m_thread_plane{nullptr}; @@ -885,7 +885,7 @@ private: // The guidance sentence for the step the armed sketch tool is on, kept so a transient // readout (the live length/angle while a segment is being dragged) can be appended to it // instead of replacing it — the guidance used to vanish on the first mouse move after a - // click, which is precisely when it is needed. snaporca-1c0c. + // click, which is precisely when it is needed. 1c0c. wxString m_sketch_step; // mode is a DesignSketchTool::Mode; passed as an int because this header deliberately does // not include the tool's, and the .cpp (which does) casts it back. diff --git a/src/slic3r/GUI/CAD/DesignSketchTool.cpp b/src/slic3r/GUI/CAD/DesignSketchTool.cpp index b0e557cd6c..716df658ed 100644 --- a/src/slic3r/GUI/CAD/DesignSketchTool.cpp +++ b/src/slic3r/GUI/CAD/DesignSketchTool.cpp @@ -2,6 +2,7 @@ #include "slic3r/GUI/GLCanvas3D.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/ImGuiWrapper.hpp" +#include "slic3r/GUI/CAD/SketchInlineEditor.hpp" #include "slic3r/GUI/Plater.hpp" #include @@ -307,7 +308,7 @@ void DesignSketchTool::set_tool(Mode mode) void DesignSketchTool::cancel() { - close_session_chrome(); // same orphaned-field freeze as finish() — see snaporca-yce + close_session_chrome(); // same orphaned-field freeze as finish() — see yce m_active = false; m_step_mode_last = -1; m_points.clear(); @@ -452,7 +453,7 @@ void DesignSketchTool::delete_selected() // now-deleted entity and freeze the flow" — it was simply never called from here. Measured: // delete a rectangle whose Width/Height were still queued, draw a circle, type its radius — // the field opens, the digits go in, and the radius does not move, because the field belongs - // to a rectangle that no longer exists. snaporca-ua9g. + // to a rectangle that no longer exists. ua9g. reset_autoedit(); // And re-solve, so the sketch's reported degrees of freedom describe the sketch that is @@ -462,7 +463,7 @@ void DesignSketchTool::delete_selected() if (on_selection_changed) on_selection_changed(0); } -// Convert the selection to/from construction geometry (snaporca-6zic). The Construction +// Convert the selection to/from construction geometry (6zic). The Construction // checkbox only ever set the mode for what you draw NEXT, so a line drawn as real geometry // could never become a guide, nor a guide become real. Whole Feature groups flip together: // a rectangle is four Line entities and converting three of them is never what was meant. @@ -1383,10 +1384,22 @@ std::string DesignSketchTool::dimtype_title(DimType k) const { // Draw-then-edit dispatcher: mirror the Select-mode quote-click logic, but target the // freshly-drawn selection's PRIMARY value and use the tentative (clean-cancel) path for // scalar quotes. Runs after render_live_quotes, so the live-quote state is populated. +// Why a draw-then-edit chain did not start. Four early returns can swallow it, and from outside +// they are indistinguishable: the shape appears, no field opens, and nothing says which guard +// fired. check-gui-click-edit.py reports that as "a value field opened (nothing did)" for every +// tool at once, which reads like a total product failure and is not necessarily one. +static void trace_autoedit(const char* why, size_t n) +{ + if (!std::getenv("ORCA_CAD_UXTRACE")) return; + fprintf(stderr, "[UX] autoedit %s steps=%zu\n", why, n); + fflush(stderr); +} + void DesignSketchTool::open_primary_autoedit() { - if (!on_inline_edit || m_awaiting_length) return; // no host, or a field is already open - if (!m_active) return; // session ended before the deferred tick + if (!on_inline_edit) { trace_autoedit("skip: no on_inline_edit host", 0); return; } + if (m_awaiting_length) { trace_autoedit("skip: a field is already open", 0); return; } + if (!m_active) { trace_autoedit("skip: session ended before the deferred tick", 0); return; } // Build ONE ordered list of edit steps covering EVERY characteristic dimension of the // freshly-drawn shape — scalar quotes (constraint-based) AND geometric editors — so every @@ -1494,6 +1507,8 @@ void DesignSketchTool::open_primary_autoedit() [this, fi](double v){ set_rect_angle(fi, v); }, span(fi), "Angle" }); } + trace_autoedit(m_autoedit_dims.empty() ? "built NO steps (no live quote matched)" : "opening", + m_autoedit_dims.size()); if (!m_autoedit_dims.empty()) { m_autoedit_dim_idx = 0; open_next_autoedit_dim(); @@ -2393,7 +2408,7 @@ bool DesignSketchTool::try_add_constraints(const std::vector loops = region_loops(d.entities); // What did the sketch decompose into, and what is under the click? This is the trace that - // settled snaporca-txp8 — it prints the loop table with each loop's hole count, so + // settled txp8 — it prints the loop table with each loop's hole count, so // "containment is wrong" and "the click landed elsewhere" stop being indistinguishable. // Guarded rather than merely silent: hit_display_sketch runs on every pick, and the message // costs a string build and a heap allocation per loop even when nothing consumes it. @@ -3145,7 +3160,7 @@ void DesignSketchTool::hit_display_sketch(const DisplaySketch& d, const Vec2d& p if (h >= 0 && h < int(loops.size()) && point_in_poly(p, loops[h].poly)) { in_hole = true; break; } if (!in_hole) { face_feat = d.feature; face_reg = r; } } - // edge_ent is printed because it is now DELIVERED (snaporca-3648) — a tool can ask for the + // edge_ent is printed because it is now DELIVERED (3648) — a tool can ask for the // line you pointed at, not just its loop, and "which entity did that click resolve to" is // otherwise unanswerable from outside. dp_pick_trace("region hit -> feat=%d reg=%d (edge_feat=%d edge_reg=%d edge_ent=%d)", @@ -3249,11 +3264,11 @@ void DesignSketchTool::select_body(int body) // Pick tracing. Selection failures on a real desktop have repeatedly turned out to be an // event that never arrived rather than a ray that missed, and the two look identical from -// the UI. Set SNAPORCA_PICK_TRACE=1 and the whole press->release->ray path narrates itself +// the UI. Set ORCA_CAD_PICK_TRACE=1 and the whole press->release->ray path narrates itself // on stderr. Off by default: no cost, no noise. static bool dp_pick_trace_on() { - static const bool on = ::getenv("SNAPORCA_PICK_TRACE") != nullptr; + static const bool on = ::getenv("ORCA_CAD_PICK_TRACE") != nullptr; return on; } @@ -3279,7 +3294,7 @@ static void dp_pick_trace(const char* fmt, ...) // // ponytail: crossing over a triangle sample set. A rectangle small enough to sit entirely // inside one flat triangle selects nothing — drag a bigger one, or click. Real multi-body -// selection (and the homogeneous-set rule that goes with it) is snaporca-9xw. +// selection (and the homogeneous-set rule that goes with it) is 9xw. void DesignSketchTool::pick_bodies_in_rectangle() { if (m_solid_mesh == nullptr || m_solid_tri_body == nullptr || m_solid_bodies == nullptr) @@ -3471,7 +3486,7 @@ bool DesignSketchTool::handle_solid_click(GLCanvas3D& canvas, const wxMouseEvent m_sel_vertex_pt = p.vertex_pt; m_solid_sel = p.kind; - // CLICK AGAIN ON THE SAME THING -> THE WHOLE BODY (snaporca-gem). Pointing at a face and + // CLICK AGAIN ON THE SAME THING -> THE WHOLE BODY (gem). Pointing at a face and // pointing at its body are different intents, and until now only the rubber band could // express the second one — so the status line said "face 0 selected" while the user // believed they had taken the body, and every body verb had to opt into the face kinds to @@ -3808,23 +3823,23 @@ void DesignSketchTool::clear_extrude_gizmo() // cone travels, an open collar receives. No surveyed CAD system encodes this at all; both ends of // their mates are drawn identically, which is why "which part moves?" is a standing complaint. // -// SNAPORCA_GLYPH=A|B selects the treatment while this is being judged on the rig: +// ORCA_CAD_GLYPH=A|B selects the treatment while this is being judged on the rig: // A three short axis arms, no head differentiation (the Onshape baseline) // B one-sided Z arrow, filled vs open head (the proposal) -- default void DesignSketchTool::render_mate_connectors() { if (m_mate_connectors.empty()) return; static const bool style_A = [] { - const char* s = ::getenv("SNAPORCA_GLYPH"); + const char* s = ::getenv("ORCA_CAD_GLYPH"); return s && (*s == 'A' || *s == 'a'); }(); // The face treatment, on by default. Read every frame rather than latched in a static, so // toggling the preference takes effect on the next repaint instead of at the next launch — // it is a look, and a look you cannot A/B without restarting will not get compared. - // SNAPORCA_GLYPH=D forces the disc regardless, which is how the rig drives the other branch. + // ORCA_CAD_GLYPH=D forces the disc regardless, which is how the rig drives the other branch. const bool face_style = !style_A && wxGetApp().app_config->get_bool("design_connector_face_glyph") - && [] { const char* s = ::getenv("SNAPORCA_GLYPH"); + && [] { const char* s = ::getenv("ORCA_CAD_GLYPH"); return !(s && (*s == 'D' || *s == 'd')); }(); const Camera& cam = wxGetApp().plater()->get_camera(); @@ -4006,7 +4021,7 @@ void DesignSketchTool::render_mate_connectors() } // --------------------------------------------------------------------------------------------- -// THE FACE TREATMENT of the mate connector (snaporca-x0kd). The disc + roll quadrant answers +// THE FACE TREATMENT of the mate connector (x0kd). The disc + roll quadrant answers // "where is X" with a shape that has to be learned; a face does not. Face orientation is // hardwired perception -- a toddler reads a face's roll and verse with no instruction at all -- // and that is the whole reason this exists. Default ON, switchable in Preferences for users who @@ -4038,7 +4053,7 @@ static const Vec2d kBearOutline[] = { // 12 verts, RDP eps 0.030, CCW static const Vec2d kBearChin[] = { // the CHIN BAR, flat. The muzzle is relief — see kBearCrest. {-0.2682, -0.3578}, {+0.2628, -0.3578}, {+0.2237, -0.1786}, }; -// {cx, cy, r}: two eyes, then the cheek dot that carries handedness (snaporca-wi3z). +// {cx, cy, r}: two eyes, then the cheek dot that carries handedness (wi3z). static const Vec3d kBearMarks[] = { {-0.1997, +0.1760, +0.0590}, {+0.1947, +0.1760, +0.0590}, @@ -6421,7 +6436,7 @@ DesignSketchTool::region_loops(const std::vector& ents) const // NESTING. A loop drawn inside another one is that one's HOLE. Without this a sketch is // just N disjoint filled polygons, so "the plate with the hole" is not expressible and the - // multi-loop kernel path (snaporca-88v) is unreachable from the viewport — which is exactly + // multi-loop kernel path (88v) is unreachable from the viewport — which is exactly // what Tommaso hit: a rectangle with a circle inside extruded to a plain box, because only // the rectangle loop could be picked and only its entities were passed on. // @@ -6435,7 +6450,7 @@ DesignSketchTool::region_loops(const std::vector& ents) const // polygon being tested answers by rounding, so the same drawing can be read either way. // Measured on the StudyCadCam corpus: the engine and an independent containment check // disagreed on 6 of 39 sheets, and every disagreement was a probe point sitting on the other - // loop's boundary. snaporca-5hvl. + // loop's boundary. 5hvl. auto poly_area = [](const std::vector& q) { double a2 = 0.0; for (size_t i = 0, j = q.size() - 1; i < q.size(); j = i++) @@ -6535,7 +6550,7 @@ int DesignSketchTool::region_at(const Vec2d& p) const // ---- rendering -------------------------------------------------------------- -// Chop a polyline into dashes (snaporca-imlq). Construction geometry is dashed in every CAD; +// Chop a polyline into dashes (imlq). Construction geometry is dashed in every CAD; // this one painted it solid grey, which against the under-constrained orange reads as "another // line", not as "reference only". The dash and gap arrive in WORLD units — the caller scales them // by units-per-pixel, so the dash keeps its size on screen at any zoom instead of turning into a @@ -7971,7 +7986,7 @@ void DesignSketchTool::confirm_op() // The sources as they stand BEFORE any of this op's constraints exist. Two jobs: every // copy is reflected from the untouched original (so a batch that moves the sketch cannot // feed a later copy moved geometry), and the invariant at the bottom has something to - // compare against. snaporca-mirror-slot. + // compare against. mirror-slot. const std::vector before = m_entities; const size_t cmark = m_constraints.size(); std::vector> fresh; // copy index -> its pristine reflection @@ -8018,7 +8033,7 @@ void DesignSketchTool::confirm_op() // postcondition on the geometry, and if a source moved it keeps the copies — which are // exactly what the preview showed — and drops the whole constraint web that moved them. // Restoring the sources needs no re-solve: the pre-batch state was itself solved, and a - // failed solve does not write back (snaporca-pl5). + // failed solve does not write back (pl5). // BOTH HALVES. Watching only the sources caught the slot (whose web dragged everything) // and missed the rounded rectangle, where the solver held the sources still and put the // COPIES somewhere else: an arc has five degrees of freedom and Symmetric on centre plus @@ -8434,7 +8449,7 @@ const ColorRGBA* DesignSketchTool::sketch_hl_color(int feature) const return nullptr; } -// Which step of the armed gesture is live, reported only when it moves (snaporca-1c0c). Called +// Which step of the armed gesture is live, reported only when it moves (1c0c). Called // from render(), which is the one place EVERY state change passes through — a per-call-site // notification would have to be added to each of the thirty-odd tool branches and would be // forgotten by the next one. Cheap: three ints compared per frame. @@ -8465,6 +8480,11 @@ void DesignSketchTool::render(GLCanvas3D& canvas) m_dim_label_seq = 0; m_render_scale = canvas.get_scale(); emit_step_hint(); // before the early returns: an armed tool on an empty sketch still guides + // The value field, BEFORE every early return below. It can be up in Constrain mode on a + // committed feature and on an empty sketch, and a field that is not drawn is a field that is + // not there — there is no window to fall back to any more. + if (inline_editor != nullptr) + inline_editor->render(*wxGetApp().imgui(), m_render_scale); (void)canvas; if (!has_display()) { if (on_readout) on_readout(std::string()); // nothing to show -> hide HUD @@ -8733,7 +8753,7 @@ void DesignSketchTool::render(GLCanvas3D& canvas) // Mirror's is the axis, Fillet/Chamfer's is the first of the two lines — and until now // every pick painted the same white, so the picture could not answer "what did I select // as what". Violet, not cyan: cyan means SELECTED here and nothing else may wear it. - // snaporca-vd6v. + // vd6v. const bool op_ref = is_edit_op_mode() && int(i) == m_op_a; ColorRGBA col; if (editing_this) col = editing; @@ -8813,7 +8833,7 @@ void DesignSketchTool::render(GLCanvas3D& canvas) if (!sel_handles.empty()) draw_vertices(m_highlight_model, sel_handles, sel_col); // Midpoint of every segment, drawn smaller and cooler than the endpoint handles - // (snaporca-te8v). Without it the Midpoint snap is invisible: it exists in the + // (te8v). Without it the Midpoint snap is invisible: it exists in the // inference engine but the user has nothing to aim at. Construction lines get one // too — you constrain to them as readily as to real geometry. std::vector mids; @@ -8857,6 +8877,7 @@ void DesignSketchTool::render(GLCanvas3D& canvas) // next render_live_quotes(), so the deferred open still sees this frame's values. if (m_autoedit_pending) { m_autoedit_pending = false; + trace_autoedit("pending -> deferring open", 0); wxGetApp().CallAfter([this] { open_primary_autoedit(); }); } if (is_edit_op_mode()) @@ -9334,7 +9355,7 @@ int DesignSketchTool::add_entities_scripted(const std::vector& ent // the 39 corpus drawings the loops that came back wrong were all TINY (1.4 to 13 mm^2), out // by up to 7e-4 relative, because a 0.005 degree tilt on a 0.3 mm chord is inside 1e-4. // With zero, only a segment that is EXACTLY axis-aligned is constrained, and constraining - // something already true cannot move it. snaporca-8xg1. + // something already true cannot move it. 8xg1. // The weld window closes too. Two endpoints a micron apart are not the same point when a // caller typed both of them: on MPD681, 20 of 363 scripted segments were dragged onto a // common point up to 0.0021 mm away, because welding is TRANSITIVE and three vertices near @@ -9349,7 +9370,7 @@ int DesignSketchTool::add_entities_scripted(const std::vector& ent // while m_awaiting_length) and swallows every letter (in_text includes inline_busy()). The // symptom was that the first key and click after sketch_add did nothing until one Escape had // dismissed the field. Resyncing the baseline here leaves an ALREADY open field alone; it - // only stops this add from being read as something the user just drew. snaporca-j7gc. + // only stops this add from being read as something the user just drew. j7gc. m_autoedit_seen = int(m_entities.size()); return base; } @@ -9561,7 +9582,7 @@ bool DesignSketchTool::select_at_screen(GLCanvas3D& canvas, int sx, int sy) // counts only m_selection, so right-clicking a sketch point produced the EMPTY // vocabulary and every SkPoint row in the atlas was unreachable from the menu. Other // entities keep the handle pick: a line's endpoint is a drag target, not a thing with a - // vocabulary of its own. snaporca-lnri. + // vocabulary of its own. lnri. if (ei >= 0 && ei < int(m_entities.size()) && m_entities[ei].type == SketchEntity::Type::Point) { if (std::find(m_selection.begin(), m_selection.end(), ei) != m_selection.end()) @@ -9643,8 +9664,8 @@ std::vector DesignSketchTool::connected_loop(int seed) const // suppresses the menu whenever it is set, so right-click became a no-op that also hid the one door // to half the vocabulary (47 of 86 verbs have no shortcut). Measured on the rig: with Line armed, // two right-clicks in a row produced no menu and no tool change; only Escape freed it. -// Same rule as snaporca-xmh6, which said it for the selection: clearing nothing is not a gesture -// terminator. snaporca-ghcz. +// Same rule as xmh6, which said it for the selection: clearing nothing is not a gesture +// terminator. ghcz. bool DesignSketchTool::right_abandon() { if (m_points.empty()) @@ -9979,7 +10000,7 @@ bool DesignSketchTool::on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas) // consumed from here on. Left-drag no longer orbits in this canvas — DesignCanvas puts // orbit on middle-drag and pan on right-drag, the CAD convention — so nothing downstream // is being starved of a gesture it used to own. - // HOVER PRE-HIGHLIGHT (snaporca-9xw part 3): say what a click would take, before it is + // HOVER PRE-HIGHLIGHT (9xw part 3): say what a click would take, before it is // taken. Plain motion only — no button down, no band running — because during a drag the // pointer is doing something else and a promise about clicking would be a lie. Returns // false so the event still reaches the camera; this only asks for a repaint, it does not @@ -10069,7 +10090,7 @@ bool DesignSketchTool::on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas) return true; } m_display_pick = -1; m_display_pick_region = -1; // clicked bare plate -> drop highlight - // ...and the SOLID selection goes with it (snaporca-od0). A click that hits nothing has to + // ...and the SOLID selection goes with it (od0). A click that hits nothing has to // mean what a rubber band that sweeps nothing already means — pick_bodies_in_rectangle // clears on an empty sweep, and the two gestures cannot disagree about the same outcome. // Until now the face survived a click on bare plate, so "click away, then click the face @@ -10717,7 +10738,7 @@ bool DesignSketchTool::on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas) return true; } if (evt.RightDown() && m_points.empty()) - return false; // no chain to end — snaporca-ghcz, let the offer open + return false; // no chain to end — ghcz, let the offer open if (evt.RightDown()) { // END the chain — do NOT close it. This used to call push_closed_lines() for three // or more points, i.e. it drew a final segment from the last point back to the @@ -11096,7 +11117,7 @@ bool DesignSketchTool::on_mouse_impl(wxMouseEvent& evt, GLCanvas3D& canvas) return true; } if (evt.RightDown() && m_points.empty()) - return false; // no poles down — snaporca-ghcz, let the offer open + return false; // no poles down — ghcz, let the offer open if (evt.LeftDClick() || evt.RightDown()) { if (m_points.size() >= 2) { const int base = int(m_entities.size()); diff --git a/src/slic3r/GUI/CAD/DesignSketchTool.hpp b/src/slic3r/GUI/CAD/DesignSketchTool.hpp index d73cfce6a8..c88b149c59 100644 --- a/src/slic3r/GUI/CAD/DesignSketchTool.hpp +++ b/src/slic3r/GUI/CAD/DesignSketchTool.hpp @@ -64,7 +64,7 @@ public: Constrain }; // Which tool is armed, and how many anchors it has down. Read-only, for the offer ladder: // "the menu armed the verb I chose" is otherwise unassertable, and a menu walk that lands one - // row off arms a NEIGHBOURING tool and then grades whatever that drew. snaporca-ekt9. + // row off arms a NEIGHBOURING tool and then grades whatever that drew. ekt9. Mode mode() const { return m_mode; } int pending_points() const { return int(m_points.size()); } void emit_step_hint(); // fires on_step_changed when the step actually moved @@ -133,13 +133,16 @@ public: bool has_entities() const { return !m_entities.empty(); } bool on_mouse(wxMouseEvent& evt, GLCanvas3D& canvas); // Right-click on a draw tool: true when an in-progress anchor was abandoned, false when - // there was nothing to abandon — and false is what lets the offer menu open. snaporca-ghcz. + // there was nothing to abandon — and false is what lets the offer menu open. ghcz. bool right_abandon(); // True if the LAST right-press was consumed as a gesture terminator (end a polyline chain, // abandon an anchor, exit a tool). Read-and-clear: the canvas asks on the matching release to // decide whether that right-click was the user's, in which case it opens the offer. bool take_right_consumed() { const bool b = m_right_consumed; m_right_consumed = false; return b; } void render(GLCanvas3D& canvas); + // The in-canvas value field, drawn by render() before any early return. Owned by + // DesignCanvas; null until it sets it. Not a window — see SketchInlineEditor.hpp. + class SketchInlineEditor* inline_editor{nullptr}; // Persistent committed sketches to draw even when no session is active (e.g. an // un-consumed sketch left visible after its extrude is removed). Each carries its @@ -210,7 +213,7 @@ public: // feature index + the clicked closed-region index within it (-1 = no specific loop). // entity = the sketch entity index under the cursor when the click landed on a loop // STROKE, else -1 for an interior/region hit. Carried because a tool can legitimately - // want the LINE you pointed at, not just the loop it belongs to (Rib, snaporca-3648). + // want the LINE you pointed at, not just the loop it belongs to (Rib, 3648). std::function on_display_sketch_selected; // Double-click on a committed sketch stroke: open THAT feature for editing. Selecting a line // and then hunting for an Edit button in a panel is the dependency this tab exists to remove. @@ -339,7 +342,7 @@ public: // Mate connectors. Until now a connector was visible only to a program — resolve_datum_coordsys // had exactly one consumer, the MCP socket — so the frame a mate is built on could not be seen - // at all. The glyph has to answer two questions on sight (snaporca-wgsc): which way does Z point + // at all. The glyph has to answer two questions on sight (wgsc): which way does Z point // (the VERSE), and which of the pair is anchored versus driven (the POLARITY). Nothing in any // surveyed CAD system encodes the second one. struct MateConnectorGlyph { @@ -437,7 +440,7 @@ public: // Live readout while drawing a Line/Polyline segment (anchor->cursor metrics). std::function on_cursor_metrics; - // Live step guidance (snaporca-1c0c). The armed tool reports WHICH STEP of its gesture the + // Live step guidance (1c0c). The armed tool reports WHICH STEP of its gesture the // user is on, every time that changes, so the status line can name the next click instead of // repeating the one-shot sentence written when the tool was armed. step = anchors/picks // already down (Mirror: 0 = no axis, 1 = axis down, 2 = ready to apply); picks = size of the @@ -676,7 +679,7 @@ private: // weld_tol: how far apart two endpoints may be and still be called Coincident. // Both default to GESTURE slack. A scripted add passes zero for both: the caller has // already said exactly what it means, and every non-zero window is a window in which the - // inference rewrites it. snaporca-8xg1. + // inference rewrites it. 8xg1. void infer_auto_constraints(int base, double ang_tol_rad = 3.0 * M_PI / 180.0, double weld_tol = 1e-3); @@ -936,7 +939,7 @@ private: // A selectable sketch region: its own boundary, plus the loops nested INSIDE it, which // are its holes. Modelling holes is what makes "the plate with the hole in it" a thing the // user can point at — without it a sketch is N disjoint filled polygons and the only - // selectable things are the rectangle alone or the circle alone (snaporca-txp8). + // selectable things are the rectangle alone or the circle alone (txp8). struct RegionLoop { std::vector poly; std::vector ents; @@ -1162,7 +1165,7 @@ private: Vec3d vertex_pt{Vec3d::Zero()}; }; bool resolve_solid_pick(GLCanvas3D& canvas, int mx, int my, SolidPick& out) const; - // HOVER PRE-HIGHLIGHT (snaporca-9xw part 3). Vertex-beats-edge-beats-face is a rule the user + // HOVER PRE-HIGHLIGHT (9xw part 3). Vertex-beats-edge-beats-face is a rule the user // cannot see until after they commit to a click; showing the outcome under the pointer is // what makes the precedence learnable at all, and is the charter's L5 (one click, one visible // change) read honestly — the change has to be predictable before the click, not only after. diff --git a/src/slic3r/GUI/CAD/McpControl.cpp b/src/slic3r/GUI/CAD/McpControl.cpp index 81f22ecfef..8e34e2aac8 100644 --- a/src/slic3r/GUI/CAD/McpControl.cpp +++ b/src/slic3r/GUI/CAD/McpControl.cpp @@ -113,7 +113,7 @@ json describe_tools() // Hand-written descriptor. The bridge turns this into MCP tool schemas; later // slices grow this list (ideally from the kernel directly). return json{ - {"app", "SnapOrca CAD"}, + {"app", "Orca CAD"}, {"protocol", "jsonrpc-2.0"}, {"slice", 5}, // Read this before using any face or edge id. @@ -2201,9 +2201,9 @@ void server_thread(std::string sock_path) void start_mcp_control_if_enabled() { - const char* env = std::getenv("SNAPORCA_MCP"); + const char* env = std::getenv("ORCA_CAD_MCP"); if (!env || !*env) return; - std::string path = (std::strcmp(env, "1") == 0) ? "/tmp/snaporca-mcp.sock" : env; + std::string path = (std::strcmp(env, "1") == 0) ? "/tmp/orca-cad-mcp.sock" : env; static bool started = false; if (started) return; started = true; diff --git a/src/slic3r/GUI/CAD/McpControl.hpp b/src/slic3r/GUI/CAD/McpControl.hpp index 106e9888b1..12e3289eb5 100644 --- a/src/slic3r/GUI/CAD/McpControl.hpp +++ b/src/slic3r/GUI/CAD/McpControl.hpp @@ -3,9 +3,9 @@ // MCP control surface (slice 1): a local JSON-RPC 2.0 server, line-delimited over a // Unix domain socket, that lets an external MCP bridge drive and perceive the Design -// tab. Off unless the env var SNAPORCA_MCP is set: -// SNAPORCA_MCP=1 -> socket at /tmp/snaporca-mcp.sock -// SNAPORCA_MCP=/path/to.sock -> socket at that path +// tab. Off unless the env var ORCA_CAD_MCP is set: +// ORCA_CAD_MCP=1 -> socket at /tmp/orca-cad-mcp.sock +// ORCA_CAD_MCP=/path/to.sock -> socket at that path // All CAD work is marshalled onto the wx main thread and runs through the SAME // CadDocument kernel the GUI uses (no parallel engine). Slice-1 methods: // describe_tools, describe_scene, extrude. @@ -15,7 +15,7 @@ namespace Slic3r { namespace GUI { -// Start the server thread iff SNAPORCA_MCP is set. Safe to call once after the +// Start the server thread iff ORCA_CAD_MCP is set. Safe to call once after the // MainFrame + DesignPanel exist. No-op when the env var is unset or on Windows. void start_mcp_control_if_enabled(); diff --git a/src/slic3r/GUI/CAD/SketchInlineEditor.cpp b/src/slic3r/GUI/CAD/SketchInlineEditor.cpp index 738d6d5470..9c438c7d06 100644 --- a/src/slic3r/GUI/CAD/SketchInlineEditor.cpp +++ b/src/slic3r/GUI/CAD/SketchInlineEditor.cpp @@ -1,368 +1,203 @@ #include "slic3r/GUI/CAD/SketchInlineEditor.hpp" -#include "slic3r/GUI/I18N.hpp" // _L for the refusal messages shown in the title line -#include +#include "slic3r/GUI/ImGuiWrapper.hpp" +#include "slic3r/GUI/GUI_App.hpp" +#include "slic3r/GUI/I18N.hpp" +#include "libslic3r/Color.hpp" -#include -#include -#include -#include -#include -#include -#include +#include +#include // BringWindowToDisplayFront / GetCurrentWindow -#include #include #include - -#ifdef __WXGTK__ -#include -#ifdef GDK_WINDOWING_X11 -#include -#endif -#endif +#include +#include namespace Slic3r { namespace GUI { namespace { -// Locale-safe value <-> text (wx sets LC_NUMERIC to the user locale, so snprintf may -// emit a comma; parsing accepts either separator). Mirrors DesignPanel's en_*. -wxString en_format(double v, int digits = 2) + +// Numbers are typed and shown with a POINT, whatever the locale: this field feeds a CAD kernel, +// and a decimal comma reaching it as a thousands separator is a silent order-of-magnitude error. +// Parsing accepts either separator because a keyboard's numeric pad may only offer one. +std::string fmt_value(double v, int digits = 2) { char fmt[16]; std::snprintf(fmt, sizeof(fmt), "%%.%df", digits); char buf[64]; std::snprintf(buf, sizeof(buf), fmt, v); - for (char* c = buf; *c; ++c) if (*c == ',') *c = '.'; - return wxString::FromUTF8(buf); -} -bool en_parse(const wxString& text, double& out) -{ - wxString t(text); - t.Replace(wxT(","), wxT(".")); - return t.ToCDouble(&out); + for (char* c = buf; *c; ++c) + if (*c == ',') *c = '.'; + return std::string(buf); } -// Present the toplevel with a real X11 server timestamp: wxFrame::Raise() maps to -// gtk_window_present() with gtk_get_current_event_time(), which inside a CallAfter is -// GDK_CURRENT_TIME (0) and is ignored by mutter's focus-stealing prevention. A server -// timestamp lets the compositor grant focus to the re-mapped window. -#ifdef __WXGTK__ -void present_toplevel(wxFrame* frame) +bool parse_value(const char* text, double& out) { -#ifdef GDK_WINDOWING_X11 - if (frame) { - GtkWidget* widget = static_cast(frame->GetHandle()); - if (widget && GTK_IS_WIDGET(widget)) { - GdkWindow* gdkwin = gtk_widget_get_window(widget); - if (gdkwin) { - gtk_window_present_with_time(GTK_WINDOW(widget), - gdk_x11_get_server_time(gdkwin)); - return; - } - } - } -#endif - if (frame) frame->Raise(); + if (text == nullptr) return false; + std::string t(text); + for (char& c : t) + if (c == ',') c = '.'; + // strtod, not std::stod: no exceptions, and `end` tells us whether the WHOLE field was a + // number. "12mm" must be refused, not silently read as 12. + const char* b = t.c_str(); + char* end = nullptr; + const double v = std::strtod(b, &end); + if (end == b) return false; + while (*end == ' ' || *end == '\t') ++end; + if (*end != '\0') return false; + out = v; + return true; } -#else -void present_toplevel(wxFrame* frame) -{ - if (frame) frame->Raise(); -} -#endif -// Between two queued dimensions (a rectangle's Width then Height) the frame is either kept -// MAPPED and merely re-titled, or unmapped and mapped again. That is a per-toolkit choice, not -// a preference: -// GTK/mutter keep it mapped. Focus-stealing prevention refuses keyboard focus to a window -// that was just re-mapped, so hiding between the two fields left the second one -// visible but dead (snaporca-p8uw). -// elsewhere map it afresh. This is what shipped before that workaround, which was applied -// with no platform guard — and it is the only difference between the first queued -// field (works everywhere) and the second (macOS wedges the whole app, PR #15238). -// A workaround for one window manager must not become a contract for all of them. -constexpr bool keep_mapped_between_fields = -#ifdef __WXGTK__ - true; -#else - false; -#endif - -void trace_inline_focus(wxFrame* frame, const std::string& title) +// One machine-readable line per event of the click-edit contract, for the UX check that runs +// after every build (scripts/CAD/check-gui-click-edit.py). Deliberately NOT the same switch as +// ORCA_CAD_KEYTRACE: that one is a debugging firehose, this one is an assertion surface and its +// format is a contract the script parses. +// +// The pair that matters is `open` vs `commit`: the check always types a value DIFFERENT from the +// prefill, so a field that is on screen but not editable commits its prefill and the two lines +// disagree. A focus flag cannot show that — it read 0 even when typing worked — but the number +// the user actually gets can. +void ux_trace(const char* event, const std::string& title, const std::string& detail) { - if (!std::getenv("SNAPORCA_KEYTRACE")) return; -#ifdef __WXGTK__ - GtkWindow* win = nullptr; - if (frame) { - GtkWidget* widget = static_cast(frame->GetHandle()); - if (widget && GTK_IS_WIDGET(widget)) win = GTK_WINDOW(widget); - } - fprintf(stderr, "[INLINE_FOCUS] title=%s active=%d toplevel_focus=%d shown=%d\n", - title.c_str(), - win ? (int) gtk_window_is_active(win) : -1, - win ? (int) gtk_window_has_toplevel_focus(win) : -1, - frame ? (int) frame->IsShown() : -1); -#else - fprintf(stderr, "[INLINE_FOCUS] title=%s shown=%d\n", - title.c_str(), frame ? (int) frame->IsShown() : -1); -#endif - fflush(stderr); + if (!std::getenv("ORCA_CAD_UXTRACE")) return; + std::fprintf(stderr, "[UX] %s title=%s %s\n", event, title.c_str(), detail.c_str()); + std::fflush(stderr); } + } // namespace -// The title line doubles as the error line, so both colours live here rather than as a -// literal at the one place that used to set it. -// Keep the frame fully on-screen: an anchor that maps off the display makes GTK drop the -// window at a default corner (top-left) instead of the requested point. Clamp to the display -// the anchor is ON, not the primary one — wxGetClientDisplayRect() only ever describes the -// primary monitor, so on a multi-head desktop this shoved the field onto a different screen -// than the app. It then sat invisible while m_awaiting_length made the sketch tool eat every -// mouse event, which read as the viewport freezing after a sketch, with only Enter able to -// release it. Shared with the error re-fit below, which can widen the frame after placement. -static wxPoint clamp_to_display(wxPoint pos, const wxSize& sz, const wxPoint& anchor, wxWindow* w) -{ - int disp = wxDisplay::GetFromPoint(anchor); - if (disp == wxNOT_FOUND) disp = wxDisplay::GetFromWindow(w); - const wxRect area = (disp != wxNOT_FOUND) ? wxDisplay(unsigned(disp)).GetClientArea() - : wxGetClientDisplayRect(); - pos.x = std::max(area.GetLeft(), std::min(pos.x, area.GetRight() - sz.GetWidth())); - pos.y = std::max(area.GetTop(), std::min(pos.y, area.GetBottom() - sz.GetHeight())); - return pos; -} - -static const wxColour kTitleFg (160, 162, 168); -static const wxColour kTitleErr(232, 106, 106); - -SketchInlineEditor::SketchInlineEditor(wxWindow* parent_canvas) -{ - m_parent = parent_canvas; // where the keyboard goes back to when this field lets go - wxWindow* top = parent_canvas ? wxGetTopLevelParent(parent_canvas) : nullptr; - // Borderless floating frame: a top-level window so the WM composites it above the - // GL canvas (a child widget would be hidden by the GL surface). Floats on its - // parent and stays on top so it tracks the main window. - // NB: no wxFRAME_FLOAT_ON_PARENT — that maps to a GTK _UTILITY_ window-type hint, which - // many WMs (incl. the xrdp/x11vnc session on :10) refuse to give keyboard focus, so the - // field opened un-focusable and needed a click before typing. Plain stay-on-top frame is - // WM-focusable; we present + SetFocus it explicitly in open(). - m_frame = new wxFrame(top, wxID_ANY, wxEmptyString, wxDefaultPosition, wxDefaultSize, - wxFRAME_NO_TASKBAR | wxBORDER_NONE | wxSTAY_ON_TOP); - m_ctrl = new wxTextCtrl(m_frame, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(82, -1), - wxTE_PROCESS_ENTER | wxTE_RIGHT | wxBORDER_SIMPLE); - m_frame->SetBackgroundColour(wxColour(40, 42, 46)); - m_title = new wxStaticText(m_frame, wxID_ANY, wxEmptyString); - m_title->SetForegroundColour(kTitleFg); - auto* sizer = new wxBoxSizer(wxVERTICAL); - sizer->Add(m_title, 0, wxLEFT | wxRIGHT | wxTOP, 3); - sizer->Add(m_ctrl, 1, wxEXPAND | wxALL, 2); - m_frame->SetSizerAndFit(sizer); - m_frame->Hide(); - - m_ctrl->Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent&) { do_commit(); }); - // The complaint goes away the moment the user starts answering it — an error that - // outlives the input it was about is just noise on the next attempt. - m_ctrl->Bind(wxEVT_TEXT, [this](wxCommandEvent& e) { clear_invalid(); e.Skip(); }); - m_ctrl->Bind(wxEVT_KEY_DOWN, [this](wxKeyEvent& e) { - // Esc on an ORPHAN (mapped, m_open already false) must still take the field off the - // screen. do_cancel() returns early there, and while the frame holds the X input focus - // this handler is the ONLY code the keyboard can still reach — so if it refuses, nothing - // else gets a turn and the application looks frozen. - if (e.GetKeyCode() == WXK_ESCAPE) { if (m_open) do_cancel(); else dismiss(); } - // Tab commits, exactly like Enter — the caller's on_commit is what walks to the next - // dimension. Left to wx's default handling it navigated within this one-control popup, - // i.e. back to the same field with the text re-selected: typing 60, Tab, 40 looked like - // two dimensions entered and silently kept only the 40. Losing typed input with no - // visible difference from a committed field is the part that made this worth a key case. - else if (e.GetKeyCode() == WXK_TAB) do_commit(); - else e.Skip(); - }); -} - -void SketchInlineEditor::open(const wxPoint& screen_px, double value, - const std::string& title, +void SketchInlineEditor::open(const wxPoint& canvas_px, double value, const std::string& title, std::function on_commit, std::function on_cancel) { - if (m_frame == nullptr || m_ctrl == nullptr) { if (on_cancel) on_cancel(); return; } - // Where the frame is kept mapped, never close/unmap on the way in: the previous queued - // dimension left it mapped (see do_commit) and re-mapping is what mutter refuses to focus, - // so reuse it and just re-title/re-position. Elsewhere, force a fresh map. - if (!keep_mapped_between_fields && m_frame->IsShown()) - m_frame->Hide(); + m_anchor = canvas_px; + m_title = title; + m_err.clear(); m_commit = std::move(on_commit); m_cancel = std::move(on_cancel); - m_ctrl->ChangeValue(en_format(value)); - if (m_title) { - m_title_text = wxString::FromUTF8(title.c_str()); - m_title->SetLabel(m_title_text); - m_title->SetForegroundColour(kTitleFg); // drop any refusal left over from the last field - m_title->Show(!title.empty()); - } - m_frame->Fit(); - const wxSize sz = m_frame->GetSize(); - wxPoint pos = clamp_to_display(wxPoint(screen_px.x - sz.GetWidth() / 2, - screen_px.y - sz.GetHeight() / 2), - sz, screen_px, m_frame); - // Show() BEFORE Move(): GTK ignores a Move() issued before the window is mapped (the - // WM places it at its default, i.e. the top-left corner). Move after Show sticks. - if (!m_frame->IsShown()) - m_frame->Show(); - m_frame->Move(pos); - present_toplevel(m_frame); // activate the top-level so SetFocus routes - m_frame->SetFocus(); - m_ctrl->SetFocus(); - m_ctrl->SelectAll(); - m_open = true; - trace_inline_focus(m_frame, title); - // Re-assert on the next tick too: the GL canvas can reclaim focus while it finishes - // handling the click/render that opened us, so a single immediate SetFocus may be stolen. - m_ctrl->CallAfter([this, title] { - if (m_open && m_ctrl) { - present_toplevel(m_frame); - m_ctrl->SetFocus(); - m_ctrl->SelectAll(); - trace_inline_focus(m_frame, title); - } - }); + const std::string v = fmt_value(value); + std::snprintf(m_buf, sizeof(m_buf), "%s", v.c_str()); + m_open = true; + // ImGui takes keyboard focus for one frame on request; asking on the frame the field first + // appears is what makes typing land without a click. There is no window manager to consult. + m_focus_pending = true; + ux_trace("open", m_title, "prefill=" + v); } -void SketchInlineEditor::do_commit() +void SketchInlineEditor::close() { - if (!m_open || m_ctrl == nullptr) return; - double v = 0.0; - if (!en_parse(m_ctrl->GetValue(), v)) { // invalid: keep editing, and SAY SO - // Silence here read as a freeze: Enter did nothing, the text re-selected itself, and - // nothing on screen said the value had been refused or what would be accepted. Every - // other CAD names the problem in place; so do we. - flag_invalid(m_ctrl->GetValue().Strip(wxString::both).IsEmpty() - ? _L("Enter a number") - : _L("Not a number")); - m_ctrl->SetFocus(); - m_ctrl->SelectAll(); - return; - } - auto cb = m_commit; - m_open = false; // logically closed; whether it stays MAPPED is per-toolkit - m_commit = nullptr; - m_cancel = nullptr; - // Unmap BEFORE the callback where we are not keeping it mapped, so the reopen the callback - // schedules starts from a hidden frame — the ordering that shipped before the workaround. - if (!keep_mapped_between_fields) - m_frame->Hide(); - if (cb) cb(v); - // Kept mapped: the callback either re-opens us for the next queued dimension (via its own - // CallAfter, queued during cb(v), therefore BEFORE the one below) or it does not. Hiding - // here would unmap the window and mutter would refuse to focus the re-map; so hide only - // after the reopen has had its turn. Harmless on the unmapped path — already hidden. - m_frame->CallAfter([this] { - if (m_open || m_frame == nullptr) return; - m_frame->Hide(); - return_focus(); // the chain is over; the keyboard belongs to the canvas again - }); + m_open = false; + m_focus_pending = false; + m_commit = nullptr; + m_cancel = nullptr; + m_err.clear(); } void SketchInlineEditor::cancel() { - if (m_open) do_cancel(); - else if (is_mapped()) dismiss(); // orphan: logically gone, still on screen, still eating keys -} - -bool SketchInlineEditor::is_mapped() const -{ - return m_frame != nullptr && m_frame->IsShown(); -} - -// Everything the frame can hold onto, released — with no m_open guard, because the state this -// exists for is precisely the one where m_open lies. -void SketchInlineEditor::dismiss() -{ - if (m_frame == nullptr) return; - m_open = false; - m_commit = nullptr; - m_cancel = nullptr; - if (m_frame->IsShown()) m_frame->Hide(); - return_focus(); -} - -// Hiding the frame is not enough: X keeps the input focus pointed at the window that had it, so -// an unmapped field keeps swallowing keys. The canvas has to ask for it back explicitly. -void SketchInlineEditor::return_focus() -{ - if (m_parent == nullptr) return; - if (wxWindow* top = wxGetTopLevelParent(m_parent)) - top->Raise(); - m_parent->SetFocus(); -} - -// Accept what is typed and close. Leaving a tool must not silently discard the value the user -// just entered — the same rule set_tool already follows for a ready edit-op. -void SketchInlineEditor::commit() -{ - // Same orphan case as cancel(): Enter or Tab forwarded by the panel must not be the one - // gesture that leaves the field on screen. - if (!m_open) { if (is_mapped()) dismiss(); return; } - do_commit(); - // do_commit REFUSES to close on unparseable text, which is right while the user is still - // typing — but this entry point is "we are leaving", and the caller (set_tool) unfreezes - // the canvas immediately afterwards. Refusing here left the field alive and focused over a - // viewport that was interactive again, editing geometry nothing was pointing at any more. - // We cannot accept the text and we must not keep it: fall back to keep-as-drawn, the same - // thing Esc means. if (m_open) do_cancel(); } -// Re-fit around a changed title, keeping the field itself where it is. The frame is anchored -// top-left, so growing it can push the right edge off the display — re-clamp after the Fit. -void SketchInlineEditor::refit() +void SketchInlineEditor::commit() { - if (m_frame == nullptr) return; - const wxPoint at = m_frame->GetPosition(); - m_frame->Fit(); - m_frame->Move(clamp_to_display(at, m_frame->GetSize(), at, m_frame)); -} - -void SketchInlineEditor::flag_invalid(const wxString& why) -{ - if (m_title == nullptr) return; - m_title->SetLabel(why); - m_title->SetForegroundColour(kTitleErr); - m_title->Show(true); - refit(); // "Not a number" is wider than "Length" — without this it renders as "Not a" - m_title->Refresh(); -} - -void SketchInlineEditor::clear_invalid() -{ - if (m_title == nullptr || m_title->GetForegroundColour() != kTitleErr) return; - m_title->SetLabel(m_title_text); - m_title->SetForegroundColour(kTitleFg); - m_title->Show(!m_title_text.IsEmpty()); - refit(); - m_title->Refresh(); + if (m_open) do_commit(); } void SketchInlineEditor::do_cancel() { - if (!m_open) return; + ux_trace("cancel", m_title, ""); auto cb = m_cancel; close(); if (cb) cb(); } -void SketchInlineEditor::close() +void SketchInlineEditor::do_commit() { - // m_closing was written and never read — a flag that looked like re-entrancy protection - // and was not. Hide() below pumps native events, so a nested close is reachable in - // principle; read the flag and the guard becomes real. - if (m_frame == nullptr || !m_open || m_closing) return; - m_closing = true; - m_open = false; - m_frame->Hide(); - m_commit = nullptr; - m_cancel = nullptr; - m_closing = false; - return_focus(); + double v = 0.0; + if (!parse_value(m_buf, v)) { + // Refusing input in silence is indistinguishable from the app having frozen: the field + // just sits there and the user has no idea what it wants. Say so in the title line and + // keep editing. + ux_trace("refused", m_title, std::string("typed=") + m_buf); + m_err = (m_buf[0] == '\0') ? into_u8(_L("Enter a number")) : into_u8(_L("Not a number")); + m_focus_pending = true; + return; + } + ux_trace("commit", m_title, std::string("typed=") + m_buf + " value=" + fmt_value(v, 4)); + auto cb = m_commit; + close(); + // AFTER close(): the callback may open the next queued dimension (a rectangle queues Width + // then Height), and doing that into a field that still believes it is open would drop the + // second one's prefill on the floor. + if (cb) cb(v); +} + +bool SketchInlineEditor::render(ImGuiWrapper& imgui, float scale) +{ + if (!m_open) return false; + + ImGuiWrapper::push_common_window_style(scale); + imgui.set_next_window_pos((float) m_anchor.x, (float) m_anchor.y, ImGuiCond_Always, 0.5f, 0.5f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 3.0f); + // NoInputs is what every other sketch overlay sets and is exactly what this one must not: + // it is the only overlay in the tab that the user types into. + imgui.begin(std::string("##sketchvalue"), + ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDecoration + | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings); + ImGui::BringWindowToDisplayFront(ImGui::GetCurrentWindow()); + + if (!m_title.empty() || !m_err.empty()) { + if (m_err.empty()) { + imgui.text(m_title); + } else { + ImGui::PushStyleColor(ImGuiCol_Text, ImGuiWrapper::to_ImVec4(ColorRGBA(0.91f, 0.42f, 0.42f, 1.0f))); + imgui.text(m_err); + ImGui::PopStyleColor(); + } + } + + if (m_focus_pending) { + ImGui::SetKeyboardFocusHere(); + m_focus_pending = false; + } + ImGui::PushItemWidth(90.0f * scale); + // EnterReturnsTrue so Enter commits from inside the widget; AutoSelectAll so the prefill is + // replaced by the first digit typed, which is what "pre-selected" meant when this was a + // wxTextCtrl and is what makes typing a value a single gesture. + const bool entered = ImGui::InputText("##sketchvalue_in", m_buf, sizeof(m_buf), + ImGuiInputTextFlags_EnterReturnsTrue + | ImGuiInputTextFlags_AutoSelectAll + | ImGuiInputTextFlags_CharsDecimal); + // MEASUREMENT, not a fix: one line per frame saying whether ImGui believes it owns the + // keyboard and whether our widget is the active one. "Typing does not arrive" has two very + // different causes — no FRAMES (this canvas repaints on demand only, so an idle canvas never + // processes ImGui's queued characters) versus frames that run while the input is not active — + // and they are indistinguishable from outside. + if (std::getenv("ORCA_CAD_UXTRACE")) { + const ImGuiIO& io = ImGui::GetIO(); + std::fprintf(stderr, "[UX] frame title=%s want_text=%d want_kb=%d active=%d buf=%s\n", + m_title.c_str(), (int) io.WantTextInput, (int) io.WantCaptureKeyboard, + (int) ImGui::IsItemActive(), m_buf); + std::fflush(stderr); + } + ImGui::PopItemWidth(); + imgui.end(); + ImGui::PopStyleVar(); + ImGuiWrapper::pop_common_window_style(); + + // Keep the frames coming while the field is up — see request_frame's note in the header. + if (m_open && request_frame) + request_frame(); + + // Act AFTER end(): do_commit can reopen the field for the next queued dimension, and that + // must not happen inside this frame's window. + if (entered) + do_commit(); + else if (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_Escape))) + do_cancel(); + return true; } }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/CAD/SketchInlineEditor.hpp b/src/slic3r/GUI/CAD/SketchInlineEditor.hpp index e0339b28e0..162d157864 100644 --- a/src/slic3r/GUI/CAD/SketchInlineEditor.hpp +++ b/src/slic3r/GUI/CAD/SketchInlineEditor.hpp @@ -4,72 +4,90 @@ #include #include -#include - -class wxFrame; -class wxTextCtrl; -class wxStaticText; -class wxPoint; +#include namespace Slic3r { namespace GUI { -// Onshape-style in-canvas value editor: a small borderless floating frame holding a -// wxTextCtrl, shown at screen coordinates over the GL canvas. A top-level frame is -// used (not a child widget) because a native child cannot be composited over the -// double-buffered wxGLCanvas under GTK3/llvmpipe — it stays invisible. Enter (or blur) -// commits the parsed number, Esc cancels. This is the single numeric-entry path for -// sketch dimensions, replacing the docked/modal value cards. +class ImGuiWrapper; + +// Onshape-style in-canvas value editor. +// +// IT IS NOT A WINDOW. It used to be a borderless top-level wxFrame holding a wxTextCtrl, and +// that is the whole history of this file: a separate top-level window can only receive typing +// if the window manager grants it focus, and whether it does is not ours to decide. openbox +// grants it; mutter's focus-stealing prevention refuses it, so on a GNOME desktop the field +// appeared, showed its value selected, and silently ignored every keystroke — Enter then +// committed the number it opened with. Seven workarounds were tried against that (a real X11 +// server timestamp for gtk_window_present, re-asserted SetFocus, dropping the _UTILITY hint, +// keeping the frame mapped between two queued fields, forwarding keys from the panel's +// CHAR_HOOK), one of them caused a macOS regression, and the test harness ended up clicking the +// field before typing — which is the workaround a user cannot be asked to perform, and is +// exactly the "label value not editable" report. +// +// So the field stops asking. It is now drawn INSIDE the GL canvas as an ImGui overlay, at the +// same screen point as before, and its keys arrive through the canvas's own key events, which +// GLCanvas3D already feeds to ImGui (see GLCanvas3D::on_key / on_char -> update_key_data). The +// canvas is part of the main window and already has focus, so there is no second window, no +// second focus, and no window manager in the path. The dimension labels next to it are already +// ImGui overlays (DesignSketchTool::draw_dim_label), so this is the same vocabulary, not a new +// one. +// +// Ownership: DesignCanvas owns it; DesignSketchTool::render() calls render() once per frame. class SketchInlineEditor { public: - explicit SketchInlineEditor(wxWindow* parent_canvas); + SketchInlineEditor() = default; - // Show the editor centred on `screen_px` (absolute screen coords), pre-filled with - // `value`. on_commit(parsed) fires on Enter with a valid number; on_cancel() on Esc. - void open(const wxPoint& screen_px, double value, const std::string& title, + // Open the field anchored at `canvas_px` (canvas DEVICE pixels, the coordinate space the + // sketch tool works in), pre-filled with `value` and pre-selected. on_commit(parsed) fires + // on Enter with a valid number; on_cancel() on Esc. + void open(const wxPoint& canvas_px, double value, const std::string& title, std::function on_commit, std::function on_cancel); - void close(); + void close(); // drop it with neither callback void cancel(); // if open, run the registered cancel (keep-as-drawn) void commit(); // if open, run the registered commit (accept the typed value) bool is_open() const { return m_open; } - // MAPPED is not the same question as OPEN, and conflating them is how the keyboard dies. - // The frame is deliberately left mapped across a queued dimension chain (mutter refuses - // focus to a re-mapped window), so there is a window in which m_open is already false and - // the frame is still on screen holding the X input focus. GTK meanwhile reports the window - // inactive, so it routes nothing to the text control — and every key the user presses lands - // in a window that cannot use it and will not give it back. Delete, Esc and typing all read - // as dead. Callers ask this to find the orphan; dismiss() is how they kill it. - bool is_mapped() const; - void dismiss(); // unconditional teardown: works on an ORPHANED frame too -private: - void return_focus(); // hand the keyboard back to the canvas, not to a hidden window -public: - // True when the field itself holds keyboard focus. Callers use this to decide whether the - // field will handle a key on its own or needs it forwarded — see DesignPanel's CHAR_HOOK. - bool has_focus() const { return m_ctrl != nullptr && wxWindow::FindFocus() == m_ctrl; } + // Draw it, and let ImGui do the editing. Called from DesignSketchTool::render() inside the + // frame's ImGui pass; `scale` is the tool's m_render_scale. Returns true if it drew. + bool render(ImGuiWrapper& imgui, float scale); + + // Ask for another frame. THE FIELD DOES NOT WORK WITHOUT THIS, and the reason is a deadlock + // that only a per-frame trace shows: + // + // [UX] frame want_text=0 want_kb=0 active=0 <- frame 1: the widget is not active yet + // [UX] frame want_text=0 want_kb=0 active=1 <- frame 2: it is now + // (nothing further) <- the canvas has nothing to redraw, so it stops + // + // This canvas repaints ON DEMAND. ImGui decides whether it wants the keyboard at the END of a + // frame, from the active item, and GLCanvas3D::on_char only calls render() when + // update_key_data() says ImGui wants it. No frames -> WantTextInput never turns on -> no + // render on a keystroke -> still no frames. The characters sit in ImGui's queue and the field + // looks exactly as deaf as the window it replaced. One repaint per frame while it is open + // breaks the circle. + std::function request_frame; + + // Kept because callers ask them, but there is no longer any difference to report: with no + // window there is no state where the field is on screen but logically closed, and no state + // where it is open but somebody else holds the keyboard. + bool is_mapped() const { return m_open; } + bool has_focus() const { return m_open; } + void dismiss() { close(); } private: void do_commit(); void do_cancel(); - // Say WHY a value was refused, in the title line above the field. Refusing input in - // silence is indistinguishable from the app having frozen — the field just sits there - // with the text re-selected and the user has no idea what it wants. - void refit(); // re-Fit around a changed title, then re-clamp on-screen - void flag_invalid(const wxString& why); - void clear_invalid(); - wxWindow* m_parent{nullptr}; // the GL canvas: where focus must go back to - wxFrame* m_frame{nullptr}; - wxTextCtrl* m_ctrl{nullptr}; - wxStaticText* m_title{nullptr}; std::function m_commit; std::function m_cancel; - bool m_open{false}; - bool m_closing{false}; - wxString m_title_text; // the real title, restored after an error message + bool m_open{false}; + bool m_focus_pending{false}; // one frame of SetKeyboardFocusHere after opening + wxPoint m_anchor{0, 0}; // canvas device px + std::string m_title; + std::string m_err; // why the last value was refused, shown in the title line + char m_buf[64]{}; // the edited text; ImGui::InputText writes into it }; }} // namespace Slic3r::GUI diff --git a/src/slic3r/GUI/ImGuiWrapper.cpp b/src/slic3r/GUI/ImGuiWrapper.cpp index eeaadd0cbd..cf107565e3 100644 --- a/src/slic3r/GUI/ImGuiWrapper.cpp +++ b/src/slic3r/GUI/ImGuiWrapper.cpp @@ -505,6 +505,23 @@ bool ImGuiWrapper::update_key_data(wxKeyEvent &evt) if (evt.GetEventType() == wxEVT_CHAR) { // Char event const auto key = evt.GetUnicodeKey(); + // THE MEASUREMENT THAT CANNOT LIE. This is the ONLY place in the application where ImGui + // is ever handed a character, so an ImGui text field that stays empty while reporting + // itself active has exactly two possible causes, and this line separates them: no output + // at all means the wxEVT_CHAR never reached the GL canvas (a focus problem, upstream of + // ImGui entirely), while output with unicode=0 means the character arrived empty and is + // being dropped right here. + // + // It lives here rather than on the canvas because a probe bound on the canvas CANNOT + // answer this: GLCanvas3D::on_char is bound later than any constructor-time probe, wx + // runs handlers in reverse bind order, and on_char returns without Skip() whenever this + // function returns true — so such a probe stays silent whether or not the key arrived. + // A day was lost to reading that silence as evidence. + if (std::getenv("ORCA_CAD_UXTRACE")) { + fprintf(stderr, "[UX] imgui_char unicode=%d keycode=%d want_text=%d\n", + (int) key, evt.GetKeyCode(), (int) io.WantTextInput); + fflush(stderr); + } if (key != 0) { io.AddInputCharacter(key); } diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index cacf4d26eb..e9791719ba 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -1361,7 +1361,7 @@ void MainFrame::init_tabpanel() { m_design_page = new wxPanel(this); m_design_page->SetSizer(new wxBoxSizer(wxVERTICAL)); m_design_page->Hide(); - start_mcp_control_if_enabled(); // opens the MCP socket iff SNAPORCA_MCP is set + start_mcp_control_if_enabled(); // opens the MCP socket iff ORCA_CAD_MCP is set } #endif diff --git a/tests/libslic3r/test_caddocument.cpp b/tests/libslic3r/test_caddocument.cpp index 1b8f3949e0..d69ada09b6 100644 --- a/tests/libslic3r/test_caddocument.cpp +++ b/tests/libslic3r/test_caddocument.cpp @@ -1,7 +1,7 @@ #include // mainline OrcaSlicer ships Catch2 v3 (v2 was catch2/catch.hpp) // Substring assertions, spelled so this file compiles UNCHANGED on both forks. -// Catch2 v2 (snaporca) spells it Matchers::Contains; v3 (orca_cad / mainline) spells it +// Catch2 v2 (Snapmaker) spells it Matchers::Contains; v3 (orca_cad / mainline) spells it // Matchers::ContainsSubstring and gives Contains an incompatible meaning — range-contains- // ELEMENT — which fails to compile against a std::string rather than failing a test. // Using find() sidesteps the rename entirely; INFO keeps the actual string in the report. @@ -613,7 +613,7 @@ TEST_CASE("entity constraints: point-on-line positions a centre onto an axis", " // vendored solver (slvs/dsc.h FindById, "Cannot find handle"), taking every later test with // it, and was quarantined for it. Fixed in SketchSolver: a full circle can no longer be handed // to SLVS_C_ARC_LINE_TANGENT, which dereferences arc endpoints a circle does not have. See -// snaporca-tkz. +// tkz. TEST_CASE("entity constraints: tangent/midpoint/symmetric/angle", "[CadDocument][sketch]") { using R = SketchPointRole; @@ -962,7 +962,7 @@ TEST_CASE("extrude taper + up-to-face distance", "[CadDocument]") } } -// Was [known-broken] until the numbers were actually measured (snaporca-kzy). The geometry +// Was [known-broken] until the numbers were actually measured (kzy). The geometry // was right all along; the TEST compared against the wrong reference. An internal thread bores // at the MINOR radius (radius - depth) and then carves the groove out to radius + depth, so a // tapped hole keeps the crests between turns and therefore holds MORE material than a plain @@ -1330,7 +1330,7 @@ TEST_CASE("datum plane: offset + tilt resolution and sketching on it", "[CadDocu // A datum-plane-only document has no solid, and that is a benign SUCCESS, not a benign // failure. It used to return false, and "benign failure" is exactly the phrasing that hid - // snaporca-mtav: two callers read the false as "unusable document" and threw the design + // mtav: two callers read the false as "unusable document" and threw the design // away — the 3MF recipe was never written, and a project that had one was refused on load. CadDocument only_plane; only_plane.add_plane(0, 10.0, 0.0, 0, "P"); @@ -1416,7 +1416,7 @@ TEST_CASE("draft tapers a solid face about the body base", "[CadDocument]") TEST_CASE("a split renumbers the bodies a later feature indexes", "[CadDocument][cut]") { - // Pins the invariant the Design tab's re-edit path depends on (snaporca-oz7): a stored + // Pins the invariant the Design tab's re-edit path depends on (oz7): a stored // target_body indexes the body list AS IT WAS when that feature ran, and a Cut placed // later in the tree changes that list. If this test ever fails, the GUI's // fill_body_choice() replay-to-timeline-slot assumption needs revisiting with it. @@ -2502,7 +2502,7 @@ TEST_CASE("datum plane construction methods", "[CadDocument][plane]") } } -// Mirrors the snaporca [Deviation] case (snaporca carries it in test_geometry.cpp; here it lives +// Mirrors the Snapmaker [Deviation] case (Snapmaker carries it in test_geometry.cpp; here it lives // alongside the CAD suite). GeometryEngine::surface_deviation = one-sided Hausdorff used by the // MCP validate_against acceptance metric. TEST_CASE("surface_deviation: identical solids ~0, shifted solid ~shift", "[Deviation]") @@ -5310,7 +5310,7 @@ TEST_CASE("thicken-surface makes a solid from a sheet", "[CadDocument][surface]" REQUIRE_THAT(double(tx1), WithinAbs(double(sx1), 2.1)); } -// Regression for snaporca-lu27, with the rig's own numbers. A 60x60x40 four-walled open box +// Regression for lu27, with the rig's own numbers. A 60x60x40 four-walled open box // used to report volume 96000 and an inertia diagonal of [-4.2e7, -4.2e7, -6.9e7] — negative // principal moments, which no real body can have. VolumeProperties was being integrated over // an open shell as though it were closed, and std::abs() on the mass hid the only obvious tell. @@ -5352,7 +5352,7 @@ TEST_CASE("mass properties of a sheet body report area only, never a volume", REQUIRE(solid.inertia[8] > 0.0); } -// snaporca-wm4s. The wall of a thickened open box must contain the corner material. Thickening +// wm4s. The wall of a thickened open box must contain the corner material. Thickening // each face along its own normal and sewing (MakeThickSolidBySimple) leaves the four vertical // corners empty and measured 29648.15 where the geometry requires 44000; the two controls below // were exact before and must stay exact, since they are what a corner-only fix must not disturb. @@ -7309,7 +7309,7 @@ TEST_CASE("interference: detects a clash created by a mate", "[CadDocument][inte // A filleted solid must reach the plate as a watertight mesh. OCCT emits one degenerate // triangle at the pole of every corner sphere patch; welded, its v->v edge counts as an open // edge and the slicer tells the user to go repair the model in another CAD application -- -// the exact round trip this feature exists to remove. snaporca-agw. +// the exact round trip this feature exists to remove. agw. TEST_CASE("CadDocument filleted solid tessellates watertight", "[CadDocument]") { CadDocument doc; @@ -7339,7 +7339,7 @@ TEST_CASE("CadDocument filleted solid tessellates watertight", "[CadDocument]") // into the params whether or not it converged, so reading geometry back unconditionally made // every failed attempt destructive -- and the fillet degrade ladder tries a deliberately // over-constrained rung FIRST, so a filleted corner was wrecked before the rung that works -// ever got a chance. snaporca-pl5. +// ever got a chance. pl5. TEST_CASE("Failed sketch solve leaves geometry untouched", "[CadDocument]") { using R = SketchPointRole; @@ -7415,7 +7415,7 @@ TEST_CASE("Failed sketch solve leaves geometry untouched", "[CadDocument]") // A subtraction whose tool misses the target is a perfectly legal boolean that removes nothing, // so OCCT reports success and the feature lands in the recipe with ok:true and an unchanged body. // That is how a hole placed with world coordinates instead of plane-frame ones read as "drilled" -// three times in a row while the volume never moved. snaporca-daf. +// three times in a row while the volume never moved. daf. TEST_CASE("A cut that removes no material is an error, not a silent success", "[CadDocument]") { // 20 x 20 box, 20 tall, centred on the origin of the XY plane. @@ -7472,7 +7472,7 @@ TEST_CASE("A cut that removes no material is an error, not a silent success", "[ // Anything else returns a null wire, and build_sketch_wire used to answer that by falling through // to its legacy tail — which ends in a rectangle built from width/height. For an entity sketch // those are whatever they were initialised to, so the extrude produced a box nobody drew. -// snaporca-88v. +// 88v. TEST_CASE("An entity sketch that forms no wire fails instead of extruding a default box", "[CadDocument]") { auto circle = [](Vec2d c, double r) { @@ -7562,7 +7562,7 @@ CadDocument plate_doc(const std::vector& entities, double distance } // namespace -// snaporca-88v: a sketch may hold more than one closed loop. The Extrude path builds the +// 88v: a sketch may hold more than one closed loop. The Extrude path builds the // sketch's planar region via SketchEngine::entities_to_wires + wires_to_face: the largest loop // is the outer boundary, every other loop a hole. Volumes are the proof — a plate with a hole // must subtract the hole, not merely "not throw". @@ -7637,7 +7637,7 @@ TEST_CASE("entities_to_wires returns one wire per loop", "[CadDocument][sketchwi // Sketching on a picked face is the most common gesture in solid modelling, and it was impossible: // the plane came from a combo of base + datum planes only, so the sole route onto a face was to // build a Coincident datum plane first. plane_of_face is the shared derivation that makes the -// viewport selection usable directly. snaporca-3a2. +// viewport selection usable directly. 3a2. TEST_CASE("plane_of_face gives a sketchable plane for a planar face only", "[CadDocument]") { // 20 x 20 x 20 box on XY, so its top face sits at z = 20 with +Z normal. @@ -7689,7 +7689,7 @@ TEST_CASE("plane_of_face gives a sketchable plane for a planar face only", "[Cad } } -// snaporca-5425 — POSITIVE-CONTRACT variant. A feature that left a body with a null +// 5425 — POSITIVE-CONTRACT variant. A feature that left a body with a null // TopoDS_Shape used to be tolerated: recompute() returned true and the document kept // advertising the body. The new guard makes that a hard failure. This test asserts the // contract the guard preserves on the healthy side: a normal box + fillet document @@ -7727,7 +7727,7 @@ TEST_CASE("recompute on a healthy box + fillet leaves no body null and no error } // ============================================================================ -// snaporca-rgbj — does a chamfer chain degenerate from a KERNEL defect, or from +// rgbj — does a chamfer chain degenerate from a KERNEL defect, or from // how the DRIVER captured its edge ids? Experiment, not a fix. // // CadFeature::dressup_edge is a GLOBAL edge id: an ordinal into @@ -8242,7 +8242,7 @@ TEST_CASE("add_extrude_entities builds a plate with a bore (clockwise circle)", } -// snaporca-mtav. A document that has only sketches in it is not a broken document, it is the +// mtav. A document that has only sketches in it is not a broken document, it is the // state every design passes through between drawing a profile and extruding it. recompute() // used to call that "no solid-producing features" and return false, and two things downstream // read that false as "the document is unusable": the GUI syncs the 3MF recipe only after a diff --git a/tests/libslic3r/test_slvs_constraints.cpp b/tests/libslic3r/test_slvs_constraints.cpp index b665a1df0c..6dfac07348 100644 --- a/tests/libslic3r/test_slvs_constraints.cpp +++ b/tests/libslic3r/test_slvs_constraints.cpp @@ -113,7 +113,7 @@ TEST_CASE("slvs: over-constrained / inconsistent is detected", "[slvs]") CHECK_FALSE(res.ok); // SLVS_RESULT_INCONSISTENT } -// snaporca-yww4. libslvs sizes its System with a compile-time `MAX_UNKNOWNS = 1024`, and the +// yww4. libslvs sizes its System with a compile-time `MAX_UNKNOWNS = 1024`, and the // solver is handed every entity in the sketch at 2 params per point — so a sketch of about 480 // lines is the last one that fits and the next comes back TOO_MANY_UNKNOWNS. Because // try_add_constraints rolls a failed batch back, that turned into: every auto-inferred constraint diff --git a/tools/snaporca_mcp_bridge.py b/tools/orca_cad_mcp_bridge.py similarity index 95% rename from tools/snaporca_mcp_bridge.py rename to tools/orca_cad_mcp_bridge.py index 2976d074ee..d54320a9cb 100644 --- a/tools/snaporca_mcp_bridge.py +++ b/tools/orca_cad_mcp_bridge.py @@ -1,20 +1,20 @@ #!/usr/bin/env python3 -"""Zero-dependency stdio MCP server bridging to the SnapOrca/Orca-CAD control socket. +"""Zero-dependency stdio MCP server bridging to the Orca/Orca-CAD control socket. Speaks MCP (JSON-RPC 2.0 over newline-delimited stdio) to an MCP client (Claude Code), and forwards each tool call to the app's Unix-domain control socket (opened by the GUI -when launched with SNAPORCA_MCP set). The tool list is built *live* from the app's own +when launched with ORCA_CAD_MCP set). The tool list is built *live* from the app's own `describe_tools` reply — introspection drives the schema, so new kernel methods surface without touching this file. -Usage: snaporca_mcp_bridge.py [SOCKET_PATH] (default /tmp/snaporca-mcp.sock) -The app must be running with SNAPORCA_MCP set; if the socket is down, tools/list falls +Usage: orca_cad_mcp_bridge.py [SOCKET_PATH] (default /tmp/orca-cad-mcp.sock) +The app must be running with ORCA_CAD_MCP set; if the socket is down, tools/list falls back to the slice-1 set and tool calls report the connection error (never crash). """ import sys, os, json, socket, itertools -SOCK_PATH = sys.argv[1] if len(sys.argv) > 1 else "/tmp/snaporca-mcp.sock" -SERVER_INFO = {"name": "snaporca-cad", "version": "0.1"} +SOCK_PATH = sys.argv[1] if len(sys.argv) > 1 else "/tmp/orca-cad-mcp.sock" +SERVER_INFO = {"name": "orca-cad", "version": "0.1"} _app_id = itertools.count(1) # --- app control-socket round-trip --------------------------------------------