mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-21 16:02:37 +00:00
This fork had never been gated end to end. Five separate things stopped it, none
of them a defect in the CAD code itself, and each failure named the wrong
subsystem — which is why they survived.
1. run-all-checks.sh invoked docs/ux/mockups/gen_offer_table.py. The design docs
moved to docs/CAD/ (bbd1989e1e) and this path did not follow, so the rung
failed on a missing file.
2. gen_offer_table.py then resolved REPO one dirname short, because it now sits a
level deeper. OUT pointed at docs/src/.../DesignOffer.hpp, which does not
exist, so --check diffed the generated table against an EMPTY file and
reported all 189 lines as a difference.
3. DISPLAY was never passed into the container. The check scripts fall back to
":10", which is the other fork's rig; this one's Xvfb is :11. Symptom:
"FATAL no app window on :10", which reads like a dead app.
4. Every absolute chrome coordinate was written in the Snapmaker fork's layout.
This fork keeps mainline's top row (File / save / undo / redo / Calibration
with the title), so the whole chrome sits 26 px lower. At the unshifted y the
Design-tab click landed in the toolbar and the app stayed on the Home page,
reported as "no sketch opened after plane click + Shift+S"; the unshifted
Construction checkbox reported "0 construction axis". The three constants now
derive from CHROME_DY. Canvas coordinates were never affected -- clickmm()
computes them from live canvas geometry -- which is why dozens of geometric
properties passed exactly on a GUI that had never been driven.
5. CON_BTN was stale for every index after 5, from the sketch epic's six new
buttons. Fixed in both forks; see the companion commit on snaporca.
pdftocairo was also missing from the rig image (installed there, not a repo
change), without which every corpus sheet threw.
RESULT: offer-table, kernel, engine, corpus, corpus-scale and offer all hold.
The gesture ladder now reaches D2 and applies Equal length through the Constrain
toolbar, both sides landing at 99.928133658; it then fails re-entering the sketch
after commit, filed as snaporca-eoj1 with the evidence and the next measurement
to take. Kernel here is 7701 assertions / 270 cases.
174 lines
8.2 KiB
Python
174 lines
8.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Emit the C++ offer table from docs/ux/tool_atlas.json.
|
|
|
|
python3 docs/ux/mockups/gen_offer_table.py
|
|
|
|
The map exists ONCE. The mockups and the shipping menu read the same rows in the same
|
|
order from the same file, so a drawing and the product cannot drift apart — which is the
|
|
only way row constancy (charter 4.1) survives contact with a codebase.
|
|
|
|
Output: src/slic3r/GUI/CAD/DesignOffer.hpp, checked in and never hand-edited.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
UX = os.path.dirname(HERE)
|
|
# One dirname more than you would expect: this generator lives at docs/CAD/ux/mockups/,
|
|
# not docs/ux/mockups/, since the design docs moved into the CAD subfolder (bbd1989e1e).
|
|
# With the old count REPO resolved to docs/, so OUT pointed at docs/src/.../DesignOffer.hpp,
|
|
# which does not exist -- and --check then diffed the real generated table against an empty
|
|
# file and reported the whole 189-line header as a difference.
|
|
REPO = os.path.dirname(os.path.dirname(os.path.dirname(UX)))
|
|
ATLAS = os.path.join(UX, "tool_atlas.json")
|
|
OUT = os.path.join(REPO, "src", "slic3r", "GUI", "CAD", "DesignOffer.hpp")
|
|
|
|
# selection id -> C++ enumerator
|
|
ENUM = {
|
|
"none": "None", "face_planar": "FacePlanar", "face_cyl": "FaceCyl",
|
|
"face_other": "FaceOther", "edge_str": "EdgeStr", "edge_circ": "EdgeCirc",
|
|
"vertex": "Vertex", "body_solid": "BodySolid", "body_sheet": "BodySheet",
|
|
"bodies_2": "Bodies2", "datum_plane": "DatumPlane", "datum_axis": "DatumAxis",
|
|
"coordsys": "CoordSys", "art": "Art", "sk_loop": "SkLoop", "sk_none": "SkNone",
|
|
"sk_line": "SkLine", "sk_arc": "SkArc", "sk_point": "SkPoint", "sk_2ent": "Sk2Ent",
|
|
}
|
|
|
|
|
|
def cstr(s):
|
|
if s is None:
|
|
return "nullptr"
|
|
return '"' + s.replace("\\", "\\\\").replace('"', '\\"') + '"'
|
|
|
|
|
|
def main():
|
|
A = json.load(open(ATLAS, encoding="utf-8"))
|
|
sels = [s["id"] for s in A["selections"]]
|
|
assert all(s in ENUM for s in sels), [s for s in sels if s not in ENUM]
|
|
slots = [s["id"] for s in A["slots"]]
|
|
|
|
lines = [
|
|
"// GENERATED FILE — DO NOT EDIT.",
|
|
"// Source: docs/ux/tool_atlas.json Generator: docs/ux/mockups/gen_offer_table.py",
|
|
"//",
|
|
"// The object-driven tool offer (charter 4.1): every verb has ONE row index, that index",
|
|
"// is the same in every selection it appears in, and verbs that do not apply are shown",
|
|
"// disabled in place with their reason rather than removed. Row order was ratified",
|
|
"// 2026-07-31; changing an index is a breaking change to every user's muscle memory.",
|
|
"#ifndef slic3r_GUI_DesignOffer_hpp_",
|
|
"#define slic3r_GUI_DesignOffer_hpp_",
|
|
"",
|
|
"#include <cstdint>",
|
|
"",
|
|
"namespace Slic3r { namespace GUI {",
|
|
"",
|
|
"// What the viewport has selected. Ordered as in tool_atlas.json; the bitmask in",
|
|
"// OfferVerb::accepts indexes these.",
|
|
"enum class OfferSel : int {",
|
|
]
|
|
for i, s in enumerate(sels):
|
|
lines.append(f" {ENUM[s]} = {i},")
|
|
lines += [
|
|
f" Count = {len(sels)}",
|
|
"};",
|
|
"",
|
|
"inline uint32_t offer_bit(OfferSel s) { return 1u << int(s); }",
|
|
"",
|
|
"// One row of the offer. `action` routes to the code that already implements the verb:",
|
|
'// "key:S+E" -> m_keys_feature[SHIFT(\'E\')]',
|
|
'// "key:L" -> m_keys_sketch[\'L\']',
|
|
'// "fly:material#4" -> row 4 of the "material" feature flyout',
|
|
'// "btn:delete" -> a standalone toolbar button',
|
|
"// nullptr -> kernel support exists, no GUI path yet (row shows disabled)",
|
|
"struct OfferVerb {",
|
|
" const char* id;",
|
|
" const char* name; // drawing-office word (L10); translated at use with wxGetTranslation",
|
|
" int row; // 0..7, the ratified index — NEVER reorder",
|
|
" const char* key; // shortcut shown in the row, or nullptr",
|
|
" const char* action;",
|
|
" const char* refusal; // why this row is greyed, in the product's own words",
|
|
" uint32_t accepts; // bitmask over OfferSel",
|
|
" int need_bodies;",
|
|
" int need_sketches;",
|
|
" bool need_sheet;",
|
|
" bool sketch_mode; // belongs to the sketch-mode vocabulary, not the model one",
|
|
" // Second level INSIDE a row, for tools that come in variants: \"Rectangle\" holds corner,",
|
|
" // centre, oblique and rounded. nullptr = sits directly in the row. Keeps the row's own",
|
|
" // address fixed (L4.1) while the variants hang one level below it, mirroring the toolbar's",
|
|
" // grouping instead of flattening 19 create tools into one wall.",
|
|
" const char* family;",
|
|
" const char* icon; // resources/images name, or nullptr — the offer draws it beside the row",
|
|
" const char* hint; // what the verb does / what to click; shown on hover",
|
|
"};",
|
|
"",
|
|
"// Row labels, in ratified order.",
|
|
"static const char* const kOfferRowNames[] = {",
|
|
]
|
|
for s in A["slots"]:
|
|
lines.append(f' "{s["label"]}",')
|
|
lines += [
|
|
"};",
|
|
f"static const int kOfferRowCount = {len(slots)};",
|
|
"",
|
|
"static const OfferVerb kOfferVerbs[] = {",
|
|
]
|
|
# A verb the user can PICK must say what it does. Fail loudly rather than ship a
|
|
# bare name — the offer is now the only door to these tools.
|
|
blind = [v["id"] for v in A["verbs"] if v.get("action") and not v.get("hint")]
|
|
assert not blind, f"wired verbs with no hint: {blind}"
|
|
for v in A["verbs"]:
|
|
# 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
|
|
# the explanation.
|
|
for ln in ([v["note"]] if isinstance(v.get("note"), str) else v.get("note") or []):
|
|
lines.append(f" // {ln}")
|
|
mask = 0
|
|
for a in v["accepts"]:
|
|
mask |= 1 << sels.index(a)
|
|
n = v.get("needs") or {}
|
|
lines.append(
|
|
" {%s, %s, %d, %s, %s, %s, 0x%08xu, %d, %d, %s, %s, %s, %s, %s}," % (
|
|
cstr(v["id"]), cstr(v["name"]), slots.index(v["slot"]),
|
|
cstr(v.get("key")), cstr(v.get("action")), cstr(v.get("refusal")),
|
|
mask, n.get("bodies", 0), n.get("sketches", 0),
|
|
"true" if n.get("sheet") else "false",
|
|
"true" if v.get("mode") == "sketch" else "false",
|
|
cstr(v.get("family")), cstr(v.get("icon")), cstr(v.get("hint"))))
|
|
lines += [
|
|
"};",
|
|
f"static const int kOfferVerbCount = {len(A['verbs'])};",
|
|
"",
|
|
"}} // namespace Slic3r::GUI",
|
|
"",
|
|
"#endif // slic3r_GUI_DesignOffer_hpp_",
|
|
"",
|
|
]
|
|
text = "\n".join(lines)
|
|
# --check: prove the checked-in header IS what this generator produces, and change nothing.
|
|
# The header calls itself GENERATED and was hand-edited anyway; a claim like that is only
|
|
# worth having if something enforces it, so run-all-checks.sh runs this on every gate.
|
|
if "--check" in sys.argv:
|
|
have = open(OUT, encoding="utf-8").read() if os.path.exists(OUT) else ""
|
|
if have == text:
|
|
print(f"{os.path.relpath(OUT, REPO)} matches tool_atlas.json")
|
|
return 0
|
|
import difflib
|
|
d = list(difflib.unified_diff(have.splitlines(), text.splitlines(),
|
|
"checked-in", "generated", lineterm="", n=1))
|
|
print(f"{os.path.relpath(OUT, REPO)} DIFFERS from tool_atlas.json:")
|
|
print("\n".join(d[:60]))
|
|
return 1
|
|
with open(OUT, "w", encoding="utf-8") as f:
|
|
f.write(text)
|
|
wired = sum(1 for v in A["verbs"] if v.get("action"))
|
|
print(f"wrote {os.path.relpath(OUT, REPO)}: {len(A['verbs'])} verbs, "
|
|
f"{len(slots)} rows, {wired} wired to existing actions")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|