Commit Graph
75 Commits
Author SHA1 Message Date
Ian BassiandRodrigo Faselli 222c6a2df5 Improve performance by migrating to Clipper2 2.0.1 (#15969)
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-10-02 17:33:41 -03:00
Ian BassiandKris Austin 70bc02467b Faster Preview View (#15884)
Co-authored-by: Kris Austin <kris.austin@gmail.com>
2026-10-02 14:45:58 -03:00
Ian Bassi a1ad2b4425 Add section view feature for 3D canvas (#15879) 2026-10-02 11:50:43 -03:00
Ian Bassi ffbbd62355 Clean design Docs and move context (#15803) 2026-09-30 14:33:35 -03:00
a5413efc37 refactor: printer agent infrastructure changes to work with other printer agents aside from bambu (#15710)
* Add developer flag for printer agents

* Parse user print info on the UI thread to prevent heap corruption (#119)

get_user_print_info()'s HTTP fetch can run on a worker thread (e.g. BindJob),
but parse_user_print_info() mutates userMachineList (insert/erase/delete
MachineObject). on_machine_alive (SSDP) mutates the same maps on the UI thread
without locking, so parsing off-thread races the map and frees MachineObjects
out from under it -> heap corruption.

Keep all device-list mutation on the UI thread: parse inline when already on
the main thread, otherwise marshal via CallAfter so it stays serialized with
on_machine_alive.

* Prevent loss of user access code on LAN reselect

Keep user access code intact to maintain access rights even if
device slot is unpopulated, ensuring continuous connection
and status message reception.

* Harden send flow and separate upload failure recovery (#111)

* fix(send): harden FT send path + IP pre-flight UX

* Remove early returns

* Working Moonraker and Qidi printer agent transport (#104)

Folds the Qidi AMS box-mapping print
overrides (apply_box_mapping +
start_* wrappers) that the transport
fix builds on.

* Add support for runtime error status in plugins

Distinguish a loaded plugin whose
capability errored (RuntimeError,
warn-styled, stays checked) from a
load-time Error. Status now derives
via resolve_plugin_status(); enum
ordinal keeps dialog sort priority.
Unloading clears stale errors.

* Resolve duplicate agent ID conflicts

Reject a printer-agent capability
whose agent ID is already owned by
another capability or built-in:
flag the plugin error, disable the
capability, and warn the user
instead of silently ignoring it.

* fix: checkbox should depend on plugin is_loaded status

* Replace fake-enum printer agent dropdown (#121)

A dedicated PrinterAgentChoice field
reads rows straight from the live
agent registry and stores the agent
id string, replacing the fake-coEnum
index mapping. The field moves to
TabPrinter and registers with the
searcher so UnsavedChanges renders
it; the PhysicalPrinterDialog copy
and its update hook are removed
(#125). switch_printer_agent now
resolves ids via
resolve_printer_agent_id.

* Reset device selection on agent swap or unload (#124)

set_live_printer_agent centralizes
the swap: deselect the machine,
clear stale sidebar state and the
previous agent's Other Devices, then
install the new agent (or null when
its provider vanished). Plugin
load/unload callbacks refresh the
dropdown and re-run agent selection.
load_last_machine no longer falls
back to the first available machine.

* Gate agent mode behind use_printer_agents toggle

Replace per-printer auto-activation
(is_current_printer_agent_plugin)
with a global experimental AppConfig
toggle, default off: legacy
print-host behavior is unchanged
until the user opts in. The toggle
drives device-tab routing, print
button defaults, connect-button
visibility and sidebar layout, and
dedups machine-select dialog opens.

* Track BBLPrinterAgentPlugin.py

* Add printer-agent and plugin status tests

Ports the agent lifecycle, duplicate
agent-id, built-in-id clash and
status-resolution tests. The loader
runs on a detached worker thread, so
the lifecycle tests live in their own
executable. Tests install the
production unload-side registry
wiring themselves (no GUI in the
test binary) and register agents
manually so concurrent loads stay
deterministic.

* fix: pin HTTP to prevent connection refusal

Set `use_ssl` to false to ensure Moonraker
connectivity, as the service uses HTTP rather
than HTTPS, preventing connection issues. Initialize
device info early for reliable name resolution.

* Bring Moonraker device panel to feature parity

The monitor panel showed wrong or missing
data for Moonraker printers, and its
controls did nothing.

Push payload now carries layer number and
total layers. Remaining time replaces the
wrong total_duration - print_duration
formula. The chamber light toggle maps to
Klipper SET_PIN / SET_LED, and pause,
resume and stop post to
/printer/print/{action}. Task thumbnails
resolve via /server/files/thumbnails onto
a new MachineObject thumbnail url.

Filament sync switches to pull mode so the
agent is queried on demand.

Not compiled or run.

* Stop blocking print on unreported nozzle data

* fix: make Klipper macro lamp control reliable

* Surface Moonraker webcams and gate unrunnable controls

* Keep Bambu AMS dialect out of the agent waist

M620 is Bambu firmware dialect, not a
neutral command. Composing it in
MachineObject let non-Bambu agents
(Moonraker/Klipper) forward it and
report success on firmware that
cannot run it.

Agents now own the dialect: the
default refusal on IPrinterAgent
returns not-supported so the UI
can say so; BBLPrinterAgent keeps
the byte-identical composition.

* Fix multi-color filament logic

Reuse color decoding across functions to improve
code readability and maintain consistency in
multi-color filament handling.

* Move Moonraker commands off the UI thread

Pause/resume/stop, g-code sends, temps, and
light ran synchronous HTTP on the UI thread,
freezing the app up to 10s per click on slow
or unreachable printers.

Run them on a single agent-owned FIFO worker
so g-code ordering is preserved, while command
translation stays synchronous so unsupported-
command dialogs still work.

Add a pending-disabled state to the pause,
resume, and abort buttons for Moonraker-family
printers: the icon only flips once the
WebSocket reports the real state, which also
rules out double-click races.

* Show Snapmaker U1 camera in Device tab

The U1 exposes no /server/webcams/list entry;
its camera only captures after an explicit
camera.start_monitor RPC, which the Moonraker
websocket executes unauthenticated but only
answers over MQTT - so the call is fire and
forget.

Start the camera when the camera view is
shown and renew every 300 s: the printer
retires the capture task at ~362 s and
stop_monitor is accepted but ineffective,
so teardown is simply to stop renewing.

Frames land in monitor.jpg as still JPEGs
(~2 fps at interval 0), so the webview loads
a local HTML wrapper that repolls with a
cache buster.

* fix: start stream when camera URL changes

* fix: stop Qidi slot parse throwing on null

* Keep printer-agent progress in sync

Keep the shared task progress aligned with agent
reports that lack Bambu cloud task identity.

Release the lazily allocated task during reset to
avoid leaks when machine objects reconnect.

* docs: document the printer-agent subsystem

* fix: merge access codes into one

* Reconcile implementation split with PR tip

* feat: abstract remaining gcode commands in devicemanager

* refactor: abstract bambu specific protocol to printer agent

* refactor: push bbl workflows to bbl printer agent

* remove unused

* fix: default impl

* fix callback error

* fix: remove redundant cache

* specify api for getting file transfer url

* revert file transfer abstraction

* fix: ams filament mapping workflow

* feat: update qidi to use subscription based filament sync mode

* fix: resolve stubgen byte header conflict

* fix: ams sync info and periodic ams sync via subscription workflow

* fix: skip filament sync dialog if filamentSyncMode is none

* feat: parse nozzle information for qidi and moonraker printer agents

* fix: extend access code requirements t 0, 8 or more characters.

* remove irrelevant docs

* fix: remove heavy includes from IPrinterAgent

* fix: defer filesystem and camera abstractions

* fix: remote do_fetch_filament_info from tests

* cleanup moonraker and snapmaker printer agents

* fix: access codes regression

* fix: tests

* fix: printer agent switching on preset change

* fix: remove unused variable

* fix: snapmaker U1 SelectMachineDialog blocking print

* fix: merge artifact

* fix: clear up some unrelated changes

* feat: connect to cloud printer and monitor

* feat: connect to cloud printer and monitor

* feat: generic camera stream support for http snapshot and rtsp

* fix: build & access code UI

* feat: generic camera stream support for http snapshot and rtsp

* fix: build & access code UI

* feat: connect to cloud printer and monitor

* feat: connect to cloud printer and monitor

* fix: build errors

* feat: camera via webrtc

* fix: build

* fix: cmake

* feat: remove frame assembler and change config to set protocol

* fix: orcaprinteragent refactor

* fix: LAN paths and camera stream

* feat: use ffmpeg to render http camera stream

* fix: make model_id/dev_type optional instead of blocking

* fix: connect via ip dialog

* feat: LAN impl for Orca Printer Agent

* fix: model_id resolution method for non bambu printers

* fix: ffmpeg http camera stream jittering due to incomplete frames

* fix: revert sdcard check

* feat: check printer storage status before sending

* fix: moonraker printer agent hang on printer power cut

* fix: shim layer for any compatibiliity changes

* fix: cloud printers were using the wrong MQTT endpoint

* feat: cloud download via HTTP

* temp: doc for intended change

* fix: warnings

* fix: warnings

* fix: camera auto-play on startup

* fix: split infra from impl

* fix: uninitialized ams state blocking print

* Fixes nullptr deref

* Log first before std::move

* fix: printer agent virutal optional functions

* fix: parameterize orcaslicer_copy_test_dlls() for printer_agent_plugin_tests

* Revert "fix: parameterize orcaslicer_copy_test_dlls() for printer_agent_plugin_tests"

This reverts commit 2f566e3779.

* Guard libdatachannel. Remove unused code

* fix: unit tests & unused variables

* fix(ci): deps build order for datachannel

* Resolve printer agent first before getting cloud printer agent

* fix(ci): set depends openssl

* fix: re-include apply header guarded by ifdef __APPLE__

* fix(ci): add libdatachannel to flatpak manifest

* fix: add internal_developer_mode chekc back to MediaPlayCtrl::load()

* fix: invoke js clearInterval on WebMediaController::stop

* fix: change rtc log level

* fix: inject provider, agent id and generation to get_user_print_info to ensure correct metadata

* fix: revert moonraker specific behavior

* fix: remove stale comment

* fix: use ORCA_CLOUD_PROVIDER instead of hardcoded string

* fix: remove hardcoded ICE servers

* feat: enable https camera stream mode

* fix: move non-mandatory printer agent function stubs to IPrinterAgent

* fix: dedupe compatible printer type check

* fix: stop the correct media controller

* fix: scope get_my_machine_list to printers listed under the current printer agent

* fix: move printer agent plugin tests into test_plugin_lifecycle.cpp

* fix: always build bundled DataChannel dep

* fix: disable unused DataChannel media support

* revert: filament sync work

* fix: wrap command_* with small wrapper

* fix: regression bug, connecting to bambu needs bblp username

* fix: default impl for vendor agnostic gcode commansd

* refactor: media controller playback routing and ownership

* fix: bump libdatachannel ver & update flatpak to use tar instead

* fix: stop flatpak DataChannel build from re-cloning over the sandboxed network

* fix: update windows ffmpeg prebuild

* fix: update printer agent plugin API

* fix: shift camera signaling channel to network agent

* fix: follow external-packages for flatpak libdatachannel deps & add flatpak path to use source_dir

* feat: add printer-agent.md doc to HLSD

* fix: guard DeviceManager command dispatch when no printer agent is bound

* fix: port BBL implementations from #15711

* refactor: connect_printer api and dialog

* fix(tests): make omitted printer agent operations answer like a missing agent

* fix: preserve printer agent defaults in PrinterAgentPluginCapabilityTrampoline

* test: cover printer agent default command dispatch

* fix: validate windows FFmpeg avformat library

* feat: extend optional printer model warnings to calibration & ams workflows

* fix: handle malformed printer progress values safely

* fix: clear webview document on stop

* chore: reduce diagnostic logging level to trace

* refactor: centralize printer compatibility checks

* tests: add device manager integration coverage

* tests: cover WebMediaController lifecycle with wxWebView stub

* fix: make integration tests headless

* refactor: collapse command_ams_refresh_rfid and command_ams_refresh_rfid2

* refactor: make printer connection SSL agent-specific

* fix: persist input printer host and port

* fix: use correct device id for Moonraker connections

* fix: make moonraker gcode commands asynchronous

* fix: preserve moonraker device names

* fix: add include for non BBL_RELEASE_TO_PUBLIC path in BBLPrinterAgent

---------

Co-authored-by: Andrew <159703254+andrewsoonqn@users.noreply.github.com>
Co-authored-by: SoftFever <softfeverever@gmail.com>
Co-authored-by: Lam Wei Lun <weilun.lam@gmail.com>
2026-10-01 00:30:44 +08:00
Kris AustinandRodrigo Faselli f5679ad343 perf: load presets in parallel, cutting preset load time by over 60% (#15943)
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-09-29 18:13:44 -03:00
Rodrigo FaselliandIan Bassi 11e9e07f20 Non-crossing infill optimization (#15931)
* Non-crossing infill optimization

* test triangles

* test grid

* cleaning

* Align and clip rectilinear infill paths

Generate infill coverage in the pattern's local frame, rotate triangular patterns by layer, and clip centerlines to the surface vicinity. Start closed outlines outside the surface so clipping splits them cleanly.

* Update test_fill.cpp

* Update multiline-infill.md

---------

Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
2026-09-28 15:13:57 -03:00
1846407e93 Add Precise Seam placement feature (#12974)
Co-authored-by: Ioannis Giannakas <59056762+igiannakas@users.noreply.github.com>
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
2026-09-28 14:40:55 -03:00
Ian Bassi ea280ba6f6 Wipe tower sparse layers combination (#15841) 2026-09-26 16:27:15 -03:00
Kris Austin 237cd10eb5 fix: Home start shows Prepare or a blank window, and Prepare opens slowly (#15878) 2026-09-26 14:23:52 -03:00
Ian BassiandRodrigo Faselli 8c03985818 Add Cubic Non-crossing multiline strategy (#15887)
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-09-26 11:40:49 -03:00
SoftFever 2b602943a5 Load profile includes so synced Bambu Lab presets get their G-code back 2026-09-24 13:22:37 +08:00
Kris Austin 66b300987b feat: faster startup by lazy-loading main window panels on idle or first use (#15811) 2026-09-23 19:11:10 -03:00
Kris Austin 824b216f18 feat(gui): assignable keyboard shortcuts (#15706) 2026-09-21 17:36:04 -03:00
SoftFever f1f68ffc3f Merge branch 'main' into cad-mainline 2026-09-18 14:01:23 +08:00
SoftFever a77209af8f Stop requiring a filament id snapshot update when filaments change 2026-09-18 11:20:57 +08:00
SoftFever ade9e77b6b Run every profile maintenance job from one tool (#15726)
* Run every profile maintenance job from one tool

orca_id_tool.py becomes orca_profile_tool.py, and orca_extra_profile_check.py
and orca_filament_lib.py fold into it as subcommands: check, generate-id, fix,
trim, update-index and update-snapshot. The three scripts already overlapped --
the checker imported half of its rules from the id tool, which in turn kept a
copy-pasted set of output helpers to avoid the resulting import cycle -- while
disagreeing on how a vendor is enumerated, how a JSON file is read and what the
exit code means. One file settles all three.

check, normalize, trim and update-index reproduce their predecessors exactly; normalize and
update-index were diffed byte-for-byte against the old scripts over a copy of
the whole tree. Deliberate changes: the compatible-printers check no longer
switches itself off when --check-materials is passed, an error exits 1 rather
than -1, update-index honours --profile-type and reports a profile it cannot
place instead of dropping it from the index, fix and update-index gained
--dry-run, trim keeps an unindexed file some surviving profile still inherits
from, and vendors are enumerated as directories with an index -- which is why
blacklist.json, a data file that an unscoped index rebuild once wrote four empty
list sections into, loses them here and will not collect them again. The dead
rename_filament_system() helper is gone.

The suite under scripts/tests now covers the maintenance commands too, and CI
runs it; nothing there ran in CI before. No shipped profile data changes apart
from those four keys.

* update vendor index files with "python3 ./scripts/orca_profile_tool.py update-index" and "python3 ./scripts/orca_profile_tool.py normalize"
2026-09-16 19:53:23 +08:00
Valerii Bokhan 3e1daccd7c Feature: Add inward wipe for external perimeters (#15407) 2026-09-15 20:01:39 -03:00
Tommaso Bianchi edb6aa1722 Sync cad-mainline with upstream main and carry the value-field + rename work on top 2026-09-10 11:01:24 +02:00
Tommaso Bianchi 31a15cc5e5 Rename snaporca/SnapOrca to orca_cad so the OrcaSlicer PR carries no Snapmaker naming 2026-09-10 10:58:46 +02:00
SoftFever 994d2de5d6 Merge branch 'main' into pr/tommasobbianchi/15238 2026-09-08 21:54:22 +08:00
SoftFever 01a03d6d32 delete plan docs 2026-09-08 14:45:34 +08:00
SoftFever 12d43433dc Merge branch 'main' into pr/tommasobbianchi/15238 2026-09-08 14:28:31 +08:00
SoftFever 9e1b000e7f Hold every filament product to exactly one id, with no exceptions
filament_id is the plain mint of the product triple (filament_vendor,
filament_type, filament name), and nothing else feeds it. The tooling used to
accept any salt iteration of a preset's own triple, and its minting policy
stepped past ids that other products held in the tree or in the snapshot, so
which id a product got could depend on history. Every "salt split" in the tree
masked a redundant preset rather than a real need, and no shipped id is
salted, so salting goes entirely: no salt parameter, no id policy object, and
--generate no longer reads the snapshot.

--check now holds every declared and every inherited id to that one value and
lists each preset that misses it, variants under a wrong root included, instead
of folding them into the root's error. Two products whose triples mint one id
is reported as a collision naming both, and --generate refuses to write it;
the remedy is a rename so the triples differ. The Bambu catalog map generator
keys its rows by the same mint rather than by what the tree already ships.

No id changes: all 913 ids in the tree are already the mint of their triple,
so --generate is a no-op and the snapshot is untouched.
2026-09-07 19:20:32 +08:00
SoftFever 3500a1e588 unify id generation scripts 2026-09-07 10:46:59 +08:00
SoftFever bf20b041e2 Make a filament's id depend only on the filament itself
A filament_id is now exactly what the preset's own filament_vendor,
filament_type and filament name mint, wherever it inherits from. Inheriting
settings no longer limits what a preset may claim, so the checks that policed
inheritance are gone, and so are the four grandfather lists that held thousands
of presets as permanent exceptions. The snapshot records sanctioned state rather
than excuses: one entry per id, carrying the product it names beside the presets
claiming it.

Profiles that disagreed are corrected instead of excused. The Elegoo TPU and
PAHT roots were named for a different product than all of their variants and
become TPU 95A and PAHT-CF; Elegoo PET-CF gains the filament_type its variants
already set; the Snapmaker breakaway support presets get an id of their own
rather than riding the PVA chain; and a BBL preset name carrying a doubled space
is fixed behind renamed_from. Their ids re-mint from the corrected identities.
The tooling and the design note also drop the word "family", which invited
reading a brand's Lite and Pro spools as one id.

No change to slicing output — only filament_id values, three preset names, the
inherits lines following those renames and the vendor indexes move.
2026-09-06 20:53:11 +08:00
SoftFever 4aa0e1d60b Translate filament ids at the printer boundary
Orca content-addresses every system filament, Bambu's included, but a printer,
its AMS and its vendor's cloud know only that vendor's own catalog ids. The
printer agent now translates between the two: outbound MQTT and FTP traffic, the
AMS mapping sent with a print job, and the ids written into a 3mf bound for the
printer all leave in the printer's own ids, while status messages, loaded
projects and SD-card prints arrive in Orca's. An id with no mapping passes
through unchanged, and an agent whose printers already speak Orca's ids
translates nothing at all.

Bambu's map is generated from BambuStudio's own shipped bundle; a missing or
unreadable file leaves every lookup an identity rather than taking the app down.
The profile check validates the map's shape, and profile CI now runs on the paths
that can change it. docs/HLSD/filament_id.md records the places the map
deliberately does not reach.
2026-09-06 20:53:11 +08:00
Tommaso BianchiandClaude Opus 5 3f52166e32 A sketch should not look like plate preparation
Three cues, because one is missed. A teal banner across the top of the viewport names the session
("Editing: Sketch N") and where its exits are; the printer bed is muted for the duration, since a
plate grid and a sketch grid are the same visual language and reading one as the other is how a
sketch gets drawn against the wrong reference; and N looks straight down the plane normal at the
current zoom, with the plane's own y axis as up, because no hand-orbit lands exactly square and a
sketch read at an angle is one whose right angles do not look like right angles.

The banner is an INDICATOR. Finish and Cancel stay on the single ribbon action bar — the tab had
three competing confirm surfaces once and that is not being reopened for a strip of colour. It
sits above the canvas rather than floating inside it: a child window over a wxGLCanvas is a native
window on GTK with no reliable stacking over GL, and being unmissable beats being clever.

The bed checkbox stays the stored preference and is restored on leaving the sketch; ticking it
mid-sketch still shows the bed, because that is a deliberate act and this is only a default.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
2026-09-05 11:45:29 +02:00
Tommaso BianchiandClaude Opus 5 324b558747 Esc is the safe key again: one press, one level, nothing destroyed
Two presses used to discard a live sketch. The key was answered in four places that could not
see each other — the inline value field, a sketch branch, a feature-card branch, and the canvas
— so a press aimed at one fell through to the next, and request_exit() carried a fourth layer
that deliberately let the SECOND consecutive press through to cancel_sketch(). The warning it
showed first did not help: the two presses are never one decision, the first is aimed at a field
or a tool and the second at whatever was underneath it.

The stack is now explicit. CadLevel (DesignInteraction.hpp) is four levels deep, the enum value
IS the LIFO depth, and cad_escape_level() is a constexpr function over a POD of four booleans —
so the ordering that is the entire contract is checked by static_assert at compile time, with no
window, GL context or event loop. DesignPanel::escape() acts on the one level escape_level()
names and on no other, and every Esc in the tab routes through it.

The destructive layer is gone from request_exit() itself rather than guarded at its callers, so
the guarantee cannot be re-opened by adding a route: a session holding geometry is left only
through Finish (keep) or Cancel (discard). Cancel now asks before discarding — it used to refuse
and tell the user to press the button they had just pressed, which meant a drawn sketch could be
kept but never thrown away.

Right-click also stops rewarding navigation with a menu: the offer needs BOTH budgets, released
within 200 ms and moved no more than 3 px, and the raycast uses the press position, so the menu
describes what was pointed at rather than where the camera stopped. Two budgets because drift
alone still popped a menu at the end of a slow, careful orbit.

docs/ux/interaction-model.md carries the state machine, the routing and the transition table.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011FbJKJAJxxkhDTs9XdZzKA
2026-09-05 11:35:12 +02:00
SoftFever 7c9b38ba04 remove retired_filament_ids.json 2026-09-03 21:03:42 +08:00
SoftFever 261cc19c59 update doc
update doc
2026-09-01 11:43:00 +08:00
Tommaso Bianchi d0791c3b8a Make the ladder runnable on this fork's rig — 6 of 7 rungs now hold
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.
2026-08-31 08:40:06 +02:00
SoftFever bbd1989e1e move design doc to CAD subfolder 2026-08-29 18:52:12 +08:00
SoftFever 8f014de84c Load the CAD recipe from projects saved before it was renamed
The recipe's 3MF entry moved from Metadata/SnapOrca_cad.bin to
Metadata/orca_cad.bin, so projects saved by earlier builds opened with an
empty Design tab. Both 3MF backends now read either name and write only the
new one; the recipe version advances to 6 to mark the move.
2026-08-29 01:56:48 +08:00
Tommaso Bianchi 13d5eac891 Move the Design-tab scripts into scripts/CAD/ and name them by role
Requested by SoftFever on PR #15238: ten of these had accumulated loose in
scripts/ next to ~20 unrelated upstream ones, with names that only meant
something to whoever wrote them. They now sit in scripts/CAD/, mirroring the
src/libslic3r/CAD/ and src/slic3r/GUI/CAD/ split, and the verb in the name is
the role: build- produces a binary, start- brings something up, run- runs a
suite, check- asserts one thing against a live app.

  kernel-test.sh        -> CAD/run-kernel-tests.sh
  ladder-all.sh         -> CAD/run-all-checks.sh
  sketch-ladder.py      -> CAD/check-sketch-engine.py
  ladder-corpus.py      -> CAD/check-sketch-engine-corpus.py
  gui-ladder.py         -> CAD/check-gui-sketching.py
  offer-ladder.py       -> CAD/check-gui-context-menu.py
  mcp-sketch-smoke.py   -> CAD/check-mcp-sketch.py
  rig-build.sh          -> CAD/build-gui.sh
  docker-iter-build.sh  -> CAD/build-gui-incremental.sh
  gui-session.sh        -> CAD/start-headless-gui.sh

"Ladder" was the worst of them: it named the shape of the test (rungs of
increasing difficulty) rather than what the test proves, so nothing in the
directory listing told you which one needed a GPU and which was pure kernel.

Every reference rewritten -- the docs, the cross-calls between the scripts,
Dockerfile.deps, and the container-side /OrcaSlicer/scripts paths. The three
shell scripts resolve REPO relative to themselves and now sit one level
deeper, so that walk went from /.. to /../.. . The copies these push into a
container's /tmp were renamed to match, or the container would have kept the
old names alive.

Two runtime paths deliberately NOT renamed. /tmp/orca-rig-build.lock is a
cross-fork contract -- both forks take the same lock so two concurrent builds
serialise instead of OOMing the box, and renaming it on one side silently
removes that guard. /tmp/gui-session.log is a runtime artefact, not a script.

Added scripts/CAD/README.md: what each script proves, what it needs, and the
two constraints that have each cost a session (never build inside the GUI
container; a window manager is required or synthetic keys are ignored).

On CI, which was the other half of the request: the kernel suite is already
there and always has been. The cases are registered in
tests/libslic3r/CMakeLists.txt under if (SLIC3R_CAD), which defaults ON and no
workflow turns off, so they build into libslic3r_tests and run under ctest on
every platform via unit_tests.yml -- like any other unit test, needing no new
job. They have simply never been seen to run, because the workflows on this PR
are still awaiting maintainer approval. run-kernel-tests.sh is the local loop
over the same cases, and it is the only script here CI could run: the other
six need an OpenGL canvas and synthetic input.

Verified: scripts/CAD/run-kernel-tests.sh from its new location, all tests
passed, 2562 assertions in 190 test cases.
2026-08-28 19:34:03 +02:00
SoftFever 0aeb6df122 Merge branch 'main' into cad-mainline 2026-08-27 17:06:37 +08:00
Noisyfox dbcb196f4e Merge branch 'main' into dev/ffmpeg-player 2026-08-26 21:47:20 +08:00
Tommaso Bianchi 0ac9ac91f7 A body can be renamed, and the one-word reason it could not
User report 2026-08-23: "clicking on a body row in feature tree, I cannot find
rename on right click", then "still i cannot rename body1 in custom name".

TWO SEPARATE CAUSES, one in data and one in a single method call.

THE MISSING ROW WAS DATA. The `rename` verb's accepts list in the tool atlas was
["sk_loop"] alone, so the offer built for a selected BODY carried no Rename row.
Right-clicking a body row already opens the offer — that is the designed gesture,
bound on m_parts as wxEVT_TREE_ITEM_MENU — so the menu the user was looking at
was the right menu, and it was simply missing the verb. accepts is now
["sk_loop", "body_solid"], the generated table regenerated with it (the accept
mask moves 0x00004000 -> 0x00004080), and the offer trace confirms the row:
"[OFFER] row=7 Modify > rename".

THE RENAME ITSELF WAS BLOCKED BY UnselectAll(). Both trees are wxTR_SINGLE, and
wxTreeCtrl::UnselectAll() is the MULTI-selection call: on a single-selection tree
it leaves the row selected. So every path that tried to open the label editor
while a body row was selected hit the BEGIN_LABEL_EDIT guard — which vetoes while
tree_body_selection() >= 0, the rule that stops a body taking a name it cannot
keep across a recompute — and the editor never opened. Unselect() is the call
that works, and it fixes every route at once.

That took five attempts, four of them wrong, and the reason they were wrong is
worth more than the fix: each one addressed a plausible cause that the evidence
did not actually support — the popup's nested event loop, keyboard focus,
deferring with CallAfter, dispatching through a different verb. What settled it
was a DISCRIMINATOR rather than another fix: pressing F2 on a selected body row
takes the same handler with no menu and no nested loop. F2 failed identically,
which ruled out every menu-shaped theory in one measurement and left only the
state the veto reads.

A body still has no name of its own — it is recomputed from the recipe on every
change and CadBody::name is derived from the feature that builds it — so the verb
resolves the body to CadBody::source_feature and renames THAT, saying so on the
status line: "A body takes its name from the feature that makes it — renaming
'Extrude'". The Bodies row now reads "Body 1 — Extrude" so the rename is visible
where it was made; the positional "Body N" leads, because every status message,
the interference report and the mate errors identify bodies that way. Confirmed
as the wanted format by the user.

Also here, from the same report: the Bodies card keeps its own action row (Move,
Show / hide, Delete, Colour), and the competing context menu an earlier pass had
added to body rows is REMOVED — right-clicking a body belongs to the offer, and
two menus on one gesture is how the offer ended up being blamed for a veto.

Verified on the rig, both routes, with a body selected:
  F2 on the body row        -> ['Sketch', 'Extrude'] became ['Sketch', 'Base block']
  the offer's rename verb   -> {'applies': True, 'dispatched': True,
                                'selection_kind': 7, 'ok': True} and the same rename

Gate green: ALL LADDERS HELD — offer table matches the atlas, kernel 188 cases /
2532 assertions, engine rungs 1-8, 977-sheet corpus + the heaviest sheets,
gesture ladder 98/98, offer ladder 108/108.

RIG DISCIPLINE, repeated twice in one session and now written down: ladder-all.sh
does not relaunch the app, so hand-driving the rig immediately before a gate
leaves state its reset_document() cannot clear — both times the first rung drew
nothing and reported "sides []", which reads exactly like a broken rectangle
tool. Relaunch before gating.
2026-08-23 18:15:11 +02:00
Tommaso BianchiandClaude Opus 5 96f9261425 The offer table is generated again, and its last verb was unreachable
snaporca-ziam said gen_offer_table.py would silently delete the model-mode "Constrain sketch"
row, because that row lived in the generated header and not in tool_atlas.json. Running it found
more than that: FOUR rows existed only in the header — constrain, rename, and the three typed-
value rows sk_length / sk_radius / sk_angdist — and sk_delete's action had drifted, pointing the
sketch row at btn:delete, the FEATURE delete.

All five are now in the atlas, so the header regenerates byte-identically from it. Verbs may carry
a `note`, emitted as a C++ comment above the row: a rationale written into a generated file is
deleted by the next regeneration, which is how this started.

snaporca-z8rs (P1), found by making that true: after the atlas held all 92 verbs, the regenerated
header differed from the checked-in one by EXACTLY ONE LINE — kOfferVerbCount, 91 against 92.
Every consumer loops i < kOfferVerbCount, so the last row of the table was invisible: never listed
by show_offer_menu, never findable by mcp_run_verb. The verb that fell off the end is sk_angdist,
"Angle / distance…" — the typed-value row for a two-entity selection. On the one selection where
you would ask for the angle between two lines, the row that types it was not in the menu.

It survived because nothing compared the Sk2Ent menu against the table: sk_angdist accepts Sk2Ent
and nothing else, so an off-by-one that dropped the LAST verb was invisible from every other
selection. The vocabulary rung now covers Sk2Ent too, and picking the pair taught it one more
rig fact — shift-clicking a circle at its +X point grabs the RADIUS GRIP, which replaces the
selection with that one entity, so the pair silently collapsed to one and the offer answered
SkLine. Correctly, for the selection that actually existed.

gen_offer_table.py --check proves header == atlas and changes nothing; it is now the first step of
scripts/ladder-all.sh, and the only one that needs no rig.

Offer ladder 107/107, gesture ladder 93/93, both on the rig.

snaporca-ziam snaporca-z8rs

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrMzTpAf78U4NG2M8jfvHY
2026-08-23 06:14:48 +02:00
Tommaso Bianchi 1274d97983 Sketch: closing a polyline is now a previewed snap, not an invisible bubble
Ported from snaporca 982968b1af.
2026-08-22 23:03:47 +02:00
ExPikaPakaandSoftFever 5ed56eb876 Cache system presets to eliminate startup and wizard load times (#14217)
* Add caching system for presets

* Removing user\bundle serialization and keeping it only for system presets

* Integrate caching into WebGuideDialog which speeds up time of SetupWizzard and PrinterSelection dialog

* Add CI\CD step to prepare cache file in ahead of time so user does not need to wait

* Add partial cache generation when only one of the vendros is changed to speed up recalculation time

* Handle corrupted files

* Add cache to GuideDialog as previos version didn't work as expected

* Add inspecting tool and fix CI cache generation

* Generate cache per vendor

* Simplify code by mergin it in PresetBundle

* Simplify code a bit more

* Add cereal serialize() to VendorProfile, PrinterModel, Preset, and Semver

* Remove CachedPrinterModel/VendorProfile/Preset mirror structs from VendorCache

* Fix use-after-free in CallAfter lambda; replace raw thread pointer with unique_ptr

* Use get_vendor_cache_key() to match cache keys written by the app

* Remove BOM added by VSC

* Skip invalid vendors

* Remove leftover cache file

* Fix build for windows arm64

* Revert json cache back

* Update check for stale cache

* Serealize all value fields for Preset class to minimize regression later

* Minimize field duplication by moving Cache thing into PresetBundle

* Add tests for Cache system

* Add a bit more tests

* Merge branch 'main' into feature/cache_profiles_and_optimize_loading_speed

* Rvert from per-verndor to single cache file

Replace N per-vendor .cache files with a single system_presets.cache
that holds all vendors and presets in one serialized blob.

Cache load is now all-or-nothing: on hit all vendors are applied from
the bundle (sub-second); on miss all vendors are parsed from JSON and
a fresh bundle is written to the user cache dir.

Invalidation is driven by bundle_key - a sorted concatenation of all
vendor JSON version strings. Any vendor update invalidates the whole
cache and triggers re-parse on next launch.

Guide wizard (WebGuideDialog) loads the bundled cache into a plain
PresetBundle instead of a separate VendorGuideData struct, removing
the duplicate data model.

generate_system_cache simplified from a per-vendor loop to a single
save_system_presets_cache() call producing one output file.

* Transfer all Preset fields from cache via move assignmet

apply_vendor_preset_group was copying fields manually and missed
bundle_id, user_id, base_id, sync_info, updated_time, key_values,
ini_str. Replace field-by-field copy with move assignment of the
fully-deserialized Preset, then restore the vendor pointer which
is excluded from serialization.

* Ignore cache for future

* Remove not used files

* Ship one preset cache per vendor in place of the profile JSONs

Each vendor's system presets serialize into a single <vendor>.opc built at
package time, and a shipped build carries that file alone — the profile JSON
and its sub-file tree are pruned. The vendor loader, the setup wizard's profile
list and the resource installer all read a vendor through its cache, falling
back to parsing whenever one is absent, stale or unreadable, so the cache stays
an optimization and never a source of truth. Caches hold presets in source form
and resolve inheritance at load, through the same code the JSON path uses.

* Make the preset cache self-describing and load each vendor from the system folder alone

The cached DynamicPrintConfig is keyed by name, through a per-file dictionary of the
distinct opt_keys, the type each was written as, and the distinct enum value names,
instead of by serialization_key_ordinal — a position assigned by declaration order at
static init, where inserting one option shifts every later ordinal and the lookup then
succeeds on the wrong option. Because a name-keyed payload drops the options this build
cannot place rather than being rejected wholesale, the schema fingerprint goes, and with
it the two fallbacks that existed only because an installed cache died on every app
upgrade: the second lookup tier into resources/profiles and the parse fallback to the
same place. A vendor is loaded from <data_dir>/system/ and nowhere else, as on main —
which is what makes the app write its .opc files there again.

* Simplify the preset cache internals after review

* Use the shared temp-dir helper in the preset bundle loading test

* Bound stamp string reads in the preset cache

* Speed up the setup wizard with a profile-data cache

The wizard's per-vendor fast path threw on vendors present only in
resources, falling back to a ~29 s raw JSON scan on every open. Each
vendor now loads from the directory it was found in, and the derived
model/machine/filament/process catalog is cached whole in
<data_dir>/cache/wizard_profile_data.json, stamped by each vendor's
name and version - a fresh cache makes an open one file read, with no
bundle built and no presets installed (~0.2 s vs ~2 s).

* Remove debug SVG dump from a geometry test

* Move the per-vendor cache file format into PresetCacheFormat

* Move the vendor install helpers from PresetBundle into Utils

* rename

* fix flatpak

* change cache version to 1

---------

Co-authored-by: SoftFever <softfeverever@gmail.com>
2026-08-21 16:56:52 +08:00
Tommaso Bianchi 2b02a8e3dd Design tab: move the CAD sources into their own folder
Review request on PR #15238: "Place CAD-related files (e.g. CadDocument/
GeometryEngine) into a separate folder."

  src/libslic3r/CAD/     the kernel — CadDocument, GeometryEngine, the four
                         Sketch* units, SketchSolver, ThreadStandards
  src/slic3r/GUI/CAD/    the tab — DesignPanel, DesignCanvas, DesignSketchTool,
                         SketchInlineEditor, McpControl, generated DesignOffer

Pure relocation: no line of logic changes. Two include rewrites follow from it —
files that moved re-spell their own neighbours against src/ (already on the
include path), and files that did not move pick up the new folder. docs and
docs/ux/mockups/gen_offer_table.py follow the same paths.

Verified: libslic3r, libslic3r_gui and libslic3r_tests all build, CAD suite green
at 2518 assertions in 194 test cases, and the sibling fork builds identically —
17 shared sources still byte-identical, 8 diverging by their expected counts.
2026-08-20 17:44:34 +02:00
Tommaso BianchiandClaude Opus 5 b4d6abc57a Design: draw the mate connector as a bear face, with the disc kept behind a preference
Tommaso's decision (snaporca-x0kd): face orientation is hardwired perception -- a toddler reads
a face's roll and verse with no instruction -- so the connector is a face by default and the
conventional disc + roll quadrant stays, selectable, for users who expect it.

  Preferences > Control > Camera > "Draw mate connectors as a face", default ON, key
  design_connector_face_glyph. Read every frame rather than latched, so toggling takes effect on
  the next repaint -- a look you cannot A/B without restarting will not get compared. Verified on
  the rig: unchecking it switches the viewport to the disc live, no restart.

WHY A RELIEF AND NOT A DRAWING. A flat face in the connector's plane foreshortens by
sin(elevation) and collapses at a grazing view exactly like the quadrant it replaces -- measured,
the quadrant falls 89 -> 20 -> 3 -> 0 lit pixels from 47 degrees to edge-on. The relief does not:
its silhouette carries the information. So the glyph is a small shaded solid, painter-sorted,
lambert-shaded against a light fixed in CAMERA space so orbiting does not swing the shading.

THE MUZZLE, AND THE MISTAKE THAT NEARLY LOST IT. It is the only feature standing along +Z, so it
says which way the connector points and it is all that survives edge-on. Two errors on the way:

  1. I built its footprint from height*tan(draft) and got a needle. The real base OVERHANGS the
     crest at both ends (0.062 nose, 0.034 tail) and that overhang is what makes it a wedge. Base
     now lifted straight off the mesh.

  2. Worse, I chased fidelity. Scaled honestly the ridge is 11.3 mm on an 83.3 mm face -- 13.6 %
     of the width -- and at 22-48 px that is a scratch. Tommaso looked at it and could not find
     the muzzle at all, which is the only test that counts. A glyph is a symbol, not a scale
     model, so it now gets two deliberate exaggerations, and COLOUR does most of the work:
     muzzle share of lit pixels at 90/16/6 deg -- body tone 14.8/11.3/17.5 %, accent gold
     18.3/19.2/23.9 %, accent gold at 1.8x width 23.5/25.2/31.2 %.

  The accent is the same gold the disc spends on its roll quadrant, so it stays this tab's "here
  is the direction that matters" colour. Polarity is still on the Z arrow's head; nothing collides.

A connector whose ROLL COULD NOT BE DERIVED keeps the disc treatment whatever the preference says.
A face asserts a definite orientation, and asserting one for a roll that was never derived is the
same confident lie that got billboarding rejected.

Geometry is emitted from the part by docs/design/mate-connectors/emit_glyph_table.py, not
hand-drawn, so glyph and printed connector cannot drift: 12-vertex outline, two eyes, chin bar,
cheek dot, and the snout wedge. Crest 29.0 mm / 6.58 mm drop / 13.1 deg against the review's
28.3 / 6.61 / 13.1 on the B-rep.

Also fixes extract_outline.py, which walked w.Edges: OCC returns them in storage order, not ring
order, ignoring per-edge orientation, so the outline was scrambled -- 45 points and perimeter
6.380 where a clean ring gives 31 and 3.335. Every measurement in the design notes was re-run.
The correction reversed one earlier finding: handedness does NOT read on its own (5.4/8.0/9.1 %
different from its mirror, not the 32-35 % the scrambled ring produced), so the cheek dot is
required rather than merely nice.

RIG-VERIFIED on Xvfb :12 against a 60x40x10 box with a face+edge connector: the face renders with
both eyes, ears, chin bar, cheek dot and a gold muzzle standing proud; the Z arrow degenerates to
its ring when viewed down the axis; and the preference switches to the disc live.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 18:26:50 +02:00
Tommaso BianchiandClaude Opus 5 555af98474 Mate connectors: bring the design record and the BearConnector pair into the repo
The connector work has lived outside the code since 2026-08-05, in a workspace repo with no
remote. It is the basis of a decision that now shapes the Design tab, so it belongs here.

  docs/design/mate-connectors/

  DESIGN_MATE_CONNECTORS.md   seven CAD systems surveyed; the frame-pair model this kernel
                              already matches; sections 8b/8c on the glyph, and section 9's
                              four open decisions (D1-D4) still awaiting Tommaso.
  bear.step                   the male, Onshape 2026-08-05T08:27Z, md5 faf228326ee3f971
  BearConnector_Female*.step/.stl, BearConnector_Cutter.step
                              built by make_female.py FROM the real male B-rep rather than
                              re-modelled, so the pocket is complementary by construction
                              including every deliberate asymmetry. Fit measured at exactly
                              0.2000 mm, zero interference, mated hosts proven coplanar.
  BEAR_CONNECTOR_REVIEW.md    the symmetry-group result: identity 81/81 edges, mirror-x 0/81,
                              mirror-y 0/81, rot180Z 0/81, rot90Z 0/81, diagonal 0/81 at
                              0.1 mm. Trivial group, so every PARTIAL view fixes orientation.
  extract_outline.py, simplify_study.py, relief_sheet.py, handedness.py, make_female.py,
  trim_female.py, fit_check.py, verify_trimmed.py, coplanar_test.py + their sheets

THE DECISION THIS SUPPORTS (snaporca-x0kd): the mate connector is drawn as a simplified BEAR
FACE by default, with the standard disc + roll quadrant + Z arrow kept behind a preference.
Face orientation is hardwired perception -- a toddler reads a face's roll and verse with no
instruction -- and no abstract glyph earns that. Measured against the alternative: the disc's
gold quadrant+tick falls 89 -> 66 -> 37 -> 20 -> 3 -> 0 lit pixels as the camera drops from
47 deg to edge-on, and is a shapeless blob by 16 deg.

WHAT THE SIMPLIFICATION STUDY SETTLED (snaporca-wi3z), all measured off the real B-rep:

  The eyes are load-bearing. Same outline and muzzle with the eyes removed stops reading as
  a face at every size. Whatever else goes, they stay.

  45 -> 22 outline vertices with no loss of read at 22 / 32 / 48 px; the muzzle reduces to
  one filled triangle. Three marks plus a cheek dot.

  Drawn FLAT the face fails exactly where the disc fails: in the connector's plane everything
  foreshortens by sin(elevation). Rendered as its real relief instead, lit pixels at 32 px go
  164 -> 210 at 16 deg and 69 -> 120 at 6 deg, and the snout ridge stands proud as a profile
  rather than smearing. The glyph must be a shaded relief, not an outline.

  Handedness already reads without any added mark -- 32 to 35 % of lit pixels differ from the
  mirror, and re-registering by best whole-pixel translation returns offset (0,0), so it is
  real shape asymmetry. But it reads only BY COMPARISON. A dot on one cheek makes it local:
  34.5 / 37.0 / 36.4 %, and unlike uneven eyes (42 %) it does not read as a defect.

Tommaso's calls: it stays a bear, and handedness must read.

The scripts were repointed at the co-located male and extract_outline.py re-run from here to
prove it -- same 45 outline points, same three inner wires, same 3829.5 mm2 back plate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-16 17:05:09 +02:00
NoisyfoxandClaude ec31750330 Add implementation plan for macOS FFmpeg player
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-14 20:11:09 +08:00
NoisyfoxandClaude 111364d880 Add design doc for macOS FFmpeg player
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-14 20:08:55 +08:00
SoftFever 030e5f469e Merge branch 'main' into cad-mainline 2026-08-13 15:22:08 +08:00
Tommaso BianchiandClaude Opus 5 606026a920 Home: axonometric view, fitted
DesignCanvas::set_view() and fit_view() were both written and then never called
from anywhere in the tree. The Design viewport has had no way back to a standard
view since it existed: no key, no button, nothing but orbiting by hand until the
model happens to drift into frame.

That is worse than a missing convenience. A camera left pointing along the bed
plane renders a scene that looks exactly like a failed renderer — geometry
present, nothing visible — and an hour went into blaming the software GL stack
before the real cause turned out to be two uncalled functions.

Home rather than a letter: every letter A-Z is already a Shift+letter tool
shortcut. Home is also the reset-the-view key most users arrive with. The
dispatcher needed no change, it keys on the raw wx keycode. set_view() already
does select_view + zoom_to_volumes, so this is fit and orient in one call.

Doc row added to the View toggles table in docs/design_tab.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-12 18:59:22 +02:00
Tommaso Bianchi 6c59898ac0 Design: pointing at part of a body is pointing at the body
Tommaso: "i deleted a body using rubber band selection, but this is not intuitive as all
the ux revolves around clicking". Correct on both counts, and a correction to what I said
last round: the rubber band IS implemented and shipping (pick_bodies_in_rectangle, m_rubber,
the drag branch in on_mouse). What is unbound is whole-body picking via CLICK; I read the
comment about the click path and wrongly generalised it to the gesture as a whole.

The handlers were never the problem either. Move, Mirror, Cut, Mass and Colour all resolve
their target through selected_body_default() / m_sel_solid_body, and that is already set when
you click a FACE — level >= 1 records the body. They would have worked from a click all
along. The only thing keeping them out was the atlas gate: accepts listed body_solid and no
face kind, so offer_selection_kind() returning FacePlanar filtered the rows away. This is
therefore an atlas-only change, no handler edits.

Cut, Split, Mirror, Transform, Mass and Colour now accept face/edge/vertex as well, matching
what Delete Body already did. Edges and vertices are included deliberately, not just faces: a
click resolves to a vertex, an edge or a face depending on where inside the pixel it lands,
so accepting only faces would make Move vanish whenever you clicked near a corner — a flicker
that reads as a bug and gets reported as "sometimes it works".

NOT widened: Extrude on a face means push/pull THAT face, and Thicken consumes the face you
point at. Both have genuine face-specific meaning, so widening them would change what they
do rather than where they can be reached from.

The rubber band keeps its job — it is still the only way to take a body without also naming
one of its faces. It just stops being the only door.

Verified on both rigs from a plain face click: Transform > Move opens with Body = Extrude2
(resolved from the face pick), Modify > Edit / Delete Face / Colour / Delete Body, and
Reference > Mass.
2026-08-02 09:57:53 +02:00
Tommaso Bianchi b2654ebd8a Design: a body knows what made it, so "Delete Body" can exist
Reported by Tommaso: select a body, and there is no Delete in the offer. Two independent
faults stacked behind that.

FIRST, clicking a body never selects the body. Whole-body picking is deliberately unbound
(DesignSketchTool.cpp) pending the rubber band, so a viewport click only ever yields
Face/Edge/Vertex. The offer therefore saw face_planar, and "delete" accepted body_solid but
no face kind, so the row was filtered out entirely — while the status line read "Body 1
face 0 selected", which actively teaches the wrong model.

SECOND, even selecting the body from the Bodies list, Delete refused in red: "Select the
FEATURE that created this body". CadBody had no link back to its maker, so the offer was
advertising a verb it could not perform — worse than the action:null rows fixed earlier this
session, because this one is ENABLED and its refusal reads like user error.

CadBody::source_feature fixes the second. It is stamped in ONE place, the recompute loop,
and the rule is just "still unset?". That is sufficient because of an invariant worth
stating: no feature ever replaces a whole CadBody. Every in-place op writes only .shape
(boolean, cut, mirror-fuse, transform, dress-up — all 8 sites checked), so a body keeps the
stamp it was born with; a consumed body is erased outright, taking its stamp with it; and
the only bodies still at -1 are the ones the current feature just pushed. A feature type
added later needs no change here as long as it keeps to that invariant.

"Delete Body" fixes the first, sitting beside "Delete Face" in Modify and reachable by
pointing at any face/edge/vertex. The two names cannot be confused, and "delete" gave up the
body kinds so both can never appear for one selection. Deleting a body removes the feature
that made it, which is a real edit to the recipe, so it asks first and NAMES the feature — a
body vanishing from the viewport is not evidence of which feature went, and this is the one
action here that cannot be eyeballed.

Multi-body delete is NOT offered. bodies_2 was in the first draft of the verb; the handler
deletes exactly one body, so a two-body selection would have silently deleted whichever was
m_sel_solid_body. Caught before it reached a binary, at the cost of one rebuild.

Verified on BOTH rigs, full round trip: click a face -> Modify > Delete Body -> "Delete
Extrude2?" -> body gone, Sketch1 correctly left behind, panel falls back to the idle hint ->
Undo -> Extrude2 and Body 1 restored.
2026-08-02 09:41:58 +02:00