Skip to content

feat: Snuggle — 3D voxel-aware genetic bed nesting#1

Closed
thereprocase wants to merge 92 commits into
mainfrom
feature/snuggle-uiux
Closed

feat: Snuggle — 3D voxel-aware genetic bed nesting#1
thereprocase wants to merge 92 commits into
mainfrom
feature/snuggle-uiux

Conversation

@thereprocase

Copy link
Copy Markdown
Owner

Summary

Snuggle is a new arrange algorithm that uses 3D voxelized mesh geometry (not just 2D convex hulls) to pack parts tightly on the print bed using GPU-accelerated evolutionary optimization.

  • 3D-aware collision detection — conservative outward voxelization ensures parts never overlap, even with complex concave geometry
  • Genetic algorithm — feasibility-first selection (Deb 2000), 4 seeding strategies, adaptive mutation, stagnation detection, multi-restart support
  • GPU acceleration — OpenGL 4.3 compute shaders for pairwise collision evaluation; automatic CPU fallback
  • Full UI integration — Enable Snuggle checkbox in arrange panel, gap/rotation/resolution/effort controls, settings persistence
  • Overflow fallback — if parts don't all fit, Snuggle places what it can, then hands overflow to the default arranger as fixed obstacles
  • 35 parts on a 256mm bed in <1 second with GPU, 1mm gap, 100% feasible

New files (3,110 lines of Snuggle code)

  • snuggle_nester.hpp — GA core (1,001 lines)
  • polite_voxelizer.hpp — conservative outward voxelizer (572 lines)
  • gpu_collision.cpp/hpp — GPU compute + CPU fallback (730 lines)
  • SnuggleArrange.cpp/hpp — Orca bridge/orchestrator (405 lines)

Modified files

  • Arrange.hpp — ArrangeParams additions (13 lines)
  • GLCanvas3D.cpp — ImGui arrange options panel (+300 lines)
  • ArrangeJob.cpp — dispatch + overflow fallback (+103 lines)

Code review

Full War Council review completed (Sauron/Gandalf/Frodo at Opus, Aragorn/Legolas at Sonnet, 10 waves of Uruk-Hai adversarial bug hunting). All blocking issues resolved. Coordinate chain triple-verified.

Also includes

  • wxWidgets 3.1.5 to 3.3.2 build system update (required for cmake 4.x compatibility)

Test plan

  • Build on Windows with MSVC (cmake + ninja)
  • Verify Enable Snuggle checkbox appears in arrange panel
  • Arrange 5-10 small parts — should complete in <2s, no overlaps
  • Arrange 30+ parts — should complete in <5s on GPU, overflow if needed
  • Toggle rotation locked/15/45/90 deg — verify constraint
  • Verify sequential print falls back to default arranger
  • Test without OpenGL 4.3 — should fall back to CPU
  • Verify settings persist across sessions

- deps/wxWidgets/wxWidgets.cmake: Add GIT_TAG v3.3.2 to track the
  correct branch; remove -DwxUSE_UNICODE=ON (unicode-only in 3.3,
  option removed)
- src/CMakeLists.txt: Bump find_package minimum version from 3.0/3.1
  to 3.3; remove SLIC3R_WX_STABLE conditional (3.0 no longer supported)
- CMakeLists.txt: Remove SLIC3R_WX_STABLE option definition
- scripts/flatpak/com.orcaslicer.OrcaSlicer.yml: Update wxWidgets
  source URL to v3.3.2 branch archive; remove sha256 (placeholder
  TODO); remove -DwxUSE_UNICODE=ON
Since we now target wxWidgets 3.3, the custom DPI change event
workaround (DpiChangedEvent, EVT_DPI_CHANGED_SLICER,
register_win32_dpi_event) is dead code. wxWidgets 3.1.3+ provides
native wxEVT_DPI_CHANGED / wxDPIChangedEvent which is already
wired up in the "true" branch of the version guards.

Removes:
- DpiChangedEvent struct and EVT_DPI_CHANGED_SLICER declaration/definition
- register_win32_dpi_event() function and its call site
- All associated #if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) guards
Remove scale_win_font() and scale_controls_fonts() functions along
with the #if !wxVERSION_EQUAL_OR_GREATER_THAN(3,1,3) guard in
rescale(). Since wx >= 3.1.3 is now guaranteed, this code could
never execute and the functions had no other callers.
wxWidgets 3.1+ accepts const argv arrays (const wchar_t* const* and
const char* const*) in wxExecute(), making the const_casts unnecessary.
Remove all 14 const_cast<char**>/const_cast<wchar_t**> wrappers around
wxExecute calls and their associated FIXME comments across GUI.cpp,
NotificationManager.cpp, and Downloader.cpp.
wxWidgets 3.3 requires macOS 10.11+, making the 10.9.5-specific crash
workaround in OpenGLManager impossible to trigger. Remove:
- OSInfo struct and s_os_info static member from the header
- OS version recording in init_glcontext()
- Conditional wxGLContext deletion in the destructor (now always deletes)
- Unused #include <wx/platinfo.h>

The MacDarkMode.hpp include is retained as mac_max_scaling_factor() is
still used by GLInfo::get_max_tex_size().
Since we now target wxWidgets 3.3, all wxCHECK_VERSION(3,1,N) checks
are always true. Remove the guards keeping only the true branches:

- I18N.hpp: Remove version guard around _wxGetTranslation_ctx macro
- ExtraRenderers.hpp, GUI_App.hpp: Simplify SUPPORTS_MARKUP to check
  only wxUSE_MARKUP (version check always true)
- ConfigWizard.cpp: Remove manual wxArrayInt comparison fallback
- SendSystemInfoDialog.cpp: Simplify display scaling guard to _WIN32 only
- GUI_Utils.cpp: Remove IsDark() fallback using luma approximation
- wxinit.h: Remove legacy wxEVT_BUTTON and wxEVT_HTML_LINK_CLICKED
  compat macros (these event names exist natively in wx 3.3)
wxWidgets 3.2+ asserts on invalid sizer flag combinations where
wxEXPAND (which fills the entire space in the secondary direction)
is combined with wxALIGN_* flags (which are meaningless when expanding).
Remove the conflicting wxALIGN_* flags from all 112 occurrences across
21 files, keeping wxEXPAND and any non-conflicting flags intact.
wxTRANSPARENT_WINDOW is removed in wxWidgets 3.3. Remove all 3
occurrences in MainFrame.cpp:

- ResizeEdgePanel constructor: already uses wxBG_STYLE_TRANSPARENT
  via SetBackgroundStyle(), so the flag was redundant
- slice_panel and print_panel: drop the style parameter entirely
  (defaults to 0)
In wxWidgets 3.3, wxWindow::Raise() no longer implies Show(). Add
explicit Show() before Raise() in two event handlers that activate the
main frame from another instance (load model, start download), and swap
the Show/Raise order in bring_instance_forward() so Show() precedes
Raise().
…s.h includes

- Wrap GetToolTipCtrl() call in GUI_App.cpp with #if wxVERSION_NUMBER < 3300
  guard, as this API may not be accessible in wxWidgets 3.3. The dark tooltip
  theming is cosmetic and non-critical.

- Add explicit #include <wx/utils.h> to 7 source files that use functions from
  that header (wxGetMousePosition, wxLaunchDefaultBrowser, wxGetDisplaySize,
  wxBell) but relied on transitive includes. This preempts breakage from
  wxWidgets 3.3 reducing transitive includes.

Files with wx/utils.h added: BBLTopbar.cpp, CreatePresetsDialog.cpp,
CameraPopup.cpp, GLCanvas3D.cpp, GCodeViewer.cpp, GUI_ObjectList.cpp,
FilamentMapPanel.cpp.

Skipped BindDialog.cpp and FilamentPickerDialog.cpp as they already include
wx/wx.h which provides wx/utils.h transitively.

Part of wxWidgets 3.1.5 -> 3.3.2 upgrade.
In wxWidgets 3.3, wxBitmapComboBoxBase::OnAddBitmap changed its
parameter from const wxBitmap& to const wxBitmapBundle&, and m_bitmaps
was replaced by m_bitmapbundles. Update OnAddBitmap signature and
OnDrawItem to use wxBitmapBundle, extracting wxBitmap via
GetBitmap(GetDefaultSize()) where needed.
In wx 3.3 with wxUSE_STD_CONTAINERS=ON, wxString is backed by
std::wstring, so direct concatenation of const char[] with
std::wstring or wxUniCharRef fails. Fix by splitting compound
concatenations into separate += operations on wxString, or by
wrapping the left operand in wxString() to use its operator+.

Files fixed:
- AuxiliaryDataViewModel.cpp: split "\\" + wxString/wstring chains
- AboutDialog.cpp: split std::string("\n") + wxUniCharRef
- Auxiliary.cpp: wrap dir.wstring() in wxString(), split "/" + wstring
wxWidgets 3.3 handles OpenGL discovery natively via imported targets
(OpenGL::GL, OpenGL::OpenGL). The override was corrupting wx-config
output with malformed "-framework OpenGL" entries, causing
FindwxWidgets.cmake to fail.
wxComboPopup no longer inherits from wxObject in wx 3.3, so
wxDynamicCast (which casts through wxObject) fails. Use dynamic_cast
directly instead.
Same pattern as earlier fixes: const char[] + std::wstring fails in
wx 3.3 where wxUSE_STD_CONTAINERS=ON. Wrap with wxString().
…ing ctor

- PhysicalPrinterDialog: disambiguate set_values() call with explicit
  std::vector<std::string> (wxArrayString now also matches initializer list)
- Preferences: use ToStdString() instead of mb_str() for std::string comparison
- Plater: use wxString::FromUTF8() for wxArrayString constructor argument
- Plater: use Add() instead of wxArrayString(size_t, wxString) ctor
- Search: change sep from std::wstring to wxString for concatenation
- SendMultiMachinePage: replace wxList::Node* with compatibility_iterator
  (Node type removed in wx 3.3 with wxUSE_STD_CONTAINERS=ON)
wxWidgets 3.3 bundles its own NanoSVG in bmpsvg.cpp, conflicting with
OrcaSlicer's bundled copy which includes the nsvgRasterizeXY extension.
Set wxUSE_NANOSVG=OFF in deps cmake to use OrcaSlicer's version only.
wxWidgets 3.3 cmake install doesn't include private headers.
OrcaSlicer uses some private headers for accessibility support.
Add a post-install step to copy the private headers directory.
On Linux/GTK, CheckBox, RadioBox, and SwitchButton set their size to
exactly the bitmap size (18x18 or 16x16), but GTK's internal CSS padding
requires additional space, resulting in negative content width warnings.
Use GetBestSize() on GTK to account for theme padding.
thereprocase and others added 19 commits March 31, 2026 05:58
Merged evolved algorithm from standalone bench testing into the
GPU branch's nester, preserving all branch-specific features
(rotation cache, GPU evaluator interface, post-GA compaction jiggle).

New from evolved GA:
- 4 seed strategies: center, line, grid, bottom-left
- Order-aware crossover: fitter parent bias (70/30 inheritance)
- Push-apart mutation (13%): moves parts away from nearest neighbor
- Adaptive mutation scale: 1x baseline, up to 3x when stagnant
- Stagnation detection: early exit after 45 gens without improvement
- Aspect ratio fitness term: penalizes elongated bounding boxes
- Proximity fitness term: rewards close-but-not-colliding pairs

Preserved from branch:
- Rotation cache (360-bin quantized)
- GPU CollisionEvaluator batch interface
- Post-GA compaction jiggle toward centroid
- compute_origin_positions for delta writeback
- bed_margin_mm, w_clustering config fields

Bench results: 30 parts in 4.9s, 38.9% bed usage, zero collisions.
Independently validated with exact Moller-Trumbore triangle intersection.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…fications

UI improvements (per UX spec):
- Renamed to "Snuggle 3D Arrangement" with teal header
- Rich tooltips on all Snuggle controls explaining what each does
- "Standard rotation disabled" note when Snuggle is active
- Sequential printing warning in footer when detected
- Footer shows "voxel collision detection" instead of GPU claim

ArrangeJob improvements:
- Overflow fallback: unarranged items go to default arranger with
  Snuggle-placed items as fixed obstacles (multi-plate support)
- Snuggle-specific completion messages with part count:
  "Snuggle complete — 5 parts arranged"
  "Snuggle placed 3 of 5 parts. Some couldn't fit."

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Sauron: coordinates clean, format strings correct, settings persist OK.

Frodo fixes:
- Sequential print warning: orange text instead of grey (was invisible)
- Rotation note: "Snuggle controls rotation" instead of "disabled"
  (instructional, not cautionary)
- Visual hierarchy: warnings use orange, info uses grey

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Newly implemented (was MISSING):
- Single-part centering: 1 part = center on bed instantly, no GA
- Fallback for 50+ parts: bail to default arranger with warning
- Sequential printing hard block: bail to default (toolhead safety)
- Oversized parts flagged: pre-flight check marks off-plate immediately
- Voxel fail fallback: failed parts marked off-plate, overflow handles them
- Bed utilization % in completion toast: "Bed usage: 47%"
- Detailed overflow message: "placed X of Y, some overflow to next plate"

Upgraded from PARTIAL to DONE:
- Overflow: items marked off-plate trigger ArrangeJob fallback
- Seq print: UI warning + actual hard block in SnuggleArrange

Already confirmed DONE by Sauron (17 of 30):
- Zero collisions, bed containment, cancel, non-blocking UI,
  distinct button, rotation locked, spacing default, remember choice,
  empty skipped, other plates safe, bed full overflow, arrange selected,
  detailed overflow msg, locked obstacles

Deferred (complex GUI work, not blocking):
- Animated part transition (requires render pipeline changes)
- Wipe tower lock icon (requires 3D scene overlay)
- Split dropdown button (requires toolbar refactor)
- Cached redo (requires undo system extension)

Undo/redo: confirmed DONE via Orca's existing take_snapshot("Arrange")
in Plater.cpp — atomic undo is free.

Final scorecard: 26/30 DONE, 0 PARTIAL, 4 deferred (GUI polish)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…i-plate params

New user-facing controls:
- Rotation dropdown: Locked / 90 / 45 / 15 / 5 / 1 degree snaps
  relative to each part's starting orientation
- Quality slider + text input: population = quality x 64, tooltip
  explains the mapping
- Collapsible "Debug / Advanced" section:
  - Timeout (5-300s, default 40s)
  - Max parts per plate (2-500, default 200)
  - Multi-plate overflow toggle

Backend changes:
- ArrangeParams: snuggle_max_parts, snuggle_timeout_s,
  snuggle_rotation_step, snuggle_multi_plate
- NesterConfig: rotation_step_rad for quantized rotation
- snap_rotation(): quantizes angle to nearest step relative to
  each part's initial_zrot (their starting position)
- Max parts limit now configurable (was hardcoded 50)
- Timeout now configurable (was derived from quality only)

All settings persist in app_config, load on startup, reset on Reset.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Bug 1 (WRONG): Reset now persists max_parts, timeout, rotation_step,
  multi_plate to app config. Was only resetting in-memory values.

Bug 2 (COSMETIC): Quality clamped to [1,10] on config load. Prevents
  population_size=0 from hand-edited config files.

Bug 3 (WRONG): Invalid rotation_step from config now snaps to nearest
  valid option (0,1,5,15,45,90). Prevents UI showing "Locked" while
  nester actually uses a non-standard step value.

Bug 4 (WRONG): snuggle_lock_rotation is now derived FROM rotation_step
  on load (step=0 -> locked=true). Legacy "snuggle_lock_rotation"
  config key no longer loaded independently. Dropdown is source of truth.

Bug 5 (WRONG): Single-part centering preserves user's Z rotation
  instead of zeroing it. Was destructively resetting orientation.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
B6 (UB/CRASH): randf(lo, hi) with lo > hi when part > half bed.
  Fix: clamp hi >= lo + 0.1 before calling randf.

B2 (OOM): Rotation cache built 360 copies per part even when locked.
  200 parts = 72K grids = potential OOM.
  Fix: When lock_rotation=true, build only 1 cache entry per part.

B5 (WRONG): Silent fallback (too many parts, seq print) left items
  with stale bed_idx. Overflow detection skipped standard arranger.
  Fix: Set all items bed_idx=-1 before returning from fallback.

B4 (WRONG): compact_toward_center micro-rotations violated rotation
  step constraint. User set 90deg, got 90.05deg after compaction.
  Fix: Re-snap all rotations after compaction.

B1 (WRONG): Single-part centering placed instance origin at bed
  center, ignoring polygon offset. Asymmetric meshes appeared off-center.
  Fix: Account for polygon centroid when computing center position.

B8 (COSMETIC): Progress callback scale was items-based, not 0-100%.
  Fix: Use gen * 100 / max_gen for percentage.

B9 (perf): Oversized parts still voxelized despite being pre-flagged.
  Already marked bed_idx=-1, nester handles gracefully. Left as-is
  (performance waste, not correctness bug).

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Bug P2-1 (WRONG): generations_run always reported max_generations
  regardless of early exit, timeout, or cancel. Now tracks actual gen.

Bug P2-2 (PERF): Compaction binary search created redundant
  rotated_copy calls inside inner loop (216K+ calls for 30 parts).
  Fix: Pre-compute rotated copies for collision partners once per
  part per sweep, reuse for all 12 binary search iterations.

Bug P2-3 (WRONG/OOM): lock_rotation cache allocated 360 bins but
  filled only 1, leaving 359 empty grids. GPU evaluator could read
  empty bin and report false-feasible (zero collisions).
  Fix: Fill all 360 bins with the same rotated copy using
  vector resize(N, value). Safe and minimal memory (shared copies).

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
U1: seed_bottom_left randf reversed range (part > half bed)
U2: seed_greedy_grid division by zero on empty parts
U3: Missing bed_idx=-1 when model instance not found
U4: rotated_copy negative dx/dy cast to size_t (silent voxel loss)
U5: population_size=0 causes UB in tournament_select

False positives triaged out: fitness div-by-zero (bed validated at
entry), mutate parts[i] bounds (sizes always match), double bed_origin
(verified correct by Sauron x3), evaluate_batch exception (CPU doesn't
throw), compaction timeout (12-iter binary search is bounded).

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Wave 3: randomize_placement hi_x/hi_y now subtracts part_margin on
  right/top side (was only subtracting lo, allowing parts to exceed bed)

Wave 4: Wildcard mutation randf(wild_margin, bed-wild_margin) guarded
  against reversed range when part > half bed size.

False positives triaged: PopItemWidth (Orca convention — 22 pushes, 2
pops across file, window end resets stack), nudge_range negative
(config is always positive), index bounds (validated upstream),
get_rotated null (private, only called from guarded path).

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
When bed_margin_mm > bed_width/2 (tiny beds like 120mm A1 Mini with
large gap settings), std::clamp(val, margin, bed-margin) had min > max
which is undefined behavior per C++17.

Added safe_clamp() that returns midpoint when min > max. Replaced all
7 std::clamp calls with bed_margin bounds in the nester.

Wave 5: CLEAN (1 of 3)
Wave 6: 1 bug fixed (this), 4 CLEAN

Running total: 3 waves needed with 0 bugs. Currently at 1 of 3.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Wave 7b: Greedy accept now skips parts with empty grids (failed
  voxelization, bed_idx already -1). Previously called rotated_copy
  on empty grids which could produce garbage results.

Wave 7c: rotated_copy now checks both lower AND upper bounds on
  destination indices (dx < rnx, dy < rny). Previously only checked
  dx >= 0, dy >= 0 — edge voxels at the exact boundary could write
  out of bounds (silently dropped by set() but indicates logic error).

False positives triaged: crossover stale fitness (re-evaluated next
gen), reset rotation=0 (intended safe default), params vs settings
mismatch (Orca's existing pattern — non-user fields computed from
printer config, not stored in ArrangeSettings).

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
padding_mm, max_parts, and timeout_s now clamped on load to match
UI slider ranges. Prevents hand-edited config from producing
out-of-range values (negative max_parts, zero timeout, etc.).

Wave 8: 1 real fix (load clamps), 2 false positives triaged
  (compact parts bounds — sizes always match; parts[0] — guarded by n<2)

Clean wave count: 0 of 3 (reset by this fix)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
snuggle_rotation_step now clamped to [0,90] on config load for
symmetry with all other numeric settings. UI already snaps to
valid values, but load path was unclamped.

Wave 9: 1 minor fix, 2 false positives (voxelizer indices validated
upstream, GPU margin is documented TODO)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Replaced quality slider with direct numeric controls:
- Population (16-1024, default 64) — GA candidates per generation
- Generations (10-500, default 30) — evolution cycles
- Resolution (0.5-5.0mm, default 2.0mm) — voxel size with slider+textbox
- Timeout (2-300s, default 5s) — per-plate time budget

Defaults from Legolas performance sweep:
- pop=64 gen=30 at 2mm: 5-10 parts in ~1-2 seconds, collision-free
- Rotation step default: 15 degrees (was locked)
- Compact: OFF by default (GA fitness handles packing, compact causes
  bugs with rotation snapping)

UI layout:
- Top level: Enable, Spacing slider, Rotation dropdown, Resolution slider
- Collapsible "Effort / Advanced": Population, Generations, Timeout,
  Max parts, Multi-plate, Post-GA compaction

All settings load/save/reset with clamps.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Changed snuggle_padding_mm default from 5.0 to 1.0 mm.

Impact (256mm bed, pop=64 gen=30, 2mm voxels):
- 5mm gap: wall at 17 parts
- 1mm gap: wall at 30 parts, 693ms, 49% bed utilization

The 5mm default was eating ~90mm of bed space in gaps alone.
1mm is sufficient for FDM — conservative outward voxelization
already provides sub-voxel clearance margin.

From Legolas v3 (509 runs): the feasibility wall is geometric
(bed area vs part footprint), not algorithmic. Reducing gap
is the single biggest lever for fitting more parts.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Seers found: GPU shader was already adaptive-voxel-safe (reads
per-grid voxel_size from GridMeta). Real bug was GPU ignoring
bed_margin in OOB checks — shader checked against [0, bed_w]
while CPU checked [margin, bed_w-margin]. Fixed by adding
u_bed_margin uniform to shader.

Test harness updates:
- GPU enabled (SLIC3R_GUI + GLEW linked)
- Multi-restart (--restarts N, keeps best of N runs)
- Adaptive voxel (--detail N, per-part resolution from AABB diagonal)
- Gap default changed to 1mm

Status: adaptive+GPU works for 10 parts. 20 parts shows oob_count
disagreement (under investigation — margin coordination issue).

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Added guards to GPU evaluator upload_grids:
- Query GL_MAX_SHADER_STORAGE_BLOCK_SIZE before upload
- Check total_voxel_bytes < UINT32_MAX (shader uses uint32 offsets)
- Fall back to CPU with warning if limits exceeded

Bug narrowed: adaptive voxels + rotation (rot=15) on GPU = broken.
Adaptive + locked rotation (rot=0) on GPU = works perfectly.
Uniform voxels + rotation on GPU = works perfectly.
The issue is specifically: mixed voxel_size parts × rotated grids.

Ents + Sonnet probe confirmed: not a buffer SIZE issue (data is small
enough). Root cause is in rotation cache metadata when parts have
different voxel_sizes and rotation produces variable-sized grids.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Remove undefined 'quality' variable in log line (Sauron: compile error)
- Make GpuCollisionEvaluator::available_ std::atomic<bool> (Aragorn: data race)
- Add index bounds check in voxelize_indexed_mesh (Aragorn: OOB read)
- Replace 6 live rotated_copy() calls in compaction with cached_rotated()
  (Legolas: 7.2s wasted allocation at 1mm voxels, exceeded compact timeout)
- Fix full-fallback finish message showing "Snuggle complete — 0 parts"
  instead of generic "Arranging done." (Frodo: misleading UX)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@thereprocase

Copy link
Copy Markdown
Owner Author

Not merging to main yet — keeping work on feature/snuggle-uiux.

thereprocase added a commit that referenced this pull request Mar 31, 2026
Ledger fixes (Sauron #1):
- W-001: clean up dangling brace in SnuggleArrange (cosmetic, already safe)
- W-003 CRITICAL: GPU cleanup() now releases DC/HWND on all failure paths
- W-006 HIGH: voxel_size<=0 guard in world_to_grid + collision_count
- W-007 HIGH: multimap instance lookup replaces O(N²) linear scan
- W-010: fmod replaces while-loop rotation normalization (DoS fix)
- W-027: shared snuggle_constants.hpp for ROT_CACHE_BINS
- W-028: PI_F/TWO_PI_F constants replace 9 bare literals

UX fixes (Sauron OrcaSlicer#2, from Frodo's supervision):
- Part gap slider min matches backend (1mm)
- Duplicate compact checkbox removed
- 4 missing tooltips added (Part gap, Timeout, Max parts, Multi-plate)
- Progress messages humanized ("Optimizing... 42%")
- Footer text clarified
- Fallback notification added
- Label references corrected

Reviewed by: Gandalf (ledger walkthrough), Frodo (final approval)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
thereprocase added a commit that referenced this pull request Apr 1, 2026
Critical: translation writeback computed bbox center instead of
polygon origin (0,0). Parts would land at wrong positions offset
by (bbox_center - origin). Fixed to track where the poly's local
origin falls in bed coordinates after placement.

High: removed dead code (6 unused variables from earlier approach).
High: config key now uses technology postfix (_fff, _sla, _seq_print)
matching the established pattern for multi-tech persistence.

Medium: guard negative pixel offsets in collides/stamp to prevent
undefined behavior from exclude polygons extending past bed edge.
Medium: replaced goto-based instance matching with bool tracking
and added warning log when no ModelInstance match is found.

Fixes: Sauron #1 (critical), OrcaSlicer#2 (high), OrcaSlicer#3 (high), OrcaSlicer#5 (medium),
       Uruk-Hai UI #1 (high), OrcaSlicer#2 (high), OrcaSlicer#3 (medium),
       Uruk-Hai ArrangeJob OrcaSlicer#2 (high)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
thereprocase added a commit that referenced this pull request Apr 5, 2026
Critical: translation writeback computed bbox center instead of
polygon origin (0,0). Parts would land at wrong positions offset
by (bbox_center - origin). Fixed to track where the poly's local
origin falls in bed coordinates after placement.

High: removed dead code (6 unused variables from earlier approach).
High: config key now uses technology postfix (_fff, _sla, _seq_print)
matching the established pattern for multi-tech persistence.

Medium: guard negative pixel offsets in collides/stamp to prevent
undefined behavior from exclude polygons extending past bed edge.
Medium: replaced goto-based instance matching with bool tracking
and added warning log when no ModelInstance match is found.

Fixes: Sauron #1 (critical), OrcaSlicer#2 (high), OrcaSlicer#3 (high), OrcaSlicer#5 (medium),
       Uruk-Hai UI #1 (high), OrcaSlicer#2 (high), OrcaSlicer#3 (medium),
       Uruk-Hai ArrangeJob OrcaSlicer#2 (high)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
thereprocase added a commit that referenced this pull request Apr 11, 2026
User priority #1: minimize print-head travel. Travel cost between two
points in a rectangle is upper-bounded by the diagonal, so minimizing
diag^2 is a direct proxy for worst-case travel across the cluster.
Area delta doesn't distinguish a 100x100 cluster (diag^2=20000) from
a 400x25 cluster (diag^2=160625) — both have area 10000 — even though
the 400x25 layout forces 2.8x longer diagonal moves for the same
total footprint. Diag^2 correctly identifies the square as tighter.

Implementation: replace the area-delta primary with diag^2-delta in
both the main placement score_at and the consolidation migration
score_dst. Same data inputs (cluster bbox corners + candidate bbox
corners), different metric. Interlock detection still works because
a rotated shape that fits inside the current bbox has delta=0 in both
area and diag^2.

Not expected to change tetromino tests much (tetrominoes are tight
interlock-dominated, scoring mostly hits the delta=0 case). Should
favor more square cluster shapes on realistic mixes — tall elongated
strips will lose to compact clumps of equal area.

Removed the now-dead cbb_area variable at line 607.

Pairs with the priority ordering saved to
memory/feedback_arrange_priorities.md.
thereprocase added a commit that referenced this pull request Apr 11, 2026
Primary stays delta(cluster_bbox_area). New secondary is
delta(cluster_bbox_perimeter). Tertiary is the existing
dist-to-anchor + tall-bias. score_at now returns
std::tuple<int64_t, int64_t, double> and std::tuple's operator< does
lexicographic comparison directly.

Rationale: when multiple candidate positions produce the same area
growth, the current tiebreaker was dist-to-anchor (positional). Now
the first tiebreak is perimeter growth, which prefers positions that
keep the cluster more square. Square clusters have shorter perimeters
which directly tracks worst-case print-head XY travel — user priority #1.

For non-tied-area cases, behavior is unchanged (primary still wins).
For tied-area cases, the secondary now drives selection toward square
clusters instead of arbitrary off-grid positions.

Earlier this session I tried diag² as a REPLACEMENT for the area
primary and that broke a pre-existing sub-pixel test fragility. This
version is lower-risk because ties on area are rare enough that the
positional shift is smaller. The 4-equal-squares case is already
solved by corner-anchor-for-all; this commit is additional insurance
for mixed-item cases where area ties happen at coarse-scan boundaries.

Tuple type change touches: score_at, CoarseBest::score, refine_score,
plus new #include <tuple>.
thereprocase added a commit that referenced this pull request Apr 11, 2026
Critical: translation writeback computed bbox center instead of
polygon origin (0,0). Parts would land at wrong positions offset
by (bbox_center - origin). Fixed to track where the poly's local
origin falls in bed coordinates after placement.

High: removed dead code (6 unused variables from earlier approach).
High: config key now uses technology postfix (_fff, _sla, _seq_print)
matching the established pattern for multi-tech persistence.

Medium: guard negative pixel offsets in collides/stamp to prevent
undefined behavior from exclude polygons extending past bed edge.
Medium: replaced goto-based instance matching with bool tracking
and added warning log when no ModelInstance match is found.

Fixes: Sauron #1 (critical), OrcaSlicer#2 (high), OrcaSlicer#3 (high), OrcaSlicer#5 (medium),
       Uruk-Hai UI #1 (high), OrcaSlicer#2 (high), OrcaSlicer#3 (medium),
       Uruk-Hai ArrangeJob OrcaSlicer#2 (high)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
thereprocase added a commit that referenced this pull request Apr 11, 2026
User priority #1: minimize print-head travel. Travel cost between two
points in a rectangle is upper-bounded by the diagonal, so minimizing
diag^2 is a direct proxy for worst-case travel across the cluster.
Area delta doesn't distinguish a 100x100 cluster (diag^2=20000) from
a 400x25 cluster (diag^2=160625) — both have area 10000 — even though
the 400x25 layout forces 2.8x longer diagonal moves for the same
total footprint. Diag^2 correctly identifies the square as tighter.

Implementation: replace the area-delta primary with diag^2-delta in
both the main placement score_at and the consolidation migration
score_dst. Same data inputs (cluster bbox corners + candidate bbox
corners), different metric. Interlock detection still works because
a rotated shape that fits inside the current bbox has delta=0 in both
area and diag^2.

Not expected to change tetromino tests much (tetrominoes are tight
interlock-dominated, scoring mostly hits the delta=0 case). Should
favor more square cluster shapes on realistic mixes — tall elongated
strips will lose to compact clumps of equal area.

Removed the now-dead cbb_area variable at line 607.

Pairs with the priority ordering saved to
memory/feedback_arrange_priorities.md.
thereprocase added a commit that referenced this pull request Apr 11, 2026
Primary stays delta(cluster_bbox_area). New secondary is
delta(cluster_bbox_perimeter). Tertiary is the existing
dist-to-anchor + tall-bias. score_at now returns
std::tuple<int64_t, int64_t, double> and std::tuple's operator< does
lexicographic comparison directly.

Rationale: when multiple candidate positions produce the same area
growth, the current tiebreaker was dist-to-anchor (positional). Now
the first tiebreak is perimeter growth, which prefers positions that
keep the cluster more square. Square clusters have shorter perimeters
which directly tracks worst-case print-head XY travel — user priority #1.

For non-tied-area cases, behavior is unchanged (primary still wins).
For tied-area cases, the secondary now drives selection toward square
clusters instead of arbitrary off-grid positions.

Earlier this session I tried diag² as a REPLACEMENT for the area
primary and that broke a pre-existing sub-pixel test fragility. This
version is lower-risk because ties on area are rare enough that the
positional shift is smaller. The 4-equal-squares case is already
solved by corner-anchor-for-all; this commit is additional insurance
for mixed-item cases where area ties happen at coarse-scan boundaries.

Tuple type change touches: score_at, CoarseBest::score, refine_score,
plus new #include <tuple>.
thereprocase added a commit that referenced this pull request Apr 12, 2026
Two fixes from visual testing with the GUI-wired snapshot:

1. island.bbox now expanded by inflation padding so locate's clamp
   accounts for the full bitmap footprint, not just raw polygon
   extent. Fixes parts hanging past bed edges on high-count plates.

2. Pre-rotate polygon ONCE per rotation, not per candidate position.
   Eliminates N_candidates rotate() calls per item — the #1 hot-path
   cost per Legolas's S3.3 review. Applied to both coarse scan and
   refine pass. Also avoids full ExPolygon clone per candidate;
   instead translates pre-extracted contour points with emplace_back.

34 cases / 318 assertions green.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants