mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-18 14:32:36 +00:00
Sync cad-mainline with upstream main and carry the value-field + rename work on top
This commit is contained in:
@@ -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 <file> orcacad-gui:/OrcaSlicer/<path>
|
||||
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.
|
||||
@@ -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 —
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
<div class="wrap">
|
||||
|
||||
<header>
|
||||
<div class="eyebrow">SnapOrca Design · assembly</div>
|
||||
<div class="eyebrow">Orca Design · assembly</div>
|
||||
<h1>Mate connector glyph — polarity and verse</h1>
|
||||
<p class="sub">
|
||||
Onshape's core (disc + roll quadrant + Z arrow) is adopted unchanged because it is proven and
|
||||
|
||||
@@ -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])}}},")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
+10
-10
@@ -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 <target>` writes `/OrcaSlicer/build/src/Release/<binary>`; only `build_linux.sh`
|
||||
additionally packages to `/OrcaSlicer/build/package/bin/<binary>`. `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.
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
# orcacad-deps, NOT snaporca-deps: see the note in run-kernel-tests.sh — the wrong image
|
||||
# orcacad-deps, NOT snapmaker-deps: see the note in run-kernel-tests.sh — the wrong image
|
||||
# fails at CMake configure, not at link time.
|
||||
IMAGE="${IMAGE:-orcacad-deps}"
|
||||
BUILD_VOL="${BUILD_VOL:-orcacad_buildcache}"
|
||||
@@ -32,11 +32,11 @@ echo "REPO=$REPO IMAGE=$IMAGE BUILD_VOL=$BUILD_VOL"
|
||||
# the build fails with "class GLCanvas3D has no member named set_design_sketch_tool".
|
||||
#
|
||||
# build_linux.sh must be mounted for the same reason, and here the stale copy is guaranteed
|
||||
# wrong rather than merely risky: orcacad-deps is layered on snaporca-deps, so the baked script
|
||||
# wrong rather than merely risky: orcacad-deps is layered on snapmaker-deps, so the baked script
|
||||
# is the OTHER fork's and builds `--target Snapmaker_Orca`. This fork's target is `OrcaSlicer`,
|
||||
# so without this mount configure succeeds and then ninja dies on "unknown target".
|
||||
# scripts/ likewise: build_linux.sh's packaging step sources scripts/appimage_lib_policy.sh,
|
||||
# which the baked snaporca tree does not have, so a fully successful link still exited
|
||||
# which the baked Snapmaker tree does not have, so a fully successful link still exited
|
||||
# non-zero with "missing AppImage helper" and the binary check never ran.
|
||||
|
||||
# ---- OOM guard (2026-08-21) -------------------------------------------------------------
|
||||
|
||||
@@ -2,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
|
||||
|
||||
Executable
+658
@@ -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())
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,7 @@ cost a GUI session and a human. The socket verbs make each one a call, and this
|
||||
loop: build a known profile, ask the app what it thinks it has, compare against arithmetic.
|
||||
|
||||
RUN IT AGAINST A RUNNING APP:
|
||||
SNAPORCA_MCP=/tmp/mcp.sock <binary> # launch with the socket enabled
|
||||
ORCA_CAD_MCP=/tmp/mcp.sock <binary> # launch with the socket enabled
|
||||
python3 scripts/CAD/check-mcp-sketch.py [socket] # default /tmp/mcp.sock
|
||||
|
||||
Exit 0 = every assertion held. Anything else prints the first mismatch and stops.
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -15,7 +15,7 @@ Every rung asserts those. Area appears only as a cross-check, never as the verdi
|
||||
|
||||
Entirely 2D: sketch entities only, no extrude, revolve or any solid feature.
|
||||
|
||||
SNAPORCA_MCP=/tmp/mcp.sock <binary>
|
||||
ORCA_CAD_MCP=/tmp/mcp.sock <binary>
|
||||
python3 scripts/CAD/check-sketch-engine.py [socket]
|
||||
|
||||
Exit 0 = every rung held. Otherwise the first broken property is named and the run stops.
|
||||
|
||||
Executable
+85
@@ -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"
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<CadBody>& 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<CadBody>& 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,
|
||||
|
||||
@@ -403,7 +403,7 @@ static SketchSolveResult solve_system(std::vector<SketchEntity>& 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];
|
||||
|
||||
@@ -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<SketchInlineEditor>(m_canvas_widget);
|
||||
m_inline_editor = std::make_unique<SketchInlineEditor>();
|
||||
// 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<void(double)> commit,
|
||||
std::function<void()> 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<void(double)>
|
||||
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);
|
||||
|
||||
@@ -94,7 +94,7 @@ public:
|
||||
void set_on_segment_drawn(std::function<void(double, double)> cb);
|
||||
void set_on_cursor_metrics(std::function<void(double, double, bool)> cb);
|
||||
void set_on_solve_state(std::function<void(int, bool, bool)> 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<void(DesignSketchTool::Mode, int, int)> 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<std::pair<int, ColorRGBA>> hl);
|
||||
void set_datum_planes(std::vector<SketchPlane> planes,
|
||||
std::vector<Vec2d> 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<DesignSketchTool::MateConnectorGlyph> g);
|
||||
void set_mate_links(std::vector<std::pair<Vec3d, Vec3d>> 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.
|
||||
|
||||
@@ -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:<family>#<row>" 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:<family>#<row>" 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<wxTextCtrl*>(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-<offer ladder>.
|
||||
// ORCA_CAD_KEYTRACE so a rig run needs one env var, not two. <offer ladder>.
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 <imgui/imgui.h>
|
||||
@@ -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<SketchEntityConstra
|
||||
m_constraints.resize(mark); // roll back the conflicting batch
|
||||
// No re-solve to "restore": a failed solve no longer touches the geometry
|
||||
// (SketchSolver.cpp only writes back on success), so m_entities still holds the
|
||||
// prior solved state exactly. snaporca-pl5.
|
||||
// prior solved state exactly. pl5.
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2501,11 +2516,11 @@ void DesignSketchTool::infer_auto_constraints(int base, double ang_tol_rad, doub
|
||||
// never costs the others. Every rule only pins a relation that is ALREADY true, so
|
||||
// nothing the user drew is moved by this.
|
||||
// A SCRIPTED ADD IS NOT A DRAWN GESTURE — the rule this function already states at its
|
||||
// bulk call site, which passes zero tolerances for exactly that reason (snaporca-8xg1).
|
||||
// bulk call site, which passes zero tolerances for exactly that reason (8xg1).
|
||||
// Relational inference must obey it too, and for a second reason beyond tolerance:
|
||||
// EqualRadius couples entities that are geometrically far apart, so on a real drawing it
|
||||
// merges independent connected components into one huge system and defeats the
|
||||
// component partitioning that makes large sketches solvable at all (snaporca-yww4).
|
||||
// component partitioning that makes large sketches solvable at all (yww4).
|
||||
// Measured 2026-08-31 on the corpus rung: geometry stayed correct (32/32 sheets clean)
|
||||
// but seven of the largest sheets hit main-thread timeout — MPD681 among them, the very
|
||||
// sheet named in the comment at the bulk call site. Exact-equality would not save it
|
||||
@@ -2636,7 +2651,7 @@ bool DesignSketchTool::add_imported_regions(
|
||||
// Art is not "just drawn", so it must NOT enter the draw-then-edit queue. Without this the
|
||||
// glyph contours are treated as fresh entities and a Length field opens on the first of
|
||||
// them — on a word, that is one value editor per segment, and an open field freezes the
|
||||
// canvas (snaporca-yce). reset_autoedit() marks every entity as already seen.
|
||||
// canvas (yce). reset_autoedit() marks every entity as already seen.
|
||||
reset_autoedit();
|
||||
// The new lines carry no constraints, so the solver has nothing to move; resolve anyway so
|
||||
// the degrees-of-freedom readout counts them instead of going stale.
|
||||
@@ -3107,7 +3122,7 @@ void DesignSketchTool::hit_display_sketch(const DisplaySketch& d, const Vec2d& p
|
||||
{
|
||||
const std::vector<RegionLoop> 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<SketchEntity>& 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<SketchEntity>& 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<Vec2d>& 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<SketchEntity> before = m_entities;
|
||||
const size_t cmark = m_constraints.size();
|
||||
std::vector<std::pair<int, SketchEntity>> 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<Vec2d> 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<SketchEntity>& 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<SketchEntity>& 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<int> 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());
|
||||
|
||||
@@ -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<void(int feature, int region, int entity)> 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<void(double length, double angle_deg, bool locked)> 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<Vec2d> poly;
|
||||
std::vector<int> 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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 <wx/display.h>
|
||||
#include "slic3r/GUI/ImGuiWrapper.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "libslic3r/Color.hpp"
|
||||
|
||||
#include <wx/frame.h>
|
||||
#include <wx/textctrl.h>
|
||||
#include <wx/stattext.h>
|
||||
#include <wx/sizer.h>
|
||||
#include <wx/window.h>
|
||||
#include <wx/toplevel.h>
|
||||
#include <wx/gdicmn.h>
|
||||
#include <imgui/imgui.h>
|
||||
#include <imgui/imgui_internal.h> // BringWindowToDisplayFront / GetCurrentWindow
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
#ifdef __WXGTK__
|
||||
#include <gtk/gtk.h>
|
||||
#ifdef GDK_WINDOWING_X11
|
||||
#include <gdk/gdkx.h>
|
||||
#endif
|
||||
#endif
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
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<GtkWidget*>(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<GtkWidget*>(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<void(double)> on_commit,
|
||||
std::function<void()> 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
|
||||
|
||||
@@ -4,72 +4,90 @@
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include <wx/window.h>
|
||||
|
||||
class wxFrame;
|
||||
class wxTextCtrl;
|
||||
class wxStaticText;
|
||||
class wxPoint;
|
||||
#include <wx/gdicmn.h>
|
||||
|
||||
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<void(double)> on_commit,
|
||||
std::function<void()> 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<void()> 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<void(double)> m_commit;
|
||||
std::function<void()> 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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include <catch2/catch_all.hpp> // 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<SketchEntity>& 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 --------------------------------------------
|
||||
Reference in New Issue
Block a user