mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-04 06:11:02 +00:00
Merge branch 'main' into pr/nuclearmistake/16007
This commit is contained in:
+16
-1
@@ -33,6 +33,21 @@ part of `libslic3r_tests` and run under `ctest` on every platform like any other
|
||||
unit test. This script exists for the local loop, where it is a two-minute round
|
||||
trip instead of a full application build.
|
||||
|
||||
## Generated sources
|
||||
|
||||
Two checked-in files are emitted from data that lives here, so the source and the thing
|
||||
compiled against it cannot drift apart:
|
||||
|
||||
| Generator | Source | Output |
|
||||
|---|---|---|
|
||||
| `gen_offer_table.py` | `tool_atlas.json` — every offer verb with its row, key, icon, accepted selections and refusal string | `src/slic3r/GUI/CAD/DesignOffer.hpp` |
|
||||
| `mate-glyph/emit_glyph_table.py` | `mate-glyph/bear_outline.json`, measured off `mate-glyph/bear.step` by `extract_outline.py` (needs FreeCAD) | the `kBear…` tables in `DesignSketchTool.cpp` |
|
||||
|
||||
`python3 scripts/CAD/gen_offer_table.py` rewrites the header and `--check` proves the
|
||||
checked-in one matches the atlas — `run-all-checks.sh` runs the check as its first rung,
|
||||
which is what makes the header's "GENERATED — DO NOT EDIT" enforceable. Edit the atlas,
|
||||
never the header.
|
||||
|
||||
## Build and run
|
||||
|
||||
| Script | Purpose |
|
||||
@@ -48,6 +63,6 @@ Two constraints that are not obvious and have each cost a session:
|
||||
- **A window manager is required.** Without one, windows are never focused, and an
|
||||
unfocused GTK app ignores synthetic keys — which looks exactly like a code bug.
|
||||
|
||||
`docs/rig_build_traps.md` documents these and three more, with symptoms and exact
|
||||
`scripts/CAD/rig_build_traps.md` documents these and three more, with symptoms and exact
|
||||
recovery commands. Read it before debugging a configure or link failure one of
|
||||
these scripts reports.
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
# On success the binary is inside the persistent volume at
|
||||
# /OrcaSlicer/build/package/bin/orca-slicer (copy it out with a follow-up
|
||||
# `docker run --rm -v orcacad_buildcache:/b alpine cp ...` or via this script's tail).
|
||||
# Rig build traps already paid for once each (stale project, NLopt cache, pybind11, OCCT_LIBS, SLIC3R_CAD gate): docs/rig_build_traps.md
|
||||
# Rig build traps already paid for once each (stale project, NLopt cache, pybind11, OCCT_LIBS, SLIC3R_CAD gate): scripts/CAD/rig_build_traps.md
|
||||
set -euo pipefail
|
||||
|
||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# 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.
|
||||
# five; all of them, with symptoms and exact recovery commands, are in scripts/CAD/rig_build_traps.md.
|
||||
# Read that file before debugging a configure or link failure this script reports.
|
||||
#
|
||||
# Usage:
|
||||
@@ -92,7 +92,7 @@ docker run --rm \
|
||||
|
||||
# A target-only build writes src/Release/, but this fork's start-headless-gui.sh may default BIN to the
|
||||
# PACKAGED path that only build_linux.sh refreshes — launching with the default would then run a
|
||||
# stale binary. Pass BIN explicitly. See docs/rig_build_traps.md.
|
||||
# stale binary. Pass BIN explicitly. See scripts/CAD/rig_build_traps.md.
|
||||
echo "=== launch the rig on the binary just built ==="
|
||||
echo " docker exec -e BIN=/OrcaSlicer/build/src/Release/$BIN ${PREFIX}-gui /OrcaSlicer/scripts/CAD/start-headless-gui.sh"
|
||||
exit "$rc"
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Emit the C++ offer table from scripts/CAD/tool_atlas.json.
|
||||
|
||||
python3 scripts/CAD/gen_offer_table.py # rewrite the header
|
||||
python3 scripts/CAD/gen_offer_table.py --check # verify the checked-in header
|
||||
|
||||
The map exists ONCE: the atlas is the only place a verb's row, key, icon and refusal
|
||||
string are written down, so the menu the user sees cannot drift from the table the code
|
||||
is compiled against.
|
||||
|
||||
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__)) # scripts/CAD
|
||||
REPO = os.path.dirname(os.path.dirname(HERE))
|
||||
ATLAS = os.path.join(HERE, "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 validate(A):
|
||||
"""Refuse an atlas the header cannot represent, naming every fault at once.
|
||||
|
||||
An unknown slot or selection would surface as a bare ValueError from .index() below, and a
|
||||
duplicate verb id would not surface at all: DesignPanel::mcp_run_verb looks a verb up by id
|
||||
and takes the first match, so the second row would be unreachable from the offer.
|
||||
"""
|
||||
slot_ids = {s["id"] for s in A["slots"]}
|
||||
sel_ids = {s["id"] for s in A["selections"]}
|
||||
errs, seen = [], set()
|
||||
for v in A["verbs"]:
|
||||
if v["slot"] not in slot_ids:
|
||||
errs.append(f'{v["id"]}: unknown slot {v["slot"]!r}')
|
||||
for a in v["accepts"]:
|
||||
if a not in sel_ids:
|
||||
errs.append(f'{v["id"]}: accepts unknown selection {a!r}')
|
||||
if v["id"] in seen:
|
||||
errs.append(f'{v["id"]}: duplicate verb id')
|
||||
seen.add(v["id"])
|
||||
for s in sel_ids - set(ENUM):
|
||||
errs.append(f"selection {s!r} has no OfferSel enumerator")
|
||||
if errs:
|
||||
sys.exit("tool_atlas.json is inconsistent:\n " + "\n ".join(errs))
|
||||
|
||||
|
||||
def main():
|
||||
A = json.load(open(ATLAS, encoding="utf-8"))
|
||||
validate(A)
|
||||
sels = [s["id"] for s in A["selections"]]
|
||||
slots = [s["id"] for s in A["slots"]]
|
||||
|
||||
lines = [
|
||||
"// GENERATED FILE — DO NOT EDIT.",
|
||||
"// Source: scripts/CAD/tool_atlas.json Generator: scripts/CAD/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 (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())
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
{"outer": [[26.711, -55.263], [42.071, -7.071], [35.0, -0.0], [-32.575, 0.0], [-33.717, -0.112], [-34.815, -0.446], [-35.828, -0.987], [-36.715, -1.715], [-39.55, -4.55], [-40.35, -5.547], [-40.914, -6.694], [-41.216, -7.937], [-41.241, -9.215], [-40.988, -10.468], [-26.711, -55.263], [-28.828, -57.312], [-29.64, -58.335], [-30.159, -59.532], [-30.35, -60.823], [-30.201, -62.12], [-29.721, -63.334], [-28.944, -64.382], [-27.718, -65.649], [-27.075, -66.035], [-26.325, -66.047], [-25.67, -65.683], [-20.613, -60.789], [20.613, -60.789], [26.711, -66.69], [32.421, -60.789], [26.711, -55.263]], "holes": [{"pts": [[19.052, -18.464], [16.474, -9.14], [-0.0, -9.104], [-21.926, -9.104], [-21.926, -3.535], [22.308, -3.535], [19.052, -18.464]], "cx": 4.719, "cz": -10.192, "d": 44.234}, {"pts": [[-11.493, -48.01], [-11.676, -49.341], [-12.211, -50.574], [-13.06, -51.617], [-14.158, -52.392], [-15.424, -52.842], [-16.765, -52.934], [-18.081, -52.66], [-19.274, -52.042], [-20.257, -51.124], [-20.955, -49.976], [-21.318, -48.682], [-21.318, -47.337], [-20.955, -46.043], [-20.257, -44.895], [-19.274, -43.977], [-18.081, -43.359], [-16.765, -43.085], [-15.424, -43.177], [-14.158, -43.627], [-13.06, -44.402], [-12.211, -45.445], [-11.676, -46.678], [-11.493, -48.01]], "cx": -16.223, "cz": -48.01, "d": 9.825}, {"pts": [[21.364, -48.01], [21.181, -49.341], [20.645, -50.574], [19.797, -51.617], [18.699, -52.392], [17.432, -52.842], [16.091, -52.934], [14.775, -52.66], [13.582, -52.042], [12.6, -51.124], [11.901, -49.976], [11.539, -48.682], [11.539, -47.337], [11.901, -46.043], [12.6, -44.895], [13.582, -43.977], [14.775, -43.359], [16.091, -43.085], [17.432, -43.177], [18.699, -43.627], [19.797, -44.402], [20.645, -45.445], [21.181, -46.678], [21.364, -48.01]], "cx": 16.634, "cz": -48.01, "d": 9.825}]}
|
||||
@@ -0,0 +1,97 @@
|
||||
"""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
|
||||
hand-authoring keeps the glyph and the printed part from drifting apart: rerun this and the table
|
||||
follows the STEP.
|
||||
"""
|
||||
import json, math, os
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
D = json.load(open(os.path.join(HERE, "bear_outline.json")))
|
||||
|
||||
def unit_frame(pts_sets):
|
||||
allp=[p for s in pts_sets for p in s]
|
||||
xs=[p[0] for p in allp]; ys=[p[1] for p in allp]
|
||||
cx,cy=(min(xs)+max(xs))/2,(min(ys)+max(ys))/2
|
||||
span=max(max(xs)-min(xs), max(ys)-min(ys))
|
||||
return cx,cy,span
|
||||
|
||||
outer=[(x,-z) for x,z in D["outer"]]
|
||||
holes=[[(x,-z) for x,z in h["pts"]] for h in D["holes"]]
|
||||
CX,CY,SPAN = unit_frame([outer]+holes)
|
||||
U=lambda pts:[((x-CX)/SPAN,(y-CY)/SPAN) for x,y in pts]
|
||||
OUT=U(outer)
|
||||
EYES=[U(h) for h,m in zip(holes,D["holes"]) if m["d"]<20]
|
||||
MUZ =U([h for h,m in zip(holes,D["holes"]) if m["d"]>=20][0])
|
||||
|
||||
def rdp(p,eps):
|
||||
if len(p)<3: return p
|
||||
ax,ay=p[0]; bx,by=p[-1]; dx,dy=bx-ax,by-ay; n=math.hypot(dx,dy)
|
||||
best,bi=-1.0,0
|
||||
for i in range(1,len(p)-1):
|
||||
px,py=p[i]
|
||||
d=abs(dx*(ay-py)-(ax-px)*dy)/n if n>1e-12 else math.hypot(px-ax,py-ay)
|
||||
if d>best: best,bi=d,i
|
||||
if best<=eps: return [p[0],p[-1]]
|
||||
return rdp(p[:bi+1],eps)[:-1]+rdp(p[bi:],eps)
|
||||
def simp(p,eps):
|
||||
r=rdp(p+[p[0]],eps); return r[:-1]
|
||||
|
||||
OUT_S = simp(OUT,.030) # 22 verts, the size the study settled on
|
||||
# wind counter-clockwise so the renderer's normals come out facing +Z
|
||||
def area2(p): return sum(p[i][0]*p[(i+1)%len(p)][1]-p[(i+1)%len(p)][0]*p[i][1] for i in range(len(p)))
|
||||
if area2(OUT_S) < 0: OUT_S = OUT_S[::-1]
|
||||
|
||||
def centroid(p): return (sum(q[0] for q in p)/len(p), sum(q[1] for q in p)/len(p))
|
||||
E=[]
|
||||
for e in EYES:
|
||||
c=centroid(e); r=(max(p[0] for p in e)-min(p[0] for p in e))/2
|
||||
E.append((c[0],c[1],r))
|
||||
E.sort()
|
||||
|
||||
lo=min(p[1] for p in MUZ); hi=max(p[1] for p in MUZ)
|
||||
bottom=[p for p in MUZ if p[1] < lo+0.06*(hi-lo)]
|
||||
apex=max(MUZ,key=lambda p:p[1])
|
||||
TRI=[min(bottom),max(bottom),apex]
|
||||
if area2(TRI)<0: TRI=TRI[::-1]
|
||||
|
||||
# the cheek dot: the handedness mark adopted after the mirror-difference study
|
||||
DOT=(E[1][0]+0.085, E[1][1]-0.10, 0.038)
|
||||
|
||||
# THE MUZZLE. Six facets lifted straight off the mesh -- every facet touching anything above the
|
||||
# 3 mm plate. Do NOT recompute the base from height*tan(draft): the first version did and produced
|
||||
# a needle, because the real base OVERHANGS the crest at both ends (0.062 at the nose, 0.034 at the
|
||||
# tail) and it is that overhang that makes it a tapered wedge instead of a blade.
|
||||
PLATE = 0.036 # 3.00 / 83.34
|
||||
SNOUT_BASE = ((-0.0727, -0.2417), (+0.0630, -0.2417), # nose end, 0.136 wide
|
||||
(+0.0259, +0.1939), (-0.0356, +0.1939)) # tail end, 0.062 wide
|
||||
CREST = ((-0.0048, -0.1793, 0.2073), (-0.0048, +0.1605, 0.1279))
|
||||
|
||||
def fmt(v): return f"{v:+.4f}"
|
||||
L=[]
|
||||
L.append(f"// Emitted by doc/design/mate-connectors/emit_glyph_table.py from bear.step — do not hand-edit.")
|
||||
L.append(f"// Normalised to the part's bounding span and centred: the renderer scales by one radius.")
|
||||
L.append(f"static const Vec2d kBearOutline[] = {{ // {len(OUT_S)} verts, RDP eps 0.030, CCW")
|
||||
for i in range(0,len(OUT_S),3):
|
||||
row=", ".join(f"{{{fmt(x)}, {fmt(y)}}}" for x,y in OUT_S[i:i+3])
|
||||
L.append(" "+row+",")
|
||||
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 (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])}}},")
|
||||
L.append("};")
|
||||
L.append("// THE MUZZLE, lifted off the mesh: a tapered wedge, base quad + crest edge, 6 facets.")
|
||||
L.append("// This is the only feature standing along +Z and the only one still legible edge-on.")
|
||||
L.append(f"static const double kBearPlateZ = {PLATE:+.4f};")
|
||||
L.append("static const Vec2d kBearSnoutBase[] = { // CCW from the nose end")
|
||||
for x,y in SNOUT_BASE: L.append(f" {{{fmt(x)}, {fmt(y)}}},")
|
||||
L.append("};")
|
||||
L.append("static const Vec3d kBearCrest[] = { // nose (tall) -> tail (short)")
|
||||
for x,y,z in CREST: L.append(f" {{{fmt(x)}, {fmt(y)}, {fmt(z)}}},")
|
||||
L.append("};")
|
||||
open(os.path.join(HERE,"bear_glyph_table.h"),"w").write("\n".join(L)+"\n")
|
||||
print("\n".join(L))
|
||||
@@ -0,0 +1,65 @@
|
||||
# Pull the bear's true silhouette and feature positions out of the supplied male B-rep, so the
|
||||
# simplification study starts from measured geometry instead of a tracing of the flat drawing.
|
||||
#
|
||||
# The part's native frame (make_female.py): flat back on Y=0, relief rising to Y=+17.27, the FACE
|
||||
# carried by X and Z. So the face plane is XZ and the silhouette is the outline projected along Y.
|
||||
import os, json
|
||||
import Part
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
s = Part.Shape(); s.read(os.path.join(HERE, "bear.step"))
|
||||
sol = s.Solids[0]
|
||||
bb = sol.BoundBox
|
||||
print(f"bbox X {bb.XMin:.2f}..{bb.XMax:.2f} Y {bb.YMin:.2f}..{bb.YMax:.2f} Z {bb.ZMin:.2f}..{bb.ZMax:.2f}")
|
||||
|
||||
# The back plate face: the planar face whose normal is -Y and which sits at Y=YMin. Its outer wire
|
||||
# IS the silhouette; its inner wires are the eye holes.
|
||||
best = None
|
||||
for f in sol.Faces:
|
||||
if f.Surface.__class__.__name__ != "Plane":
|
||||
continue
|
||||
n = f.Surface.Axis
|
||||
if abs(abs(n.y) - 1.0) > 1e-6:
|
||||
continue
|
||||
c = f.CenterOfMass
|
||||
if best is None or c.y < best[0]:
|
||||
best = (c.y, f)
|
||||
y, face = best
|
||||
print(f"back plate at Y={y:.3f} wires={len(face.Wires)} area={face.Area:.1f} mm2")
|
||||
|
||||
def wire_pts(w, tol=0.05):
|
||||
# ORDER MATTERS and w.Edges does not carry it: OCC hands the edges back in whatever order the
|
||||
# face stored them, so concatenating their discretisations gives a scrambled ring. The first
|
||||
# version of this script did exactly that and emitted an outline with 7 duplicated points and
|
||||
# twice the perimeter it should have. OrderedEdges walks the wire, and each edge is reversed
|
||||
# when its own orientation runs against the walk.
|
||||
pts = []
|
||||
for e in w.OrderedEdges:
|
||||
d = e.discretize(Deflection=tol)
|
||||
if e.Orientation == "Reversed":
|
||||
d = list(reversed(d))
|
||||
for p in d:
|
||||
pts.append((round(p.x, 3), round(p.z, 3)))
|
||||
# drop consecutive duplicates
|
||||
out = [pts[0]]
|
||||
for p in pts[1:]:
|
||||
if abs(p[0]-out[-1][0]) > 1e-4 or abs(p[1]-out[-1][1]) > 1e-4:
|
||||
out.append(p)
|
||||
return out
|
||||
|
||||
data = {"outer": None, "holes": []}
|
||||
outer = face.OuterWire
|
||||
data["outer"] = wire_pts(outer)
|
||||
for w in face.Wires:
|
||||
if w.isSame(outer):
|
||||
continue
|
||||
pts = wire_pts(w)
|
||||
xs = [p[0] for p in pts]; zs = [p[1] for p in pts]
|
||||
data["holes"].append({"pts": pts,
|
||||
"cx": round(sum(xs)/len(xs), 3), "cz": round(sum(zs)/len(zs), 3),
|
||||
"d": round(max(xs)-min(xs), 3)})
|
||||
print(f" hole: centre ({data['holes'][-1]['cx']}, {data['holes'][-1]['cz']}) dia {data['holes'][-1]['d']}")
|
||||
|
||||
print(f"outer wire: {len(data['outer'])} points")
|
||||
json.dump(data, open(os.path.join(HERE, "bear_outline.json"), "w"))
|
||||
print("WROTE bear_outline.json")
|
||||
@@ -0,0 +1,151 @@
|
||||
# Rig build traps
|
||||
|
||||
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 |
|
||||
|---|---|---|---|---|---|
|
||||
| `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
|
||||
target-only `ninja` into the volume the GUI rig launches from, so a session can test a single
|
||||
change without a full repackage, whereas `build-gui-incremental.sh` runs the full packaged build.
|
||||
Both start a throwaway container from the deps image with the live repo mounted over the baked
|
||||
tree — never build inside the GUI container (Trap 1).
|
||||
|
||||
Every trap below has already cost about a session to re-derive, once each. They are recorded now
|
||||
so no fresh session pays them again. Symptoms, causes, and exact recovery commands follow.
|
||||
|
||||
---
|
||||
|
||||
## Trap 1 — never configure inside the GUI container
|
||||
|
||||
**Symptom.** After building inside the GUI container, the fork's targets no longer exist; ninja
|
||||
reports an unknown target, and `orca-slicer` / `OrcaSlicer` have been replaced by
|
||||
`snapmaker-orca` / `Snapmaker_Orca`.
|
||||
|
||||
**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
|
||||
`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
|
||||
image with the live repo mounted over the baked tree — `src`, `resources`, `cmake`, `deps_src`,
|
||||
`localization`, `CMakeLists.txt`, `version.inc` — and writes into the same volume the rig
|
||||
launches from.
|
||||
|
||||
---
|
||||
|
||||
## Trap 2 — stale `NLopt_DIR` in CMakeCache
|
||||
|
||||
**Symptom.** Configure fails with `Cannot find NLopt library 'nlopt_cxx' in '<prefix>/lib'`.
|
||||
|
||||
**Cause.** `cmake/modules/FindNLopt.cmake:26` is `set(NLopt_DIR $ENV{NLOPT})`. With `NLOPT`
|
||||
unset that expands to `set(NLopt_DIR)` — zero arguments — which *unsets the normal variable* and
|
||||
lets a leftover CACHE entry of the same name (e.g. `<prefix>/lib/cmake/nlopt`) show through the
|
||||
following `if(NOT NLopt_DIR)`. The `else()` branch then searches for `nlopt_cxx` under
|
||||
`${NLopt_DIR}/lib` with `NO_DEFAULT_PATH`, while the deps prefix ships plain `nlopt`.
|
||||
|
||||
**Fix.** From inside the build dir:
|
||||
|
||||
cmake -U NLopt_DIR -U NLopt_LIBS .
|
||||
|
||||
Do **not** `sed` the entry out of `CMakeCache.txt` — deleting a line breaks the cache parser.
|
||||
|
||||
---
|
||||
|
||||
## Trap 3 — the image lacks `deps_src/pybind11`
|
||||
|
||||
**Symptom.** Configure aborts with `pybind11 headers not found in /OrcaSlicer/deps_src/pybind11.
|
||||
Did you initialize submodules?` (the `FATAL_ERROR` guarding `PYBIND11_SOURCE_DIR` in the mainline
|
||||
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; Snapmaker has neither.
|
||||
|
||||
**Fix.** Mount `deps_src` over the baked tree — `scripts/CAD/build-gui.sh` does. Corollary: mounting a
|
||||
Snapmaker tree into an `orcacad-deps` build reproduces this error exactly.
|
||||
|
||||
---
|
||||
|
||||
## Trap 4 — `OCCT_LIBS` lags one configure
|
||||
|
||||
**Symptom.** A wall of undefined references to `TopOpeBRepBuild` symbols. It reads as a broken
|
||||
OCCT installation. It is not.
|
||||
|
||||
**Cause.** `src/libslic3r/CMakeLists.txt:603` does
|
||||
`set(OCCT_LIBS "${OCCT_LIBS}" CACHE INTERNAL "OCCT toolkits linked by libslic3r")` at the END of
|
||||
its own configure, while the consumer in the root `CMakeLists.txt` (`if (NOT OCCT_LIBS)` …
|
||||
`foreach (_tk IN LISTS OCCT_LIBS)`) reads whatever is already in the cache. The first reconfigure
|
||||
after the `TKFillet TKOffset` prepend (`src/libslic3r/CMakeLists.txt:599`) therefore links the
|
||||
previous list and drops `TKBool`/`TKOffset`.
|
||||
|
||||
**Fix.** Configure twice. `scripts/CAD/build-gui.sh` runs `cmake .` twice for exactly this reason; if
|
||||
you ever configure by hand, run it twice.
|
||||
|
||||
---
|
||||
|
||||
## Trap 5 — `SLIC3R_CAD=ON` in the cache, macro never defined
|
||||
|
||||
**Symptom.** The build succeeds and links, but the Design tab is simply absent — or it fails with
|
||||
`class GLCanvas3D has no member named set_design_sketch_tool`.
|
||||
|
||||
**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
|
||||
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.
|
||||
This is why `scripts/CAD/build-gui-incremental.sh`, `scripts/CAD/run-kernel-tests.sh` and `scripts/CAD/build-gui.sh`
|
||||
all mount both.
|
||||
|
||||
---
|
||||
|
||||
## The binary the rig actually launches
|
||||
|
||||
`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 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 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.
|
||||
|
||||
Note also that the GUI containers do **not** mount `scripts/`: `/OrcaSlicer/scripts` inside them
|
||||
is the baked copy, so a local edit to `start-headless-gui.sh` has no effect until you
|
||||
`docker cp scripts/CAD/start-headless-gui.sh <container>:/OrcaSlicer/scripts/`.
|
||||
|
||||
---
|
||||
|
||||
## Trap 6 — `src/Release/` resolves resources to `build/resources`, which may not exist
|
||||
|
||||
The binary derives `resources_dir()` from its own location, so the `src/Release/` one looks in
|
||||
`/OrcaSlicer/build/resources` while the packaged one looks inside `build/package/`. Only the
|
||||
packaging step creates the latter; nothing creates the former. Without it the app fails every
|
||||
`Failed to add custom font ".../build/resources/fonts/…"`, logs `Health check is not running`,
|
||||
and **exits 255 with nothing on stdout** — which reads exactly like a crash in whatever you just
|
||||
changed. Measured 2026-08-02: an hour was nearly spent bisecting a GUI change that was fine.
|
||||
|
||||
`build/` is the shared cache volume, so one symlink fixes it permanently, and pointing it at the
|
||||
bind-mounted repo tree means the rig also picks up new `resources/images/*.svg` without a rebuild:
|
||||
|
||||
docker exec <fork>-gui ln -sfn /OrcaSlicer/resources /OrcaSlicer/build/resources
|
||||
|
||||
Tell the two apart before debugging: a resource failure dies in the first second with no window;
|
||||
a real fault in your code gets past the version banner. Compare
|
||||
`~/.config/<App>/log/<newest>.log.0` against a known-good run — 47 lines versus 340 is the tell.
|
||||
|
||||
## Trap 7 — a single-instance app plus a path-matched `pkill`
|
||||
|
||||
`start-headless-gui.sh` used to kill by `"$BIN"`, while its own `app_pid()` matched by BASENAME. Launch
|
||||
with a `BIN` that differs from the running instance's path and the old process survives, keeps
|
||||
the single-instance lock, and the new one exits seconds after loading fonts — then `status`
|
||||
reports the *stale* pid as a healthy session. Fixed by killing on the basename; `status` now also
|
||||
prints `binary : $(readlink -f /proc/<pid>/exe)`. **Read that line before trusting a screenshot.**
|
||||
@@ -54,10 +54,9 @@ run_in_rig() { # copy the script in fresh, then run it ther
|
||||
# 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 (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
|
||||
# The atlas and its generator live beside this script, in scripts/CAD/: they are build inputs
|
||||
# for a checked-in header, not documentation, so they do not belong under docs/.
|
||||
step "offer table matches the atlas" python3 scripts/CAD/gen_offer_table.py --check
|
||||
|
||||
step "kernel suite" scripts/CAD/run-kernel-tests.sh --vol "${KVOL:-orcacad_kerneltest}"
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
# working tree to a per-volume staging dir on that host and re-runs this same script
|
||||
# there, so the verification contract is identical either way. Drop --host once the image
|
||||
# is present locally.
|
||||
# Rig build traps already paid for once each (stale project, NLopt cache, pybind11, OCCT_LIBS, SLIC3R_CAD gate): docs/rig_build_traps.md
|
||||
# Rig build traps already paid for once each (stale project, NLopt cache, pybind11, OCCT_LIBS, SLIC3R_CAD gate): scripts/CAD/rig_build_traps.md
|
||||
set -euo pipefail
|
||||
|
||||
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user