Compare commits

...
Author SHA1 Message Date
SoftFever 802f125d68 Translate the web dialogs and pages through the .po catalogs 2026-10-11 17:43:06 +08:00
SoftFever bae8f64b8b Complete and refine Simplified Chinese translations (#16356)
* Complete Simplified Chinese translations

* Fix Simplified Chinese 3D view, camera and extruder terms

* Use Chinese CAD view names on the navigator cube
2026-10-11 15:35:29 +08:00
HanifKoh 2df07b7ed8 Verify the TLS Certificate When Downloading a Cloud Plugin (#16253) 2026-10-11 14:25:50 +08:00
Klober81andRodrigo Faselli 295094a594 Pack bed texture and model into printer bundles (#15965)
* feat(preset): pack bed texture and model into printer bundles

Export copies bed_custom_texture and bed_custom_model into the .orca_printer zip under bed/. The manifest lists them in printer_asset. Import extracts each file into the bundle folder and rewrites the preset to that path. A missing file is skipped. A path that leaves the bundle directory is rejected. An old bundle with no printer_asset imports as before.

* Add includes flagged by clang-tidy misc-include-cleaner

* Gate bed asset packing on an export checkbox and key printer_asset per printer

The Export Configs checkbox can leave a custom bed out of a shared printer bundle. printer_asset is keyed by each printer_config zip entry, with files under bed/<n>/, so a later multi-printer bundle does not need a second manifest. The unshipped flat printer_asset object is not read.

---------

Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-10-11 13:32:07 +08:00
Ian Bassi 210bc927bc Revert "Add Thai Localization (Educational / Hybrid Translation)" (#16349) 2026-10-10 22:39:45 -03:00
SoftFever 00cfb5b259 Refresh translation catalogs and fix xgettext warnings (#16346) 2026-10-11 08:45:44 +08:00
chettasit 2bd105b0ce Add Thai Localization (Educational / Hybrid Translation) (#15940)
Hi OrcaSlicer Team,I have completed the Thai localization for OrcaSlicer (over 7,000 lines).Please note: This is a "Hybrid Translation" designed specifically for educational purposes. Instead of direct literal translations (which often confuse beginners due to technical jargon), I have included brief explanations of the mechanical and physics principles directly within the UI strings.Example: For "TPU is not supported by AMS", the translation briefly explains why (because the material is soft, rubber-like, and will jam the long tubes).This approach was developed for the EDrobot project in Khon Kaen, Thailand to help STEM students and beginners understand 3D printing concepts immediately without a steep language barrier.[cite: 8] We focus on empowering students to learn robotics and 3D design effectively.[cite: 8]   I understand this makes some strings longer than standard localizations, but it has proven incredibly helpful for beginners. I would be honored if you consider adding this to the official release as the Thai language option. I am happy to maintain this file for future updates.Thank you for building such an amazing slicer!Best regards,Chetsit Srichan (EDrobot)
2026-10-11 08:34:19 +08:00
Ian BassiandRodrigo Faselli b81d761605 Refresh plate tab after bed type change (#16344)
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-10-10 21:06:29 -03:00
6922edfc20 Fix Anycubic Vyper start G-code hotend wait (#16216)
* Fix Anycubic Vyper start gcode hotend wait

* Bump Anycubic profile version

---------

Co-authored-by: 陈诗健 <chenshijian@chenshijiandeMacBook-Air.local>
Co-authored-by: SoftFever <softfeverever@gmail.com>
Co-authored-by: SoftFever <103989404+SoftFever@users.noreply.github.com>
2026-10-11 07:34:11 +08:00
Javad Shafique 7ec147603f Default Bambu profiles to 3MF output (#14767) 2026-10-11 06:55:50 +08:00
jorymorrison 8ee9e5915a Fix Prusa CORE One pressure advance condition in filament profiles (#16093) 2026-10-11 06:39:53 +08:00
SoftFever cd5b786704 Add AnkerMake PLA+ filament profiles (Basic, Matte, Metallic, Silk, Glitter) (#16293)
# Description

Adds Anker's own AnkerMake PLA+ filament line to the Anker vendor
bundle: **Basic, Matte, Metallic, Silk, Glitter**. Until now the bundle
only had `Generic PLA+ @Anker`.

Each filament follows the existing Anker layout. A `@Anker base` profile
(`instantiation: false`, inherits `Generic PLA+ @Anker base`,
`filament_vendor: AnkerMake`) holds the settings. An instantiated
`@Anker` profile holds only `compatible_printers`. `filament_id` and
`setting_id` were minted with `scripts/orca_profile_tool.py
generate-id`, and `Anker.json` was re-indexed with `update-index`. The
Anker vendor version goes from `02.04.00.05` to `02.04.00.06`.

## Source of the values

The values come from the AnkerMake filament profiles bundled with
**eufyMake Studio** (Anker's PrusaSlicer 2.6 fork, AGPL-3.0, source at
https://github.com/eufymake/eufyMake-PrusaSlicer-Release), in
`profiles/Anker-ini/AnkerMake base/base.ini`. OrcaSlicer is also
AGPL-3.0.

Conversion notes:
- PrusaSlicer keys are mapped to their Orca equivalents. For example,
`bed_temperature`/`first_layer_bed_temperature` go to `hot_plate_temp*`
and `textured_plate_temp*`, `fan_below_layer_time` goes to
`fan_cooling_layer_time`, `slowdown_below_layer_time` goes to
`slow_down_layer_time`, and `extrusion_multiplier` goes to
`filament_flow_ratio`. Only keys that differ from the inherited `Generic
PLA+ @Anker base` chain are written.
- Where Studio sets retraction but leaves deretraction unset (Metallic,
Silk), `filament_deretraction_speed` is set equal to the retraction
speed. That matches Prusa's "0 = same as retract" meaning.
- **Max volumetric speed is new.** Studio ships
`filament_max_volumetric_speed = 0` (uncapped) for these filaments, and
Orca has no equivalent. The caps were derived from the peak flow that
Studio's own M5C "Fast" process pushes: Basic/Matte/Glitter 22 mm³/s,
Metallic 20, Silk 13. Please treat these as the least-validated numbers
in the PR.
- Studio has pressure advance disabled for these filaments, so no PA
values are carried over.
- Colour variants are not included because they differ only in colour.
`default_filament_colour` uses each family's base colour.
- `compatible_printers` follows Studio's `compatible_printers_condition`
for each family, limited to the 0.4/0.6 printers that `Generic PLA+
@Anker` covers:
  - Basic, Glitter: M5, M5 All-Metal, M5C at 0.4 and 0.6
- Silk: 0.4 on all three, plus M5C 0.6 and M5 All-Metal 0.6 (Studio
excludes M5 0.6)
  - Matte, Metallic: 0.4 only (Studio restricts these to 0.4)
- Studio also allows Basic on 0.2 nozzles. That variant is not added,
because the 0.2 Generic profiles use a separate low-flow cap.

# Screenshots/Recordings/Graphs

N/A (profile-only change).

## Tests

- `scripts/check_profile.sh` (local twin of the CI job, run against the
full profile tree with the nightly macOS validator) passes all five
checks: `profile_tool`, `validate_system`, `validate_slice`,
`validate_filament_subtypes`, `validate_custom` (fixtures v1.9.0 to
v2.4.2).
- `python3 -m unittest discover -s scripts/tests -t scripts`: OK.
- The submitter printed with these filament settings in eufyMake Studio
on the M5C's stock brass 0.4 nozzle without issues. This Orca conversion
has been loaded and sliced in Orca 2.4.2 (as user presets inheriting the
Anker generic PLA+) but not yet printed on brass. The printer now has a
different nozzle fitted.

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-10-11 06:26:26 +08:00
HanifKoh d715bb760c Let Plugin Windows Be Maximized on X11 (#16335)
wxGTK types every wxDialog as a dialog window, and Mutter offers no
maximize for anything but a normal window, so the maximize button and
shortcut did nothing on GNOME under X11. Modeless plugin windows are now
normal windows, kept off the taskbar as before. Modal ones stay dialogs
so the window manager keeps routing focus from the main window to them.
2026-10-11 04:40:56 +08:00
HanifKoh de5f2cb3b9 Expose App, GL, Printer and Project Facts to Plugins (#16324)
* Expose App, GL, Printer and Project Facts to Plugins

Plugins that help users file bug reports could only guess these from log lines: the app config is
deny-listed, and the project path, a project export and the device list had no API.

orca.host gains app_info(), gl_info() and selected_printer(), and Plater gains project_path() and
export_3mf_copy(path). The copy export leaves the project's file name, saved state and model
unchanged, does not write the signed-in account as the designer, and raises the audit event of
open(path, "w") so the plugin gets the same permissions as for writing the file itself.

* Remove app_language in Favour of app_info

app_info() returns the same language. app_language() was added after the last release, so no released build has it.
2026-10-11 04:35:37 +08:00
Riccardo BRAMATI 07a12f5fe4 Read a local plugin's version from its file header (#16118)
* Read a local plugin's version from its file header

read_install_state() let the sidecar's installed_version override the version
parsed from the entry file's PEP 723 header. That is right for cloud plugins,
whose header may be stale, but a local plugin has no other source of truth, so
the UI kept showing the version recorded at first install after the file was
edited. For local installs, use the header version.

* Keep the cloud installed version of an unsubscribed plugin

A cloud plugin kept as local after unsubscribing is rewritten with installed_from=local and no cloud_uuid, which the local-header rule could not tell apart from a real local install. Its installed_version is still the cloud one while the header may lag, so the next scan showed the stale header version. Record the conversion in the install state and exclude those plugins from the header rule. Add tests for a local plugin following its header and for an unsubscribed cloud plugin keeping its cloud version.

* Add tests for the version of a local plugin

A local plugin's version comes from its entry file header. Cover the case of a plugin edited after install, and a plugin unsubscribed from the cloud, which is now a normal local package.
2026-10-11 03:09:32 +08:00
Ian Bassi ea10c84d6b Fix scale and mirror of rotated mirrored objects (#16131)
* Fix decomposition of mirrored transformations

computeRotationScaling() may place the mirror on any axis, giving unstable rotation/scale for mirrored matrices. Decompose them by flipping each local axis and choosing the one needing the least rotation (lowest axis on ties). Merge the extract helpers into a single extract_rotation_scale() and simplify contains_skew() to check that the scale is diagonal.

set_scaling_factor() now preserves the mirror sign on the scaled axis. Selection::scale_and_translate() uses Transformation's rotation and scale instead of calling computeRotationScaling() directly, so scaling a mirrored instance adds no skew.

Add tests for mirrored decomposition, scaling, and skew detection.

* Use Eigen polar decomposition for instance scaling

Selection::scale_and_translate now splits the instance matrix with computeRotationScaling instead of get_rotation_matrix().

Adds a Model test checking that an instance created from another's offset, scale, rotation and mirror reproduces its matrix (as Add instance and Fill bed do).

* tidy
2026-10-10 16:03:34 -03:00
SoftFever e5d5fb47ca Fix BBL infill retraction reduction default (#16201)
# Description

Restore `"reduce_infill_retraction": "1"` in the shared Bambu Lab
process profile. The BambuStudio profile sync (#15851) replaced it with
`reduce_infill_retraction_mode`, which OrcaSlicer does not support,
causing the setting to fall back to disabled.

This restores the pre-sync behavior and bumps the BBL bundle version to
02.08.00.13.

# Validation

- All 233 selectable BBL process presets inherit the restored setting
with no other resolved settings change.
- Static profile validation, system loading, filament validation, and
all 1,263 slice cases pass.

 Fixes #16194
<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-10-10 22:44:44 +08:00
SoftFever 63918b44a8 Merge branch 'main' into anker-ankermake-pla-plus-filaments 2026-10-10 22:41:16 +08:00
SoftFever 53c57c94fd Fix the 3D view stuttering every few seconds while signed in to Orca Cloud (#16340)
# Description

Orbiting or panning the 3D view no longer hitches every five seconds on
Windows and macOS while signed in to Orca Cloud. The stutter is a
regression from #15710.

No change to slicing or to the Orca Cloud connection status shown in the
GUI.

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-10-10 22:39:45 +08:00
SoftFever 53eeedb642 Fix the 3D view stuttering every few seconds while signed in to Orca Cloud 2026-10-10 21:15:45 +08:00
SoftFever aa14598e32 iXex: Parallel Printing Support for IDEX and IQEX Printers (#13086)
## Summary

This PR adds first-class parallel printing support to OrcaSlicer for
printers with multiple independent X-axis carriages — the **IDEX** and
**IQEX** hardware families. The feature is named **IMEX** (independent
multi-extruder) internally, which captures the supported topology space
more accurately than either acronym alone, and is exposed in the UI
under the user-facing label **IDEX/IQEX Configuration**. It is designed
to be printer-agnostic and firmware-flexible, with Klipper,
RepRapFirmware, and Marlin all supported for per-tool Pressure Advance
and per-layer temperature management. MMU/AFC setups where multiple
logical filament slots share one physical extruder are supported via a
`physical_extruder_map` profile option. Printers whose firmware handles
copy/mirror placement internally — RepRapFirmware IDEX duplication mode
on Flashforge Creator Pro 2 / Creator 3 Pro is the canonical example —
are supported through a firmware-managed-zones flag that emits a
centered single-half slice for the firmware to fan out.

The implementation spans printer configuration, process settings UI, bed
visualization, per-plate mode selection, ghost-object rendering with
per-plate filament overrides, placement validation, pre-slice conflict
warnings, G-code generation (with per-tool PA, per-layer temperature
management, and optional center-origin slice frame), and layer preview
animation. It supports five distinct topology paradigms:

- **1 gantry × 2–4 tools** — classic IDEX with 2 tools (BCN3D Sigma,
Snapmaker J1, Tenlog Hands-2 style — two independent X carriages on a
shared Y gantry); multi-extruder on a shared rail when extended to 3 or
4.
- **2 gantries × 1 tool each** — two fully-independent XY systems
sharing a bed (Vivedino Xplorer style). Both Y rails independent —
effectively "half an IQEX."
- **2×2 grid, independent quadrants** — IQEX running four separate
prints in parallel.
- **2×2 grid, paired-gantry multicolor** — IQEX where cross-gantry tools
share color responsibilities via the **Span** tile state.
- **Firmware-managed center-origin** — slicer emits a centered
single-half slice; firmware decides where to physically fan it out.
Overlays on any of the above hardware topologies; canonical example is
Flashforge IDEX with RepRapFirmware.

---

## Feature Walkthrough

### Printer Configuration

Printer preset options that declare IMEX capability and geometry:

| Option | Type | Description |
|--------|------|-------------|
| `is_imex` | bool | Marks this printer as IMEX-capable |
| `imex_firmware_managed_zones` | bool | Emit centered single-half
slice; firmware handles copy/mirror placement (default `false`) |
| `imex_gantry_count` | int | Number of independent Y-axis gantries
(rows) |
| `imex_tools_per_gantry` | int (1–4) | Toolheads per gantry along X
(columns) |
| `imex_nozzle_clearance_x` / `imex_nozzle_clearance_y` | float |
Nozzle-to-collision-edge distance in mm (literal, not halved) |
| `imex_tool_layout` | string | Physical orientation: front-left /
front-right / rear-left / rear-right |
| `imex_mode_names` | string[] | Names of user-defined parallel modes;
first entry is the reserved `primary` sentinel |
| `imex_mode_active_tools` | string[] | Tool role assignments per mode
(e.g. `0:P,1:C,2:M,3:M`, or `0:P,1:S,2:M,3:M` with Span) |
| `imex_mode_gcodes` | string[] | Firmware macro to activate per mode |
| `physical_extruder_map` | string[] | 0-indexed map from logical
filament slots to physical extruders (for MMU/AFC — see below) |

Tool roles per mode: **Primary** (P), **Copy** (C), **Mirror** (M),
**Span** (S), **Inactive**.

### Parallel Mode Editor

[Kooha-2026-05-14-13-45-35.webm](https://github.com/user-attachments/assets/af7650e6-d636-478b-9398-3f02ec662f03)

The **IDEX/IQEX Configuration** and **IDEX/IQEX Parallel Modes**
sections live in the printer preset's **Multimaterial** page (visible
only when `is_imex = true`). The mode editor is a visual grid:

- Each row defines one named parallel mode. The first row is a reserved,
non-deletable **Primary** row stored as the sentinel `primary` in
`imex_mode_names`.
- The Primary tool is always T0; **Tool 0 Position** chooses which
physical corner it occupies, and its tile is read-only. The other tool
buttons cycle Inactive → Copy → Mirror → Span → Inactive, color-coded,
with Span offered only where it applies.
- **Span** is only offered when `imex_gantry_count >= 2` AND the tile is
on the primary's gantry row — it declares "this tool is the multi-color
partner of Primary on the same gantry," distinct from a Copy/Mirror role
on a non-primary gantry.
- A G-code field per mode holds the firmware macro to activate that mode
(e.g. `IMEX_COPY` for Klipper). A placeholder-browser button per row
opens the `EditGCodeDialog` for quick insertion.
- Modes can be added and removed (removed via a dedicated
`imex_remove.svg` ScalableButton).
- Deleting the active mode resets affected plates to Primary.
- Tool assignments are preserved across `imex_gantry_count` changes:
going from IQEX (4 carriages) → IDEX (2 carriages) → IQEX restores all
previously assigned roles, and the visible grid anchors to the gantry
row containing the Primary assignment.

<!-- TODO: replace this placeholder with a re-recorded
parallel-mode-editor video
showing the Span tile state, the remove button, and the
placeholder-browser button.
Old URL (out of date):
https://github.com/user-attachments/assets/cabf1f70-206d-4f84-9c2e-594b38b83951
-->

### Span Tile State — Paired-Gantry Multicolor
<img width="2833" height="1300" alt="2026-05-14-135412_grim"
src="https://github.com/user-attachments/assets/52203133-7380-459a-973f-0502a1edc3c2"
/>

For IQEX printers (`imex_gantry_count >= 2`), the **Span (S)** tile role
enables paired-gantry multicolor mirror mode. The same active-tools
string `0:P,1:C,2:M,3:M` is ambiguous between two distinct hardware jobs
— four independent quadrants vs. paired-gantry multicolor mirror — so
the topology has to be declared explicitly rather than inferred. Span on
the primary's gantry row is the declaration.

Behavior driven by Span presence:

- **Multicolor block rule**: requires Span on primary's gantry to allow
multi-color slicing in a parallel mode. Without Span, multi-color
slicing in a parallel mode is blocked with an actionable error message.
- **Ghost aggregation**: one aggregated ghost per non-primary gantry
when Span is present, using the column-paired representative tool.
Mixed-role gantries fall back to per-tool.
- **Zone aggregation**: one row-strip zone per non-primary gantry when
Span is present (cell at primary's column collapses col-sep;
`make_boxes` expands to full-X strip).
- **Aggregated mirror drag**: ghost translates 1:1 with primary in X
(copy-style), with X-flip baked into the mesh-local frame so geometry
still reads as mirrored. Gantries don't share an X rail, so reflecting
motion serves no collision purpose.
- **Carriage collision strip audit**: X-boundary checks require `zr ==
pri_row_k` (matching the existing Y-boundary `c == pri_col` constraint).
Prevents spurious strips on primary's right edge in paired-gantry
mc-mirror, where T3 sits diagonally and can't actually collide with
primary's carriage.

Single source of truth:
`IMEXHelpers::group_imex_active_tools_by_gantry(active_tools_str,
tools_per_gantry)`. Ghost factory and zone calculator both consume it,
so pairing logic lives in one place.

### Per-Plate Mode Selection

Each build plate has an IMEX mode icon in its toolbar (normal, hover,
dark, and dark-hover SVG variants). The mode can be set independently
per plate:

- **Left-click** cycles through all non-sentinel modes in order.
- **Right-click** opens a popup menu listing all modes as radio items
for direct selection.
- Mode changes are recorded in the undo/redo snapshot system.
- The selected mode is persisted in the 3MF project file per plate
(`imex_parallel_mode` key).


https://github.com/user-attachments/assets/5ab497e3-9c2f-476a-ac67-ef34a592395b

When a parallel mode is active alongside multi-material objects on the
same plate, a warning badge (`obj_warning.svg`) overlays the plate icon
— see **Pre-Slice Warning System** below.

### Bed Visualization + Auto-Arrange Constraints

When an IMEX mode is active, the build plate renders the carriage grid:

<img width="2761" height="1447" alt="IMEX bed visualization with active
primary zone and dimmed secondary zones"
src="https://github.com/user-attachments/assets/3ee64b7b-a3ed-40aa-ad07-fa2f1cb74edf"
/>

- The **active (primary) zone** is full brightness.
- **Inactive zones** are dimmed with a color-coded overlay (blue for
copy, orange for mirror).
- **Zone dividers** are rendered as lines across the bed.
- The grid is a full 2D layout: `imex_tools_per_gantry` columns ×
`imex_gantry_count` rows. Zone sizing is based on the **active** tool
count only — inactive tools donate their bed share to active neighbors.
- Span-present configs render as a single row-strip zone per non-primary
gantry rather than per-tool quadrants, reflecting that the firmware will
paint the multi-color across the entire strip.
- Colors are drawn from the Okabe-Ito palette for colorblind
accessibility, with a deuteranopia/protanopia-safe alternate theme.
- **Auto-arrange** is constrained to the primary zone when a parallel
mode is active — `ArrangeJob::process()` replaces full-bed `bedpts` with
the primary zone corners via `PartPlate::imex_primary_zone()`. Collision
strips are additionally registered as hard obstacles through
`m_unselected` (the working NFP placer input, not the dead
`excluded_regions` field), so placement cannot drop parts into the
danger strips.

### Placement Validation


https://github.com/user-attachments/assets/5cb5a94a-981e-4fe9-898e-7e2f891b4f40

Objects placed outside the primary zone block slicing:

- `has_imex_placement_violations()` in `PartPlate` checks each object's
convex hull against the primary zone boundaries and collision strips.
- Violations inject into the existing `update_background_process`
validation pathway — the Slice button is disabled and an error
notification is shown.
- Mirror tools additionally generate X-axis collision strips (copy tools
move in the same direction and cannot collide). Y-direction strips are
scoped to same-column tools to avoid false positives from diagonal
mirror pairs.
- Strip width is taken **literally** from `imex_nozzle_clearance_x` (the
measurement is nozzle-to-collision-edge distance, not carriage
half-width).
- Multi-color block path: a pre-slice rule blocks multi-color slicing in
parallel modes that can't physically support it (e.g. a "fake IMEX" mode
where all tools sit on a single gantry without a Span partner). The rule
is centralized in `imex_multicolor_block_reason()` with unit-test
coverage of every gating case.

### Ghost Object Rendering + Per-Plate Filament Picker

When a parallel mode is active, the slicer renders colored, transparent
**ghost copies** of primary-head instances on the plate — one per
secondary active head, transformed under its Copy/Mirror role (or
aggregated per gantry when Span is present).

- Ghosts track the primary through drag/rotate/scale/mirror and
invalidate on mode, filament-map, or `physical_extruder_map` changes.
- **Left-click** on a ghost opens the `IMEXFilamentPickerPopover` for
that ghost's head — a compact `BitmapComboBox` that writes directly to
the per-plate `imex_head_filament_map` (MMU lane override).
- Mirror ghost geometry is a **true reflection about the zone-boundary
plane** (`x = primary_zone_center.x + gantry_offset.x/2`), so the ghost
stays anchored in the target zone as the primary moves and drag reflects
correctly (primary +X → ghost −X, Y tracks 1:1).
- Hover tooltip: `"Tn → filament N"` with a color swatch. When no
filament resolves to a head, the tooltip surfaces an actionable message
directing the user to extend the extruder count in the Machine tab.
- Ghost rendering + picking are scoped to the active plate — non-active
plates don't draw stale ghosts during arrange/preview transitions, and
click-picking never falls through to a non-current plate's ghost
geometry.
- Per-plate `imex_head_filament_map` round-trips through the 3MF project
file.

### Pre-Slice Warning System
<img width="1786" height="911" alt="2026-05-14-135714_grim"
src="https://github.com/user-attachments/assets/16532d47-2da5-4472-ab9c-4b8b00303bae"
/>

Before a plate slices, IMEX parallel-mode plates are checked for three
classes of conflict:

1. **Multi-material on secondary tools** — when the mode has Copy/Mirror
tools and the plate has multiple filaments active; plate icon gets a
warning badge.
2. **Bed temperature mismatch** — any two carriages configured >5 °C
apart.
3. **Filament type incompatibility** — filaments from different type
families on active carriages.

A dismissible Yes/No `RichMessageDialog` fires once per user action from
both `on_action_slice_plate` and `on_action_slice_all`. The dialog has a
**"Don't show again"** checkbox that persists to `app_config` as
`imex_pre_slice_warnings=false`. A re-enable toggle lives in **Printer
Settings → Multimaterial → IDEX/IQEX Configuration** so suppressed
warnings can be restored.

### Firmware-Managed Zones — Center-Origin Slice
<img width="701" height="197" alt="2026-05-14-135842_grim"
src="https://github.com/user-attachments/assets/3e2dfaab-fb4f-42b1-bf9d-c88dc0c95110"
/>

The `imex_firmware_managed_zones` printer-config option (default
`false`) supports IDEX/IQEX printers whose firmware applies its own
copy/mirror offsets in non-primary modes. Canonical examples:
**RepRapFirmware IDEX duplication mode** on **Flashforge Creator Pro 2 /
Creator 3 Pro**. These printers expect a centered single-half slice at
bed origin and fan toolheads out from there — the slicer-managed
paradigm of placing toolpaths at zone-relative positions produces gcode
the firmware can't reconcile, since it'd double-apply offsets.

When the flag is on and the active mode is non-primary, the slicer
subtracts the primary zone's plate-local center from the gcode emission
frame:

- **Writer offset** is augmented by the primary zone center so emitted
gcode is centered at bed origin.
- **Processor offset** stays at plate_origin only, so the gcode-preview
visualizer renders the centered toolpath at bed center rather than at
the prepare-view zone placement. The user sees what'll physically print
after firmware fan-out.
- **`translate_to_print_space()`** is augmented for frame coherence so
`first_layer_print_min/max` placeholders consumed by user start_gcode
(e.g. Flashforge's M118 "max delta from zero" header) reflect the
centered frame.

Slice-handoff runs per-slice: `PartPlate::refresh_imex_slice_offset()`
is called from `Plater::priv::update_background_process` after
`Print::apply()`, so reslicing with mode toggled but no plate change
picks up the updated offset.

When the flag is off, all related code paths reduce to no-ops
byte-identical to standard slicer-managed behavior. The two stock-code
touches at `Print.cpp:2553-2554` (gcode_offset composition) and
`Print.cpp:2823-2829` (translate_to_print_space) are explicit additive
shifts that collapse to identity when the offset is `Vec2d::Zero()`.

### G-code Injection

The selected parallel mode's G-code is written into the output file
immediately before `machine_start_gcode`:

- Looks up the plate's active mode name in `imex_mode_names` and writes
the corresponding `imex_mode_gcodes` entry.
- Processed through `placeholder_parser_process()` first, so
Klipper-style variable substitution works **and** any `{global}`
declarations flow forward into `machine_start_gcode`.
- Primary mode's G-code field is emitted too.
- Guarded against headless CLI slicing — `ensure_imex_zones()`
short-circuits when `m_plater` is null so the CLI path (used by
upstream's regression-test CI step) doesn't segfault on
`wxGetApp().preset_bundle` dereference.

### Placeholder Parser Integration
<img width="535" height="393" alt="2026-05-14-140227_grim"
src="https://github.com/user-attachments/assets/6d059c77-38f6-4418-bfe9-3bf963e8c836"
/>

Three placeholders register under **Slicing State** and are settable
from anywhere downstream:

| Placeholder | Type | Description |
|---|---|---|
| `imex_mode` | string | The active mode name |
| `imex_mode_index` | int | Index into `imex_mode_names` |
| `imex_mode_gcode` | string | The resolved mode G-code (post-parser) |

### Per-Tool Pressure Advance — Firmware-Agnostic

`set_pressure_advance()` takes an optional tool index (default `-1`,
preserving existing behavior for all non-IMEX call sites). Per firmware:

- **Klipper**: `EXTRUDER=extruder[N]` when `tool >= 0`, bare command
otherwise
- **RepRapFirmware**: `M572 D<N>` when `tool >= 0`, `M572 D0` otherwise
(preserves the pre-IMEX output; a bare `M572` applies to whatever tool
is selected and errors when there is none)
- **Marlin 2**: `M900 K<X> T<N>` when `tool >= 0`, bare `M900` otherwise
- **Marlin Legacy / fallback**: `M900 K<X>` always
- **Repetier**: `M233 X<X> Y<X>` (X is quadratic, Y is linear; same
value applied to both)

`m_imex_parallel_mode` is set once per export from the active plate
mode. PA tool-qualification is gated on this being a **non-primary**
parallel mode — primary-mode prints emit ordinary tool-change PA exactly
like any non-IMEX printer. Secondary active tools in parallel modes
receive explicit per-tool PA at print start since they never go through
a tool-change sequence.

### Per-Layer Temperature Management

In IMEX parallel modes, all active tools (primary + secondaries) get
temperature commands in `layer_change_gcode`:

- Layer 1 temperatures only emit on the first layer; subsequent layers
use normal layer-change temperatures.
- IMEX temperature handling is consolidated into the second-layer
transition.
- Layer-change temperature commands use `M104 T<N>` with
`physical_extruder_map` translation when applicable.

### MMU / AFC Support via `physical_extruder_map`

For printers where multiple logical filament slots share one physical
extruder (MMU, AFC, toolchangers), the `physical_extruder_map` profile
option translates tool slot indices to physical extruder qualifiers
before G-code emission.

- **Fallback**: on IMEX printers, `Print::apply()` uses the profile's
map only when it has one entry per extruder (the `nozzle_diameter`
count). Anything else, including the single-entry default, is replaced
by the identity map `0..n-1`.
- **Used by**: IMEX PA emission, layer-change temperature commands
(`M104 T`, `EXTRUDER=`, `M572 D`), ghost color resolution, ghost cache
key, tooltip lookup, click gate.
- **Profile authoring example** for a 7-slot printer with a 4-lane MMU
on extruder 0 and three independent direct drives on extruders 1/2/3:
  ```json
  "physical_extruder_map": ["0","0","0","0","1","2","3"]
  ```
  Non-MMU printers need no action — the identity fallback handles them.
- A centralized helper in `IMEXHelpers`,
`effective_physical_extruder_map(explicit_pem, nozzle_count)` (with a
`PresetBundle` overload that prefers the project's map over the
printer's), routes all PA/temp/ghost-color lookups through a single code
path.
- **No UI for editing the map** in this PR — non-trivial MMU/AFC layouts
require profile-authoring (hand-edit the printer JSON) or an updated
printer profile shipped by a vendor. A future enhancement would expose a
per-slot extruder picker in the Multimaterial section.

### Layer Preview Multi-Carriage Animation

The sequential preview (scrubber) animates all active carriages
simultaneously:

- One toolhead marker (colored cube) per active carriage, in addition to
the primary.
- Secondary marker colors: cyan (T1), yellow (T2), magenta (T3).
- **Copy** tools: marker placed at the same relative position within
their bed zone as the primary is in the primary zone.
- **Mirror** tools: reflect across the target zone's facing edge
(left-of-copy reflects across copy zone's left edge; right-of-copy
across the right edge). Y position is always zone-relative copy (all
tools on a row share a physical Y rail).
- Carriage footprint boxes use per-carriage `box_offset_x/y` so the
nozzle marker sits at the physically correct edge of the footprint —
zone-based X by default, collision-side edge for Mirror; gantry-behind Y
for back-row primaries, flipped for front-row primaries with a back-row
secondary.
- The filament usage legend notes the active carriage count and mode
name (e.g. `IMEX: ×2 (copy_mode)`).
- **View menu toggle**: `View → Show IDEX/IQEX Toolhead` (visible only
on the Preview tab, only when the active printer is IDEX/IQEX)
hides/shows the per-carriage toolhead representation during preview
playback. State persists to `app_config` as `show_imex_toolhead_boxes`.
Useful when scrubbing through dense toolpaths and the boxes get in the
way of seeing the underlying geometry:


[Kooha-2026-05-14-14-04-09.webm](https://github.com/user-attachments/assets/9869cd4d-3b60-4a6b-8dca-7ee1fc573ac1)

Copy mode:


https://github.com/user-attachments/assets/0774d285-ba36-4b86-a594-fb6572c3aede

Mirror mode:


https://github.com/user-attachments/assets/51528926-94dc-4d90-9b1a-1b42d9044be7

---

## Known Limitations

**Per-layer G-code collision detection**
The placement-time zone check catches gross violations (object placed in
wrong zone) but does not verify that toolpaths on any given layer
maintain adequate X separation between adjacent carriages. A per-layer
check via `ConflictChecker` was designed but deferred. Without it, a
print that passes placement validation could still crash carriages if
the primary object's toolpaths reach too close to a zone boundary.

**Brim avoidance of IMEX zones**
Standard `bed_exclude_area` exclusion zones are already respected by
brim generation. IMEX collision strips are not — the brim generator has
no visibility into them. The correct fix (feeding computed strip
polygons from `PartPlate` through the `Print` object to `Brim.cpp`) was
designed but deferred. In practice, users should leave adequate
clearance between printed objects and zone boundaries to account for
brim width.

**`extruder_printable_area` integration**
IMEX zones are not clipped against per-extruder printable polygons, and
there is no violation check for placing an object outside the
intersection of its active extruder's printable area and the IMEX
primary zone. Deferred pending clarification on the tool→extruder index
mapping.

**Ghost rendering in firmware-managed mode**
When `imex_firmware_managed_zones` is on, ghost rendering is suppressed
entirely. The existing `imex_head_transform` math is slicer-managed
semantics (places ghosts at `primary_zone_center + gantry_offset`) and
produces wrong positions when the toolpath is being emitted in a
centered frame. Proper firmware-managed ghost rendering — showing where
copies/mirrors will physically print after firmware fan-out — needs new
transforms designed around firmware-frame positions rather than a
coordinate-flip of the slicer-managed ones. Deferred to a follow-up.

**Filament-accurate multi-region ghost color**
For paired-gantry multicolor mirror (Span mode), the aggregated ghost
currently uses the representative tool's filament color as a single
solid swatch. A future enhancement would render the ghost split into
per-source-tool regions, each colored by the secondary tool that mirrors
it. Slicer-side correctness is already shipped — the G-code emits the
right T-codes; the ghost is a visual aid only.

**Ghost path overlays**
Secondary carriage toolpaths are not rendered in the layer preview. The
secondary markers animate correctly, but the paths they would trace are
not drawn. Adding ghost path rendering would require duplicating and
offsetting the toolpath geometry per secondary carriage, which is a
significant addition to the libvgcode rendering pipeline.

**No per-mode slicing**
All carriages in a mode execute the same sliced toolpaths (transformed
per zone). There is no support for slicing different objects for each
carriage independently within one mode.

**Global default IMEX mode**
There is no job-level IMEX default mode. Each plate's mode must be set
individually (default is always Primary). A future improvement would add
a global default in the sidebar (following the same pattern as bed type
and nozzle diameter), with per-plate overrides.

**Dynamic GL-rendered mode icons**
The per-plate icon currently uses static SVGs. A GL-rendered
carriage-grid icon that visually represents the mode's tool layout would
be a nicer UX but is deferred.

**Slice-all thumbnail icon refresh**
On the "Slice All" path, plates flagged as IMEX-violated do not refresh
their toolbar icon. Root cause and fix identified, not yet applied.

**Start-G-code filament placeholder**
The resolved per-head filament map is not yet exposed as a
`PlaceholderParser` vector. Exposing it would let MMU firmware macros
pre-load lanes before print start.

**No UI for `physical_extruder_map` authoring**
The MMU/AFC slot-to-physical-extruder map is currently profile-only —
there's no in-app dropdown or editor for it. Users with non-trivial
MMU/AFC setups must hand-edit the printer-preset JSON (or rely on a
vendor-supplied profile). A future enhancement would expose a per-slot
extruder picker in the Multimaterial section so users can declare the
mapping without touching JSON.

**Bundled IQEX printer profile**
A full IQEX printer profile with cover image, bed mesh, and matched
process/filament profiles is not bundled with this PR. Users must
currently author their own printer preset. Deferred to a follow-up
profile-only PR.

**Mode lifecycle gap**
Per-plate mode is stored as a string (the mode name). If a mode is
renamed or deleted from the printer preset after a project is saved, the
plate's saved mode name will not resolve and will silently fall back to
Primary on next load. A warning on load would be a useful addition.

**Firmware-managed prepare→preview frame jump**
In firmware-managed-zones mode, the prepare view shows the part at its
placed-in-zone position while the preview view shows the centered slice.
This is intentional — the views truthfully represent the prepare frame
vs. the post-firmware-fan-out frame — but the visual jump can be
confusing on first use.

---

## Firmware Assumptions

- The implementation assumes the firmware handles all carriage
synchronization and offset math. OrcaSlicer only injects the
mode-activation macro before machine start and is entirely dependent on
user configuration.
- Per-tool Pressure Advance is firmware-aware (Klipper / RRF / Marlin 2
/ Marlin Legacy / Repetier). Layer-change temperature commands use `M104
T<N>` with `physical_extruder_map` translation.
- Tested against Klipper on a real IQEX printer. RepRapFirmware gcode
emission is exercised via profile-driven slice tests (Flashforge Creator
Pro 2 profile); Marlin and RRF paths have not been validated on real
hardware print runs.
- Mid-print mode switching is explicitly not supported. The mode is
locked at print start.
- Firmware-managed-zones mode requires the printer's firmware to
translate centered slice coordinates into physical toolhead positions;
the slicer does not attempt to model the firmware's offset logic.

---

## Files Changed (High-Level)

| File | Change |
|------|--------|
| `src/libslic3r/PrintConfig.{cpp,hpp}` | IMEX config option definitions
+ declarations, `IMEXMode` enum, `physical_extruder_map`,
`imex_head_filament_map` (plate option), `imex_firmware_managed_zones` |
| `src/libslic3r/Preset.cpp` | IMEX keys registered in printer and
process preset option lists |
| `src/libslic3r/PrintApply.cpp` | `physical_extruder_map` identity
fallback on IMEX printers |
| `src/libslic3r/Print.{cpp,hpp}` | IMEX slice-offset field + accessors;
`translate_to_print_space` augmentation for frame coherence |
| `src/libslic3r/GCode.{cpp,hpp}` | Mode G-code injection, per-tool PA
emission (firmware-agnostic), per-layer temperatures for all active
tools, `m_imex_parallel_mode` state, `set_gcode_offset_with_imex_shift`
writer/processor split |
| `src/libslic3r/GCodeWriter.{cpp,hpp}` |
`set_pressure_advance(tool_index = -1)` per-firmware implementation |
| `src/libslic3r/IMEXHelpers.{cpp,hpp}` | `imex_head_transform`
(Primary/Copy/Mirror), `parse_imex_active_tools`,
`imex_primary_tool_for_mode`, `effective_physical_extruder_map`,
per-head filament resolution, `group_imex_active_tools_by_gantry` (Span
pairing), `compute_imex_slice_offset` (firmware-managed) |
| `src/libslic3r/Format/bbs_3mf.cpp` | Per-plate IMEX mode and
`imex_head_filament_map` serialization |
| `src/slic3r/GUI/Tab.{cpp,hpp}` | `IMEXModesCtrl` in Multimaterial
page, tool assignment persistence across gantry count changes, `primary`
sentinel handling, Span tile state, firmware-managed-zones checkbox,
null guard in `clear_pages()` |
| `src/slic3r/GUI/PartPlate.{cpp,hpp}` | Zone visualization (Span
row-strip aggregation), placement violation detection, per-plate mode
icon, ghost volume rebuild on mode/map/object mutation, warning-badge
overlay, pre-slice warning collection, `refresh_imex_slice_offset()`,
ghost suppression in firmware-managed mode, headless-CLI guard |
| `src/slic3r/GUI/Plater.cpp` | Validation pathway injection, per-plate
mode popup, ghost click handling, pre-slice warning dialog with "Don't
show again", IMEX multimaterial conflict routing, per-slice IMEX offset
refresh in `update_background_process` |
| `src/slic3r/GUI/GLCanvas3D.cpp` | Ghost rendering with per-head
filament color and translucent blending, picking via volume composite
id, active-plate scoping |
| `src/slic3r/GUI/GCodeViewer.cpp` | Multi-carriage marker animation,
Mirror math fix, carriage footprint box offsets, legend annotation |
| `src/slic3r/GUI/IMEXFilamentPickerPopover.{cpp,hpp}` | Ghost-click
filament picker |
| `src/slic3r/GUI/Jobs/ArrangeJob.cpp` | Auto-arrange constrained to
IMEX primary zone + collision strip exclusion via `m_unselected` |
| `src/slic3r/GUI/OG_CustomCtrl.cpp` | Empty `option_set` guard for
widget-only lines |
| `resources/images/plate_imex_mode*.svg`, `imex_remove.svg` | Per-plate
mode icons (light/dark/hover variants), remove button |
| `tests/libslic3r/test_imex_helpers.cpp` | Coverage for IMEX head
transforms, `effective_physical_extruder_map`,
`imex_multicolor_block_reason` gating,
`group_imex_active_tools_by_gantry` Span pairing,
`compute_imex_slice_offset` |

---

## Testing Notes

Validated on an IQEX printer (4 carriages, 2×2 grid, Klipper firmware)
and against the Flashforge Creator Pro 2 profile (2-tool IDEX, RRF
flavor, center-origin bed) with the following configurations:

- **Primary only** — baseline, no regression vs. standard
single-extruder workflow
- **Copy mode** (T0 Primary, T1 Copy, same row) — carriage markers
animate in sync offset by strip width; ghost tracks drag/rotate/scale
- **Mirror mode** (T0 Primary, T1 Mirror, same row) — T1 marker reflects
T0 across zone-boundary plane; ghost drag reflects X correctly while Y
tracks 1:1
- **Cross-row copy + mirror** (T0 Primary row 0, T2 Copy row 1, T3
Mirror row 1) — T2 follows T0's zone-relative position; T3 mirrors T2's
X; Y shared per row
- **4-tool copy mode** (T0 Primary, T1/T2/T3 Copy) — zone sizing
correct, all four markers + ghosts render in sync
- **Paired-gantry multicolor (mc-mirror, Span)** — `0:P,1:S,2:M,3:M`:
ghost aggregates to one per non-primary gantry, X-flipped, drag tracks
1:1 with primary; collision strip audit doesn't fire spurious strips on
primary's right edge
- **Firmware-managed copy mode** (Flashforge Creator Pro 2) — emitted
gcode coordinates are centered at bed origin (X = −cube_half_width …
+cube_half_width), `first_layer_print_min/max` placeholders evaluate
symmetrically, M118 header produces correct "max delta from zero" values
for Flashforge's existing template, ghosts suppressed
- **Firmware-managed flag toggle** — flipping the checkbox off restores
slicer-managed iMEX behavior (toolpath at zone position)
byte-identically; flipping on restores center-origin slice
- **Delete active mode** — no crash; plate resets to Primary
- **Placement outside primary zone** — slicing blocked with error
notification
- **Multi-material conflict warning** — plate icon gets warning badge;
Yes/No dialog fires on slice; "Don't show again" checkbox persists
- **Multi-color block rule** — `0:P,1:C` on a single gantry blocks at
slice time with actionable error message
- **Ghost filament picker** — left-click on ghost opens picker;
selection writes to `imex_head_filament_map` and round-trips through 3MF
save/load
- **Per-plate mode selection** — left-click cycles modes, right-click
shows popup, undo/redo correctly reverts mode changes, mode persists
through project save/load
- **Gantry count change round-trip** — reducing from IQEX (4 carriages)
to IDEX (2 carriages) and back restores all previously assigned tool
roles
- **MMU/AFC `physical_extruder_map`** — explicit map routes PA and
temperature commands to correct physical extruder qualifiers;
auto-derive fallback preserves 1:1 behavior for non-MMU printers
- **Headless CLI slicing** — `orca-slicer --slice project.3mf` does not
segfault on IMEX-enabled printers
- **Dark mode** — all four plate-icon variants render correctly
- **Pre-slice warnings suppress + restore** — `app_config` flag flips on
checkbox; re-enable toggle in Multimaterial config restores dialog
- **Stock non-IMEX printer regression check** — slicing a single-head
Voron Trident 350 profile produces byte-identical gcode vs. baseline;
the firmware-managed flag and Span tile state both no-op when not
applicable
- **Full unit test suite** — 247/247 passing, including Layer 1 coverage
for `compute_imex_slice_offset`, `group_imex_active_tools_by_gantry`
Span pairing, and `imex_multicolor_block_reason` gating
2026-10-10 20:34:56 +08:00
Kris Austin cf5f7b778d Show OK instead of Yes on notice dialogs (#16332)
Several warning and info dialogs passed wxYES as their only button, so
the only choice was "Yes" even though nothing is asked. Use wxOK.

None of the callers act on wxID_YES. They ignore the result, except
Field.cpp, which only checks that it's nonzero, and every button id is.
wxYES_DEFAULT is 0, so dropping it next to wxOK changes nothing.

Also drop the empty `if (ShowModal() == wxID_YES) {}` in
Plater::priv::load_files.
2026-10-10 15:30:17 +03:00
SoftFever b14ed3e0c1 Multi nozzle UI Fixes / Improvements (#16075)
# New multi nozzle icon for guidance

new icon has tooltip for guidance
<img width="407" height="77" alt="Screenshot-20261005181353"
src="https://github.com/user-attachments/assets/f27bbf79-7570-4f30-ab3c-52fa972c9fa3"
/>

**Before**
<img width="414" height="228" alt="Screenshot-20261002144627"
src="https://github.com/user-attachments/assets/7941891a-5a27-478b-a63f-49cf386e430f"
/>

**After - Moved icon to pre label position**
• reduces possibility of wrapping of labels
• Aligns labels and improves readability

<img width="399" height="239" alt="Screenshot-20261005181511"
src="https://github.com/user-attachments/assets/93f6b555-bbec-496a-b708-8c12c3b3185b"
/>

# Fix > Modified state not applied on tabs
**Before - modified colors not applied to control**
<img width="751" height="157" alt="Screenshot-20261001004828"
src="https://github.com/user-attachments/assets/0c149fce-45e8-498f-8daa-7b54803399e9"
/>

**After**
<img width="752" height="150" alt="Screenshot-20261005181614"
src="https://github.com/user-attachments/assets/5a930bb2-6487-4987-b58d-850de6ae6779"
/>

Applied color as border because changing text color was not readable
<img width="159" height="71" alt="Screenshot-20261002152522"
src="https://github.com/user-attachments/assets/f694fc8d-c9f6-46d7-9e06-7762003c9f20"
/>

and applying as background color was too eye catchy
<img width="184" height="80" alt="Screenshot-20261002152533"
src="https://github.com/user-attachments/assets/2b67c1dd-8977-46ea-88fc-ce57bfad55bd"
/>

# Usable tabs on Printer settings
• Fix > Using long string makes "Motion ability" and "Notes" tab
unreachable
• Matches T1,T2.. terminology with nozzle section
• Makes https://github.com/OrcaSlicer/OrcaSlicer/pull/14737 available to
use now
 
**Before**
<img width="751" height="126" alt="Screenshot-20261002152643"
src="https://github.com/user-attachments/assets/0241d3c4-acc1-491e-ab85-fa60385c5e85"
/>

**After**
<img width="748" height="129" alt="Screenshot-20261002152628"
src="https://github.com/user-attachments/assets/c475d6c8-4f0f-47df-aa04-9d894b1c358d"
/>

No issue with 8 items
<img width="748" height="124" alt="Screenshot-20261002153018"
src="https://github.com/user-attachments/assets/9a8c4e12-2cdc-4508-b82b-134476589d80"
/>

# Dynamic Padding
• buttons uses dynamic horizontal padding now depends on item count.
reduces padding size to reduce scrolling

**Before**
<img width="416" height="91" alt="Screenshot-20261002153309"
src="https://github.com/user-attachments/assets/977b22db-e686-4967-95be-13772d77cefd"
/>

**After** 
<img width="403" height="79" alt="Screenshot-20261005181728"
src="https://github.com/user-attachments/assets/9aac0d17-0729-408f-9237-62b17236488f"
/>

**Before**
<img width="745" height="153" alt="Screenshot-20261002153659"
src="https://github.com/user-attachments/assets/06951759-419a-40fb-8fc2-d93f5b04d6ea"
/>

**After** 
<img width="748" height="149" alt="Screenshot-20261005181833"
src="https://github.com/user-attachments/assets/9ccec344-1dd0-4b03-ac12-20d298f413c8"
/>

**After** 
<img width="756" height="178" alt="Screenshot-20261009144637"
src="https://github.com/user-attachments/assets/1343c39e-5cbd-4f9e-b501-df3797579961"
/>
<img width="761" height="198" alt="Screenshot-20261009144459"
src="https://github.com/user-attachments/assets/efb1ef75-a602-4d3a-a17f-9354ed633357"
/>
<img width="758" height="196" alt="Screenshot-20261009144510"
src="https://github.com/user-attachments/assets/df1d88c5-1b6f-4748-a6bd-a96659a83fe0"
/>
<img width="764" height="190" alt="Screenshot-20261009154210"
src="https://github.com/user-attachments/assets/b876ed71-5fa0-47e4-83c8-a33100e0a229"
/>

# Keeping compact look & Making it more readable
• Removed ":" from labels to gain space as much as possible and
increased font size a bit but UI didnt allowed bigger font

• Matched font sizes of nozzle section while keeping height of items.
Previous font was too small. removed "mm" text to gain more space but
added it in tooltip
**Before-After** 
<img width="800" height="240" alt="Screenshot-20261002154132"
src="https://github.com/user-attachments/assets/bc4ddcb1-2248-4fc7-9492-ab4fb68cb659"
/>

• Matched font sizes for BBL nozzle selectors
**Before-After** 
<img width="806" height="200" alt="Screenshot-20261002154513"
src="https://github.com/user-attachments/assets/b3d38317-8e4d-4c1e-a490-ce4959ed700a"
/>

<img width="794" height="156" alt="Screenshot-20261002154649"
src="https://github.com/user-attachments/assets/00917584-8e1a-44b0-8c0b-79cec30059bf"
/>

# Layout and Styling fixes
• Matched background color of non active buttons with (Global/Objects)
switch
**Before**
<img width="405" height="159" alt="Screenshot-20261002154947"
src="https://github.com/user-attachments/assets/8a8adcc8-431a-48be-aabd-a981335b6ab9"
/>

**After** 
<img width="407" height="159" alt="Screenshot-20261005182106"
src="https://github.com/user-attachments/assets/7f837be3-ce85-49e6-909a-05350b3d8b30"
/>

# Other effected dialogs
<img width="541" height="195" alt="Screenshot-20261002165003"
src="https://github.com/user-attachments/assets/109abbc3-d7c0-4384-9b85-d943e2027963"
/>
<img width="547" height="308" alt="Screenshot-20261002165307"
src="https://github.com/user-attachments/assets/951d9a8d-56f9-4659-aebe-77f236e8c838"
/>


<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-10-10 20:27:37 +08:00
packerlschupfer d59155b27f Moonraker: pass print=true in upload — fix Upload & Print race (fixes #14945) (#15032)
* Moonraker: pass print=true in upload — fixes Upload & Print race with power-on-upload

Closes #14945.

Upload & Print on the Moonraker (Klipper) host type failed with
HTTP 503 "Klippy Host not connected" on any printer that Moonraker
powers up in response to an upload (the [power] on_when_upload_queued
feature). The file landed on disk, the print never started, and the
user hit an error dialog.

Root cause: after POST /server/files/upload succeeds we immediately
fire POST /printer/print/start. On a cold printer that Moonraker just
powered up, Klippy is still coming up when /printer/print/start
arrives, so Moonraker returns 503.

Fix: add `print=true` to the upload multipart form. Moonraker's own
upload endpoint queues the print inside the upload response — and
when a [power] device with on_when_upload_queued is configured, it
powers the printer on and waits for Klippy READY before starting.
That's the whole point of the power-on-upload feature; our second
POST was defeating it.

Also read `result.print_started` from the upload response — when
true, skip our explicit /printer/print/start (Moonraker handled it);
when false (older Moonraker or buddy-fork that ignores the print
flag), fall back to the explicit call so the existing behaviour is
preserved for those servers.

Reporter and root-cause identification: @RubenOllesch.

(cherry picked from commit bd442155ba)

* Moonraker: treat print_queued as Moonraker owning the print

Reading only result.print_started missed the exact case this PR set out to
fix. Moonraker's upload response carries two flags:

    print_started : it began the print immediately
    print_queued  : it accepted the job but has not started it yet

The power-on path (`[power] on_when_upload_queued`) is the second one:
Moonraker queues the job, powers the printer up and waits for Klippy to
report READY, so it answers print_started=false, print_queued=true.

With only print_started read, moonraker_started_print stayed false, the
fallback fired, and our explicit /printer/print/start hit the same not-ready
Klippy that produced the original 503 — i.e. the fix did not fix #14945 for
the configuration that reported it.

Verified the response schema against Moonraker v0.11.0 (API 1.5.0); an
upload with print=true on a ready printer returns:

    {"action": "create_file", "item": {...},
     "print_started": true, "print_queued": false}

Both fields are present, so reading print_queued is safe on this version and
the `false` default keeps older hosts on the existing fallback path.

Caught by @raistlin7447 in review of #15032; the fix is their suggestion.

(cherry picked from commit 43e8eff003)

* Moonraker: read the upload reply's fields at top level

Moonraker's FileUploadHandler writes the upload result straight to the
response instead of wrapping it in {"result": ...} like the endpoints
registered through register_endpoint. item.path, print_started and
print_queued are therefore top-level keys. Reading them under result.
silently fell back to the local filename and to "not started", so the
explicit /printer/print/start still ran after every upload.

Also correct the comments on when Moonraker queues a job instead of
starting it, and on what it renames on upload.
2026-10-10 09:27:05 -03:00
SoftFever 640dfad6e9 List IDEX/IQEX parallel printing in the README's main features 2026-10-10 19:11:57 +08:00
SoftFever 0a9e7f33e1 Link the IDEX/IQEX settings to their wiki sections 2026-10-10 19:09:14 +08:00
SoftFever 74493429fd Name extruders in the unsaved changes dialog like the printer tabs 2026-10-10 15:02:16 +08:00
Clifford GarwoodandClaude Opus 5.5 2ede6d1631 Arrange every IDEX/IQEX plate inside its own primary zone
Arrange read the zones of the selected plate only and applied them to
every bed it packed. Arranging all plates with a plate in Primary
selected spread a parallel plate's parts across its whole bed, and with
a parallel plate selected, a Primary plate's parts were squeezed into
the zone. Arranging a single plate other than the first also ignored its
collision strips: they were tagged with the plate's index, while the
plate packs into the arranger's bed 0.

The IDEX/IQEX constraints now live in ImexArranger (IMEXArrange), and
ArrangeJob only snapshots each plate's zones on the main thread and
calls it:
- When every bed, plates the arrange may add included, has the same
  primary zone, the bed shape is that zone, so parts stay centered in it.
- Otherwise each plate in a parallel mode fences off the rest of its bed
  with fixed items on its own bed, and its parts are then arranged again
  inside the zone, so they sit centered rather than piled against the
  zone edge nearest the bed's center. Plates in Primary keep the whole
  bed, and plates the arrange adds take the process preset's mode.
- Zone edges inside the bed get the bed's own edge margin, and strips
  also keep room for the brim.
- A part too big for its zone, which libnest2d's first-fit retry places
  across the fixed items, is left unarranged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 01:44:42 -04:00
Clifford GarwoodandClaude Opus 5.5 8b5280098f Merge upstream main: center of mass markers, adaptive TPMS, texture bake color mixing, post-processing preview fix, GLEW/OpenCSG removal
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 01:44:42 -04:00
SoftFever e4142db820 Let texture bakes create a chosen number of mixed colours (#16320)
# Description

Follow-up to #16242, which turned texture colour mixes into mixed
filament slots but created a slot for every possible mix, even when only
previewing. A new Mixed colors setting (default 8) caps how many mixed
filaments a bake adds. They are picked from the texture's own colours,
and only the ones the bake actually paints with are created. Previews no
longer touch the project's filaments, both previews show a mix in the
colour its slot will have, and the bake paints each mix with the slot it
got, which went wrong whenever the project already held mixed slots.

It also improves the colour preview: the green paint highlight no longer
covers the colours, and the colours a bake writes stay visible in the
gizmo.

Only texture displacement changes. Models without a colour layer behave
as before, and there is no change to project or profile formats.

# Screenshots/Recordings/Graphs

**Before**



https://github.com/user-attachments/assets/554a0d0d-cd9f-4095-840e-ca764dde6d52

**After**



https://github.com/user-attachments/assets/7fe10431-8c3f-4759-bd3f-78b4f7a12fc7



## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-10-10 12:00:39 +08:00
SoftFever d629922060 Keep the paint highlight off every texture preview between strokes 2026-10-10 11:44:09 +08:00
Kris Austin 4a20168742 Fix loading a multi-toolhead 3MF that has no filament_self_index (#16331) 2026-10-09 22:07:33 -03:00
Kris Austin 1466c0e57f Fix rare hang in mcut mesh booleans (#16330) 2026-10-09 21:55:06 -03:00
yw4z c23ea82ad1 revert extruder term on printer settings and optimize dynamic paddings 2026-10-10 01:17:33 +03:00
Kris Austin cd02116242 ci: replace the Docker tag action with git commands (#16327) 2026-10-09 19:17:16 -03:00
Kris Austin 35bac4cb69 Remove unused OpenCSG, GLEW and GLU dependencies (#16318) 2026-10-09 16:50:19 -03:00
0f3e8fbf27 Add center of mass markers to Prepare and Preview (#16291)
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
Co-authored-by: Kris Austin <kris.austin@gmail.com>
2026-10-09 16:48:48 -03:00
TheLegendTubaGuy 93c6ab93dc Merge branch 'main' into fix/16194-bbl-infill-retraction 2026-10-09 13:57:18 -05:00
SoftFever 901b14a723 Merge branch 'main' into feature/texture-color-number 2026-10-10 02:46:28 +08:00
SoftFever bef49a330b Keep extruder switch button padding after a DPI change 2026-10-10 02:45:18 +08:00
SoftFever 3755618146 Keep nozzle row label fonts after a DPI change 2026-10-10 02:32:18 +08:00
SoftFever ebaff19754 Merge branch 'main' into extruder-variants-ui-2 2026-10-10 00:42:48 +08:00
Misterff1 b5ef24e7ff Fix regression: Arc Fitting setting for BBL P2S (#16319)
Disable Arc Fitting for BBL P2S
2026-10-10 00:32:46 +08:00
Ian Chua 8585eae816 fix: hide symbols of the bundled static openssl (#16317)
* fix: hide symbols of the bundled static openssl

* fix: hide the bundled static OpenSSL symbols on Linux

* fix: relink _ssl/_hashlib when OpenSSL recipe changes
2026-10-10 00:26:02 +08:00
Lam Wei Lun e72ace164b feat(speed-dial): add plugin page capabilities as actions
Plugin Pages capabilities (top-level notebook tabs) were missing from the Speed Dial because ActionRegistry only ingested Script capabilities.

Enumerate and subscribe to Pages as well. Launching a page action switches the notebook to that page, swapping it into the visible slot first when it lives behind the overflow dropdown.
2026-10-09 23:44:52 +08:00
Ian BassiandRodrigo Faselli b5f5b50157 Adaptive TPMS (#16005)
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-10-09 12:36:09 -03:00
e0b35f9ec9 Fix garbled G-code preview when a post-processing script is used (#15005)
* Rebuild the G-code line offsets after post-processing scripts run in place

* Clamp the G-code window reads to the mapped file size

* Add tests for rebuilding the G-code line offsets

* Include <mutex>, <ios> and boost/filesystem/operations.hpp where they are used

* Keep the preview's G-code lines and highlight in step with post-processing scripts

---------

Co-authored-by: SoftFever <103989404+SoftFever@users.noreply.github.com>
Co-authored-by: SoftFever <softfeverever@gmail.com>
2026-10-09 23:08:43 +08:00
SoftFever b8665b69b0 Merge branch 'main' into feature/texture-color-number 2026-10-09 22:58:10 +08:00
SoftFever 7d141bd691 Keep the texture displacement gizmo out of the assemble view 2026-10-09 22:26:19 +08:00
SoftFever 10787dd59d Show the model's painted colours in the texture displacement gizmo 2026-10-09 22:25:36 +08:00
SoftFever 1fb5da4148 Keep the paint highlight off the texture colour preview between strokes
The green highlight and tint no longer cover a colour preview, and return while a stroke is
painted. The Fast view also keeps the other parts of a multi-part object.
2026-10-09 22:24:40 +08:00
SoftFever 0473da4ef8 Let texture bakes create a chosen number of mixed colours
The new Mixed colors setting caps how many mixed filaments a bake adds. They are picked from the
texture's colours, and only the ones the bake paints with are created. Previewing no longer creates
filament slots, both previews show a mix in its slot's colour, and the bake paints each mix with the
slot it actually got.
2026-10-09 22:23:07 +08:00
Ian Chua eb28daf0fe fix: persist user id so that setting_id doesn't get wiped (#16246) 2026-10-09 21:00:27 +08:00
yw4z 96a1877447 optimize dynamic padding for printer settings dialog 2026-10-09 14:44:55 +03:00
Clifford GarwoodandClaude Opus 5.5 f970a607ae Merge upstream main: belt printing, texture displacement color mixing, 3MF component cycle checks, undo blocked during background jobs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 07:44:12 -04:00
ExPikaPaka 6ac7ae24a3 Block undo and redo while a background job runs (#16015)
* Block undo and redo while a background job runs

A job is queued against the model as it stands and hands its result back
when it finishes, so undoing underneath it leaves that result landing on
geometry it was never computed for. Undo and redo now wait for the job
and say so, and can_undo()/can_redo() report the same, so the toolbar
and the menu items stay in step.

* Say what to do about the running operation, not just that it blocks

Review feedback: "Stop it first" is the only way out the old text offered,
and stopping is rarely what the user wants. Waiting for the operation to
finish works just as well, so the notification now names both.
2026-10-09 19:26:37 +08:00
ExPikaPaka d55e32fed4 Stop reading a missing AMF metadata type as a string (#16060)
A `<metadata>` element without a `type` attribute makes
`get_attribute()` return nullptr, which was then assigned to a
`std::string` and read as a C string.

Check it the way the sibling metadata handler already does, and stop the
parse.

Regression test in `tests/libslic3r/test_amf.cpp`, a new file: the suite
had no AMF test at all.
2026-10-09 19:25:53 +08:00
SoftFever cd0b529e31 Reject 3MF component references that form a cycle (#16059) 2026-10-09 19:23:20 +08:00
yw4z 81e523e8fb fix m_variant_combo expands vertically 2026-10-09 14:21:52 +03:00
yw4z 86a6841a06 optimize dynamic padding 2026-10-09 14:21:27 +03:00
SoftFever a124b7d2df Reject cyclic 3MF components without running out of memory, however many there are 2026-10-09 19:15:24 +08:00
yw4z f2d3e14f2b fix scaling of multi_extruder and extruder_sync icons on top sizer 2026-10-09 13:59:10 +03:00
yw4z d496ebfd69 Update OG_CustomCtrl.cpp 2026-10-09 13:38:05 +03:00
yw4z b83e4d2890 fix build 2026-10-09 13:33:28 +03:00
yw4z 1b310c1a6d macos placement fix 2026-10-09 13:27:39 +03:00
SoftFever 3d2b219d6a Merge branch 'main' into fix/3mf-component-cycle 2026-10-09 18:14:00 +08:00
ExPikaPaka 62b829d1cb Texture displacement: mix filament colours in the slicer (#16242) 2026-10-09 17:52:35 +08:00
SoftFever 1bddcf3ab6 Retire the separate belt-printer nightly builds (#16308)
# Description

Belt printer support is now part of `main` (#14394), so the separate
`_belt` nightly builds from the `belt-printer` branch are no longer
needed. Nightly builds are published from `main` only again, under their
usual names. The README drops its section on the parallel belt builds
and lists belt printer support among the main features, crediting its
main author, Joseph Robertson (@HarrierPigeon). The nightly release page
has already been updated to match.

No change to slicing output or to main's nightly asset names — CI and
README only.

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-10-09 16:25:31 +08:00
SoftFever 18b9b0840f List belt printer support among the README's main features 2026-10-09 16:23:53 +08:00
SoftFever 6026100f83 Stop publishing separate belt-printer nightly builds 2026-10-09 16:23:53 +08:00
TheLegendTubaGuy a3005a96ac Merge branch 'main' into fix/16194-bbl-infill-retraction 2026-10-09 02:33:14 -05:00
SoftFever 489e337bad Merge Belt Printing Into Upstream (#14394)
# Belt printing for OrcaSlicer

This merges the `belt-printer` branch into `main`. It adds support for
conveyor-belt ("infinite Z") printers like the BabyBelt Pro, the
IdeaFormer IR3 V2 and the CR-30 family, along with the things that had
to grow around it: tilted-plane slicing, supports that terminate on the
belt, belt-aware brims, a purge tower that works without a flat bed,
multi-colour belt prints, a preview that shows the part the way it was
designed, and starter profiles.

It has been a long road (the first attempt was #12733 back in March, the
pipeline has been rebuilt twice since) and a lot of people have put work
into it. Credits are at the bottom; please tell me if I've missed
anyone.

Closes #2628, closes #11344, closes #6885, closes #9004, closes #14188.

---

## How belt printing works

Slicers assume the bed sits on the XY plane and layers stack along Z. A
belt printer breaks that in two ways at once: the belt is tilted
(usually 45°) and the axis that advances the belt is the printer's Z, so
the "bed" lives on the XZ plane and is, in principle, infinitely long.

<img width="6360" height="2702" alt="A flat bed vs a tilted belt"
src="https://github.com/user-attachments/assets/3f5542f2-6dab-42bf-9233-f96d863b40c8"
/>

Rather than teach every part of the slicer about tilted beds, the branch
leaves the slicing engine alone and transforms what goes in and what
comes out. The pipeline has five steps.

![printer
pipeline](https://github.com/user-attachments/assets/4d2f9965-f768-4b22-afbe-b3fda05354c4)

<details>
<summary><b>The five steps in detail</b></summary>

### 1. Pre-slice rotation
The mesh is rotated by the belt angle before slicing
(`BeltSliceStrategy`, `BeltTransform`), so that the ordinary horizontal
slicer produces layers that are actually tilted planes relative to the
part. Rotation is about X by default (belt along Y); Y rotation and a Z
lift are supported too. A "global" mode rotates every object about one
common origin, which is what keeps several parts on one belt consistent
with each other.

### 2. Slice
Nothing special. The mesh is rotated, the slicer does what it always
does. This is the reason most of Orca's features (walls, infill, seams,
ironing, painting, …) just work on a belt without belt-specific code.

### 3. Supports
Supports are generated after slicing against a virtual floor: the belt
surface, expressed in the rotated frame (`BeltFloorContext`). Normal,
tree and organic supports all terminate on that plane instead of on Z=0,
and nothing may be generated below it. Painted supports and seams are
transformed with the same `trafo_sliced()` as the layers.

![adding
supports](https://github.com/user-attachments/assets/39c1a858-23e2-48ee-8eb3-7b1abb9be172)

### 4. G-code back-transform
The G-code is rotated back into the model's Cartesian frame
(`BeltBackTransform`). A nice side effect: with step 5 switched off you
can slice at a non-standard angle and print the result on a normal
printer. The first belt-sliced Benchy was printed exactly that way, on a
Sovol SV08.

### 5. Machine frame
The printer doesn't know its bed is tilted. To make straight walls come
out straight, the axes are remapped (the default mapping is X → reversed
X, Y → Z, Z → Y) and the result is sheared and scaled:

```math
\begin{bmatrix} X \\ Y \\ Z \end{bmatrix}
\longrightarrow
\begin{bmatrix} X \\ \dfrac{Y}{\cos\alpha} \\ Z + Y\cdot\tan\alpha \end{bmatrix}
```

This lives in `GCodeWriter` behind a small `MachineKinematics` strategy
(`BeltKinematics` on belts, identity otherwise), so the writer itself
has one code path. The slicing angle and the machine angle can differ if
you want to, e.g. slice at 30° on a 45° machine — mind your nozzle
clearance if you do.

</details>

A short recording of the back-transform from the original PR:


https://github.com/user-attachments/assets/cdb9cc83-711d-48b7-9d86-a014a32c5e8e

---

## What had to change to make it work

Belt mode is gated on the `belt_printer` printer setting; with it off,
every code path below is the old one.

<details>
<summary><b>Slicing and geometry</b></summary>

- `PrintObjectSlice` / `PrintObject`: the rotation, Z lift and
per-object layer-grid shift, plus the invalidation that goes with them.
Modifiers and painted volumes are transformed with the same matrix as
the model.
- `FirstLayerPlane`: "the first layer" on a belt is a band along the
belt, not the first slicing layer. First-layer speed, line width and the
fan band are measured against it.
- `Print::validate`: clearance checks against the gantry instead of the
printable height; skirt, raft, draft shield, the classic prime tower,
arc fitting, spiral vase with brim, and scarf-joint seams are refused or
disabled on belts because each of them either doesn't exist on a belt or
moves the belt the wrong way (a scarf seam starts one layer low, which
on a belt is a 0.28 mm back-step into the previous layer at every seam).
- Z-hop defaults to 0 on belt profiles; a lift on a belt is a belt move.

</details>

<details>
<summary><b>Supports</b></summary>

- `SupportMaterial`, `TreeSupport`, `TreeSupport3D`, `TreeModelVolumes`:
a shared `BeltFloorContext` provides the belt plane; supports clip to
it, extension layers are numbered sequentially, and the first-layer
flange that used to be stamped under everything is gone.
- `build_plate_tilt_x/y` (from #12733) is now derived from the slicing
rotation in `Print::apply`, so GUI and CLI agree; it's capped below 90°.
- Organic supports that reach the belt no longer produce negative flow.

</details>

<details>
<summary><b>Brims (#15155)</b></summary>

A belt first layer is effectively a single line of contact, so a brim
matters more than usual. `BeltBrim` generates per-layer bands along the
belt plus an "apron" ahead of the part, in the object's brim filament.
Two belt-specific settings came with it: **Leading edge brim length**
(more lines on the side printed first) and **Extra brim width** (across
the belt), plus a **Leading edge only** brim type.

<img width="1849" height="1043" alt="belt brim"
src="https://github.com/user-attachments/assets/f963ed8e-53e7-48f8-a495-123cb9ae27f7"
/>

</details>

<details>
<summary><b>Multi-colour: the belt purge tower</b></summary>

The classic wipe tower can't exist on a belt (its G-code bypasses the
transform and it needs a flat bed to stand on). Instead the GUI
generates a long, thin "purge prism" beside the parts, flush with the
far edge of the belt, one per plate (`BeltPurgeTower.cpp`). It is a real
model object, so it is sliced like everything else, and
`Print::_plan_belt_purge` routes every filament change into it via
`flush_into_objects`. It's sized from the flush matrix, split into one
island per simultaneous tool change, snapped onto the parts' layer grid,
cut off after the last colour change and stripped of infill no change
claimed, so what prints is a good deal smaller than the model you see in
Prepare.

</details>

<details>
<summary><b>G-code generation and cooling</b></summary>

- `GCodeWriter` + `MachineKinematics`/`BeltKinematics`: the
back-transform, axis remap, shear and scale, lifts that are belt moves,
and the first-layer travel speed.
- `GCode.cpp` / `BeltGCode`: a belt header (slicing rotation, remaps,
machine tilt) that the preview reads back; it is written outside the
optional header block so printers with a BTT TFT thumbnail still get it.
Exclude-object outlines are emitted in the plate frame.
- `CoolingBuffer`: the "first layers" the fan stays off for are a band
above the belt, marked per extrusion segment by the generator
(`;_BELT_BAND_START/END`) and honoured on every layer.
- `GCodeProcessor`: belt header parsing, start-G-code Z handling, and
height checks that don't compare belt travel against the printable
height.
- `ToolOrdering` / `BeltPurge`: filament changes are detected by
scanning the ordering (an apron layer never carries the first-layer
flag).

</details>

<details>
<summary><b>GUI</b></summary>

- Printer settings tab: the belt group (slicing rotation, angle, global
mode, infinite Y, purge tower, floor settings). The axis remap and
pre-slice remap options are Develop-mode only.
- `ConfigManipulation`: everything that doesn't apply on a belt is
greyed out (skirt, raft, draft shield, the wipe tower group, scarf
seams, …).
- Preview: a "designed view" that back-transforms the toolpaths onto the
model so you see the part upright, with `B` toggling the raw
machine-frame G-code (`GCodeViewer`, `Shortcuts`). The tilt comes from
the belt header, so imported G-code behaves.
- Arrange: parts are packed from the end of the belt that prints first,
colours are grouped into runs so each filament change happens once, the
purge tower's strip and the brim width are reserved, and piles aimed at
an off-centre `best_object_pos` are clamped to the bed (`Arrange.cpp`,
`ArrangeJob.cpp`, one clamp in `libnest2d`).
- `PartPlate`: plate icons stay in the gap between plates on a long,
narrow bed; the plate is open along Y for containment tests on an
infinite-Y belt.
- Calibration: a belt temperature tower (overhang variant) that slices
correctly on a tilt.
- The old tilted-bed rendering in Prepare was dormant and has been
removed; the bed is shown as the slicing pipeline treats it.

</details>

<details>
<summary><b>Config options</b></summary>

Printer: `belt_printer`, `belt_printer_infinite_y`,
`belt_slice_rotation`, `belt_slice_rotation_angle`,
`belt_slice_rotation_global`, `belt_preslice_global`,
`belt_frame_tilt_decouple`, `belt_frame_tilt_angle`,
`belt_support_floor_mode`, `belt_support_floor_offset`,
`belt_support_z_offset_mode`, `enable_belt_purge_tower`,
`first_layer_plane`, `first_layer_plane_offset`,
`first_layer_plane_thickness`, `build_plate_tilt_x/y`,
`gcode_back_transform`, `gcode_remap_x/y/z`, `preslice_remap_x/y/z`,
`preslice_remap_global`.
Process: `belt_purge_tower_width`, `leading_brim_length`,
`extra_brim_width`, brim type `leading_edge_only`. Object:
`belt_purge_tower_object`.

All of them have defaults that leave non-belt printers untouched, and
old `belt_support_floor_mode` values map to `none`.

</details>

<details>
<summary><b>Tests</b></summary>

`tests/libslic3r/test_belt_brim.cpp` and `test_arrange.cpp`, and belt
cases in `fff_print` (`test_print.cpp`, `test_skirt_brim.cpp`,
`test_gcodewriter.cpp`, `test_gcode_processor.cpp`): scarf gate, fan
band, gantry clearance, organic supports on the belt, brim with and
without the purge tower, apron widths, machine mapping at non-45°
angles, first-travel lift, start-G-code Z, arrange clamp and colour
grouping. All three suites pass on Linux, and the tree-wide profile
check passes.

</details>

---

## Starter profiles

Three vendors ship belt profiles. Belt mode needs the printer settings
at **Advanced** or above to show its group.

<img alt="advanced mode"
src="https://github.com/user-attachments/assets/7a519ce5-b3b5-400c-a914-4f208bb577b0"
/>

| Printer | Vendor bundle | Nozzles | Processes | Filaments |
|---|---|---|---|---|
| **Generic Belt Printer** (`MyBeltPrinter`) | Custom | 0.2, 0.4, 0.6,
0.8 | 0.20mm Standard, 0.12mm Fine | library |
| **BabyBelt Pro** (Printcepts) | Printcepts | 0.4 | 0.20mm Standard |
Generic PLA, Generic PETG, eSUN PLA |
| **IdeaFormer IR3 V2** | IdeaFormer | 0.4 | 0.20mm Standard | Generic
PLA, Generic PETG, eSUN PLA |

To set up a printer that isn't listed: pick **Generic Belt Printer**,
set the belt width and length, save the profile, then copy in your
machine's start/end G-code and limits and tune from there.

<img alt="generic belt printer"
src="https://github.com/user-attachments/assets/90134d55-d6fe-4dba-8695-5ea44e78ec2b"
/>

<details>
<summary>BabyBelt Pro</summary>
<img width="2467" height="1392" alt="BabyBelt Pro"
src="https://github.com/user-attachments/assets/2b448cee-339e-43c9-964b-1ee9044044c7"
/>
</details>

---

## Contributions

### @HarrierPigeon
***Majority of design & implementation***

I did most of the work here by myself with AI tools (primarily Claude,
some Codex.)
PRs #12733, #12998, #14385, #15155, #15361, #15156, #15526, #16127

### The @Unlayered3D Team
***Rotation-Mode Pipeline***

The initial version of this sheared the model in the pre-slice pipeline.
Talking with them convinced me to switch to the current
rotate->slice->unrotate-> remap & shear method, which had significant
immediate improvements. Working with them has been a blast.


### @tommasobbianchi
***IdeaFormer IR3V2 Profile, eSUN PLA Tuning, G-Code Render / Preview***

TommyB came in at the perfect time to help keep me motivated and
contributed several things I hadn't had the werewithal to implemement
yet. Without their contributions and encouragement, we wouldn't be here
yet.


### The BabyBelt Community

**BabyBelt Pro** — @rexit1982 for the profile, and @RobMink of
Printcepts for the printer and a lot of patient testing.

**Field reports** — Many members of the BabyBelt community helped,
testing on their machines, providing G-Code and examples of issues, and
encouraging me to keep working on it. Among them:
- @RobMink - creator of the BabyBelt
- @rexit1982 - initial BabyBelt Pro profile, bug hunter
- @shubhracc - found a *lot* of technical bugs
- @NeoDLC - bug hunter
and BabyBelt Discord members who found bugs & gave feedback in no
particular order:
- @horatio42 - also provided a build machine while mine was down
- @HotCubCar - requested belt printer brim support
- Swap_File
- @Nyctelios
- Sup
- @matschi140
- @Rise-Run
- @shooby-dooby

### OrcaSlicer Maintainers
**Generic belt printer** — @SoftFever
**Keeping on top of upstream** @RF47 & @HanifKoh
**Review and fixes** — @HanifKoh (#15685 and the review on this PR).
Among them: plates after the first printed off the bed on belts; painted
supports and seams ignored the belt transform; support generation failed
at a 90° tilt; the object table crashed on the Support column on every
printer because tilt keys were in the per-object tables; apron brims
printed in the wrong filament; `build_plate_tilt` went stale outside the
GUI; the CLI reserved a wipe tower on belts; every G-code file was
treated as belt G-code because the config block carries the angle; tests
didn't compile on Clang/MSVC; plus a long list of smaller clean-ups and
the review questions that led to the clearance check, the header length
fix, the retired floor modes and the removal of the diagnostic logging.

### Additional Thanks

A special thanks to LDO Motors, who provided equipment for validating
multicolor, and my wife, who not only put up with with this obsession,
and the addition of three belt printers to our home, but has encouraged
me to keep going ever since I started this project six months ago.

---

## Known Issues

- The purge prism's first tool is chosen by the shared `ToolOrdering`
logic; on some layouts the print opens on the wrong filament and makes
one extra change at the thin tip of the prism (the "cannot absorb the
full purge volume" warning at a low height).
- Colour grouping in arrange is a soft cost: when the belt is too short
for clean runs, colours overlap rather than spill onto another plate.
- Three OrcaFilamentLibrary filaments still carry `filament_z_hop` 0.4;
belt profiles override it to 0.

Also: slicing at an angle other than the machine's (decoupled frame
tilt) is supported but not something the starter profiles exercise.
2026-10-09 15:24:47 +08:00
Ian Chua 86ff2a9de7 fix: avoid substring denies in audit path keywords (#16243)
## Summary

   Fixes #15944.

   The plugin audit deny-list matched `secret`, `cert`, and
 `conf` as substrings of every path component. This blocked
 valid imports during plugin capability execution, for example
 `numpy/__config__.py`, because `conf` appeared inside the
 module filename.

   This PR changes deny keyword matching to use whole path
 components instead of substring matches. It keeps the
 intended protections for sensitive locations and config
 files, while allowing dependency and stdlib modules whose
 names merely contain those strings.

   ## Changes

   - Match denied path keywords as whole components instead of
 substrings.
   - Keep denying sensitive directory names such as:
     - `secret`
     - `secrets`
     - `cert`
     - `certs`
     - `certificate`
     - `certificates`
     - `conf`
     - `config`
   - Keep denying config files by extension:
     - `.conf`
     - `.ini`
   - Allow legitimate Python module/package paths such as:
     - `numpy/__config__.py`
     - `numpy/_core/_ufunc_config.py`
     - `configparser.py`
     - `sysconfig.py`
     - `logging/config.py`
     - `certifi/cacert.pem`
   - Include the denied target and reason in `PermissionError`
 messages when the audit hook blocks an operation.
   - Remove an unused `<memory>` include from
 `PluginAuditManager.hpp`.

   ## Why

   The previous substring matching caused false positives for
 common dependency and standard-library paths. It also made
 failures hard to diagnose because the Python exception did
 not include the refused path.

   The new behavior is narrower: it blocks sensitive path
 components and config file extensions without treating
 unrelated names like `__config__.py`, `configparser.py`,
 `Conference`, or `Concert` as secrets.

   ## Testing

   - Added/updated unit coverage in
 `tests/slic3rutils/test_plugin_audit.cpp` for:
     - whole-component keyword matches
     - `.conf` / `.ini` blocking
     - case-insensitive matching
     - false-positive paths from #15944

Plugin used for testing:

[orca_audit_numpy_config_repro.py](https://github.com/user-attachments/files/33143848/orca_audit_numpy_config_repro.py)
2026-10-09 15:20:38 +08:00
ExPikaPaka 39e6249e5b Include <tuple> in bbs_3mf 2026-10-09 09:12:44 +02:00
ExPikaPaka b3f4ea62a8 Merge remote-tracking branch 'origin/main' into fix/3mf-component-cycle
# Conflicts:
#	tests/libslic3r/test_3mf.cpp
2026-10-09 08:57:16 +02:00
ExPikaPaka 6d616c288a Texture displacement: speed up the connected-net layout 3x (#16149)
Make the connected-net layout affordable on a dense patch

Laying a patch out as a connected net cost ~175 ms on a 42k-triangle patch,
against ~21 ms for the unwrap it works from, and the gizmo asks for it on every
preview, overlay and bake. Measured on a real project the grid behind it ran
~19 million triangle-pair tests per net, nearly all of them misses: a cell
holds every triangle whose box touches it, and a candidate really meets a
couple of them.

Keep a bounding box with each stored triangle and answer those misses with four
comparisons instead of a full intersection. The net drops to ~53 ms with
identical output - the seam metrics on the test project did not move by one.

Two further attempts were measured and dropped, and are recorded in the comment
so they are not tried again: a free-space pre-check per chart came out slower,
because a folded chart lands against the net by construction and the cells
under it are occupied anyway, and splitting the boxes into their own array for
locality lost more to growing two vectors per bucket than it gained.

Also pick a pair's fold line from the longest boundary they share rather than
whichever edge came first, and grow the net strongest-adjacency-first rather
than breadth-first by area. Only the fold a chart is reached by comes out
matching, so a chart claimed across a short boundary leaves the long one it
shared with its true neighbour torn.

texture_unwrap_dump reports an unwrap from a saved project - charts, their
topology, folded triangles, where the texture is discontinuous and how long
those seams are. All of the above was found with it, and it is what keeps a
claim about this code honest; reading the 3D view and guessing had produced
three wrong diagnoses in a row.
2026-10-09 14:23:23 +08:00
Clifford GarwoodandClaude Opus 5.5 dafc8c5f92 Let the pre-slice warnings checkbox redraw when clicked
Its toggle handler did not call Skip(), so CheckBox's own handler, which
redraws the tick, never ran. Each click still flipped and saved the
setting, but the box kept showing it ticked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 01:29:18 -04:00
SoftFever a5d21b9922 Merge branch 'main' into belt-printer 2026-10-09 13:28:39 +08:00
Clifford GarwoodandClaude Opus 5.5 7c2566488a Keep the IDEX/IQEX settings out of Simple mode
The Multimaterial page showed an IDEX/IQEX section in Simple mode on
every FFF printer. The pre-slice warnings row's hand-built option
definition left its mode at comSimple; it now uses comAdvanced, like the
IMEX options around it. The parallel modes grid had a group of its own
holding only a full-width widget line, which records no mode, so that
group showed in every mode. The grid now sits in the configuration
group, which follows its Advanced rows. That group shows or hides
everything in it, so the grid keeps itself hidden on non-IMEX printers
(IMEXModesCtrl::set_applicable).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 01:22:04 -04:00
ExPikaPakaandExPikaPaka bff19b07d2 Texture displacement: fix the Checker and Distortion views, add UV editor tooltips (#16147)
* Explain the texture displacement views that cannot be used, and show them

Checker and Distortion are views of a layer's unwrap, so they mean nothing on
any other mapping - but they were offered on all of them and simply drew
nothing when picked. Fade them out on anything but Unwrap (LSCM), with the
reason in the tooltip, and have a click bring the UV editor up on the view it
selected, since that pane is where the unwrap is actually worked on.

Neither view was visible even when it did apply. The overlay is built from the
base patch and drawn with a polygon offset, which biases depth values rather
than moving geometry, so it cannot win against the displaced preview standing
in front of it. Leave that preview out while a UV-check view is on and draw the
undisplaced surface instead, which is what the offset assumes and what the
mapping being inspected belongs to.

In the UV editor, a tool that cannot be used right now is faded rather than
disabled. A disabled window gets no mouse events on GTK or MSW, so every one of
those tools - Cut, Join, Unjoin, Clear seams, Clear UV edits, the select modes,
Snap, Frame - silently had no tooltip in the state where the user most needs to
know what is missing. Each now says what to do instead. The tile size, the
island statistics, the status line and the three select modes gained tooltips
of their own; the select modes now name the gestures they enable, which were
documented nowhere.

* UV editor: keep the mouse capture balanced

The canvas captured the mouse on every button press without checking whether
it already held one, released it in a single place, and handled no capture
loss at all. Two sequences leaked a capture: pressing a second button during a
drag nested a second one that the single release on button-up could not undo,
and a modal R/S skipped that release entirely while waiting for a confirming
click that may never come.

A leaked capture is not a local problem on macOS, where wxEVT_MOUSE_CAPTURE_LOST
is never sent and nothing recovers it. While any wx window holds a capture,
wxOSX routes every mouse event to that window and never calls through to
NSWindow, so the application stops seeing motion and enter/leave, and native
tooltips stop appearing anywhere in it.

Capture through grab_mouse()/drop_mouse() so there is at most one, give it back
on any button-up including a modal gesture (which tracks the pointer and needs
no capture), and cancel on wxEVT_MOUSE_CAPTURE_LOST: commit nothing, put back
what a modal rotate or scale already applied, and do not release a capture that
is already gone.

---------

Co-authored-by: ExPikaPaka <mrfsfyt@gmail.com>
2026-10-09 12:14:47 +08:00
yw4z 25ec403156 Unify wiki & video guide buttons (#16159)
* init

* fix build

* Update PrintOptionsDialog.cpp

* Update TroubleshootDialog.cpp
2026-10-09 12:13:00 +08:00
ExPikaPakaandRodrigo Faselli 018c4f49ee Fix a crash on loading a 3MF with empty project settings (#16016)
* Fix a crash on loading a 3MF with empty project settings

opt_float() dereferences what option<>() returns without checking it, and
option<>() is called with create = false. Three CLI sites read printable_height
that way, so a 3mf whose Metadata/project_settings.config holds an empty object
takes the CLI down with a null dereference. Both models shipped in
resources/handy_models are such files, so `--info` on either of them segfaults.

Guard the three reads the way the neighbouring reads of
extruder_clearance_height_to_rod and friends already are. All three target
variables are initialised to 0 and the consumer tests for > 0, so an absent
setting already had a defined meaning and nothing changes for a project that
carries the setting.

* Add a CLI regression test for a project with empty settings

Runs --info over a copy of a shipped model whose Metadata/project_settings.config
has been rewritten to an empty object, so the test keeps covering the crash no
matter what settings the shipped models carry later.

Verified both ways: the test passes against this branch and fails with a
segmentation fault against a build without the guards.

---------

Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-10-09 12:07:03 +08:00
ExPikaPaka 4c74d84b94 Fail cleanly on a truncated or corrupt PNG (#16014)
libpng reports a bad file by longjmp()ing back to the buffer set with
setjmp(), and the frame it lands in must own nothing that needs
destroying: with exceptions enabled MSVC unwinds the stack as part of
longjmp, and returning from a frame unwound that way crashes. It did on
Windows while working everywhere else.

The read callback also returned quietly on a short read, leaving libpng
to decode whatever happened to be in the output buffer.

The calls that can fail now sit in two helpers that own nothing but
pointers, so every C++ object the decoders need stays in their own
frames, and a short read is reported through png_error().
2026-10-09 11:52:55 +08:00
HanifKoh 65978fc87a Cap Recursion Depth in the Plugin JSON Converters (#16255)
* Cap Recursion Depth in the Plugin JSON Converters

* Add Missing Includes to the Plugin JSON Depth Test
2026-10-09 11:39:46 +08:00
HanifKoh 9a9285a894 Log Instead of Showing Info and Substitution Dialogs When Opening a BambuStudio Project (#16153)
Every BambuStudio project opened with a "BambuStudio Project" info dialog
(or, from BambuStudio 2.8.2, one saying the file is newer than the
compatible version and to update the software), followed by the
configuration-substitution dialogs for the project and its embedded
presets. None of them asks anything and all of them fire for every
BambuStudio file.

For BambuStudio projects (untagged files newer than 2.3.2, the existing
test) log the version with the unrecognized settings, and each replaced
value, instead. The geometry-only, invalid-values and G-code safety
dialogs stay, and other 3MFs are unchanged.
2026-10-09 11:33:53 +08:00
HanifKoh ae59656fa4 Dismiss the Slice-Plate Popup Before the Filament Grouping Dialog Opens (#16284)
The Slice-plate hover popup (FilamentGroupPopup, a wxPopupTransientWindow)
takes the mouse capture while it is shown, and on macOS its OnIdle handler
reacquires that capture whenever the cursor sits outside the popup. If the
popup is still shown when the modal filament grouping dialog opens, wx routes
every dialog mouse event to the now-hidden popup, because WX_filterSendEvent
short-circuits to the capture window while GetCapture() is non-null. The
dialog's filament blocks never receive a mouse-down, so they can't be dragged
and the whole app looks frozen even though its modal loop is healthy and the
keyboard still works.

Dismiss the popup synchronously before the dialog opens: Dismiss() hides it
and releases the capture, and hiding it stops OnIdle from reacquiring. This is
a no-op where the popup is never shown (Linux, where the hover popup is
disabled, and any non-dual-nozzle printer).
2026-10-09 11:33:24 +08:00
Clifford GarwoodandClaude Opus 5.5 d4edf59301 Merge upstream main: vase settings from plate settings, infill density with modifiers, STEP chamfers, plugin thread permission
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 23:31:15 -04:00
Misterff1 627ae12dd3 Fix regression: bring back some machine specific BBL P2S start gcode (#15967)
Fix removal of custom code for machine_start_gcode for Bambu Lab P2S
2026-10-09 11:21:35 +08:00
Harm Berntsen b3490a1cdc Prevent segfault when X509_get_default_cert_file_env() environment variable is not set (#16128) 2026-10-08 22:39:26 -03:00
TheLegendTubaGuy c7e7fa2a0b Multi Object Vase Settings from Plate Settings Change (#16091) 2026-10-08 20:40:52 -03:00
lodriguez 16380e8560 add fallback to link spnav dynamiclly (#14223) 2026-10-08 20:35:47 -03:00
Clifford GarwoodandClaude Opus 5.5 45932dd956 Leave the AFC lane sync out of the IMEX work
MoonrakerPrinterAgent filled physical_extruder_map from AFC's per-lane
extruder_index whenever the printer's filaments were synced. IMEX does
not need it: printers carry their map in the printer profile, and IMEX
reads it from there. The sync also wrote device state into the edited
printer preset, so a sync could mark the preset modified or replace a
hand-tuned map. The two Moonraker agent files go back to upstream's
version. The sync can return separately, writing the map somewhere other
than the printer preset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 19:11:18 -04:00
Ian Bassi 785a1946a6 Fix Adaptive and Support Cubic infill density with modifiers (#16295) 2026-10-08 18:28:56 -03:00
Joseph Robertson 64cff12b9d Merge branch 'main' into belt-printer 2026-10-08 16:15:51 -05:00
Joseph Robertson a1169b492b Belt printer: address the 2026-10-08 review of #14394 (#16292)
## Description

Addresses the three items in @raistlin7447's review of 2026-10-08 on
#14394
(https://github.com/OrcaSlicer/OrcaSlicer/pull/14394#pullrequestreview-5459955028),
one commit each.

**Leading-edge brim with a leading overhang (BeltBrim.cpp).** The
leading-edge cut was taken at the first layer with geometry. With an
overhang on the leading side that layer is the overhang's tip, which is
sliced before the part reaches the belt and does not touch it, so the
cut lay ahead of the part. Reproduced on the BabyBelt profile with a 20
mm cube and a fin over its leading end, leading brim length 10 mm, width
5 mm:

| Part | Leading-edge brim before | After |
|---|---|---|
| Plain cube | 53 brim lines | 53 (unchanged) |
| Cube + 30 mm fin | 9, a sliver well ahead of the part | 53 |
| Cube + 40 mm fin | none | 53 |
| Cube + 30 mm fin, leading length 0 | none | 18 |

As suggested, the cut now uses the first layer whose contact band in the
footprint loop is non-empty: the loop records it while it builds the
footprint.

**Dead empty-layer drop (GCode.cpp).** The by-layer
`collect_layers_to_print()` built its groups only from the per-object
entries, and the per-object overload already drops every belt entry that
prints nothing, so the group-level drop could never remove anything.
Removed; its explanation moved to the drop that does the work. No output
change.

**First-layer point test comment (GCode.cpp).** Reworded to say what the
lambda undoes (what `point_to_gcode()` added and the writer took off),
since on a belt `m_origin` is rotated by `on_set_origin()` and is not
"the instance part". Comment only.

## Tests

- New test "Leading-edge-only brim ignores an overhang ahead of the
part" (30 and 40 mm fins): the leading-edge brim of the cube with the
fin must match the plain cube's. Fails without the fix (plain 53 brim
layers vs 10 and 0 with the fins), passes with it (53 and 53).
- `fff_print_tests` 364 cases and `libslic3r_tests` pass.
- Before/after G-code on the current `belt-printer` head (baseline built
from it, both binaries run from the build tree with the same resources):
byte-identical for a multi-color belt project, a two-filament belt
project with the belt purge tower, two cubes printed by object, a
flat-bed organic-support project, and two plain-cube leading-edge brims.
Only the three overhang cases change, as in the table.
- `OrcaSlicer_profile_validator -s` on Printcepts, IdeaFormer, Custom
(belt) and Prusa (control): clean.
- `scripts/clang_tidy_diff.py` against `belt-printer`: clean.

Unrelated, noticed while comparing outputs: `PrintObject::m_id`
(Print.hpp) has no initializer, so on the CLI path the `; printing
object ... id:` labels can carry an arbitrary value that differs between
builds. Pre-existing; not touched here.

OS: Linux (Ubuntu), GCC, local build. Written with AI assistance (Claude
Code); every change reviewed and tested locally as listed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-10-08 16:15:36 -05:00
Kris Austin 8790b07773 fix: missing chamfers on STEP import since the OCCT 8.0.1 update (#16290) 2026-10-08 18:10:37 -03:00
mosfet80 1ec9b315f5 Update CMake minimum version to 3.10 (#16097) 2026-10-08 17:43:38 -03:00
Fernando Marino` e6be22dd4c Assign lib_name from $1 in has_host_runtime_library (#15912) 2026-10-08 17:22:57 -03:00
mateuszandClaude Opus 5.5 64d09c9bca Add AnkerMake PLA+ filament profiles for Anker printers
Adds AnkerMake PLA+ Basic, Matte, Metallic, Silk and Glitter, converted from eufyMake Studio's bundled AnkerMake profiles (AGPL-3.0). Bumps the Anker vendor version to 02.04.00.06.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 14:13:00 -05:00
Rodrigo Faselli ee2c40ea85 Fix Mesh Boolean negative (cut) text object (#16275)
* Fix Mesh Boolean negative (cut) text object

* Update test_meshboolean.cpp

* missing headers

* Update test_meshboolean.cpp

* Refactor mcut difference test for source splits

* Apply suggestion from @raistlin7447
2026-10-08 16:00:28 -03:00
Error404JoyNotFound 9d32c1c545 docs: correct layer_num index tooltip to 0-based (fixes #10340) (#15984) 2026-10-08 15:44:03 -03:00
harrierpigeonandClaude Opus 5.5 679638fca3 Belt G-code: correct the first-layer point test comment
The comment said the lambda takes off "the instance part" of m_origin.
On a belt m_origin has been rotated by on_set_origin() by then, so
m_origin minus the plate offset is not the instance shift.  Say what the
code does: undo what point_to_gcode() added and what the writer took
off.  Comment only.

Reported by raistlin7447 in the review of #14394.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:40:52 -05:00
harrierpigeonandClaude Opus 5.5 d29e3be0a0 Belt G-code: drop the empty layers in one place
The by-layer collect_layers_to_print() dropped every merged layer group
in which nothing prints, and the per-object overload drops every belt
entry that prints nothing.  The merged groups are built only from the
per-object entries, so after the second drop every group holds at least
one entry that prints and the first never removed anything.  Remove it
and keep its explanation at the drop that does the work.  No output
changes.

Reported by raistlin7447 in the review of #14394.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:40:52 -05:00
harrierpigeonandClaude Opus 5.5 c59eb1bcf9 Belt brim: cut the leading-edge brim at the first layer on the belt
The leading-edge-only brim is the outer brim cut down to the part's first
contact with the belt.  The cut was taken at the first layer with
geometry, but with an overhang on the leading side that layer is the
overhang's tip, which is sliced before the part reaches the belt and
does not touch it.  The cut then lay ahead of the part: a 30 mm fin
left a sliver of brim well ahead of a 20 mm cube, and a 40 mm fin, or a
leading brim length of 0, left none at all.

The footprint loop already finds the layers that touch the belt (their
contact band is not empty); record the first of them and cut there.

The new test slices a cube with and without a 30 or 40 mm fin over its
leading end and checks that the leading-edge brim is the same.  It fails
without the fix.

Reported by raistlin7447 in the review of #14394.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:40:52 -05:00
SoftFever 6edc858f8b Merge branch 'main' into belt-printer 2026-10-09 00:55:12 +08:00
Ian Chua 67a16dabca fix: prompt for permission when plugin tries to create a thread (#16248)
* fix: prompt for permission when plugin tries to create a thread

* fix: request permission on main thread
2026-10-08 23:49:33 +08:00
SoftFever 5072f6b614 Merge branch 'main' into belt-printer 2026-10-08 23:03:56 +08:00
Rodrigo Faselli d09850c528 Merge branch 'main' into main 2026-10-08 10:45:49 -03:00
Ian Bassi 59fc97fb28 Extend Separated Infills (#16274) 2026-10-08 09:43:46 -03:00
SoftFever bf59db1053 Merge branch 'main' into extruder-variants-ui-2 2026-10-08 20:31:57 +08:00
LixNix 7d44b60ae4 H2D X2D Multi Nozzle Size Printing Support (#14125) 2026-10-08 20:28:32 +08:00
ExPikaPakaandExPikaPaka 776ca6a3e4 build_linux.sh: add -J to build several dependencies at once (#16282)
* build_linux.sh: add -J to build several dependencies at once

The top-level deps build is fixed at -j1, so one dependency compiles at a
time while the small ones leave most cores idle. -J N raises that level.

-j still applies in full to each dependency, so the worst case is -J times
-j compile jobs: ninja has no job server to share a pool across the nested
builds. Without -J nothing changes.

* Quote the job count for shellcheck (SC2086)

---------

Co-authored-by: ExPikaPaka <mrfsfyt@gmail.com>
2026-10-08 18:56:02 +08:00
SoftFeverandxxxsam ee26e94170 Fix H2D prints rejected for a missing Filament Track Switch
Fixes #15927

Co-authored-by: xxxsam <31843242+xxxsam92123@users.noreply.github.com>
2026-10-08 17:49:04 +08:00
Ian Chua d401d4f837 revert: profile changes made for OTA testing (#16280) 2026-10-08 16:13:12 +08:00
HanifKoh 18b70844d1 Defer Plugin Dock Panes Until the Plater Is Shown (#16257)
A plugin enabled at startup loads before the main frame exists, so a
dock panel it opens from on_load was dropped by the one-shot CallAfter
that found no plater. Opened a moment later, before the frame was laid
out, the pane was sized against the unsized frame and track_docked_size
kept that width. Poll until the plater is shown on screen, then build
the pane; release the reserved id instead when the app is closing.
2026-10-08 14:18:51 +08:00
HanifKoh c8f2498681 Keep GLCanvas3D Building With SLIC3R_CAD Off (#16256)
The bed-axes toggle added for the Design tab's reference planes reads
m_design_sketch_tool, which only exists under SLIC3R_CAD. Compute the
flag once and read the sketch tool inside the same guard as its other
uses.
2026-10-08 14:02:34 +08:00
HanifKoh 30902561c0 Stop Exporting Names Through Usings in the Remaining Headers (#16250)
The last headers with a using or namespace alias at namespace scope:

- TCPConsole.hpp imported boost::asio::ip::tcp into Slic3r::Utils for
  two member declarations. The alias is now a private member of the
  class.
- WebSocketClient.hpp declared four namespace aliases and a tcp alias
  at global scope, each used only by the header. The names are spelled
  out.
- Repair.hpp aliased CGAL::Polygon_mesh_processing as PMP in
  Slic3r::tex2color. The three functions that use it declare the alias
  themselves.
- PreciseSeam.hpp, Thumbnails.hpp and MarchingSquares.hpp used a
  using-declaration or directive for one or two spots each; those spots
  are qualified. Thumbnails.hpp's "PNG"sv default argument becomes
  "PNG", which converts to the std::string_view parameter the same way.
- tests/sla_print/sla_test_utils.hpp had "using namespace Slic3r;" and
  tests/filament_group/fg_test_serialization.hpp "using json =
  nlohmann::json;" at global scope. The headers qualify their own names;
  the two SLA test sources get the directive themselves.

Also removed: twelve type aliases in headers that nothing references
(ConflictObjName, CircleSqf, CircleSqd, TRawBuffer, DistanceFunction,
SamePair, ExtruderNozzleInfos, Vec2dEvent, Vec2dsEvent, Vec3dEvent,
t_option, t_optgroups, Plater::fs_path) and a duplicate
fn_ft_job_msg_destroy alias in FileTransferUtils.hpp.
2026-10-08 13:59:12 +08:00
HanifKoh 3fc515f48d Prompt for Permission When a Plugin Calls os.exec (#16254)
* Prompt for Permission When a Plugin Calls os.exec

* Add a ProcessReplace Audit Category for os.exec
2026-10-08 13:52:09 +08:00
HanifKoh 6cd5feed79 Remove Duplicate Includes and the Dead GCodeSender Sources (#16249)
46 files include the same header twice at file scope, outside any #if,
66 times in all:
Model.cpp included Model.hpp twice, Utils.hpp <algorithm> and
<string_view> twice, seven GUI headers <wx/dataview.h> and
<wx/artprov.h> twice. The second include of each is removed.

GCodeSender.cpp and GCodeSender.hpp have been commented out of
libslic3r/CMakeLists.txt since 2022 and their only two includes are
commented out as well. Both files go, with the commented lines, and
the CMake entry for SLA/SupportTreeIGL.cpp, a file that no longer
exists.
2026-10-08 13:51:50 +08:00
HanifKoh 09530ef7c4 Size a Project's Mixed-Colour Metadata to the Filaments in the CLI (#16247)
The mixed-colour metadata options are parallel per-slot arrays in the project
config. A project saved before they were sized per slot stores a single value
for the gradient ones, and one saved before they existed stores none. The GUI
sizes all seven to the filament count when it opens a project; the CLI kept
the stored arrays and exported one-element defaults for absent ones, so a
project it exported carried one-element arrays where the GUI writes one entry
per filament. Slicing is unaffected, every reader treats a missing entry as
not mixed / no gradient, but the GUI-vs-CLI comparison reported the four
gradient keys on every mixed-filament project.

The resize helper moves from PresetBundle.cpp, where it was file-local, to
PrintConfig.cpp next to set_filament_dev_options(). It creates an option the
config lacks before sizing it, a no-op for the bundle's project config where
all seven always exist. The CLI calls it with its filament count once the
project and loaded filaments are merged, after the check that every mixed
slot has a filament of its own.
2026-10-08 13:40:36 +08:00
Joseph Robertson ed1537cdeb Belt printer: address the 2026-10-07 review of #14394 (#16277)
## Description

Addresses every item of @raistlin7447's review of 2026-10-07 on #14394
(https://github.com/OrcaSlicer/OrcaSlicer/pull/14394#pullrequestreview-5447529307),
one commit per item, plus a follow-up commit from a second adversarial
pass over the result.

**Organic supports (the one non-belt difference raistlin's export
fixtures found).** The debug-strip commit dda58b07cd had deleted the
loop in `organic_draw_branches()` that trims every branch slice against
the collision volume, the bed and the belt plane. It is restored exactly
as on `main` (plus the belt-floor clip). New test: a cube carrying a 60
mm plate, organic supports, flat-bed printer; on every support layer no
support extrusion may come within 0.2 mm of the part's slice. To be
clear about what it proves: it guards that invariant, but on this
fixture the loop's own effect is a sub-millimetre reshaping of one
branch (checked by running the test with the loop compiled out), so the
test does not by itself fail without the loop. The loop's effect is
shown separately by slicing six organic fixtures with the stripped and
the restored binary (CLI): on a plate-over-cube fixture the stripped
build brings a branch to 0.02 mm from the part's slice at the cube's
corner where the restored build keeps 0.39 mm; the Bulbasaur project
differs in ~2000 support lines; a fixture with no wall near the branches
is byte-identical.

**G-code (belt only).**
- First-layer speed test: the writer passes points with the plate origin
already removed, so only the instance part of `m_origin` is subtracted
now.
- The mixed-filament sub-layer pass calls `on_set_origin()` like the
main instance loop.
- `m_belt_in_band` is reset per object in by-object printing, with the
cooling buffer.
- `m_layer_count` counts only the layers that are written, through the
same predicate `collect_layers_to_print()` uses
(`belt_object_layer_prints_something()`); the by-object overload drops
the empty belt layers as well, so both print sequences write the same
layer changes. The empty-layers test now runs for both sequences and
checks `; total layers count` too. Side effect worth knowing: with the
empty entries dropped per object, a multi-filament belt layer no longer
selects a filament it then prints nothing with. On belt_project.3mf (two
filaments, belt purge tower) the T commands go from 472 to 106 with the
extruded length per filament unchanged; every removed tool change was
followed by no extrusion.

**Invalidation / ordering.**
- `posSlice` now also invalidates `posDetectOverhangsForLift` (not
belt-gated: a re-slice starts the layers over with empty overhang
regions while the step stayed done; this makes an incremental re-slice
match a fresh slice).
- `btLeadingEdgeOnly` takes part in the layer-0 outer-wall-first rule
and the matching `brim_type` → `posPerimeters` rule (not belt-gated:
`Print.cpp` already prints it as an outer brim on a flat bed).
- Adding or removing an object invalidates the support step of the other
belt-brim owners, so their brims are clipped against what is on the
plate now.

**Belt brim (found during the GUI pass, pre-existing since #16236).**
"Leading edge only" produced no brim at all: the cut that narrows the
outer brim to the first contact was taken at `layers().front()`, which
since the lead-in change is an empty layer whose contact lies ahead of
the part, so the whole region was clipped away. The cut is now taken at
the first layer with geometry; `leading_edge_only` joins the
all-brim-types test and a new test checks the brim starts no later than
the part and covers fewer layers than the outer brim.

**UI.** Build plate tilt X/Y are read-only on a belt printer (they are
derived from the belt tilt). The belt temperature tower refuses a range
without an embossed model, before the project is replaced, instead of
falling back to the 230–190 model.

**Strings, dead code, comments.** Tooltip and comment say cot and
1/|sin| (what `MachineFrameTransform.cpp` does); `gcode_remap_*` labels
and tooltips are `L("literal")` so they are extracted; removed
`belt_remapped_bbox()`, `belt_min_z()`, `m_belt_global_xy_correction`,
`LayerTools::has_belt_brim`, the `belt_surface_z` constant, and (second
pass) the unused kinematics inverse (`to_logical`,
`apply_axis_remap_inverse`, `to_build_volume` and their state), the
`world_coordinates()`, `is_active()` and `belt_brim_areas_by_layer()`
accessors and two unused overloads; rewrote the comments that still
described removed code (BeltBrim.cpp SEQUENCING, GCodeWriter.hpp,
calib.cpp/hpp, GCode.hpp, BeltSliceStrategy, PrintObjectSlice.cpp,
PrintApply.cpp).

Not changed, noted for a follow-up: the outer-wall-first rule keys on
numeric layer 0, which on a belt is usually an empty lead-in layer, so
the part's first contact layer does not get the rule; and a
leading-length-only brim (zero base width) is excluded by the
`brim_width > 0` test. Both need a geometry-based rule rather than a
one-line change.

## Screenshots/Recordings/Graphs

Build plate tilt fields greyed out on a belt printer, the temperature
tower error dialog, and the brim before/after deleting a neighbouring
object are attached below (from the Xvfb GUI pass).

## Tests

- `fff_print_tests`: all cases pass (includes the new organic test and
the extended empty-layers test in both print sequences);
`libslic3r_tests` pass.
- Organic test run with the loop compiled out (temporary local switch):
passes either way on this fixture, see above; the CLI comparison on six
fixtures is where the loop's effect is visible.
- `OrcaSlicer_profile_validator -s` on the belt vendors and Prusa as
control; `scripts/orca_profile_tool.py check`; profile tool unit tests
(281).
- `scripts/clang_tidy_diff.py` against `belt-printer`: clean.
- GUI pass on Xvfb (Linux): tilt fields greyed/editable with belt
on/off; temperature tower error for 250–200 leaves the project
untouched, 230–190 loads the tower; multi-colour demo by layer 595
slider layers = 595 layer changes with matching labels and no greying
while dragging; two cubes by object 314 = 314; outer brim complete after
deleting the neighbouring cube; organic supports clear of the part on
the belt preset and on a flat-bed variant; raw G-code toggle via menu
and `B` keeps the slider index; no crash or assert in the logs. The
leading-edge brim finding from this pass is fixed above.

OS: Linux (Ubuntu), GCC, local build. Written with AI assistance (Claude
Code), every change reviewed and tested locally as listed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-10-07 21:45:56 -05:00
harrierpigeonandClaude Fable 5.1 ed8c1f92d7 Belt brim: cut the leading-edge brim at the first contact layer
The leading-edge-only brim is the outer brim cut down to the part's
first contact with the belt, and the cut was taken at layers().front().
Since the slicing frame starts at the belt below the footprint (#16236)
that is an empty lead-in layer whose contact lies ahead of the part, so
the cut removed the whole region and the brim type produced no brim at
all.  Take the cut at the first layer with geometry.

The all-brim-types test now includes leading_edge_only, and a new test
checks that the brim starts no later than the part and covers fewer
layers than the outer brim.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 19:08:59 -05:00
harrierpigeonandClaude Fable 5.1 cfad587c2d Belt: follow-ups from a second review pass
Guard the layer count and the per-object layer collection against an
object that is left without a layer to print on a belt (the counting
loop stepped before begin() and front() was taken of an empty vector).
Check the belt temperature tower's embossed model before the current
project is replaced, not after.  Only invalidate the support step of
objects that own a belt brim when an object is added or removed.  The
empty-layers test now counts an extrusion only where material is laid
down along a move.  The BeltBrim.cpp SEQUENCING note says exactly which
layers are read, and the machine-frame scale is 1/|sin|.

Remove more code that nothing calls: the kinematics inverse
(to_logical, apply_axis_remap_inverse, to_build_volume and the state
kept for them), the world_coordinates(), is_active() and
belt_brim_areas_by_layer() accessors, the PrintConfig overload of
physical_tilt() and the DynamicPrintConfig overload of
compute_belt_height_and_floor().  Comments in GCode.hpp,
BeltSliceStrategy.hpp/.cpp and PrintObjectSlice.cpp that described the
retired pre-slice remap and plane-evaluator still did; the purge-tower
width tooltip named the wrong switch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 19:08:59 -05:00
harrierpigeonandClaude Fable 5.1 a564ec23fe Belt: refresh comments that described earlier code
BeltBrim.cpp still described the brim as running inside the parallel
support step; it runs sequentially after it (generate_belt_brim).  The
GCodeWriter, calib.cpp and calib.hpp comments referred to an inheritance
layout and a dynamic_cast that no longer exist.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 19:08:59 -05:00
harrierpigeonandClaude Fable 5.1 441113cf3b Belt: remove dead code
belt_remapped_bbox() had no callers; belt_min_z() and
m_belt_global_xy_correction were written but never read;
LayerTools::has_belt_brim was set but never read; belt_surface_z was a
named zero.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 19:08:58 -05:00
harrierpigeonandClaude Fable 5.1 1fb585dc5c Belt config: fix the tilt tooltip math and extract the remap strings
The machine-frame transform is a shear of cot(tilt) and a scale of
1/sin(tilt), not tan and 1/cos; fix the tooltip and the matching comment
in BeltGCode.cpp.  The gcode_remap_* labels and tooltips were passed
through L() as variables inside a lambda, which the string extraction
does not see; pass L("literal") at the call sites.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 19:08:58 -05:00
harrierpigeonandClaude Fable 5.1 e78f437ce4 Belt temperature tower: refuse a range without a model
The calibration fell back to the 230-190 tower when no embossed model
existed for the requested range, so the printed numbers did not match
the temperatures.  Show an error naming the range and stop instead.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 19:08:58 -05:00
harrierpigeonandClaude Fable 5.1 798ca8272e Printer settings: build plate tilt is read-only on a belt printer
update_fff() derives build_plate_tilt_x/y from the belt tilt on a belt
printer, so a value typed into those fields was silently overwritten.
Disable the two fields while belt_printer is on.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 19:08:58 -05:00
harrierpigeonandClaude Fable 5.1 d5cfaae5dd Belt brim: adding or removing an object rebuilds the other objects' brims
A belt brim is clipped against the other objects on the plate and is
built with its object's support step.  When an object was added or
deleted only the print-level skirt/brim and export steps were
invalidated, so the remaining objects kept brims clipped against objects
that were no longer there, or overlapping ones that had arrived.
Invalidate posSupportMaterial on every object in that case on a belt
printer.  Also reword the comments that still described the global Z
offset as a minimum across all objects.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 19:08:58 -05:00
harrierpigeonandClaude Fable 5.1 9289fc4dd3 Leading-edge brim takes part in the outer-wall-first rule
The first-layer rule that prints the outer wall first when a brim is
attached to it, and the brim_type change rule that regenerates the
perimeters for it, only knew btOuterOnly.  btLeadingEdgeOnly, the belt
brim at the part's first contact, is an outer brim too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 19:08:58 -05:00
harrierpigeonandClaude Fable 5.1 b2fb552e32 PrintObject: a re-slice invalidates posDetectOverhangsForLift
posSlice's invalidation list did not include posDetectOverhangsForLift.
A re-slice starts the layers over with empty overhang regions while the
step stayed done, so GCode::needs_retraction() had no overhangs to test
against until something else invalidated it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 19:08:58 -05:00
harrierpigeonandClaude Fable 5.1 febd82f6df Belt G-code: count only the layers that are written
collect_layers_to_print() drops the belt layers that print nothing (an
object's empty lead-in), but m_layer_count still counted every object
and support layer, so "total layers count", the total_layer_count
placeholder and the M73 progress disagreed with the layer changes in the
file.  Count with the same predicate, shared through
belt_object_layer_prints_something().  The by-object overload of
collect_layers_to_print() now drops those layers as well, so both print
sequences write the same layer changes.

Dropping the empty entries per object has one more effect on multi-
filament belt prints: a layer no longer selects a filament that it then
prints nothing with.  On belt_project.3mf (two filaments, belt purge
tower) the T commands go from 472 to 106 while the extruded length per
filament is unchanged; each of the removed tool changes was followed by
no extrusion.

The empty-layers test now runs for both print sequences and also checks
"total layers count" against the layer changes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 19:08:58 -05:00
harrierpigeonandClaude Fable 5.1 fba8b8b029 Belt G-code: reset the first-layer band state per object
In by-object printing the cooling buffer is reset for every object, but
m_belt_in_band, which tracks whether the extrusion is inside the band
along the belt where the part fan stays off, kept the previous object's
value.  If the previous object ended inside the band the next one never
emitted its band start marker.  Reset it with the cooling buffer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 18:24:27 -05:00
harrierpigeonandClaude Fable 5.1 05e37d838c Belt G-code: the mixed-filament sub-layer pass rotates its origin too
process_layer()'s sub-layer pass (several filaments in one layer without
a purge tower) calls set_origin() per instance like the main instance
loop, but not on_set_origin(), which on a belt printer runs the origin
through the belt transform.  Add the call so both passes place the
instance the same way.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 18:24:27 -05:00
harrierpigeonandClaude Fable 5.1 4eaaf9a992 Belt G-code: first-layer speed test subtracts the instance offset only
The writer hands set_first_layer_point_test() a point with the plate
origin (its own XY offset) already removed, but the test subtracted the
whole of m_origin, which carries the plate origin as well as the
instance shift.  On a plate other than the first the point was moved by
the plate origin a second time and the band test looked at the wrong
spot.  Subtract only the part of m_origin that is not the writer's
offset.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 18:24:27 -05:00
harrierpigeonandClaude Fable 5.1 54e079ae25 Organic supports: restore the per-slice collision, bed and belt trim
dda58b07cd stripped debug instrumentation from TreeSupport3D.cpp with a
script, and that script also deleted the loop in organic_draw_branches()
that trims every branch slice against the collision volume, the bed and,
on a belt, the belt plane.  This is the generator every printer uses, not
a belt code path, and it is the one place where raistlin7447's export
fixtures differed from main with belt printing off.  Restore the loop as
it was on main, with the belt-floor clip.

The new test prints a cube carrying a 60 mm plate with organic supports
on a flat-bed printer and checks on every support layer that no support
extrusion comes within 0.2 mm of the part's slice.  It guards that
invariant; on this fixture the loop's own effect is a sub-millimetre
reshaping of one branch (verified by slicing the fixture with and
without the loop), below the asserted gap, so the test does not by
itself fail without the loop.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 18:24:27 -05:00
e729dbf142 Upgrade CGAL from 5.6.3 to 6.2.1 (#16258)
Co-authored-by: Andrei <1331777+andreili@users.noreply.github.com>
Co-authored-by: Amelia <26681721+elihwyma@users.noreply.github.com>
Co-authored-by: Kris Austin <kris.austin@gmail.com>
2026-10-07 20:24:22 -03:00
yw4z 33f886406e Profile folder optimizations (#16259)
* anycubic kobra 3 v2

* bbl

* rh3d

* melting point

* Pragostroj

* prusa

* ultimaker

* creality

* kingroon

* m3d

* wondermaker

* bbl fix plate origins

* update some of creality plates

* Update Creality.json
2026-10-08 01:24:57 +03:00
Kris AustinandRodrigo Faselli c33515914d fix: Create Printer finds no system vendors in release builds (#16175)
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-10-07 17:30:36 -03:00
Joseph Robertson d0c2ada32a Belt printer: classic tree branches land on the belt instead of sliding down it (#16263)
Classic tree supports (tree hybrid / slim / strong) on a belt slid down
the belt plane ahead of the part instead of landing on it.
`TreeSupportData` added the belt surface to every layer's outlines, so
the belt fed the collision and avoidance maps, and a node that descends
onto an obstacle is pushed out of it; on a tilted surface that walks the
branch down the belt. This takes the belt out of the outlines. The belt
is where a branch ends, and that is already handled: `drop_nodes()`
stops a node once its whole circle is in the belt
(`belt_node_landed()`), and `draw_circles()` clips every layer's circles
to the belt plane, so the branch tapers to a tip on it. Organic got the
same treatment in #16236 (the belt is no longer a support blocker
there).

One file, +7/−12. Non-belt printers are untouched: the removed block
only ran when the belt floor context was active.

## Before / after

Cube with a fin whose underside is parallel to the layers, 20 mm ahead
of the cube, tree hybrid, Left view:

| | support footprint along the belt | filament for support |
|---|---|---|
| before | belt Z 43–139 (sweeps 72 mm ahead of the part) | 2403 mm |
| after | belt Z 60–139, columns parallel to the up direction | 1606 mm
|

Organic on the same model: belt Z 74–139 (unchanged). Before/after
screenshots follow in a comment.

## Tests

- *Belt supports reach the belt under a leading overhang* passes for
normal, organic and tree_hybrid; all `[belt]` tests pass;
`fff_print_tests` 355 and `libslic3r_tests` 1116 pass on the branch.
- `scripts/clang_tidy_diff.py --base upstream/belt-printer`: no
findings.
- Fork CI (Build all) on this change: unit tests green on Linux x86_64,
Linux aarch64 and macOS arm64
(https://github.com/HarrierPigeon/OrcaSlicer/actions/runs/37601644023;
its Windows and slice-check failures are the ones #16262 fixes).
- Scripted GUI pass on belt-printer + this change: tree hybrid, organic
and normal supports at Y≈120 all reach the belt (lowest 0.17–0.19 mm);
with the part within its height of Y = 0 all three generators now behave
the same (support before the belt start, plate-boundary error shown),
where tree hybrid used to be the odd one out (clipped, hanging 9.5 mm
above the belt).
- Written with Claude Code; reviewed and run by me.
2026-10-07 14:21:34 -05:00
harrierpigeonandClaude Fable 5.1 d527db6bdf Belt printer: classic tree branches land on the belt instead of sliding down it
TreeSupportData added the belt surface to every layer's outlines, so the
belt fed the classic tree's collision and avoidance maps.  A node that
descends onto an obstacle is pushed out of it, and on a belt that walked
the branch down the tilted surface, ahead of the part, before it could
end: tree hybrid/slim/strong supports swept far along the belt where
organic supports dropped straight down.  Take the belt out of the
outlines.  The belt is where a branch ends, and that is already handled:
drop_nodes() stops a node once its whole circle is in the belt
(belt_node_landed()) and draw_circles() clips every layer's circles to
the belt plane, so the branch tapers to a tip on it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 14:15:14 -05:00
Joseph Robertson e39d6cc089 Merge branch 'main' into belt-printer 2026-10-07 14:14:20 -05:00
Joseph Robertson 827efc3bc2 Belt printer: get the branch CI green (Windows Polyline clash, validator belt slice) (#16262)
Two one-file fixes that get `belt-printer`'s CI green again after
#16236; both failures are mine.

## Changes

1. **Tests: qualify `Polyline` in the belt overhang test for Windows.**
Both Windows builds fail at `tests/fff_print/test_print.cpp:1398`
("reference to 'Polyline' is ambiguous"): the GDI function of the same
name, like the `Polygon` fix in #16196. `Slic3r::Polyline`.
2. **Profile validator: slice belt printers with two cubes along the
belt.** The slice check (`-s`) prints one cube per printer with a height
range 4–10 on filament 2 and, on belt printers, expects a plain `T1`.
Since #16236 a belt object's slicing Z starts at the belt below its
leading end, well below the part's first printed layer, so that range
falls into the empty lead-in and filament 2 is never used; all six belt
printers reported "the filament change never fired". Belt printers are
now sliced with two cubes one behind the other along the belt, the
second on filament 2. Other printers are unchanged.

## Tests

- Root cause for both confirmed in the upstream logs (run 37583336264
and the push run on 0b11311d40) and reproduced locally with the rebuilt
validator.
- `OrcaSlicer_profile_validator -s -l 2`: Printcepts 8/8, IdeaFormer
8/8, Custom 20/20 (the four MyBeltPrinter nozzles included), Prusa 95/95
as a non-belt control.
- `fff_print_tests` and `libslic3r_tests` pass;
`scripts/clang_tidy_diff.py --base upstream/belt-printer`: no findings.
- A fork run of Build all with these two commits on top of belt-printer
(plus a pending belt change) was green on every job: Windows x64 and
arm64 builds, Slice check, unit tests on Linux x86_64, Linux aarch64,
macOS arm64, Windows x64, Windows arm64 and both Flatpaks:
https://github.com/HarrierPigeon/OrcaSlicer/actions/runs/37607869527
- Written with Claude Code; reviewed and run by me.
2026-10-07 14:14:06 -05:00
harrierpigeonandClaude Fable 5.1 006a9377c3 Profile validator: slice belt printers with two cubes along the belt
The slice check (-s) prints one 10 mm cube per printer with a height
range on filament 2 and expects the filament change to fire.  Since
#16236 a belt object's slicing Z starts at the belt below its leading
end, well below the part's first printed layer, so the range 4-10 falls
into the empty lead-in and filament 2 is never used: every belt printer
reported "the filament change never fired" and the Slice check job on
belt-printer went red.  A height range in slicing Z does not map onto a
part on a belt in any case.  Slice belt printers with two cubes one
behind the other along the belt, the second on filament 2, which gives
the one plain T1 the check looks for.  Other printers are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 14:11:15 -05:00
harrierpigeonandClaude Fable 5.1 ef6d4a71de Tests: qualify Polyline in the belt overhang test for Windows
Windows headers declare a global Polyline, so the unqualified name in
test_print.cpp is ambiguous there (both Windows builds of belt-printer
fail at tests/fff_print/test_print.cpp:1336), as Polygon was in #16196.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 14:11:15 -05:00
Joseph Robertson 0b11311d40 Belt printer: no layer changes that print nothing, and a preview that survives them (#16245)
Fixes the preview layer bar on belt prints with several parts along the
belt (reported with a cube on filament 1 and a 3DBenchy on filament 2,
no purge tower): the top slider layer held nearly the whole print, the
slider jumped every other layer through the single-colour stretch before
the second part, and with the belt purge tower the whole print greyed
out while dragging.

## Cause

Since #16236 the slicing frame of a belt object starts at the belt below
its leading end, so its first layers are empty. On a single part they
carry the brim bands. With several parts along the belt the later parts'
empty layers fall between the earlier parts' printing layers and were
written to the G-code as layer changes with no moves at all. The preview
numbers its layers (`libvgcode::Layers`) from the vertices it is given
and expects consecutive ids, so at the first such gap it stopped
creating layers and folded everything after it into the last one.

## Fix

- `GCode::collect_layers_to_print` drops the belt layers that print
nothing (no object, support or brim content): no layer change without
moves in the file.
- `libvgcode::convert` renumbers the layers consecutively over the moves
that exist, so a file with empty layers from any source still previews
correctly.
- The layer slider labels each belt layer with its print Z (the slicer's
layer Z, which increases along the belt) instead of libvgcode's toolpath
height, which on a tilted layer is wherever its last extrusion happened
to end; the slider assumes the list increases, so the labels showed "0 /
max" on alternate layers. The processor reads that print Z from the
`;Z:` tag non-BBL printers write (it only knew `; Z_HEIGHT:`), on belt
printers only, so nothing changes for other printers.

## Verification

- New regression test *Belt G-code has no layer that prints nothing*
(two cubes 60 mm apart along the belt): fails on the previous code with
one empty layer, passes now.
- `fff_print_tests` 356 passed, `libslic3r_tests` 1116 passed;
`scripts/clang_tidy_diff.py --base upstream/belt-printer`: no findings.
- The reported project sliced through the CLI: 595 layers, none without
an extrusion, Z strictly increasing.
- Scripted GUI pass on the reported project with and without the purge
tower: the slider has one entry per G-code layer, each step shows a thin
tilted strip advancing along the belt, the top layer alone is a thin
strip, nothing greys out while dragging, the slider opens at the top
after slicing, and every label reads the layer number and the print Z
matching the G-code's `;Z:` (checked at the top, mid-print and through
the two-part stretch); raw-view toggle and slider retention unchanged.

Left as is: the lower handle at the bottom still reads `1 / 0.00` rather
than the first layer's Z (index correct); pre-existing.
2026-10-07 12:01:20 -05:00
SoftFever e027f8b225 Remove the CAD Primitive and Sketch gizmos from Prepare 2026-10-07 21:20:05 +08:00
Clifford GarwoodandClaude Opus 5.5 dbaec929bc test: rely on upstream's tests for the variant truncation guard
Remove three update_non_diff_values_to_base_config scenarios that
tested the truncation guard from upstream #13316. IMEX does not change
that code, and upstream's own tests cover it: removing the guard fails
#13316's test, and loosening it to `>=` fails #16107's.

The equal-size scenario broke when #16107 changed how variants are
matched, and the scalar-key scenario could not fail at all.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 08:54:18 -04:00
SoftFever 27418cea0f Merge branch 'main' into extruder-variants-ui-2
# Conflicts:
#	src/slic3r/GUI/Tab.cpp
2026-10-07 19:40:01 +08:00
SoftFever 7fdfea6efa Merge branch 'main' into main 2026-10-07 19:31:24 +08:00
SoftFever 6d02a945e0 Speed up the Linux incremental rebuild (optimizes packaging step) (#16244)
## Speeds up OrcaSlicer incremental rebuild on Linux

Profiled `build_linux_image.sh`: 96 s, of which 51 s in the dependency
audit.

**`appimage_is_elf_file()`** ran `file` and `grep` per candidate. An
AppDir holds ~9.6k of them, 4.8k being the bundled Python runtime and
none of them ELF: ~19k processes, 14 s. Reads the four-byte magic
instead. Checked against the old result on 4000 files, no disagreement.

**The dependency walk** popped its queue with `"${queue[@]:1}"`, which
rebuilds the whole array each time. At ~4.8k entries that was 22 s of
copying an array around. Uses a read index.

Audit still passes. `shellcheck` v0.11.0, the version CI uses, is clean.

## Notes
The 96 s -> 12.7 s. Measured on a 32-core / 48 GB machine, but the audit
is a serial bash loop, so cores and RAM is not the bottleneck. On slower
hardware the saving should be larger

## Images
<img width="1987" height="782" alt="Screenshot_20261007_092516"
src="https://github.com/user-attachments/assets/96fcb917-38e5-49e7-8cbe-b37be1a2f23a"
/>
<img width="1807" height="742" alt="Screenshot_20261007_092621"
src="https://github.com/user-attachments/assets/4a65ffae-4d59-41f0-a1c7-ee5b49f3c4be"
/>
2026-10-07 19:07:22 +08:00
SoftFever 01afd3b5b9 Use multiswitch for extruders instead new tabs on printer settings & publish dialog (#16162)
# CHANGES / TESTS
• Uses "Extruders" as tab name if it has multiple or it uses "Extruder" for single ones
• Keeps selected extruder while switching between "Extruders" and "Motion ability" tab
• Revert functions are working
• New extruders generated with values so they will shown in "Unchanged values" dialog if you try to change preset while its edited. revert functions not works properly without this
• BBL printers visible as Left / Right while normal printers visible as T1 / T2. i think there should be a separate option for how many toolheads and how many extruders it has. we might see 4 nozzles on same toolhead if one brand is brave enough :)
• Sidebar and other sections updates itself properly


<img width="751" height="173" alt="Screenshot-20261005173918" src="https://github.com/user-attachments/assets/3be0a6bd-84bd-4d15-822c-ed34acd98a9f" />
<img width="768" height="184" alt="Screenshot-20261005173905" src="https://github.com/user-attachments/assets/46164926-e385-4482-9fd1-41325eb9f61d" />
<img width="755" height="289" alt="Screenshot-20261005175318" src="https://github.com/user-attachments/assets/4550fa51-4c71-4a0a-b15e-9ca82dd0f1ad" />

# FIXES
• Extruders count on parameters section not updated when extruder count changed on printer settings. fixed on this PR
<img width="800" height="478" alt="Screenshot-20261005174702" src="https://github.com/user-attachments/assets/734e53df-f23f-4f83-8f87-2ecfeb6c162c" />

• New extruders gets randomly modifed parameters. fixed on this PR
<img width="871" height="87" alt="Screenshot-20261005174840" src="https://github.com/user-attachments/assets/254dae2a-fcfa-44ed-b3b0-038faf019abb" />

• Changed parameters not triggers revert / modified on extruder tabs. fixed on this PR

• Multi switch on motion ability tab not updated on extruder count change. fixed on this PR
<img width="813" height="184" alt="Screenshot-20261005174929" src="https://github.com/user-attachments/assets/f4f33f8f-b1bd-4a30-b91b-6ff632d08c2a" />
2026-10-07 19:05:37 +08:00
SoftFever b805f2ffd6 Fill a new extruder's machine limits the same way as its other settings 2026-10-07 18:42:09 +08:00
SoftFever 6b2b200f50 Merge branch 'main' into pr/yw4z/16162 2026-10-07 18:40:37 +08:00
yw4z a38e6c61f2 Show dummy image for printers without cover on printer selectiondialog (#16235)
init
2026-10-07 12:57:28 +03:00
SoftFever cc3769453f Merge branch 'main' into pr/yw4z/16162 2026-10-07 17:25:29 +08:00
harrierpigeonandClaude Fable 5.1 f64b49ab52 Belt printer: no layer changes that print nothing, and a preview that survives them
Since the slicing frame of a belt object starts at the belt below its
leading end, its first layers are empty.  On a single part they carry
the brim bands; with several parts along the belt the later parts'
empty layers fall between the earlier parts' printing layers and were
written to the G-code as layer changes with no moves at all.  The
preview numbers its layers (libvgcode::Layers) from the vertices it is
given and expects consecutive ids, so at the first such gap it stopped
creating layers and folded everything after it into the last one: the
top slider layer held nearly the whole print, the slider jumped every
other layer through the single-colour stretch before a second part on
another filament, and with the belt purge tower the whole print greyed
out while dragging.

Drop the belt layers that print nothing (no object, support or brim
content) in GCode::collect_layers_to_print, and renumber the layers
consecutively over the moves that exist when converting a result for
libvgcode, so a file with empty layers from any source still previews
correctly.  The layer slider labels a belt layer with its print Z (the
slicer's layer Z, which increases along the belt) instead of libvgcode's
toolpath height, which on a tilted layer is wherever its last extrusion
ended; the slider assumes that list increases and showed "0 / max" on
alternate layers.  The processor reads that print Z from the ";Z:" tag
non-BBL printers write (it only knew "; Z_HEIGHT:"), on belt printers
only, so nothing changes elsewhere.  Regression test: two cubes 60 mm apart along the
belt produce no layer without an extrusion and the header's layer count
matches.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 03:50:51 -05:00
HanifKoh f9ddb23804 Keep Each Filament's Device Drying Values When the CLI Merges Filaments (#16157)
The device drying options hold several values per filament, as many as
the filament preset gives, and a project stores them as the filaments'
values one after another. The CLI filament merge wrote them like an
option with one value per filament, putting each preset's first value at
the filament's own index, so a project with three filaments whose preset
gives "1", "0" was exported with 1;1;1;0;1;0 where the GUI writes
1;0;1;0;1;0.

The merge now leaves these options out of the per-filament pass and
rebuilds them afterwards from every filament's values in slot order.
Without a fixed number of values per filament one slot cannot be
replaced in place, so the stored values are kept when any slot has no
config to rebuild from.
2026-10-07 15:54:53 +08:00
HanifKoh ff8aebe76f Allow Unsigned Executable Memory in the macOS Entitlements (#16203)
The Bambu network plug-in's code protector rewrites one page of its own signed __TEXT after loading. The hardened runtime tolerates that until the page is evicted; the next read of it then kills OrcaSlicer with CODESIGNING Invalid Page. Bambu Studio signs with allow-unsigned-executable-memory for this reason; with it added, the same build survives critical memory pressure that killed it in 30 s without.
2026-10-07 15:52:18 +08:00
HanifKoh a56d1bf33e Keep a Project's Changed Values on Extruder Variants It Doesn't List (#16107)
A project's listed settings are carried onto its base preset by update_non_diff_values_to_base_config, which matched variants by exact name and id. A variant the base gained after the project was saved got the base's value, while the same value in a user preset now falls back to the preset's first variant of that extruder. So an old project opened with its printer preset already modified, and saving it wrote the base's values into the 3MF.

The function now maps variants with map_variant_indices, as update_diff_values_to_child_config does: a base variant the project does not list takes the project's first variant of the same extruder. The variant lists themselves stay the base's, so a fallback never writes one variant's name over another's.
2026-10-07 15:51:49 +08:00
HanifKoh 662a8e340d Support the 02.08.04 Bambu Network Plug-in Series (#16202)
The 02.08.02 series appended queue_plate_id to PrintParams and nothing
after it changed the ABI OrcaSlicer calls, so adding the field brings
the current layout up to 02.08.04. Make 02.08.04 the latest series and
drop 02.08.01 from the whitelist: its PrintParams no longer matches, and
its malformed bind table is refused by dyld on macOS 27, so it cannot
load there. A stored 02.08.01 falls back to the latest series through
the existing unsupported-version path.
2026-10-07 15:51:21 +08:00
ExPikaPaka bd0ef35b90 Speed up the Linux image build
Profiling build_linux_image.sh: 96 s, of which 51 s in the dependency audit.

appimage_is_elf_file() ran file(1) and grep per candidate. An AppDir holds ~9.6k
of them, 4.8k being the bundled Python runtime, none of them ELF: ~19k processes
for 14 s. Read the four-byte magic instead; checked against the old result on
4000 files, no disagreement.

The dependency walk popped its queue with "${queue[@]:1}", which rebuilds the
whole array each time. At ~4.8k entries that was 22 s of copying. Use a read
index.

96 s -> 12.7 s. The audit still passes.
2026-10-07 09:25:19 +02:00
HanifKoh 6639a32b0c Remove using namespace std from json_diff.hpp (#16222)
The directive sat at global scope in a header that DeviceManager.hpp
includes, so most of the GUI compiled with all of std in the global
namespace. 42 files had come to rely on it, mostly for string, vector
and unordered_map, four of them for the ""sv and ""ms literals.

Those sites are qualified. GCodeViewer.cpp spelled the type as
std::vector<::string>, which only resolved through the directive. The
files that use the ""sv and ""ms literals get a file-scope
"using namespace std::string_view_literals;" or
"using namespace std::chrono_literals;", as other sources already do.
2026-10-07 14:58:49 +08:00
Joseph Robertson 6927475499 Belt printer: bring back the raw G-code view in the canvas view menu (#16241)
The *Show raw G-code (belt only)* toggle retired in #16236 returns, as
an item of the Preview canvas view menu (the eye-icon popup, after
*Labels*) with its `B` shortcut, and only there: no legend checkbox.
Unlit, the preview shows the designed (upright) view; lit, the raw
machine-frame G-code, which is what to look at when checking the machine
frame transforms. The item only appears on a belt printer in Preview.

The toggle is view only (exported G-code is byte-identical either way),
and the layer slider now keeps its layer index across the reload (the
layer Z values differ between the two views, so the old keep-by-Z lost
the position).

## Verification
- Scripted GUI pass: item present only in the belt Preview menu (absent
in Prepare and on a non-belt printer), toggles from the menu and from
`B` with the eye following the state, legend has no belt entry, exported
G-code identical with the view on and off, slider stays at its layer
through toggles, 3MF reopen and printer switch unaffected.
- `fff_print_tests` 355 passed, `libslic3r_tests` 1116 passed;
`scripts/clang_tidy_diff.py --base upstream/belt-printer`: no findings.

Wiki: OrcaSlicer/OrcaSlicer_WIKI#374 documents the menu item and
shortcut with screenshots.
2026-10-07 01:45:53 -05:00
Joseph Robertson 0f86115eb6 Profile tool: list the retired belt keys as obsolete (#16240)
Gets the `belt-printer` CI green again after #16236:

- **Check profiles**: the profile tool's unit test
`test_obsolete_keys_match_the_loader_ignore_set` compares
`OBSOLETE_KEYS` with the loader's ignore set in
`PrintConfigDef::handle_legacy()`, which gained the twelve retired belt
keys. Adds them to the tool's list.
- **clang-tidy**: `tests/fff_print/test_print.cpp` used `std::sqrt`
without `<cmath>` (misc-include-cleaner).

Verification:
- `python3 -m unittest discover -s scripts/tests -t scripts`: 281 tests
pass.
- `scripts/orca_profile_tool.py check`: no errors.
- `scripts/clang_tidy_diff.py -p build-tidy --base upstream/main` on
this head, i.e. every line the belt branch changes against `main` (100
files, the same check the *Merge Belt Printing Into Upstream* PR runs):
no findings.
2026-10-07 01:45:38 -05:00
SoftFever b0da039eae Merge branch 'main' into pr/yw4z/16162 2026-10-07 14:44:59 +08:00
SoftFever 1ec195a221 Update build commands in AGENTS.md to use Release configuration 2026-10-07 14:44:04 +08:00
harrierpigeonandClaude Fable 5.1 e34f4aa0e5 Belt printer: bring back the raw G-code view in the canvas view menu
The "Show raw G-code (belt only)" toggle, retired in #16236, returns as
an item of the Preview canvas view menu (with its B shortcut), and only
there: no legend checkbox.  Unlit, the preview shows the designed,
upright view; lit, the raw machine-frame G-code, which is what to look
at when checking the machine frame transforms.  The toggle is view
only; exported G-code is the same either way.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 01:43:56 -05:00
Ian Chua e5324ae319 Fix Orca Cloud API requests to use HTTPS (#15391)
# Description

The default Orca Cloud API URL omits its scheme, so libcurl interprets
it as HTTP and follows the server redirect to HTTPS. Recent libcurl
versions intentionally do not forward the `Authorization` header across
protocol/port-changing redirects, causing Orca Cloud profile sync to
receive HTTP 401 `missing_authorization` responses and eventually log
the user out.

Use the HTTPS API URL directly. Besides restoring sync with current
libcurl versions, this improves security by preventing the bearer access
token from being sent in the initial unencrypted HTTP request.

# Screenshots/Recordings/Graphs

N/A — no UI changes.

## Tests

- `git diff --check`
- Confirmed with current libcurl that the scheme-less URL redirects and
loses the authorization header, while the direct HTTPS URL retains it
2026-10-07 14:30:07 +08:00
harrierpigeonandClaude Fable 5.1 cd155f07ae Tests: include <cmath> for std::sqrt in the belt overhang test
clang-tidy (misc-include-cleaner) flagged it on belt-printer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 01:29:53 -05:00
HanifKoh 559ad3e2b7 Flag Global Usings in Headers with clang-tidy (#16225)
A using-directive or using-declaration in the global namespace of a
header reaches every file that includes it, and a using-declaration also
makes the include checker treat that header as the one to include for
the name. google-global-names-in-headers reports both, on changed lines
like the existing check, so headers that still have one are not held to
it until the line is touched.

The check does not see a using inside a namespace.

clang_tidy_diff.py's closing message assumed every finding was a missing
include; it now says other findings need a manual fix.
2026-10-07 14:23:11 +08:00
HanifKoh de6b0b9f2d Remove Header Usings and Aliases Nothing Depends On (#16224)
- ClipperUtils.hpp imported jtMiter, jtRound and jtSquare into the
  global namespace for every includer. No code names them there.
- BBLStatusBar.hpp, BBLStatusBarBind.hpp, BBLStatusBarPrint.hpp,
  BBLStatusBarSend.hpp and ProgressStatusBar.hpp re-exported their class
  into Slic3r::GUI. Nothing refers to the class through that namespace.
- Jobs/SendJob.hpp, Jobs/BindJob.hpp, Jobs/UpgradeNetworkJob.hpp and
  AuxiliaryDataViewModel.hpp declared "namespace fs = boost::filesystem;"
  at global scope without using it.
2026-10-07 14:23:00 +08:00
HanifKoh f02292f163 Stop Exporting Names Through Usings in GUI and Voronoi Headers (#16223)
Each of these headers put a using or namespace alias at global or
namespace scope, which every includer inherited:

- BBLTopbar.hpp: "using namespace Slic3r::GUI;" at global scope, reached
  through MainFrame.hpp. Seven source files used GUI names unqualified
  outside the namespace because of it, one of them as "::RadioBox".
- IMSlider.hpp and TickCode.hpp: "using namespace CustomGCode;" inside
  Slic3r.
- ProjectTask.hpp, Jobs/PrintJob.hpp and ConfigWizard_private.hpp:
  "namespace fs = boost::filesystem;". PresetBundle.cpp and GUI_App.cpp
  had no alias of their own.
- VoronoiUtils.hpp: "using VD = Slic3r::Geometry::VoronoiDiagram;" at
  global scope.

The headers now spell the names out. Source files that used them get
the qualifier, or a using of their own where there are many uses.
2026-10-07 14:22:45 +08:00
HanifKoh 23546e71ce Remove Unused Usings and the Includes Kept Only for Them (#16220)
151 using-directives, using-declarations, type aliases and namespace
aliases in source and test files that nothing refers to: the name is
never used, it duplicates a using already in scope, or the code sits
inside the namespace it names. Each one was removed on its own and the
file still compiled, both as it is and with every header-level using
taken away, so none of them was only redundant because a header leaks
the same name.

With the using gone, 28 #include lines and one forward declaration had
no other reference left in their file (boost/optional.hpp without any
optional, property_tree headers without any ptree) and go with it.

No header is touched.
2026-10-07 14:22:32 +08:00
HanifKoh 73d32d4791 Stop Leaking json Through Headers and Drop Includes Kept Only for the Name (#16221)
* Stop Leaking json Through Headers and Drop Includes Kept Only for the Name

AppConfig.hpp, DeviceManager.hpp and UserManager.hpp carried a global
"using namespace nlohmann;", json_diff.hpp a global "using json =
nlohmann::json;" and PrinterFileSystem.h a global "using nlohmann::json;".
Every file that included one of them, directly or not, could write a
bare json, and 63 did without declaring it.

The last two also made the include checker treat json_diff.hpp and
PrinterFileSystem.h as the headers that provide json, so they were
included from files that use nothing else from them: 57 of the 59
includers of json_diff.hpp never name json_diff.

The five statements are removed. Headers that use the type now spell
nlohmann::json, source files declare their own "using json =
nlohmann::json;", and the includes that only supplied the name are
dropped or replaced by <nlohmann/json.hpp>.

Eight files reached json_diff.hpp only through an include that is now
gone and with it lost that header's "using namespace std;". The std
names they used unqualified are qualified.

* Declare json in OrcaSlicer.cpp on Every Platform

OrcaSlicer.cpp had its "using namespace nlohmann;" and the json include
inside the Linux-only include block, so on Windows and macOS it took
json from AppConfig.hpp's global directive, which is gone. The include
and a "using json = nlohmann::json;" now sit outside the block.
2026-10-07 14:22:16 +08:00
harrierpigeonandClaude Fable 5.1 314e227cee Profile tool: list the retired belt keys as obsolete
The loader's ignore set in PrintConfigDef::handle_legacy() gained the
belt options retired in #16236, and the profile tool's unit test checks
that its OBSOLETE_KEYS matches that set, so the Check profiles job
failed on belt-printer.  Add the twelve keys.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 00:53:05 -05:00
Joseph Robertson ef3c47135f Belt printer: retire the redundant and unused options (#16236)
Follow-up to #16195 and the review discussion on #14394 (yw4z's note
about the third column on the *Belt tilt* row). Removes the belt options
that are redundant or unused before the branch ships, so they never need
compatibility handling after a release, and fixes supports under a
leading overhang. Every removed key is on `handle_legacy()`'s ignore
list, so existing profiles and 3MFs load silently.

## Removed

- **`belt_slice_rotation_global`**, **`preslice_remap_global`**,
**`belt_preslice_global`** (*Global mesh transforms*) and
**`gcode_back_transform`** — the global mode and the back-transform are
what belt printing is; they are presumed on wherever the flags were
consulted (`PrintObjectSlice`, `BeltBackTransform`, `BeltGCode`,
`Print::process`, `PrintApply`, `GCodeViewer`). The *Belt tilt* row is
axis + angle only; the three `fdm_belt_common.json` drop the keys.
- **`preslice_remap_x/y/z`** — no profile used the pre-slice axis remap;
the belt tilt axis plus the G-code axis remap cover the machines that
exist, and its implementation only agreed with itself for a plain swap.
The forward transform is the rotation.
- **`belt_support_z_offset_mode`** and **`belt_support_floor_mode`** —
the first was never read by a generator; the second's only shipped value
(*Generator only*) is now the behaviour.
- **`first_layer_plane`**, **`first_layer_plane_offset`**,
**`first_layer_plane_thickness`** and `FirstLayerPlane.{cpp,hpp}` — the
first-layer band is measured from the belt surface and is one first
layer height thick.
- `belt_brim_instances_compatible()` and its validation warning:
instances along the belt get their brim.

## Supports under a leading overhang (the clipping at the object's local
Z = 0)

The slicing frame of a belt object started at its lowest vertex, but the
belt under the leading end of an overhang lies below that, so no
generator could reach it: normal supports stopped at the object's lowest
layer, and both tree generators carried extension hacks sized from the
pre-rotation bbox and capped at global Z = 0 (right only for the
trailing half of the belt). The frame now starts at the lowest
belt-floor point under the footprint, less a 10 mm margin along the belt
for the base of a support column, and the extensions are gone:

- **Normal supports** run in the object frame and get the global belt Z
offset shifted onto the result (as organic already did). With the offset
on the object layers, a top contact at negative Z turned the
intermediate-layer count negative and the generator allocated layers
until the kernel killed it — any overhang in the leading half of the
belt did this. The first-layer flange expansion is skipped on a belt
(the first support layer is the leading tip, not a flange).
- **Classic tree** nodes keep dropping until their whole circle is in
the belt, so a branch tapers to a tip on the belt instead of stopping a
radius above it.
- **Organic**: the belt is no longer a support blocker. A blocker is a
collision, and a branch descending onto one slides off it, down the belt
and ahead of the part; the belt is where branches end, which the
per-layer floor clipping already does.

Regression test *Belt supports reach the belt under a leading overhang*:
a cube with a fin whose underside is parallel to the layers, 20 mm ahead
of the cube and up to 41 mm of slicing Z above the belt, for normal,
organic and classic tree supports; the lowest support layer must sit on
the belt beneath its own lines.

The belt object height (the layer range) is now estimated from the box
of the mesh as placed on the bed. `raw_bounding_box()` has the
instance's Z offset removed, which was harmless for the old
rotated-extent estimate but not for one anchored at the belt floor (a
point's rotated z and the floor under it move in opposite directions
under a Z shift): with the first version of this change every part came
out as a wedge, sliced only up to its diagonal, in the GUI and CLI
alike. Caught by a GUI test pass; the leading-overhang test now also
checks that the whole part is sliced.

## Belt brim after the parallel support step

`belt_brim_obstacles()` reads every object's layers and support layers,
which another object's support step rebuilds (and now shifts) at the
same time. The brim is generated sequentially once the parallel step is
over (`PrintObject::generate_belt_brim()`). This is the race behind the
Windows arm64 segfault in *Belt brim of each object precedes its
perimeters on its own filament*.

## UI

- *Belt tilt* is two rows: the angle (Advanced) and the axis (Developer;
a profile-level kinematics choice). A shared line is shown by its first
option's mode, so they cannot share one.
- *Machine frame transforms* is five single-option rows (G-code remap X
/ Y / Z, Decouple machine-frame tilt, Machine-frame tilt angle — the
angle row only appears when decoupled) instead of two multi-column
lines; the remap fields got full labels since they stand alone now.
- The gravity indicator on the bed is a plain line along the up
direction (no cone, 60 % of the axes' length), per yw4z.
- The *Show raw G-code (belt only)* legend/canvas toggle and its `B`
shortcut are gone; the preview is the designed view.

Also carries the two-line `phong.fs` fix from #16226 (merges as a
no-op).

## Verification

- `libslic3r_tests` 1116 passed (92 648 assertions); `fff_print_tests`
351 passed (561 696 assertions).
- `scripts/clang_tidy_diff.py --base upstream/belt-printer`: no
findings.
- `scripts/orca_profile_tool.py check`: no profile references a removed
key.
- GUI target builds; a scripted GUI pass (xdotool) checked the settings
groups in every mode, slicing, export, instances, the purge tower,
calibration dialogs, the wizard, printer switching and 3MF round-trip.

The wiki pages (OrcaSlicer/OrcaSlicer_WIKI#374) get a follow-up dropping
the removed sections once this is in.
2026-10-07 00:44:41 -05:00
SoftFever ba5ecc5470 Edit the selected extruder's machine limits on the Motion ability page 2026-10-07 13:41:42 +08:00
SoftFever 82349e1ba4 Give each extruder its own machine limits 2026-10-07 13:41:42 +08:00
harrierpigeonandClaude Fable 5.1 a2f5a8ce2b Belt printer: size the layer range from the on-bed box
The belt object height is estimated from a bounding box swept through
the tilt rotation.  raw_bounding_box() has the instance's Z offset
removed, which did not matter while the estimate was the box's rotated
Z extent (a Z shift moves every corner alike), but the frame now starts
at the lowest belt-floor point under the footprint, and a point's
rotated z and the floor under it move in opposite directions under a Z
shift: the offset box under-estimated the height by twice the object's
height above the bed, so the layers stopped at the part's diagonal and
every part came out as a wedge (GUI and CLI alike; the unit tests never
checked the top).  Use the box of the mesh in the frame it is sliced in
(trafo_centered(), Z as placed on the bed), and have the leading
overhang test check that the whole part is sliced.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 00:10:29 -05:00
harrierpigeonandClaude Fable 5.1 dda58b07cd Belt printer: supports reach the belt under a leading overhang
The slicing frame of a belt object started at its lowest vertex, but the
belt under the leading end of an overhang lies below that, by the
overhang's length times the tilt's shear.  Every support generator works
in layers at z >= 0, so none of them could reach it: normal supports
stopped at the object's own lowest layer, and the two tree generators
each carried a stack of hacks to extend themselves below it (a post-hoc
copy of the lowest base area in TreeSupport, "virtual belt raft layers"
in TreeSupport3D/TreeModelVolumes), sized from the pre-rotation bbox
and capped at global z = 0, which is only right for the trailing half
of the belt.

Start the frame at the lowest belt-floor point under the footprint
instead, less a 10 mm margin along the belt for the base of a support
column (BeltSliceStrategy::apply_preslice_transforms and
BeltTransformPipeline::compute_belt_height_and_floor agree on it).  The
layers between it and the first vertex come out empty, which belt
slicing already tolerates, and the generators need no extension at all:

- normal supports: the generator anchors its layer grid at the frame
  origin, so run it in the object frame and shift the global belt Z
  offset onto the result afterwards, as organic supports already did.
  With the offset on the object layers a top contact at negative z
  turned the intermediate-layer count negative and the generator
  allocated layers until the kernel killed it (any overhang in the
  leading half of the belt).  Drop the first-layer flange expansion on a
  belt: the first support layer is the leading tip of the support, not
  a flange, and inflating it put lines in the air ahead of the belt.
- classic tree: a node now keeps dropping until its whole circle is in
  the belt, so the branch tapers to a tip on the belt instead of
  stopping, a radius above it, when its centre crosses.
- organic: the belt is no longer a support blocker.  A blocker is a
  collision, and a branch descending onto one slides off it, down the
  tilted belt and ahead of the part; the belt is where branches end,
  which the per-layer m_belt_floor clipping already does.

The belt brim is generated after the parallel support step instead of
inside it: belt_brim_obstacles() reads every object's layers and support
layers, which another object's support step rebuilds (and, now, shifts)
at the same time.  This is the race behind the Windows arm64 segfault
in "Belt brim of each object precedes its perimeters on its own
filament".

Also: the belt tilt axis moves to Developer mode as its own row (a
shared line is shown by its first option's mode), first_layer_plane
band thickness, belt_support_floor_mode, belt_preslice_global and
gcode_back_transform are retired and presumed on, the gravity arrow is
a plain line along the up direction, and the "Show raw G-code (belt
only)" preview toggle is gone.

Regression test: "Belt supports reach the belt under a leading
overhang" slices a cube with a fin whose underside is parallel to the
layers, 20 mm ahead of the cube and up to 41 mm of slicing Z above the
belt, for normal, organic and classic tree supports, and checks that
the lowest support layer sits on the belt beneath its own lines.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 22:14:25 -05:00
Joseph Robertson 7f749ee02b Merge branch 'main' into belt-printer 2026-10-06 18:27:21 -05:00
harrierpigeonandClaude Fable 5.1 8039d4d2ac Belt printer: retire the redundant and unused options
Removed, with the keys added to handle_legacy()'s ignore list so saved
profiles and 3MFs keep loading:

- belt_slice_rotation_global and preslice_remap_global. Both were only
  consulted when belt_preslice_global ("Global mesh transforms") was off,
  which no profile does; belt_preslice_global is now the single global
  mode and is presumed on everywhere the old flags were ORed in
  (PrintObjectSlice, BeltBackTransform, BeltGCode, Print::process,
  PrintApply). The Belt tilt row is axis + angle only.
- preslice_remap_x/y/z. No profile used the pre-slice axis remap; the belt
  tilt axis plus the G-code axis remap cover the machines that exist, and
  its implementation only agreed with itself for a plain swap (matrix
  columns vs remap_bbox rows). BeltTransformPipeline::build_preslice_remap,
  remap_bbox and has_preslice_remap are gone, the forward transform is the
  rotation, and the G-code header no longer carries the remap.
- belt_support_z_offset_mode. Saved and invalidated steps, but no support
  generator read it.
- first_layer_plane and first_layer_plane_offset, with FirstLayerPlane.cpp.
  On every shipped configuration the band is measured from the belt
  surface (GCode::belt_height_above_floor) and the evaluator was only
  reached for an explicit XY/YZ/XZ choice or a non-zero offset, which
  nobody set. first_layer_plane_thickness stays as the band unit,
  relabelled "First layer band thickness".

UI: the Machine frame transforms group is five single-option rows (G-code
remap X / Y / Z, Decouple machine-frame tilt, Machine-frame tilt angle;
the angle row is shown only when decoupled) instead of two multi-column
lines, and the remap fields carry full labels.

Also carries the phong.fs struct fix from #16226 so the worktree build
links its shaders.

libslic3r_tests and fff_print_tests pass; clang-tidy diff check clean;
orca_profile_tool.py check clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 18:14:34 -05:00
Rodrigo Faselli 78f74a6276 add missing headers of test_design_sketch_tool.cpp (#16233) 2026-10-06 18:25:28 -03:00
Kris AustinandIoannis Giannakas c3cfb03dfe fix: crash on macOS when quitting from the Dock with the Bambu network plugin loaded (#16212)
Co-authored-by: Ioannis Giannakas <59056762+igiannakas@users.noreply.github.com>
2026-10-06 17:22:20 -03:00
dependabot[bot] 1c247bf266 Bump actions/create-github-app-token from 1 to 3 (#16049)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-06 16:42:09 -03:00
dependabot[bot] d08b6b5096 chore(deps): bump actions/setup-python from 6 to 7 (#15057)
Signed-off-by: dependabot[bot] <support@github.com>
2026-10-06 16:42:01 -03:00
Ian Bassi af05a991ee Translate new Design tab strings and auto-label localization (#16227) 2026-10-06 16:26:08 -03:00
Joseph Robertson 0c81d6f97e Belt printer: declare up_direction in both phong fragment shaders (#16226)
Fixes the `unable to load shaders: phong` error at startup on
`belt-printer` after #16195. That PR added `vec3 up_direction` to the
`SlopeDetection` uniform struct in `phong.vs` (110 and 140) but not in
`phong.fs`, so the vertex and fragment stages declared the `slope`
uniform with different struct types and the program failed to link.
`gouraud.fs` already carried the member; `phong.fs` now does too.
Shader-only change.
2026-10-06 14:03:28 -05:00
harrierpigeonandClaude Fable 5.1 2867a701af Belt printer: declare up_direction in both phong fragment shaders
#16195 added slope.up_direction to the SlopeDetection uniform struct of
phong.vs (110 and 140) but not to phong.fs, so the two stages declared the
uniform with different types and the program failed to link: "unable to
load shaders: phong" at startup, and studio lighting / realistic phong
rendering fell back. gouraud.fs already carries the member.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 13:59:28 -05:00
Joseph Robertson 199758f67f Belt printer: link every belt option to its wiki page (#16198)
Gives every option in the *Belt printer* and *Machine frame transforms*
groups, the build plate tilt, the belt purge tower enable and the belt
purge tower width a wiki link (the *Wiki* button next to the option),
pointing at the pages and anchors added in
OrcaSlicer/OrcaSlicer_WIKI#374. The two purge tower links that pointed
at a whole page now point at their section. String arguments and
`label_path` assignments only; the wiki's Tab-link validator passes
against this `Tab.cpp` with that branch.
2026-10-06 13:03:40 -05:00
SoftFever c740ddf0a6 Publish each extruder's own retraction and Z-hop values 2026-10-07 01:45:24 +08:00
SoftFever d068468bbb Simplify the unified Extruder page and its Publish dialog switch 2026-10-07 01:40:10 +08:00
SoftFever 67e29f69e7 Merge branch 'main' into exturuder-tab-multi-switch 2026-10-07 01:38:10 +08:00
SoftFever 3f36a86f39 Use PrusaSlicer's filament-change settings on Prusa MMU3 and INDX printers (#16209)
# Description

Filament changes on the Prusa CORE One MMU3, MK4 MMU3 and CORE One INDX
now use the settings PrusaSlicer 3.0 ships for them. On the MMU3
printers these are Prusa's per-material ramming, load and unload speeds,
cooling moves and stamping. On the INDX they are Prusa's multi-tool
ramming and 10 mm³ minimal purge, plus a filament start G-code that
restores pressure advance after the purge station disables it and then
sends `M573 R`, as PrusaSlicer does.

This supersedes #16008. Thanks to @nuclearmistake for that work, which
identified what these printers need for reliable filament changes. This
PR reaches the same goal with the existing `include` mechanism rather
than new printer-level overrides. The values live in shared templates in
the Prusa bundle (PLA and PETG families with High Flow ramming variants,
and one for the INDX), and the filament presets include them, so each
material keeps its own values as in PrusaSlicer. For the MK4 MMU3, ten
small presets inherit the MK4 filament tunes and include the templates;
they cover the materials PrusaSlicer offers with the MMU3 and replace
the library generics as that printer's defaults. The CORE One MMU3 0.6
nozzle now uses the 0.6 Generic PLA and Prusament rPLA profiles, as in
PrusaSlicer. Unlike #16008, filaments from Orca's shared library keep
Orca's defaults on these printers.

No engine change, and printers without an MMU3 or INDX are unaffected.
MK4 MMU3 users also get the MK4's material tuning, such as temperatures
and cooling, from the new presets.

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-10-07 01:35:47 +08:00
SoftFever 748d86d89f Orca CAD feature improvements (#16158)
# Description

This PR follows up on #16019 with fixes and workflow improvements to the
Design tab. Sketching now waits for you to pick a plane, the reference
planes stay out of the way until they are needed, and the camera follows
the mouse controls set in Preferences, as suggested by @Felix14-v2 in
https://github.com/OrcaSlicer/OrcaSlicer/pull/16019#issuecomment-5932747510.
It also fixes rendering, camera and Move bugs, which are listed below.


https://github.com/user-attachments/assets/6aac0a03-8405-403a-93b2-1fbf95da9fe7

**Improvements:**
- Pan and orbit follow Preferences > Control while sketching too. A
right-drag no longer ends the polyline chain or drops the point already
placed, and in the Touchpad camera style Alt+move and Shift+move orbit
and pan while a draw tool is selected.
- The XY, XZ and YZ planes are separate labelled squares around the
origin, in their axis colours, with dash-dot axes. They no longer cross
through the bed as one grey smear. Hovering over a plane greys it out,
and selecting one makes it solid.
- The reference planes stay hidden until a sketch needs them. A new
Origin row in the Feature tree keeps them on screen, and Ctrl+Shift+O
toggles it.
- A sketch opens only once its plane is chosen. If a flat face or a
reference plane is already picked, the sketch opens on it at once.
Otherwise the planes appear, and the next plane or face you click opens
the sketch. Esc or Cancel leaves without one.
- The Bed checkbox is now a row under Origin, and its state is
remembered across sessions. Both rows sit above the feature list as
fixed view switches.
- Zoom to selection is available on Feature tree and Bodies rows and in
the right-click menu.
- A Sketch button replaces the FEATURES label, the import icons now have
an arrow, and the document icons are sized consistently.

**Fixes:**
- Feature previews no longer z-fight with the bodies. The Hole preview
now hides the bodies and shows only the result, as Fillet/Chamfer and
Draft already do.
- Body edge lines stay in sync with the bodies after undo, hide and
delete.
- The bed, grid and reference planes stay aligned on every plate, not
only the first.
- The Fit camera button frames the selection and sketches, or everything
on show, instead of always framing the whole bed.
- Grabbing a move arrow no longer makes the body jump on the first drag.
- Move no longer gets stuck after Esc or a click off the gizmo. Esc and
Cancel put the body back, and undo waits until the Move is confirmed or
cancelled.
- The camera stays still when the first body appears.
- The view buttons no longer cover the status line at display scales
above 100%.
- The import buttons no longer stay highlighted after a click.
- The Bodies right-click menu no longer closes at once on Linux.

# Screenshots/Recordings/Graphs

- **Z fighting issue**
Before fix:


https://github.com/user-attachments/assets/0da30897-8d81-494a-a206-9b4dfd8fa9c0

After fix:



https://github.com/user-attachments/assets/04ccad39-dbbc-4bcf-8d5c-3ba4894cb755


- **Sketch plane rendering**
Before:
<img width="374" alt="{CCFCEB16-20FA-4202-A5F8-5F2219E44C0B}"
src="https://github.com/user-attachments/assets/2dfd19b2-9991-45c7-91e3-e139d111229c"
/>

After:

<img width="1706" height="1258" alt="image"
src="https://github.com/user-attachments/assets/3ec89038-4614-4c04-9f20-37bfc50ee5ef"
/>


- **Wrong body frame lines**
Before:


https://github.com/user-attachments/assets/81ccae26-2a9d-4ecb-9928-678dbe9fb94c

After


## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-10-07 01:34:18 +08:00
SoftFever 95a168a376 Merge branch 'main' into feature/CAD-improvements 2026-10-07 01:32:42 +08:00
SoftFever f69eccd32a Open the Design tab on a front-right isometric view 2026-10-07 01:23:21 +08:00
Ian BassiandIoannis Giannakas c1baacf8a0 fix: thin-walled holes classic walls flip the direction (#16051)
Co-authored-by: Ioannis Giannakas <59056762+igiannakas@users.noreply.github.com>
2026-10-06 14:09:36 -03:00
SoftFever 7708ca568d Keep the Design tab's camera still when the first body appears 2026-10-07 00:30:25 +08:00
SoftFever 505a712f86 Replace the Design tab's FEATURES label with a Sketch button 2026-10-07 00:30:25 +08:00
SoftFever e4b0828644 Add Zoom to selection to the Design tab's rows and right-click menu 2026-10-07 00:01:07 +08:00
SoftFever ec21c84738 Keep the Design tab's Bodies right-click menu open on Linux 2026-10-06 22:46:00 +08:00
Rodrigo Faselli 13d4a0d922 Avoid center plug for internal solid infill (spiral inset) (#15705)
avoid center plug for internal solid infill
2026-10-06 11:41:25 -03:00
Ian Bassi c74ac8a6c3 Keep FPS/timings overlay below the toolbar on narrow canvases (#16215) 2026-10-06 11:36:47 -03:00
Ian Bassi 2d90345d1e Move fill-pattern-tops wiki under 3D Honeycomb (#16214) 2026-10-06 10:34:28 -03:00
Ioannis Giannakas 5a8eb3f14e Fix one wall on top dropping inner walls of narrow Arachne walls (#16174)
* Fix one wall on top dropping inner walls of narrow Arachne walls

* Fix clang tidy errors on the test suite
2026-10-06 14:07:01 +01:00
Kris Austin e7d6f5e6c0 fix: crash in LAN mode when the printer type is not known yet (#16191)
* fix: crash in LAN mode when the printer type is not known yet

InputIpAddressDialog::set_machine_obj() built the help image name from
the printer config with no fallback. If the printer type is empty or
unknown, for example before the first push_all arrives on a flaky LAN
link, the lookup returns "" and create_scaled_bitmap("_en") throws.
The dialog is opened by the "LAN Connection Failed" handlers in
MediaPlayCtrl and MediaFilePanel, where nothing catches the exception,
so the app crashes.

Use input_access_code_x1 when there is no image, and the _cn image for
zh_CN, the same as ConnectPrinterDialog::init_bitmap().

Ported from Bambu Studio 52ca2ec5d1.

* fix: return an empty bitmap for an empty icon name

create_scaled_bitmap() threw when a caller passed an empty name. That
happens when a printer config lookup has no entry, for example in
AMSSetting::update_ams_img() for a printer type with no AMS image.
Log an error and return wxNullBitmap instead.

Ported from Bambu Studio 52ca2ec5d1.
2026-10-06 09:55:25 -03:00
SoftFever ce7d4815e6 Set the Design tab's Origin and Bed rows apart from the feature list
They now sit above the features' framed list as fixed view switches:
a click no longer selects them, they stay put while the features scroll,
their eyes line up with the features' eyes, and their labels dim when
hidden. Ctrl+Shift+O toggles the Origin from the keyboard, as
Ctrl+Shift+B does the Bed. The test script's position for the first
feature row is calculated, not measured, and needs re-measuring on the
test setup.
2026-10-06 20:46:09 +08:00
SoftFever 6d8a13c33b Move the Design tab's Bed toggle into the Feature tree, under the Origin row
The bed's show/hide state is now remembered across sessions like the Origin
row's, shown by default. The GUI ladder's ribbon x-coordinates are shifted by
the removed checkbox's derived width and still need re-measuring on the rig.
2026-10-06 20:44:43 +08:00
SoftFever 4d4e52722f Open the Design tab's sketches only once their plane is picked
Sketch on a picked flat face or reference plane opens the sketch on it at once. With nothing
picked it no longer enters sketch mode: the reference planes and axes come up, and the plane or
flat face clicked next opens the sketch and puts them away. Esc or Cancel leaves without one.
A picked plane is used up by the sketch on it and dropped by Esc or a click on nothing, the plane
prompt is no longer replaced by a stale tool hint, and clicking the face a sketch was just
cancelled on picks that face again rather than the whole body.
2026-10-06 20:17:47 +08:00
SoftFever 919f507cca Merge branch 'main' into exturuder-tab-multi-switch 2026-10-06 20:04:09 +08:00
SoftFever 26329e7147 Show Left/Right Extruder or Extruder N on the extruder switch 2026-10-06 20:03:07 +08:00
SoftFever 406ef0dd03 Stop the Design tab's import buttons from staying highlighted after a click 2026-10-06 19:25:22 +08:00
SoftFever 206a7461ea Give the Design tab's import icons an arrow and size its document icons consistently 2026-10-06 19:03:30 +08:00
Kris Austin e098c933f0 perf: write post-processed G-code without per-line copies to speed up export by up to 6% (#16167)
* perf: write post-processed G-code without a per-line copy

* perf: size the post-process line map from the first pass

* test: line ends of the exported G-code

* test: include the headers the line-ends test and gcode() helper use
2026-10-06 07:50:08 -03:00
SoftFever 78a22fca93 Hide the Design tab's reference planes until a sketch needs them, with an Origin row to pin them 2026-10-06 18:38:57 +08:00
SoftFever db33aaa4dc Extend Prusa's tip forming to the MK4 MMU3 and CORE One INDX 2026-10-06 18:25:06 +08:00
SoftFever cd959d5d87 Match PrusaSlicer's tip forming on CORE One filaments
Ramming, load/unload, cooling-move and stamping values follow the
PrusaSlicer 3.0 presets, which retune several of them relative to
2.9.6. The CORE One MMU3 0.6 nozzle now uses the 0.6 Generic PLA and
Prusament rPLA profiles, as in PrusaSlicer.
2026-10-06 18:02:32 +08:00
SoftFever e47fdcaef7 Merge branch 'main' into exturuder-tab-multi-switch 2026-10-06 17:56:06 +08:00
HanifKoh d1afb1fed6 Use a System clang-tidy When Available and Make --fix Converge in One Pass (#16199)
* Use a System clang-tidy When Available and Make --fix Converge in One Pass

scripts/run_clang_tidy.sh only looked at CLANG_TIDY and the venv it creates,
so a clang-tidy already on the system was never used. It is now the first
choice: the pinned version outright, another version after a prompt that
says results may differ slightly from CI, which -y and an existing pinned
venv skip.

Two problems in clang_tidy_diff.py made --fix need several runs and still
leave the plain check failing:

- A deleted #include orphans uses on unchanged lines. The plain check runs
  such a file whole and reports them, but --fix kept the line filter to the
  changed lines, so they were never fixed. Fix mode now runs the file whole
  first and then fixes exactly the changed lines plus the lines that run
  found wanting, so unrelated lines are still never rewritten.

- clang-tidy exits non-zero for the findings it just fixed, so every fixed
  file was reported as failed and the user ran --fix again to see what was
  left. A file --fix changed is now checked again and the fixed files are
  listed separately from what --fix could not add.

CI runs the script without --fix and is unchanged.

* Keep the a/ b/ Diff Prefixes Whatever the User's Git Config Says

parse_diff recognises a changed file by its +++ b/ header. With
diff.noprefix or diff.mnemonicPrefix set, git prints +++ src/x.cpp or
+++ w/src/x.cpp instead, every file was dropped, and the local check
reported no changed C++ lines. The diff is now asked for the a/ and b/
prefixes outright, which overrides both settings.

* Warn When No Remote Points at OrcaSlicer/OrcaSlicer

Without one, run_clang_tidy.sh compares against origin/main. When origin
is a fork whose main already holds the commits, the check finds nothing
and says so, without hinting at why. The script now names the base it
fell back to and how to point it at the upstream repository.
2026-10-06 17:16:09 +08:00
SoftFever 064eee8b84 Stop the Design tab's body Move from getting stuck after Esc or a click off the gizmo
Esc and Cancel now put the body back, and Confirm keeps the new position. A click off the
gizmo only moves the camera and no longer leaves the Move card and its buttons dead. Starting
another edit or a rebuild keeps the position, and undo is refused until the Move is confirmed
or cancelled.
2026-10-06 16:57:08 +08:00
SoftFever 250fed8808 Show the Design tab's reference planes as separate labelled squares around the origin
The XY, XZ and YZ planes no longer cross through the bed. Each is a small square in its axis
colour, set off from the axes into the corner that faces the default front view, with its name
written in the plane. Dash-dot axes run between the squares and replace the bed's axis triad
while they show. Hovering a plane greys it and selecting one makes it solid, and picking a
solid face now clears a previously picked plane.
2026-10-06 16:32:36 +08:00
TheLegendTubaGuy ff3c151417 Fix BBL infill retraction reduction default 2026-10-06 03:26:24 -05:00
yw4z f8dd56053c Match style of height range modifier section on sidebar (#15703)
* init

* update

* rescale layer icon
2026-10-06 11:00:04 +03:00
Ioannis Giannakas 6561c4d35b Merge branch 'main' into main 2026-10-06 08:01:59 +01:00
SoftFever 0c2e26fa3d Stop the Design tab's move arrows from jumping the body on the first drag
Grabbing an arrow anywhere along its length snapped the body's centre to that point as soon
as the mouse moved. The body now moves by how far the cursor travels from where the arrow
was grabbed, including when it is grabbed while looking straight down the axis.
2026-10-06 14:48:48 +08:00
SoftFever e328b60992 Make the Design tab's reference planes readable where they cross
The XY/XZ/YZ planes are cut along each other and drawn back to front, with
lines along every crossing, and their fills are strong enough for the order
to show. Before, they blended into one grey smear and were too pale to work with.
2026-10-06 14:19:46 +08:00
harrierpigeonandClaude Fable 5.1 97f0b49a1e Belt printer: link every belt option to its wiki page
Every option in the Belt printer and Machine frame transforms groups, the
build plate tilt, the belt purge tower enable and the belt purge tower
width get a wiki link, matching the pages added in
OrcaSlicer/OrcaSlicer_WIKI#374. The two purge tower links that pointed at a
page now point at their section.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 00:56:52 -05:00
Joseph Robertson 34e3fabc56 Belt printer: qualify Slic3r::Polygon in the blocker-index test (#16196)
Fixes the Windows x64 and arm64 build failures on `belt-printer` after
#16195: `tests/fff_print/test_print.cpp` includes `<Windows.h>`, so the
unqualified `Polygon` in the new TreeModelVolumes blocker test is
ambiguous with GDI's `Polygon()` (`error: reference to 'Polygon' is
ambiguous`). It is the only error in both logs. The type is now written
`Slic3r::Polygon`.
2026-10-06 00:23:50 -05:00
harrierpigeonandClaude Fable 5.1 f97aa6a5af Qualify Slic3r::Polygon in the blocker-index test
tests/fff_print/test_print.cpp includes <Windows.h>, so an unqualified Polygon
in the new TreeModelVolumes test is ambiguous with GDI's Polygon() and fails
the Windows x64 and arm64 builds on belt-printer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 00:12:59 -05:00
SoftFever afa9252b59 Make the Fit camera button frame the Design tab's selection and sketches
In the Design tab the button always swung the camera to the plate view and framed the whole
bed, whatever was selected: the tab's picks and sketches are neither the canvas's selection
nor its volumes. It now frames the selected faces, body, edges, vertex, sketch region or
sketch entities, and with nothing selected everything on show — the visible bodies, the
feature preview and the sketches — keeping the current view direction. An empty tab still
frames the bed as before.
2026-10-06 12:28:52 +08:00
Joseph Robertson d2aa937ad7 Merge branch 'main' into belt-printer 2026-10-05 23:20:23 -05:00
Joseph Robertson c43d541267 Belt printer: address the review on #14394 (#16195)
Follow-up to #14394, addressing @raistlin7447's review (review
5421968464) item by item, plus the tests it asked for.

## Review items

1. **Stale belt offsets after switching printers** —
`PrintObject::slice()` now zeroes `m_belt_min_z`,
`m_belt_global_z_offset` and `m_belt_global_xy_correction` before
slicing. They were only written in belt mode, so a project switched to a
normal printer (or whose tilt axis was set to None) kept the old
offsets, which shifted the adaptive infill octree and the organic
support layers.
2. **Blocker indexing in `TreeModelVolumes`** — a test now pins the
index the support blockers land on with a raft (object layer + raft
layers), including the layers just below and just above where an
unshifted blocker would sit.
3. **Arrange clamp** — the final-alignment clamp in libnest2d is opt-in
(`NfpPConfig::clamp_to_bin`) and arrange sets it for belt printers only.
Printers with an off-centre `best_object_pos` (A1 mini, H2 family) keep
their alignment; a flat-bed test pins that and the existing clamp test
is now a belt test.
4. **Belt view from the file, not the preset** —
`GCodeProcessor::apply_config(DynamicPrintConfig)` carries the file's
belt keys (and, for a belt file, its
`printable_area`/`printable_height`, which the Rev remaps need) into
`export_config_for_render()`; `GCodeViewer` enables the belt view from
the header tilt. A normal `.gcode` opened with a belt printer selected
is no longer back-transformed, and a belt file opened on another printer
brings its own tilt and remaps.
5. **Purge-prism snap vs. support-only changes** —
`belt_shift_layer_grid()` also shifts `m_belt_floor_z_shift_cached` and
`m_belt_global_z_offset`, so the restored floor and the organic support
layers follow the snapped grid.
6. **Raft / draft shield on a belt** — `update_print_fff_config()`
resets `raft_layers` and `draft_shield` with the usual warning dialog
instead of only greying out the fields `Print::validate()` rejects.
7. **First-layer travel speed and second-layer temperature** —
`GCodeWriter` takes a first-layer point test instead of the
`FirstLayerPlane`; `GCode` installs one that goes through
`on_first_layer(point)` (the belt surface, as the extrusions use),
converting the writer's logical point back to the object frame.
`past_first_layer_band` uses a new `belt_layer_past_first_layer_band()`
on the same basis. The `FirstLayerPlane` path is kept for an explicit
XY/YZ/XZ choice or a non-zero plane offset, as before.
8. **Leading-edge brim test** — `belt_brim_clip_leading_edge()` is
exported and called by both the generator and the test (which also
checks the kept area and the cut-beyond-region cases).
9. **phong.vs** — both `110/phong.vs` and `140/phong.vs` get
`up_direction` and the `dot()` slope test, so studio lighting and
realistic phong highlight overhangs with the tilt.

## Remap gating

`preslice_remap_*` and `gcode_remap_*` are gated on `belt_printer`
through one helper, `BeltTransformPipeline::axis_remap_enabled()`. The
fields are only offered in the belt group, so a value left in a profile
must not change a non-belt print. That helper is the one place to widen
if a non-belt use ever needs them.

## Tests (as requested)

- Belt-only keys at non-default values leave non-belt G-code unchanged.
- Switching a sliced project from belt to non-belt (and to tilt axis
None) matches a fresh slice.
- A support-only change on a belt purge print matches a fresh slice.
- Non-belt start G-code moves keep the first-layer Z in the processor.
- The belt brim's segment count (not pass count) catches a band emitted
twice back to back.

## One fix outside belt code

The belt-to-non-belt test exposed a gap that `main` shares:
`PrintObject::invalidate_step(posSlice)` re-invalidates
`posSupportMaterial` but not `posSimplifySupportPath`
(`invalidate_steps()` does not propagate), so after any re-slice the
regenerated support paths were exported unsimplified — extra vertices
and tiny `E.00001` moves. `posSimplifySupportPath` is now in that list;
with it the re-sliced and fresh outputs match byte for byte (comments
aside).

## Verification

- `libslic3r_tests`: 1116 passed, 2 skipped. `fff_print_tests`: 351
passed (561 427 assertions). Built on Linux with GCC against OCCT 8.0.1
deps.
- `scripts/clang_tidy_diff.py -p build-tidy --base eb5b9a77b9`: no
findings.
- The GUI files (`ConfigManipulation.cpp`, `GCodeViewer.cpp`) compile;
the preview change was not exercised interactively.
2026-10-05 23:19:41 -05:00
harrierpigeonandClaude Fable 5.1 61a0db4a87 Belt printer: address the review on #14394
Code review items (raistlin7447):

1. PrintObject::slice() zeroes m_belt_min_z, m_belt_global_z_offset and
   m_belt_global_xy_correction before slicing. They were only written in belt
   mode, so a project switched to a normal printer, or whose tilt axis was set
   to None, kept the old offsets and shifted the adaptive infill octree and the
   organic support layers by them.
2. TreeModelVolumes shifts the support blockers into the raft-offset index
   space; a test now pins the index the blocker lands on.
3. The final-alignment clamp in libnest2d is opt-in (NfpPConfig::clamp_to_bin)
   and arrange sets it for belt printers only. Printers with an off-centre
   best_object_pos keep their alignment; a flat-bed test pins that.
4. The preview's belt view follows the loaded G-code, not the selected printer:
   GCodeProcessor carries the file's belt keys (and, for a belt file, its bed)
   into export_config_for_render(), and GCodeViewer enables the belt view from
   the header tilt.
5. belt_shift_layer_grid() also shifts the cached belt floor and the global Z
   offset, so a support-only or brim-only change after the purge-prism snap
   matches a fresh slice.
6. update_print_fff_config() resets raft_layers and draft_shield on a belt
   printer instead of only greying out the fields Print::validate() rejects.
7. GCodeWriter takes a first-layer point test instead of the FirstLayerPlane;
   GCode installs one that measures from the belt surface, like its
   extrusions, so the first-layer travel speed and the second-layer
   temperature change no longer depend on the gcode_remap_* convention.
8. belt_brim_clip_leading_edge() is exported and called by both the generator
   and the test.
9. Both phong.vs shaders use slope.up_direction for the overhang highlight.

The pre-slice and G-code axis remaps are gated on belt_printer through
BeltTransformPipeline::axis_remap_enabled(), so belt keys left in a profile
cannot change a non-belt print.

Tests requested in the review: belt-only keys at non-default values leave
non-belt G-code unchanged; switching a sliced project from belt to non-belt
(and tilt axis None) matches a fresh slice; a support-only change on a belt
purge print matches a fresh slice; non-belt start G-code moves keep the
first-layer Z in the processor; the belt brim's segment count catches a band
emitted twice.

The belt-to-non-belt test exposed an unrelated gap: invalidate_step(posSlice)
re-invalidated posSupportMaterial but not posSimplifySupportPath, so after
any re-slice the regenerated support paths were exported unsimplified.
posSimplifySupportPath is now in that list.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-05 23:06:01 -05:00
SoftFever 56ebde968a Keep the Design tab's status line clear of the view buttons at any display scale
The line started at a fixed inset scaled like the ImGui style, while the navigator and the
round canvas buttons scale with the monitor's DPI on Windows, so at 150% the buttons covered
its first words. It now starts past the edge the canvas reports for that corner.
2026-10-06 11:55:41 +08:00
SoftFever 840d44b611 Keep the Design tab's edge lines in step with the bodies after undo, hide and delete 2026-10-06 11:21:18 +08:00
Joseph Robertson a3dea04ade Belt printer: add the includes the clang-tidy include cleaner asks for (#16192)
Follow-up to #14394. The `clang-tidy` job on that PR fails on 129
`misc-include-cleaner` findings: the belt sources and tests use `std::`,
Eigen, `Point`/`PrintConfig` and `BeltBrim` symbols without including
the header that provides them, which only compiled because the
precompiled header supplied it.

This adds every include the job names, in each file's existing include
style (`"../"` in the `GCode/` and `Support/` subdirectories, quoted
`libslic3r/` paths in the GUI and tests). 41 files, includes only, no
code changes.

Verified locally with the job's own command, `scripts/clang_tidy_diff.py
-p build-tidy --base eb5b9a77b9` (compile database configured with
`SLIC3R_PCH=OFF`, clang-tidy 22.1.8): no findings left on the 105
changed files.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01L6Kg5igmmMU2YLoK6HrsWV
2026-10-05 21:40:18 -05:00
harrierpigeonandClaude Fable 5.1 9dbd0307f8 Add the includes clang-tidy's include cleaner asks for on the belt files
The clang-tidy job on #14394 fails on 129 misc-include-cleaner findings:
the belt sources and tests use std::, Eigen, Point/PrintConfig and
BeltBrim symbols without including the header that provides them, which
only compiled because the precompiled header supplied it. Every include
the job names is added, in each file's existing include style ("../" in
the GCode/ and Support/ subdirectories, quoted libslic3r/ paths in the
GUI and tests). No code changes.

Verified with scripts/clang_tidy_diff.py -p build-tidy --base eb5b9a77b9
(SLIC3R_PCH=OFF compile database, clang-tidy 22.1.8): no findings left.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6Kg5igmmMU2YLoK6HrsWV
2026-10-05 20:43:18 -05:00
Kris Austin 1d577ea4e2 build: add the missing includes only a Windows build reports (#16110) 2026-10-05 21:49:32 -03:00
Kris Austin 2bd868ecd0 perf: avoid print config copies to speed up slicing by up to 5% (#16183) 2026-10-05 21:21:13 -03:00
Kris Austin 9e5bfc272c fix: Compare Presets crash on filaments with different variant counts (#16176) 2026-10-05 21:20:12 -03:00
Kiss Lorand fefedb66c4 Fix overlapping internal bridges (#16177) 2026-10-05 21:17:20 -03:00
David Eccles (gringer)andRodrigo Faselli 1dcbb2c02a Fill in truncated octahedron tops (optional setting) (#12541)
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-10-05 20:32:18 -03:00
Rodrigo Faselli b4577dbdc4 Hide smooth factor if there is no infill. (#16172) 2026-10-05 20:27:37 -03:00
Joseph Robertson bfd3c32ef7 Preview: reuse a belt print's toolpaths until the view is toggled (#16173)
raistlin7447 pointed this out on #14394. The preview skips reconverting
a G-code result it already shows, but belt printers were exempt from
that check, so every time you came back to the Preview tab on a belt
print, it reconverted every toolpath and uploaded it to the GPU again,
even though nothing had changed.

The exemption existed for one reason: switching between the designed and
raw views changes the toolpaths for the same result, so the B toggle
needs a fresh conversion. This change keeps that, but narrows it. The
viewer now remembers which view the result was converted for and reuses
it as long as the view hasn't changed. B, the legend checkbox and the
toolbar menu still trigger a new conversion. The print settings the
back-transform reads can't change without producing a new G-code result,
so the result id covers those the same way it does for every other
printer.

I checked it with the same scripted GUI run on both builds: a BabyBelt
Pro benchy, sliced once, then three Prepare → Preview round trips and
two presses of B, counting the viewer's own log messages.

- Before: 10 full conversions, 0 reuses.
- After: 3 full conversions (the slice and the two B presses) and 7
reuses for the tab switches. B still switches views and comes back
exactly.

The merged tree builds cleanly, and the fff_print (including all the
belt tests) and libslic3r suites pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01AJzy1xeQV3FePh5HfahDyn
2026-10-05 15:46:43 -05:00
SoftFever 34696f5651 Name the Design canvas flag after its role, not the axes it once moved 2026-10-06 03:44:15 +08:00
SoftFever c300fa1e8d Keep the Design tab's bed, grid and reference planes aligned on every plate 2026-10-06 03:41:33 +08:00
harrierpigeon 98c5eed4a6 Preview: reuse a belt print's toolpaths until the view is toggled
load_as_gcode() skips the conversion and GPU upload when it is handed the
result it already shows, but belt printers were exempt from that cache,
because the designed/raw view changes the toolpath geometry for the same
result. So every preview reload of a belt print, switching back to the
Preview tab for one, converted and uploaded every toolpath again.

Remember the view the result was converted for and reuse it while both
match. Toggling the view (B, the legend checkbox, the toolbar menu) still
converts again. The print config the back-transform reads cannot change
without a new G-code result, so the result id covers it as it does on any
other printer.

Suggested by raistlin7447 on #14394.
2026-10-05 14:19:53 -05:00
Joseph Robertson 8d0608275b Check profiles: leave engine PRs' slice sweep to Build all (#16169) 2026-10-05 14:05:17 -05:00
harrierpigeon 2cb72dce0a Check profiles: leave engine PRs' slice sweep to Build all
The slice sweep here runs the nightly validator, built from main, so it
cannot expand custom G-code that uses a setting the PR adds to the engine
and reports it as an undefined placeholder. The belt printer PR fails on
exactly that: the BabyBelt Pro start G-code passes
[belt_slice_rotation_angle] to its firmware, and main has no such setting.

A PR that changes src/ also runs Build all, whose Slice check runs the same
sweep with the validator built from the PR (it passes on that PR). So the
sweep here now runs only for PRs that leave src/ alone, which are the
profile-only PRs it exists for and the ones pr-merge-bot gates on. If the
base commit cannot be fetched, the sweep runs as before.
2026-10-05 14:02:15 -05:00
Joseph Robertson ac51e157ea Belt printer: merge main (Oct 5) (#16168)
Merges `main` (eb5b9a77b9) into `belt-printer` so #14394 is mergeable
again. It had gone CONFLICTING after main moved 55 commits past this
morning's merge.

The only conflict is in `src/slic3r/GUI/Plater.cpp`: main translates the
pressure-advance test name (#16142) on the line right after the belt
guard that keeps PA Line and PA Pattern off belt printers. Both are
kept:

```cpp
    // ORCA-Belt: PA Line / PA Pattern have the belt plumbing in place ...
    { ... belt guard unchanged ... }
    const auto calib_pa_name = _L("Pressure Advance Test");
```

Main's other changes since the last merge that touch belt-modified files
were checked by hand, and none of them reach belt code:
- `GLCanvas3D.cpp`: popup flag and comments around the canvas-toolbar
menu; the belt "Show raw G-code" item is untouched.
- `GCodeViewer.cpp`: position-window scrollbar colours.
- `Tab.cpp`, `calib_dlg.cpp`, `PrintConfig.cpp`, `ArrangeJob.cpp`,
`bbs_3mf.cpp`, `GUI_App.cpp`, `GUI_ObjectList.cpp`, `Plater.hpp`: small
edits away from belt code.

A follow-up PR fixes the red **Check profiles** on #14394; merge it
right after this one.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01AJzy1xeQV3FePh5HfahDyn
2026-10-05 13:57:15 -05:00
harrierpigeon a7c1595738 Merge upstream/main into belt-printer
Brings belt-printer up to main eb5b9a77b9. One conflict: main translates
the pressure advance test name (#16142) on the line after belt's guard that
keeps PA Line and PA Pattern off belt printers; both are kept.
2026-10-05 13:40:41 -05:00
Ian BassiandAlexandre Folle de Menezes eb5b9a77b9 Update Translations + String improvements (#16142)
Co-authored-by: Alexandre Folle de Menezes <afmenez@gmail.com>
2026-10-05 14:43:19 -03:00
Kris Austin c1e6de7e4e Fix the clang-tidy check on Windows and for unusual file paths (#16163)
run_clang_tidy.ps1 had not been run on Windows before.

- Run native commands through Invoke-Quiet. Under
  $ErrorActionPreference = "Stop", Windows PowerShell made CMake's
  first stderr line fatal, so configure always failed.
- Pass the --line-filter name with native separators. clang-tidy
  matches it against the end of the file's native path, so on Windows
  every misc-include-cleaner finding was dropped.
- Decode subprocess output as UTF-8 and let stdout replace characters
  it cannot encode. A changed line with text such as 打印 crashed the
  script under cp1252.
- Check VCToolsInstallDir and WindowsSdkDir in VsDevCmd's output
  before applying it, so a failure names the command to run and leaves
  the calling shell untouched.
- Log the git_commit_hash_header build, use -LiteralPath for logs, and
  hide VsDevCmd's stderr as build_win.bat does.

On every platform, git quotes non-ASCII paths and appends a tab to a
+++ header whose path contains a space, and parse_diff dropped both.
changed_files and the workflow's changed-files step now pass
core.quotePath=false, and parse_diff strips the tab.
2026-10-06 00:35:03 +08:00
yw4zandNoisyfox 1496906939 UI Fixes / Improvements (#15074)
* plates-toolbar-scrollbar-size

* update

* filament grouping dialog

* mixed filament list

* Update StepMeshDialog.cpp

* moves plot scrollbar

* fix position of popups

* printer agent combo box width

* match multiline text control background

* add dots to configure button

* also correct label color for multiline input

* side tools connecting text color

* recenter dialog text color

* preferences experimental features

* upgrade panel hyperlink color + right margin

* fix position of + sign on printer selector

* speed control popup

* bbl fan control window

* AMS materials setting

* monitor > ams section

* match background color of multiline text editor on project page

* Fix SwitchButton colors

* bbl camera popup

* BBL > Send print dialog

* Revert "BBL > Send print dialog"

This reverts commit 128e145897.

* Revert "bbl fan control window"

This reverts commit 4a0db62790.

* Revert "bbl camera popup"

This reverts commit e2301e3237.

* Revert "monitor > ams section"

This reverts commit 66e6894eb1.

* Revert "fix position of + sign on printer selector"

This reverts commit 1523ba24c0.

* Revert "upgrade panel hyperlink color + right margin"

This reverts commit 541f2514c5.

* Revert "side tools connecting text color"

This reverts commit a9ab074247.

* Revert "recenter dialog text color"

This reverts commit f4670d32b2.

* Revert "AMS materials setting"

This reverts commit b12827f9b5.

* Revert "Fix SwitchButton colors"

This reverts commit 2d4f1d1fe9.

* match object list background color

* match compare dialog wxDataViewCtrl background color

* fix centering of iconized buttons on linux

* fix compare dialog background color not applied on linux

* edit gcode dialog components background color

* Update Plater.cpp

* fix dev button font size

* Fix scaling issue on SwitchButton while using 150%

* transfer or discard changes dialog wiki label

* "Transfer or Discard changes" / "Unsaved Changes" dialog header color

* object table colors & header spacing

* progress dialog

* fix progressbar look on linux

* fix build

* fix build

* fix centering of iconized icon again

* fix header background color

* revert bbl fan control

* add includes

---------

Co-authored-by: Noisyfox <timemanager.rick@gmail.com>
2026-10-05 19:06:24 +03:00
Clifford GarwoodandClaude Opus 5.5 53dad9f4fb Include what the IMEX code uses
Upstream's clang-tidy gate now checks that the lines a pull request changes
include the header for every symbol they use. The IMEX sources, their tests,
and the lines this PR adds to shared files relied on the precompiled header and
transitive includes. This adds the includes clang-tidy names; no code changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 11:55:18 -04:00
SoftFever cd30b196b2 Let the camera pan and orbit freely while sketching in the Design tab
Panning or orbiting with a right-drag no longer ends the polyline chain or drops
the point already placed. Only a right-click that doesn't move does. In the
Touchpad camera style, Alt+move and Shift+move now orbit and pan even while a
draw tool is armed.
2026-10-05 23:24:50 +08:00
Rodrigo Faselli 881f2e0c12 Merge branch 'main' into main 2026-10-05 12:17:33 -03:00
yw4z 0dbd9f01fd update icon color 2026-10-05 18:13:42 +03:00
yw4z 19cc65f43a Merge branch 'main' into extruder-variants-ui-2 2026-10-05 18:05:41 +03:00
Alexandre Folle de Menezes 6b8df05d40 Use the appropriate Unicode symbol for °C (Celsius) (#16135) 2026-10-05 11:55:45 -03:00
yw4z 26be825234 init 2026-10-05 17:37:52 +03:00
Alexandre Folle de Menezes 47f1f2de5f Add context to XYZ axis names (#16134) 2026-10-05 10:38:52 -03:00
Terasit Juntarasombut 5dcf89d094 l10n(th): align Publish 3MF and new-feature strings with Thai glossary (#15681) 2026-10-05 10:15:17 -03:00
Alexandre Folle de Menezes 318f6178ab Improve and complement pt-BR translations (#16103) 2026-10-05 10:13:31 -03:00
Surfoo 37dbad5752 i18n(fr): added French strings (#16161) 2026-10-05 10:13:03 -03:00
SoftFever b67269ccc4 Fix z-fighting between feature previews and the bodies in the Design tab
The bodies now always draw over the preview on every face a feature leaves unchanged, for
all features. Since a hole's cut sits inside the body, the Hole preview now hides the
bodies and shows the result alone, as Fillet/Chamfer and Draft do.
2026-10-05 19:38:46 +08:00
SoftFever 6dfd1c2a0a Design tab: one rule for Enter/Esc/mouse, kernel fixes, Text and Revolve-axis features, studio lighting"th 7cp6pk upstream (#16019)
# Description

Follow-up to #15238. A review of the Design tab, and then testing its
Linux AppImage on desktops, turned up UX inconsistencies, kernel bugs
and a crash. This PR fixes them and fills the gaps found along the way.
Everything stays behind `SLIC3R_CAD` and the `enable_cad_feature`
preference. With the preference off, Prepare and Preview behave and
render exactly as before.

The first four commits are Design-tab fixes that landed on the fork
after #15238 and were never sent upstream: the value-field label, the
`GUI.hpp` include, sketch tool batches 9–14 and honest tool messages.
The later commits build on them.

**Interaction: one rule for mouse, Enter and Esc** (charter
`docs/CAD/cad_ux_guidelines.md` §4.2)
- Enter does what ✓ does; Esc does what ✗ does. Esc steps back one level
(value field → gesture → tool → selection) and never deletes anything.
- A click on empty space clears the selection and never applies a
pending operation.
- A right-click with nothing pending opens the offer. The click is
recognised by drift alone; the 200 ms timing rule is gone, since §6.2
forbids timing-dependent gestures.
- The panel's CHAR_HOOK owns Delete, Esc, Ctrl+Z/Y and F, so the
duplicate handlers in `GLCanvas3D` are removed.
- Ctrl+Z/Y work inside a sketch, and Edit ▸ Undo/Redo drive the Design
history while that tab is shown.
- A value the geometry cannot take (zero length, sweep > 360°, …) is
refused inside the field, and the field says why.

**Kernel**
- Solver: tangency and point-on-line pick the correct side;
circle–circle tangency is handled; the partitioned solve maps sentinel
references; constraints that could not be applied are reported instead
of silently dropped.
- Trim, extend, offset and mirror are fixed for arcs and ellipses.
Negative scale is handled, and zero-radius circles no longer produce
geometry.
- Feature → body references resolve by identity, so hiding, reordering
or deleting a feature no longer re-targets later features.
- Hole standards table corrected: inch countersinks are 82°. New threads
use the nominal diameter; new patterns use inclusive spacing. Both are
gated by flags, so existing recipes rebuild unchanged.
- **A closed loop that crosses or folds back is not a region.** Such a
loop passed the closed-loop check and MakeFace, and extruded into an
invalid solid with no caps. `SketchEngine::wires_to_face` now checks the
face (BRepCheck) and fails with a reason; `sketch_loop_defect()` finds
the crossing or cusp exactly, and the sketch tints the loop red and
marks the point.
- Recipe: saves origin, body colours and the loop auto-close setting in
a trailing block; the v4 reader is frozen as `load_flat_v4`; the 3MF
reader caps the entry at 1 GiB and the legacy entry name never overrides
the current one. New fields (`dressup_edges`, the Text parameters,
`revolve_axis_entity`) are appended at the end of the framed recipe, so
existing projects load and rebuild unchanged.
- OCCT failures are caught before `std::exception` in the new code (OCCT
≥ 8).

**Modelling**
- **Several solid edges in one Fillet/Chamfer.** Shift/Ctrl+click adds
or removes edges of the picked body; one feature dresses them all at one
size, every id resolved against the same body. The card and the offer
name the count.
- **Sketch Offset takes the whole outline** the picked curve belongs to,
with a live preview, instead of one segment (on a text outline a segment
is a fraction of a millimetre, so nothing seemed to happen).
- **Revolve about a line of the sketch.** The axis was only the sketch
plane's X or Y axis. It can now be any line of the profile sketch: a
construction centerline (preselected when the sketch has exactly one) or
an edge of the profile. The gizmo draws the axis dashed. A profile on
both sides of the axis is refused with the reason (MakeRevol failed
there with none). Surface Revolve takes the same axes.

**Panel, offer, text**
- The offer menu: its strings are translatable (the generator emits
`L()` markers, and the files are added to `list.txt`); it opens with a
title naming the selection; blocked verbs stay in place, greyed, with
their reason; each verb has one refusal wording.
- Extrude defaults to Join when the profile touches a solid, otherwise
New body. All result menus use one vocabulary: New body / Join / Cut /
Intersect.
- Interference and volume/area reports go to the status line, in
mm³/mm². Delete Body no longer asks for confirmation, since it is
undoable.
- **Text is its own feature** ("Text N" in the tree). Its dialog offers
any installed font (bold, italic) and the height in mm; it is modeless
and opens at the top right of the window, and the outline is drawn in
the view where it will go while typing. Editing the feature reopens the
dialog with its string, font and height. The outlines are saved too, so
the project opens the same on a machine without that font.
- New feature names match the card header (`Extrude 3`) and are
translated. Terminology and units are unified ("Coordinate system",
"Angle (°)").
- The Prepare Sketch/Primitive gizmos can be selected only when the CAD
feature is on.
- MCP: write methods are refused while the tab is busy (including while
the Text dialog is open); a timed-out command never runs; only a socket
is ever unlinked; `fillet`/`chamfer` accept an array of edges.

**Rendering in the Design view**
- Once extruded, a part was hard to read: both lights of the object
shaders sit near the camera, so the sides of a part came out in nearly
the same tone, and nothing marked where one face ends.
- The phong shader gains a studio lighting model, selected by a new
`lighting_model` uniform: a sky/ground hemisphere in world space, a key
light from the upper left and a weak fill, a plastic-like highlight, and
a darker base with a faint sheen toward the silhouette.
`GLCanvas3D::set_studio_lighting()` enables it per canvas; only the
Design canvas does. Every canvas sets the uniform on each use (0 for the
slicer's canvases), so Prepare and Preview render exactly as before.
- Every B-rep edge of a body is drawn as a 2 px dark line, depth tested
and pulled a few pixels toward the eye, so it hides behind the faces in
front. Seams of closed surfaces and degenerate edges are left out
(`GeometryEngine::display_edges`).

**Crash fix (Linux AppImage)**
- Drawing anything in a sketch crashed the AppImage: the constraint list
labels were formatted from narrow literals holding `—`, `·` and `°`;
`AppRun` sets `LC_ALL=C`, so wx's conversion returned NULL and
`wxString::Format` dereferenced it. They now go through
`wxString::FromUTF8`, as the panel's other non-ASCII literals already
do.

Docs updated to describe the design as it now stands: `design_tab.md`,
`interaction-model.md` and the portability note.

No change to slicing, profiles or presets. Existing 3MF recipes load and
rebuild as before: the v3/v4/v5 fixture tests pass.

# Screenshots/Recordings/Graphs

None attached. The behaviour was checked on the fork's Linux tester
AppImages, driven under Xvfb (see Tests).

## Tests

- `libslic3r_tests` on this branch, rebased on `main` (dc0e26918): 943
of 944 cases pass (1 skipped), 79,733 assertions. Linux, system OCCT.
- New or updated cases cover: the solver side and tangency fixes;
trim/offset/mirror on arcs and ellipses; body identity across history
edits; datum remapping, expressions, bindable fields; circular pattern,
hole standards, threads; the round trip of colours, origin and
auto-close; `body_touching_sketch`; multi-edge fillet/chamfer and its
save/load; the crossing/cusp loop analysis and the extrude refusal; the
Text parameters' save/load; `display_edges` on box, cylinder and cone;
revolve about a centerline, about the profile's own edge, refused
through the profile, stale axis index, axis save/load. The
truncated-recipe test accounts for every new tail field.
- All touched GUI translation units compile with GCC, and with clang
`-fsyntax-only` under the macOS job's warning flags. The modified
shaders pass `glslangValidator`.
- `gen_offer_table.py --check` passes, and `xgettext` + `msgfmt
--check-format` are clean on the CAD sources.
- On the fork's Linux tester AppImages (same code on `cad-mainline`),
driven headless:
  - drawing lines and rounded rectangles no longer crashes;
- Offset on one side of a rectangle selects and previews the whole
outline; Enter adds it;
- Text: the dialog opens at the top right, "Text N" appears in the tree
and the outline in the view while typing; double-clicking the feature
reopens it and a new string redraws in place;
- a block with a through hole and fillets: faces separate by
orientation, fillets shade round, edges are drawn and hidden behind the
part;
- a half-profile beside a construction centerline: Revolve preselects
the centerline, shows the axis dashed and builds the expected tube
(4000π mm³).
- **Not verified by hand:** multi-edge selection and the red loop
marking; Windows and macOS were not run.

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-10-05 18:58:21 +08:00
SoftFever 7495b9fda2 Merge branch 'main' into claude/inspiring-knuth-7cp6pk-upstream 2026-10-05 18:48:18 +08:00
SoftFever 3f844ee99c fix clang-tidy errors 2026-10-05 18:47:28 +08:00
HanifKoh 92de6f1e5b Restore Thread.hpp in MediaPlayCtrl.cpp for the Windows Non-Release Ping Test (#16155)
The only use of Slic3r::create_thread in this file sits behind
!BBL_RELEASE_TO_PUBLIC && __WINDOWS__, so the include looked unused in
every configuration CI builds. Windows Debug and RelWithDebInfo builds
failed without it.
2026-10-05 18:33:49 +08:00
SoftFever 257c589331 Merge branch 'main' into claude/inspiring-knuth-7cp6pk-upstream 2026-10-05 18:29:35 +08:00
HanifKoh c8edb29ddc Gate Pull Requests on clang-tidy Missing-Include Checks (#16154)
A Linux job configures without the precompiled header and runs clang-tidy over the C++ lines a pull request changes. The only check for now is misc-include-cleaner for missing includes; .clang-tidy is where further checks get enabled.

scripts/run_clang_tidy.sh (Linux, macOS) and scripts/run_clang_tidy.ps1 (Windows) run the same check locally: the same configure, the clang-tidy version pinned in scripts/clang_tidy_requirements.txt, and the same comparison against OrcaSlicer's main. They offer to install what is missing, or print the command to do it by hand.
2026-10-05 18:27:22 +08:00
Hanif Koh 325ecdda10 Merge Main into Belt Printer 2026-10-05 17:27:52 +08:00
HanifKoh 4895bc03b4 Remove Unused Project Includes and Forward-Declare Where a Type Is Only Referenced (#16099)
* Remove Unused Project Includes and Forward-Declare Where a Type Is Only Referenced

Generated with include-what-you-use and applied conservatively. Only OrcaSlicer's own headers, the ones under src/ and tests/, are removed or forward-declared; standard-library and third-party includes are left alone. An include is removed only when both the Release and the Debug configuration leave it unused, never from inside a conditional block, and never from a file with platform-specific blocks, which only gain includes. Files whose only use of a header sits behind a feature or debug macro (libvgcode's OpenGL ES and marker code, the ARACHNE/TESTS_EXPORT_SVGS debug output) keep their includes.

clonable_ptr.hpp gains #pragma once; it had no include guard and was only safe while Config.hpp was its sole includer.

* Remove Unused Project Includes From Files With Platform-Specific Code

A Linux include-what-you-use run cannot see the code inside _WIN32, __APPLE__ or __linux__ blocks, so its verdict is only taken where nothing the removed header declares, directly or through what it includes, is named inside those blocks. Removals also have to hold in both the Release and Debug configuration and never touch a line inside a conditional block.

* Restore the libslic3r Precompiled Header and Direct Includes Lost in the Platform Pass

The platform-file pass treated pchheader.hpp as an ordinary header and
emptied it, and left GUI_Preview.hpp and 14 other files relying on
headers they no longer reached directly.

* Restore MainFrame.hpp in ParamsDialog.cpp for the Windows-Only Reparent Call

* Include Headers That Files Reached Through Ones the Cleanup Removed

* Drop Includes Duplicated by the Cleanup or by Main's Own Additions

* Leave PreciseSeam.cpp as Main Has It After the Precise Seam Rework
2026-10-05 16:47:17 +08:00
SoftFever fa56a9cfeb Fix crash when returning to the Design tab after committing a body to the plate
The Design view drew sinking outlines by looking up its bodies in the
plate's model, reading past the end of an object's volumes once a body
was committed. The Design canvas no longer draws sinking outlines.
2026-10-05 16:22:52 +08:00
Ian Chua 86153752e6 test: OFL OTA for ARM64 test (TO BE REVERTED AFTER 2.5.0 ALPHA) (#16151)
Merged by /bot merge on behalf of @peachismomo (id 52488812).
Grants: resources/profiles/OrcaFilamentLibrary/filament/Elegoo, resources/profiles/OrcaFilamentLibrary.json, resources/profiles/Elegoo, resources/profiles/Elegoo.json
Head: 00276bf8ee
2026-10-05 08:19:40 +00:00
SoftFever e7545f0bc0 Commit Design bodies to the plate as one assembly, keeping their positions
Commit to Plate now sends all visible bodies to Prepare as one object with a
part per body, so their relative placement survives. A dropdown beside the
button switches to Commit to Plate (as bodies), the previous one-object-per-body
behaviour, and the choice is remembered.
2026-10-05 14:41:07 +08:00
HanifKoh 63d5fa23b6 Remove the OBJ Color Dialog That Texture Import Replaced (#16122)
* Remove the OBJ Color Dialog That Texture Import Replaced

* Remove references to ObjColorDialog
2026-10-05 14:39:10 +08:00
HanifKoh 1869895b0a Load One jQuery 3.6.0 Copy from include on Every Web Page (#16143)
Home, Project and seven setup-guide pages move from jQuery 2.1.1 to 3.6.0, and the four other copies are removed.
2026-10-05 14:32:38 +08:00
HanifKoh 48a8e33ec3 Delete Gizmo, SLA and Utils Files Nothing Builds or Includes (#16101)
GLGizmoSlaSupports, GLGizmoHollow, GLGizmoFaceDetector, GLGizmoText and GLGizmoAdvancedCut were already left out of the build, and GLGizmos.hpp, the only header including some of them, had no includers. VoxelizeCSGMesh.hpp uses types that no longer exist, SLA/bicubic.h does not compile, and Utils/ProfileDescription.hpp is included nowhere. Their CMake and gettext source-list entries go with them.
2026-10-05 14:26:15 +08:00
HanifKoh 2dbfc6bc5b Add Missing Includes to Tests Merged Since the Last Include Sweep (#16146) 2026-10-05 14:19:47 +08:00
HanifKoh 017bf0a2c8 Remove Unused Web Resources: Duplicate Swiper and jQuery, Test Data and Images (#16119)
* Trim the Bundled Swiper Library to the Files the Web Pages Load

* Remove Unused jQuery Copies, Model Test Data and Unreferenced Web Images
2026-10-05 14:16:59 +08:00
HanifKoh c44f2d6324 Remove the Unreachable MakerWorld Publish Dialog (#16121) 2026-10-05 14:12:00 +08:00
Joseph Robertson c57bf3c044 Belt printer: sync with main, plus the fixes the merge needs (#16145)
Brings `belt-printer` up to date with `main` (4b4a261787) so that #14394
merges cleanly again, and adds the follow-up fixes the merge needs. This
PR targets `belt-printer`, not `main`.

## Commits

1. **GCode: hold the writer by value again.**
- Belt printing had turned `GCode::m_writer` into a `unique_ptr`, so
that `BeltGCode` could swap in a new writer carrying the belt
kinematics.
- Nothing subclasses `GCodeWriter`, and `set_kinematics()` can install
the belt mapping on the existing writer. This commit removes the swap.
- About 190 `m_writer->` edits revert, which takes `GCode.cpp` from 29
conflict hunks with main down to 3.
- The G-code is identical to the current `belt-printer` head on two
BabyBelt projects (see Verification).
2. **Merge upstream/main.** The resolutions are listed in the merge
commit. The ones that needed a decision:
- `write_belt_header()` follows main's relocated header block (#15897,
#15915).
- First-layer acceleration keeps the per-path first-layer plane test,
now with main's cached nozzle index (#16028).
- The arc-to-polyline fallback moves into the out-param
`extrude_arc_to_xy`, which is the overload `GCode` now calls (#16108).
   - The belt fields join `GCodeProcessorResult`'s forwarding assign.
   - The Clipper2 renames (#15969).
   - Belt printers still reserve no CLI wipe tower (#15837).
- The new sparse-layer tower options are hidden for belt printers
(#15841).
3. **Belt: register the raw G-code toggle as a Preview shortcut.**
- Main's assignable shortcuts (#15706) replaced the key switch that
carried **B**.
- The toggle is now `ToggleBeltRawGcode`, bound to B in the Preview,
where B was free. It is listed in the shortcuts dialog and can be
rebound, and the legend shows whichever key is bound.
4. **Precise Seam: slice modifiers in the belt slicing frame.**
- The new `slice_single_volume_regions()` (#16072) sliced modifiers with
`trafo_centered()`, so on belt prints the modifier regions landed in the
unrotated frame.
- It now uses `trafo_sliced()`, as the seam enforcers and support
volumes already do. On non-belt printers the two transforms are the
same.
   - A regression test is included.
5. **Belt profiles: inherit what they repeat and pass main's profile
checks.**
- The BabyBelt Pro and IR3 V2 filaments name their single extruder
variant, as the library-based filaments of other vendors do, and drop
overrides that repeat the library value.
- The Custom belt base inherits `printer_extruder_id` from its parent.
- `normalize` drops the obsolete keys. `fix-variant` gives the machine
limits their silent-mode entry, which they previously read from the
single value.
   - All three vendor versions are bumped.
- Printcepts and IdeaFormer keep their own machine and process bases,
because only filaments can inherit across vendor bundles.
6. **Profile validator: accept a belt printer's tool change without a
tower.**
- The slice sweep (185cfe4323) forces a prime tower and requires its `CP
TOOLCHANGE START` block.
- Belt printers have no wipe tower: they purge into a prism object.
Their filament change is the plain `T` command, which they emit
throughout the slice.
- On belt printers the validator now looks for that `T1` line instead.

## Verification

All on Linux.

- **`m_writer` equivalence.** The current `belt-printer` head
(b22384a559) and commit 1 were each built and used to CLI-slice two
BabyBelt benchy projects (389k and 804k lines of G-code). The output is
identical apart from the per-run object ids in the `printing object …
id:` comments.
- **Merged branch, belt-specific checks.**
- The belt header is written, no `;_BELT_BAND` markers leak, and the
belt axis never steps back.
- Against the Oct 3 belt + main merge, the only G-code differences are
fill ordering on a few layers and time estimates. Both come from main's
changes since then.
- Against the pre-merge output the differences are much larger. That is
expected: main's CLI now refreshes a project's settings from its system
presets (#15953, #16038), so for example `z_hop` follows the belt
profiles' 0.
- **Tests.**
- `fff_print_tests`: 345/345 test cases pass, including 30 `[belt]`
cases.
  - `libslic3r_tests`: 1081 passed, 2 skipped.
- The new Precise Seam test fails 266 of its 284 assertions with the fix
reverted.
- **Profiles.**
  - `scripts/orca_profile_tool.py check` passes for all 69 vendors.
- `OrcaSlicer_profile_validator -s -l 2`: all 1271 slices succeed.
Before commit 6, the six belt printers failed.
- A flattened before/after snapshot of every belt preset shows no value
a belt printer reads has changed.
- **GUI** (BabyBelt Pro, clean datadir):
- B switches the Preview between the designed and the raw machine-frame
G-code, and pressing it again restores the view exactly.
  - The legend reads "Show raw G-code (belt only) [B]".
- The shortcuts dialog lists the toggle under Preview → Display as
rebindable.
- The sparse-layer tower options stay hidden in Advanced and Expert
modes.

Main has since gained one CI-only commit (f3d0b8a553), which merges
cleanly on top.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01AJzy1xeQV3FePh5HfahDyn
2026-10-04 23:13:41 -05:00
harrierpigeon 7eb9aa3832 Profile validator: accept a belt printer's tool change without a tower
The validator slices every printer with two filaments and the prime tower
forced on, then requires the tower's CP TOOLCHANGE START block as proof
that change_filament_gcode ran. A belt printer has no wipe tower: it purges
into a prism object on the belt, so Print::has_wipe_tower() is false and
the change is the plain T command set_extruder() emits. All six belt
printers failed the sweep on that alone, although each one changes
filament throughout the slice. Look for the T1 line on belt printers.
2026-10-04 22:57:53 -05:00
SoftFever 19af2b79f7 Merge branch 'main' into claude/inspiring-knuth-7cp6pk-upstream 2026-10-05 11:43:06 +08:00
SoftFever d999a0bac9 Merge branch 'main' into pr/tommasobbianchi/16019 2026-10-05 10:58:44 +08:00
harrierpigeon 56f5ff8128 Belt profiles: inherit what they repeat and pass main's profile checks
main's profile checks reject the belt bundles: their filaments override
variant keys with one value under the library's six-variant presets, the
copied vendor commons carry keys the slicer no longer reads, the IR3 V2
and BabyBelt Pro machine limits miss the silent-mode entry, and the Custom
belt base pins printer_extruder_id to one entry under a three-variant list.

- The BabyBelt Pro and IR3 V2 filaments name the one extruder variant
  their printers have, as the other vendors' filaments built on the
  library do, and drop every override that only repeats the library value
  (diameter, density, temperature range, most of the fan settings...).
  The eSUN filaments inherit the narrowed list.
- The Custom belt base inherits printer_extruder_id from its base.
- normalize drops silent_mode, adaptive_layer_height and
  tree_support_with_infill; fix-variant gives the machine limits their
  silent-mode entry, which they read from the single value before.
- The IR3 V2 drops two limits equal to its base in both modes.
- Custom's index is regenerated and all three vendor versions bumped.

Flattening every belt preset before and after, nothing a belt printer
reads changes: printer_extruder_id is one id per variant, all 1.

Printcepts and IdeaFormer keep their own machine and process bases: only
filaments can inherit across vendor bundles (from OrcaFilamentLibrary), so
they cannot build on the Custom belt printer.
2026-10-04 19:28:36 -05:00
harrierpigeon 1b4153f0cd Precise Seam: slice modifiers in the belt slicing frame
slice_single_volume_regions() sliced Precise Seam modifiers with
trafo_centered(), but a belt printer slices its layers with
trafo_sliced(): the belt rotation, any pre-slice remap and the lift off
the plate on top. On a belt print the modifier regions landed in the
unrotated frame, away from the walls they were meant to place the seam
on. Slice them with trafo_sliced(), as the support volumes and the seam
enforcers already are. It equals trafo_centered() off a belt printer.
2026-10-04 19:22:42 -05:00
harrierpigeon e8a499df7b Belt: register the raw G-code toggle as a Preview shortcut
Main's assignable shortcuts replaced the canvas key switch that carried
the belt "show designed / show raw G-code" toggle on B. Register it as
ToggleBeltRawGcode, bound to B in the Preview (B is only taken on the
Plater, by the mesh boolean gizmo), so it can be rebound and is listed in
the shortcuts dialog. The legend checkbox shows whatever key is bound.
2026-10-04 19:21:22 -05:00
harrierpigeon bbb94724ba Merge upstream/main into belt-printer
Brings belt-printer up to main 4b4a261787. Resolutions:

- G-code header (#15897, #15915): main moved the header, config and
  thumbnail block later in _do_export; write_belt_header() moves with it,
  still after the thumbnails and outside the BTT_TFT gate.
- _extrude: first-layer acceleration keeps the per-path first-layer plane
  test with main's cached nozzle index (#16028); main's set_speed out-param
  form (#16108) everywhere else.
- GCodeWriter (#16108): the arc-to-polyline fallback for machine mappings
  that cannot express G2/G3 now runs in the out-param extrude_arc_to_xy,
  which is the overload GCode calls, and appends to the caller's string.
- GCodeProcessorResult: the belt fields join main's forwarding assign.
- Clipper2 (#15969): belt arrange helpers take Slic3r::Point; the tree
  support join types lose their ClipperLib qualifier.
- CLI arrange (#15837): belt printers still reserve no wipe tower.
- Wipe tower options (#15841): the two new sparse-layer toggles are hidden
  for belt printers like the rest of the tower options.
- Keyboard shortcuts (#15706): main's registry replaces the old key switch;
  the belt view toggle is re-registered in the next commit.
- Print::process: the belt purge-plan undo runs before main's SliceStarted
  event.
- scripts/filament_id_snapshot.json: deleted on main (a77209af8f).
- Includes and appended tests: union of both sides.
2026-10-04 19:20:33 -05:00
harrierpigeon ee88b3f0b0 GCode: hold the writer by value again
Belt printing turned GCode::m_writer into a unique_ptr so BeltGCode could
swap in a freshly built writer carrying the belt kinematics. Nothing
subclasses GCodeWriter: the machine mapping lives in its MachineKinematics,
which set_kinematics() installs on an existing writer. A GCode is built for
every export and the only state on the writer when init_belt_writer() runs
is the plate offset, which the swap had to copy across by hand.

Install the belt kinematics on the writer in place, drop the copied offset,
and drop the virtual markers on GCodeWriter that the old subclass needed.
Every m_writer-> in GCode.cpp goes back to m_writer., which is most of the
belt diff in that file and most of its conflicts with main.

The pressure-advance pattern keeps its shared_ptr writer: the unique_ptr
kinematics make GCodeWriter move-only and that class must stay copyable.
2026-10-04 19:16:59 -05:00
Kris Austin f3d0b8a553 ci: move macOS jobs off the retiring macos-14 runner (#16088) 2026-10-04 21:12:32 -03:00
TheLegendTubaGuy 4b4a261787 Fix small binary STLs failing to load as ASCII (#16130) 2026-10-04 18:23:40 -03:00
Damir Galeev fb529f8315 Fix startup freeze from synchronous scripts in the camera view (#16104) 2026-10-04 18:22:35 -03:00
TheLegendTubaGuy 93fca83122 Fix memory leak of gap fill paths in solid infill (#16137) 2026-10-04 22:13:17 +01:00
Ioannis Giannakas 7fd6e5fd72 Fix crash on macOS when OrcaSlicer is quit from the Dock, a logout or a restart (#16136)
Fix crash when quitting from the Dock, logout or restart on macOS
2026-10-04 21:54:47 +01:00
Ioannis Giannakas b29c3b36ec Fix a small memory leak when creating default enum list options (#16133)
Fix memory leak in ConfigOptionDef::create_default_option for enum lists
2026-10-04 20:08:42 +01:00
SoftFever 4cb6ca7ecb Let Esc and a click on empty space deselect Design feature and body rows 2026-10-05 02:50:38 +08:00
SoftFever a29e3078e8 Keep automatic Design body colours clear of the selection colour 2026-10-05 02:50:21 +08:00
SoftFever 073e2d9c44 Highlight the faces a selected feature made in the Design tab
Selections are drawn as opaque faces in the selection colour with a cased outline instead of a
translucent tint over the body, so they read on a body of any colour. Selecting a Feature tree
row lights the faces that feature made rather than its whole body, which also makes fillet and
chamfer rows highlight again.
2026-10-05 02:50:14 +08:00
Damir GaleevandIan Bassi b6d11b2b3a Precise Seam: remove known limitations and rework perimeter intersection (#16072)
Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
2026-10-04 14:47:56 -03:00
Joseph Robertson b22384a559 Belt Printer Updates - Oct 4 (#16127)
# Belt Printing Bug Fixes & Feature Updates


This should be the majority of substantive work keeping ``belt-printer``
from being ready to merge into ``main``. It includes Hanif Koh's review
fixes from #15685 and the answers to his review on #14394, findings from
running the branch on a BabyBelt Pro and an IR3 V2, crash fixes
contributed by Unlayered3D, and arrange and purge-tower changes for
multi-colour belt prints.

The merge of current `main` into this branch is prepared and tested
locally. The conflicts are in the acceleration refactor of
`GCode::_extrude`, the ClipperLib namespace clean-up and a few test
files.

Tested with `libslic3r_tests`, `fff_print_tests` and `libnest2d_tests`
on Linux, validated on a stock Klipper BabyBelt Pro.

## New features

**Belt arrangement.** Parts of the same colour are grouped along the
belt into a single print run. Packing starts at the end that prints
first, following the slicing rotation and the sign of the angle. Arrange
reserves the purge prism's strip and the brim width along the bed edges,
then regenerates the prism from the result instead of moving it as a
part. Piles aimed at an off-centre `best_object_pos` are clamped to the
bed. Grouping uses a soft cost: if the belt is too short for separate
runs, colours overlap rather than move to another plate.

**Purge tower sizing.** The prism stops at the plate end. The purge
planner's existing warning reports what a shortened bar can't absorb. A
brim is accepted next to the purge tower again because the purge plan's
layer-grid shift now also moves the brim's apron bands.

**First-layer fan band.** On a belt, "the first layers" are a band along
the belt rather than the first slicing layers. The generator marks where
each extrusion enters and leaves the band. The cooling buffer keeps the
fan off inside it on every layer, taking precedence over overhang and
bridge fan requests.
Thanks to:
@Unlayered3D, @shubhracc, @dlc60, @Rexit

**Profiles.** Z-hop defaults to 0 on belt printer bases and belt
filaments; it can be turned back on. Axis remap options are shown only
in Develop mode. IdeaFormer, Printcepts and Custom bundle versions are
bumped.

Thanks to: @RobMink, @Rexit

## Bug fixes


- Scarf joint seams no longer start below the layer on a belt.
Previously, each seam caused a 0.28 mm belt back-step into the previous
layer ("the belt jumped backwards and the head hit the part"). — credit:
@dlc60
- The CLI no longer rejects every belt print with -102. The
printable-height check compared machine Z, which is belt travel on a
belt printer.
- The belt header is written outside the optional file header block, so
printers with a BTT TFT thumbnail still get belt view in the preview. —
credit: BabyBelt Discord
- The dormant tilted-bed rendering is removed from Prepare view; the bed
is shown as the slicing pipeline treats it. — credit: HanifKoh
- Plate icons, number and name no longer run across the neighbouring
plate on a long, narrow bed; their scale is bounded by the gap between
plates.
- Modifiers and support blockers no longer extend a belt object's sliced
range. The `is_model_part` filter had gone missing with some debug
logging. — credit: HanifKoh
- Crossing-perimeter avoidance no longer dereferences a null layer in
either pass while travelling on a brim apron layer. — credit:
Unlayered3D
- 3MF files with non-finite vertex coordinates are rejected instead of
crashing qhull during load. — credit: Unlayered3D
- The CLI no longer crashes on a project without `printable_height` or
with fewer filaments than were loaded. — credit: Unlayered3D
- The island tour cache is keyed on the island layout, preventing
out-of-bounds reads on later layers with fewer islands. — credit:
Unlayered3D
- The top/bottom painting projection no longer erases from an empty
vector when no shell layers are requested. — credit: Unlayered3D
- Belt purge planning detects filament changes by scanning the tool
ordering instead of checking the first layer's flag, which a brim apron
layer never carries. — credit: Unlayered3D
- The purge prism never gets a brim, regardless of its config. — credit:
Unlayered3D
- Containment tests treat the plate as open along Y on an infinite-Y
belt printer. — credit: Unlayered3D
- Belt brim lattice lines close to the belt move uphill; narrow bands no
longer get near-duplicate lines.
- Organic supports that reach the belt slice without negative flow.
- Hanif Koh's review items: restored the gantry clearance check in
`Print::validate`, read the pre-slice remap header at its real length,
removed unused `clip_support_fills()` and the two unimplemented support
floor modes (legacy values map to `none`), dropped the per-extrusion
transform determinant, indexed apron layers into the first layer's
nozzle map, read the brim axis from the config, removed tagged
diagnostic logging and planning-doc references, and documented the
exclude-object frame. — credit: Hanif Koh
- Hanif Koh's fixes from #15685 include the plate offset in the belt
writer, painted supports and seams under the belt transform, shared
build-plate tilt helpers, the belt header as the source of the tilt,
brim band loop and filament, and G-code export invalidation. — credit:
hanifkoh

At this time there are no known issues with belt printing nor any known
regressions in non-belt-printing execution paths. I have been using
these builds for all of my printing for several months now and have had
no issues.
2026-10-04 11:29:42 -05:00
TheLegendTubaGuy 73a4ff9b16 Fix adding filaments with incomplete mixed metadata (#15728) 2026-10-04 13:03:42 -03:00
TheLegendTubaGuy 67a976e002 Fix mirrored transforms when loading 3MF files (#15731) 2026-10-04 12:53:34 -03:00
000f8abc6d CoreOne INDX 8T and 4T: welcome to earth, MMU3 fixes (#15903)
* Add opt-in printer overrides for filament tool-change settings

Allow printer presets to define uniform ramming, loading, unloading,
cooling, purge, filament scripts and pressure-advance enable settings
without duplicating material presets. Apply overrides during preset
composition and FDM normalization, and expose the switch in Multimaterial.

Keep the feature disabled by default. Omitted or empty override vectors
preserve material settings; a single value applies to every filament,
including an explicitly empty script. Reject multi-value overrides.

Preserve empty float vectors across project serialization and initialize
empty nullable filament overrides before resizing them, preventing preset
cache generation from accessing an empty vector.

Include focused override tests and document the configuration semantics,
Prusa MMU3 integration and INDX tool-change behavior.

Co-authored-by: Codex <codex@openai.com>

* Add Prusa MMU3 and CORE One INDX profiles with shared material tuning

Add MK4 MMU3 and four-tool/eight-tool CORE One INDX printer definitions,
process presets and printer resources. Reuse ordinary MK4 and CORE One
printer/process inheritance while retaining device-specific startup,
shutdown, tool-change and wipe-tower behavior.

Move uniform MMU3 tip forming and INDX handling into machine filament
overrides. Keep MMU3 pressure advance and purge material-specific, retain
INDX material tuning, and share surviving materials with migration aliases
for retired MMU3 and XL tool-change copies.

Preserve unrelated Prusa filament identities, scalar value formats and
inheritance rather than applying broad profile cleanup.

Validation: Prusa profile checks and all 69 printer smoke slices passed.
The final cleanup preserved emitted commands with identical filament
selections.

Co-authored-by: Codex <codex@openai.com>

* Refresh filament controls after loading printer presets

Synchronize the plater filament controls after preset loading, even when the internal filament list already matches the nozzle count.

Co-authored-by: Codex <codex@openai.com>

* Fix nullable Z-hop overrides in Prusa filament variants

Represent empty overrides as nil for each inherited extruder variant so
the native profile loader preserves machine Z-hop settings.

Validation: full profile checks, native loading, and 1,115-printer slicing
sweep passed.

Co-authored-by: Codex <codex@openai.com>

* Separate Prusa profiles from machine-owned filament overrides

Retain profile tuning without unsupported machine override keys. Move supporting code, tests and override documentation into a separate feature change.

Co-authored-by: Codex <codex@openai.com>

* Default INDX tools to hardened high-flow nozzles

Use the High Flow variant for every INDX tool and its dedicated filament presets. Raise Generic PLA throughput to 28 mm3/s.

Co-authored-by: codex <codex@openai.com>

* Use normal filament-change lifts for INDX and MMU3

Avoid duplicate INDX retraction and account for its 12.5-second dock swap in print estimates.

Co-authored-by: codex <codex@openai.com>

* Set z_hop_types for the machine too

* Fix profile check failures in the Prusa CORE One filament presets

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: SoftFever <103989404+SoftFever@users.noreply.github.com>
Co-authored-by: SoftFever <softfeverever@gmail.com>
2026-10-04 23:52:56 +08:00
Kris Austin 78a4f2867c build: update OCCT to 8.0.1 (faster STEP and Design tab, Windows STEP crash fix) (#16089) 2026-10-04 12:36:54 -03:00
MNKczPragostroj 0fd04ff07c Add Pragostroj KINARB profile set (#16045)
* Add Pragostroj KINARB profile set

This adds the new Pragostroj vendor profile with KINARB 1HB and 2HB machine models, nozzle variants, common machine/process settings, and default material mappings. It also includes the corresponding filament and print presets for PLA, PETG, PP, and HIPS, covering the printer family’s standard profiles and tuning.
2026-10-04 23:01:50 +08:00
SoftFever be72afc9f7 Fix hidden features refusing to show again after their body's feature was hidden 2026-10-04 22:21:06 +08:00
SoftFever 1f91fdf637 Show feature and body actions on their rows in the Design sidebar
Each Feature tree row now carries Edit, Show/hide and Delete, and each
Bodies row carries Move, Show/hide and Delete. These act on that row
instead of on the selection. The eye shows a closed eye when the item
is hidden. Every clickable icon in the sidebar now highlights on hover.

Rename and Color now come first in the right-click menu, as items of
their own instead of inside Modify. This applies wherever they appear,
whether opened from the viewport or from a Bodies row.

Clicking an already-selected row no longer starts a rename; use F2,
the row's right-click menu, or the Rename command.
2026-10-04 21:27:12 +08:00
Kiss Lorand 5efb3bef45 Fix missing slicing progress on the first slice (#16000)
Restore slicing progress after notification reset

Ensure the cleared slicing-progress controller is recreated before its initial state transition, and calculate the Daily Tips size before positioning the popup.
2026-10-04 14:06:23 +03:00
Kris Austin 90ac58d3cd perf: skip the unused curled wall estimate to speed up slicing by up to 9% (#16113)
perf: skip estimating curled walls when nothing reads them

The curled extrusion estimate ran whenever a region had overhang speed on,
which is the default, but only the slowdown for curled perimeters reads the
curled lines it produces, and that slowdown is off by default. The step now
also requires a region with the slowdown on, and clears the curled lines
when it skips the estimate, so none are left from an earlier slice. 
Also fixes stale fan commands due to the stale curled lines on the reused layers.
2026-10-04 09:45:19 +01:00
HanifKoh 88346efceb Stop Format/STEP.hpp Defining a Global fs Alias (#16102)
Every file that included STEP.hpp, directly or not, got namespace fs = boost::filesystem at global scope, and 29 sources and three headers relied on it without saying so. Headers now spell out boost::filesystem, and each source that uses fs declares the alias itself.
2026-10-04 14:50:30 +08:00
HanifKoh 5a95ba4bd5 Add Missing Includes to Code Merged Since the Include Cleanup (#16106)
* Add Missing Includes to Code Merged Since the Include Cleanup

* Add Missing Includes to Code Merged Since the Previous Sweep
2026-10-04 14:50:15 +08:00
Kris Austin 36fb9905e5 fix: Linux Flatpak freeze and blank toolpaths after changing a setting in Preview (#16109) 2026-10-03 22:40:33 -03:00
harrierpigeon 69e7cc1544 Preview: judge a belt print's toolpaths by their back-transformed box
all_paths_inside() accepts the path bounding box only within 3*EPSILON of the
bed and otherwise tests every move; a belt print's moves are machine-frame
coordinates whose Z is belt travel, so that test can never pass, and the
designed view's min-corner anchor leaves the box a fraction of a millimetre
below zero. Every multi-object belt plate therefore reported a path beyond the
plate. The belt preview now judges the back-transformed box with a millimetre
of room.
2026-10-03 19:19:02 -05:00
harrierpigeon 6a0d07664f Belt purge tower: one prism per plate
ensure_belt_purge_tower only ever looked at the current plate and kept a single
prism, so the other plates had no tower and switching plates moved the one
prism around. Every plate is now planned on its own: a prism that lies on no
plate is stale, a plate whose prism matches its recorded inputs is left alone,
the rest are deleted and recreated, highest index first.
2026-10-03 18:59:01 -05:00
Kiss Lorand 674308f691 Fix internal bridge limiting area expansion units (#16056) 2026-10-03 20:31:38 -03:00
Kris Austin 5be5c90e59 perf: speed up G-code export by up to 8% via cheaper G-code text building (#16108) 2026-10-03 20:29:54 -03:00
TheLegendTubaGuy 32b1e69fdd Fix AppConfig text persistence and section-specific boolean reads (#16092) 2026-10-03 20:27:44 -03:00
yw4z 5e2d8ab4f0 Optimize file sizes on resources folder (#16111)
* init

* update
2026-10-04 01:58:21 +03:00
harrierpigeon ca934716f5 Plate icons: keep them inside the gap to the next plate
The plate's icons, number and name scale with the plate's depth, but they sit in
the gap to the next plate, which scales with its width. On a long, narrow bed
(a 95 x 500 mm belt) they came out 40 mm wide and ran across the neighbouring
plate. The scale is now also bounded by the gap, which leaves ordinary beds
unchanged.
2026-10-03 15:39:49 -05:00
HanifKoh d1a3ef68c5 Fix CLI Crashes on Malformed Project, Assemble List and No-Input Runs (#15978)
* Fix CLI Crashes on Malformed Project, Assemble List and No-Input Runs

Four CLI paths indexed vectors without checking their size and crashed
with SIGSEGV on malformed input:

- A project inherits_group whose length is not the filament count plus
  the process and printer entries was split by position. It is now
  ignored with a warning, as if the project had none.
- An assemble list object with an empty filaments list passed validation
  and was then read at index 0. It is now rejected as a config error, as
  is a negative filament id.
- --slice N --arrange 1 on a project without plate metadata read the
  missing plate data. It now falls back to the plate's own filaments,
  like the other plate data reads.
- --assemble with no input model built an object with no volumes. It is
  now rejected as invalid parameters.

A tests/cli script covers each case through the binary, since all four
live inline in CLI::run().

* Move the Assemble List Parser into libslic3r

Behaviour-preserving move of the --load-assemble-list JSON parser and
its plate/object structs from the CLI into libslic3r/Format/AssembleList,
so the format can be unit tested. The parser returns its own
AssembleListResult and takes the plate limit as a parameter; CLI::run
maps the result to the same exit codes as before. Every validation rule
and log message is unchanged.

Adds Catch2 coverage of the valid layout and each validation rule.

* Keep the Process and Printer of an inherits_group of the Wrong Length

A project whose inherits_group did not have one entry per filament plus
the process and printer entries was loaded as if it had none. The CLI
then looked for system presets under the names of the user presets,
found none and refused to slice a project that slices on main.

The group is now read as before: the process first, the printer last
and the filaments in between, up to the filament count. A filament
without an entry counts as a system preset. A group with fewer than two
entries is still ignored. The warning stays.
2026-10-04 03:17:40 +08:00
harrierpigeon 82c462ab2a Prepare view: drop the dormant tilted-bed rendering
Plater::set_bed_shape read the belt keys from the plater's own config, which
never carries them, so the branch that tilted the bed model, drew the slicing
arrow and plane and switched the build volume to belt mode never ran. The
Prepare view shows the bed as the slicing pipeline treats it, flat; the
gravity arrow from build_plate_tilt stays, as does the preview's belt view,
which takes its angle from the G-code header.
2026-10-03 13:12:41 -05:00
harrierpigeon 2782374d8d Belt: write the belt header outside the optional file header block
The G-code viewer takes the belt tilt only from the belt header comments, but
they were written inside the header block that is left out when a BTT TFT
thumbnail is configured, so such a printer never got belt view. The comments
are not part of the header block; they go after it, and after the thumbnails
that firmware needs first.
2026-10-03 13:12:41 -05:00
Rodrigo Faselli 88eb219868 Merge branch 'main' into main 2026-10-03 14:41:20 -03:00
harrierpigeon 0c3b7bc72e Belt brim: move the apron bands with the purge plan's layer grid
The purge plan snaps every object onto one layer grid after the brim is built;
the per-layer brim bands follow their layers but the apron bands below the
first layer carry their own print_z and were left behind, which is why a brim
was refused next to a purge tower object. The shift now moves them too and the
combination is accepted again.
2026-10-03 12:03:18 -05:00
harrierpigeon 6b304884d2 Belt arrange: group colours along the belt, keep the purge tower's strip free, stop the tower at the plate end
On a belt the parts print in belt order, so every colour change between parts
is a filament change. Arrange packs items in extruder order already, but it
grew the pile around its centre, so the colours ended up interleaved. A belt
print now packs from the leading end of the bed, each row filling across the
belt before the pile advances, and the objective charges an item for every
packed part of another colour it does not fully follow along the belt,
counting the tilted layers that reach cot(angle) * height past a part, so
each colour prints as one run. The direction follows the slicing rotation: a
rotation about X prints toward +Y, one about Y toward -X, and a negative angle
flips it. Packing from the edge also means the brim has to be kept on the
bed: a belt brim is printed brim_width wide for every brim type, so that much
is reserved along every edge (between parts the brims may overlap, as on any
printer).

The purge prism is regenerated from the arranged parts, flush with the far
edge of the bed, yet arrange moved it about like a part and packed parts into
the strip it comes back to. Arrange now skips the prism and reserves its strip
with a fixed virtual item, like a bed exclusion area, whenever the parts use
more than one filament.

The prism's length follows the parts plus a ramp per unit of height; with the
height at its cap that ran 100 mm past the end of a 500 mm belt and the project
could not print. The bar now stops at the plate end, and the purge planner's
existing warning reports what the shortened bar cannot absorb.
2026-10-03 12:03:18 -05:00
HanifKoh a80c323614 Let the CLI Resolve Presets on Installs That Ship Preset Caches Only (#16047)
Release builds install each vendor as its preset cache alone. The
read-only preset load the CLI uses to resolve an inheriting user preset
passed allow_cache = false to keep caches from being written, which
also stopped them from being read, so every vendor fell back to JSONs
that are not installed and the CLI failed.

The flag now only gates writing: a read-only load reads caches and
writes none. The filament library is also read from its cache whenever
that is all that is installed, so a vendor updated over the air still
resolves against it.
2026-10-04 00:40:38 +08:00
HanifKoh 52ff374870 Refresh a CLI Project's Filament Settings From Their System Presets (#16038)
* Refresh a CLI Project's Filament Settings From Their System Presets

The CLI loads a project's printer and process settings as the GUI does,
taking every key the project does not list as changed from the current
system preset, but it kept the stored filament values. A project saved
before a profile update then sliced with old filament values on the
command line and with the current ones in the GUI.

Every project filament that no loaded filament replaces is now resolved
by its system preset name and fed to the filament merge the up-to-date
path already uses, which keeps the keys listed in
different_settings_to_system and maps per-variant values onto the
preset's variants. This covers a plain run, --uptodate without
--uptodate-filaments, and the slots --load-filaments leaves empty. The
merge tells refreshed entries from loaded ones per entry instead of by
the global loaded-filament count, and the entries are kept in slot
order. A project filament saved under a name the presets have since
split per nozzle is resolved through the name conversion the GUI uses,
which PresetBundle now exposes.

* Check the Project Refresh Test's Result Directly

Shellcheck SC2181: test the checker's exit status in the if instead of
reading $? afterwards.
2026-10-04 00:36:10 +08:00
Kris AustinandRodrigo Faselli 6e0f04815b perf: speed up G-code export by up to 7% via post-processing fixes (#16031)
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-10-03 13:29:55 -03:00
SoftFever 1d06b8576b Fix Design tab getting stuck unable to start a new sketch
After undo, redo, New Design, a project load, a delete or a reorder, a sketch
profile picked beforehand stayed selected even though it was gone. The
right-click menu then offered "Sketch profile" over an empty design with Create
greyed out. Those operations now clear the selection.

The Confirm button no longer stays greyed in a sketch session after a card whose
preview was invalid has been closed.

Delete and reorder now wait while a sketch or constrain session, the Text dialog,
an Insert placement or the move gizmo is open, instead of editing whichever
feature took its place in the list.
2026-10-04 00:01:05 +08:00
harrierpigeon 488c6d8226 Belt profiles: bump the IdeaFormer and Printcepts bundle versions
The IR3 V2 and BabyBelt Pro printer profiles changed since the last bump; the
updater only installs a strictly newer bundle.
2026-10-03 10:02:36 -05:00
Kris AustinandRodrigo Faselli c67b54b39d perf: speed up G-code export by 4-17% via parallel overhang precompute (#16050)
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-10-03 11:50:36 -03:00
SoftFever 25f755c434 Open the Design tab sidebar at Prepare's width instead of its minimum 2026-10-03 21:01:56 +08:00
a6dbf2502d Device tab blank for webui printers after switching language (#14547)
* Save device url in all cases and load printer url after hot-reload finishes

* Recreate web view from scratch as only URL fix seems not robust enough

* Add the same robust browser recreation for WebViewDialog

It should eliminate possible issue with blank Home and other pages
in the same way as Printer page

* Remove redundant fallback leftover

* Fix webview reset state and replay Project info on page reload

The first-show webview reset now runs only on Windows, reloads the last
printer URL and resets the Project page's ready state. The Project tab
replays its 3MF info whenever the page reloads, so it no longer goes
blank after a theme switch or a slow first load. The Device tab no longer
loads an extra time on first open, and the Home tab no longer navigates
twice. NeedsRecreateOnShow() logs is_recreating_gui so one language
switch shows whether the reset ever fires.

* Build plugin pages on first show

A language switch rebuilt every plugin page's browser while the main
window was being recreated, which left plugin tabs blank on Windows.
Plugin pages are now lazy pages, never prebuilt, and are removed left
to right so removing pages never builds one only to destroy it.

A plugin page's script now starts when its tab is first opened;
messages posted before that are dropped.

---------

Co-authored-by: SoftFever <softfeverever@gmail.com>
Co-authored-by: Noisyfox <timemanager.rick@gmail.com>
Co-authored-by: SoftFever <103989404+SoftFever@users.noreply.github.com>
2026-10-03 20:06:50 +08:00
SoftFever d47fb804c3 Make the Design tab sidebar dockable and collapsible like Prepare's
The sidebar can move to either side, float, be resized, and collapse with
the canvas button or Shift+Tab. Its layout is remembered separately from
Prepare's, starts where Prepare's sidebar is, and View > Reset Window
Layout resets both tabs.
2026-10-03 19:05:53 +08:00
SoftFever a9c8721380 Add a build-large-panels-hidden rule to the orca-wxwidgets skill 2026-10-03 19:05:53 +08:00
Claude 815716a4b5 Merge branch 'main' into claude/inspiring-knuth-7cp6pk-upstream
Conflicts were only in include lists (CadDocument.cpp, SketchEngine.cpp);
both sides' includes are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
2026-10-03 08:12:47 +00:00
HanifKoh 8a6377f087 Add Missing Includes Across src/libslic3r (#16068)
* Add Missing Includes Across src/libslic3r

Every libslic3r source and header now directly includes the headers declaring what it uses, rather than relying on the precompiled header or transitive includes. Generated with clang-tidy misc-include-cleaner, with libslic3r headers spelled libslic3r/... so they resolve outside the library's private include paths. MultiMaterialSegmentation.hpp, Support/SupportParameters.hpp and Format/STEP.hpp are made self-contained by hand.

* Make the libslic3r Headers Compile on Their Own

Each now includes, or forward-declares, what it uses instead of relying on what its includers happened to include first. Left out: I18N.hpp, which errors on purpose when included from GUI code, and VoxelizeCSGMesh.hpp and SLA/bicubic.h, which nothing includes and which no longer compile at all.

* Add the Includes Missing From the Hand-Fixed libslic3r Headers

clang-tidy would not edit these headers while they failed to compile on their own, so the first pass skipped them. With the headers now self-contained, a second pass adds the rest.

* Keep Windows Setup Ahead of the Added libslic3r Includes

Print.cpp and Thread.cpp open with a _WIN32 block that has to come first; without the precompiled header, Print.cpp otherwise reaches windows.h through OCCT with NONLS defined and boost/regex fails. OpenVDBUtils.cpp and SLA/SupportTreeBuilder.cpp had includes inside #ifndef NOMINMAX, which libslic3r defines on Windows, so those were skipped there. .clang-tidy also ignores the MSVC STL and UCRT internals, Boost.Multiprecision's fwd.hpp and CPython's Windows include directory.

* Re-Add libslic3r Includes After the Clipper2 2.0.1 Migration

Rebasing onto main took main's version of the files the Clipper2 migration rewrote, so their added includes are restored here, along with includes for main's new code. Clipper2's individual headers are now ignored by clang-tidy: they only build the Z variant through clipper2_z.hpp, which defines USINGZ first, so including clipper.core.h and the like directly broke ClipperZUtils.cpp.
2026-10-03 15:31:11 +08:00
SoftFever c86e33db6d Make the orca-wxwidgets skill find the wx source on Windows and in worktrees 2026-10-03 14:11:21 +08:00
HanifKoh 84657ff11e Add Missing Includes Across the Remaining Sources and Tests (#16071)
* Ignore Clipper, libpng, mcut and Boost.Polygon Internals in clang-tidy

Each only works through a wrapper or umbrella header: libslic3r/clipper.hpp or clipper_z.hpp configure Clipper before including it, png.h pulls in libpng's config headers, and Boost.Polygon's headers only compile through polygon.hpp or voronoi.hpp.

* Ignore minilzo's Config Headers in clang-tidy

lzoconf.h and lzodefs.h are internal to minilzo.h, which is what the code includes.

* Add Missing Includes Across the Remaining Sources and Tests

Covers src/slic3r/Utils, src/slic3r/plugin, src/slic3r/Config, src/libvgcode, src/dev-utils, src/OrcaSlicer.cpp and tests/, the directories left after src/slic3r/GUI and src/libslic3r. Generated with clang-tidy misc-include-cleaner. libvgcode's own headers are included by relative path as in the rest of that library, and Catch2 and pybind11 with angle brackets as elsewhere in the repo.

* Make the GUI and Test Headers Compile on Their Own

Each now includes, or forward-declares, what it uses instead of relying on what its includers happened to include first. Headers that only compile on one platform, or that nothing built includes, are left alone.

* Keep Windows and nanosvg Setup Ahead of the Added Includes

OrcaSlicer.cpp and several tests set _WIN32_WINNT, WIN32_LEAN_AND_MEAN or NOMINMAX before including Windows.h, and the profile validator defines NANOSVG_IMPLEMENTATION before any libslic3r header. The added includes had landed above those blocks, which broke the Windows build.

* Add the GUI Includes the First Pass Missed

Covers headers that only became editable once they compiled on their own, and wx symbols whose suggested header changed as the clang-tidy ignore list grew after the src/slic3r/GUI pass.

* Keep the Added Test Includes Below the NOMINMAX Guard

test_marchingsquares.cpp and test_texture_displacement.cpp had includes inside #ifndef NOMINMAX, which the tests inherit as defined on Windows from libslic3r, so those were skipped there. .clang-tidy also ignores the MSVC STL and UCRT internals, Boost.Multiprecision's fwd.hpp and CPython's Windows include directory, as in #16068.
2026-10-03 13:45:21 +08:00
SoftFever 38df8022e3 Open the Design tab quickly on Windows the first time
A page built on first click is now built before it is shown, as the idle
prebuild already does, so its controls are not created inside a visible
window. This takes the Design tab's first open on Windows from ~9 s to ~1 s.
2026-10-03 12:38:47 +08:00
Clifford GarwoodandClaude Opus 5.5 3ae74ff952 Count layer slider tool changes in the parallel-mode checks
A tool change added from the layer slider switches heads mid-print like a
painted color, but the parallel-mode checks only looked at the filaments of the
plate's objects, support and prime tower. A one-filament plate with a slider
change to a copying head's filament passed them. They now include those tool
changes, as the plate's warning badge already did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 00:33:57 -04:00
Clifford GarwoodandClaude Opus 5.5 d4d79fa328 Refuse Span-mode colors on tools that cannot print them
Only the primary and its Span tools print a Span plate's colors; the other
active tools replay them. The multicolor rule never checked where each color
went, so a filament on a copying or mirroring tool, an unused one, or past the
end of the physical extruder map was accepted. It is now refused, and the
message names the tools that print colors and where the map sends the
offending filament.

The plate's warning badge also read the raw physical_extruder_map rather than
the effective one slicing uses, so on printers that set no map it disagreed
with the slicer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 00:33:57 -04:00
Clifford GarwoodandClaude Opus 5.5 3637a6d164 Merge upstream, including cached config lookups in G-code export
Three commits: G-code export caches its filament config slot and repeated
option lookups, per-plate bed type overrides follow the printer's multi-bed
support, and the gizmo checkboxes and texture displacement panel get styling
and refresh fixes. No conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:19:48 -04:00
Clifford GarwoodandClaude Opus 5.5 9c51f7339e Keep the mode name list shut when it has nothing to offer
The name field rebuilt its suggestions in wxEVT_COMBOBOX_DROPDOWN, which
ComboBox sends after it has sized and shown the popup from the items it
already held. With every suggested name taken, the popup opened around an
empty list: a small empty box on GTK, a black one on Windows. The list is now
rebuilt from the field's own mouse-down and double-click, which run ahead of
ComboBox's handlers. With nothing to offer the popup stays closed and the
click focuses the field instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:17:21 -04:00
Clifford GarwoodandClaude Opus 5.5 9c5f4ebe47 Stop duplicate mode names from trapping a plate's mode cycle
The editor only replaced an empty name, so a row could be given a name another
row already had. A plate stores its mode by name and find_imex_mode() takes the
first row with it, so the second row was unreachable, and the plate's mode
list repeated the name: left-click stuck on it, or looped without getting back
to Primary.

An edited name that another row already carries, or the reserved Primary name
in any case, is now replaced when the edit is committed: "copy" becomes
"copy 2". The edited row yields, so plates keep resolving to the row they
meant, and tabbing through a field without changing it checks nothing.
Resetting a row to a saved name that another row has since taken does the
same. The plate's mode list comes from imex_plate_mode_choices(), which lists
each name once, so a profile that already has duplicates still cycles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:17:21 -04:00
Clifford GarwoodandClaude Opus 5.5 8af5fa45e7 Keep the parallel modes editor on the settings page's colors
The editor took its background from the app's window default, which in dark
mode is a gray the dark-mode walk has no entry for. In dark mode it never
matched the page's palette color, and an editor built in dark mode kept that
gray after a switch to light while the controls inside it changed. It now
takes the page's color, which the walk remaps both ways.

Its labels and G-code text had no color of their own, so on Windows they took
the system's text color, which follows Windows' theme rather than Orca's. They
now start from the page's label and input text colors, which the walk maps
with the page.

The walk also ran the tool tiles' role colors through the palette map, which
turned their labels gray after a theme change. The tiles carry wxBU_AUTODRAW,
the flag it skips, so the role colors stay as set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:17:20 -04:00
Clifford GarwoodandClaude Opus 5.5 697e6c7cf3 Reuse Orca's own icons in the parallel modes editor
The remove button drew imex_remove.svg, a redrawn delete.svg, and the help
button drew the mascot question icon. They now use delete and icon_qusetion,
the tip icon the send-print dialog uses, and imex_remove.svg is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:17:20 -04:00
Kiss Lorandandyw4z 00bb4202fe Fix gizmo checkbox contrast; align Texture Displacement styling and panel refresh behavior (#16076)
* Fix gizmo checkbox styling and Texture Displacement resize

Use shared BBL checkboxes in Texture Displacement and restore white toolbar checkmarks so other gizmos keep proper checkbox contrast. Also fix Texture Displacement resizing only after mouse movement by requesting additional frames while its layout is still changing.

* Fix gizmo checkbox styling and Texture Displacement resize

Use shared BBL checkboxes in Texture Displacement and restore white toolbar checkmarks so other gizmos keep proper checkbox contrast. Also fix Texture Displacement resizing only after mouse movement by requesting additional frames while its layout is still changing.

* Update GLGizmoTextureDisplacement.cpp

---------

Co-authored-by: yw4z <ywsyildiz@gmail.com>
2026-10-03 03:12:31 +03:00
Kiss Lorand 1241dd5521 Fix per-plate bed type handling (#15916) 2026-10-02 21:02:36 -03:00
Kris Austin eb30ea1eb8 perf: speed up G-code export by 3-9% via cached config lookups (#16028) 2026-10-02 19:34:47 -03:00
Clifford GarwoodandClaude Opus 5.5 ee30dc1de0 Merge the PR's upstream update without the assembly view painter block
The update merged the same upstream commits this branch already carries. Its
GLCanvas3D resolution kept the assembly view painter block after the ghost
render, which section view removed along with the members it calls, so that
tree does not build. This merge keeps the removal; nothing else differs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:48:30 -04:00
Rodrigo Faselli 6bd90798dc Merge branch 'main' into main 2026-10-02 17:46:18 -03:00
Clifford GarwoodandClaude Opus 5.5 cafbac4816 Merge upstream, including the move to Clipper2 2.0.1
One commit: libslic3r migrates to Clipper2 2.0.1 for performance. No conflicts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 16:38:41 -04:00
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
yw4z 489b24bc0c Merge branch 'main' into extruder-variants-ui-2 2026-10-02 22:39:48 +03:00
Clifford GarwoodandClaude Opus 5.5 b1e44199c1 Merge upstream, including section view and a faster preview
Five commits: a section view for the 3D canvas, a faster G-code preview, user
preset values kept on extruder variants they don't list, debug build CMake
fixes, and a 30 minute timeout on macOS notarization.

Two conflicts, both in GLCanvas3D. The header's were neighboring
declarations, kept from both sides. In the transparent pass, the section view
removed the assemble view's painter block that followed our ghost render;
the ghost render stays where it was, at the end of that pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:14:03 -04:00
SoftFever 9351bbaa83 Merge branch 'main' into claude/inspiring-knuth-7cp6pk-upstream 2026-10-03 02:30:59 +08:00
harrierpigeon d35af1806a Belt cooling: keep the fan off in the band inside the layer cooling pass
The band was a second pass over the finished layer that fought the fan commands
the layer pass had already written (overhang, bridge and resume requests). The
generator now marks where each segment enters and leaves the band and the layer
pass treats the band as the strongest fan request, so there is one place that
decides the fan.
2026-10-02 12:46:44 -05:00
harrierpigeon 5ac0dd19d0 Belt brim: no near-duplicate lines in a narrow lattice band
A band barely wider than one line got a second line almost on top of the first,
and lattice rows could repeat within half a pitch.
2026-10-02 12:46:44 -05:00
harrierpigeon 550229b0fa Belt validation: refuse a brim next to a purge tower object
The purge plan moves objects onto a common layer grid after the brim bands are
built, so the two cannot share a print. The prime tower setting alone still does
not block a brim. The missing-prism warning now counts the filaments the objects
use, as the GUI does.
2026-10-02 12:46:44 -05:00
harrierpigeon 997fac8752 GCodeProcessor: key the belt height-check skip on belt_printer
The check was skipped for any machine-frame transform; what makes the printable
height meaningless is the belt axis, so ask for that directly.
2026-10-02 12:46:44 -05:00
harrierpigeon 007071bd56 GCode: skip the second crossing-avoidance pass on a brim apron layer too
travel_to runs avoid-crossing a second time after a wipe; that call needs the
current Layer as much as the first one does.
2026-10-02 12:46:44 -05:00
harrierpigeon 7a23d9ea2c Belt slicing: only model parts set the layer range again
The filter was lost with the debug logging it shared an #if with, so modifiers
and support blockers stretched the sliced range of a belt object.
2026-10-02 12:46:43 -05: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
Kris Austin 4ffba13210 ci: time out macOS notarization after 30 minutes (#16087)
notarytool submit --wait has no timeout. On 2026-10-02 it hung for
over 5 hours in a main build. Since #16044 a new push no longer
cancels a running main build, so nothing stopped it and six waiting
main runs were replaced without starting.

Over the last 30 days the step succeeded 206 times, with a median of
4.3 minutes and a maximum of 14.8.
2026-10-02 14:40:51 -03:00
harrierpigeon bfbf5ad1c2 Arrange: keep a pile aligned to an off-centre point on the bed
With best_object_pos away from the bed centre the placer packs the pile
inside the bin and then translates it so its centre lands on that point,
without checking that it still fits there. A belt printer aims at the
leading end of the belt (BabyBelt Pro: 0.5, 0.05), so any pile longer than
the 50 mm around that point was pushed past the edge: four 90 mm parts on
the 95 x 500 mm belt ended with one across the edge and one outside while
290 mm of belt stayed free.

The final alignment now stops the pile at the edge of the bin; the items'
inflated boxes leave the object spacing as the margin. A pile that does not
fit along an axis is centred on it, as before.
2026-10-02 12:09:59 -05:00
0d36324323 Show the plate's IMEX mode on its mode button
The plate's mode button showed one icon whatever the mode. It now shows the
mode the plate slices as, the one its ghosts follow. imex_mode_kind() reads it
from the heads beside the mode's primary: none is Normal, any Span head is
Custom (the multicolor modes), otherwise any Mirror head is Mirror, and heads
that all copy are Copy. One Mirror head is enough because an IQEX mirror mode
copies within the primary's gantry.

The icons are Felix14-v2's: the four kinds, each in light and dark with a hover
state, replacing the single mode icon. Two fixes to them: the light Normal
border used the dark theme's gray, and an opacity="undefined" attribute hid one
of its strokes in nanosvg. The knight outlines are drawn at 1.0 rather than
0.8, matching the other plate icons and keeping Copy and Mirror apart when
zoomed out.

Co-authored-by: Felix14_v2 <75726196+Felix14-v2@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 11:29:33 -04:00
Ian Bassi a1ad2b4425 Add section view feature for 3D canvas (#15879) 2026-10-02 11:50:43 -03:00
yw4z cffd1ab053 Update UnsavedChangesDialog.cpp 2026-10-02 17:07:02 +03:00
Noisyfox 6c6e8be43d Fix debug build cmake errors (#14593)
* Fix debug build after qhull upgrade

We upgraded qhull from 8.0.1 to 8.0.2 in 504a5d3b70, which contains a commit qhull/qhull@16159c648c `use same CMake target name for Debug and non-Debug`, so this target name check is no longer required

* Fix issue like `IMPORTED_LOCATION not set for imported target "opencv_world" configuration "RelWithDebInfo".` when build Debug config
2026-10-02 09:46:24 -03:00
Clifford GarwoodandClaude Opus 5.5 e8cd4be675 Merge upstream, including deterministic painted multi-material slicing
Seven commits: painted multi-material segmentation made deterministic, missing
includes added across src/slic3r/GUI, U1 high-flow nozzle variants, Windows
ARM64 build and HTTPS fixes, and a rule added to the wxWidgets agent skill.

Three conflicts, all include lists: upstream's include pass and ours each added
to the same lists in GCodeViewer.hpp, PartPlate.hpp and PartPlate.cpp. Resolved
as the union of both. Three includes both sides had added at different places,
which git kept twice, are kept once: Color.hpp and <set> in PartPlate.cpp,
<sstream> in Plater.cpp.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 08:22:47 -04:00
47de490647 Size the IMEX test fixtures' flush matrix per nozzle
The IMEX fixtures set seven nozzles but kept multifilament_config's single
filaments x filaments flush block. get_flush_volumes_matrix splits that block
across the nozzles, leaving each with 7 values, and
ToolOrdering::reorder_extruders_for_minimum_flush_volume then reads them as a
7 x 7 matrix, past the end of the buffer. One of the affected tests segfaulted
on Windows x64; ASan reproduces the overflow in that test on Linux, where it
passed only by luck.

The fixtures now repeat the block once per nozzle, as the GUI does, and size
flush_multiplier to match, since append_full_config takes the nozzle count from
it. The helper is shared in test_helpers.

Co-authored-by: HanifKoh <76276251+HanifKoh@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 08:22:36 -04:00
yw4z b8166f9fb7 Update OG_CustomCtrl.cpp 2026-10-02 15:21:25 +03:00
HanifKohandSoftFever 205de9ce63 Keep User Preset Values on Extruder Variants They Don't List (#16046)
* Keep User Preset Values on Extruder Variants They Don't List

A user preset stores the variant list its parent had when it was saved.
When the parent later gains variants, update_diff_values_to_child_config
matched variants by name only and left the new ones at the parent's
value, so the user's settings were silently replaced there, and a
re-save wrote the system values into the user's file.

An unmatched parent variant now takes the child's first variant of the
same extruder, the rule slicing already uses in get_config_index_base.
A child without a variant list covers the parent's first extruder. The
name match also no longer indexes the child's extruder ids when it has
none.

* Share One Variant Column Rule Between Slicing, User Presets and Projects

Three places chose which variant column a value comes from, each with
its own copy of "the same variant and owner, else the owner's first
column": get_config_index_base when slicing, the user preset merge in
update_diff_values_to_child_config, and normalize_filament_values_to_variants
for projects and the CLI.

find_variant_column now holds that rule and map_variant_columns applies
it to a variant list, so a change to how missing variants are filled
reaches all three. Each caller keeps its own copy step. There is no
behaviour change: G-code is identical before and after. The one
relaxation is that get_config_index_base no longer reads past a short
id list when its two lists differ in length, which its assert already
rules out.

* Rename variant column helpers to variant index

---------

Co-authored-by: SoftFever <softfeverever@gmail.com>
2026-10-02 20:13:06 +08:00
yw4z e7f5d7be30 init 2026-10-02 14:39:05 +03:00
1255af1e9c Bundle uv in Windows ARM64 builds (#16070)
* fix: include bundled UV binary for arm64

* fix: update unit test CI

* Install unit-test numpy only with the bundled uv

* Simplify the unit-test script's uv lookup

---------

Co-authored-by: SoftFever <103989404+SoftFever@users.noreply.github.com>
Co-authored-by: SoftFever <softfeverever@gmail.com>
2026-10-02 19:37:29 +08:00
Kris Austin 8bf7b73141 ci: build Windows ARM64 with CMake 4.3 like the other platforms (#16052)
The ARM64 jobs pinned CMake 3.31 because CMake 4 dropped pre-3.5 policy
compatibility and its ARMASM support broke Boost.Context. Both are
handled now. deps/CMakeLists.txt sets CMAKE_POLICY_VERSION_MINIMUM on
CMake 4, and Boost.Context uses the winfib implementation on ARM64, so
nothing assembles with armasm.

CMake 3.31 also predates VS 2026. Its InstallRequiredSystemLibraries
treats the v145 toolset as v143, searches only the VS 2017-2022 install
directories and finds no runtime, so the ARM64 installer ships without
msvcp140.dll and vcruntime140.dll. CMake 4.2 and newer find the VC145
redistributable.

get-cmake also installs Ninja, so the ARM64 jobs now use its latest
release instead of the one already on the runner, as x64 does.

The install now fails when InstallRequiredSystemLibraries returns no
msvcp140.dll or vcruntime140.dll, after a configure warning naming the
CMake and MSVC versions. A CMake that predates the Visual Studio in use,
on a developer machine or after the next runner image update, then
stops the installer build instead of shipping one that cannot start.

The build_win.bat prerequisite installer drops its matching 3.31.8 pin.
2026-10-02 08:24:39 -03:00
SoftFever e63c6d0594 Fix Windows ARM64 builds crashing on every HTTPS connection (#16073) 2026-10-02 19:04:55 +08:00
SoftFever 79a89f817f Add a never-Raise-a-popup rule to the orca-wxwidgets skill 2026-10-02 18:16:15 +08:00
Eric McCann 83ee4f4476 U1: add HF nozzle variants and output flow type to avoid warnings on printer (#16043)
This is the bulk of the profile changes for U1 that led to the cooling
catiant connection.

The ugly end gcode is used by the printer's UI to complain if normal
nozzle is installed but the file was sliced for highflow.

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-10-02 18:06:13 +08:00
HanifKoh 390b7d8e6d Make Painted Multi-Material Slicing Deterministic (#15899)
* Make Painted Multi-Material Slicing Deterministic

Painted (multi-material) models sliced to slightly different G-code on
every run: ±1 µm wall coordinates and reordered islands. Hashing each stage
of the segmentation across runs showed the projected painted lines and the
per-layer Voronoi segmentation were stable; the raw top/bottom projections
from slice_mesh_slabs() were not. Three causes, all thread-order dependent:

- slice_slabs_make_lines() appends each slab's intersection lines from a
  parallel facet loop and never restored a canonical order, so the loop
  start vertices and polygon order from make_slab_loops() depended on
  scheduling. Sort every slab's lines with the same key slice_make_lines()
  already uses.
- segmentation_top_and_bottom_layers() wrote a layer's shell projections
  into neighbouring layers' vectors from the parallel loop, relying on a
  parity double-buffer that assumes TBB ranges are exactly one group wide
  and aligned, which blocked_range does not guarantee; two threads could
  append to the same vector. Each source layer now records its projections
  in its own slot and they are gathered per target layer in source order.
- The painted-line sort in post_process_painted_lines() was not a total
  order: projections of one span from facets of different colours tied on
  every key and the first one won the span. Colour and end points now break
  the tie.

Three multi-threaded runs of each painted fixture now give one G-code;
unpainted output is unchanged.

* Test That Slab Slicing Does Not Depend on the Thread Schedule

Projects a dense, tilted sphere with slice_mesh_slabs() on one thread and
then three times multi-threaded, and requires the polygons to match exactly,
vertex order included. Fails without the canonical line sort, passes with it.
2026-10-02 16:51:06 +08:00
harrierpigeon 31494a9836 tests: give the belt fan band test a band the walls reach 2026-10-02 02:42:14 -05:00
harrierpigeon 441ae8e877 Belt: decide the fan band per extrusion segment
A tilted layer runs from the belt to the top of the part, so a wall loop
that starts above the belt still passes along it. Tagging only the path's
first point left such loops out of the band entirely; the band is now
evaluated at each segment, with the tag capped where the fan stops
depending on it.
2026-10-02 02:36:13 -05:00
harrierpigeon 461063856d tests: fix two belt test expectations
The clearance test needs the relative-E reset in its layer change G-code to
get past validate()'s other checks, and now asserts the height message. The
fan band test counts cycles rather than commands: the band is decided per
path start, so a cube cycles the fan far less often than a benchy.
2026-10-02 02:33:14 -05:00
HanifKoh 1a5f91d727 Add Missing Includes Across src/slic3r/GUI (#16048)
* Add Missing Includes Across src/slic3r/GUI

Every GUI source and header now directly includes the headers declaring what it uses, rather than relying on the precompiled header or transitive includes. Generated with clang-tidy misc-include-cleaner, plus one hand edit making CalibrationPanel.hpp self-contained.

* Drop the OS-Specific Includes Added Outside Their Platform Guards

GLib, GTK, D-Bus and POSIX headers are only used inside platform #if blocks, which already include them. Added unconditionally at the top of the file they broke the Windows build.

* Add the clang-tidy Configuration That Generated These Includes

Only misc-include-cleaner's missing-include check, with the headers it must never suggest: per-platform, internal and OS-specific ones that would break other platforms or are not meant to be included directly.

* Match Windows Paths in the clang-tidy Ignore List

Header paths use backslashes on Windows, so every / in a pattern is now [/\\]. The Windows SDK headers are ignored alongside the other OS-specific ones, and the list is one pattern per line. Suggested by @raistlin7447 from a Windows clang-cl run.
2026-10-02 14:56:56 +08:00
harrierpigeon 1b392955de tests: organic tree supports reaching the belt slice without a negative flow
Covers the case from Hanif Koh's review of #14394 (belt raft layers below
the object with no lower bound), which the negative-Z bottom layer fix in
layer_initialize() addresses.
2026-10-02 01:13:29 -05:00
harrierpigeon 3beb448ae6 Belt brim: lattice lines closer to the belt than the band fraction move uphill
With a first layer of about 0.28 mm or more at 45 degrees (or a shallower
belt) the brim band is wider than one bead and its lines go on the nominal
lattice. A lattice line could land where the belt is almost at the band's
print_z; its flow was clamped to half a layer while the nozzle sat nearly on
the belt. Such a line now moves uphill to the 0.75 fraction the single-line
case uses, and a line that lands on the previous one is skipped.

Ported from the Unlayered fork (patch 0007 of its belt port series, found
there by fuzzing first layer heights). The fork's companion fix, restricting
the brim filament to those the writer was handed (0008), is not needed here:
ToolOrdering registers the brim filament on every band's layer, so the writer
always has it. A test pins that with every object a flush target.
2026-10-02 01:13:29 -05:00
harrierpigeon 412564cae3 Belt: document the frame of the exclude-object outlines
EXCLUDE_OBJECT_DEFINE keeps plate coordinates on a belt printer: the frame
after the slicing rotation is undone and before the G-code axis remap and
machine-frame shear, which is where the object stands on the belt.

Raised in Hanif Koh's review of #14394.
2026-10-02 01:11:59 -05:00
harrierpigeon 360a68e078 Belt: warn when the purge tower is enabled but the project has no tower object
The purge tower is a model object the GUI creates and sizes, and libslic3r
only purges into one that exists. A multi-filament belt project sliced from
the CLI without it changed filament with nowhere to purge, silently.

Raised in Hanif Koh's review of #14394.
2026-10-02 01:11:59 -05:00
harrierpigeon 556569c0e3 Belt: drive the first-layer fan band from the generator, not from parsed moves
The cooling buffer's band pass rebuilt positions from the layer's G-code
and tested them against the first-layer plane. The G-code is in machine
coordinates and the plane is in slicing coordinates, so on the shipped
profiles the nearest move was over 100 mm from a 0.2 mm band and the pass
never changed the fan. GCode::_extrude() already knows each path's height
above the belt, so it now tags the band changes and the buffer applies and
strips the tags.

The pass also took the S of every M106 as the part fan, whatever its P
index, and stored that 0..255 value where a percentage was expected (an
auxiliary fan line came back as M106 S651); it now uses FanMover's parser,
which ignores other fans, and converts to percent. It no longer overwrites
the layer's intended speed, only the fan's actual state.

Raised in Hanif Koh's review of #14394.
2026-10-02 01:10:57 -05:00
harrierpigeon 646fe6384c Belt: retire the two support floor modes nothing implements
clip_only and both were never read and behaved like none; old values now
load as none.

Raised in Hanif Koh's review of #14394.
2026-10-02 01:07:45 -05:00
harrierpigeon f7b822abb7 Belt: remove the unused clip_support_fills()
It had no caller besides its own recursion.

Raised in Hanif Koh's review of #14394.
2026-10-02 01:07:45 -05:00
harrierpigeon c0ba2c8c44 Belt brim: read the belt axis from the config in the instance check
belt_brim_instances_compatible() runs while the slicing parameters can be
stale, like the rest of the brim predicates, which read the print config.

Raised in Hanif Koh's review of #14394.
2026-10-02 01:07:44 -05:00
harrierpigeon 0701f64ba9 Belt: index apron layers into the first layer's nozzle map
Apron bands looked up their filament and nozzle config slot with a running
counter, while object layers use Layer::id(), so band N read the map of
object layer N. They precede layer 0 and now use its assignment.

Raised in Hanif Koh's review of #14394.
2026-10-02 01:07:44 -05:00
harrierpigeon c49e8d32c8 Belt: drop the per-extrusion transform determinant
The mesh transform is a rotation and an axis permutation, so its
determinant is always 1; rebuilding the forward transform on every
extrusion to divide the flow by it changed nothing.

Raised in Hanif Koh's review of #14394.
2026-10-02 01:07:43 -05:00
harrierpigeon 3752144995 Belt: do not refuse a brim because the prime tower setting is on
enable_prime_tower stays on for any multi-filament project, but a belt
printer never prints the classic tower and the belt purge prism is an
ordinary object that never takes a brim, so every brim on a multi-filament
belt print was refused for nothing.

Raised in Hanif Koh's review of #14394.
2026-10-02 01:07:42 -05:00
harrierpigeon 2aa4122aae Belt: check object height against the gantry clearance again
validate() skipped the build-volume height check whenever the machine-frame
transform was active, which is every shipped belt profile, so a 400 mm
object passed on a 300 mm printable_height. The transform only changes how
the height is written to G-code; the clearance check from f682ab5cd3
applies regardless.

Raised in Hanif Koh's review of #14394.
2026-10-02 01:07:42 -05:00
harrierpigeon 97034b22f6 Belt: remove the tagged diagnostic logging
Drops the [BELT-DEBUG], [BELTRACE], [BELT-CALIB] and [BELT-PREVIEW] log
lines, the SLIC3R_BELT_DIAGNOSTIC_LOG blocks, and the counters and
temporaries that existed only to feed them. Six of the purge tower lines
logged at warning level, which is Orca's default, on every plan. Raised in
Hanif Koh's review of #14394.
2026-10-02 01:04:33 -05:00
harrierpigeon 3776739cb5 docs: drop the private build notification note
The build-notify workflow is a local tool of one contributor and does not
belong in the shared agent instructions.
2026-10-02 01:02:58 -05:00
harrierpigeon 93fc4b31e9 Belt: show the axis remap options in Develop mode only
preslice_remap_*, preslice_remap_global and gcode_remap_* describe the
printer's kinematics and are set once by its profile. A wrong value sends
the gantry outside the machine (a user preset with the pre-slice remap in
place of the G-code remap emitted gantry moves to Y=646 mm), so they are no
longer offered in Expert mode.
2026-10-02 01:02:58 -05:00
harrierpigeon 33fc99785a Belt profiles: print without a z-hop by default
On a belt printer a lift is a move along the belt axis (0.4 mm / sin 45 =
0.57 mm of belt travel out and back on every hop), not a lift away from the
part. The three belt printer bases now ship z_hop 0, the IR3 V2 leaf no
longer restates 0.4, and the BabyBelt Pro and IR3 V2 filaments stop
overriding the printer with filament_z_hop 0.4. The option stays editable.
2026-10-02 01:02:57 -05:00
harrierpigeon f219431935 GUI: the plate is open along Y for containment tests on an infinite-Y belt printer
PartPlate's containment tests treat the plate as open along Y on a belt printer with
belt_printer_infinite_y, so a long part is no longer flagged outside the plate in Prepare
while the slicer and the G-code checks accept it. The check reads the printer preset
through the app object, which does not exist headlessly, so it is guarded on the plater.
2026-10-02 01:00:36 -05:00
harrierpigeon b8a8e38ceb G-code: no crossing-perimeter avoidance while travelling on a brim apron layer
Crossing-perimeter avoidance dereferenced the (null) layer while travelling on a brim
apron layer.
2026-10-02 01:00:36 -05:00
harrierpigeon d755580c99 Belt brim: the purge prism never gets a brim, whatever its config says
The prism's generator already sets no_brim; PrintObject::has_belt_brim() now also ignores
any brim setting on the prism (belt_purge_tower_object), so a brim on the parts beside it
never blocks purging.
2026-10-02 01:00:36 -05:00
harrierpigeon e8926efbe3 Belt purge: detect filament changes by scanning the ordering, not the first layer's flag
ToolOrdering::has_wipe_tower() reads the first layer's flag. On a belt the first layer may
be a brim apron band, which carries neither object nor support and never gets the flag, so
with a brim the purge plan returned early and nothing was purged. Scan the layers for a
change.
2026-10-02 01:00:36 -05:00
harrierpigeon 2ae41bb0ef Painting: guard the top/bottom projection erase when no shell layers are requested
With top_shell_layers = 0 the `top` vector is never filled and erasing its begin() was
undefined (found by fuzzing on a painted object dropped below the plate).
2026-10-02 01:00:36 -05:00
harrierpigeon 6a6ceb2612 G-code: key the island tour cache on the island layout, never index past the islands
The per-filament island tour was cached by island centroids only. A later layer with the
same centroids but fewer islands (thin walls, negative volumes) reused the stale visit
list, whose catch-all index pointed past the layer's islands, and extrude_perimeters read
freed memory (three fuzz crashes, planar and belt). The per-instance island layout is part
of the cache key and the use site never indexes past the islands.
2026-10-02 01:00:36 -05:00
harrierpigeon 6be7911e40 CLI: survive a project with missing printable_height or fewer filaments than loaded
Found by fuzzing the headless slicer:

- A BBS-style 3MF without Metadata/project_settings.config segfaulted the CLI silently on
  the missing printable_height option.
- A project saved with fewer filaments (or filament groups) than --load-filaments overran
  the filament variant tables (segfault in the variant match) and then hit an uncaught
  ConfigurationError from set_with_restore_2 (std::terminate). The tables are regenerated
  for the filaments the project did not know about, the destination vectors grown first
  (only from a non-empty source), the match bounded, and a failure becomes a CLI config
  error.
2026-10-02 01:00:36 -05:00
harrierpigeon d57549159d 3MF loader: refuse non-finite vertex coordinates
A 3MF vertex with a nan/inf coordinate was accepted by both parsers and crashed qhull in
ModelVolume's convex hull while the file was still loading. Both vertex handlers refuse it,
and volume generation checks again whichever parser produced the geometry. The main
parser's _stop_object_xml_parser keeps a message a handler already set.
2026-10-02 01:00:36 -05:00
Clifford GarwoodandClaude Opus 5 c9b2b89b2b Correct the variant-keying note on the wipe tower estimate
filament_minimal_purge_on_wipe_tower became variant-keyed upstream, which
invalidated the comment claiming none of full_config's keys were.
WipeTower2::extract_wipe_volumes indexes it by raw filament slot. Not reachable
with shipped presets; noted rather than worked around.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-01 21:40:39 -04:00
Clifford GarwoodandClaude Opus 5 6c46942c57 Fix the mode name being cleared when a suggestion is picked
ComboBox::SetLabel is overridden and writes the text control when the text
control is shown, which it is on an editable combo. Clearing the label after a
pick therefore erased the name. Removed; the label is never written on this
control.

Suggestions are also rebuilt when the list opens. Built in add_row they filtered
against only the rows that already existed, so a row was offered names the rows
below it had taken.

Drops the 1px inset on the Primary label, which matched a frame the name field
no longer has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-01 21:08:58 -04:00
Clifford GarwoodandClaude Opus 5 1f8310c727 Merge upstream, including texture displacement and the instance lock
138 commits. The count is large because texture displacement merged with its
whole branch history behind it, going back to July, alongside config and preset
file locking across instances, a foundation for configurable printer agent
connections, and a day of smaller fixes and CI work.

Two conflicts, both the same shape: each side had appended to a sorted list and
git could not choose an order. libslic3r's CMakeLists gained InstanceLock
alongside our IMEXHelpers and IMEXZones, and the preset bundle loading test
gained an include for ParallelResolve alongside ours for IMEXHelpers. Both sides
kept, alphabetical. No logic conflicted.

Verified: 740 targets build clean under -Werror, and the Release suite passes
1740 of 1740, up from 1665 -- the 75 new cases arrived with the merge and all
pass. That mattered more than usual here, since preset loading and config
locking are both areas the IMEX preset code touches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-01 18:46:19 -04:00
Clifford GarwoodandClaude Opus 5 3c7b62bbca Offer the mode names this printer can carry in the name field
The name field becomes an editable ComboBox, the same pattern the sidebar uses
for parameters like sparse infill anchor length: predefined entries in a
drop-down, with the text still typeable. The mode table is authored for whatever
hardware the user has, so a closed list would be wrong -- nothing in the slicer
reads a mode's name except as the key a plate stores -- but the conventional
names are worth offering rather than leaving everyone to retype them.

What is offered follows the tool grid rather than a fixed list. Four carriages
get iq-copy and iq-mirror; multicolor needs a Span partner beside the primary
and a second gantry to copy the pair onto, so mc-copy and mc-mirror appear only
on a grid that can hold one. imex_resolve_routing() already refuses a multicolor
mode with no Span on the primary's gantry, and suggesting a name it would then
reject is worse than not suggesting it. Names a row already uses are dropped, so
the list only ever offers what is still free.

Two things about ComboBox matter when it is editable, which nothing else in the
tree does -- the other 78 call sites all pass wxCB_READONLY:

GetValue() returns the drop-down selection whenever there is one, so a name
typed after picking a suggestion would be silently discarded. Every read goes
through GetTextCtrl() instead. Field.cpp reconciles the same way for its own
open enums.

The constructor hands its value to TextInput as the LABEL -- the small
right-aligned slot a unit like "mm" occupies -- because a read-only combo hides
the text control and shows the label in its place. Left there, the name rendered
as a greyed echo beside the hint while the field itself sat empty. The combo is
built empty and the value written to the text control, and the selection handler
clears the label again afterwards, since SetSelection() writes there too.

The mode column widens to 176 to leave room for the drop-down arrow, with the
header spacer deriving from the same constant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-01 18:37:53 -04:00
Kris Austin 9d71125495 ci: allow running the Main build status workflow manually (#16057) 2026-10-01 18:25:12 -03:00
Kris Austin 88f05d3060 ci: move the nightly Build all to the quietest hour for macOS runners (#16053) 2026-10-01 17:58:00 -03:00
Kris Austin 4743d00793 ci: README build badge no longer shows failing when a queued main build is cancelled (#16054) 2026-10-01 17:56:57 -03:00
SoftFever 6a90946294 Keep the sketch value field on top of the dimension labels
Dimension labels drawn after the value field each lifted their own
window to the front, so a label could print over the number being typed.
2026-10-02 01:11:04 +08:00
Claude 410399fdb5 Design tab: follow Orca's theme, scale, mouse settings, dialogs and undo
Felix14-v2's review of OrcaSlicer#16019 found the tab behaving as a world of its own.

- Icons: the design_* glyphs were drawn in a fixed light grey, made for the dark ribbon, and the
  toolbar re-tinted some of them by rebuilding the bitmap from a wxImage, which drops the HiDPI
  scale factor Orca sets on Windows: at 150 % the icons came out half again too large for buttons
  that were sized in raw pixels, overlapping and clipped. The glyphs now use Orca's sidebar icon
  grey (#949494), which the icon cache maps per theme, nothing is re-tinted, toolbar glyphs drawn
  for Prepare's light toolbar use their "_dark" twin, and every size is in DIP.
- Theme: the chrome colours were read once, at construction, and nothing in the tab answered a
  theme switch, so switching left light surfaces and unreadable text in a dark tab and the other
  way round. The colours are now {light, dark} token pairs; MainFrame::on_sys_color_changed
  reaches DesignPanel::on_sys_color_changed, which moves every token colour onto the other
  theme's, runs the app's dark pass and re-rasterises the icons. Card borders are StateColors,
  resolved at paint time.
- Scale: MainFrame::on_dpi_changed reaches DesignPanel::msw_rescale, which re-rasterises every
  icon (buttons, flyout rows, card headers, the tree's image list, now sized from its bitmaps)
  and re-measures the Orca widgets.
- Mouse: the canvas no longer forces middle-drag to orbit and right-drag to pan; it reads the
  drag actions in Preferences > Control like Prepare. Left-drag is shared with picking, so the
  whole-body rubber band takes Shift+left-drag while left-drag is given to the camera.
- FPS counter: ImGui's display size is shared and only refreshed when a canvas sees its own
  size change; the Design canvas now re-announces its size when the tab is entered (and the
  editor canvas when it is left), as Plater does between Prepare and Preview.
- Viewport text: the status line and the tool readout were top-level popups over GL. A popup
  does not follow its frame, so the empty-canvas hint floated over other applications, and the
  readout was never taken down with the tab. Both are drawn by the canvas in the tool's ImGui
  pass now, with the theme's overlay style.
- Dialogs: messages use MessageDialog/RichMessageDialog; Add/Edit Variable is one Orca dialog
  with Name and Expression fields instead of two native text prompts; the Text dialog uses
  TextInput, ComboBox and CheckBox (its height is a TextInput: SpinInput is integer-only), and
  enumerates the installed fonts once per session. The ribbon's Confirm/Cancel, the reference
  pick buttons and the expression buttons are Orca Buttons; the variable actions are icon
  buttons like the other cards'.
- Undo: the tab's own Undo/Redo buttons are gone. The top bar's Undo/Redo drive the Design
  history while the tab is shown, greyed to what an undo would do, as Ctrl+Z and Edit already
  did.
- The first build of the tab logs how long each phase took ("Design tab build: ..."): it is
  under a second here but was reported at about fifteen on Windows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
2026-10-01 15:38:24 +00:00
Ian Chua 2f4758446a feat: add foundation for configurable printer agent connection (#16027)
* feat: add foundation for configurable printer agent connection

* fix: regression after merge

* chore: remove duplcate http include

* fix: remove unrelated moonraker.cpp changes

* chore: port add_platform_root_certificates to 15711

* fix: clean diff
2026-10-01 22:50:56 +08:00
HanifKoh c023632a7d Lock Config and Preset Files Across Instances and Write Them Atomically (#15861)
* Lock Config and Preset Files Across Instances and Write Them Atomically

Every running instance shares one OrcaSlicer.conf and one user preset
tree, and nothing kept their writers apart. Two instances saving at the
same moment, or the cloud preset sync thread writing while the GUI thread
saved, could interleave, and a reader in another instance could open a
preset JSON or .info file between truncate and close and get a partial
file, dropping that preset for the session with a parse error.

Add InstanceLock, a scoped guard that serialises the threads of one
process through a recursive mutex and other processes through an advisory
OS file lock: flock on POSIX, held on the guard's own descriptor so no
other close in the process can drop it, and LockFileEx on Windows. The
outermost guard opens the lock file and closes it on release, so nothing
stays open between saves and a data dir can be removed once nothing is
saving into it; the file itself is kept, since deleting it would let a
third instance lock a fresh file while the second still holds the old
one. It is best effort: when the lock file cannot be opened or locked, or
another instance still holds it after a second, the guard logs once and
lets the write proceed, then leaves the file alone for ten seconds, so a
hung instance never blocks every other one and a holder stuck in a
debugger does not cost a stall per save. The guard sits at the leaf
readers and writers: set_sync_info_and_save() calls save_info() under the
preset collection mutex, so a batch lock around save_user_presets() would
invert the order against the sync thread. The user preset scan reads its
files on worker threads without the guard, since the mutex would
serialise them, and takes it per file in the serial commit step, so a
save never waits for the whole scan. Each read keeps the bytes of the
preset and its .info as they were before parsing; commit compares them
with the disk under the guard and reads a file that changed again, so it
never deletes or writes back over another instance's newer save, nor
installs a .json and .info from two different saves; a preset another
instance removed in the meantime is not installed. Without the guard, in
a cool-down, the scan still loads the presets but leaves their files
alone: an unreadable file stays for the next scan, and a derived
compatible printer is not written back. Read-only scans, which is what
the CLI does, take no lock and create no lock file.

AppConfig holds OrcaSlicer.conf.lock in load() and save(); load is
included because the Windows path restores from the .bak copy. Every
user preset writer and reader holds user.lock: Preset::save(), which
writes no .info when the preset itself could not be written, since an
.info without its preset reads as a cloud deletion request, save_info(),
reload() and remove_files(), each preset the scan commits, the
bundle metadata reads and write, the .info removal after a cloud-confirmed
delete, the orphaned-.info scan on the sync thread, the bundle folder
removal on unsubscribe and the physical printer writers and delete
paths. A bundle import extracts under cache/ into a folder per process
and per import, where no scan reads.

Preset JSON, .info, bundle metadata, physical printer and config files,
and the caches and state files that already used a temporary by hand,
now go through write_file_atomically(), which writes <file>.<pid>.<n>.tmp
beside the target and renames it over, so a reader that never waits sees
a complete old or new file. A symlink is followed; a target that is not
a regular file is written in place; and when no temporary can be created
beside an existing target, or the rename itself is refused, by a Windows
reader holding the file open or a mount that cannot replace in one step,
the helper writes in place as before, since losing the save is worse
than a torn read. On POSIX the rename replaces the
target atomically where the old code removed it first and left a window
with no file at all; only a mount that refuses a one-step replace gets
the old remove-then-rename. A crash between temporary and rename leaves
the temporary behind, which no scan reads. Preset::save() returns
whether it wrote the preset, so the scan counts a compatible printer it
could not write back as an error. A failed config write keeps
the config dirty, and the idle handler waits ten seconds before retrying
while an explicit save always tries.

* Run the Cross-Process Lock Test on Every Platform

The test that checks the guard yields to a lock held elsewhere forked a
child to hold it, so it was left out on Windows. The OS lock belongs to
the handle on Windows and to the open file description elsewhere, so a
second handle in the same process is refused like another instance
would be. The test now holds the lock that way and runs everywhere.
2026-10-01 22:18:21 +08:00
SoftFever 96bf754af9 Merge branch 'main' into claude/inspiring-knuth-7cp6pk-upstream 2026-10-01 21:57:57 +08:00
SoftFever b102ae3aaa delete unwanted docs 2026-10-01 21:56:56 +08:00
SoftFever 1046851b50 add orca-wxwidgets skill 2026-10-01 21:56:56 +08:00
Lam Wei Lun 03377a0242 Missing cassert include (#16039) 2026-10-01 10:34:43 -03:00
Kris Austin e7c0e2cd82 ci: let main builds finish instead of cancelling them on every merge (#16044) 2026-10-01 10:34:13 -03:00
Kris Austin d1d14329d9 fix: exporting a sliced print again gives different G-code (#16025)
extrude_infill() and extrude_support() reversed the layer's extrusion
entities in place while chaining them, so each export started from the
previous one's reversed toolpaths, and each copy of an object from the
copy before it. The export-time region lists now hold const pointers,
and chaining reverses a clone instead.
2026-10-01 08:08:03 -03:00
HanifKoh 236a8786ef Keep the Orca Cloud Agent Tests Out of the System Keychain (#15980)
The display-name tests call set_user_session(), which persists the session.
The agent they built was in keychain mode, so every run saved a fake
OrcaSlicer/Auth session into the system keychain of whoever ran the tests,
replacing their real Orca Cloud login on any desktop with a working keychain.

Move them next to the other agent tests and build the agent the same way:
encrypted-file mode with a throwaway config directory.
2026-10-01 16:58:52 +08:00
HanifKoh 92d30fbc55 Clamp Ironing Line Spacing to a Usable Minimum (#15949)
An ironing line spacing of 0 reached the fillers from a 3MF, the CLI or
the per-filament override, which has no GUI guard. Concentric ironing
then never finished slicing, because a zero inset never shrinks the
region, and rectilinear ironing was silently dropped. Tiny positive
values produced an unprintable number of lines.

Top surface and support ironing now clamp the spacing to the 0.05 mm
floor the process GUI guard already enforces, so these configurations
iron at that spacing. Spacings at or above the floor, including every
shipped profile, are unchanged. The concentric filler also returns early
on a non-positive step so no other caller can hang it, and the filament
settings page now resets a too-small override the same way the process
page does.
2026-10-01 16:53:16 +08:00
ExPikaPaka ae3e41eb02 Add a regression test for a cyclic component reference
Stores a painted cube, points its component back at the object that holds it and
expects the load to fail. Without the bound the test does not finish: the work
list grows until the process is killed.
2026-10-01 09:14:46 +02:00
SoftFever 3384daa6bc Fix bundled Python crashing on macOS 26 and older when built with Xcode 27 (#16035)
# Description

With Xcode 27, building deps on macOS 26 fails at the Python install
step with a segfault, and a libpython built with Xcode 27 crashes on
macOS 12–26 the first time anything calls `os.pipe()`, which every
plugin `subprocess` call does. The macOS 27 SDK declares `pipe2()` and
`dup3()` as macOS 27-only, and CPython 3.12 calls them without a runtime
check once configure finds them, so on older systems they resolve to
NULL. This keeps CPython on the `pipe()`/`dup2()` fallbacks it already
uses with older SDKs; upstream fixed it in 3.13+
([python/cpython#153711](https://github.com/python/cpython/issues/153711))
but not in 3.12.

No change for builds with Xcode 26 or older, or on Linux and Windows.

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

## Tests

Rebuilt deps with Xcode 27 on macOS 26.6: the Python install step now
completes, the installed libpython no longer imports `pipe2`/`dup3`, and
CPython's `test_os`, `test_subprocess` and `test_posix` pass, apart from
one test that needs `_testcapi`, which `--disable-test-modules` leaves
out. Running CPython's configure against the macOS 26.5 and 27.0 SDKs
gives a byte-identical `pyconfig.h` for 26.5 with and without this
change, and for 27.0 with it. The x86_64 cross-build path was configured
on arm64 to confirm both of its configure runs pick up the change.

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-10-01 14:57:30 +08:00
SoftFever 377ebf2a03 Merge branch 'main' into claude/inspiring-knuth-7cp6pk-upstream 2026-10-01 14:51:35 +08:00
ExPikaPaka 92ab583ecc Reject 3MF component references that form a cycle
_generate_current_object_list expands component references through a work list
with no bound. An object whose component points back at itself, or a pair that
point at each other, makes the list grow until the process runs out of memory:
a few hundred bytes of XML take the slicer past 20 GB of resident size.

Bound the expansion by the number of objects in the file. A reference chain
longer than that has to revisit an object, so this rejects every cycle and no
acyclic file, however deeply nested. A second bound on the number of expanded
components stops an acyclic graph that fans out exponentially.
2026-10-01 08:50:22 +02:00
HanifKoh 6842d9c778 Keep the Plugin Tests' Python Packages Out of the Working Directory (#15981)
The plugin test fixtures start the interpreter before the test points
data_dir at its temporary directory, so PythonInterpreter creates
{data_dir}/python/packages and {data_dir}/log with an empty data_dir: a
python/ and log/ folder in whatever directory the tests run from. When that
is the test binary's folder, the next run's embedded-interpreter tests took
the stray python/ as their home and failed to start Python.

Give each fixture that initializes the plugin manager its own temporary
data directory, set up before initialize(), and only use the python/ folder
next to the test binary as the interpreter's home when it holds a standard
library.
2026-10-01 14:42:20 +08:00
HanifKoh 1a5bc8982d Stop Logged-Out Login Polling from Hitting the System Keychain (#15979)
With stealth mode off the home page asks for the login status every 2 s,
and while nobody is logged in that ends in clear_user_secret(), which
opened the system keychain and deleted the OrcaSlicer/Auth entry on the
UI thread every tick. A working keychain cost a D-Bus round trip per tick;
a keychain that never answers blocked the UI for 25 s per tick. It also
deleted a login another running instance had just saved, and ignored
use_encrypted_token_file, so opting out of the keychain did not help.

Remember whether this process read a secret from the store or wrote one,
and only then touch the store when a logged-out poll asks for a logout.
A logged-out instance never touches the keychain, and in encrypted-file
mode the poll no longer opens the keychain at all. A secret this process
cannot read, such as a token file encrypted for another OS user sharing
the data directory, is left alone by the poll. An explicit logout still
wipes both backends, so a token stranded by switching the token storage
option cannot sign the account back in later.
2026-10-01 14:41:50 +08:00
SoftFever 2a9cb32c1f Fix bundled Python crashing on macOS 26 and older when built with Xcode 27
Building deps with Xcode 27 on macOS 26 failed at the Python install step,
and a libpython built with Xcode 27 segfaulted on macOS 12-26 whenever a
plugin started a subprocess.
2026-10-01 12:29:57 +08:00
158ada9054 Merge the finer Xplorer 0.4 tiers and the IQEX toolchange prime
Community work from Rob Niccum, adding 0.04mm Ultra Fine and 0.08mm Extra Fine
for the 0.4 nozzle, a toolchange prime on the three IQEX machines, and
recompressed cover art and build plate model that are byte-identical in content.

Bundle version bumped to 02.04.00.13 here rather than in his branch, so the two
of us were not editing the same line while his review was open. Without the bump
the updater refuses the bundle outright, and nothing in CI catches that.

Verified rather than assumed, since his own validator predates the parallel
printing code and the fine tiers changed shape during review: the full profile
check passes all five stages tree-wide, and both new tiers slice clean on all
four 0.4 machines, 8 of 8. His last revision dropped a bottom_shell_thickness
override that was making those tiers thinner against our common process rather
than thicker against his; the emitted G-code now carries the inherited 1.0,
which is what that fix was for.

Co-Authored-By: Rob Niccum <klober81@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-01 00:01:48 -04:00
Clifford GarwoodandClaude Opus 5 74409f1d3b Merge upstream, and move the IMEX placeholders onto the new def macro
Eleven commits, including a typed-config G-code export speedup, a printer agent
refactor that generalizes the infrastructure beyond Bambu, gyroid optimization,
and three CLI crash fixes.

One conflict, in PrintConfig.cpp. Upstream introduced a new_def macro and began
migrating the placeholder table onto it, adding curr_bed_type that way in the
same block where this branch had added imex_mode, imex_mode_index and
imex_mode_gcode in the older def = this->add(...) form. Both sides are kept and
ours are converted to the macro, which expands to the same three statements and
wraps label and tooltip in L() exactly as before, so nothing changes about what
is registered or what is translatable.

Note for anyone building this branch: the agent refactor adds a dependency,
LibDataChannel, so the deps tree needs dep_DataChannel built before the app will
configure. A distribution package of the same name will be found first if one is
installed, and the resulting error names a missing RelWithDebInfo location
rather than the wrong package, so point LibDataChannel_DIR at the dependency
prefix if that happens.

Verified: 789 targets build clean under -Werror, and the Release suite passes
1665 of 1665, up from 1630 before the merge -- the 35 new cases arrived with it
and all pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 23:55:21 -04:00
Clifford GarwoodandClaude Opus 5 dafc4ce39c Rework the parallel modes editor from the UI review
Addresses the interface notes on the IDEX/IQEX modes editor.

Add Mode moves from below the rows to the top of the panel, beside a "?" button
that now carries the overview text as its tooltip. At the bottom the button
shifted down the page every time a mode was added, so where it sat depended on
how many modes already existed. It is an Orca Button in the Confirm style, width
matched to the mode column it creates a row in, and the panel opens on the
legend rather than on a paragraph.

Remove moves out of the right-hand column, where it sat one icon away from
Edit -- a destructive control beside the one pressed most -- to under the name
field it deletes, and its icon becomes a boxed minus rather than an X, which
read as "close". Reset joins Edit in the right-hand column, which is now
top-aligned so the icons hold position regardless of row height. Tool tiles are
square at 24px, and the header spacer tracks that width so the column titles
stay over their columns when the grid changes shape.

The two text fields were landing on GTK's near-black default border, invisible
against the panel: measured 45,45,49 against a 43,43,43 background, where the
settings fields above use 74,74,81. wxTextCtrl cannot color its own border, so
each sits in a one pixel frame taking the color TextInput derives for the
theme, and carries wxBORDER_NONE so Windows and macOS do not draw a native edge
inside it. The G-code boxes also take the monospace face EditGCodeDialog uses.

Bed zone fills drop to roughly half opacity in the Standard theme. They cover
whole quadrants for a whole session, so at swatch saturation they dominate the
scene. The collision strip is dimmed less, since it marks where a head hits
something. The deuteranopia, tritanopia and high contrast themes keep their
alphas: those are chosen for discriminability, which is the opposite trade.

Also fixes the icon size never applying. All four ScalableButton call sites
passed eight arguments, so the size bound to use_default_disabled_bitmap and
bmp_px_cnt kept its default of 16. Both are passed now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 23:38:37 -04:00
Clifford GarwoodandClaude Opus 5 e7fad16137 State the no-tool pressure advance contract per firmware
set_filament_pressure_advance's declaration said -1 omits the tool qualifier,
without qualification. That holds on Klipper, Marlin and BBL but not on
RepRapFirmware, where no-tool keeps the historical `D0`: a bare M572 applies to
whichever tool is selected and errors when none is, so omitting the qualifier
would make pressure advance depend on tool-selection state for every RRF user,
none of whom are using IMEX. The behavior is deliberate and unchanged; only the
declaration overstated it. GCodeWriter::set_pressure_advance and the index-space
notes in IMEXHelpers.hpp already described it correctly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 21:00:55 -04:00
Clifford GarwoodandClaude Opus 5 d5d940d6b4 Cover the per-carriage pressure advance and temperature emissions
In a parallel mode no tool changes occur, so two loops address each active
carriage explicitly: pressure advance before the print, and the second-layer
drop off the initial-layer temperature. Neither had coverage, and both fail
silently -- a carriage missing from one emits nothing at all, so it holds the
initial-layer temperature for the whole job, or runs on whatever pressure
advance the firmware was last given. The new case pins that the carriages
addressed are exactly the ones the mode declares active, each with the values of
the slot physical_extruder_map routes its head to.

The test needs filament_self_index, set here on imex_7x4_printer() so the whole
file has it. Production authors that key 1..n; its all-1s default collapses
every per-filament vector to filament 1's value through get_config_index_base(),
which leaves a per-slot assertion comparing a value against itself.

Also bounds the second-layer loop on filament_diameter alone. The bound belongs
in slot space, and filament_diameter is the one per-filament vector never
expanded per variant; the previous min() against nozzle_temperature mixed the
two index spaces without changing the result. The pressure advance loop already
bounds this way, so the two now read alike.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 20:54:17 -04:00
Clifford GarwoodandClaude Opus 5 477fb2eca9 Pair the AFC lane map with the device it was polled from
The lane map fetched over Moonraker was applied to whichever printer preset
happened to be edited when the response arrived, with no check that it was the
machine polled. Not a race, as the whole chain runs on the GUI thread with the UI
blocked, but a steady-state mismatch: two IMEX printers of different models with
the same logical extruder count both pass every existing guard, and the map is
written to the wrong preset and dirties it with no user action.

The pairing is now evaluated when the callback runs, against the selected device,
on the predicate update_sync_status() uses. Capturing an identity at connect time
would instead ask whether the edited preset had changed since then, and would
reject the user who selects a machine and only then switches to its matching
preset.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 20:54:17 -04:00
Clifford GarwoodandClaude Opus 5 a3eccdc8b7 Resolve the IMEX mode the same way everywhere it is read
A plate can carry its own parallel mode, or leave it on Primary and inherit the
process preset's. set_imex_mode() erases the key on Primary precisely so the
preset's value survives the config merge, and the slicer honors it: Print::validate()
and the G-code path both read the merged object config, so such a plate slices, and
emits, in the preset's mode.

Three places that describe that plate did not resolve it the same way. They read
the plate's own value and stopped, so on a plate left at Primary:

  the multi-material conflict badge stayed dark on a plate validate() will refuse,
  which is the one invariant the comment above it claims to keep;
  the bed-temperature and filament-type warnings never ran, so the job went out in
  copy or mirror with a mismatched bed and no notice;
  the plate tooltip reported Primary for a plate about to print in another mode.

The geometric badge beside the first of those already resolved correctly, because
it goes through the zone layout, so one badge fired while its neighbour stayed
dark on the same plate.

The fallback had been written out by hand four times. Three are now collapsed onto
one accessor, get_effective_imex_mode(); the fourth is in libslic3r, which cannot
call a GUI method and resolves the two modes itself from arguments. The remaining
callers of the raw get_imex_mode() want the raw value and keep it: the accessor
itself, the zone layout call that passes both modes separately, the mode menu and
the left-click cycle, which act on what the plate stores, and the reset that looks
for plates whose own mode was removed.

The tooltip is the one place the two readings meet. It headlines the stored mode,
because it sits on the button whose menu and click act on that value, and names
the inherited mode after it when they differ -- so one control no longer says
three different things while still telling the user what will actually print.

Reachability, honestly: nothing in the UI writes the process preset's copy today,
so this needs a hand-edited preset, a vendor process profile or a project that
carries one. It is latent rather than live -- and it stops being latent the moment
a process-level mode selector exists. Note the key lives in the process preset, so
one value there would govern every Primary plate in every project using it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-30 20:54:17 -04:00
harrierpigeon 4f110bc261 Belt scarf test: slice without a z-hop
The default 0.4 mm z-hop is a 0.57 mm move along the belt axis and its
return tripped the back-step check. Shipped belt profiles print without a
z-hop, so the test does too.
2026-09-30 17:42:16 -05:00
harrierpigeon 69a08915d1 Belt: do not fail the G-code height check against belt travel
check_multi_extruder_gcode_valid() compares each object's max Z with
printable_height. On a belt printer machine Z is belt travel (a 3DBenchy
on the BabyBelt Pro runs from Z=197 to Z=309 on a 69 mm printable_height),
so every belt export set the over-height error bit and the CLI refused the
plate with -102 "G-code in unprintable area". The preview already skips its
ToolHeightOutside warning for the same reason; the export check now does
too. The XY printable-area check is unchanged.
2026-09-30 17:29:33 -05:00
harrierpigeon 2c0570c97d Drop references to planning docs that are not in the tree
The MachineKinematics comments pointed at docs/superpowers plan files,
which are gitignored working notes.
2026-09-30 15:44:49 -05:00
harrierpigeon 48af5b5d97 Read the pre-slice remap header tags at their real length
The header tags lost their belt_ prefix in the Part 3.2 rename (20
characters now), but the parser still skipped 25, so every axis read as
pos_x. Found in Hanif Koh's review of #14394.
2026-09-30 15:44:18 -05:00
harrierpigeon 6cf747808c Belt: never start a scarf joint seam below the layer
A scarf joint begins one layer height below the current layer and ramps
up along the wall. On a tilted belt that start is a step backwards along
the belt axis, into the previous layer's wall at the seam: 0.283 mm per
0.2 mm layer at 45 degrees. With an aligned seam the nozzle rams the same
spot on every layer. A BabyBelt Pro benchy with seam_slope_type=external
showed 601 such back-steps from layer 107 on, and in the field the belt
"jumped backwards" and the head knocked the part loose.

Belt printers now skip the scarf in GCode::extrude_loop, and the process
tab greys the scarf controls out for them, as it already does for arc
fitting. The regression test slices a cube on a belt with the scarf
enabled and checks the belt axis never steps back by a layer pitch.
2026-09-30 15:44:18 -05:00
harrierpigeon ad6afce67b Merge upstream/hanif/belt-printer-fixes into belt/final-round
Brings in upstream/belt-printer (the Sept 14 main merge) plus Hanif Koh's
21 review-fix commits from PR #15685, on top of the MachineKinematics
refactor and the purge-prism / tree-support / first-layer-speed fixes.

Conflict resolution:
- BeltGCodeWriter is gone (kinematics refactor), so Hanif's plate-offset
  fix for it is ported into GCodeWriter: the first-layer-plane checks in
  travel_to_xy / travel_to_xyz / _travel_to_z now evaluate the plate-local
  point, and BeltGCode::init_belt_writer hands the stored plate origin to
  the writer it installs.
- init_belt_writer(Print&) takes Hanif's signature; the BBL flag is set on
  the surviving writer by GCode::_do_export.
- The shared emit_belt_brim_bands() loop keeps the BeltFloorObjectGuard the
  local branch added, so apron bands classify first-layer height against
  their own object.
- eager_lift keeps effective_type: it now carries set_force_normal_lift().
- GCodeWriter's initializer list follows Hanif's member order with
  m_kinematics in its declared position.
- TreeSupport::detect_overhangs uses Hanif's clamped build_plate_tilt_slope()
  for the non-belt path and the belt shear for the belt path.
2026-09-30 15:22:19 -05:00
Claude 5254007a10 Merge main into the Design tab follow-up
main moved the CAD docs into docs/HLSD/design-tab.md and the offer
generator and atlas into scripts/CAD/ (#15803).

- The four docs this branch had edited are deleted as on main. What the
  branch changes about the design goes into the HLSD doc: the right-click
  is judged by drift over the whole press, with no time budget; a Text
  feature stores its outlines as well as its string, font and height; a
  new section on how the Design canvas renders bodies (studio lighting
  through the shared phong shader, B-rep edge ribbons).
- gen_offer_table.py keeps both main's atlas validation and this
  branch's L() markers on user-facing strings.
- The Text verb's hint is now changed in scripts/CAD/tool_atlas.json as
  well, so `gen_offer_table.py --check` passes.
- DesignPanel.cpp keeps the DesignTextDialog include and main's new
  path in the offer comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
2026-09-30 18:00:46 +00:00
Claude d33dfa8f2c Tests: keep CAD test names ASCII so Windows CTest can select them
OrcaSlicer#16019's Windows unit-test jobs failed on one test, "Hole
standards: ... 82° countersink": CTest passes the test name to Catch on
the command line, the ° arrives in the ANSI code page, the filter
matches no test and the run counts as failed ("No test cases matched",
"No tests ran"). The name now says "82 degree"; it was the only test
name in the suite outside ASCII.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
2026-09-30 12:29:39 +00:00
Claude da76c3f43f Design tab: Offset does not name a variable near
The Windows builds of OrcaSlicer#16019 failed in DesignSketchTool.cpp:
windows.h defines near (and far) as empty macros, so
`Vec2d near = m_op_anchor;` reads as `Vec2d = m_op_anchor;` under
clang-cl. The variable is now called closest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
2026-09-30 11:23:12 +00:00
Claude 1ff8fc465c Design tab: body edges 2 px wide
Seen on the rig: at 1.5 px the body edges read as hairlines next to the
sketch and selection strokes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
2026-09-30 08:34:46 +00:00
Claude a4387e93f2 Design tab: revolve about a line of the sketch
Reported: the Revolve axis could only be the sketch plane's X or Y axis
through its origin, so a half-profile drawn beside a centerline, the
usual way, could not be revolved about that centerline.

- CadFeature::revolve_axis_entity names a Line of the profile sketch to
  revolve about (a construction centerline, or an edge of the profile
  itself); -1 keeps revolve_axis. Appended at the end of the framed
  recipe, so existing projects load and rebuild unchanged. Revolve and
  Surface Revolve resolve their axis in one place (revolve_axis_of); an
  index that no longer names a line fails with a reason.
- SketchEngine::make_revolve takes the world axis. A profile with points
  on both sides of it is refused with "the profile crosses the revolve
  axis"; MakeRevol failed there with no reason.
- The Axis list of both cards reads Plane X, Plane Y, then every line of
  the sketch, named as the constraint list names them (Centerline E4,
  Line E3). A fresh revolve preselects the sketch's centerline when it
  has exactly one. The gizmo turns about the chosen axis and draws it
  dashed; construction lines no longer pull its centre.

Tests: a rectangle beside a construction centerline revolves into the
tube of the expected volume along that line; about its own edge, into a
cylinder; an axis through it is refused with the reason; a stale axis
index fails with a reason; the axis survives save and load. The
truncated-recipe test accounts for the new tail field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
2026-09-30 08:34:46 +00:00
Claude b8d45c6bb1 Design tab: studio lighting and body edges in the 3D view
Reported: once extruded, a part is hard to read; the lighting says
little about its shape. Both lights of the object shaders sit near the
camera, so the sides of a part come out in almost the same tone, and
nothing marks where one face ends and the next begins.

- The phong shader gains a studio lighting model, chosen by a new
  lighting_model uniform: a sky/ground hemisphere in world space (up
  faces cool and bright, down faces warm and dark), a key light from the
  upper left and a weak fill from the right, a plastic-like highlight,
  and a darker base with a faint sheen toward the silhouette so curved
  faces read as round. GLCanvas3D::set_studio_lighting() makes a canvas
  draw its objects with it whatever the realistic-view preferences; only
  the Design canvas turns it on. Every canvas sets the uniform on each
  use, 0 for the slicer's, so they render as before.
- Every B-rep edge of a body is drawn as a thin dark line over it, depth
  tested and pulled a few pixels toward the eye so it wins against the
  faces meeting at it and hides behind the faces in front. The seam of a
  closed surface and degenerate edges are left out
  (GeometryEngine::display_edges). Edges are sampled once per shape and
  kept across recomputes that leave a body unchanged; bodies faded by
  body focus get fainter edges, and a dress-up previewing its result
  alone hides them with the bodies.

Tests: display_edges gives a box its 12 edges at their lengths, a
cylinder its two round rims without the seam, a cone its base rim
without the seam or the apex.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
2026-09-30 08:34:46 +00:00
Claude b060101707 Design tab: Text verb hint describes the Text feature
The context menu still said the text outline is added to the sketch as
editable lines, which is what it did before Text became its own
feature. Only SVG goes into the open sketch now; the comments on that
path say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
2026-09-30 08:34:34 +00:00
Claude 5d4622c822 Design tab: Text is its own feature, previewed in the view and editable
Reported on the rig: Text showed no preview where it would go, its
dialog was pinned over the middle of the window (GNOME attaches a modal
dialog to its parent and it cannot be moved), and once confirmed the
text could not be edited and did not appear in the feature tree. Inside
an open sketch it became loose lines of that sketch.

- Text is always a feature, "Text N" in the tree. A new text goes on the
  plane of the open sketch (committed first when it holds anything,
  closed when it is empty), else centred on the picked face, else on the
  reference plane.
- The dialog is modeless and opens at the top right of the window. The
  feature is created at the first character and redrawn on every change,
  so the text appears in the view where it will be as it is typed. Enter
  inserts it, then the usual move/scale gizmo and Confirm; Esc, Cancel or
  closing the dialog takes it out again (undo to the checkpoint taken
  when it appeared).
- CadFeature keeps text_string, text_font (the WxFontUtils descriptor)
  and text_height, appended at the end of the framed recipe. Editing a
  Text feature reopens the dialog with them and redraws the outline in
  place, keeping its placement. The outlines are still saved, so the
  project opens the same on a machine without that font.
- The MCP control refuses writes while the Text dialog is open.

Tests: the text parameters survive a save and load along with the
outline; the truncated-recipe test accounts for the new tail fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
2026-09-30 08:34:34 +00:00
Claude 4983796970 Design tab: sketch Offset takes the whole outline, not one segment
Offset worked on the single entity under the pointer. On an outline made
of many short entities (a text glyph, an imported shape) that is a
segment a few tenths of a millimetre long, and the starting distance was
a tenth of that: the ghost was too small to see, and a typed distance
moved one invisible segment. It read as "Offset does nothing, no
preview".

- The pick takes the chain the entity belongs to (connected_loop, same
  construction state), highlights all of it, and offsets it as one
  outline through offset_entities, which already joins and trims chains
  at their seams.
- The starting distance is a twentieth of the outline's size, so the
  preview is visible at once.
- The arrow reads its side off the ghost, since the engine may walk the
  picked entity backwards in the chain; it keeps pointing at the offset
  copy, flipped for a negative distance.
- A single entity still gets its Parallel/Concentric constraint. A chain's
  offset is placed as geometry, since its joined entities no longer map
  one to one onto the originals.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
2026-09-30 08:34:33 +00:00
Claude c664a24f4b Design tab: a closed loop that crosses or folds back is not a region
A sketch drawn on the rig extruded to walls with no caps. Every joint of
its loop met, so the loop analysis called it closed and MakeFace
accepted it, but the loop crossed itself: an arc left the top line's end
heading back over it and crossed it again 2.5 mm on. A second arc left a
0.28 mm line tangent to it but the other way, a cusp. The prism of that
face is an invalid solid, and it was shipped as a body.

- SketchEngine::wires_to_face checks the face it builds and, when OCCT
  calls it invalid, fails with "the profile crosses or folds back on
  itself, so it does not bound one region". The extrude reports that
  instead of producing the broken body.
- sketch_loop_defect() judges a closed loop of lines and arcs exactly:
  any contact between two of its entities away from the joints they
  share, or a joint where the curve turns straight back (a cusp; OCCT
  still builds that one, but it is never what was meant). It returns the
  point.
- The sketch uses it on every region: the loop is tinted red, the point
  gets a marker, and the status line says what the red means the first
  time one appears. The MCP loop report lists the defects and no longer
  calls such a profile buildable.

Tests: the rig's profile, with each defect and with both, from a
recording of the real entities. The analysis names the cusp at its joint
and the crossing on the top line, in either traversal order, and
passes ordinary tangent and collinear joints. The extrude refuses every
crossing variant with the reason, and the same arcs swept the other way
round extrude to a valid solid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
2026-09-30 08:34:33 +00:00
Claude 3c218591bc Design tab: pick several solid edges and dress them in one feature
A solid edge could only be picked one at a time, and Fillet/Chamfer took
either that one edge or a whole face group. Rounding three chosen edges
meant three features, whose edge ids each resolve against a body the
previous one had already changed.

- Shift+click (or Ctrl+click) on an edge of the body already picked adds
  it to the selection, or removes it; the same modifiers that extend a
  sketch selection. The whole set is highlighted. A plain click replaces
  it, as before.
- Fillet/Chamfer dresses every picked edge in ONE feature at one size,
  all ids resolved against the same body. The card says "3 edges", the
  status line and the offer header name the count.
- CadFeature gains dressup_edges, appended at the end of the framed
  recipe, so existing projects load and rebuild unchanged. dressup_edge
  keeps the first edge, so an older build opening a newer project still
  dresses that edge instead of falling back to the face group.
- The MCP fillet/chamfer verbs take `edge` as one id or an array.

Tests: a fillet on the four picked top edges equals the Top face group
exactly; the list survives save/load; two opposite chamfers remove
exactly twice one; a missing id fails with a reason. The truncated-
recipe test accounts for the new tail field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
2026-09-30 08:34:33 +00:00
Claude f4cdeb1706 Design tab: build constraint labels from UTF-8, not the C locale
Drawing a line or a rounded rectangle crashed the Linux AppImage: the
constraint list labels were formatted from narrow literals holding "—",
"·" and "°". wx converts a bare char* with the current locale, and the
AppImage's AppRun sets LC_ALL=C, so the conversion failed, the format
string came out NULL and wxString::Format dereferenced it
(wxFormatConverterBase<wchar_t>::Convert, from constraint_label via
rebuild_constraint_list). A build started under a UTF-8 locale never
showed it.

These literals, and the "…" of the interference report, now go through
wxString::FromUTF8, as the panel's other non-ASCII literals already do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
2026-09-30 08:34:32 +00:00
Claude ae1dc95756 CAD: catch OCCT failures in body_touching_sketch, Standard_Failure first
The extrude default query must never throw; on OCCT >= 8 Standard_Failure
derives from std::exception, so its handler comes first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
2026-09-30 08:34:25 +00:00
Claude 205bc1b332 Design tab: Text dialog with font, height and live outline; offer header
- Text opens a dialog instead of a bare text entry: any installed font
  (bold, italic), the height in mm, and a live outline of exactly what
  will be inserted with its size. Enter inserts, Esc cancels; the last
  font and height are remembered. text_to_regions gains an overload
  taking a loaded font.
- The offer menu opens with a greyed title naming what the rows act on
  ("Flat face 4 of Body 2", "Sketch line", "Nothing selected").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
2026-09-30 08:34:25 +00:00
Claude c74ddf8bb5 Design tab: translatable offer, one vocabulary, reports in the status line
- Offer table: user-facing strings carry the L() marker so xgettext
  extracts them; DesignOffer.hpp, DesignSketchTool.cpp and
  SketchInlineEditor.cpp are listed in localization/i18n/list.txt.
- Offer: model-mode Constrain sits in the same row as the sketch one;
  Interference is wired; Rib shows its R key; a verb that accepts the
  selection but is blocked by the document stays greyed with its reason
  instead of vanishing from the submenu; one refusal wording per verb.
- Extrude infers Join when the profile touches a solid (new
  CadDocument::body_touching_sketch) and on face push/pull; New body in
  free space. Revolve/Sweep/Loft/Boolean use the same result words.
- Interference and volume/area reports go to the status line in mm3/mm2
  instead of modal dialogs; Delete Body no longer asks (it is undoable).
- New feature names match the card header ("Extrude 3"), translated;
  "Coordinate system", "Angle (°)", center/color spelling, translated
  face and length readouts, slot hints say width.
- CAD gizmos in Prepare are selectable only with the CAD feature on; the
  sketch auto-close setting is stored per design.
- Docs: confirm/cancel rules, enabling the feature and MCP in
  design_tab.md; drift-only right-click in interaction-model.md; the
  portability note rewritten to describe the integration as it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
2026-09-30 08:34:25 +00:00
Claude 829124982d Design tab: one rule for mouse, Enter and Esc; honest status messages
Keyboard and mouse
- Esc drops what is pending (picks, a dimension's first point, an edit-op or transform) and
  never applies it; Enter applies a ready edit-op/transform, ends a polyline/spline chain,
  ends an armed tool, and confirms a feature card exactly when its ✓ is enabled.
- Right-click only abandons the gesture in progress; with nothing pending it opens the offer
  in every tool (Trim, edit-ops, transforms, Dimension, Constrain, TransformArt, move gizmo).
  Clicking empty space no longer commits. The offer needs no timing, only a still press.
- Delete removes only an explicit selection. Undo/redo inside a sketch go through the same
  route as the buttons (whole shapes, with redo); Edit > Undo follows the shown tab.
- The canvas no longer handles Delete/Esc/Ctrl+Z itself (Backspace in a value field deleted
  the geometry it measured); F is in the panel's key map.
- Value fields: a refused value keeps the field open with the reason; click outside and Tab
  commit; an untouched field commits the exact value; any decimal separator is accepted;
  lengths are always mm; validation is the same for every editor.
- Snapping: the marker shows only where the click will actually snap; pick tolerances are
  one set of pixel budgets (Constrain picks within reach; no mm floor on labels).

Messages and consistency
- set_status(kind, text) gives every status line its own colour and glyph; kernel errors are
  translated into sentences and formatted, not concatenated; sketch refusals go to the status
  line instead of the per-frame HUD that erased them.
- Hints describe the gestures that now work; Dimension shows its second step; Constrain uses
  the sketch palette (red means conflict only); the straight slot's value is its width.
- Hole/Thread/Project keep the user's pick or refuse up front; circular pattern opens with its
  own preview; thread fields use the nominal diameter and the ISO internal depth.

Integration
- MCP loads the project's recipe before touching the document, refuses to mutate it while the
  tab is busy, never runs a request that already timed out, only replaces a socket at its
  path, caps line length and removes the socket at exit; not started in the G-code viewer.
- Hiding a feature keeps later body references on their bodies; a design keeps its modeling
  origin across printer changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
2026-09-30 08:34:25 +00:00
Claude a043979f44 CAD kernel: fix the sketch solver, trim/offset/mirror, and history references
Sketch layer
- Trim/extend of an arc (and a circle opened into an arc) rewrites p0/p1 from the new angles;
  the wire builder, the solver and snapping read them.
- Tangency binds the arc end that touches the line (or the other arc) instead of always the
  start, so a fillet tangent to both legs solves; circle-circle / circle-arc tangency uses the
  centre distance instead of CURVE_CURVE_TANGENT, which aborts on a circle.
- Point-on-line distances and circle-line tangency keep the side the geometry is on; a point
  on an arc's rim uses PT_ON_CIRCLE.
- The partitioned solve keeps constraints onto the origin/axes and counts free entities' DOF.
- Zero-radius circles get no solver primitive and build no wire; constraints the solver cannot
  apply are reported in SketchSolveResult::skipped.
- EllipseArc: mirror no longer yields the complement; after a solve its angles and ends are
  re-derived; on the XZ plane it is no longer built mirrored.
- Offset: a circle follows the "+d = left of travel" rule (it shrinks, like a CCW arc chain);
  chains are joined at the weld tolerance. Bridge end pole fixed (G1, no cusp). Negative-scale
  transforms keep arcs and ellipses on their ends. Inference tolerances aligned with the weld.

Model layer
- Body references follow the body across delete / reorder / hide (resolved by the feature
  that made it); datum-plane ordinals are re-pointed; an index past the end is an error, not
  "the last body". New set_feature_enabled(). A move that puts a consumer above its input is
  refused.
- Threads made from now on read thread_radius as the nominal major radius (internal: bore to
  minor, groove to major; external: groove cut into the rod); older recipes build as before.
  Bad thread parameters say why. Circular patterns span their angle end to end (new ones);
  add_pattern pivots on the modeling origin.
- clear() drops variables; expression fields the GUI offers are bindable (thread_diameter,
  helix_*, thicken_thickness, ...); deg()/rad() in expressions; the recipe saves the modeling
  origin and body colours; names/colours follow bodies by identity.
- Boolean and dress-up failures throw instead of returning the input; one produces_body();
  hole standards corrected (82° inch countersinks, UNC names, #10-24); legacy profile solve
  validates indices and writes back only on success; v4 recipes read with a frozen field list.

Tests cover each of the above.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QK4VgguuCAk2hZLWgcjJb9
2026-09-30 08:34:25 +00:00
Tommaso Bianchi 73bfcccd50 The sketch tools stop lying about what they did
Twelve defects found by the 2D design pass.

- ONE erase path repairs the dimensions' cached constraint indices. Removing a
  constraint renumbered m_constraints and left every later DimAnnot.con pointing
  one slot short, and set_dimension_value checked only the range, not the slot's
  identity — so editing a dimension's value could overwrite an unrelated
  constraint. All five mutators (badge delete, fillet/chamfer, Move/Rotate/Scale,
  the two drop paths) now route through erase_constraints().
- apply_dimension validates BEFORE moving or recording. A value outside a case's
  threshold moved nothing and then recorded the constraint anyway, handing the
  solver a number it could never satisfy; the socket guarded against this, the
  tool did not.
- A Distance and its zero case are one dimension slot: typing 0 and then 5 used
  to leave a Coincident AND a Distance on the same operands.
- A dimension whose constraint the solver rejected is named: the label renders in
  the refusal colour and the commit says the sketch is over-constrained, instead
  of showing a number the geometry does not have.
- Six silent refusals now speak: fillet/chamfer on a non-corner and on an
  overrunning radius, offset on an ellipse or spline (the kernel's own reason),
  a rejected array binding ladder, a dimension pick on an unsupported entity.
- set_tool commits a ready transform instead of dropping it, the rule the ready
  edit-op already followed.
- Constraint releases are reported, and roles_of is one function again (the two
  copies had already diverged on EllipseArc).
- Labels no longer collide: every label is its own centred ImGui window at an
  anchor whose offsets are multiples of the text height, so on a feature smaller
  than one text height a line's Length and Angle labels landed on the same spot.
  draw_text, the one function all of them pass through, now pushes a colliding
  label clear of the ones already drawn this frame.

Verified: build and LTO link on behemoth (exit 0, new binary); on the rig,
badge-delete took constraints 3->2 with dof 9->10 and the geometry untouched;
the draw-then-edit chain committed a typed 70 to exactly 70.0 mm; and a 5.4 mm
selected line renders "5.4 mm" and "21.8°" as separate readable labels.
NOT yet exercised: the stale-index corruption itself (needs a middle delete with
a labelled dimension after it), the poison-value guard through the field, the
transform commit, and the red refusal label.
2026-09-30 08:34:25 +00:00
Tommaso Bianchi c7700acc95 The 2D sketcher tool architecture, batches 9-14
The design pass over the rest of the 2D tool suite: Select/Measure,
Dimensioning, Constraints, Modification (Trim/Extend/Split/Offset/Mirror),
Dress-up (Fillet/Chamfer) and Transform, each with its FSM, its event routing,
its C++20 blueprint and the defects it exposed.

Thirty-two findings, every one verified against the tree with file:line, plus
nineteen open scope calls and a closing note that orders the six cross-cutting
changes by what unblocks what. Batches 1-8 were delivered in-session; only
their cross-cutting results (six archetypes, two defect classes) survive here.
2026-09-30 08:34:24 +00:00
Tommaso Bianchi 54b3e6c7da Include GUI.hpp where into_u8 is used (the upstream sync dropped the transitive path) 2026-09-30 08:34:24 +00:00
Tommaso Bianchi 812063d085 The label under an open value field is not drawn twice 2026-09-30 08:34:24 +00:00
Rob Niccum 2f3545e27b Vivedino Xplorer: fine tiers inherit bottom_shell_thickness from common 2026-09-29 23:05:37 -04:00
Clifford Garwood 8350bac123 Merge the per-toolhead extruder colors 2026-09-29 21:43:55 -04:00
Clifford GarwoodandClaude Opus 5 0c09327c1a Merge upstream, and index pressure advance by extruder variant
Upstream moved pressure advance onto the extruder variant: enable_pressure_advance,
pressure_advance and the four adaptive keys joined filament_options_with_variant,
the repeated inline blocks in set_extruder() became a helper, and the lookups
moved from the filament id to get_filament_config_index().

All three conflicts were the same collision, because this branch had modified two
of those same inline blocks to pass a tool qualifier so each carriage is addressed
explicitly in parallel modes. Taking either side whole would have lost something:
upstream's drops the qualifier and leaves parallel carriages with no pressure
advance, ours drops the per-variant indexing and reads the wrong column on a
multi-variant printer. The helper now takes an optional tool, defaulting to -1,
which omits the qualifier. imex_pem_tool_for() already returns -1 off IMEX and in
primary mode, so non-IMEX output is unchanged, and the three call sites that never
passed a tool keep upstream's behavior exactly.

The third conflict was two test cases appended at the same place. Both are kept.

Separately, one defect that merged cleanly and so was not flagged: the loop that
emits pressure advance for secondary carriages at the start of a print still
bounded and indexed those vectors with a raw filament id. They are variant
expanded now, so their length is columns rather than filament slots -- the value
read was the wrong column, and the bound no longer sat in slot space, letting an
out-of-slot filament through. It now bounds on filament_diameter and translates
with get_filament_config_index(), which is what the sibling second-layer
temperature loop already does.

Verified: both changed translation units compile clean under -Werror. The merge
was resolved independently twice and the two resolutions agree on every line of
code. Not yet run: the Release test suite and a parallel-mode slice sweep.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-29 21:43:47 -04:00
Clifford GarwoodandClaude Opus 5 bfac55335a Give each Xplorer toolhead its own color
Every extruder on every model carried the same yellow. The key exists so the
plater, the preview and the filament mapping can tell toolheads apart, which
matters most on the machine this bundle exists to demonstrate: four independent
heads that were indistinguishable at a glance.

Yellow, blue, plum and orange, assigned in tool order, so the two-tool models
take the first two. Red and green are avoided as a pair because they are the
hardest to separate for the commonest color vision deficiency.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-29 20:35:42 -04:00
Rob Niccum c8e4e74024 Vivedino Xplorer: finer 0.4 layers, IQEX toolchange prime, lighter assets
- IQEX (0.4/0.6/0.8): retract_restart_extra_toolchange 1 -> 0 on all four
  tools. With the silicone ooze blockers the parked nozzle stays primed,
  so an extra 1 mm after each toolchange can over-extrude at the restart.
- Add 0.04mm Ultra Fine and 0.08mm Extra Fine @Xplorer 0.4 on the existing
  fdm_process_xplorer_common ladder for all four 0.4 printers. Same speeds,
  accelerations and line widths as the 0.12mm Fine tier; only the layer
  height, shell layers, bottom_shell_thickness 0.6 and (0.04) top solid
  infill flow differ. Support stays off, as on the other tiers.
- Xplorer_buildplate_model.stl: decimated from 82,508 to 10,000 triangles
  (4.1 MB -> 500 KB). Same bounding box and origin, so bed_model is unchanged.
- Covers: one distinct 240x240 image per model instead of the same image
  copied four times (39 KB each -> about 11 KB each).
2026-09-29 15:18:34 -04:00
Clifford Garwood 3334c2ed2a Merge the Vivedino Xplorer printer profiles
Adds the four Xplorer configurations, their twelve machines and fifteen
processes, so the parallel printing modes can be exercised on a real printer
rather than only on hand-built configurations.
2026-09-29 05:03:26 -04:00
Clifford Garwood b6b2f0d3b8 Merge upstream into the parallel printing work
Brings in nine commits, including stricter slice validation of custom G-code and
filename formats across system profiles, OBJ and DRC import hardening, and the
Ender-3 V3 SE extrusion mode fix. None of them touch the files this branch
changes.
2026-09-29 05:03:21 -04:00
9a7e2681d4 Add Vivedino Xplorer printer profiles
The Xplorer ships in four configurations that differ in how many toolheads they
carry and how those heads are arranged: Single, IDEX with two heads on one
gantry, Dual Gantry with one head on each of two, and IQEX with two on each of
two. Each is offered at 0.4, 0.6 and 0.8 mm, giving four machine models, twelve
machines over a shared base, and a five-tier process ladder per nozzle.

The dual-gantry machines park the second gantry's tools over the plate, so the
area both gantries can reach is 57.5 mm shallower than the plate itself. Those
two declare 400 x 342.5 where Single and IDEX declare 400 x 400. That depth is
what the parallel print modes divide into equal zones, so it has to be the
reachable area rather than the physical one.

The bed textures are drawn to match. Texture coordinates are normalised per
axis, so a canvas whose aspect differs from printable_area is stretched and
anything drawn outside the plate is pulled onto it; each viewBox equals its own
model's area. They mark the real reach limits and nothing else, because the
print zones are computed and drawn per plate at run time and a static copy of
them only disagrees with the live one.

Motion limits are the firmware's: 5000 acceleration, 300 mm/s, 100 on Z, 120 on
the extruder, and a square corner velocity of 5 rather than a jerk, which Klipper
does not have. Every per-extruder and per-variant value is written at full width,
because padding a short array repeats its first value rather than its last pair
and would otherwise hand every extruder past the first a normal-mode figure in
its silent slot.

The processes are based on a profile tuned on the hardware. Line widths are
expressed as percentages of the nozzle so one statement serves all three sizes,
and the ladder varies only what belongs to layer height. Extruder variants are
declared rather than left to a default that would have capped volumetric flow at
a figure describing a plain V6.

Verified by slicing all fifteen tiers on all four models: every one completes,
emitted accelerations and widths match what the profiles resolve to, no
coordinate leaves the printable area, and the two pre-existing Troodon models in
the same bundle are unaffected. The bed graphics, the print zones and the
volumetric cap resolve through paths the command line does not exercise and want
confirming in the application. Values tuned against hardware the author does not
have - the 0.8 ladder in particular - are a starting point rather than a result.

Co-Authored-By: Dan_3dp <corexy.diy@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-29 04:58:22 -04:00
Clifford GarwoodandClaude Opus 5 bd8b7ced6a Colour the carriage markers by filament where the preview does
Multi-material previews now open in Filament view, where toolpaths are coloured
by the filament printing them, and a marker carrying a fixed palette colour can
sit on toolpaths of that same colour. The plan already resolves which physical
head each carriage is, so in that view every carriage - the primary marker
included - takes its head's filament colour, from the accessor the ghosts
restamp themselves with, and marker, ghost and toolpath agree on what is loaded
where. The primary marker is the one every preview draws, so it goes back to its
own colour when no parallel mode is active.

Every other view keeps the Okabe-Ito palette, which is what identifies the
carriages when colour means something else, and so does a head whose filament
does not resolve. Each head is resolved once per frame and shared by its marker
and its toolhead box, the way the ghosts already hoist the map they all read.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-28 17:31:27 -04:00
Clifford GarwoodandClaude Opus 5 4e65b741cc Draw the IDEX/IQEX ghosts in the X-Ray pass that replaced their own
X-Ray takes both shaded passes over and returns, and it draws only the volume
collection, which the ghosts are not part of. They disappeared while their
picking pass, hover tooltip and filament picker all kept answering, so a plate
in a parallel mode offered an invisible click target.

They go through the X-Ray shader rather than their own, so a ghost reads as one
more see-through body, which is what the mode is for. The pass supplies z_range
and clipping_plane itself, since the volume collection sets them for the whole
pass it runs and the vertex shader discards everything outside z_range. It also
draws two-sided, so a hollow ghost shows its far wall like a real body does, and
hands the shader an opaque colour: X-Ray derives coverage from the view angle
and multiplies the colour's alpha into it, so a ghost carrying its own
translucency as well would composite far fainter than the body it mirrors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-28 17:31:27 -04:00
Clifford Garwood 299093844f Merge upstream main: multi-variant printers, X-Ray view, preview and startup work
The two conflicts are both places where upstream landed on top of IDEX/IQEX
code. In GCode.cpp the relocated file header block meets the IMEX placeholder
block, and the placeholders are set first: file_start_gcode is processed through
the placeholder parser now, which throws on a name it does not know, so a script
naming {imex_mode} would abort the export if the header ran first. In
test_multifilament.cpp both sides appended a case at the end of the file.
2026-09-28 17:12:53 -04:00
Rodrigo Faselli 468b0bb94d Merge branch 'main' into main 2026-09-24 09:15:04 -03:00
Clifford GarwoodandClaude Opus 5 af1cf71a4c Read the IDEX/IQEX enum settings back as the values they were saved as
A coEnum config value has two representations: the typed ConfigOptionEnum<T> a
config cloned from the static classes carries, and the ConfigOptionEnumGeneric
that a config assembled from the option definitions creates - which is what a
preset, a project's own settings and the CLI all hold. imex_cfg_enum() accepted
only the first, so every IDEX/IQEX reader took the option default instead: a
printer saved as rear-left came back front-left in the settings, the bed zones
and the carriage markers, while the preset on disk still held rear-left.

Read the generic form too, keyed on the value map it carries, since only T's own
map yields a T. The last reader that matched on the coEnum tag alone and cast
across the two hierarchies now goes through the helper with everything else.

Declaring the three keys the static classes were missing is what lets a change
to imex_tool_layout invalidate the slice it moves, which it never did before.
The two that are only ever drawn stay out of that: a colour scheme and the
advisory margin bands do not reach a slice, so changing one must not discard it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 18:09:40 -04:00
Clifford GarwoodandClaude Opus 5 42a47d6bd7 Keep the plate mode name visible beside its conflict warning
set_hover_tooltip records one string per frame, so the multi-material warning
replaced the mode tooltip instead of joining it, and hovering the icon on a
conflicted plate no longer said which mode was active or that clicking cycles
it. The two are composed into one string, paragraph separated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 17:10:23 -04:00
Clifford GarwoodandClaude Opus 5 1f1d13e3d9 Describe the estimate's actual blind spots in the tower comments
The estimate no longer reports a tower for a single filament whose flush matrix
purges, so the comments that justify calling prime_tower_is_printed() instead of
reading a depth now cite what still holds: it reads neither enable_prime_tower
nor print_sequence. GCodeViewer's comments name render_scene(), the function
that replaced the render() they still pointed at, and the pass contract mentions
the toolhead boxes it draws.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 17:10:23 -04:00
Clifford GarwoodandClaude Opus 5 2d80c6a24e Drop the single-filament tower case the estimate no longer reports
The estimate now returns no tower for a lone filament whatever the flush matrix
says, so the discrepancy this case pinned between it and normalize_fdm_2 is gone
and the sibling case covers what remains.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 09:56:44 -04:00
Clifford c078765f95 Merge pull request #3: port the IDEX/IQEX rendering onto the render_scene/render_overlay split
Fix the GCodeViewer.cpp conflict resolution from the upstream merge
2026-09-21 09:56:36 -04:00
raistlin7447 068dc4346f fix(gcode-viewer): port the IDEX/IQEX rendering onto the render_scene/render_overlay split
The merge of upstream main (dc021c6ef6) resolved the GCodeViewer.cpp conflict
from #15674 by keeping both sides. That left a stray `}` in
SequentialView::render_overlay, so the file no longer compiles. It also kept
the IMEX carriage update and the toolhead-box GL draw in the ImGui overlay
pass, which #15674 no longer uses for 3D geometry.

The carriage update and the toolhead boxes now run in render_scene next to the
primary marker, and render_marker draws the secondary markers. render_overlay
goes back to what upstream has, without the duplicated marker-position block
and the unused bottom_margin the merge left behind.

The same upstream change renamed PartPlate::show_tooltip to set_hover_tooltip;
the two IMEX call sites follow it.
2026-09-21 08:22:07 -05:00
Rodrigo Faselli dc021c6ef6 Merge branch 'main' into main 2026-09-21 09:47:39 -03:00
Clifford GarwoodandClaude Opus 5 1c42bb0cd6 Merge upstream main: Design tab CAD, OTA OPC CI, JAYO filament profiles
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 08:00:12 -04:00
Clifford GarwoodandClaude Opus 5 84c13d4913 Merge upstream main: wipe tower port, per-filament bounds checks, CLI mixed rules
Four files overlapped, and each resolution favours upstream where the two sides
had done the same work:

WipeTower's M104/M109 tool qualifier. Both sides bounds-checked the physical
extruder map lookup; upstream omits the T qualifier when the map cannot answer,
where this branch fell back to the logical index and so named a carriage that may
not be the one printing. Upstream's behaviour is what this branch documents
elsewhere, so its version is taken and the local helper is dropped.

get_extruders' mixed-slot switch. Upstream added the same concept to the CLI
overload as expand_mixed_slots, so the GUI overloads' parameter is renamed to
match rather than carrying two names for one idea.

GLCanvas3D's sequential-clearance branch gains upstream's
update_compacted_wipe_tower_clearance for the by-layer case.

The printer_agent re-sync in TabPrinter::reload_config was upstream's and their
preset-undo fix removed it, so it goes; the IMEX modes grid re-sync beside it
stays, since it spans three options and is not a Field.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 01:50:53 -04:00
Clifford GarwoodandClaude Opus 5 b6a2c76e5d Share the prime-tower rule with the slicer instead of copying it
imex_wipe_tower_hull() open-coded "does this plate print a tower?" as one
filament and no forcing reason. normalize_fdm_2, which is what actually
clears enable_prime_tower before slicing, has a second arm the copy omitted,
and reads the mixed-filament flag project-wide where the copy read the
plate. Two cases went wrong in opposite directions.

A ByObject plate with several objects prints no tower and the scene draws
none, yet the copy validated one and could refuse the slice with "the prime
tower overlaps an area reserved for IDEX/IQEX parallel printing" - with
nothing on screen to move.

A plate using one plain slot while some other slot in the project is a blend
does print a tower, because normalize_fdm_2 keeps it for any mixed filament
in the project, and the copy skipped validation entirely, so the tower could
be placed in a carriage zone and sliced.

prime_tower_is_printed() in libslic3r states the rule once, and both the
gate and a test use it. The counts are the ones normalize_fdm_2 is handed:
filament slots as authored, so a mixed slot counts once, and distinct
objects rather than instances. filament_is_mixed is a project option, so it
is passed in rather than read from the print preset.

The test drives every combination the rule looks at and compares the verdict
against normalize_fdm_2 itself, so the two cannot drift again without
failing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 01:41:42 -04:00
Clifford GarwoodandClaude Opus 5 0e44269c5e Correct the prime-tower validation comments after the upstream rename
The block describing what imex_wipe_tower_hull() does and does not cover had
drifted. It named estimate_wipe_tower_size, which upstream replaced with
estimate_wipe_tower_footprint; it described the plate filament count as an
override where the rewritten estimate treats it as a floor; and it
documented a Type2 stabilization cone as unhandled when the estimate now
folds the cone's worst-axis bulge into the margin the hull is built from, so
a second allowance would double-count. The comment on the m_print arm of the
guard now says what that arm actually does, which is nothing, since the
estimate stopped reading m_print.

It also records why the gate takes "is a tower printed" from
normalize_fdm_2's rule rather than from the estimate: the estimate reports a
tower for a single filament whenever the flush matrix purges, which would
hard-block a plate whose tower normalize_fdm_2 had already cleared, with
nothing drawn on screen to move.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 01:41:42 -04:00
Clifford GarwoodandClaude Opus 5 182dbd55e4 Pin the flush-matrix purge for a single filament
With single_extruder_multi_material and purge_in_prime_tower, the flush
matrix replaces the prime volume and its average is non-zero for one
filament, so the estimate reports a tower. normalize_fdm_2 clears the tower
for that same plate, so none is printed: the estimate answers how big a
tower is, never whether there is one, and a caller reading a non-zero depth
as "a tower is printed" reserves space for, or blocks on, a phantom. The
test asserts that disagreement directly.

Type1 is the exception in the same case, because it decides from its
per-filament purge list and a lone filament is never changed to. The scene
reads the same estimate either way. Type2 is the default for every non-Bambu
printer, and so for every IDEX/IQEX one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 01:41:42 -04:00
Clifford GarwoodandClaude Opus 5 3c31856b85 Bound the second-layer temperature lookup in filament-slot space
nozzle_temperature is variant-expanded, so its length is columns rather than
filament slots. On the dynamic-nozzle path that makes it longer than the
slot count, and an out-of-slot index reached get_filament_config_index() and
came back as filament 0 - the clamp the bounds-checking was meant to remove.
Bound by the slot count instead; on the ordinary path the two are equal and
nothing changes. The is_extruder_used write gains the matching lower-bound
guard.

IMEXHelpers.hpp now states both halves of the rule its call sites follow.
Bound anything derived from the extruder map against the filament slot count
before using it as a filament id, not against the option about to be read.
And a miss is -1, which is a correct tool qualifier but matches no physical
head, so it cannot serve as a skip-the-primary sentinel: which head prints a
filament is answered by the filament and the map, never by a mode role,
since a primary-mode print may use any or all tools, one at a time.

The consequence is recorded there rather than left implicit. The two skip
sites skip nothing for a slot past the end of the map, so a plate with more
slots than nozzles double-writes the primary's pressure advance. It is
narrow and unreported, and a guard there would be a smaller change than
naming a head.

The header also records that RepRapFirmware sends an unqualified pressure
advance as M572 D0, naming drive 0 absolutely rather than the active tool.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 01:32:27 -04:00
Clifford GarwoodandClaude Opus 5 c62385214f Drop two dead lambda captures that only a dev build compiles
CameraPopup and StatusPanel each bind a toggle handler that captures this
and never uses it, inside an #if !BBL_RELEASE_TO_PUBLIC block. CMakeLists
defines that as $<CONFIG:Release>, so the block compiles in every
configuration except Release - and Release is the only one CI builds. Under
-Werror the two captures therefore fail RelWithDebInfo, which is what a
development build uses, while CI never sees them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 01:32:27 -04:00
Clifford GarwoodandClaude Opus 5 b6f690a65c Migrate the IDEX/IQEX config keys testers already have
The parallel printing options shipped to testers as is_ixex and ixex_*,
became is_imex and imex_* when the feature stopped being called iXex, and
the two clearance keys were renamed once more to say what they measure:
nozzle to carriage edge on the collision side, not the carriage's full
width. Nothing translated any of it, so loading an existing printer profile
dropped every one of these values - the keys are unknown and cleared.
is_ixex is the one that matters most, because without it the others migrate
into a feature that stays switched off, leaving settings that look
configured and do nothing.

Per-plate mode is persisted twice and only one path went through
handle_legacy. Plate metadata in a 3MF is matched by exact attribute name
and written with set_key_value, so a project saved between the per-plate
mode landing and the rename loaded every plate back on Primary and sliced
single-carriage with no warning. The loader now accepts the old attribute
name.

ixex_primary_col and ixex_primary_row are dropped rather than mapped: the
primary is a role inside the mode's active-tools string now, not a grid
coordinate, and they were never in an option list, so no saved file carries
them.

The test drives the full era-1 key list and asserts the enum values rather
than non-nullness, since a forward-compatible substitution would otherwise
hide a failed deserialize behind a default. handle_legacy had no test before
this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 01:32:27 -04:00
Clifford GarwoodandClaude Opus 5 f7a08b0868 Spell the mode loop initializers as std::string
Two range-for loops bound const std::string& to braced lists of string
literals, so each iteration constructed a temporary to bind to. GCC 16
reports it as -Wrange-loop-construct, which upstream's blanket -Werror turns
into a build failure; clang does not report it at all. Spell the
initializers the way the loop above them already does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 01:32:26 -04:00
Clifford GarwoodandClaude Opus 5 98544fd6bc Bound the parallel pressure-advance lookup to the filament count
Review findings on the preceding commit, plus one defect it should have
caught.

- The IDEX/IQEX pressure-advance loop fed resolve_filament_for_head()'s
  result straight into enable_pressure_advance and pressure_advance. That
  result is bounded by physical_extruder_map, which holds one entry per
  NOZZLE, while both options are indexed per filament SLOT. On a printer
  with more nozzles than the project has filaments the two spaces diverge
  and get_at() clamped the overflow onto filament 0, emitting its pressure
  advance on a secondary carriage. The second-layer temperature loop bounds the
  same lookup, but against nozzle_temperature, which is variant-expanded and so
  is not the slot count either -- it is not the precedent it looks like.
  IMEXHelpers.hpp states the rule
  once, and a test pins the contract that makes the bound necessary:
  resolve_filament_for_head() answers in nozzle space, so a non-negative
  result is not by itself safe to use as a filament id.

- The header claimed every caller renders a -1 tool qualifier as "emit
  none". RepRapFirmware substitutes the historical D0 instead, deliberately
  and with its own comment in GCodeWriter. Say so, rather than leaving a
  contract a future author would code against.

- A cross-reference pointed at a hard-coded line number that the preceding
  commit had itself shifted by nine lines. Name the function instead.

- The multi-color rejection reasons reach the user through Print::validate()
  as raw English, while the returns on either side of them use L(). Wrap
  them and register IMEXHelpers.cpp for extraction. They also still said
  "IMEX", the internal name, so they move to IDEX/IQEX with the rest of the
  user-facing strings rather than shipping the internal one to translators.

- Trim the preceding commit's comments. One block explained the same
  clamping hazard six times; the canonical explanation now lives in
  IMEXHelpers.hpp and the call sites point at it. The mode grid carried
  twelve lines of commentary and no code, most of it archaeology already in
  the commit message, and one claim about the modes editor that was not
  true. The ArrangeJob threading note stays: it documents an invariant that
  cannot be recovered from the code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 01:32:26 -04:00
Clifford GarwoodandClaude Opus 5 e309029b28 Fix extruder-map bounds, popover lifetime, and arrange thread safety
Review findings on the IDEX/IQEX parallel printing code, all in paths the
feature owns.

- physical_extruder_map lookups used ConfigOptionVector::get_at(), which
  clamps an out-of-range index to values.front() rather than reporting a
  miss. The map holds one entry per nozzle while filament ids index slots,
  and nothing caps the slot count at the nozzle count, so a project authored
  with more filaments than the printer has extruders silently addressed the
  primary's head: pressure advance pinned to the wrong carriage, and
  skip-primary loops suppressing whichever head sat at pem[0]. Bounds-check
  at all four sites and treat the miss as "no mapping" (-1). Covered by a new
  imex_pem_tool_for test; the header note now warns against get_at here.

- IMEXFilamentPickerPopover leaked a top-level window per ghost click:
  wxPopupTransientWindow::Dismiss() only hides, and never reaches OnDismiss().
  Destroy from an OnDismiss() override and dismiss the picker through
  DismissAndNotify(), which is the path a successful pick takes.

- ArrangeJob read PartPlate's IMEX zone cache from the worker thread, where
  a cache miss rebuilds GLModel members with no GL context current while the
  GUI thread may be painting them. Snapshot the zones in prepare(), on the
  main thread, already converted to plate-local coordinates.

- The mode grid anchored its row window to the Primary's gantry row. A window
  as tall as the grid can only start at row 0, so this drew tiles for tools
  that do not exist and hid real ones. Render the whole grid instead; a
  Primary outside it is a data problem the zone layout already reports.

- Build the mode tooltip from one format string rather than two catalog
  fragments concatenated around a runtime value, so translators can move the
  mode name within the sentence, and register IMEXModesCtrl.cpp for string
  extraction.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-18 01:32:26 -04:00
Hanif Koh 2b2c710626 Order GCodeWriter Initializers Like the Members Moved to Protected
The lift, speed and cached-extruder members now live in the protected section ahead of the private ones; list their initializers first so the list reads in construction order. No behaviour change.
2026-09-14 17:27:33 +08:00
Hanif Koh 2b1a7e38df Drop Redundant Belt Checks in BeltGCode
BeltGCode is only created for belt printers, so its hooks no longer re-check belt_printer, and the BBL-machine flag is set once on whichever writer survives init_belt_writer instead of on one about to be discarded.
2026-09-14 17:27:33 +08:00
Hanif Koh b4052ec99f Drop the Redundant Lift Type Alias in eager_lift
effective_type was a plain copy of the parameter.
2026-09-14 17:27:33 +08:00
Hanif Koh 14cf7861e0 Fix Belt Tooltip Spacing and Legend Casing
Drop double spaces in the belt tilt tooltips and match the preview legend header to
the "Belt printer" settings group.
2026-09-14 17:27:33 +08:00
Hanif Koh 18c08862a8 Align up_direction Position in TriangleSelector Fill Calls
seed_fill_select_triangles() now takes up_direction right after highlight_by_angle_deg, as select_patch() does.
2026-09-14 17:27:33 +08:00
Hanif Koh 62ef0fa4f8 Write the Standard Layer Change Tag on Belt Brim Apron Layers
Apron layers appended print_z to the layer change tag, unlike every other layer; write the plain tag line.
2026-09-14 17:27:33 +08:00
Hanif Koh a8a45439db Share One Build Plate Tilt Up-Direction Helper Across the GUI
The bed gravity arrow, volume rendering and the painter/support gizmos each rebuilt
the tilt up-vector from build_plate_tilt_x/y; use one helper that also tolerates
presets without the keys.
2026-09-14 17:27:33 +08:00
Hanif Koh 5cd6661280 Check the Belt Temperature Tower Model Load
Bail out like the other calibration paths when add_model() fails instead of
indexing the empty model.
2026-09-14 17:27:33 +08:00
Hanif Koh 33aec258da Skip CLI Wipe Tower Reservation on Belt Printers
Print::has_wipe_tower() is always false for belt printers, but CLI arrange, plate checks and the pre-slice tower clamp still reserved a phantom tower footprint and wrote a clamped wipe_tower_x/y into the config.
2026-09-14 17:27:33 +08:00
Hanif Koh 76aba24ddf Number Belt Extension Support Layers Sequentially
Extension layers were all created with id 0, so every one of them could be taken for the first layer by id-only checks (ooze-prevention standby temperature, cached layer ids). Renumber the support layers after inserting them.
2026-09-14 17:27:33 +08:00
Hanif Koh cb5b489e90 Invalidate Only G-code Export for Belt Output Options
gcode_back_transform, first_layer_plane* and belt_printer_infinite_y fell through to invalidate_all_steps(), which re-ran tool ordering, skirt/brim and G-code export on toggles that only affect G-code export.
2026-09-14 17:27:33 +08:00
Hanif Koh 19a085206c Print Belt Brim Aprons in Each Object's Brim Filament
Apron-only layers printed every band with the first tool, so objects with different brim filaments at the same apron Z shared one filament. Emit each brim filament's bands with its own toolchange.
2026-09-14 17:27:33 +08:00
Hanif Koh 708212a306 Share One Belt Brim Band Loop Between Apron-Only and Ordinary Layers
The ordinary-layer path kept its own copy of the apron band loop. Give emit_belt_brim_bands() an optional brim filament filter and call it from the per-extruder lambda; without a filter it still prints every band, so apron-only layers are unchanged.
2026-09-14 17:27:33 +08:00
Hanif Koh 3b302b4666 Derive Belt Support Tilt From Slicing Rotation in Print::apply
build_plate_tilt_x/y was synced from belt_slice_rotation* only by the printer Tab, so CLI or 3MF edits of the rotation left the support tilt stale.
2026-09-14 17:27:32 +08:00
Hanif Koh 4a7311bf01 Rebuild the Brim Type Combobox Only When Its Entries Change
toggle_options() now runs on every value change and mode switch; rebuild the
brim_type choices only when the leading-edge entry has to be added or removed.
2026-09-14 17:27:32 +08:00
Hanif Koh f2a11928f6 Read the Belt Tilt Only from the Belt G-code Header
Every printer's config block lists belt_slice_rotation_angle (default 45), so the processor marked all G-code as belt G-code: imported flat G-code got the belt view on a belt printer, and the belt-only Z handling in the processor ran for non-belt prints whose config block precedes the body. Take the angle only from outside the config block, where only the belt header writes it.
2026-09-14 17:27:32 +08:00
Hanif Koh 4ed56954e8 Limit Build Plate Tilt Range Below 90 Degrees
A 90 degree tilt has no finite gravity drift per layer, so the option range
now stops at 89 degrees, matching the cap applied by the support generators.
2026-09-14 17:27:32 +08:00
Hanif Koh 6e33f3f5dd Share and Clamp the Build Plate Tilt Shift in Support Generators
The three support generators each computed lh * tan(tilt), which overflows
coord_t at 90 degrees and flips sign beyond it (belt sync can write up to
180). One helper now returns the tilt slope with the tilt capped at 89 degrees.
2026-09-14 17:27:32 +08:00
Hanif Koh 5092831007 Apply the Belt Slicing Transform to Painted Supports and Seams
Painted support/seam facets, support volumes, seam occlusion, MMU and fuzzy skin painting (top/bottom
and side facets) and the adaptive infill octree used trafo_centered(), or trafo() with a centre-offset
shift, while the layers were sliced with the belt rotation, remap and Z lift; they now share
PrintObject::trafo_sliced().
2026-09-14 17:27:32 +08:00
Hanif Koh 9cca3093ee Keep the Plate Offset When Swapping in the Belt Writer
The belt writer replaced the plate-offset-carrying writer mid-export, so belt G-code for any plate but the first kept the plate origin and long-travel clipping used the wrong frame. GCode now remembers the offset and hands it to the new writer, and the writer's first-layer probes use the plate-local point it emits.
2026-09-14 16:41:40 +08:00
Hanif Koh 6873267a9c Remove Plate Tilt Keys from Per-Object Settings Tables
build_plate_tilt_x/y are printer-preset keys; listing them in the per-object
frequent-settings and object-table bundles stored ignored values in object
configs and crashed the object table on the process config lookup.
2026-09-14 16:41:40 +08:00
Hanif Koh 8e330f951a Merge Main into Belt Printer
Merge origin/main (00429da739) into belt-printer.

Conflicts resolved:
- src/CMakeLists.txt: keep both wxInspector workarounds.
- GCodeProcessor.cpp: keep the belt compare_pos / z_for_height lines.
- PrintObjectSlice.cpp: the belt bbox-Z guard also covers main's
  printable_region_ids bookkeeping.
- TreeSupport.cpp: the belt-floor check runs before main's PendingNode
  queueing.
- Tab.hpp: keep the belt fields, drop the removed upload description
  fields.
- tests/libslic3r/CMakeLists.txt: keep both test files.

Also included:
- eSUN PLA belt presets declare their own filament_id (OFkrxQC4) and
  scripts/filament_id_snapshot.json is regenerated, as main's filament_id
  check requires.
- Custom.json version bumped to 02.04.00.05 so the belt entries reach
  existing installs.
- Fix the ambiguous WithinRel call in the belt apron width test, which
  otherwise breaks the fff_print build.
2026-09-14 16:33:08 +08:00
Clifford Garwood b17414b160 Merge remote-tracking branch 'upstream/main' into integration/upstream-main-sync 2026-09-13 02:21:43 -04:00
harrierpigeonandClaude Fable 5.1 9c83631d20 TreeSupport: drop the <cstdio> include left over from removed debug output
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SsuY8Laiyh7q2zPVVKV3HZ
2026-09-12 22:23:14 -05:00
harrierpigeonandClaude Fable 5.1 a430430690 Belt: size the purge prism against physical filaments, not mixed slots
A mixed filament slot is virtual: ToolOrdering::resolve_mixed_filaments()
replaces it with its physical components before any G-code is emitted, so
the toolchanges the prism has to absorb are between those components.
ensure_belt_purge_tower() counted the slot as a filament of its own,
provisioning one island per mixed slot that no swap can ever reach -- the
"extra purge tower" on MCTEST5, where filament 5 is a 50/50 blend of 2
and 4 and the G-code reports 0.00 g of it used.

Expand the assigned set with the same expand_mixed_filaments() the
backend uses, so the GUI sizes the prism against the filament set the
slicer actually produces. No-op when nothing is mixed. Test covers the
MCTEST5 shape, a mixed slot whose components are otherwise unused, and
the no-mixing case.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SsuY8Laiyh7q2zPVVKV3HZ
2026-09-12 22:23:14 -05:00
harrierpigeonandClaude Fable 5.1 1bcfe58064 Belt: stop the purge prism printing plastic no toolchange needs
Two independent leaks of filament on the belt purge prism, plus the
replan safety net the second one needs.

1. The early-truncation scan bounded itself with the prism's own
   toolchanges. ToolOrdering covers the whole print and the prism is a
   printed object in it, so the "last toolchange" the scan found was on
   the prism's own top layers -- it runs past every model object by
   design -- and the truncation cancelled nothing. Bound the scan at the
   tallest non-prism object (support layers included; on a belt they can
   top the object). On MCTEST5 that was 197 toolchanges over 39.4 mm of
   tower that no swap ever needed.

2. On a layer with no toolchange, the prism's entire fill printed as
   solid infill in its own filament. Drop the fills no toolchange
   claimed, right after the purge marking and before
   ensure_perimeters_infills_order() force-overrides whatever is left.
   Perimeters stay so the bar keeps a continuous wall. An earlier version
   of this deleted the entities and had to be reverted: psWipeTower can
   rerun without regenerating infill, and a later tool ordering may claim
   what this one did not. The entities are now stashed with their layer,
   region and index and put back exactly, the same reversibility contract
   layer truncation already had.

3. Both stashes go stale if an object step reruns: make_fills() clears
   and regenerates fills over m_layers only, so a stale stash would put
   old fills back next to new ones, and truncated layers would keep old
   perimeters/fills. Undo the plan's edits at the top of Print::process()
   whenever psWipeTower is not done. Every object-step invalidation also
   invalidates psWipeTower, so that condition is exactly "some object
   step may rerun"; when it is done nothing regenerates and the edits
   must stay. This also covers a prism left behind after belt mode is
   turned off, which previously stayed truncated forever.

WipingExtrusions::is_entity_overridden() becomes public so the prism can
tell claimed fills from unclaimed ones.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SsuY8Laiyh7q2zPVVKV3HZ
2026-09-12 22:23:14 -05:00
harrierpigeonandClaude Opus 5 1b6fb2a81f Belt: fix first-layer speed and the slow_down_layers ramp never applying
Fixes the report in #12998 (comment 5465250754): first-layer speed and the
slow_down_layers ramp were ignored on a belt printer. The report reads as a
per-object problem, but neither applied to *any* object -- the reporter's first
part slowed down because slow_down_for_layer_cooling was on, which is
CoolingBuffer's time-per-layer mechanism, not initial_layer_speed.

FirstLayerPlane decides first-layer-ness by perpendicular distance to a plane it
derives by composing gcode_remap_* with compute_machine_z_affine(). The plane is
therefore a function of how G-code is *addressed*, not of where the belt is:
change the output axis convention and the plane moves. On MCBELT-TYPE2 the
first layer measured 86.2 mm from the plane and got effective index 431, far
past any slow_down_layers ramp.

on_first_layer(point) and effective_layer_index_for_point() now measure height
above the belt surface, using the belt description already carried in
SlicingParameters -- belt_floor_shear_factor / belt_floor_from_axis /
belt_floor_z_shift -- the same description the support generator uses. That is a
property of how the object was sliced, so no remap or back-transform can perturb
it.

Deliberately not via BeltFloorContext: its init() folds in
belt_support_floor_offset, a support-generator diagnostic, and letting that
option steer the model's first-layer speed band would be a surprising coupling
(a negative value would switch the slowdown off outright).

Preserving the existing first-layer-plane settings:

  * first_layer_plane XY/YZ/XZ keeps the FirstLayerPlane evaluator, as those are
    explicit opt-outs.
  * A non-zero first_layer_plane_offset also keeps it. The offset is a machine-Z
    shift that FirstLayerPlane converts into a perpendicular distance in the
    slicing frame; this evaluator measures along slicing Z, so there is no
    faithful translation. Deferring to the evaluator that implements the setting
    beats silently ignoring it.
  * The two thresholds stay separate, exactly as FirstLayerPlane keeps them:
    the first-layer boolean tests initial_layer_print_height, while the
    effective layer index counts bands of first_layer_plane_thickness.

Brim and coincident apron bands are emitted before m_layer is switched to their
object -- for an apron band there is no Layer at all -- so both paths publish the
belt-floor owner explicitly. Without that a brim's classification would borrow
whichever object was visited previously, making it depend on plate order.

Note that first-layer-ness drives more than speed: extrusion acceleration, jerk,
the first-layer flow ratio and eligibility for overhang speed/fan analysis all
read it, so all of them are corrected on belt printers by this change.
Classification still samples only each path's first point, as it did before.

Non-belt is unaffected by construction: belt_height_above_floor() returns false
when the belt floor is inactive and both call sites fall back to the previous
path. FirstLayerPlane stays in place for its other modes and for CoolingBuffer,
whose machine-coordinate probe is a separate outstanding bug.

Measured, MCTEST4 on MCBELT-TYPE2 (initial_layer_speed=5, slow_down_layers=40):
15 distinct feedrates with no gradient and F300 absent, becomes 70 including the
full ramp 300(5) 382(6) 465(8) 630(10) 795(13) ... Two bare cubes on a belt:
0 slow extrusions becomes 2378 across Z 32.36..95.18. The same two cubes on a
Cartesian printer keep their slow extrusions confined to Z 0.20..2.00.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011jgzj1sf53KMLPweZ8yeUQ
2026-09-08 23:59:02 -05:00
harrierpigeonandClaude Opus 5 767db71500 Belt: fix three tree-support bugs, one of which blocked slicing entirely
1. Belt tree support could not slice at all.

layer_initialize() hardcodes layer 0's bottom_z to 0, encoding "below layer 0 is
the build plate at z = 0". True for a flat bed; false for a belt, whose virtual
support layers legitimately extend below zero. The bottom-most belt layer
therefore got height = print_z - 0 = -9.8, which reached Flow::with_height() and
threw FlowErrorNegativeFlow.

A 3DBenchy, a mushroom, an L-bracket and an extruded L all failed identically
with negative flow / return -100. Only a bare cube sliced, because its support
never reached that far down.

The bottom is now taken from the previous layer's z, and only a layer 0 whose
print_z is itself negative gets a synthesised bottom below it. Every
non-negative print_z -- every non-belt configuration -- keeps exactly the
previous 0, so this is behaviour-preserving off a belt by construction. An
earlier form used min(0., layer_z(0) - layer_height), which regressed flat beds
whenever the initial layer was thinner than the layer height.

  3DBenchy on a 45-degree belt with organic tree support: fails to slice ->
  247 support blocks / 168,596 extrusions.

2. Support generated against the belt, and against belt-tilted walls.

A plain 20mm cube on a 45-degree belt generated 86 support blocks and 46,307
support extrusions. Three causes, all gated on the belt floor being active:

  a. The build-plate tilt compensation shifted the lower layer the wrong way.
     tan(build_plate_tilt_*) carries a magnitude but no direction, and the sign
     chosen moved the lower layer away from the newly appearing material rather
     than under it, doubling the mismatch. The shift now comes from
     belt_floor_shear_factor / belt_floor_from_axis, which carry sign and axis
     exactly. Non-belt tilted beds keep the previous behaviour.
  b. Material resting on the belt was treated as unsupported. The belt surface
     is now unioned into the effective lower layer, sampled at the bottom of the
     layer -- a layer meets the belt across its thickness and print_z is the
     top. The half-plane is clipped to the layer's bounding box first: unioning
     a +/-1000mm half-plane raw with 20mm-scale geometry put a huge dynamic
     range through Clipper and left intermittent artefacts every few layers.
  c. The object's first slice can be empty on a belt (the bottom vertex is a
     sub-extrudable sliver), leaving the layer above with an empty predecessor
     even though it rests on the belt. (b) already covers that per island. What
     did need fixing is sharp-tail detection, which tests each island against
     the raw lower slices; with an empty predecessor that test is trivially true
     and every belt-contact island read as a sharp tail. It now tests against
     the same effective lower layer.

     An earlier form instead skipped the whole layer when the point of
     get_extents(curr_polys) -- the bounding box of the union of every island --
     nearest the belt was in contact. That was wrong in a way worth recording:
     one island resting on the belt could suppress overhang and sharp-tail
     detection for a separate island floating well above it. Every decision here
     is per-island.

  Cube on belt: 46,307 -> 0 support extrusions. Same cube non-belt: 0 before and
  after. Benchy on belt still 247 blocks / 168,596 extrusions and a mushroom
  111 / 82,157, so false positives are removed without suppressing true ones.

  Non-belt is unchanged by measurement, not only by the belt_ovh_active gate:
  the same mushroom sliced on a Cartesian printer before and after gives 65,866
  support extrusions and 68,717 total extrusions both times, the two G-code
  files differing in exactly one line -- the object's plate position.

3. m_anti_overhang was filled and read in different index spaces.

It is consumed in the same index space as m_layer_outlines, where object layer i
lives at num_raft_layers + i, but was filled in object-layer space. Every entry
landed num_raft_layers too low (50 for a 20mm cube at bed Y=50) and the topmost
object layers got none. The belt injection also ran before m_raft_layers was
extended, so it could not have known the offset.

The array is now shifted as a whole and the injection moved after the raft
extension. This also repairs user support blockers under a raft, which is not
belt-specific: it changes behaviour for any ordinary raft, not just the belt's
virtual one, and should be reviewed as a general fix. Measured effect on the
cube was small on its own (46,307 -> 46,334 before the other fixes) because
m_anti_overhang only feeds calculate_placable; kept as a correctness fix on its
own merits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011jgzj1sf53KMLPweZ8yeUQ
2026-09-08 23:58:47 -05:00
harrierpigeonandClaude Opus 5 4d2c3a0af4 GCodeWriter: fix two machine-mapping bugs the extraction preserved
Both change emitted G-code, which is why they were kept out of the extraction
commit. Both are wrong only where the machine mapping is non-identity, which is
the definition of each bug.

1. Suppress lifts commanded through an unknown position.

_travel_to_z() emits full XYZ whenever the mapping must emit every axis, because
the mapping can make machine Z depend on logical X/Y, and it builds that point
from m_pos. At print start, and after any custom G-code that invalidates
position, m_pos.xy is the uninitialised origin; mapping (0, 0, z) through a
non-identity remap produces a real but wrong machine point -- for a reverse
mapping, build_vol_max, i.e. the far corner of the bed. The subsequent full-XYZ
move corrects the position, but the lift has already commanded a rapid across
the whole bed at travel speed.

Belt kinematics already guarded this; the Cartesian path did not. The guard is
now applied at all three lift sites through must_skip_lift_now(), not just the
one the extraction covered: travel_to_xyz()'s pending-lift branch,
lazy_lift(spiral_vase=true), and eager_lift(). The latter two also needed the
state fix -- both recorded m_lifted = target_lift regardless, so suppressing
only the emission would leave a later unlift() descending from a height that was
never commanded.

2. Never emit a G2/G3 arc a mapping cannot represent.

extrude_arc_to_xy() emitted G2/G3 with logical X/Y and I/J and never consulted
the mapping. There is no general fix by transforming the arc: a permutation
moves it out of the XY plane that I/J describes, a negation reverses handedness,
and the belt shear maps a circle to an ellipse that G2/G3 cannot express at all.

So supports_arc_moves() gates generation through the existing
GCode::should_disable_arc_fitting() hook, and BeltGCode's special-case override
is deleted -- belt now gets the same behaviour from the general rule instead of
its own exception.

supports_arc_moves() is m_remap_x == 0 && m_remap_y == 1, not !has_axis_remap():
an arc emits only X/Y/I/J, so a mapping that merely negates or reverses Z leaves
every emitted word untouched and keeps its arcs.

The fallback for an unrepresentable arc tessellates it into linear segments at a
0.005mm chord tolerance rather than substituting a single chord, and splits dE
proportionally across the segments. The capability check is hoisted above every
extrusion mutation: an earlier form ran it after filament()->extrude(dE) and so
extruded 2*dE on the fallback path.

Known limits of that fallback, since it is worth stating rather than discovering:
emitted relative E is conserved only to per-segment rounding (a radius-5
semicircle with dE=1.5 emits 1.50012 across 36 segments); the 0.005mm bound is a
logical-frame bound, about 0.00855mm in machine space under a 45-degree belt
shear; unequal endpoint radii and non-finite inputs are unchecked. Ordinary
export takes the original polyline when the mapping rejects arcs, so this path
is a fallback rather than the normal route.

Known gap, not claimed fixed: classic wipe towers have their own
enable_arc_fitting and their own G2/G3 emitter in GCode/WipeTower.cpp, which
should_disable_arc_fitting() does not govern. Belt printers are barred from
classic wipe towers; a remapped Cartesian printer is not.

Tests in tests/fff_print/test_gcodewriter.cpp: reverse-X remap with unknown and
with known position plus an identity control; eager_lift emitting nothing and
recording nothing; the arc-capability matrix including the Z-only cases; and the
tessellated fallback. E accounting is asserted through used_filament() rather
than E(), which resets per line in relative-E mode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011jgzj1sf53KMLPweZ8yeUQ
2026-09-08 23:58:19 -05:00
harrierpigeonandClaude Opus 5 e695da66df GCodeWriter: extract MachineKinematics, delete BeltGCodeWriter
BeltGCodeWriter subclassed GCodeWriter and overrode seven methods, five of them
by copying the base body and changing the transform. The base writer already
carried an axis remap and already branched at each of its seven
coordinate-emission decisions; the subclass did the same branching with a
different transform, and the two copies had begun to drift.

Replace the inheritance with a strategy object owned by GCodeWriter:

  CartesianKinematics  to_machine = the existing apply_axis_remap; today's base
                       behaviour, moved rather than changed.
  BeltKinematics       to_machine = MachineFrameTransform o axis_remap o
                       BeltBackTransform, plus a world_coordinates variant for
                       the PA calibration generators.

New: src/libslic3r/GCode/MachineKinematics.{hpp,cpp}, GCode/BeltKinematics.{hpp,cpp}
Deleted: src/libslic3r/BeltGCodeWriter.{hpp,cpp} (341 lines)

Points worth a reviewer's attention:

  * The predicate is must_emit_all_axes(), not couples_axes(). The base returns
    true for any non-identity remap, including pure permutations that do not
    physically couple axes, so the question is "must every axis word be
    emitted", not a statement about kinematics.
  * Every per-site word-omission branch is preserved. The base deliberately
    emits X/Y only, or Z only, or drops Z when its quantised value is unchanged.
    The strategy changes which transform applies, never whether words are
    omitted.
  * set_kinematics() replays the configured remap and build volume onto a newly
    installed strategy, because BeltGCode::init_belt_writer runs before
    GCode.cpp calls set_axis_remap/set_build_volume_max.
  * uses_pointwise_travel_speed() preserves a pre-existing divergence rather
    than introducing one: the base travel_to_xyz emits the raw configured travel
    speed in its final branch, ignoring the first-layer value computed at the
    top, whereas the belt path used the first-layer-aware value throughout. Both
    are kept. Unifying them changes feedrates and belongs in its own change.
  * The [BELT-DEBUG] block is deleted; it rate-limited itself with a
    function-local static thread_local in the hot emission path, and this is the
    commit that would otherwise have moved it into shared code.

This commit is intended to preserve existing export output. That is reviewed by
construction -- each emission site keeps its own omission branch and each policy
divergence is preserved -- and is NOT verified against a G-code diff corpus.
Building that corpus is the outstanding work here.

Two API-equivalence exceptions, neither reachable by any caller today:

  * Belt kinematics with no plane pointer installed, m_is_first_layer true,
    initial and normal travel speeds differing, travel_to_xyz() reaching its
    final branch: the old belt writer selected the initial-layer speed, the new
    writer selects the normal travel speed. The pending-lift and XY-only
    branches keep their previous selection.
  * Belt kinematics installed without set_force_normal_lift(true) and a
    non-normal lift requested: the old belt writer forced a normal lift, the new
    writer can take the slope branch.

The PA-pattern generator reaches the writer through explicit travel_to_z() /
travel_to_xy(), not travel_to_xyz() or the lazy/eager lift paths, and normal
belt export installs both the plane and the forced-normal-lift policy, so
neither exception changes output produced today. They are recorded because a
future caller could reach them.

tests/fff_print/test_gcodewriter.cpp was also not compiling before this branch:
it called writer.to_machine_coords(), a method that existed only on
BeltGCodeWriter. It never surfaced because the build targets OrcaSlicer, not
all, and BUILD_TESTS defaults to OFF, so that translation unit was outside every
compile path. Fixed here; the existing 30-degree coordinate assertions are kept
verbatim as the best available regression net.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011jgzj1sf53KMLPweZ8yeUQ
2026-09-08 23:57:55 -05:00
Clifford GarwoodandClaude Opus 5 6c67fcfe07 Read IMEX geometry defaults from print_config_def instead of literals
Five sites carried a hardcoded fallback for imex_tools_per_gantry that had
to match the value registered in print_config_def, with nothing enforcing
the agreement, and several explanatory comments miscounted the sites they
described or cited stale line numbers.

Add imex_cfg_int/_float/_bool/_enum<T> to IMEXHelpers, which return the
value registered for the key when it is absent from the config, so the
registration is the single source and there is nothing left to keep in
sync. Route every read of the IMEX geometry keys through them: 32 call
sites across IMEXZones, PartPlate, GCodeViewer and Tab. The only direct
lookup left is the bail in PartPlate::imex_multicolor_block_reason, which
must not default because it reports a routing conflict and a defaulted
grid would produce a false warning.

imex_cfg_enum uses dynamic_cast on both halves rather than the type()
comparison the others use: every ConfigOptionEnum<T> reports coEnum, so a
type() check cannot tell one enum type from another and would cast a
ConfigOptionEnum<OtherEnum> to the requested T. The ConfigOptionPercent :
ConfigOptionFloat inheritance that rules dynamic_cast out for the float
accessor has no analogue for enums.

Correct the comments that prompted this: the cache-key input list in
PartPlate named five inputs for a nine-part key, the ImexMarkerKey note
in GCodeViewer called imex_tool_layout an input only the preview reads
when the plate keys it too, and four file:line citations pointed at the
wrong lines. Values are unchanged at every converted site; cache key
strings keep their existing representation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 20:34:04 -04:00
Clifford GarwoodandClaude Opus 5 e45c241466 Address the review on the IMEX parallel printing PR
Resolves all 21 inline comments, plus six changes that altered behaviour for
users not using the feature and seven defects found alongside them.

The reviewer's central point generalised: a pressure-advance change had moved
every RepRapFirmware user onto an unverified command form. Auditing for that
class found five more — 14 config keys leaking into every exported g-code, an
ungated Moonraker sync writing to non-IMEX printers' presets, every slice
eagerly re-rendering all plate thumbnails, a preset delta-encoding regression,
and physical_extruder_map being normalised for printers that read it the other
way.

The structural asks landed as asked: the 392-line bed-zone geometry moved to
libslic3r and is now unit-tested, the preview consumes that same layout instead
of a second copy, nine open-coded mode lookups became one, and the modes editor
moved out of Tab.cpp. Making the geometry testable exposed three further
defects in it, including an aggregated gantry that raised no collision strip.

The worst bug was not in the review: the GUI computed the firmware-managed
slice offset in the plate-list world frame while both consumers subtracted the
plate origin again, so every plate after the first failed to slice with 'part
is off the plate'.

User-facing strings now read IDEX/IQEX, honouring 461c69c83e. Config keys, C++
identifiers and 3MF metadata keys keep the imex_ spelling as on-disk format.

815/815 tests pass in Release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 00:26:21 -04:00
Clifford GarwoodandClaude Opus 5 c09ce3a0d1 Merge upstream main: CLI argument parsing, GUI string fixes, nozzle type undo tracking, warning cleanups
No IMEX code upstream, so nothing in this merge touches the feature. All 21 overlapping
files auto-resolved; verified every upstream addition is present in the merged tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 00:17:36 -04:00
Joseph Robertson c96945490b Belt Printer Sept 1 Rebase (#15526)
Also a bunch of bug fixes, thanks to the Baby Belt community for finding
issues!
2026-09-03 13:35:23 -05:00
Clifford GarwoodandClaude Opus 5 5eac300d91 Use IDEX/IQEX in the user-facing strings
Closes review comment 10.

`461c69c83e` settled this in April — IMEX internally, IDEX/IQEX as the user-facing label — but the
UI strings were never converted. Every translated string naming the feature now reads IDEX/IQEX:
41 occurrences across the printer and process option labels and tooltips, the modes editor, the
plate mode indicator, the pre-slice warnings, the placement refusals and the slicing errors. The
reviewer listed eight; the rest were in the same class.

Nothing else moves. The config keys keep the `imex_` spelling — `is_imex`, `imex_mode_names`,
`imex_parallel_mode` and the rest are on-disk format in existing printer presets and 3MF projects,
so renaming them would break every profile and project already saved. C++ identifiers, filenames,
comments and test names keep IMEX as well: it stays the internal name of the subsystem, which is
what covers the topology space (one gantry with 2-4 tools, 2x1 and 2x2 grids) that neither acronym
names on its own. Where a tooltip quotes a key, the key spelling is preserved and only the feature
word around it changed.

The `is_imex` tooltip is reworded rather than substituted: it already named the hardware families
parenthetically, so a literal replacement would have said IDEX/IQEX twice in one sentence.

No translation impact — no IMEX string had reached OrcaSlicer.pot or any catalogue, so there is
nothing to migrate. One test asserted on the old error text and now matches the new one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 10:05:37 -04:00
Clifford GarwoodandClaude Opus 5 bd8dfd6250 Cover the IMEX slice offset, the mode G-code placeholders, and the non-IMEX heater guard
Closes review comments 13, 14 and 15, and adds the test for a shipped-profile
regression that nothing guarded.

- 14 and 15: compute_imex_slice_offset had eight tests on the calculation and none
  on the result, which is the whole firmware-managed path. test_imex_slice_offset
  now covers the derivation end (which config produces a non-zero offset, and that
  it is plate-local rather than moving with the plate origin -- the bug that
  shifted every plate after the first) and the consumption end (emitted
  coordinates and first_layer_print_min/max both move by the derived amount).
  The first_layer case also cross-checks the two consumers against each other: the
  declared bounds must keep the same relationship to the emitted toolpaths in both
  frames, which fails if exactly one of them is shifted. It deliberately does not
  pin the size of that gap -- it is 2.225 mm here, set by the wall generator, the
  same with no offset at all, and pinning it would fail on an unrelated change.
- 13: nothing exercised the imex_mode / imex_mode_index / imex_mode_gcode
  placeholders or the {global} flow into machine_start_gcode that their ordering
  exists to guarantee. Seven cases now do, including the ordering itself -- the
  mode script declares a global and machine_start_gcode reads it back, so moving
  the mode processing later leaves the variable undefined and fails the export --
  plus the inert cases (Primary mode, and a printer with the table filled in but
  is_imex off). All matching is whole-line, because the config block the exporter
  appends repeats machine_start_gcode verbatim and would make substring checks
  meaningless.
- New: GCodeWriter passes this->config.is_imex.value into the heater remap, and
  nothing tested that it passes the flag rather than a constant. Hardcode true
  there and the whole suite stays green while fdm_bbl_3dp_002_common, which ships
  physical_extruder_map [1,0], starts sending filament 0's M104/M109 to heater 1.
  The new case runs a two-nozzle non-IMEX printer with that map and asserts each
  filament's temperature reaches only its own tool. It uses idle_temperature via
  ooze prevention rather than nozzle_temperature: keys in
  filament_options_with_variant are re-indexed per filament by variant slot at
  apply time, and this harness pins nozzle_diameter to one value, so every filament
  resolves to the same slot and the temperatures stop telling the heads apart.

Also fixes two weaknesses in tests added earlier in this branch: an assertion that
would have been prefix-satisfied by the very routing it was meant to exclude, and
a whole-file command comparison between two slices, which this slicer's output is
not stable enough to support.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 00:57:14 -04:00
Clifford GarwoodandClaude Opus 5 7c49660c05 Share one routing derivation with the slicer, and stop two GUI paths acting on non-IMEX printers
Closes review comment 20; the other two are non-IMEX leaks found auditing the
branch.

collect_imex_warnings re-derived the active mode, its tools, the primary and the
filament routing that Print::validate also derives, so the warning and the hard
block could drift apart -- and this function had already had one index-confusion
bug, the AFC/MMU wrong-filament names fixed in fbc58d2a1d. Both now read
imex_resolve_routing() and derive nothing themselves. The function stays
file-static: what is left in it is PresetBundle lookups, bed-type resolution and
formatting, none of which can disagree with the slicer about what the plate is
doing, and the index-confusion surface is now library code with tests covering the
AFC manifold in both directions. A latent out-of-bounds read went with it -- the
primary fallback can return -1 and the bounds checks were upper-only, so
filament_presets[-1] was reachable on a profile whose roster names a head absent
from the map.

Slicing eagerly re-rendered every plate thumbnail on the main thread after
switching to Preview, up to two blocking offscreen GL renders per plate on every
slice click. The work was already redundant: select_view_3D("Preview") invalidates
the thumbnails and marks the toolbar dirty, and the next frame force-regenerates
them anyway. It could not have served its stated purpose either, since it ran
immediately after reslice(), which only starts the background slice. Both calls
dropped; the plate badge state is recomputed per frame and is unaffected. The
export_3mf thumbnail log lines are back at info, and the slice-event traces
restored.

The Moonraker device sync wrote physical_extruder_map into the edited printer
preset ungated, so any Klipper machine running a current AFC build had its preset
marked dirty with no user action, and saving persisted the map into every 3MF
after. The map is indexed by logical extruder while the device reports one entry
per lane, and nothing in the lane payload carries the logical slot, so the two
index spaces coincide only when the counts match. Now gated on is_imex, written
only when the counts agree, and only when the value actually differs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 00:56:55 -04:00
Clifford GarwoodandClaude Opus 5 1e608fe24c Have the preview consume the shared zone layout instead of rebuilding it
Closes review comment 17.

GCodeViewer had its own copy of the flip_x/flip_y corner mapping, the active
column/row sets, the physical-to-zone index mapping and the zone pitch -- the same
derivation as the plate's, with nothing keeping the two in step. It now calls
compute_imex_zone_layout() and consumes head_zone_centers. The mirror is expressed
as a reflection about the midpoint of the two zone centres rather than about a
zone-relative strip width, which is algebraically identical for equal-sized zones
and needs no pitch, and the toolhead-box face is chosen by comparing zone centres
instead of physical columns.

The July report of a math error in the visualizer for non-primary heads was this
drift: the sec_box_offset_y else-branch hardcoded -imex_box_wy, which happened to
equal the primary's offset on the rear-* layouts and pointed the wrong way on the
front-* ones. Structurally unreachable now.

Verifying the two sides matched turned up two config defaults that disagreed, both
fixed in their own commits: imex_nozzle_clearance_x/y (the viewer's 30.0f matched
PrintConfig, the zone code's 0.0 did not, and the strip loops are gated on it) and
imex_tools_per_gantry (the library's 2 matched, both GUI paths used 1).

Consuming the shared function meant resolving it per frame, and the sequential-view
marker flag is sticky, so one drag of the slider made every subsequent frame parse
five strings and allocate a dozen containers from inputs that never change. The
resolve now sits behind a cache key mirroring PartPlate::build_imex_cache_key(),
plus the two inputs only the preview reads -- the bed extents and the tool layout.
An idle frame compares scalars and allocates nothing. The toolhead-box mesh, which
was being re-uploaded to the GPU every frame for the same reason, is rebuilt only
when the clearances change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 00:56:55 -04:00
Clifford GarwoodandClaude Opus 5 838c2f2df8 Reset plates to Primary when their mode is deleted
Closes review comment 9.

PartPlate::reset_imex_mode() had no callers, while the PR description said
deleting a mode resets affected plates to Primary. IMEXModesCtrl now exposes an
on_mode_removed callback that TabPrinter::build_fff handles by resetting every
plate whose mode matches the deleted row, under a single undo snapshot, followed
by the same dirty/update sequence the plate's own mode button runs.

Wired to deletion only, on purpose: the name field notifies on every keystroke, so
routing renames through the same path would orphan and reset the plate on the
first character typed. Renames stay covered by the slice-time fallback and its
warning. The callback is copied to a local before notify(), because notify()
reaches load_from_config() -> clear_rows(), which tears down the row the handler
is running inside.

The rest of this file is the modes editor moving out to its own translation unit,
leaving the include and the construction site.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 00:56:55 -04:00
Clifford GarwoodandClaude Opus 5 f75bdd6013 Move the IMEX modes editor into its own file and fix four defects in it
Closes review comments 21, 4 and 12, and the widget half of 19.

- 21: IMEXModesCtrl was 572 lines inside Tab.cpp. It now lives in
  IMEXModesCtrl.{hpp,cpp} next to IMEXFilamentPickerPopover, which was the
  precedent named in the comment. The move itself is exact -- member order,
  comments and every string literal unchanged -- and the class had no file-local
  dependencies in Tab.cpp, only its include list, so the new source states those
  explicitly.
- 4: a mode row with an empty Name was silently dropped on save, tools and G-code
  with it, and matches_config() compared against that same filtered output so the
  preset never went dirty and the row stayed on screen. Rows are now given a
  generated unique name instead of being discarded, and add_row() pre-fills one so
  the common path never produces a blank. Names are deliberately not translated:
  objects store a mode name in imex_parallel_mode and GCode.cpp matches it by
  string, so a localized name would break a project reopened in another language.
- 12: the editor had a third parser that read a bare token and an unknown role
  suffix as Primary, while parse_imex_active_tools reads both as Copy -- so the
  editor and the slicer could read one imex_mode_active_tools string two different
  ways. Deleted; the editor now uses the same two helpers the slicer does.
- 19: tile state was an int shadowing ImexRole, with the role letters duplicated in
  a second switch that wrote the on-disk format. The tile now holds
  optional<ImexRole>, with Inactive spelled as the absence of a role rather than a
  fifth integer, and the letters come from kImexRoleTable.

Four further changes, from testing rather than the review:

- Deleting a mode reported only the row's current name, so renaming a mode and then
  deleting it left every plate using it stranded on a name that no longer exists.
  Both the build-time and current names are now reported, minus any a surviving row
  still carries.
- The instruction text and colour legend were built once in the constructor and
  never rebuilt, so raising gantry count to 2 gave the tiles a Span role the legend
  never explained until the preset was saved and the page reopened. Both are
  rebuilt with the grid, and the per-role detail moved into legend tooltips so the
  panel no longer opens with a paragraph.
- The tile holding Primary is now read-only. Primary is tool 0 and moves only via
  Tool 0 Position; a click could previously demote the only Primary, leaving a mode
  that parses to no primary at all, which degrades the plate to an ordinary
  single-tool print with nothing in the editor showing what is wrong. A mode
  arriving without a Primary keeps every tile live so it can still be repaired.
- Names and G-code were read with ToStdString() (the ANSI codepage on Windows) and
  written with from_u8() (UTF-8). On a non-UTF-8 codepage a name like "Modus A"
  with a diaeresis was stored as invalid UTF-8, came back blank, and was then
  silently renamed by the auto-naming above. Every read is now into_u8() and every
  write from_u8(); EditGCodeDialog was affected in both directions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 00:56:26 -04:00
Clifford GarwoodandClaude Opus 5 5b30af463d Reduce PartPlate's zone code to a wrapper and key its cache on the tool layout
calc_imex_zones() is now 69 lines: fetch the two edited configs, call
compute_imex_zone_layout(), store the result, and clip each returned rect to the
bed outline to build the GLModels. That last step is the only part that needs GUI
types, which is why it stayed. See the extraction commit for the behaviour-
preservation evidence.

refresh_imex_slice_offset() is deleted along with its call in
update_slice_context(); the offset is derived in the engine now, and it was
computing it in the plate-list world frame, which double-counted the plate origin
for every plate after the first.

Two smaller changes:

- The zone/ghost cache key omitted imex_tool_layout, which decides which physical
  corner tool 0 occupies and therefore moves every zone rectangle, collision strip
  and ghost offset while every other keyed field stays put. A layout change
  produced an identical key. That this currently appears to work is incidental --
  some other path happens to rebuild -- and not something to depend on. Found by
  building the preview's own cache key against this one.
- The tools-per-gantry fallback for a missing key was 1 in two places where
  PrintConfig registers 2 and the zone code uses 2. All four sites now agree; a
  missing key otherwise grouped tools against a grid divided a different way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 00:56:26 -04:00
Clifford GarwoodandClaude Opus 5 5ac1d05fcb Stop three IMEX changes from altering non-IMEX behaviour
None of these came from the review; they were found auditing the branch for the
same class of leak review comment 6 identified.

physical_extruder_map was normalised through effective_physical_extruder_map on
every Print::apply(), so any printer with more than one nozzle and no authored map
got the identity [0,1,...,n-1] where the single-element {0} default belongs. The
key carries two readings: the IMEX paths index it by logical extruder and need one
entry per extruder, while the inherited BBL paths read it through the clamping
get_at(), for which {0} means "everything is physical 0". Deriving unconditionally
imposed the IMEX reading on profiles that mean the other one, changing the config
block line and the {first_tools} / {first_filaments} / {curr_physical_extruder_id}
placeholders for multi-nozzle non-IMEX printers. Gated on is_imex; every consumer
needing the per-extruder form is already IMEX-gated, and profiles with an authored
map of the right length are unaffected either way.

That gating unmasked a latent out-of-bounds read: WipeTower's M104/M109 emitters
index m_physical_extruder_map by tool with no bounds check, which reads past the
end of the single-element default on a multi-nozzle machine. Upstream's bug, from
the BambuStudio wipe tower sync, previously hidden because the map was being
widened for everyone. Now bounds-checked, falling back to the tool's own index --
the form GCodeProcessor already uses for the same map.

Preset::save() and get_preset_differed_for_save() carried a branch storing the
full vector whenever a child and its parent had different lengths. It was written
against a set_with_nil that threw on mismatched sizes; upstream #13035 replaced
that with a tolerant version that keeps the child vector verbatim and nil-marks
only the overlapping range, and that fix was already in the tree when this branch
was rebased. Left in, it defeated the delta encoding for every user printer preset
whose extruder count differs from its parent's: a 7-extruder profile inheriting a
single-extruder base wrote out all of its per-variant retraction keys as literals,
including ones identical to the parent, pinning them against future vendor updates
while the UI still reported the preset as inheriting. Removed; the two save loops
are now identical to upstream. Note this only affects new saves -- presets already
written keep their frozen values until re-saved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 00:55:58 -04:00
Clifford GarwoodandClaude Opus 5 a5ba39393e Escape the IMEX plate attributes written into the 3MF
Closes review comment 1.

imex_parallel_mode and imex_head_filament_map were streamed raw into XML attribute
values, while every other free-text attribute in the same writer goes through
xml_escape. Mode names are free text, so "PLA & ABS", a quote or a "<" made the
document malformed. The failure is not a bad value on reload: both load paths for
model_settings.config return false on an expat error, and m_is_bbl_3mf is set
before the second entry loop runs, so the whole project fails to open with
"Archive does not contain a valid model config".

Both attributes now use xml_escape_double_quotes_attribute_value(), which also
emits tab, CR and LF as numeric character references. That matters and plain
xml_escape would not do: XML normalises literal whitespace in attribute values on
read, so a tab in a mode name would come back as a space and silently rename the
mode. The read side needs no change -- it takes expat's already-decoded value with
no second unescape -- so this is a lossless round trip and a file written by the
new code still loads in an older build.

The round-trip test used "copy_mode", which exercised none of this; it now carries
&, <, a quote and a tab, and also pins that ' and > come back unmodified, since
both are legal raw inside a double-quoted value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 00:55:58 -04:00
Clifford GarwoodandClaude Opus 5 5629dd29e9 Restore the pre-IMEX pressure advance output for non-IMEX printers
Closes review comment 6.

The per-tool pressure advance work changed set_pressure_advance() for users who
are not using the feature. RepRapFirmware lost its D qualifier when no tool index
was supplied: upstream emits M572 D0 S<pa> unconditionally, and a bare M572
applies to whatever tool is currently selected and errors when there is none, so
PA started depending on tool-selection state for every RRF user. The D is back,
defaulting to 0, and D<tool> is reached only from the IMEX paths.

The same rewrite had also changed the comment separator from "<value>; Override"
to "<value> ; Override" on the Klipper, RRF, Marlin 2.x and Marlin Legacy
branches, so every non-IMEX print of those flavors carried a one-byte diff.
Restored. Upstream is internally inconsistent here -- BBL and Repetier do use the
spaced form -- and the point is to match it exactly rather than to tidy it.

Emitted output for all six flavors with no tool index is now byte-identical to
upstream. Verified on a real slice: a Klipper profile emits
"SET_PRESSURE_ADVANCE ADVANCE=0.02; Override pressure advance value", an exact
string match, with no EXTRUDER= qualifier. The tests were pinning the regressed
form and are inverted.

Also records at the imex key registrations why they are kept out of the g-code
config block, matching the house convention at the other banned keys.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 00:55:35 -04:00
Clifford GarwoodandClaude Opus 5 612e0e3932 Fall back to Primary when an IMEX mode does not resolve, and keep imex keys out of the config block
Closes review comment 2, part of 18, and one non-IMEX regression the review did
not cover.

An imex_parallel_mode naming no entry in imex_mode_names still entered the
parallel branches. get_imex_active_tools() returned empty and the else was
skipped, so no head received its 1st-to-2nd layer temperature transition, and
imex_suppresses_bare_toolchange() still dropped the initial T<n> on the
expectation that a mode script would select the tool. validate() did not catch it
because its guard is declared_primary >= 0 and an unresolved mode yields -1. You
reach it by renaming a mode after a plate is set to it, or by opening a 3MF whose
printer preset names its modes differently. m_imex_parallel_mode is no longer
assigned before the lookup; a non-Primary name that matches no row now warns and
re-resolves against the Primary row, which is the fallback the PR description
already claimed. A mode that resolves to an empty tool roster takes the same path,
since the emitted G-code is wrong in the same way.

Warned rather than blocked: opening someone else's 3MF is a legitimate way to get
here and the Primary reading prints correctly, so refusing to slice would turn a
recoverable situation into a dead end. Silent was not an option either, because
the plate keeps showing the stale mode name while drawing no zones.

Separately, the 14 imex config keys all register non-nil defaults, so
append_full_config was emitting "; imex_* = <default>" into every exported
G-code, including on single-nozzle printers with nothing to do with the feature.
They are banned from the dump, matching the treatment already given to the
fast-purge, extruder-change and timelapse keys, so the config block is
byte-identical to the pre-IMEX baseline for the whole shipping fleet. Nothing
reads them back: GCodeProcessor has no imex reference, and the two per-plate keys
round-trip through the 3MF's model_settings.config on an independent path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 00:55:35 -04:00
Clifford GarwoodandClaude Opus 5 3629056831 Derive the IMEX slice offset in the engine
Closes review comment 11, and fixes a worse bug found while doing so.

The offset was only ever pushed from PartPlate::refresh_imex_slice_offset(),
reachable from update_slice_context() and the plater -- both GUI-only, and it
dereferences wxGetApp(). A headless slice therefore kept Vec2d::Zero(), so
orca-slicer --slice on a plate with imex_firmware_managed_zones emitted
slicer-managed coordinates while the firmware applied its own offsets on top.
Print::update_imex_slice_offset() now derives it from the applied config and runs
from process() and export_gcode(), so a CLI slice gets the value a GUI slice does.
It reads m_full_print_config rather than m_config because imex_tool_layout and
imex_carriage_margin are printer-preset options with no member in the static
PrintConfig, and it takes the mode from the same place GCode.cpp resolves it, so
the shift cannot disagree with the mode that is emitted.

The GUI push and Print::set_imex_slice_offset() are deleted rather than kept as an
override, because the two did not agree. calc_imex_zones() divides
get_extents(m_shape), and set_shape translates m_shape by the plate position, so
the pushed offset carried the plate origin -- which translate_to_print_space() and
the writer offset already subtract. Plate 1 sits at the origin and agreed by
accident; every later plate had the origin subtracted twice and was shifted by a
full plate stride. Not silent, either: the displaced geometry fell outside the
printable area, so slicing plate 2 failed validation with "part is off the plate".
Confirmed fixed on hardware profiles -- the same model on plates 1 and 2 now emits
identical extents.

Deleting the push also removes the post-apply ordering constraint that forced the
duplicate call in Plater::priv::update_background_process: the value is computed
at the point of use, and process()/export_gcode() are structurally after apply().

Also routes validate()'s primary-routing check through imex_resolve_routing() so
the hard block and the plater's warning cannot describe a plate differently
(review comment 20), and through find_imex_mode() for the mode lookup (18).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 00:55:35 -04:00
Clifford GarwoodandClaude Opus 5 fcfda6c835 Move the IMEX bed-zone geometry into libslic3r
Closes review comment 16, and fixes three defects the move exposed.

PartPlate::calc_imex_zones() was 392 lines deciding where every zone, collision
strip and safety margin sits, in the GUI layer, with no test coverage. The three
wxGetApp() calls that kept it there were all in its first 25 lines, fetching two
configs. The geometry now lives in compute_imex_zone_layout(); the wrapper fetches
the configs, calls it, and clips the returned rects to the bed outline for the
GLModels, which is the only part needing GUI types. libslic3r gained no wx
dependency: it takes DynamicPrintConfig directly, so the option lookups moved
verbatim rather than through a hand-written value struct that could drift.

The move is otherwise exact -- verified by a line-for-line diff of every
arithmetic expression against the original, and by running 15 scenarios through
the extracted code against hand-derived values. The only deletion is a lambda that
was never called.

Three fixes on top, each of which needed the code to be testable:

- An off-grid or absent Primary left pri_col/pri_row at their (0,0) initialisers
  and built a layout from them, reporting the whole bed as the clear primary zone
  and the whole bed as a blocked mirror zone at once; under
  imex_firmware_managed_zones it shifted the slice by the bed centre. Guarding on
  the resolved primary head covers both routes. Reachable only from a hand-edited
  preset or a 3MF authored against another printer -- the editor pins Primary to
  tool 0 -- but that is the same class the unresolved-mode fallback handles.
- imex_nozzle_clearance_x/y fell back to 0.0 where PrintConfig registers 30.0.
  Both strip loops are gated on the value being positive, so the fallback emitted
  no collision strips at all while the preview still drew 30 mm toolhead boxes.
- The collision-strip loop asked each mirror head for its own grid cell, but an
  aggregated gantry's cell is pinned to the primary's column and expanded into a
  full-width row strip. Where the representative's column differed from the
  primary's, no boundary matched and the plate came back with no strips and no
  margin bands -- an object flush against the shared boundary sliced without a
  warning while the far carriage occupied it. Present since Span aggregation was
  added in 4966d0fae8 and carried out of PartPlate verbatim. The flags now come
  from the painted cells; an exhaustive sweep of the reachable grid, role and
  layout space (1,630,720 configurations) shows the only behaviour change is the
  missing strips appearing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 00:55:04 -04:00
Clifford GarwoodandClaude Opus 5 171a721304 Harden the IMEX helper layer and give it one mode lookup
Closes review comments 3, 5, 7, 8, 18 and 19, plus the library half of 20.
These share a file, so they share a commit; each is independent of the others.

- 3: ::isspace(char) is undefined for bytes above 0x7F because char is signed on
  our targets. Three call sites now go through one strip_whitespace() using an
  unsigned char cast. Line 308 parses imex_head_filament_map straight out of 3MF
  metadata, so a non-ASCII byte reached it without passing through the UI.
- 5: an imex_head_filament_map override past the end of physical_extruder_map now
  falls through to the printer's own routing instead of resolving to a wrong
  filament. Bounded in resolve_filament_for_head, where the slot count is known,
  rather than at the parse site, which has no count to check against; the parse
  site also gains the absolute MAXIMUM_EXTRUDER_NUMBER cap its sibling already had.
- 7: imex_physical_heater_for's !is_imex early return is what keeps a stock BBL
  profile (physical_extruder_map [1,0]) out of the heater remap, and had no test.
  Six cases now cover it, pinning pass-through rather than get_at()'s clamp.
- 8: ImexRole::Span was missing from the imex_head_transform switch, so it warned
  under -Wswitch. Identity is correct, not merely convenient: a Span tool prints
  the primary's own zone through mid-print toolchanges and has no zone to be
  translated into.
- 18: three positionally coupled string vectors were resolved by nine open-coded
  lookups using three incompatible bounds idioms. None read out of bounds, but six
  folded the guard into the match condition, so a ragged row did not stop the scan
  and a later duplicate name could win. struct ImexMode + find_imex_mode() is now
  the only resolution rule: the names array is the roster, first match wins, a
  short sibling pads to empty and sets ragged, not-found is an explicit -1.
- 19: the letters P/C/M/S existed in three independent copies, one of which was the
  writer of the on-disk format. kImexRoleTable is now the single source, read by
  both parsers and the serializer. Adding a role was 14 edit sites with one
  compiler-enforced; it is now the enum, the table entry, and four -Wswitch
  switches. Verified by adding a fifth enumerator and recompiling: exactly four
  warnings, nothing else.
- 20: imex_resolve_routing() extracts the mode/primary/routing chain that
  Print::validate and the plater's warning collector each derived separately.

The three config keys keep their names, types and on-disk representation. This is
a read-side view only; presets and 3MF files are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 00:55:03 -04:00
Clifford GarwoodandClaude Opus 5 855c1b51ac fix: size the destination row before migrating per-variant values
update_values_from_multi_to_multi_2 iterates the destination PRINTER's variant
list while writing into a row taken from the destination PRINT preset. Those two
lengths are maintained independently -- print_extruder_variant against
printer_extruder_variant -- and Tab::load_current_preset() runs the migration
before the print preset is re-selected for the new printer. Opening a project
saved on a single-variant printer and switching to a seven-variant one therefore
wrote six elements past the end of a one-element vector. The corruption stays
silent until the next allocation, so the abort surfaces somewhere unrelated and
the backtrace points at innocent code.

Size the row to the variant count before indexing it. Every write is then in
range, and the result carries one value per destination variant, which is what
the callers consume. Pad with nil rather than a copied value: set_to_index()
skips nil entries, so a variant the object has no opinion about keeps tracking
the print preset instead of being pinned to another variant's number.

The same shape -- a count from one array indexing another -- appears twice more
in this file. update_values_from_multi_to_multi has three of these writes
protected only by assert(idx < old_count), and NDEBUG is defined for every
non-Debug configuration, so those guards are absent from shipping builds.
update_values_from_single_to_multi has the read half. Both are bounded here;
leaving them would fix one third of one defect.

Source reads are bounded too. is_nil(size_t) indexes values[idx] without
checking, so an index past the end was undefined behaviour on that side as well.

Where the row already matches the variant list -- every case that was not
corrupting the heap -- the resize is a no-op and the output is unchanged.

Fixes #15455

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 01:47:45 -04:00
harrierpigeon e5d4ad2aa7 Merge remote-tracking branch 'upstream/main' into haryr/aug25-rebase
# Conflicts:
#	src/libslic3r/Support/TreeSupport.cpp
2026-08-30 23:31:48 -05:00
harrierpigeonandClaude Opus 5 4fab8d0b39 fix: adapt belt sub-layer group emission to upstream m_writer unique_ptr
Upstream changed GCode::m_writer from a value to std::unique_ptr<GCodeWriter>;
the belt mixed_sub_layer_groups path still used value syntax and did not compile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ChxXYc6Dp46qAN9c2rQCe
2026-08-30 23:31:21 -05:00
harrierpigeon e341a9b84e Merge remote-tracking branch 'upstream/main' into haryr/aug25-rebase
# Conflicts:
#	src/libslic3r/GCode/ToolOrdering.cpp
#	src/libslic3r/Print.cpp
#	src/libslic3r/PrintApply.cpp
#	src/libslic3r/PrintConfig.cpp
#	src/slic3r/GUI/Tab.cpp
2026-08-30 23:30:51 -05:00
Clifford Garwood 9abc3bcaf7 Merge upstream main: mixed-filament extruder-count fix, FFmpeg camera view, plugin storage API, warning cleanups 2026-08-30 23:58:48 -04:00
Clifford GarwoodandClaude Opus 5 5c73f0b1c8 refactor(gcode): drop the wipe tower's unreachable preheat rewrite
append_tcr2 scanned the tower's G-code for a "preheat T<n>" comment and rewrote
its S value to the interface temperature. Nothing it could match was ever there.

That comment has exactly one producer, GCodeProcessor's backtrace injector, and
that runs inside run_post_process() -- a pass over the finished, exported file.
append_tcr2 runs while the file is still being generated, so the text it looked
for did not exist yet and could not.

The loop therefore walked every line, matched none, and swapped the string for
an identical copy. Delete it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 10:36:15 -04:00
Clifford GarwoodandClaude Opus 5 1aa9bb8d93 fix(imex): make the plate badge count filaments the way validate does
The badge is meant to predict whether slicing will be refused, and it delegates
to the same helper for that reason. It was feeding that helper a different
filament list. get_extruders(true) resolves a mixed slot into its physical
components -- right for AMS mapping, which has to know what is actually loaded
-- while Print::validate counts the slot itself.

So a plate holding one two-component blend reads as two filaments to the badge
and one to validate. The badge sees two, decides the plate is fine, and stays
silent; the slice is then refused. It also runs the other way: a plate the user
sees as a single colour draws a multi-material warning, because the expansion
made it look like two.

Give get_extruders an expand_mixed flag, defaulted so every existing caller
keeps the resolved list, and have the badge ask for the authored one.

The badge was also only mirroring validate's multi-color rule, not its first
one -- a mixed filament is unsupported in a parallel mode outright. Without it
the badge stays quiet on exactly the plate validate refuses first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 10:36:15 -04:00
Clifford GarwoodandClaude Opus 5 2473ca7496 docs(imex): record that the primary-routing refusal is a scope choice
The rule's comment claimed the plate "is not printable as configured: the
primary tool executes the toolpaths while the flow and temperatures were
computed for a filament it cannot load". That is not what the emitter does. It
never uses the declared primary -- it re-derives an effective one from the
filament actually in use -- so a plate whose only filament sits on a Span tool
sharing the primary's gantry produces coherent G-code and would print.

The refusal is still correct, but it rests on intent rather than physics: a
parallel mode exists to run carriages in parallel, and a single-colour plate
riding one span lane is not that. Left as a physical-impossibility claim, the
rule reads as a false positive to anyone who checks it against the emitter --
a review already flagged it as one -- and the obvious "fix" is to relax it.

Say which it is, and keep the genuinely-broken case distinct: a filament routed
to a head outside the mode's active tools still yields a stuck-hot nozzle, and
that one is not a matter of taste.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 09:04:50 -04:00
Clifford GarwoodandClaude Opus 5 018091f43b fix(imex): let the mixed-filament refusal outrank the multi-color rule
A mixed filament is unsupported in a parallel mode outright, but the rule saying
so ran third. A plate carrying a blend plus any second filament tripped the
multi-color rule's used > 1 gate first and was told its active tools all sit on
one gantry -- a diagnosis of a multi-color print the user never configured,
whose remedy is to go rework the mode's tool roster. The blend was never
mentioned. Move the check ahead of both rules below it; being unsupported
regardless of routing or topology, it dominates them.

Nothing is masked that leads anywhere else: every branch of
imex_multicolor_block_reason is itself confined to non-primary modes, so the
mixed message's remedy -- switch this plate to Primary -- silences those too.

Say "Mixed filaments", not "Blended". Every other string in the app calls these
mixed, including the button that creates one and the sibling refusal for the
wipe tower filament, so the user had no way to connect the message to the
feature it names.

Three comments in the block were wrong, and two of them were newly wrong. The
routing rule's bounds-check note still said "Blended slots are out of range by
construction, but they never reach here -- the rule above returns first": the
rule above is now the multi-color one, which does not return first for a single
mixed filament, and out-of-range is not guaranteed at all. Mixed slots are kept
at the tail of the filament arrays by convention, not by enforcement --
PresetBundle::set_num_filaments grows filament_is_mixed with resize(), so
raising a printer's extruder count with a blend present lands physical slots
after the mixed one. The scan is position-agnostic and stays correct; only the
stated reason was wrong.

The same discovery makes the empty-routed_list guard live rather than the dead
code it was described as. Print::apply() normalises physical_extruder_map before
validate() runs, so an unauthored map is never the cause -- but a printer with
more filaments than logical extruders leaves the tail slots outside the map, and
raising the extruder count does exactly that.

The new test validates the plate twice. The first pass, with no blend, asserts
the multi-color rule is armed at all; without it the second proves nothing,
because the rule only fires here thanks to a degenerate fixture mode whose two
tools share a gantry. Give that mode a Span tool and the whole test would pass
under either ordering while appearing to guard it. It also pins err.object,
which the mixed path sets and the multi-color path leaves null -- a discriminator
that survives the next wording change. Verified by reverting the order: the test
fails on both the message and the object.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 08:48:14 -04:00
Clifford GarwoodandClaude Opus 5 2e883df7d9 fix(imex): name the physical head in M104/M109 tool indices
M104/M109 address a heater, but every caller of the instance
GCodeWriter::set_temperature overload addresses filaments by logical id, so on a
printer whose physical_extruder_map is not the identity the emitted T named the
wrong head -- or, where the logical id exceeds the head count, no head at all.
With a map of 0,0,0,0,1,2,3 a toolchange to filament 5 emitted "M109 S265 T4"
and "M104 S190 T4 ;cooldown" while the head it meant was T1.

Upstream already treats these commands as physical: the preheat it injects in
GCodeProcessor maps through the same map before emitting, and BBS's own wipe
tower does likewise. Emitting logical is the half that never got the memo.

That mismatch also disabled the cooldown suppression beside the preheat, which
compares the line's T against pem[tool_number] and so never matched a logical
one -- 171 cooldowns survived in a two-head print where none should have. Worse,
it could match the wrong line: a cooldown for filament 1 emitted T1, and a
toolchange to filament 5 gives pem[4] == 1, so a legitimate cooldown for head 0
was deleted because the incoming head happened to be numbered 1.

Translate once, in the instance overload every logical-space caller passes
through. The static overload is already physical-in and is left alone.

Gated on is_imex. physical_extruder_map carries two readings in this tree: the
BBS paths index it by extruder id, the IMEX paths by filament id, and the two
coincide only when the filament and nozzle counts match. Mapping unconditionally
would impose the IMEX reading on profiles that mean the other one --
fdm_bbl_3dp_002_common ships a non-identity [1,0], spared today only because
single_extruder_multi_material suppresses the T qualifier entirely.

The wipe tower's interface-temperature pass has to move with it. It strips the
M109 that post_toolchange emits by searching for that filament's tool index, so
it now searches for the mapped one; left alone it would have stopped matching,
and the surviving blocking M109 would have silently defeated the interface
temperature. Its sibling pass reads WipeTower2 output, which emits no T at all,
and is deliberately unchanged.

The bare T<n> toolchange stays logical -- it selects an AFC lane, not a heater.

Test slices two objects across a head boundary, the only case that reaches this
emission: the same-physical short-circuit in set_extruder suppresses the
cooldown entirely for lane swaps within one head. It scans every M104/M109
rather than matching fixed strings, so it catches any unmapped emission and not
just the two sites changed here. With the mapping neutered it reports 101
offending lines; with it in place, none.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 22:04:23 -04:00
Clifford GarwoodandClaude Opus 5 f0778ef5fa fix(imex): shorten the off-primary message and block blended filaments
The routing error ran to roughly 450 characters and explained the mechanism
before it got to the remedy. It also offered to "edit the mode in Printer
Settings so its Primary tool is one of %3%", which on a plate whose filaments
resolve to no head at all rendered as "one of no configured extruder". Cut it
to the mode, the tool it prints with, where the plate's filaments actually are,
and the two things the user can do about it.

The second msgid that named candidate modes went with it. It could only suggest
a mode whose primary is among the routed heads, and every mode on the printers
this fires for declares 0:P, so it had nothing to offer.

Blended filaments now return before that check rather than falling through it.
A blend is mixed at the nozzle by its component toolheads, and a parallel mode
is already using those toolheads to print copies or mirrors, so the two cannot
run at once regardless of where the components route -- including when a
component sits on the declared primary. Reaching the routing rule would also
have described them wrongly: mixed slots sit past the end of
physical_extruder_map, so they resolve to no head and read as merely unrouted.

Keeps the empty-list guard the shortening first dropped. validate() reads the
raw physical_extruder_map, whose registered default is a single entry, so a
profile that declares IMEX modes without authoring a map leaves every slot past
the first outside it -- and the sentence ended in a dangling "on .".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 22:04:02 -04:00
AveryanAlex 67ff6bd34b Fix Orca Cloud API URL scheme 2026-08-26 19:52:16 +03:00
Clifford GarwoodandClaude Opus 5 b24733cea8 Merge the color mixing subsystem into the IMEX work
Brings the upstream color-mixing feature and its follow-ups onto the branch so the
IMEX placement and primary-routing checks are built and tested against them for the
first time.

Merged clean, no conflicts. Not yet exercised together: a mixed filament is a virtual
slot no nozzle carries, while physical_extruder_map routes logical slots to physical
heads, so the IMEX pem lookups have no defined answer for one. Print::extruders()
lists mixed slots under their own id while tool_ordering.all_extruders() lists them
post-expansion, and the IMEX code reads both.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 09:25:41 -04:00
Clifford GarwoodandClaude Opus 5 008d13a59e fix(imex): keep the placement error across every validation path
Plater::validate_current_plate() runs the same background_process.validate() as
update_background_process(), and on success clears update_apply_result_invalid(false)
and closes the ValidateError notification -- but it never consulted
imex_placement_violation(). Any event reaching it wiped a live IMEX placement error
and re-enabled the Slice button on a plate the slicer still refused; pressing Slice
then hit the check in reslice() and returned early, so the job simply never started.

Reproduce by clicking the bed with the prime tower overlapping a reserved area.
Plater::select_plate_by_hover_id() -> select_plate() calls validate_current_plate()
unconditionally, even when the clicked plate is already current, and deselects as a
side effect -- which makes it look as though deselecting the tower cleared the error.
Escape and clicks off the bed go through selection_changed(), which only renders and
clears nothing.

Extract the violation-to-message mapping into one helper and call it from both paths.
model_fits is set alongside err.string in validate_current_plate, mirroring the
missing-plugin block below it: the slice is already gated by m_apply_invalid, but
leaving m_ready_for_slice true would trap a future consumer that reads it alone.

These are the only two sites that matter. NotificationType::ValidateError has exactly
three references in the tree and update_apply_result_invalid exactly four; the other
slice-ready writers can only touch m_ready_for_slice, never m_apply_invalid, so they
cannot re-enable Slice on their own.

Three adjacent gaps are left alone, all pre-existing: "Slice all" is hard-coded
always-enabled regardless of plate state; a slice-all batch halts silently at a
violating plate because reslice() returns above the line that queues the advance; and
object_list_changed() computes its own can_slice from geometry, harmless only because
the result is ANDed with PartPlate::can_slice().

This is a hole in the shipped tower-zone check rather than a regression from rotating
the tower hull -- it was simply invisible until a tower could be placed in violation.

No automated gate: Plater is GUI-only, the helper is file-local and unlinkable, and
imex_placement_violation() needs a live wxApp and preset bundle. "Both call sites
consult it" is a call-graph property no unit test can express. The grep for a single
imex_placement_violation reference is a future regression tripwire, not evidence this
refactor happened -- it already returned 1 beforehand.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 08:25:25 -04:00
Clifford GarwoodandClaude Opus 5 ba07716ddb Merge upstream main: color mixing feature and related fixes
Brings in 54 upstream commits, the bulk of them the BambuStudio-ported color
mixing / mixed filament subsystem (#15347) plus its follow-ups, along with the
Assimp-backed colored OBJ import, warning-policy build changes, and assorted
profile and localization updates.

Two conflicts, both "each side added at the same point", resolved by keeping
both:

- Print::validate() -- our IMEX multi-color block and upstream's new gradient
  mixed filament warning were inserted at the same spot after the empty
  extruders check. They test unrelated conditions, so both are kept, each with
  its own closing brace.
- tests/libslic3r/test_3mf.cpp -- our three IMEX per-plate round-trip scenarios
  and upstream's mixed-filament round-trip scenario both append to the end of
  the file, and each side added one include. All four scenarios and both
  includes are kept.

Everything else merged cleanly, including GCode.cpp, ToolOrdering.cpp,
PartPlate.cpp and PrintConfig.cpp. Upstream left the is_extruder_used block
untouched, so the IMEX supplement still applies, and estimate_wipe_tower_polygon
is unchanged, so the prime tower hull work is unaffected.

Not addressed here, and worth its own change: a mixed filament is a virtual slot
that no nozzle carries, while physical_extruder_map routes logical slots to
physical heads. Print::extruders() lists mixed slots under their own id whereas
tool_ordering.all_extruders() lists them post-expansion, so the IMEX pem lookups
have no defined answer for a mixed slot. Upstream's own guards reject a mixed
filament where a physical slot is required; IMEX likely wants the same.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 13:07:26 -04:00
Clifford GarwoodandClaude Opus 5 baef398ee3 fix(imex): refuse a plate whose filament cannot reach the mode's primary tool
The IMEX primary tool prints the sliced paths directly, so it can only load a
filament that physical_extruder_map routes to it. The ghost filament picker
enforces that for the secondary tools -- it offers only lanes whose pem entry
equals that head -- but the primary's filament comes from the ordinary object
filament selector, which has no IMEX awareness. Nothing detected the mismatch:
collect_imex_warnings() computes the same condition and discards it into a
display fallback, and the multi-color rule never examines it.

Block it in Print::validate() via the existing imex_primary_tool_for_mode and
imex_primary_logical_from_objects helpers. The message names the declared
primary, the heads the plate's filaments actually live on, and any configured
modes whose primary would work, and carries the object so the notification can
offer a jump to it.

Blocks rather than warns, matching the multi-color rule: the plate is not
printable as configured, and where the routed head is also absent from the
mode's active tools the 1st->2nd layer temperature branch skips it too, leaving
that head at its initial-layer temperature for the whole job.

The multi-color check now runs first. Its constraints -- an MMU manifold sharing
one head, a single-gantry mode -- cannot be fixed by switching mode, so the more
specific error should win rather than be masked by routing advice that leads
straight back to it. The extruders().size() > 1 gate moved onto that call, since
the routing check must also see single-filament plates, which is its common case.

The copy-mode guard-rail test printed on a filament routed off the primary, so
it asserted a plate this rule now refuses; retargeted to a well-formed plate.
Its replacement pins the object's own extruder, because ModelVolume reports its
extruder_id and would otherwise put a primary-routed slot on the plate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 12:58:52 -04:00
harrierpigeon a7bc054974 docs: authorize private build notifications 2026-08-25 10:27:21 -05:00
harrierpigeon 30351d40e1 tests: adapt belt brim coverage to upstream validation 2026-08-25 10:27:08 -05:00
harrierpigeon d289478618 Merge remote-tracking branch 'upstream/main' into haryr/aug25-rebase
# Conflicts:
#	resources/profiles/Custom.json
#	src/libslic3r/Brim.cpp
#	src/libslic3r/GCode.cpp
#	src/libslic3r/GCode.hpp
#	src/libslic3r/Preset.cpp
#	src/slic3r/GUI/3DScene.cpp
#	src/slic3r/GUI/ConfigManipulation.cpp
#	src/slic3r/GUI/GLCanvas3D.cpp
#	src/slic3r/GUI/Plater.cpp
2026-08-25 06:50:46 -05:00
Joseph Robertson 306e379a2a Multicolor Belt Support & various bug fixes (#15361)
# Description
lots of small bugfixes, and multicolor belt support.
[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-08-24 23:23:47 -05:00
Clifford GarwoodandClaude Opus 5 8edf6b9d4f fix(imex): rotate the prime-tower hull before checking it against carriage zones
imex_wipe_tower_hull() took the axis-aligned box estimate_wipe_tower_polygon()
returns and compared it to the IMEX collision zones as-is. The real tower is
rotated about its anchor corner before placement -- first_layer_wipe_tower_corners
builds the box in tower-local coordinates, rotates about the local origin, then
translates by wipe_tower_x/y -- so a rotated tower's true footprint fell outside
the hull and the placement check passed on a tower that intrudes into a
carriage's reserved space.

The gap was documented in place and previously harmless, because
wipe_tower_rotation_angle was read from the project config after it had been
moved to the print preset, so the setting did nothing. Upstream repaired that
read ("Fix prime tower rotation angle setting not working"), which makes the
angle reachable and the stale hull wrong.

Rotation is applied to the hull alone. estimate_wipe_tower_polygon() still
returns an unrotated box and still leaves ArrangePolygon::rotation unset, since
the arranger consumes that field separately.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 01:09:38 -04:00
Clifford GarwoodandClaude Opus 5 90d1481463 Merge upstream main into the IMEX work
Upstream replaced MainFrame's fixed-index TabPosition enum with string-based
page ids ("feat: refactor notebook/tabs to be string based instead of fixed
index based"). The IMEX toolhead-visibility menu item was the only consumer of
that enum left on this branch, so its enable check now compares
m_tabpanel->GetSelectedPageName() against TAB_ID_PREVIEW -- the same form the
neighbouring upstream menu items use.

That mismatch is what broke CI: the branch built on its own, but the merge
commit CI builds no longer had TabPosition declared. No other conflicts.

666/666 tests pass in Release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 17:34:10 -04:00
Clifford GarwoodandClaude Opus 5 99486a5597 Merge IMEX G-code emission and plate-icon raycaster fixes
- parallel modes now give the printing head its second-layer temperature
  transition; it previously held nozzle_temperature_initial_layer all print
- single-tool primary mode no longer marks a phantom filament slot used, which
  made machine_start_gcode heat an extruder that never prints
- the IMEX mode icon's bed raycaster is re-registered after the icon is
  rebuilt, fixing a use-after-free in the picking pass

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 14:54:35 -04:00
Clifford GarwoodandClaude Opus 5 a5b2600b2c fix(imex): re-register the IMEX mode icon's bed raycaster after rebuilding it
SceneRaycasterItem keeps the MeshRaycaster it was registered with as a raw
pointer, while PickingModel::reset() destroys it through a unique_ptr. Rebuilding
an icon therefore invalidates any registration still referring to it.

refresh_imex_icon(), reached only from Plater::on_config_change when is_imex or
the bed shape changes, rebuilt the IMEX mode icon without touching the
SceneRaycaster. The stale entry survived, and the next picking pass dereferenced
freed memory inside AABBMesh::intersect_ray.

Swap that one registration in place, matching how calc_vertex_for_plate_name()
handles the name-edit icon. Only the mode icon is registered for picking -- the
warning badge beside it is a plain GLModel -- so a single id is affected and the
blanket remove/re-register reload_scene() performs is not needed here.

Crashes were delayed and looked unrelated to the config change, because bed
raycasters are only tested when the camera looks down (SceneRaycaster::hit). The
reported dump landed on File > New Project, whose render ran a picking pass with
a registration that had gone stale earlier.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 12:37:28 -04:00
Clifford GarwoodandClaude Opus 5 ef8d80980d fix(imex): enumerate no secondary carriages in single-tool primary mode
get_imex_active_tools returned every physical head named by the active mode's
tool string, including the one carrying the Primary role. The pressure-advance
and nozzle-temperature sites are already gated on the mode not being primary, so
only the is_extruder_used supplement was exposed.

In primary mode that supplement treated the mode's single declared tool as a
secondary carriage and marked its filament slot used, so machine_start_gcode
emitted a heat command for an extruder that never prints. The phantom slot
appears when the mode's declared tool differs from the head the initial tool
routes to through physical_extruder_map -- on an AFC/MMU layout, printing with a
filament that lives on any head other than the declared one.

Return an empty roster for primary mode, where there are no parallel carriages by
definition. This lives in the enumerator rather than at the call site because the
mode is already resolved and normalized there, and the two guarded callers cannot
reach it in that mode, so their behaviour is unchanged.

Scope: this closes the primary-mode instance. The same phantom slot still occurs
in a parallel mode when the initial tool's head is not the mode's declared
Primary, which turns on which of the two notions of "primary" the three emission
sites should skip. That question is unresolved and deliberately left alone here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 12:37:28 -04:00
Clifford GarwoodandClaude Opus 5 c94e8324d0 fix(imex): give the printing head its second-layer temperature in parallel modes
The IMEX branch of the 1st->2nd layer temperature transition is mutually
exclusive with the standard per-extruder path in its `else`, but it skipped the
head carrying the print's own toolpaths on the premise that "the standard
per-extruder temp path already addresses it". That path is the `else` branch and
never runs for a parallel mode, so the printing head received no transition at
all and held nozzle_temperature_initial_layer for the entire job.

Emit for every carriage the mode drives, the printing one included. The printing
head takes this layer's own filament; the parallel carriages, which carry no
toolpaths of their own, keep resolving through the per-plate head map with pem
inversion as the fallback. The lookup now goes through get_filament_config_index()
like the standard path, since a variant-expanded printer gives a filament its own
column and a raw index would read the wrong one.

Reproduced on a 4-carriage IQEX in copy mode: the only temperature command in the
whole file set the idle secondary carriage to the value it already had, while the
head doing the printing never left its first-layer temperature. The defect is
invisible whenever initial and regular temperatures match, which is why earlier
per-tool validation passed.

Tests cover both gantry counts, since the active set comes from the mode's tool
roster: an IDEX copy mode drives two carriages, an IQEX mode drives four, and the
IQEX case asserts a first-layer filament and a second-layer transition for each of
the four.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 12:37:28 -04:00
Clifford GarwoodandClaude Fable 5 8a5bc75e7b fix(imex): key the resolved tool roster and fw-managed flag in the zone/ghost cache
Review follow-up. Removing pem from the ghost cache key also removed its
accidental role as the key's only printer-identity signal: the resolved
active-tools string (roster + primary) and imex_firmware_managed_zones both
shape the baked zone/ghost set but were never keyed directly, so a printer
swap between presets with matching mode names and topology could leave a
stale ghost set. Key all three in build_imex_cache_key, which also hardens
the zone cache against the same pre-existing gap.

Also from review: the tooltip swatch reuses the pem/map its label already
hoisted (one resolution, not two); the bake constructs ghosts with no color
at all, making update_imex_ghost_colors the sole color author; the headless
!m_plater guard is documented as the wxGetApp sentinel it is.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 16:54:48 -04:00
Clifford GarwoodandClaude Fable 5 28c0c681fe refactor(imex): make render the sole author of ghost RGB; scope cache key to mesh inputs
Follow-up to the live ghost-color fix, addressing review findings: with the
render loop restamping ghost colors every frame, the bake-time resolution in
calc_imex_ghosts was dead code (its RGB was displayed for zero frames), and
the pem + head-filament-map entries in the ghost cache key had become
color-only inputs that forced a full mesh re-bake — including a visible
hitch on every ghost-picker selection — for what is now a pure recolor.

- calc_imex_ghosts bakes an alpha-only placeholder; IMEX_GHOST_ALPHA is
  hoisted to file scope as the single opacity authority (no more reading
  alpha back out of the field the restamp overwrites).
- build_imex_ghost_cache_key drops pem and the head-filament map; the
  forced invalidations in set/reset_imex_head_filament_map go with them.
  Picker selections now recolor live with no rebuild.
- The restamp moves into PartPlate::update_imex_ghost_colors(), beside the
  transform refresh, so PartPlate owns its volumes' colors and the canvas
  calls one hook. Plate-level inputs (pem, override map) are hoisted once
  per frame via a new get_imex_head_filament_color overload that the
  single-head form delegates to, keeping tooltip parity by construction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-14 16:39:14 -04:00
Clifford Garwood f41752546c Merge upstream main (multi-nozzle override fix, slice-all toolbar crash guard, filament_colour_type G-code skip) into IMEX branch 2026-08-14 16:00:26 -04:00
Clifford GarwoodandClaude Fable 5 bdbdd8e222 fix(imex): resolve ghost colors live at render time, not at bake time
Ghost GLVolumes baked their filament color once in calc_imex_ghosts and
only rebuilt when build_imex_ghost_cache_key changed. The key covers the
mesh-shaping inputs (mode topology, pem, object set, per-plate head map)
but not filament_colour, and no invalidation hook fires on filament
preset or color changes — so a ghost baked under a transient palette
(late-loading project colors, a subsequently edited filament color)
kept the stale color forever. A failed lookup at bake time baked
GLVolume::UNPRINTABLE_COLOR, which renders as a jet-black ghost while
the hover tooltip — which re-resolves live — shows the correct color.

Re-stamp each ghost's color in _render_imex_ghosts from
get_imex_head_filament_color, the same resolution the tooltip runs, so
the two can never disagree. Resolution is hoisted into a per-head map
so instances sharing a head resolve once per frame; the alpha baked by
calc_imex_ghosts is preserved. The render loop already stamps color per
ghost per frame (set_render_color + model.set_color), so the added cost
is a couple of map lookups. The cache key stays scoped to what it
actually protects: the expensive mesh bake and transforms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 07:45:50 -04:00
harrierpigeon 3d11b60e71 Harden belt purge tower replanning 2026-08-08 20:14:52 -05:00
Joseph Robertson e8597fe942 Merge pull request #61 from HarrierPigeon/belt/purgeTower
Add Purge Tower and Finalize Multicolor Support
2026-08-08 18:59:00 -05:00
harrierpigeon 99ee8893cd Fix belt purge tower activation and placement safety 2026-08-08 17:14:26 -05:00
harrierpigeon ee3e014f02 allow belt purge to skip unnecessary purge volume 2026-08-08 16:35:55 -05:00
harrierpigeon 3d270c2aa7 workable belt purge, via N-1 individual "purge objects" 2026-08-08 16:35:55 -05:00
harrierpigeon 5ea6ccc56a cleanup, early purge tower stop if no longer necessary 2026-08-08 16:35:55 -05:00
harrierpigeon bcfb09481c pull purge tower into its own files, make purge tower semi-transparent like other purge towers 2026-08-08 16:35:55 -05:00
harrierpigeon c80f1ab312 cancel top of purge tower early if no extra parts to print 2026-08-08 16:35:55 -05:00
harrierpigeon 131b61b726 auto purge tower height calculation works 2026-08-08 16:35:55 -05:00
harrierpigeon d367bcef92 extra height compensation 2026-08-08 16:35:55 -05:00
harrierpigeon 79c93733d3 purge tower additional compensation 2026-08-08 16:35:55 -05:00
harrierpigeon 7cc50d750c automated placement works 2026-08-08 16:35:55 -05:00
harrierpigeon 62d8f22f52 purge tower still centered on X max 2026-08-08 16:35:55 -05:00
harrierpigeon 60e9ee26c9 strategy incremental 2 2026-08-08 16:35:55 -05:00
harrierpigeon 854dae8dd2 strategy incremental 2026-08-08 16:35:54 -05:00
harrierpigeon ec4e9717d3 Part Two: Functional Results 2026-08-08 16:35:54 -05:00
harrierpigeon 88726d76e8 Purge tower part 1 2026-08-08 16:35:54 -05:00
Joseph Robertson 39b087d6ea fix non 45 degree slicing methods (#15181)
# Description
During the UI/UX improvements about a month ago, I got the transforms
wrong, and slicing at anything other than a 45 degree angle was
affected.

Validated on a baby belt pro at 30 & 45 degrees.


[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-08-08 12:37:17 -05:00
harrierpigeon 8b2e28817d fix non 45 degree slicing methods after regression created while cleaning up UI 2026-08-08 12:33:41 -05:00
Clifford GarwoodandClaude Opus 5 ec309e1cc6 fix(imex): apply the same-physical cool-down skip only on IMEX printers
set_extruder skips the ooze-prevention standby cool-down when the outgoing and
incoming filament route to the same physical extruder. That is an IMEX behaviour --
an AFC/MMU lane swap keeps the same heater selected -- but the check was not gated,
so it ran on every printer.

73 shipping profiles enable ooze_prevention by default (37 Snapmaker, 21 WonderMaker,
9 Flashforge, plus Lulzbot, Prusa, re3D, iQ and the MyToolChanger), and the 150
multi-nozzle machines behind them author no physical_extruder_map. They were spared
only because every one declares a single variant per extruder, so the map came out as
the identity and nothing was ever suppressed. Correctness should not rest on that.

Gate on is_imex. Verified on one fixture with only is_imex differing, with two
filaments mapped to the same physical extruder: 0 cool-downs emitted with it on, 26
with it off.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 12:20:52 -04:00
Clifford GarwoodandClaude Opus 5 df4009e734 fix(imex): size physical_extruder_map from the nozzle count
physical_extruder_map has one entry per logical extruder -- the index space of
nozzle_diameter -- and its consumers size their own arrays from that count. It was
being derived from printer_extruder_id, which is indexed by variant slot: one entry
per extruder+variant pair. An X1 Carbon has one nozzle and printer_extruder_id
{1,1}; an H2D 0.4 has two nozzles and {1,1,2,2,2}. The two spaces coincide only
when every extruder declares a single variant.

The visible effect was on the standby cool-down. set_extruder skips it when the
outgoing and incoming filaments share a physical extruder, and that check is not
gated on IMEX. With the map built from the wrong array, two filaments on a
dual-nozzle machine read as sharing one hotend and the cool-down was dropped --
caught by "Toolchange temperature commands are unchanged when the wipe tower wait
is off", which failed on all five CI platforms with the ;cooldown line missing.

Derive the identity over the nozzle count instead, the same fallback Plater.cpp
already applies where a profile authors no map. A profile counts as authoring one
only when its length matches the nozzle count, so the single-element PrintConfig
default is replaced rather than read as a one-extruder machine. Authored maps pass
through untouched, including the {1,0} numbering permutation the BBL dual-nozzle
profiles ship.

Tests pin the four branches and the length invariant the consumers depend on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 12:20:52 -04:00
Rodrigo Faselli 1a30c49993 Merge branch 'main' into main 2026-08-07 10:50:35 -03:00
Joseph Robertson ac9b5433b7 Belt printer: regression fixes + Belt Printer Brims (#15155) fixes (#15156)
## Summary

Fixes for the `belt-printer` branch ahead of upstreaming, in two groups
(10 commits). Targets `belt-printer` (not `main`) since group 2 fixes
the not-yet-merged Belt Printer Brims feature.

Every fix keeps non-belt (and brim-disabled) output unchanged; belt-only
behavior is corrected. All changed translation units and the two test
files were type-checked (`-fsyntax-only`); a full build + `ctest` still
needs to run in an environment with current deps.

## Group 1 — pre-existing belt-printer regressions

- **[HIGH] BuildVolume belt state not reset when leaving belt mode** —
toggling belt off (or switching belt→normal with matching bed geometry)
left the `BuildVolume` with `m_is_belt_printer=true` and inflated Y
bounds, so out-of-bounds objects were treated as printable on a normal
printer.
- **[HIGH] `GCodeProcessorResult::reset()` didn't clear belt fields**
(`belt_tilt_angle`, `belt_z_origin`, `preslice_remap_*`) — a reused
result corrupted a normal print's start-gcode preview Z.
- **[LOW-MED] `TreeSupport::drop_nodes`** — restored the single critical
section around node invalidation (the two `valid=false` writes had been
moved outside the mutex on the shared tree-support path); removed an
unused local.
- **[LOW] Support overhang hot paths** — avoid unconditional lower-layer
polygon copies when there is no build-plate tilt (`SupportMaterial`,
`TreeSupport3D`); untilted output matches upstream exactly.
- **[LOW] Render loop** — hoisted the frame-invariant slope
`up_direction`/`normal_z` (and their per-volume config lookup) out of
the per-volume loop.
- **[LOW] FDM-support "select by angle"** — restored the exact upstream
threshold when the build plate is untilted (the generalized form
differed for non-uniformly-scaled objects); tilted-gravity form kept
only under tilt.
- **[LOW] Printer tab tilt sync** — only clears the belt-derived
`build_plate_tilt` on a genuine in-place belt→off toggle (tracked,
seeded on preset load), no longer wiping a manually-set tilt.
- **[LOW / opt-in] Axis-remap G-code emission** — always emit full XYZ
under an active `gcode_remap_*`, apply the remap on all base
`travel_to_xyz` destinations, fall back to a linear lift for spiral/arc
under remap, sync `set_axis_remap` each export; fixed belt first-layer
travel speed. Identity/default output unchanged.

## Group 2 — Belt Printer Brims (#15155) fixes

- **[CRITICAL] Dropped brim at first belt contact** — a coincident brim
band on an object layer with no extrusion pass (zero-extrusion leading
slice, or belt support below the Z=0 floor with no coinciding object
extrusion) was never emitted. Now each coincident band's brim filament
is registered in `ToolOrdering`, each band is emitted exactly once in
its brim-filament pass, and an end-of-layer orphan sweep emits any band
whose object layer produced no visit.
- **[Multi-extruder] Wrong tool / double emission** — apron and
coincident bands now print once, in the correct brim-filament pass,
brim-first (were previously emitted with the active tool and could
double-emit per filament plan). Single-extruder / single-object output
is byte-identical apart from the previously-dropped bands now printing.
- **Inner-only predicate** — `has_belt_brim()` no longer reports a brim
(and no longer rejects the prime tower / spiral vase) for `inner_only` +
`brim_width=0` + leading/extra > 0, which produces no inner geometry;
mirrored in `wants_brim`.
- **ToolOrdering raft-gap comment** — clarified why raft-gap synthesis
is suppressed for all belt printers (belt has no rafts;
sub-object-bottom layers are apron / belt-support-below-floor /
lead-in). No behavior change.
- **Tests** — deterministic coverage: brim present at first belt contact
(support on/off), brim-before-perimeters once (no drop/double), single-
and multi-extruder tool selection with no doubling, multi-object
per-filament ordering, inner-only+leading-only not rejecting prime
tower/spiral, and inner-ring / leading-edge-only geometry units.

## Testing

- `-fsyntax-only` passes for all 16 changed source TUs + 2 test TUs
against this branch.
- Please run the full build and `ctest -R 'SkirtBrim|BeltBrim'` before
merging.

## Known follow-up (out of scope)

`extrude_arc_to_xy` does not remap its I-J center, so arc-fitted
*extrusions* under standalone axis-remap would be geometrically wrong —
a separate fix if that combination is supported.

Opened as **draft**.
2026-08-06 17:38:11 -05:00
Clifford Garwood 520cf3a415 Merge upstream main: prime tower on non-rectangular beds, toolchange temperature wait, ironing speed override, preset dialog QOL
# Conflicts:
#	src/libslic3r/GCode.cpp
#	src/slic3r/GUI/Tab.cpp
2026-08-06 17:07:02 -04:00
harrierpigeon a453cb1eba tests: cover belt-brim first-contact emission, tool selection, inner/leading-edge, and predicate (E)
Deterministic tests for: coincident brim at first belt contact not dropped
(C), single- and multi-extruder brim tool selection with no doubling (B),
multi-object apron ordering, inner-only+leading-only not rejecting prime
tower/spiral (D), and inner/holed + leading-edge-only geometry.
2026-08-06 15:40:04 -05:00
harrierpigeon 1bd3404c03 Fix belt brim emission: dropped first-contact bands, tool selection, inner-only predicate (A,B,C,D)
- Emit coincident belt_brim_by_layer bands even when the leading object layer
  has no InstanceVisit (zero-extrusion lead-in / no coinciding support), so the
  brim at first belt contact is no longer dropped.
- Register each coincident band's brim filament in ToolOrdering and emit each
  band exactly once, in its brim-filament pass; emit ordinary-layer aprons in
  the brim pass before object extrusion (correct tool, brim-first) instead of
  with whatever tool was active.
- has_belt_brim(): inner-only brims need brim_width>0 (leading/extra produce no
  inner geometry), fixing spurious prime-tower/spiral rejection; mirror in
  wants_brim. Single-extruder/single-object output is unchanged except
  previously-dropped bands now print.
2026-08-06 15:40:04 -05:00
harrierpigeon c1a90fc451 Fix: correct axis-remap G-code emission and belt first-layer travel speed (B2, B3)
- BeltGCodeWriter::travel_to_xyz final branch used config.travel_speed
  instead of the computed first-layer-aware travel_speed.
- extrude_to_xyz decided emit_xyz vs emit_xy from pre-remap Z; emit full
  XYZ whenever an axis remap is active so remapped machine-Z is never
  dropped.
- base travel_to_xyz now applies apply_axis_remap() on all emitted
  destinations (standalone remap on non-belt printers was unremapped).
- spiral/arc travels fall back to normal linear lift under active remap
  (endpoint-only remap can't preserve arc plane/I-J).
- set_axis_remap() is now synced unconditionally each export to avoid a
  reused writer retaining a stale non-identity mapping.
2026-08-06 14:26:45 -05:00
harrierpigeon 7fc86db5f0 Fix: only clear belt-derived build_plate_tilt on genuine belt->off transition (R8)
update_fff() zeroed any build_plate_tilt matching the dormant belt-derived
tilt (default X/45) within 0.01, wiping a legitimate manual tilt on a
non-belt tilted-bed printer. Track the belt->non-belt transition and the
exact values belt-sync wrote, clearing only those on an in-place toggle;
reset tracking on preset load so preset switches never wipe tilt.
2026-08-06 14:26:45 -05:00
harrierpigeon 6fd2de76e6 Fix: preserve upstream select-by-angle behavior when build plate is untilted (R7)
select_facets_by_angle replaced upstream's limit.dot(down) threshold with
cos(threshold), changing facet selection for non-uniformly-scaled/mirror
objects on ALL printers. Restore the exact upstream computation when no
build-plate tilt is active; keep the tilted-gravity form only under tilt.
2026-08-06 14:26:45 -05:00
harrierpigeon f8fe5a07cd Perf: hoist frame-invariant slope up_direction/normal_z out of the per-volume render loop (R6)
Belt slope-shading changes recomputed up_direction (with a printer-preset
config lookup) and normal_z per volume; both are frame-invariant. Compute
once before the to_render loop and reuse the already-hoisted
support_normal_z. Uniforms are still set per volume; visuals unchanged.
2026-08-06 14:26:45 -05:00
harrierpigeon 049612022a Perf: avoid unconditional lower-layer polygon copies in support overhang paths (R4, R5)
SupportMaterial::detect_overhangs copied lower_layer_polygons per region
even without build-plate tilt; hoist the tilted copy out of the region
loop and use the original polygons directly when untilted. TreeSupport3D
flattened lslices_extrudable to Polygons unconditionally; restore the
upstream ExPolygons offset on the untilted path.
2026-08-06 14:26:45 -05:00
harrierpigeon 10810908a9 Fix: restore atomic node invalidation in TreeSupport::drop_nodes + drop unused var (R3, R9)
The 2-node merge moved the two valid=false writes outside the mutex that
upstream held together with the contact_nodes push_back; restore a single
critical section per branch (belt branch also guards to_buildplate).
Remove an unused top_interface_layers local in drop_nodes.
2026-08-06 14:26:45 -05:00
harrierpigeon 0d92180325 Fix: clear belt fields in GCodeProcessorResult::reset() (R2)
reset() cleared the sibling machine_frame_transform_active but not
belt_tilt_angle/belt_z_origin/preslice_remap_*; a reused result carried
stale belt metadata into a subsequent normal print, flipping the store_z
branch and corrupting start-gcode preview Z for non-belt prints.
2026-08-06 14:26:45 -05:00
harrierpigeon 88d5e9e442 Fix: reset BuildVolume belt state when leaving belt mode (R1)
Non-belt branch of set_bed_shape reset only the 3DBed renderer, not the
BuildVolume; Bed3D::set_shape early-returns on unchanged bed, so a
belt->normal switch or in-place belt toggle-off left the BuildVolume with
m_is_belt_printer=true and inflated Y bounds -> out-of-bounds objects
treated as printable on a normal printer.
2026-08-06 14:26:45 -05:00
Joseph Robertson c51d19f6b2 Add Belt Printer Brims (#15155)
# Description

This adds brim support to belt printers.

Added a new belt printer specific mode, Leading Edge Only and two new
belt-specific parameters, Leading Edge Brim Length, which increases the
number of brim lines on the side of the part printed first, and Extra
Brim Width, which increases the width of brims along the X axis. Because
belt printer first layers are effectively a single line, getting them to
stick properly can be a pain. This PR aims to help alleviate that, or at
least give more options for control.


<img width="1849" height="1043" alt="Screenshot from 2026-08-06
12-20-21"
src="https://github.com/user-attachments/assets/f963ed8e-53e7-48f8-a495-123cb9ae27f7"
/>



[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-08-06 14:18:21 -05:00
harrierpigeon 849e6493f8 Belt brim: offer "Leading edge only" only on belt printers
"Leading edge only" describes where a part meets a moving belt, so it has no
meaning on a fixed bed and should not clutter the Brim type dropdown there.

Filtered the same way support_style and wipe_tower_wall_type already are a few
lines above in TabPrint::toggle_options(): the field holds its own copy of the
option definition, and Choice maps the combobox selection straight onto that
copy's enum_values, so rewriting the values, the labels and the combobox items
together keeps the mapping correct.

The entry is kept when it is the current value, so opening a project that uses
it on a non-belt printer cannot leave the control displaying an option it does
not offer - which would silently rewrite the setting on the next edit.
Print::validate() already warns that it prints as an ordinary outer brim there.

Matches the scope of the existing precedents: the per-object override panel is
not filtered.
2026-08-06 03:54:19 -05:00
harrierpigeon b1905ebc20 Belt brim: fixes from review
Six issues found by reviewing the previous commit against belt-printer, two of
them release-blocking.

Data race (high).  Print::process() runs generate_support_material() for all
objects in a tbb::parallel_for, and make_belt_brim() runs at its tail, but
belt_brim_obstacles() read every OTHER object's support_layers() - which a
concurrent task may be inside clear_support_layers() deleting.  That is a
use-after-free, and even when it survives, the obstacle set depends on which
object finishes first.  Only this object's own supports are consulted now; they
are complete at that point.  Foreign objects still contribute their slices,
which are finished and immutable before the support phase.

Apron bands dropped (high), two separate causes.  An apron band prints below
its own object's first layer, but another object can already be printing at
that print_z, in which case process_layer() takes the ordinary path and never
emitted the band - the emission is now shared by both paths.  Separately, a
band whose print_z matched a support layer of the SAME object was overwritten
in the print-wide merge, which keeps one record per object per z and could not
detect the collision because LayerToPrint::layer() is null for a band.  The
per-object pairing loop is now a three-way merge over object, support and apron
streams, so each object contributes at most one record per z.

Multi-instance was far too strict (medium).  It refused belt brim for every
multi-instance object, killing plain brim width and inner brim too, and only
warned when a leading length was set.  Only movement ALONG the belt changes an
instance's belt-floor Z, so copies side by side ACROSS the belt share one set of
bands perfectly well; belt_brim_instances_compatible() now tests just that, and
the warning fires whenever the brim is actually suppressed.

Apron layer bookkeeping (medium).  Apron layers count toward m_layer_count and
advance m_layer_index, but emitted no Z/height tags, left m_last_layer_z,
m_max_layer_z and m_last_height stale - so the first object layer computed its
height against a pre-apron Z - and skipped before_layer_change_gcode and
layer_change_gcode entirely.  All of that now matches the ordinary path.

Obstacle cost (low).  belt_brim_obstacles() ran a full-plate union per band.
A bounding-box pre-filter drops non-overlapping objects before materialising any
polygon, and the union is skipped for trivial inputs.

Deliberately unchanged: every apron band still reports cooling layer_id 0.
CoolingBuffer uses it for the initial_layer_fan_speed override and the
close_fan_the_first_x_layers gate, and every band lies on the belt plane itself,
so it is all first-layer material by the only definition that means anything on
a belt.  Numbering the bands would ramp the fan up while still printing on the
belt.  Now documented at the assignment rather than left implicit.
2026-08-06 01:08:44 -05:00
harrierpigeon 55b4dca9bc Belt printers: brim laid onto the tilted belt, with a leading apron
A belt printer slices in a rotated frame, so the belt surface is a tilted
plane rather than the Z=0 bed plane.  Each slicing layer touches the belt
only along a narrow strip at its leading edge - about 0.2mm at 45 degrees -
so a part's first layer is really a first line, with almost no contact patch
to hold it down while the belt drags it forward.  Brim was hard-disabled on
belt printers, leaving no remedy at all.

Generate the brim on the belt plane instead.  The object's belt footprint is
the union over layers of each slice clipped to that layer's contact band; the
brim is offset from it in a "flattened" frame where the shear axis is
stretched by 1/cos(tilt), so ordinary Clipper offsets measure true on-belt
distance.  It is emitted as cross-belt lines, one per layer band, anchored to
a fixed fraction of the band so every line shares a nozzle-to-belt clearance
and therefore comes out the same width; flow is matched to the resulting band
pitch, keeping the sheet uniform and gap-free.

Three new controls, all belt-only:

  * Leading brim length - extends the brim ahead of the part along the belt,
    on every downhill-facing edge of its contact area.  This apron necessarily
    prints BELOW the object's first layer, since layer 0 is the part's leading
    contact, so it needs brim-only bands of its own.
  * Extra brim width - widens the brim sideways across the belt only.
  * Brim type "Leading edge only" - brim at the part's first belt contact and
    nothing after it.  Appended last in BrimType so no existing value shifts;
    degrades to an outer brim off belt printers, with a warning.

The apron bands are lightweight records rather than a Layer subclass, so no
fabricated Layer::id() can leak into initial-layer temperature selection, the
spiral vase probe, cooling or gradual interpolation.  They are generated in
posSupportMaterial because their print_z values must exist before ToolOrdering
is built at psWipeTower, and they are emitted from a short dedicated branch in
process_layer that runs before any layer pointer is dereferenced.

The footprint is closed before offsetting outwards: a belt contact patch is
often a broken-up strip, and the merged offset rings of two islands closer
than 2 x brim_width would otherwise fill the space between them - space that
lies under the part.

Also fixes a pre-existing bug where PrintObject::get_first_layer_bbox()
overwrote a valid bbox with an unassigned one on any belt printer with a brim
configured, because has_brim() was true while make_brim() returned early.

Belt brim is refused alongside the prime tower and spiral vase, and requires
one instance per PrintObject - translating an instance along the belt axis
changes its physical belt-floor Z.  Untilted belt printers are unchanged: they
still get no brim, since the plate brim is emitted out of skirt_brim_groups(),
which _make_skirt() never builds for a belt printer.
2026-08-06 01:08:44 -05:00
Joseph Robertson 386364f84b belt profiles: fix belt printer CI failures (slice check + setting_id) (#15127)
The belt-printer branch is failing two profile gates. Both stem from the
three belt-only vendors (Custom's generic belt printer, IdeaFormer,
Printcepts) not existing upstream, so upstream maintenance passed them
by.

Slice check: 4 of 1015 printers failed - Custom's MyBeltPrinter 0.2/0.4/
0.6/0.8 nozzle all fell back to "Default Setting". No process profile in
the Custom vendor listed any MyBeltPrinter in compatible_printers, and
Custom's fdm_belt_common pointed default_print_profile at "0.20mm
Standard @System", which does not exist in that vendor's index, so the
generic belt printer had no usable process at all. This gap dates to
when MyBeltPrinter was added (2026-04-07); it only started failing now
because the slice-check job is newer than that.

Adds two process profiles modelled on the sibling @MyKlipper ones:
  - 0.20mm Standard @MyBeltPrinter - 0.4/0.6/0.8 nozzles
  - 0.12mm Fine @MyBeltPrinter     - 0.2/0.4 nozzles
The split is forced by hardware: the 0.2 nozzle preset caps
max_layer_height at 0.16, so a single 0.20mm profile cannot legally
cover
it. fdm_belt_common now defaults to the standard profile and the 0.2
nozzle preset overrides to the fine one.

setting_id: 14 files failed the rules introduced in #14432. That
migration renumbered 7425 files across 61 vendors but skipped these
three, leaving BabyBelt Pro, IdeaFormer IR3 V2 and MyBeltPrinter
squatting the "G*" id space reserved for Bambu (GMPC0BBP01, GMIF001,
GM_BELT_00x) and four instantiated filament/process presets carrying no
setting_id at all. Regenerated with
scripts/assign_vendor_setting_ids.py.

Also repoints the identical dangling "0.20mm Standard @System" in
Printcepts' and IdeaFormer's fdm_belt_common at their own real process
profiles. That is a no-op today because both concrete printers override
it, but it is the same landmine that took out MyBeltPrinter.

Vendor index versions bumped so check_installed_vendor_profiles() will
re-install the corrected profiles over an existing install.

Note: changing a shipped preset's setting_id can orphan user presets
that reference it as base_id. #14432 accepted that tradeoff for 61
vendors; this keeps these three consistent with the rest.

Verified: orca_extra_profile_check.py reports 0 errors across 66 vendors
(was 14 files with errors), and OrcaSlicer_profile_validator -s slices
all 1015 printer presets successfully (was 4 failures).


[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-08-04 17:16:14 -05:00
harrierpigeon 725df64108 profiles: fix belt printer CI failures (slice check + setting_id)
The belt-printer branch is failing two profile gates. Both stem from the
three belt-only vendors (Custom's generic belt printer, IdeaFormer,
Printcepts) not existing upstream, so upstream maintenance passed them by.

Slice check: 4 of 1015 printers failed - Custom's MyBeltPrinter 0.2/0.4/
0.6/0.8 nozzle all fell back to "Default Setting". No process profile in
the Custom vendor listed any MyBeltPrinter in compatible_printers, and
Custom's fdm_belt_common pointed default_print_profile at
"0.20mm Standard @System", which does not exist in that vendor's index,
so the generic belt printer had no usable process at all. This gap dates
to when MyBeltPrinter was added (2026-04-07); it only started failing now
because the slice-check job is newer than that.

Adds two process profiles modelled on the sibling @MyKlipper ones:
  - 0.20mm Standard @MyBeltPrinter - 0.4/0.6/0.8 nozzles
  - 0.12mm Fine @MyBeltPrinter     - 0.2/0.4 nozzles
The split is forced by hardware: the 0.2 nozzle preset caps
max_layer_height at 0.16, so a single 0.20mm profile cannot legally cover
it. fdm_belt_common now defaults to the standard profile and the 0.2
nozzle preset overrides to the fine one.

setting_id: 14 files failed the rules introduced in #14432. That
migration renumbered 7425 files across 61 vendors but skipped these three,
leaving BabyBelt Pro, IdeaFormer IR3 V2 and MyBeltPrinter squatting the
"G*" id space reserved for Bambu (GMPC0BBP01, GMIF001, GM_BELT_00x) and
four instantiated filament/process presets carrying no setting_id at all.
Regenerated with scripts/assign_vendor_setting_ids.py.

Also repoints the identical dangling "0.20mm Standard @System" in
Printcepts' and IdeaFormer's fdm_belt_common at their own real process
profiles. That is a no-op today because both concrete printers override
it, but it is the same landmine that took out MyBeltPrinter.

Vendor index versions bumped so check_installed_vendor_profiles() will
re-install the corrected profiles over an existing install.

Note: changing a shipped preset's setting_id can orphan user presets that
reference it as base_id. #14432 accepted that tradeoff for 61 vendors;
this keeps these three consistent with the rest.

Verified: orca_extra_profile_check.py reports 0 errors across 66 vendors
(was 14 files with errors), and OrcaSlicer_profile_validator -s slices all
1015 printer presets successfully (was 4 failures).
2026-08-04 17:15:20 -05:00
Clifford Garwood 7541d6a1e6 Merge upstream main: printer-specific OrcaFilamentLibrary profiles, convex_hull_2d test replacement
# Conflicts:
#	tests/libslic3r/test_3mf.cpp
2026-08-03 15:17:00 -04:00
Clifford Garwood 612af109f5 Merge upstream main: outer-only brim ears, Hilbert infill smooth factor, ImGui mouse capture and gizmo fixes, Wayland splash and Linux control styling, 32-bit and warning cleanups 2026-08-03 10:58:46 -04:00
Clifford GarwoodandClaude Opus 5 5e38877ba1 refactor(imex): use IMEX consistently in user-facing strings
Labels, tooltips, menu items and dialog text mixed "IDEX/IQEX" with "IMEX"
for the same feature. IMEX is now the user-facing name throughout: IDEX and
IQEX are hardware categories, IMEX is the feature spanning them. The
hardware terms remain only where they help a user tell whether the feature
applies to their printer.

Comments and log messages keep IDEX/IQEX, where the specific carriage
topology is the more precise term.

Also corrects the imex_parallel_mode tooltip, which pointed at a
"Printer -> IDEX/IQEX tab" that does not exist; the options live under
Printer -> Multimaterial -> IMEX Configuration.

None of these strings appear in any .po or the .pot, so no translation is
affected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 08:26:38 -04:00
Clifford GarwoodandClaude Opus 5 3852f4be61 fix(imex): block slicing when the prime tower overlaps a parallel-printing zone
IMEX placement validation only walked model instances. The prime tower is
not a ModelObject, so it could sit in a secondary zone or a carriage
collision strip and slice with no warning -- on mirror mode, a carriage
crash. Span (paired-gantry multicolor) is what made towers reachable in
parallel modes, so this is a gap in that feature, not inherited breakage.

The check now returns a cause instead of a bool so the message can name the
offender, and the tower and per-instance paths share one predicate,
imex_hull_violates_zones(), moved to libslic3r and covered by tests.
Overlap is area-based: a hull flush against a zone boundary is legal, only
a crossing violates. The tests pin that in both directions, since switching
to a touch-based test would silently block placements that work today.

The tower footprint comes from the same estimate the scene draws, so
validation matches what the user sees and drags. Three config reads there
are load-bearing in non-obvious ways and are commented at the point of use.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 08:26:38 -04:00
Joseph Robertson c5bf238859 Update Belt-Printer Branch (#15087)
gets belt-printer on top of upstream again.
2026-08-03 02:09:41 -05:00
harrierpigeon f563df04f6 belt: default first_layer_plane to Auto, not BeltAffine
BeltAffine activates the FirstLayerPlane evaluator unconditionally, so on a
non-belt printer on_first_layer(point) stopped agreeing with the legacy
slicing-layer-0 test. Every per-path first-layer call site in _extrude then
took the non-first-layer branch, and first-layer speeds were skipped: brim
came out at the volumetric fallback (24.6 mm/s) instead of initial_layer_speed
(10 mm/s). This is the shared speed path, so it affected all printers on this
branch, not just belt ones.

Auto resolves to BeltAffine only when belt_printer is set with a non-zero
slicing rotation, and to XY (evaluator inactive, legacy behaviour) otherwise --
exactly what the option's own description already promised.

Caught by "Brim uses first layer speed" (upstream #14616), which arrived with
the upstream merge; the bad default dates back to a9bae54f20 (#30). Verified
against a pristine upstream/main build, which passes the same test.

tests/fff_print: 100/100 test cases, 1085 assertions (was 99/100).
Both belt regression tests still pass, confirming Auto still resolves to
BeltAffine for belt printers.

Note: this changes a config default. Projects and profiles that stored
first_layer_plane explicitly are unaffected; those relying on the default will
now get correct first-layer speeds on non-belt printers, so their G-code
changes accordingly.
2026-08-03 01:52:43 -05:00
harrierpigeon 613dad92a1 Add belt-printer regression test for prepare-stage move Z
Processes a minimal belt start sequence through GCodeProcessor::process_buffer
and asserts the move preceding the first extrusion keeps its real Z, so it can
no longer back-transform to model Y~=0 and produce the phantom extrusion line.

Belt printers are non-Bambu, so the processor uses the compatible reserved
tags ("TYPE:"); the test sets s_IsBBLPrinter=false (saved/restored via an RAII
guard) to mirror the real printer. Proven to fail without the fix (the
prepare-stage move's Z is pinned to the first-layer height, 0 here) and pass
with it.
2026-08-03 01:18:55 -05:00
harrierpigeon a83cd8aa29 Fix belt printer phantom extrusion line from Y=0 in preview
On a belt printer the sliced preview drew a stray extrusion-colored line
from Y~=0 to the model, rendered in the first extrusion role's color. It is
not a travel and does not occur on non-belt printers.

GCodeProcessor::store_move_vertex pins a move's stored Z to the first-layer
height during the start-G-code "prepare" stage. That is a harmless cosmetic
tidy-up on a normal printer, but on a belt printer the designed-view
back-transform couples machine Z into the rendered model Y (the belt tilt
mixes the height and belt-feed axes). Pinning Z back-transforms the last
prepare-stage move (the unretract before the first extrusion) to model
Y ~= 0, and libvgcode then draws a phantom extrusion segment from Y ~= 0 to
the first real toolpath.

Keep the real Z for belt printers (gated on belt_tilt_angle, parsed from the
G-code header before the body) so prepare-stage moves back-transform
correctly. Non-belt processing is byte-identical. The emitted G-code was
already correct; this is a preview-geometry fix.
2026-08-03 01:09:23 -05:00
harrierpigeon 02e313a115 Add belt-printer regression test for start-of-print gantry move
Locks in the fix from the previous commit. A fresh BeltGCodeWriter has an
unestablished planar position (is_current_position_clear() == false) and its
m_pos.xy is the origin (0,0). With a pending NormalLift z-hop, travel_to_xyz
used to lift in place via _travel_to_z(), which in belt mode shears the origin
into a machine Y ~= the layer Z — a move far up the gantry.

The test configures an X-tilt 45 deg belt transform, defers a z-hop via
lazy_lift, travels to a near-belt first point (transformed gantry Y ~= 1mm),
and asserts no emitted move has Y anywhere near the layer Z. Verified to fail
without the fix (max emitted Y = 100.0 vs the destination's ~1.0) and pass with
it.
2026-08-03 01:09:11 -05:00
harrierpigeon 04554abae6 Fix belt printer illegal gantry move at print start
On a belt printer the first travel of the print emitted a bogus move to
the bed corner with the nozzle far up the gantry, e.g.
  G1 X95 Y168.19 Z237.857 F12000
right after the first "; printing object" line. Y168 (≈ the layer Z)
is out of the gantry's range.

Root cause: the layer-change z-hop is deferred via lazy_lift and consumed
by the first BeltGCodeWriter::travel_to_xyz, whose NormalLift branch does a
separate lift-in-place via _travel_to_z(target.z()). On a normal printer
_travel_to_z emits a Z-only move, but in belt mode Z is coupled to Y/X, so
_travel_to_z re-emits the current m_pos through the belt shear. At print
start (and after custom gcode) m_pos.xy is still the uninitialised origin
(0,0), which the back-transform + axis-remap shear into machine
(X=bed_max, Y=layer_z) — the illegal move.

Guard the NormalLift branch on is_current_position_clear(), matching the
SlopeLift branch directly above it which already does so. When the position
isn't established there is nothing to lift over, and the xy_z_move that
follows travels straight to the destination with full XYZ, establishing the
correct position. Bookkeeping is unaffected: in this path m_lifted stays 0,
so no spurious restore move is produced.

Verified by re-slicing the repro project: the start-of-print move is now
G1 X44.946 Y.621 Z237.857 (straight to the first object point), no move
touches the bed-max X edge, and the max Y over the whole file is 62.8mm
(printable_height 100).
2026-08-03 00:15:24 -05:00
HarrierPigeon 0342e06d87 last step in fixing the g-code stuff up 2026-08-02 22:13:34 -05:00
HarrierPigeon 79fd847ce3 fix pre-slice warnings 2026-08-02 22:12:46 -05:00
HarrierPigeon 8f6802fff8 step one: post-process analysis 2026-08-02 22:12:09 -05:00
harrierpigeon b61ba98183 belt: adapt BeltGCodeWriter to upstream's per-extruder speed options
Upstream retyped travel_speed and travel_speed_z to ConfigOptionFloatsNullable
and initial_layer_travel_speed to ConfigOptionFloatsOrPercentsNullable, so the
scalar .value / get_abs_value() accessors no longer compile. BeltGCodeWriter.cpp
is belt-only and merged without conflict, so this only surfaced at build time.

Index them the way the base GCodeWriter does -- .get_at(m_cached_extruder_idx)
and get_abs_value_at(..., m_cached_extruder_idx) -- keeping belt's per-point
first_layer_for_point test rather than the base class's m_is_first_layer.

m_cached_extruder_idx moves from private to the existing protected block that
already exposes writer state to subclasses, so the belt writer resolves the
per-extruder index identically to the base writer instead of guessing one.
2026-08-02 16:20:22 -05:00
harrierpigeon 175075fd08 Merge upstream/main into belt-printer
Brings the belt-printer work up to date with 591 upstream commits.

Conflict resolutions (12 files, 42 hunks):

- GCode.cpp: adopted upstream's per-filament/per-nozzle config refactor
  (get_filament_config_index, NOZZLE_CONFIG), the extracted
  generate_timelapse_gcode + farthest-point timelapse, and the
  ConfigOptionFloatsNullable calibration options. Re-applied the belt
  hooks on top: init_belt_writer / axis remap / FirstLayerPlane setup,
  on_set_origin, the belt-corrected calib_z for the volumetric speed
  tower, and path_on_first_layer (belt's per-path first-layer test) in
  place of upstream's layer-index on_first_layer() in the acceleration,
  jerk and overhang-detection paths. Swept upstream's new m_writer.
  uses to m_writer-> since belt holds the writer by unique_ptr.
- interpolate_value_across_layers: kept upstream's banded stepping and
  belt's object-Z-span ratio; dropped upstream's duplicate ratio decl.
- Plater.cpp: took upstream's guarded add_model(...) early-returns and
  the VFA vfa_layer_height plumbing; kept the belt temp-tower path,
  _calib_apply_belt_mode and belt_calib_flip_ringing_tower. Dropped the
  VFA "cut upper" block, superseded upstream by model scaling.
- Brim.cpp: upstream's ObjectInstanceID-keyed brimAreaMap, keeping the
  belt early-return.
- 3DScene.cpp: kept both the belt build-plate tilt up_direction and
  upstream's per-extruder printable-height shading.
- GCodeViewer.cpp: kept upstream's dim-previous-layers setup and belt's
  exemption from the same-result early return.
- TreeSupport.cpp: upstream's >= 0 roof-layer fix inside belt's
  belt-floor branch.
- calib.cpp / GCode.hpp / GCodeWriter.{cpp,hpp} / Print.hpp: upstream's
  additions adapted to belt's pointer-held writer and helpers.
- Custom.json: kept profile version 02.04.00.03 (belt) over upstream's
  02.04.00.01; both bumped from 02.04.00.00.

Building this tree needs the wxInspector dependency, which upstream
added in the interim (python3 and wxWidgets 3.3.2 were already present
in the shared deps prefix).
2026-08-02 16:09:27 -05:00
Clifford Garwood 3315b88012 Merge upstream main: rib wall prime tower, wipe tower sync, multi-extruder config fixes, plugin resolution fixes 2026-07-31 15:09:53 -04:00
Clifford Garwood bbcd2e2a80 Merge upstream main: cyclic print ordering, brim overlap fix, perimeter and support fixes 2026-07-31 01:42:48 -04:00
Clifford GarwoodandClaude Opus 5 4995a59685 fix(tests): restore SCENARIO closing brace lost in upstream merge
The merge of upstream main into the PR branch resolved the conflict in
test_gcodewriter.cpp by keeping both sides but dropping the closing brace
of the set_pressure_advance BBL SCENARIO. That nested the incoming
TEST_CASE inside it and broke the build on every target that compiles the
test suite:

  test_gcodewriter.cpp:1087: error: namespaces can only be defined in
  global or namespace scope

Close the SCENARIO before the TEST_CASE. Both tests are preserved and no
test logic changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 23:34:13 -04:00
Rodrigo Faselli fccd9d52ce Merge branch 'main' into main 2026-07-25 19:19:07 -03:00
Clifford GarwoodandClaude Opus 4.8 f74b8177af fix(imex): repaint plate-selector icon when a plate's slice-ready state flips
The big upstream merge replaced the old per-idle unconditional plate-selector toolbar
refresh with a dirty flag (Plater::mark_plate_toolbar_image_dirty), which the
geometry-change sites already set. PartPlate::update_slice_result_valid_state flips a
plate's slice-ready / IMEX blocked-plate ("naughty plate") state without any geometry
change and did not set the flag, so after the merge nothing repaints that plate's
thumbnail or warning badge — the old per-idle refresh used to mask it. Mark the toolbar
image dirty when the state actually changes, matching the established pattern.

Cosmetic; no effect on slicing. Surfaced by the merge review of 58b4a68a10.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 19:10:08 -04:00
Clifford Garwood 58b4a68a10 Merge upstream main: Python plugins, Bambu device rework, macOS fixes
Resolved 4 conflicts:
- PrintConfig.hpp / Preset.cpp: upstream restored calib_flowrate_topinfill_special_order
  (removed in the prior merge, now re-added and re-registered); kept it alongside our
  IMEX options (imex_parallel_mode, imex_head_filament_map).
- Plater.cpp: kept our <sstream> include plus upstream's <optional> and plugin includes.
- tests/fff_print/test_gcodewriter.cpp: both sides appended disjoint scenarios after a
  shared base. Reconstructed as upstream's full file (base + its 3 toolchange/H2C
  scenarios + the boost/filesystem include its helper needs) followed by our 10
  set_pressure_advance / set_temperature scenarios. 22 scenarios total, no duplicates.
2026-07-19 18:33:58 -04:00
Clifford Garwood 061190558f Merge upstream main: surface fill order, retract-after-wipe, i18n
Resolved conflicts in PrintConfig.hpp and Preset.cpp. Both were adjacent to upstream's
removal of calib_flowrate_topinfill_special_order (superseded by the new
top/bottom_surface_fill_order options); took upstream's removal from the option class
and the print-options list while keeping our new IMEX options (imex_parallel_mode,
imex_head_filament_map) and the IMEX enums (ImexToolLayout, ImexVizTheme) alongside
upstream's new SurfaceFillOrder enum. The option remains in PrintConfig's legacy
ignore-set so old projects still load.
2026-07-15 01:45:44 -04:00
Clifford GarwoodandClaude Opus 4.8 90e8ff7cee fix(imex): align the preview marker grid with the plate zone grid
Follow-up to the cross-gantry mirror axis change, from an adversarial review of it.

GCodeViewer builds its own copy of the zone grid to place the sequential-preview
carriage markers, and it must reproduce PartPlate::calc_imex_zones exactly or the
markers drift away from the ghosts they are meant to track. It did not, in two ways,
because sizing the grid and placing a cell answer different questions:

  - Sizing: calc_imex_zones counts every Copy/Mirror tool's OWN column, including the
    non-representatives of an aggregated (Span) gantry -- they still donate a column.
    GCodeViewer only ever saw the representative, so on an aggregated gantry it could
    count fewer columns than the plate and lay its markers out against wider strips.

  - Placement: calc_imex_zones PINS an aggregated cell to the primary's column, because
    that row-strip spans the full bed and has no column of its own. GCodeViewer used the
    representative's own column, which put the marker a strip away from the ghost
    whenever the representative was not column-paired with the primary.

Track the two sets separately: grid_tool_ids sizes the grid from own columns, eff_col_of
pins only aggregated tools when placing. Out-of-grid tool indices are deliberately left
unfiltered -- calc_imex_zones drops them while calc_imex_ghosts keeps them, so no policy
here can agree with both, and a comment says so rather than pretending otherwise.

Also:
  - The mirror-axis rule lived in three copies (two PartPlate lambdas plus an inline
    re-derivation here). Hoist it to imex_mirror_axis_for() so the ghosts and the markers
    cannot drift apart, and unit-test it, including degenerate tools_per_gantry.
  - Drop imex_head_transform's mirror_axis default. A defaulted axis silently hands a
    forgetful caller the X reflection, which is wrong for every cross-gantry tool and
    fails silently -- exactly how a stale test kept certifying the old rule.
  - Replace that stale test: it asserted a diagonal mirror "flips X only", the rule this
    work overturned, and stayed green only because of the default.
  - A secondary sharing the primary's gantry now takes the primary's Y box facing. The
    box shows the side a tool could be hit from, and two tools on one beam can only be
    hit by the same other gantry. No-op on the rear-* layouts, where the hardcoded value
    already matched; on front-* layouts it pointed the box away from the only tools that
    could reach it.
  - Correct two comments that described the aggregated X-frame substitution as a
    reflection plane. It is not one: it exists to zero gantry_offset.x, and removing it
    would push aggregated ghosts a column off their strip.

Verified: 385/385 tests; CLI slice of the IMEX regression project is byte-identical to
the previous commit's G-code apart from the timestamp, confirming this is
visualization-only and cannot affect sliced output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 10:01:09 -04:00
Clifford GarwoodandClaude Opus 4.8 0cb1788b23 fix(tests): use ASCII hyphens in IMEX test names so Windows can run them
catch_discover_tests registers each Catch2 test with ctest by name, and ctest then
re-invokes the binary passing that name back as a -# filter. The IMEX test names
contain em dashes (U+2014). They survive discovery, but on Windows the round trip
through the console codepage mangles them, so the filter matches nothing:

    Filters: "imex_head_transform G-- copy mode is pure translation"
    No test cases matched  ->  No tests ran  ->  exit != 0  ->  ctest: Failed

All 85 IMEX tests were reported as failures on both Windows x64 and arm64 without a
single one of them ever executing. Linux and macOS are UTF-8 end to end and were
unaffected, which is why this went unnoticed since the names were introduced in
77c32a2e15.

These were the only non-ASCII test names in the whole tests/ tree. Renaming them to
plain hyphens costs nothing and keeps the suite portable.

Test names only -- no assertion, no logic, no comment is touched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 10:00:50 -04:00
Clifford Garwood 11b894dc85 Merge upstream main (wipe tower height initialization fix) 2026-07-14 00:57:31 -04:00
Clifford Garwood f850912123 fix(imex): mirror across the gantry-row axis on two-gantry printers
A Mirror tool reflects across the boundary it shares with the primary's zone, and
which boundary that is depends on where the tool sits:

  - Same gantry: the tools are side by side along X, so the shared boundary is
    vertical and the reflection negates X. This is what single-gantry IDEX does,
    and it was the only case the code modelled.
  - Different gantry: the zones are stacked along Y (front strip vs rear strip),
    so the shared boundary is horizontal and the reflection negates Y. The part
    that comes off gantry 1 is a Y-reflection of the tool directly behind it.

imex_head_transform() hardcoded diag(-1, 1, 1) for every mirror, as its own TODO
acknowledged. Lift the axis to a caller-supplied ImexMirrorAxis; PartPlate picks it
from the tool's gantry row. Both reflections keep det = -1, so a mirrored part stays
a true mirror image rather than a 180-degree rotation, which would print the
primary's part merely turned around.

The correct axis removes two workarounds. Both ghost paths special-cased aggregated
mirrors to "drop the X reflection, translate 1:1 and bake the flip into the mesh"
because reflecting X pushed the ghost off-bed as the primary was dragged. With a Y
reflection the X translation is already zero for aggregated tools, so that falls out
for free and the special cases are deleted.

Preview markers follow the same rule, which also fixes two placement bugs:

  - Mirrors reflected across a Copy tool's zone edge, falling back to the primary's
    column when a row had no Copy. In iq-mirror (0:P,1:C,2:M,3:M) the front row has
    no Copy, so t2 and t3 both fell back and computed the identical X — both drawn
    on top of each other in t3's zone. A mirror now reflects within its own zone.
  - The toolhead footprint box flipped to the far side of the nozzle for any mirror
    right of the primary. That only holds for an X-axis mirror, which reverses the
    carriage's orientation; a cross-gantry mirror keeps the X orientation of the
    tool behind it, so its box stays on the same side.

Tests cover the cross-gantry and diagonal cases, that the axis is caller-supplied
rather than inferred from the offset vector, and that both axes are reflections
(det = -1) rather than rotations.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 00:57:31 -04:00
Clifford Garwood 7e272f0303 fix(print): keep derived physical_extruder_map in the original-config snapshot
Print::apply() derives physical_extruder_map from printer_extruder_id and writes
it into new_full_config, but does so after m_ori_full_print_config is snapshotted.
The selector write-back path rebuilds m_full_print_config from that snapshot, so
m_full_print_config kept the unexpanded default while every subsequent apply
re-derived the expanded map. An unchanged config therefore diffed on
physical_extruder_map on every re-apply, and since that key is not handled by
invalidate_state_by_config_options() it fell through to the catch-all and
invalidated every step — forcing a full reslice on each apply.

Mirror the derived map into the snapshot so the two stay consistent.

Caught by the upstream test 'Selector write-back expands migrating filaments and
survives re-apply', which asserts a re-applied unchanged config is not
APPLY_STATUS_INVALIDATED.
2026-07-13 16:22:28 -04:00
Clifford Garwood fdb0e20f36 Merge upstream main (Bambu H2C/A2L multi-nozzle support) into IMEX branch
Conflicts were all co-located additions rather than design collisions:
- GCode.cpp: adopt upstream's toolchange(filament_id, nozzle_id) signature and
  per-variant set_config_index() while keeping the IMEX bare-T<n> suppression;
  rebase the second-layer temperature loop's non-IMEX branch onto upstream's
  get_filament_config_index() resolution.
- Preset.cpp / PresetBundle.cpp / PrintConfig.cpp: keep both sides' option-list
  and enum-map entries.
- GLCanvas3D.cpp: upstream's printable_heights argument plus the IMEX ghost pass.
- PartPlate.cpp: keep <set> (still used).
- test_gcodewriter.cpp / test_3mf.cpp: keep both sides' test cases.
2026-07-13 16:22:22 -04:00
Clifford Garwood 0ba09780d1 Merge upstream main into IMEX parallel printing branch
Resolves two conflicts:
- GLCanvas3D.hpp: keep both the IMEX ghost render declarations and
  upstream's _render_wireframe_overlay().
- test_gcodewriter.cpp: both sides appended test cases to the same
  region; keep upstream's origin/machine-limit tests alongside the
  pressure-advance and temperature scenarios.
2026-07-13 13:17:45 -04:00
Clifford Garwood 17584e2ffb Merge remote-tracking branch 'upstream/main' 2026-06-30 01:55:14 -04:00
Clifford GarwoodandClaude Opus 4.8 4677c79e18 Merge upstream/main into iXex PR branch (#13086)
Catches the iXex/IDEX branch up to upstream main (44 commits). One
content conflict resolved:

- src/libslic3r/Print.hpp: kept upstream's default-initialized
  m_origin {0,0,0} alongside our m_imex_slice_offset member.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 12:02:29 -04:00
Joseph Robertson 5428a0715d update belt-printer (#14446)
[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-06-26 23:02:49 -05:00
Clifford GarwoodandClaude Opus 4.8 c5d0cdbcff Merge upstream/main into iXex PR branch (#13086)
Catches the iXex/IDEX parallel-printing branch up to upstream main
(102 commits). Two content conflicts resolved:

- src/libslic3r/Preset.cpp: s_Preset_printer_options — kept upstream's
  new "use_3mf" key and our iMEX printer-capability/mode keys.
- tests/fff_print/test_gcodewriter.cpp: upstream revived the disabled
  suite (#14196), dropping the obsolete [.]-tagged lift() test and its
  config_lift_unlift.ini; kept their set_speed + z_hop tests and appended
  our 10 per-firmware set_pressure_advance/set_temperature scenarios.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 23:29:35 -04:00
Joseph Robertson 75770321dd Update Belt-Printer (#14425) 2026-06-25 22:40:26 -05:00
Joseph Robertson c950c3fb6b Add BabyBelt Pro Profile, Courtesy of Rexit (#14424) 2026-06-25 22:39:12 -05:00
Joseph Robertson 2ca843a38e Belt Printing: Bugfix: Solid Organic Tree Base, Slim Tree Skirt, Renderer (#14395)
* fix tree support brim
* treesupport3d part 1: more diagnostic logging.  (todo once things are fixed: remove this / gate it properly)
* make area under Z=0 in rotated slice pipeline not solid
* fix solid Z=0 layer for belt printers
* fix renderer
* clean up logging
* final review pass
2026-06-24 22:01:29 -05:00
Joseph Robertson 0ef7c6d581 Belt Printing: Update (#14393)
# Description

<!--
> Please provide a summary of the changes made in this PR. Include
details such as:
  > * What issue does this PR address or fix?
  > * What new features or enhancements does this PR introduce?
> * Are there any breaking changes or dependencies that need to be
considered?
-->

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-06-24 21:34:53 -05:00
Joseph Robertson 34b0d36cda Belt Printer Initial Push (#14385)
# Description

Initial push - documentation available at #12998 

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-06-24 09:42:40 -05:00
Joseph Robertson d619c7e19c Merge branch 'belt-printer' into belt/baseChanges 2026-06-24 09:42:25 -05:00
Joseph Robertson 31b44cb731 Merge pull request #66 from HarrierPigeon/belt/tommyb-rendererChanges
Clean up and implement @tommasobbianchi's belt renderer changes
2026-06-23 00:27:39 -05:00
harrierpigeon ddbee84e68 render the G-code preview upright (designed view) + toggle UI 2026-06-23 00:14:17 -05:00
Joseph Robertson bf6cce1f40 Merge pull request #45 from tommasobbianchi/feat/belt-gcode-cartesian-preview
belt: render the G-code preview upright (model/Cartesian space)
2026-06-22 19:59:27 -05:00
Joseph Robertson 8bdf0df00a Merge branch 'main' into belt/baseChanges 2026-06-22 19:36:17 -05:00
Joseph Robertson d6c9187c71 Merge branch 'main' into belt/baseChanges 2026-06-22 19:36:17 -05:00
Ian Bassi 0cdfb88357 Lang: Gettext update (#14361) 2026-06-22 20:16:55 -03:00
foXaCe 14cec7239b i18n(fr): translate strings added after the post-refactor sync (#14304) 2026-06-22 20:13:19 -03:00
Heiko LiebscherandClaude Opus 4.8 86c6a1a66f Improve German (de) translation (#14352)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 15:40:14 -03:00
SoftFever 07f08dfe40 bump version to 2.5.0-dev 2026-06-22 00:50:51 +08:00
Noisyfox a4fb5af9e1 Don't allow adding more colors for non-semm printers on obj import color remapping dialog (#14275) 2026-06-21 18:20:58 +08:00
Tommaso Bianchi 8593d66a39 belt: correct the designed-view preview's belt-Z origin and reject mis-mapped outliers
The Cartesian designed-view preview over-extended the toolpaths past the model
shell by a height-proportional amount (up to ~20mm tall parts), most visibly on
long multi-part prints; compact parts like a calibration cube looked fine.

Two coupled causes:
- Belt start G-code that primes with a Z advance and a 'G92 Z0' reset leaves a
  constant machine-Z origin in the GCodeProcessor, so move positions are stored as
  gcode_Z + origin. The linear back-transform mixes that constant with the
  gantry-Y term, leaving a per-move designed-Y error that min-corner anchoring
  cannot cancel when an elevated move (e.g. a bridge) happens to cancel it at the
  bbox minimum. Expose GCodeProcessorResult::belt_z_origin (the m_origin[Z] left by
  the start G-code) and subtract it before the back-transform.
- Elevated features (bridges/overhangs) are mis-mapped by the linear inverse to
  outside the model body; build the anchor bbox only from moves within model_bb +/-
  10mm, with a fallback to the full bbox when the clip would drop the bulk (object
  placed away from the belt entry) so the gross-offset case still anchors.

Preview-only; G-code output is unchanged.
2026-06-21 06:48:44 +02:00
Tommaso Bianchi 3fc3b8a8ae belt: anchor the designed-view G-code preview onto the model bounding box
The belt designed (upright) preview back-transforms the machine-frame G-code
into model space with the linear belt inverse. That inverse recovers the
print's shape and orientation, but not the per-object placement/lift
translation: the object's position on the belt, the BeltSliceStrategy min-Z
lift, and the centering pre-translate are applied OUTSIDE
build_forward_transform() (see PrintObjectSlice.cpp), so its linear inverse
cannot undo them. The result was a constant offset (~20 mm on the belt-advance
axis) of the toolpaths from the model shell, on every model.

Recover the missing translation generally — independent of the offset's exact
source or the axis remap — by anchoring the back-transformed object body
(extrusions on layer_id >= 1, i.e. excluding the layer-0 prime/skirt) onto the
upright model bounding box, the same space the shells render in, and folding
that translation into the belt inverse before converting to libvgcode.

Replaces the previous Y=0 anchoring in LibVGCodeWrapper, which pinned the
toolpaths to the belt entry rather than to the model and so left the offset in
place for any object not sitting at the origin.
2026-06-21 06:48:44 +02:00
Tommaso Bianchi 695a1f897a belt: render the G-code preview in model (Cartesian) space
On a belt printer the emitted G-code is in the machine frame (45-deg sheared,
axis-remapped, scaled), so the toolpath preview shows the print as a sheared
slab floating off the bed. Map each toolpath vertex back to model/Cartesian
space for the "designed" view.

The back-transform is the inverse of the full G-code forward pipeline
(BeltGCodeWriter::to_machine_coords):
  model = [BeltForward^-1 if !gcode_back_transform] . AxisRemap^-1 . MachineFrame^-1
built from config, so it handles any rotation / shear / scale / axis-remap
combination, not just plain 45-deg belt slicing. Computed in load_as_gcode()
from print.config() and applied per-vertex inside libvgcode::convert (display
position only; layer_id, times and the volumetric/flow math keep the raw
machine values, so the layer slider and stats are unaffected).

- Toggle with the existing "Show designed view" checkbox / hotkey B; off shows
  the raw machine-frame G-code (useful for debugging the transform itself).
  Defaults to on.
- Belt printers skip the same-result-id load cache so the upright view applies
  and the toggle takes effect even when the G-code is unchanged.
- The object extrusions (layer_id >= 1) are anchored to the belt entry to drop
  the constant machine-origin offset (start-G-code belt advance) that the linear
  back-transform alone does not capture; start-G-code prime lines are excluded
  so they don't steal the anchor.
2026-06-21 06:48:44 +02:00
Tommaso Bianchi 2d69f6e17c belt: expose MachineFrameTransform's composed matrix
Add a const accessor for the shear*scale transform so the G-code viewer can
build the machine->model back-transform for the upright belt preview.
2026-06-21 06:48:44 +02:00
Joseph Robertson 340ce575e2 Merge branch 'main' into belt/baseChanges 2026-06-20 15:56:59 -05:00
Joseph Robertson d795900fcf Merge pull request #64 from tommasobbianchi/feat/esun-pla-maxvolspeed-tuning
IdeaFormer IR3 V2: tune eSUN PLA white speed from HW max-vol-speed calibration
2026-06-18 09:42:19 -05:00
Joseph Robertson 9b1fb2217a Merge branch 'main' into belt/baseChanges 2026-06-18 09:41:14 -05:00
Tommaso BianchiandClaude Opus 4.8 ef6f65eacc IdeaFormer IR3 V2: tune eSUN PLA white speed from HW max-vol-speed calibration
Physical max-volumetric-speed test (belt #62 v4 asset) on the IR3 V2 with eSUN
PLA white: the wall stayed clean up to ~100 mm/s = ~20 mm3/s before
under-extrusion. The shipped cap of 10 mm3/s was ~half the real ceiling and
was silently throttling infill.

- eSUN PLA @IdeaFormer IR3 V2: filament_max_volumetric_speed 10 -> 20
- 0.20mm Standard @IdeaFormer IR3 V2: sparse_infill_speed 200 (~18 mm3/s at the
  new cap, no longer throttled). Outer wall (45), PA (0.12), accel (1000)
  unchanged — accuracy preserved.
- IdeaFormer.json version bump for profile-cache refresh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 07:13:59 +02:00
Joseph Robertson 0add523e1b Merge branch 'main' into belt/baseChanges 2026-06-13 08:23:47 -05:00
Joseph Robertson 375036f330 Merge pull request #44 from tommasobbianchi/feat/belt-skip-height-check
belt: don't reject long objects (skip build-height check on belt printers)
2026-06-13 08:23:26 -05:00
Joseph Robertson fbbeb1fab0 Merge pull request #58 from HarrierPigeon/belt/tempTower-TommyB
Belt/temp tower tommy b
2026-06-12 05:53:32 -05:00
harrierpigeon 0bca3fd2e5 make belt printer specific temp tower only accessible to belt printers 2026-06-12 05:13:08 -05:00
Tommaso Bianchi 85fd613cf7 feat(belt/calib): add Overhang temperature-tower model (selectable) (#48)
Belt printers can't slice a tall vertical temperature tower. This adds a
belt-specific temperature-tower model — a row of discrete, individually
engraved provini laid along the belt, each printed at one temperature via
custom per-layer M104. Each provino is an inverted-L overhang that stresses
print quality, so the operator reads the best temperature off overhang
quality rather than a continuous ramp.

It is offered as a "Test model" choice in the temperature calibration dialog
(mirroring the Cornering test's selector), so users keep Joe's counter-rotated
sectioned tower as "Standard" and can pick this one as "Overhang":
- Calib_Params::test_model (existing field) carries the choice.
- Temp_Calibration_Dlg gets a Standard/Overhang radio.
- Plater::calib_temp belt branch: test_model 0 -> _calib_temp_belt_sectioned
  (unchanged Standard path), 1 -> the discrete-provini Overhang path.

Assets: belt_temp_provino_unit.stl + belt_temp_tower_<start>_<end>.stl (6
ranges) + gen_belt_temp_tower.py (manifold engraving). Based on
belt/generic-calibrations. The Overhang path is HW-validated on the IdeaFormer
IR3 V2 (discrete M104 + engraved numbers); not re-validated since the rebase.
2026-06-12 05:13:07 -05:00
Joseph Robertson 0da24cd38b Belt/Standard calibrations (#54)
Enables supported printing of standard Orcaslicer calibration profiles.

* Build 2 Checkpoint

* fix support generation wedge, ghost layers

* flip cornering tests 180 deg to waste less supports

* fix row spacing on the flow ratio calibrations

* more testing, this didn't fix anything

* switched rotation tools, same issue

* fixed Z-offset issues

* add rest of PA features, may look a bit weird on a belt

* make temp towers work

* re-enable spiral on calibrations that want it

* Final cleanup pre-PR and community testing
2026-06-12 03:14:12 -05:00
Rodrigo Faselli d7b75540d0 Merge branch 'main' into belt/baseChanges 2026-06-11 11:59:53 -03:00
Tommaso Bianchi b7bda9912b belt: fix IR3 V2 end G-code reversing the belt into the part (#56)
The IdeaFormer IR3 V2 End G-code ran `G28 ; home all`, which homes the
Z (belt) and Y (gantry) axes. On a belt printer Z is the conveyor, so
homing it runs the belt all the way back to origin, dragging the finished
part back under the gantry that G28 has just lowered — the head knocks the
print (reported by an IR3 V2 user; the `G1 Y50` lift came after the G28,
too late).

Replace the end sequence with a belt-safe one: switch to relative mode
(G91), lift the gantry for clearance, advance the belt forward one full
machine-depth (Z676, the 676 mm product depth) to eject the part and cycle
the belt surface clean, then home X only — never the Z/belt axis.
2026-06-11 09:30:35 -05:00
Tommaso Bianchi 4f3a608009 belt: don't flag the lead-in as an empty-layer error on belt printers (#47)
collect_layers_to_print() warns (CRITICAL) when an extrusion layer sits above
the previous one with an empty gap below — the fixed-bed assumption that
material with nothing under it is floating and unprintable. On a belt printer a
*leading* empty range (the gap starts at Z=0, no prior extrusion layer) is not
floating: it is the conveyor lead-in, and the part rests on the advancing belt
as the first material is laid down well above Z=0. A part not designed for a
belt (e.g. a flat test model tilted into the belt frame) then trips this as a
false "Object can't be printed for empty layer between 0 and N" error.

Suppress only the leading case (belt_printer && last_extrusion_layer == null);
genuine internal gaps are still flagged, since on a belt those can be an
over-angle overhang printing into air. Non-belt output is unchanged.
2026-06-10 23:54:02 -05:00
Tommaso BianchiandClaude Opus 4.8 f682ab5cd3 belt: replace height-check skip with a belt-correct vertical-clearance check
The original PR skipped the max-print-height check entirely on belt printers
because the sliced (virtual) Z is belt travel, not build height. As the reviewer
noted, that removed the only working height guard. Restore a correct guard:

- Print::validate: on belt printers, compare the upright object height
  (max over instances of the scene-space bbox) against printable_height directly.
  printable_height is the usable VERTICAL clearance above the belt: the gantry
  travels up the tilted plane (reach = height/cos(tilt)) and its axis range is
  sized for that (IR3 V2: ~354 mm gantry travel = 250 mm vertical at 45deg, and
  printable_height = 250). Hardware-confirmed 250 mm vertical clearance, so no
  cos(tilt) factor is applied.
- BuildVolume::set_belt_printer: drop the diagonal Z scaling; the build-volume Z
  already equals printable_height, keeping the live 'outside build volume'
  highlight in agreement with validate().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 21:41:18 +02:00
harrierpigeon 2bcb775b90 update IdeaFormer profiles to new generic belt printer config 2026-06-10 05:10:20 -05:00
Joseph Robertson e29a82c672 add attribution and design notes 2026-06-10 05:10:20 -05:00
Joseph Robertson 5b243eec92 Relocate Pre-Slice remap logic 2026-06-10 05:10:20 -05:00
Joseph Robertson fee6be98b2 unify frame tilt work 2026-06-10 05:10:20 -05:00
Joseph Robertson 9405ac5976 remove mesh origin snapping 2026-06-10 05:10:20 -05:00
Tommaso Bianchi 6ed2437848 Add IdeaFormer IR3 V2 belt printer profile - credit: tommasobbianchi (#43)
* Add IdeaFormer IR3 V2 belt printer profile

Self-contained vendor profile for the IdeaFormer IR3 V2 (45 deg belt printer):
machine (0.4 nozzle) + 0.20mm process + Generic PLA/PETG filaments, with the
belt machine-frame transforms set explicitly on the machine preset
(belt_printer, belt_slice_rotation x/45/global, build_plate_tilt_x=45,
gcode_remap_x/y/z, gcode_shear_z=pos_tan, gcode_scale_y=inv_cos).

The vendor bundles its own machine/process commons (fdm_belt_common,
fdm_klipper_common, fdm_machine_common, fdm_process_common) on purpose:
OrcaSlicer resolves system-preset inheritance per-vendor, so a profile that
inherits the Custom vendor's commons cross-vendor fails to resolve its parent
and the whole IdeaFormer vendor silently fails to load. Bundling the commons
(and listing them in IdeaFormer.json in dependency order) keeps the vendor
self-contained, matching how every other vendor folder is structured.

Machine limits, bed temperature (75 C for belt PLA) and start/end G-code are
taken from a working IdeaFormer IR3 V2.



* feat(belt/profile): eSUN PLA @IdeaFormer IR3 V2 — HW-calibrated belt filament

Add an eSUN PLA belt profile for the IR3 V2, inheriting Generic PLA @IdeaFormer
IR3 V2 (self-contained: parent is in the same IdeaFormer vendor, registered
after it in filament_list). HW-calibrated on the IR3 V2:
- nozzle_temperature 200/200 (temp-tower calibration)
- pressure_advance 0.12 (PA calibration)
- filament_max_volumetric_speed 10 mm³/s (max-vol-speed calibration: wall
  failed at 126 mm/s → 126 × 0.0798 mm³/mm ≈ 10 mm³/s)
2026-06-10 04:13:56 -05:00
Clifford Garwood 62074b409d fix(gcode): skip ooze prevention for same-physical filament changes
Ooze prevention currently treats every toolchange as a transition between
independent hotends — the OLD filament's extruder cools to a standby
temperature (via standby_temperature_delta or the filament's
idle_temperature) before the change, and the NEW filament's extruder
ramps back up afterward. This is correct for IDEX/toolchanger setups
where the parked nozzle would drip otherwise.

For AFC/MMU lane swaps where the SAME physical extruder stays selected
(only the loaded filament changes), the cool-down → re-heat round trip
is pointless: same nozzle, same heater, just a different filament feeding
it. In-print this costs 30+ seconds per lane swap, and the AFC tip-form
sequence ends up running on a cooling extruder.

Gate the pre_toolchange call on physical_extruder_map: when both the old
and new filament index map to the same physical extruder, skip the
standby cool-down. post_toolchange is left untouched — its M109 to the
new filament's print temp is still emitted, so per-lane temperature
differences (e.g. PLA → PETG on the same AFC manifold) are still handled.

Note on pem sizing: the guard requires physical_extruder_map to be sized
to the filament count for the per-filament lookup to succeed. The option's
registered default is a single-entry [0], which is shorter than the filament
count on any multi-filament setup, so the bounds check fails and ooze runs
as before. The fix fires only on profiles that explicitly author pem to
match filament count (the AFC/MMU/toolchanger configs that actually encode
same-physical routing).

Behavior matrix:

| Config | pem | Ooze behavior |
|---|---|---|
| Single-extruder + SEMM | (any) | `init_ooze_prevention` already disables ooze. No change. |
| Single-extruder, no SEMM | (any) | Single filament, no toolchanges. N/A. |
| IDEX / Toolchanger | `[0,1,…]` per-filament | Cross-physical → ooze runs as before. |
| Vanilla AFC, SEMM=true | (any) | `init_ooze_prevention` disables ooze. No change. |
| Vanilla AFC, SEMM=false | `[0,0,…]` per-filament | Same physical → **ooze SKIPPED** (the fix). |
| Toolchanger + AFC | per-filament | AFC swaps skip, cross-physical swaps run. |
| Default pem `[0]` (1 entry) | shorter than filament count | Bounds check fails for filament index ≥ 1 → ooze runs as before. No-op for profiles that haven't authored a per-filament pem. |
| Empty pem | `[]` | Guard returns false → ooze runs as before. |

The guard depends only on physical_extruder_map; no machine-class check.
Any printer whose pem is sized to filament count and indicates multiple
logical slots routed to the same physical hotend benefits.
2026-06-08 03:13:44 -04:00
Clifford Garwood 896adc7071 Merge: hide secondary g-code preview carriages under firmware-managed-zones
The firmware-managed-zones writer offset puts prim_pos and the preview
toolpaths in a frame shifted relative to the bed bounds the zone math
uses, so secondaries would have rendered off the plate. Skipping the
secondary computation when firmware-managed is on keeps the centered
preview honest.

Addresses Felix14-v2 feedback on PR #13086.
2026-06-08 01:26:33 -04:00
Clifford Garwood e502175030 Sync upstream main
124 upstream commits including CrealityPrint integration (added include
in Plater.cpp alongside the existing IMEXHelpers include), profile fixes
and version bumps (#14084, #14085, Polymaker), CI artifact publishing,
test refactors (arachne walls test added; test_3mf/test_config/
test_gcodewriter content moved/removed upstream — IMEX test file
preserved as it is feedback-only), translations.
2026-06-08 01:26:33 -04:00
Clifford Garwood d0c6e8305d fix(imex): hide secondary carriages in g-code preview when firmware-managed-zones is on
The firmware-managed-zones writer offset centers the slice at bed origin,
so prim_pos and the preview toolpaths sit in a shifted frame relative to
the plate-local bed bounds the secondary-marker zone math uses. Computing
secondaries against unshifted bed bounds puts them off the build plate.

Skip the secondary-marker / toolhead-box computation when the
firmware-managed-zones option is enabled. The firmware physically fans the
centered toolpath out into the zones at print time, so the honest preview
is the single centered toolpath with no secondaries.

Reported by @Felix14-v2 on PR #13086.
2026-06-08 01:24:35 -04:00
Joseph Robertson da3fee2dfa Merge branch 'main' into belt/baseChanges 2026-06-05 11:55:44 -05:00
Joseph Robertson c0d6ae8540 Merge branch 'main' into belt/baseChanges 2026-06-05 03:12:27 -05:00
Joseph Robertson 573e1c6544 Belt/fix profiles and minor oopsies (#42)
* fix duplicate printer, bump version

* clean up extra tab in space

* fix generic defaults
2026-06-05 03:11:38 -05:00
Rodrigo Faselli 20be78a96e Merge branch 'main' into belt/baseChanges 2026-06-04 17:32:35 -03:00
Joseph Robertson 02d45c3258 Finish Fixes from Copilot Review (#39)
* fix: restore BuildVolume bounds when toggling belt mode

set_belt_printer() mutated m_bboxf when enabling but never restored
the original extents on disable or when switching infinite_y true->false,
leaving stale max.y/max.z values that broke collision and object_state
checks. Recompute m_bboxf from m_bed_shape + m_max_print_height at the
top of each call, then apply belt-specific adjustments on top.

Addresses Copilot review comment on PR #12998 (BuildVolume.cpp:196).

* chore: drop [BELT-DEBUG] to_machine_coords log to trace

Was emitting at warning level once per 0.2mm Z bucket during every belt
print export, polluting default user logs. Trace level matches the rest
of the belt diagnostics and is silent in production.

Addresses Copilot review comment on PR #12998 (BeltGCodeWriter.cpp:86).

* chore: drop [BELTRACE] make_perimeters/support logs to trace

Eight warning-level traces around make_perimeters and
generate_support_material were emitting on every call/exit during normal
slicing, cluttering default logs. They're concurrency-debug breadcrumbs
not user-facing diagnostics, so drop them to trace.

Addresses Copilot review comment on PR #12998 (PrintObject.cpp:438).

* perf: gate BeltSliceStrategy diagnostic bbox tracking behind compile flag

apply_to_trafo() walked every model vertex twice (once for min_z, once
for per-volume mesh/slicer bboxes) and emitted seven trace logs per
call. The bboxes and logs are diagnostic only; min_z is the load-bearing
output. Wrap the bbox accumulation, logging, and supporting headers in
SLIC3R_BELT_DIAGNOSTIC_LOG so production builds do the bare min_z scan.

Addresses Copilot review comment on PR #12998 (BeltSliceStrategy.cpp:95).

* fix: apply part_cooling_fan_min_pwm to first-layer plane fan crossings

apply_first_layer_plane_fan_eval emitted band-crossing M106 commands
through GCodeWriter::set_fan() without the per-printer PWM floor that
every other set_fan call in CoolingBuffer applies. On printers with a
non-zero part_cooling_fan_min_pwm, fans could fail to spin up at low
requested speeds near the belt surface.

Addresses Copilot review comment on PR #12998 (CoolingBuffer.cpp:1227).
2026-06-04 14:40:45 -05:00
harrierpigeon f9888c7d7a Merge remote-tracking branch 'upstream/main' into belt/baseChanges 2026-05-31 05:17:32 -05:00
Joseph Robertson 0bda684dd7 delete mesh transforms (#37)
* delete mesh shear, scale and refactor logger

* clean up config options

* reorder UI elements
2026-05-31 05:08:42 -05:00
Joseph Robertson 8a578cdf00 Merge branch 'main' into belt/baseChanges 2026-05-30 21:39:03 -05:00
Clifford Garwood 92de4e3f87 Merge IMEX g-code preview carriage placement fix
Secondary carriage markers now read the tool layout enum correctly,
aggregate to one active marker per gantry in Span modes, and mirror
across the bed centerline so they render on-bed.
2026-05-29 09:50:25 -04:00
Clifford Garwood ed6c608dc2 Sync upstream main
Pulls latest upstream fixes (Default filament option #13887, SEMM
multi-extruder crash #13896, linux wxstring crash #13925, Polymaker
profiles, et al.).
2026-05-29 09:50:18 -04:00
Clifford Garwood b2f548b098 Merge PR branch to converge histories
Brings the standalone PrintApply dedup-fix commit and the prior upstream
merge into staging. Their content is already present here (the dedup fix
was re-applied during the latest upstream sync), so this only reconciles
the commit ancestry — no tree changes expected.
2026-05-29 09:40:04 -04:00
Clifford Garwood 4a6342d646 fix(imex): correct secondary carriage placement in g-code preview
- Layout enum read via opt<ConfigOptionEnum<ImexToolLayout>>() did a
  dynamic_cast that fails for the ConfigOptionEnumGeneric type enums load
  as from presets, silently defaulting to FrontLeft. This desynced flip_y
  from PartPlate::calc_imex_zones, computing secondaries against the wrong
  primary zone and rendering them off the bed. Use option<>() (type-checked
  static_cast), matching how PartPlate reads the same key.

- Span modes now collapse each non-primary gantry to one active marker via
  group_imex_active_tools_by_gantry() (the same aggregation PartPlate uses
  for zones), since only one tool prints per zone at a time. Previously every
  Copy/Mirror tool got its own marker.

- Aggregated mirror markers reflect across the bed centerline rather than a
  column edge; the aggregated strip spans full-X, so edge reflection would
  push the marker off the bed.
2026-05-29 09:31:15 -04:00
Clifford Garwood 9b3c1b87f4 Sync upstream main
Includes nozzle-diameter guards for printers without nozzle info (#13255,
now landed upstream) and manual-calibration nozzle mismatch fix (#13882).

# Conflicts:
#	src/slic3r/GUI/GCodeViewer.cpp
#	src/slic3r/GUI/GLCanvas3D.hpp
#	src/slic3r/GUI/Tab.cpp
2026-05-29 01:49:42 -04:00
Rodrigo Faselli 6b256db012 Merge branch 'main' into belt/baseChanges 2026-05-28 07:44:43 -03:00
Joseph Robertson 2dc4900292 Copilot review fixes & upstream code interaction fix (#34)
* first pass at review issue 8
* delete detritus
* fix build compile error due to upstream changes
2026-05-27 21:53:04 -05:00
Joseph RobertsonandCopilot Autofix powered by AI 0f75d6bc4e Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-27 19:25:02 -05:00
Joseph Robertson e913621369 Merge branch 'main' into belt/baseChanges 2026-05-27 11:50:16 -05:00
Joseph Robertson 48b6db93b8 Belt/slice rotate (#33)
* initial commit
* fix upper bounds for assemblies
* significantly less Z shift issues, still not quite tamped down yet though
* add instrumentation to logs
* finally found the issue
* update printer defaults
2026-05-27 11:45:38 -05:00
Clifford Garwood 4fd19a5531 fix: drop duplicate filament-extruder reshape in Print::apply
Upstream #13360 moved `m_ori_full_print_config = ...` and
`update_values_to_printer_extruders_for_multiple_filaments(...)` into a
new `if (!extruder_applied)` block. The 3-way merge with our IMEX
`physical_extruder_map` derivation block (sitting between the old and
new positions) silently kept both copies, running the per-filament
reshape twice and miscounting filaments for the multi-temperature
compatibility check. Removed the obsolete second copy at the old
position. Resolves `p1s_multicolor.3mf` regression test failure.
2026-05-25 05:13:44 -04:00
Clifford Garwood d6de031c8a Merge remote-tracking branch 'upstream/main' into merge-test-upstream
# Conflicts:
#	src/slic3r/GUI/GCodeViewer.cpp
#	src/slic3r/GUI/GLCanvas3D.hpp
#	src/slic3r/GUI/Tab.cpp
2026-05-25 03:11:50 -04:00
Clifford Garwood c89de62fc2 Merge feedback into main
Brings in upstream sync + AFC extruder_index integration:
- physical_extruder_map auto-population from AFC lane data
- verified end-to-end against xplorer (7 lanes, pem [0,0,0,0,1,2,3])
2026-05-25 02:46:46 -04:00
Clifford Garwood 0adc6024a4 Merge feature/extruder-mapping into feedback
Auto-populate physical_extruder_map from AFC extruder_index field.
Verified live on xplorer: 7/7 lanes report extruder_index, gate fires.
2026-05-25 02:05:08 -04:00
Joseph Robertson 72cafcbe06 Merge branch 'main' into belt/baseChanges 2026-05-22 15:23:07 -05:00
Joseph Robertson a9bae54f20 Rotate instead of shear for slicing stage (#30)
* initial commit

* fix upper bounds for assemblies

* significantly less Z shift issues, still not quite tamped down yet though

* add instrumentation to logs

* finally found the issue

* update printer defaults
2026-05-22 15:21:33 -05:00
Joseph Robertson 218881c6f6 fix assembly bounding box truncation problems noticed by hotcubcar (#28) 2026-05-20 02:46:41 -05:00
Joseph Robertson cd5fb68d38 Merge branch 'main' into belt/baseChanges 2026-05-19 23:00:14 -05:00
Joseph Robertson f87a46ec6e fix X mirroring (#26)
Thanks to @hotcubcar for catching this!
2026-05-19 22:54:50 -05:00
Rodrigo Faselli 8dc91d8b1d Merge branch 'main' into belt/baseChanges 2026-05-19 08:06:57 -03:00
Joseph Robertson da8b11b8ab HOTFIX: update generic belt printer profile (#23)
oops
2026-05-19 01:06:39 -05:00
Joseph Robertson c79970bedb Clean Up Settings Interface, Update Generic Profile (#22)
* clean up UI elements

* further cleaning

* final cleanup for first round of settings UI streamlining

* update generic belt printer settings

* fix generic again
2026-05-19 00:56:08 -05:00
harrierpigeonandClaude Opus 4.7 7252f6acb7 Merge upstream/main into belt/rebase/may-18
Reconciles the belt-printer branch with upstream PRs through #13723. Six
files had conflicts; three additional files needed manual follow-up fixes
where the auto-merge produced code that referenced upstream-renamed fields
or changed function signatures.

Notable reconciliations:
- TreeSupport.cpp: kept belt-floor early-exit branches around HEAD's
  drop-down logic, folded upstream's `(distance_to_top > 0 ? 1 : 0)`
  formula into the non-belt-floor path (upstream PR #11812). Dropped dead
  `roof_enabled`/`force_tip_to_roof` locals.
- TreeSupport3D.cpp: combined upstream's safety-offset + remove_small
  changes with HEAD's belt-floor clip in the per-slice trim loop. Dropped
  HEAD's `else` block (superseded by upstream's rewritten bottom-contact
  propagation) and re-added the belt-floor clip into the new propagation
  loop. Gated the propagation on belt printers to prevent OOM when
  belt-floor clipping produces empty initial slices.
- TriangleSelector.{cpp,hpp}: merged both new `select_patch` parameters
  (HEAD's `up_direction` and upstream's `select_partially`); body uses
  `dot(up_direction)` for the overhang angle check and forwards
  `select_partially` to `select_triangle`.
- SupportMaterial.cpp: `slicing_params.soluble_interface` →
  `zero_gap_interface_bottom` in HEAD's `detect_belt_floor_bottom_contacts`,
  matching upstream's same-purpose rename at line 2495.
- Custom.json, GCodeWriter.cpp: simple additive merges (kept entries /
  includes from both sides).

Verified by building OrcaSlicer (RelWithDebInfo) after a full deps
rebuild (Eigen v5.0.1, libigl v2.6.0 are now managed deps) and slicing
a scaled Benchy on the NORMALIZER belt-printer profile without OOM.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 21:53:24 -05:00
Joseph Robertson 6a2d690f45 Decouple Slicing From Machine Frame Logic (#21)
* minor logic swap

* first attempt, has a race condition

* fixed the offset issue

* found a solution, I think things work now (at least once I quash this race condition)

* still chasing down race conditions

* add manual shear / scale order strategy swap

* tweak manual shear, fix ui uninitialization crash

* fix z height / g-code desync issue

* fix shear then scale cutoff planes

* getting closer

* fix support termination planes

* fix incorrect offsets in shear-then-scale mode

* test - fix overextrusion due to model/layer scale
2026-05-18 19:01:43 -05:00
Clifford Garwood 5fd0f3c5b7 Merge remote-tracking branch 'upstream/main' into feedback 2026-05-17 20:21:23 -04:00
Clifford GarwoodandClaude Opus 4.7 cb66867768 feat(imex): topology-aware Tool 0 Position dropdown
For single-gantry IMEX setups, front/rear is meaningless — only left/right
matters. Collapse the imex_tool_layout dropdown to two items ("Left" / "Right",
mapped to front-left/front-right internally) when imex_gantry_count == 1, and
show all four corner items otherwise. Normalize stored rear-* selections to
their front-* equivalents on the transition so the displayed selection always
matches the persisted value.

Also rename the field label "Tool 0 Corner" → "Tool 0 Position" so it reads
correctly in both single- and dual-gantry contexts, with a tooltip that
explains the IDEX/IQEX distinction.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 13:25:37 -04:00
Clifford Garwood f11011efac Merge branch 'feedback' 2026-05-13 02:05:11 -04:00
Clifford GarwoodandClaude Opus 4.7 ca06900d33 chore(imex): drop "(center-origin slice)" subtitle from firmware-managed zones label
Tooltip already explains the mechanism; the parenthetical clutters the label.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 02:04:45 -04:00
Clifford Garwood de8448f048 Merge remote-tracking branch 'origin/main' 2026-05-13 01:52:29 -04:00
Clifford Garwood ab63789076 Merge remote-tracking branch 'upstream/main' into feedback
# Conflicts:
#	src/libslic3r/GCodeWriter.cpp
#	src/libslic3r/PrintApply.cpp
#	src/slic3r/GUI/Tab.cpp
#	tests/libslic3r/test_config.cpp
2026-05-13 00:57:36 -04:00
Clifford GarwoodandClaude Opus 4.7 48c603faa5 feat(imex): firmware-managed zones — center slice + ghost suppression
Adds the `imex_firmware_managed_zones` printer-config key (default off) for
IDEX/IQEX printers whose firmware applies its own copy/mirror offsets in
non-primary modes (e.g. RepRapFirmware IDEX duplication mode, Flashforge
Creator Pro 2/3 Pro). For these printers the slicer needs to emit a single
centered slice at bed origin and let the firmware fan toolheads out from there;
the previous slicer-managed iMEX rendering would draw a print at the primary
zone's world position (off-bed for the firmware-fan-out paradigm).

When the flag is on and the active mode is non-primary, the slicer subtracts
the primary zone's plate-local center from the gcode emission frame. The
writer offset is augmented but the gcode-processor offset stays at plate_origin
so the gcode-preview visualizer renders the centered slice at the bed center
rather than at the prepare-view zone placement. translate_to_print_space is
augmented too so first_layer_print_min/max placeholders (consumed by user
start_gcode like Felix's M118 header) reflect the centered frame.

Slice handoff lives in PartPlate::refresh_imex_slice_offset, called from both
update_slice_context (plate switch) and Plater::priv::update_background_process
(every-slice path — reslice() goes through here with switch_print=false so the
plate-switch hook alone wouldn't fire on mode toggle).

calc_imex_ghosts early-returns in firmware-managed mode: the existing
imex_head_transform math places ghosts at primary_zone_center + gantry_offset
(slicer-managed semantics), which renders off-bed when the toolpath is being
emitted in a centered frame. Proper firmware-managed ghost rendering (showing
where copies/mirrors will actually print after firmware fan-out) is deferred.

When the flag is off, all the new code paths reduce to no-ops byte-identical to
prior behavior. Layer 1 unit tests in test_imex_helpers cover every gating path
of compute_imex_slice_offset; full ctest suite passes (247/247).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 00:55:45 -04:00
RF47 8fa6a4602b fix profile indentation 2026-05-09 19:51:18 -03:00
harrierpigeonandClaude Opus 4.7 0f29437135 Merge remote-tracking branch 'upstream/main' into belt/baseChanges
Conflicts resolved in src/libslic3r/GCode.cpp and src/slic3r/GUI/GUI_Factories.cpp.

GCode.cpp: combined upstream's air-filtration per-extruder gating
(activate_air_filtration_during_print / _on_completion), the new
extrusion-role-change gcode lambda, ZAA's path.z_contoured arc-fit
disable, raft-aware slow_down_layers branch, and Vec3d/Line3 ZAA
plumbing with the local belt-printer changes (path_on_first_layer,
effective_layer_index_for_point, should_disable_arc_fitting). All
auto-merged m_writer.X() calls converted to m_writer->X() to match
the local unique_ptr<GCodeWriter> refactor.

GUI_Factories.cpp: inserted brim_flow_ratio in the Support category
list and renumbered around the local build_plate_tilt_x/y entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 16:23:41 -05:00
Rodrigo Faselli f0271df707 Merge branch 'main' into main 2026-05-09 11:11:17 -03:00
Clifford Garwood 493a9e96d2 Merge remote-tracking branch 'upstream/main'
# Conflicts:
#	src/libslic3r/Preset.cpp
2026-05-02 01:17:02 -04:00
Clifford GarwoodandClaude Opus 4.7 6c7bd484db fix(imex): guard IMEX zone refresh against headless CLI slicing
`check_outside` (placement check) calls `ensure_imex_zones()` to make sure
zone geometry is current before deciding if an instance is in-bounds. In
the GUI path that's fine, but the CLI / headless 3MF-load path also reaches
this through `PartPlateList::load_from_3mf_structure -> reload_all_objects
-> add_instance -> check_outside`, and CLI mode has no GUI_App initialized.

`build_imex_cache_key` and `calc_imex_zones` both dereference
`wxGetApp().preset_bundle` — without a GUI_App, `wxGetApp()` returns memory
that segfaults on member access, killing the slicer with SIGSEGV before
any G-code is produced.

Latent since 461c69c83e (Apr 9), surfaced now that upstream's main carries
the headless regression-test CI step (#13353) that exercises CLI slicing
on every PR build.

Fix: short-circuit `ensure_imex_zones()` when `m_plater` is null (already
the GUI/CLI marker used by `calc_imex_ghosts`). Also tighten the existing
`build_imex_cache_key` null check to consult `m_plater` first as defence
in depth, so the function stays safe if reached from another headless
caller.

Verified locally against the upstream regression suite — klipper /
p1s_multicolor / toolchanger_4_color all slice cleanly within the
20% baseline tolerance.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 08:14:07 -04:00
Clifford Garwood b452a8a28f Merge remote-tracking branch 'upstream/main' into feedback 2026-05-01 01:30:40 -04:00
Clifford GarwoodandClaude Opus 4.7 1d6fab7a44 fix(imex): pivot Span ghost X-flip on mesh bbox center
The aggregated-mirror ghost was X-flipping about the mesh's local origin,
which shifted the ghost sideways for models whose local origin sits at a
corner (calibration cubes, calicat, most STL imports anchored at the
min corner). Visible as a constant left-X offset between the primary's
position and the ghost's position.

Pivot on `mo->raw_mesh_bounding_box().center()` instead, applied through
the instance transform so rotated objects flip about the rotated bbox
center too. Same correction applied to the live-drag update path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 01:27:47 -04:00
Clifford GarwoodandClaude Opus 4.7 4966d0fae8 feat(imex): Span tile state for paired-gantry multicolor
Introduce ImexRole::Span as a 5th tile cycle state that declares "this
tool is the multicolor partner of Primary on the same gantry." Encoded
as the `S` role suffix in `imex_mode_active_tools` (e.g. `0:P,1:S,2:M,3:M`).
Disambiguates paired-gantry mc-mirror from 4-independent-copies — both
share the same active_tools shape sans the marker.

Span drives:
- Multicolor block rule: now requires Span on primary's gantry to allow
  multi-color slicing in a parallel mode. Replaces the prior "≥2 tools
  on primary's gantry" check; pre-existing 4-tool multicolor configs
  need T1 flipped to Span.
- Ghost aggregation: one ghost per non-primary gantry when Span is
  present, using the column-paired representative. Aggregated-mirror
  drag tracks primary 1:1 in X (gantries don't share an X rail) with
  X-flip baked into mesh-local frame so geometry still reads as mirrored.
- Zone aggregation: one full-X row strip per non-primary gantry instead
  of per-tool quadrants.
- UI: 5th button in IMEXModesCtrl. Cycle Off→P→C→M→S→Off, only offered
  on multi-gantry printers and only on tiles sharing primary's gantry row.

Single source of pairing truth: group_imex_active_tools_by_gantry in
IMEXHelpers, consumed by ghost factory and zone calculator.

Also fixes the carriage collision strip's X-boundary check, which lacked
the row constraint its Y-boundary counterpart already had — paired-gantry
mc-mirror was drawing a spurious right-edge strip from T3 sitting
diagonally from primary.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-01 01:16:02 -04:00
Clifford GarwoodandClaude Opus 4.7 a8dbdf90de fix(imex): scope ghost render + picking to the active plate
_render_imex_ghosts and _picking_pass_imex_ghosts both walked every
plate in the partplate list and rendered/hit-tested all ghosts found —
which meant background plates' ghosts bled through into the active
scene whenever GL state was shared (most visibly when entering paint
mode), and clicks could land on a ghost that belonged to a plate the
user wasn't actually looking at.

Switch both paths to read get_curr_plate() and skip the per-plate loop.
Per-plate ghost volumes still live on each PartPlate so 3MF
round-trips work and switching plates picks up the new active plate's
ghosts cleanly; we just don't draw or hit-test the ones whose plate
isn't the active one.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 23:01:15 -04:00
Clifford GarwoodandClaude Opus 4.7 2c604173d3 fix(imex): block multi-color slicing for single-gantry "fake IMEX" modes
Catches the case where a non-primary IMEX mode's active tools all sit on
one gantry — e.g. mode "0:P,1:C" on a 2x2 IQEX where T0 and T1 share
gantry 0. The "Copy"/"Mirror" label is decorative there: nothing actually
parallel-prints, but the user's mode_gcode still fires and emits firmware
setup that doesn't apply, while the slicer treats it as a multi-color
parallel print. Conceptually it's just a regular multi-tool single-gantry
print and belongs in Primary mode.

Changes:
- imex_multicolor_block_reason now collects the set of distinct gantries
  spanned by the active tools and short-circuits with a clear "single
  gantry — not a parallel-print scenario" message before falling through
  to the existing within-gantry-swap check.
- Drops the redundant pre-slice "Multi-material objects detected" soft
  warning from collect_imex_warnings — the slice-time block surfaces a
  more specific message at the right moment, and the soft warning was
  vague handwaving in front of it. Bed-temp + filament-type checks stay.
- New unit test covering the single-gantry block.

Behavior matrix on a 2x2 IQEX with multi-color:
  "0:P,1:C"           single gantry         -> BLOCK (new)
  "0:P,2:C"           dual gantry, 1 each   -> BLOCK (existing within-gantry-swap)
  "0:P,1:C,2:M,3:M"   dual gantry, 2 each   -> ALLOW
  multiple filaments to same physical via pem -> BLOCK (existing MMU sharing)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 22:53:33 -04:00
Clifford GarwoodandClaude Opus 4.7 66891899e9 refactor(imex): drop unused Split mode-type infrastructure
The mode-type tag (imex_mode_types config + imex_mode_type_for helper +
Split sentinel) was added in 0bb1cef as scaffolding for the Split rendering
work that landed in 4370cca and then got reverted in d9be71b. With Phase 2-4
gone, this scaffolding is now unused dead code — and the design we settled
on instead is to leave topology entirely implicit (parsed from active_tools_str)
rather than carrying a per-mode type tag the user would otherwise have to
manage explicitly.

The multi-color slicing block + bare T<n> suppression that were the actual
substance of the safeguards work stay in place:
- imex_multicolor_block_reason still allows multi-color exactly when 2+ tools
  are active on the primary's gantry — Felix's hypothetical IQEX paired-gantry
  case works through this path, no new mode type required.
- Slicer-side: bare T<n> stays suppressed at print-start in IMEX parallel modes;
  mid-print T<n> emits naturally for the legitimate IQEX 4-tool-active scenario.

Removed:
- ConfigOptionStrings imex_mode_types (PrintConfig.hpp/cpp + Preset.cpp key list)
- imex_mode_type_for helper + kImexModeType{Primary,Copy,Mirror,Split} sentinels
- mode_type parameter on imex_multicolor_block_reason and the Split short-circuit
- mode_type plumbing in Print::validate and PartPlate::has_imex_multimaterial_conflict
- Three unit tests for imex_mode_type_for + two Split-specific multicolor block tests

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 00:48:39 -04:00
Clifford Garwood d9be71b1ca Revert "feat(imex): Split mode rendering + modes editor Type column"
This reverts commit 4370cca4c9.
2026-04-28 00:20:31 -04:00
Clifford GarwoodandClaude Opus 4.7 4370cca4c9 feat(imex): Split mode rendering + modes editor Type column
Completes the Split-type IMEX mode plumbing started in 0bb1cef. Phase 1
landed the config option, helper, validator, and plater badge updates;
this commit lands the visualization side and the editor surface so users
can actually create and use Split modes.

Zone aggregation (PartPlate::calc_imex_zones)
- When mode_type == "split" and there's row separation between primary
  and secondaries, collapse copy_cells and mirror_cells per-gantry
  (each cell gets primary's column) and force has_col_sep = false.
  make_boxes then takes its full-X-row branch and renders one zone
  covering each non-primary gantry's full Y band, instead of per-tool
  quadrants.

Ghost aggregation (PartPlate::calc_imex_ghosts)
- Pre-scan the active mode's tool list to pick a canonical head per
  non-primary gantry (the tool whose physical column matches primary's;
  fallback to first-seen). For Split modes, the per-head emission loop
  skips non-canonical tools so each non-primary gantry gets exactly one
  aggregate ghost rendered at the canonical's mirrored position.
- update_imex_ghost_transforms unaffected — it iterates the already-
  filtered ghost set.

Modes editor Type column (IMEXModesCtrl in Tab.cpp)
- Each non-primary mode row gains a wxChoice dropdown selecting Copy /
  Mirror / Split. Primary row gets a static "Primary" label.
- Header row picks up a "Type" column header with sized spacer that
  aligns with the dropdown.
- get_mode_data() now returns a fourth tuple element (types vector).
  load_from_config() reads imex_mode_types via imex_mode_type_for so
  legacy presets without the new option still infer types from mode
  names. on_change writes imex_mode_types back to config.
- matches_config(), snapshot_row(), row_differs_from_parent(), and
  reset_row_to_parent() all extended to track type alongside name/
  tools/gcode — per-row reset arrows reflect type-only changes, and
  reset restores the parent's type via imex_mode_type_for fallback.
- Type-choice dropdown fires notify() on change so the dirty/save flow
  catches it like any other row edit.

End-to-end: switching a row to Split type in the modes editor, then
selecting that mode on a plate, allows multicolor slicing (the gantry-
pair check is bypassed in imex_multicolor_block_reason) and the bed
visualization shows a single aggregate zone + one ghost per non-primary
gantry instead of per-tool clutter.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 23:17:24 -04:00
Clifford GarwoodandClaude Opus 4.7 0bb1cef78e feat(imex): polish UI per PR feedback + add parallel-mode safeguards
Felix14-v2 PR review feedback (https://github.com/OrcaSlicer/OrcaSlicer/pull/13086#issuecomment-4323696312)
plus the slice-time validation work that follows from his bug list.

UI polish:
- Capitalize "Primary" in display (right-click mode menu, plate tooltip).
  Sentinel stays lowercase for wire compatibility.
- Pre-slice-warnings checkbox now uses Orca's ::CheckBox so it matches the
  green toggle style of the rest of the IDEX/IQEX configuration page.
- DPI-scaled the IMEXModesCtrl (modes editor) sub-panels, button grid,
  text wraps, gcode textarea, and the ghost-tooltip swatch (imgui.scaled).
  Legend swatches sized to body-text height for visual balance.
- Primary mode tool buttons in the modes editor are now disabled (read-
  only): cycling roles on the IMEX-off mode is a no-op and confusing.
- Modes editor sub-panels now explicitly inherit the app's window-default
  dark colour so chromeless ScalableButtons don't render with a visible
  light box around their icons on GTK dark themes.
- Per-mode-line reset arrows in the modes editor: each row gets a small
  reset bitmap that snaps that row's name+tools+gcode triplet back to the
  saved preset's value. matches_config() guard on the page-level reload
  prevents the textbox-being-typed-into from being destroyed mid-keystroke.
- New View menu item "Show IDEX/IQEX Toolhead" — toggles the per-carriage
  footprint boxes during G-code preview playback. Gated to Preview tab +
  IMEX printer; backed by app_config so it persists.

Coordinated config migrations:
- imex_tool_layout and imex_viz_theme migrate from coString to coEnum
  (ImexToolLayout / ImexVizTheme). Existing wire format preserved so
  saved presets and 3MFs deserialize unchanged. Side-benefit: both now
  pick up standard Field rendering and so finally show reset arrows.

Slice-time safeguards (the validation half):
- imex_suppresses_bare_toolchange(parallel_mode, count): suppresses the
  slicer's bare T<n> at print-start in any IMEX parallel mode (the user's
  imex_mode_gcode + machine_start_gcode owns tool activation there). Mid-
  print T<n> emits normally — Print::validate blocks the configurations
  where mid-print T<n> wouldn't make sense. Applied to both code paths
  inside GCode::set_extruder (the long multi-extruder path AND the
  single-extruder path that fires when multiple_extruders=false).
- imex_multicolor_block_reason(): hard-stop validator returning a user-
  facing reason string when the active IMEX configuration can't physically
  support multi-color. Catches IDEX (1 tool/gantry), 2-tool-active IQEX
  (no within-gantry swap topology), and any MMU/AFC lane sharing among
  used filaments. Wired into Print::validate as a slice blocker, and into
  PartPlate::has_imex_multimaterial_conflict so the plater badge agrees
  with the slice block (no more false positives where the badge warns but
  the slice goes through).
- New imex_mode_types config option (parallel array to imex_mode_names)
  and imex_mode_type_for() helper. Mode-type tag drives behaviour: zones,
  ghosts, and validation interpret modes differently per type. Initial
  types: "primary", "copy", "mirror", "split". Split modes are explicitly
  designed for paired-gantry IQEX multi-color and bypass the gantry-pair
  check in imex_multicolor_block_reason (MMU sharing still blocks them).

Test coverage:
- New unit tests cover imex_suppresses_bare_toolchange (4 cases),
  imex_multicolor_block_reason (8 cases including IDEX, IQEX 2-/4-tool-
  active, MMU sharing, missing primary, Split type), and imex_mode_type_for
  (3 cases including legacy fallback). Total: 186 IMEX assertions across
  71 test cases, all passing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-27 22:51:05 -04:00
Clifford GarwoodandClaude Sonnet 4.6 9676f04cbf feat: auto-populate physical_extruder_map from AFC extruder_index
AFC now publishes an extruder_index field per lane. This lets Moonraker
auto-derive physical_extruder_map without user configuration:

- Add extruder_index field to AmsTrayData (-1 = not provided)
- Parse extruder_index from AFC lane JSON using contains() check since
  0 is a valid extruder index (safe_json_int returns 0 for missing fields,
  which would be ambiguous)
- After build_ams_payload, if all trays have extruder_index, derive
  physical_extruder_map and write it to the printer preset config via
  CallAfter so IMEX PA and temperature emission use the correct physical
  extruder qualifier

AFC lanes sharing a carriage all report extruder_index=0; independent
direct-drive tools on separate carriages report their carriage index.
PrintApply.cpp's auto-derive from printer_extruder_id is skipped when
physical_extruder_map has more than 1 element, so the AFC-populated map
takes precedence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-26 13:04:28 -04:00
Clifford Garwood b10d6e1b9f Merge remote-tracking branch 'origin/main' 2026-04-25 05:17:47 -04:00
Clifford GarwoodandClaude Opus 4.7 41457635e9 test(imex): extract physical→logical helpers + cover them with unit tests
Three of the four IMEX physical-vs-logical fixes landed earlier on this
branch (fbc58d2a1d, fa048babeb, a38b95bf45, e11e7d46df) used inline
lambdas / direct loops to translate physical extruder indices to logical
filament slots.  No test coverage existed for the specific composition,
even though the underlying primitives (resolve_filament_for_head,
first_filament_for_physical_head) were tested.

Pull two patterns out of Plater.cpp and GCode.cpp into IMEXHelpers as
named helpers, then unit-test them:

  imex_primary_logical_from_objects(used_slots_1b, pem, primary_physical)
    Walks the plate's used filament slots (1-based) and returns the
    first one whose pem entry maps to the primary's physical extruder.
    This is what the warning's `logical_for_primary` now delegates to —
    moves the "look at object assignments, not pem first-routed default"
    behavior introduced in fa048babeb out of the lambda and into a
    separately-testable function.

  imex_secondary_logical_slots(active_physicals, primary_physical,
                               plate_head_filament_map, pem)
    Iterates IMEX active physicals, skips the one matching primary,
    resolves each remainder via resolve_filament_for_head (per-plate
    override + first-routed fallback), deduplicates, drops -1 entries.
    Replaces the inline loop in GCode::_do_export's is_extruder_used
    marking (a38b95bf45 + e11e7d46df).

10 new test cases in test_imex_helpers.cpp cover the cases that
correspond directly to bugs hit:

  imex_primary_logical_from_objects:
    - AFC primary picks the object's slot (the user's specific bug)
    - Multi-color AFC primary returns first input-order match
    - Direct extruder primary unambiguous (no MMU)
    - No object routed to primary returns -1
    - Empty inputs (no objects, empty pem)

  imex_secondary_logical_slots:
    - Copy mode skips primary, falls back to first-routed
    - IQEX 4-mode enumerates all three secondaries
    - Per-plate override wins over first-routed
    - Drops unrouted physicals + deduplicates
    - Only-primary-active returns empty

All [IMEX] + [Variant] + [3mf] regression: 166 assertions / 61 cases.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 05:07:34 -04:00
Clifford GarwoodandClaude Opus 4.7 d41e0d09b4 fix(imex): skip primary in IMEX is_extruder_used marking
a38b95bf45 translated physical -> logical for IMEX active tools but used
resolve_filament_for_head for *all* active tools including the primary.
For the primary, that falls back to first_filament_for_physical_head —
which returns the FIRST logical slot routed to the primary's physical
extruder, not the slot the user actually assigned to the printing object.

On the user's Neo XP 0.6 (pem [0,0,0,0,1,2,3,3]) printing in copy mode
with the object on slot 2 (PLA dark grey):
  - tool_ordering correctly marks slot 2 (object's filament).
  - The IMEX-marking loop then *also* marked slot 0 (ABS) as the primary's
    "first-routed" slot — wrong: slot 0 isn't loaded, slot 2 is.
  - Start-gcode template emitted EXTRUDER=260 EXTRUDER2=235 EXTRUDER4=235;
    the ABS-temp emission for slot 0 was harmless noise in this macro
    design but conceptually bogus.

The primary's filament is already correctly covered by
tool_ordering.all_extruders() — that lists the slots the objects on the
plate are assigned to.  Skip the primary in the IMEX-marking loop using
the same pattern as the IMEX PA emission path at GCode.cpp:3265
(translate initial_extruder_id -> physical via pem, skip that physical).

Result on the user's setup post-fix:
  - is_extruder_used: slot 2 (object), slot 4 (T1's filament in copy).
  - Start-gcode emits EXTRUDER2=235 EXTRUDER4=235 — exactly two temps,
    one per active heater, with no spurious ABS bookkeeping.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 05:07:34 -04:00
Clifford GarwoodandClaude Opus 4.7 2b7f438887 fix(imex): translate physical→logical when marking is_extruder_used for IMEX active tools
is_extruder_used is a logical-filament-slot indexed bool array — start-gcode
templates use it as `is_extruder_used[N]` where N is a logical filament slot
(matches how the rest of the codebase consumes per-filament arrays like
filament_settings_id, nozzle_temperature_initial_layer, etc.).

`tool_ordering.all_extruders()` returns LOGICAL slots and was correctly
marking those. But the IMEX-secondary marking added in 5aa624b025 was
writing PHYSICAL extruder indices straight into the logical array, mixing
index spaces. On any printer with physical_extruder_map size > 1 (MMU/AFC),
this marks the wrong slots and misses the right ones.

Symptom on the user's Neo XP 0.6 (pem [0,0,0,0,1,2,3,3]) in copy mode
[0:P,1:C] with object on slot 2 (PLA):
  - tool_ordering marks slot 2 (correct: object's filament).
  - IMEX active = [0, 1] (physical T0, T1) → mistakenly marks logical
    slots 0 (ABS) and 1 (ASA), neither of which is used.
  - Slot 4 (PLA on physical T1, the actual filament that loads on the
    secondary in copy mode) is NOT marked.
  - Start-gcode template emits EXTRUDER=ABS_temp EXTRUDER1=ASA_temp
    EXTRUDER2=PLA_temp; no EXTRUDER4.
  - PRINT_START macro reads t4=0, skips heating extruder1 — T1 stays
    cold during the print.

Translate physical → logical via resolve_filament_for_head before marking
(per-plate imex_head_filament_map override consulted, with first-routed
fallback when no override is set). This matches what the firmware actually
loads on each carriage during the parallel-mode print, and what the rest
of the IMEX hot path (PA emission, layer-change temperature) already does.

Result on the user's setup post-fix:
  - is_extruder_used[2]=true (object), is_extruder_used[4]=true (T1 in copy).
  - Start-gcode emits 2 temps for the actually-used filaments.
  - extruder1 heats correctly to slot 4's temp before the print begins.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 05:07:34 -04:00
Clifford GarwoodandClaude Opus 4.7 c8e7c5678b fix(imex): primary's filament lookup must come from object assignment, not pem default
The pre-slice warning's primary-tool filament lookup (added in
fbc58d2a1d) used resolve_filament_for_head, which returns the FIRST
slot routed to the primary's physical extruder via pem. That's the
right rule for *secondaries* (no object owns them in copy/mirror mode;
filament comes from the per-plate imex_head_filament_map override) but
wrong for the *primary*: the primary prints the actual objects on the
plate, and the slot it uses is whatever the user assigned to those
objects — not whatever happens to be at the head of the AFC manifold.

Symptom on the user's Neo XP 0.6:
  - pem = [0,0,0,0,1,2,3,3] (slots 0-3 share AFC manifold on physical T0)
  - filaments: slot 0 ABS, slot 2 PLA, slots 4-6 PLA, etc.
  - object assigned to slot 2 (PLA), IMEX mode "copy" (T0 primary, T1 copy)
  - User picks slot 5 PLA for T1 via the IMEX ghost picker (writes a
    per-plate imex_head_filament_map).
  - Warning reads slot 0 (ABS) for primary because that's
    first_filament_for_physical_head(pem, 0). Fires "T0 ABS vs T1 PLA
    type mismatch" even though the actual print uses slot 2 (PLA) for
    the primary — slicer and warning disagree.

Split the lookup:
  logical_for_primary(physical_idx)
    Walks plate->get_extruders(true) (1-based slots used by objects on
    this plate), returns the first slot whose pem entry maps to
    physical_idx. Falls back to first_filament_for_physical_head if no
    object on the plate routes to this physical extruder (defensive).

  logical_for_secondary(physical_idx)
    Unchanged behavior: per-plate imex_head_filament_map override with
    first_filament_for_physical_head fallback.

The user-facing "T%d" labels still display the physical extruder index
(carriage identity); only the filament-info lookups change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 05:07:34 -04:00
Clifford GarwoodandClaude Opus 4.7 0db99d21dd fix(imex): translate physical→logical when looking up filament info in pre-slice warnings
collect_imex_warnings parses imex_mode_active_tools to get the active tool
indices. Those are PHYSICAL extruder indices (one per carriage). It then
used those same indices to look up filament_presets[tool_idx] and
bed_temps[tool_idx] — but both arrays are indexed by LOGICAL filament slot.

For MMU/AFC layouts where multiple logical slots feed one physical
extruder (e.g. AFC manifold: 4 lanes on physical T0), the warning would
report the wrong filament: a secondary on physical T1 would be named with
filament_presets[1] (= AFC lane 2) instead of the actual filament on T1.

Symptom: in IMEX parallel mode on the user's IQEX-AFC printer (pem
[0,0,0,0,1,2,3]), the multi-extruder warning called the secondary tool
"T1" but reported the filament type for logical slot 1 (an AFC lane),
not the actual filament 4 routed to physical T1.

Translate physical → logical via effective_physical_extruder_map (with
per-plate imex_head_filament_map override) before indexing into
filament_presets and bed_temps. The displayed "T%d" still shows the
PHYSICAL extruder number — that's the carriage identity the user sees
on hardware. Only the filament-info lookup is changed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 05:07:34 -04:00
Clifford GarwoodandClaude Opus 4.7 6e176434d0 docs(imex): document physical-vs-logical index distinction
Three IMEX bugs of the same shape have surfaced over the lifetime of
this feature:
  - Inline pem lookup duplicated at two PA emission sites
  - Pre-slice warnings indexing filament_presets by physical index
  - Ghost color resolution with stale default pem handling

Each was a place where a per-filament array got indexed by what the call
site had on hand (a physical T-number) without translating through
physical_extruder_map. On non-MMU/non-AFC printers the indices coincide
and nothing breaks; on AFC layouts the slicer reads the wrong filament
preset for a carriage with no error or log line.

Add a header comment block to IMEXHelpers.hpp describing the two index
spaces, when each is used, how to translate, and a list of the bugs we
hit so future contributors can recognize the pattern.

The constant kImexPrimaryMode and the helper declarations follow this
block; readers searching for pem helpers will land on the guidance first.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 05:07:34 -04:00
Clifford GarwoodandClaude Opus 4.7 d329fce6b0 Merge branch 'tests/imex-coverage' into feedback
Brings in the IMEX test coverage (PA per-firmware, Temperature per-firmware,
[Variant] expansion, 3MF round-trip, imex_pem_tool_for helper + tests, and
the cherry-picked variant-truncation regression test).

Resolution notes:
- Two GCode.cpp call sites for set_pressure_advance had divergent edits:
    * tests/imex-coverage rewrote them to use the new imex_pem_tool_for
      helper (commit c2492ccc47), eliminating the inline parallel-mode
      check entirely.
    * feedback replaced the literal "primary" with kImexPrimaryMode in the
      same lines (commit 085f5ccec8).
  Resolution: keep the helper-call form. The kImex change is moot on lines
  the helper replaces, and imex_pem_tool_for in IMEXHelpers.cpp is also
  updated to use kImexPrimaryMode for consistency with the rest of the
  codebase.

- Test test_3mf.cpp updated for upstream's load_bbs_3mf signature change
  (PR adds is_orca_3mf out-parameter between is_bbl_3mf and file_version).
  All three call sites in the new IMEX 3MF round-trip tests pass &is_orca
  in addition to &is_bbl.

Full regression post-merge:
  libslic3r:    143 cases / 48,553 assertions  (+10 cases from new tests)
  fff_print:     24 cases /    245 assertions  (+10 cases from new tests)
  sla_print:     21 cases / 14,100 assertions
  libnest2d:     14 cases /    488 assertions
  slic3rutils:    3 cases /      3 assertions

All tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 01:17:27 -04:00
Clifford Garwood 357fcb374d Merge remote-tracking branch 'upstream/main' into feedback 2026-04-25 00:46:48 -04:00
Clifford GarwoodandClaude Opus 4.7 212f81446c refactor(imex): convert remaining "primary" literals in GCodeViewer
Follow-up to 085f5ccec8. Self-review of that commit's grep output missed
GCodeViewer.cpp. Three sites in the layer-preview multi-carriage marker
logic still compared against the bare "primary" literal:

  - GCodeViewer.cpp:1538 — process-preset mode default
  - GCodeViewer.cpp:1542 — per-plate mode override gate
  - GCodeViewer.cpp:1551 — secondary marker computation gate

All three now use kImexPrimaryMode. The file already includes
IMEXHelpers.hpp (line 13) so no new include needed.

Verified by grepping the full IMEX-touching set: only IMEXHelpers.hpp
itself (the constant definition) still references the literal string,
which is correct.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 00:42:51 -04:00
Clifford GarwoodandClaude Opus 4.7 7dcb74e504 docs(imex): document inputs of build_imex_cache_key
The IMEX zone cache key drives ghost rebuild and zone-overlay
invalidation. Adding a printer config option that affects zone geometry,
ghost transforms, or collision strips without extending the key produces
a silent staleness bug: the cache thinks the zones are still valid and
ghost meshes / overlays don't refresh after the new option changes.

Document what currently feeds the key and pin the precision convention
(*10 scale on float values for 0.1 mm resolution) so future contributors
know the contract and where to extend it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 00:32:42 -04:00
Clifford GarwoodandClaude Opus 4.7 4e0a2704b9 refactor(imex): consolidate IMEX active-tools parsing through one helper
GCode.cpp's static get_imex_active_tools() inlined its own "phys[:role]"
tokenizer with subtly different semantics from IMEXHelpers'
parse_imex_active_tools — only the GCode version bounded against
MAXIMUM_EXTRUDER_NUMBER. Three other call sites (PartPlate zones,
GCodeViewer legend, Plater warnings) routed through parse_imex_active_tools
already.

Move the bounds check into parse_imex_active_tools so all consumers get
it consistently, then rewrite get_imex_active_tools to do only the
Print-extraction portion (active mode lookup, tools-string fetch) and
delegate token parsing to the helper. Keeps Print out of IMEXHelpers'
include set.

No behavior change for the non-pathological case (mode strings have always
parsed identically); for indices >= MAXIMUM_EXTRUDER_NUMBER (64) the three
older call sites silently filter them out now where previously they would
have accepted them — this matches what get_imex_active_tools already did.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 00:32:34 -04:00
Clifford GarwoodandClaude Opus 4.7 085f5ccec8 refactor(imex): replace "primary" magic string with kImexPrimaryMode constant
Across 6 files the literal "primary" string was the sentinel for "no IMEX
parallel mode active" — used for short-circuiting in serialization, ghost
visualization, zone calc, popup-menu list construction, the IMEXModesCtrl
non-deletable first row, and several layer-of-export checks. A typo in
any one would silently bypass the guard.

Define kImexPrimaryMode in IMEXHelpers.hpp with a docstring describing
what equality with it means semantically, and route every call site
through it. No behavior change.

Touched: bbs_3mf.cpp, GCode.cpp, PartPlate.cpp, Plater.cpp, Tab.cpp.
The bbs_3mf and Tab files now include IMEXHelpers.hpp; the other three
already did.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 00:32:22 -04:00
Clifford GarwoodandClaude Opus 4.7 efa9d65cb4 fix(imex): code-review nits — null check, cache-key precision, lambda capture
Four quick-fix items surfaced by pre-PR self-review.

GCodeViewer.cpp:1607
  Null-check get_curr_plate() before dereferencing. Other call sites in
  the file already guard; this was the only unguarded one in the IMEX
  layer-preview path. In practice m_plate_list always has a plate, but
  the inconsistency is easy to fix and removes the only ungated deref.

PartPlate.cpp:build_imex_cache_key
  Cache key for IMEX zone geometry truncated nozzle_clearance_x/y to int
  before stringifying — a config change from 30.0 to 30.5 would not
  invalidate the cache. Match the *10 precision pattern already used for
  imex_carriage_margin so 0.1 mm steps invalidate correctly.

Plater.cpp:select_plate_by_hover_id (right-click popup)
  Two issues:
  1. Lambda captured `modes` by reference. PopupMenu() is synchronous
     today so the reference outlived the menu's event handling, but the
     pattern is fragile — anyone refactoring to async Popup() would
     silently dangle. Capture by value.
  2. Used wxID_HIGHEST + i for menu item IDs — standard wx anti-pattern
     because it can collide with other handlers listening in that range.
     Allocate per-item IDs via wxNewId() and look up the chosen mode by
     finding the event ID in a parallel vector. The lookup becomes O(N)
     instead of O(1) but N is small (mode count) and this is clicker
     latency, not a hot path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 00:07:35 -04:00
Clifford GarwoodandClaude Opus 4.7 7b9070d521 fix(gcode): restore BBL initial-filament PA emission removed in error
af59501f4a ("feat: firmware-agnostic per-tool PA emission for IMEX
parallel modes") inadvertently deleted the BBL-specific PA emission for
initial_non_support_extruder_id while adding IMEX per-tool PA support.
That deletion was scope creep into core BBL functionality and not part
of the IMEX feature.

Restore the original block verbatim. The new IMEX-parallel-modes PA
emission (per-secondary-tool, gated on m_imex_parallel_mode) is left
untouched — that's legitimately IMEX scope. BBL printers in non-IMEX
mode now get back the pre-PR initial-PA behavior.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 00:04:41 -04:00
Clifford GarwoodandClaude Opus 4.7 ee757dac20 test: address self-review findings on [Variant] and [3mf][IMEX] coverage
Self-review found two weaknesses in the preceding test commits:

1) The equal-size [Variant] scenario claimed to distinguish the truncation
   guard's `cur > target` predicate from a regression to `cur >= target`,
   but both paths yield identical child values in practice: when
   extruder_variant names match, set_with_restore's variant_index is fully
   populated (no -1 slots) and the merge path restores every position from
   backup — producing the same {1.5, 2.5} output as the skip path. The
   test passes in both guard states.

   Rewritten to use mismatched variant names between child and parent.
   variant_index then has -1 slots, and set_with_restore overwrites those
   positions with parent values. Now the merge path yields {0.8, 0.8} and
   the skip path yields {1.5, 2.5} — observably different. Verified:
     - `cur >  target` (correct):  4 scenarios pass, 15 assertions
     - `cur >= target` (regressed): equal-size scenario fails with
       "1.5 is within 0.000000001 of 0.80000000000000004"
     - Guard removed entirely: child>parent + stride=2 both fail with
       truncation ("1 == 2" / "2 == 4")

2) The [3mf][IMEX] round-trip only covered a single plate. A plate-
   indexing regression (IMEX metadata landing on the wrong plate, or
   bleeding across plates on reload) would not have been caught.

   Added a multi-plate scenario: two plates with distinct mode and
   head-filament-map values. Asserts both land on their respective
   destination plates after reload. Load-bearing verified:
     - With IMEX serialization intact:    3 scenarios pass, 45 assertions
     - With IMEX serialization disabled:  positive + multi-plate fail
       (both "nullptr != nullptr"); primary-mode passes (expects nullptr)
     - With primary-mode short-circuit removed: primary-mode scenario
       fails ("0x... == nullptr") because primary modes now serialize

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 23:29:01 -04:00
Clifford GarwoodandClaude Opus 4.7 b45d8a5b7b test(gcode): per-firmware coverage for GCodeWriter::set_temperature(tool)
Four scenarios cover the temperature emission surface that IMEX layer-change
handling routes through (Tier 1 of the deferred Target B test plan —
pure-function-only, no fixture).

- Per-flavor command routing: Marlin (M104), RRF (G10 — M104 is deprecated
  on RRF), Mach3/Machinekit (P-prefix for value instead of S).
- Wait handling: Marlin emits M109, MakerWare/Sailfish silently drop wait
  requests (the firmware doesn't support blocking waits), Teacup and RRF
  both emit a separate M116 poll.
- Per-tool qualifier for IMEX secondary carriages: Marlin and Klipper
  emit T<N>, RRF uses P<N> (same P override as its wait poll). This is
  exactly the path that lets IMEX set secondary-tool layer temperatures
  without a tool-change.
- Instance overload's multi-extruder gating: a tool index passed to a
  single-extruder GCodeWriter is discarded (no spurious T0 on
  single-tool printers), but a multiple_extruders writer passes it
  through verbatim.

All 24 assertions in 4 cases pass under [Temperature].

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 23:08:57 -04:00
Clifford GarwoodandClaude Opus 4.7 c2492ccc47 refactor(imex): extract imex_pem_tool_for helper + unit tests
The physical_extruder_map translation used by IMEX per-tool PA emission was
inlined identically at two sites in GCode.cpp (tool-change and second-layer
transition). Extract to IMEXHelpers so the routing rule ("parallel mode AND
populated pem → physical index, else -1") is testable in isolation and the
call sites read as intent rather than re-deriving the conditional.

Production change is behavior-preserving:
- Same predicate (`!mode.empty() && mode != "primary"`)
- Same empty-pem short-circuit returning -1
- Same get_at() dispatch on hit
- Both call sites replaced with a single call

Four unit tests in [IMEX] cover the routing matrix:
  - non-IMEX ("") and primary mode short-circuit
  - parallel mode + empty pem short-circuits (defense-in-depth; get_at would
    throw on empty values otherwise)
  - identity pem (non-MMU IDEX) routes filament to itself
  - MMU collapse routes multiple logical slots to one physical (7-slot profile
    with 4-lane MMU on physical 0 and direct drives on 1/2/3)

All IMEX + Variant regression suites pass post-refactor (133 assertions / 48
cases under libslic3r, 25 assertions / 6 cases under fff_print [PressureAdvance]).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 23:07:23 -04:00
Clifford GarwoodandClaude Opus 4.7 cb15f35444 test(3mf): round-trip coverage for per-plate IMEX state
Validates that imex_parallel_mode and imex_head_filament_map survive a
full store_bbs_3mf → load_bbs_3mf cycle — the same silent-state-loss bug
class that produced the variant-vector truncation regression, applied to
IMEX plate state which rides the same XML metadata path.

- Positive round-trip: a plate with copy_mode + a non-trivial head
  filament map ("1:2,2:3") is saved and reloaded; both options land on
  the destination plate's config with the exact values preserved.
- Guard scope: a plate with mode="primary" and empty head-filament-map
  does NOT emit metadata (per the serializer's short-circuit), and the
  reload leaves both options absent from the destination config. If the
  serializer ever regressed to writing primary-mode plates, the load
  path would surface phantom "primary" strings on plates that shipped
  clean — this catches that.

Both scenarios call set_temporary_dir to point the BBS exporter's backup
scaffolding at a writable per-process temp directory (by default it
resolves under root at runtime, which fails for non-root test
processes).

All 27 assertions in 2 test cases pass under [3mf][IMEX].

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 22:58:19 -04:00
Clifford GarwoodandClaude Opus 4.7 5434a5217c test(gcode): per-firmware coverage for GCodeWriter::set_pressure_advance(tool)
Exercises the IMEX per-tool PA emission surface added in af59501f4a
("feat: firmware-agnostic per-tool PA emission for IMEX parallel modes").
Six scenarios cover the full routing matrix:

- Negative PA returns empty across all flavors (early-exit guard).
- Klipper: bare vs EXTRUDER=extruder vs EXTRUDER=extruderN. Asserts the
  tool=0 case emits the unsuffixed extruder name (first Klipper extruder
  is named "extruder", not "extruder0") — a subtle edge case easy to
  regress.
- RRF: bare vs D0 vs DN. The D0 case matters: passing tool=0 explicitly
  must emit `D0`, not the current-tool fallback.
- Marlin 2.x: bare vs T0 vs TN.
- Marlin Legacy: tool index is silently dropped — verifies the fallback
  branch can't accidentally start emitting T qualifiers on firmware that
  doesn't support them.
- BBL: flag wins over firmware flavor (Marlin 2 flavor + BBL flag emits
  the BBL-specific `M900 K... L1000 M10`) and BBL never emits a per-tool
  qualifier regardless of the tool argument.

All 25 assertions across 6 cases pass under [PressureAdvance].

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 22:53:24 -04:00
Clifford GarwoodandClaude Opus 4.7 af8fe649ef test(preset): expand [Variant] coverage — stride=2, equal-size, non-variant guard
Adds three scenarios alongside the existing child>parent stride=1 regression
test for update_non_diff_values_to_base_config:

- stride=2 child>parent: machine_max_acceleration_x (size 4 vs 2) — confirms
  the truncation guard fires for the (normal,silent)-pair stride=2 path, not
  just stride=1. Catches a regression class the existing test would miss
  because stride=2 routes through normalize_stride2_floats and a different
  set_with_restore call site.

- equal-size (2=2): exercises the path the guard does NOT short-circuit;
  asserts child per-extruder values survive set_with_restore's nil-restore
  merge. Catches any future change that breaks the equal-size merge — the
  fix's `cur > target ? skip` predicate could regress to `cur >= target` and
  silently override child values otherwise.

- non-variant scalar: layer_height in `keys` and `different_keys` but absent
  from printer_options_with_variant_1/_2. Hits the is_scalar() / "nothing to
  do" branch and must remain untouched. Scopes the guard's blast radius.

All four scenarios in the [Variant] tag pass: 15 assertions, 4 test cases.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 22:49:59 -04:00
Clifford GarwoodandClaude Opus 4.7 6ed688dfc5 test(preset): regression test for child>parent variant-vector truncation
Adds a Catch2 scenario that builds a 2-extruder child DynamicPrintConfig
inheriting from a 1-extruder parent, calls update_non_diff_values_to_base_config
through the real printer_options_with_variant_1 / _2 key sets, and asserts
that printer_extruder_id, printer_extruder_variant, and retraction_length
retain their full size after the merge. Covers three distinct
set_with_restore<T> instantiations (Ints, Strings, Floats) and verifies
both size preservation and per-extruder value preservation.

Verified load-bearing: with the guard in update_non_diff_values_to_base_config
temporarily removed, the test fails with "1 == 2" on pe_id.values.size() and
retraction_length.values.size(); with the guard restored, all six assertions
pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-24 20:42:57 -04:00
Clifford 0c829c4619 Merge branch 'main' into main 2026-04-22 02:21:37 -04:00
Clifford GarwoodandClaude Opus 4.7 135a571379 fix(preset): don't truncate child variant vectors to parent size on load
update_non_diff_values_to_base_config sizes variant_index to the parent's
(inherits-target's) extruder count, and set_with_restore then replaces
the child's vector with a parent-sized one. When the child preset has
more extruders than the parent (e.g. an IDEX preset inheriting from a
single-nozzle base), every key in printer_options_with_variant_1 is
truncated to the parent's size on project reload, destroying per-extruder
data.

Observable symptoms: objects render with the wrong color (often black)
after reopening the project, and the printer preset shows a permanent
dirty-asterisk that no save/reload cycle can clear.

The child's saved value is authoritative for its own extruder count, so
skip the parent-shaped merge for the variant-keyed branch when cur >
target. Confirmed by loading a 2-extruder IDEX preset inheriting from a
single-nozzle base: all 24 variant-keyed options previously truncated
from child_size=2 -> 1 are now preserved at size 2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 01:20:29 -04:00
Clifford GarwoodandClaude Opus 4.7 9d75ea2ebc fix(imex): clear stale ghost-hover head when ghosts disappear mid-hover
_picking_pass_imex_ghosts is what resets m_hover_ghost_head, but _picking_pass
early-returns (mouse drag, mouse off-canvas, gizmo drag) skip that reset. If
the user switches from an IMEX printer to a non-IMEX one during such a window
the plate clears its ghost volumes while the stale head index survives,
producing an orphan tooltip anchored to nothing.

Validate the hover state against live ghost volumes before rendering the
tooltip and self-heal the indices when they no longer point at anything.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-21 22:52:03 -04:00
Clifford GarwoodandClaude Opus 4.7 9a87b96ded refactor(imex): Mirror reflects about zone-boundary plane, not primary origin
Replace imex_head_transform's fifth argument (Vec3d primary_origin) with a
Vec2d primary_zone_center and rewrite the Mirror branch as a true reflection
about the plane x = primary_zone_center.x + gantry_offset.x/2. Previous math
flipped about the primary's current origin, which:
  * let the ghost drift out of the target zone as the primary moved, and
  * made mirrored drag motion track 1:1 with the primary instead of reflecting.

The new transform places the ghost at the mirrored position within the target
zone (matching where the mirror tool actually prints) and reflects drag so
primary +X → ghost -X while Y tracks 1:1 — i.e. the ghost stays a true
mirror while the user drags. Off-row Mirror targets (e.g. T3 on a 2x2) still
reflect across the same X-plane as on-row peers.

PartPlate::calc_imex_ghosts and update_imex_ghost_transforms now feed primary_off
(the primary head's zone center) instead of an instance-space Vec3d.

Mirror tests rewritten against the new geometric contract: ghost origin at the
reflected position, primary drag deltas reflected across the zone-boundary plane.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-21 22:51:56 -04:00
Clifford GarwoodandClaude Opus 4.7 6a3de6a28f feat(imex): effective_physical_extruder_map helper + diagnostic no-filament tooltip
Centralize the project→printer→printer_extruder_id fallback for the physical
extruder map. PrintApply, PartPlate (ghost color + cache key), Plater (tooltip
+ click gate) all previously open-coded the three-step lookup, and each handled
the "pem unset, derive from pei" case slightly differently — an IDEX printer
without an explicit pem could paint an UNPRINTABLE_COLOR ghost even though the
slicer would have derived a valid mapping.

- IMEXHelpers: add effective_physical_extruder_map(explicit_pem, pei) and a
  PresetBundle overload that wraps the project→printer precedence.
- PrintApply: use the helper in place of the inline pei→pem normalization.
- PartPlate / Plater: call the PresetBundle overload at every ghost-color,
  ghost-cache-key, tooltip, and click-gate site.
- Plater::format_imex_ghost_tooltip: when no filament resolves to a head,
  surface an actionable message directing the user to extend the extruder
  count in the Machine tab, instead of the generic "(no filament routed)".
- IMEXFilamentPickerPopover: hold m_pem by value so callers can pass a
  stack-local derived pem without lifetime worries.
- Tests: 5 new cases covering explicit-wins, default-pem fallback, null
  inputs, and the IDEX ghost-color regression that motivated this.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-21 22:51:31 -04:00
Clifford GarwoodandClaude Opus 4.7 b2128cc330 refactor(imex): route three inline tool-state parsers through parse_imex_active_tools
PartPlate::calc_imex_zones, GCodeViewer::render, and Plater::collect_imex_warnings
each hand-rolled their own "phys:P/C/M" tokenizer with subtly different error
handling. Replace the three inline loops with parse_imex_active_tools +
imex_primary_tool_for_mode so the Primary/Copy/Mirror classification agrees
across zones, the G-code viewer legend, and slice warnings.

No behavior change: the shared helpers preserve the 1=Primary / 2=Copy /
3=Mirror encoding already consumed downstream and continue to accept the
legacy bare-index form as Primary.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-21 22:49:08 -04:00
Clifford Garwood 77c32a2e15 feat(imex): plate ghost renderer + per-plate filament map
Replaces the plater-icon popover with colored transparent ghost copies
of primary-head instances on the plate, one per secondary active head
under its Copy/Mirror role transform. Left-click on a ghost opens a
compact filament picker popover for the ghost's head (MMU lane override).
Ghosts track the primary through drag/rotate/scale/mirror and invalidate
on mode or pem changes.

Key pieces:
  IMEXHelpers -- imex_head_transform (Primary/Copy/Mirror), shared role
    parser, per-head filament resolution with X-axis Mirror anchor.
  PartPlate -- ghost state, volume rebuild on mode/map/object mutation,
    primary_origin plumbed for Mirror reflection across the primary-row
    gantry plane.
  GLCanvas3D -- ghost rendering with per-head filament color and
    translucent blending; picking routed via volume composite id.
  Plater -- ghost click + tooltip; plater icon left-click always cycles.
  IMEXFilamentPickerPopover -- BitmapComboBox row for one secondary head,
    writes imex_head_filament_map on selection.
  bbs_3mf -- round-trip the per-plate imex_head_filament_map option.
  PrintConfig -- add imex_head_filament_map as a plate option.

MMU/AFC routing for parallel modes relies on the printer profile's
physical_extruder_map (see prior commit for authoring format). Primary-
row heads and their per-plate filament overrides are resolved through
that map, so PA and temperature emission address the correct physical
extruder when multiple logical slots share one carriage.

Tests: IMEXHelpers coverage for Primary/Copy/Mirror transforms
including a 2x2 off-row regression guard for the X-axis reflection fix.
2026-04-21 14:56:22 -04:00
Clifford GarwoodandClaude Sonnet 4.6 0530d6c4da feat: derive physical_extruder_map from printer_extruder_id; use for IMEX PA/temp
Auto-populate physical_extruder_map (0-indexed) from printer_extruder_id
(1-indexed) in Print::apply(). The guard only runs when the map is still
at default size (<= 1 element), so printer profiles that set it explicitly
are untouched.

All IMEX parallel-mode PA and temperature emission now routes tool slot
indices through physical_extruder_map before constructing firmware
extruder qualifiers (EXTRUDER=, M104 T, M572 D). This ensures AFC/MMU
setups where multiple slots share one physical extruder get the correct
qualifier -- e.g. T6 on physical extruder 3 emits EXTRUDER=extruder3
instead of EXTRUDER=extruder6.

Profile authoring for MMU/AFC printers:
  Add physical_extruder_map to the printer profile JSON as a 0-indexed
  string array, one entry per logical filament slot, whose value is the
  physical extruder carrying that slot. The array size must be > 1 for
  the explicit map to override the auto-derive. Example for a 7-slot
  printer with a 4-lane MMU on extruder 0 and three independent direct
  drives on extruders 1/2/3:

    "physical_extruder_map": ["0","0","0","0","1","2","3"]

  Non-MMU printers need no action -- printer_extruder_id already encodes
  the 1:1 mapping and the auto-derive handles it.

Deeper integration (zone validation, collision detection, filament
assignment grouping, Moonraker agent auto-population) is deferred.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 14:56:22 -04:00
Clifford Garwood 481cd7d8e7 Merge remote-tracking branch 'upstream/main' into feedback 2026-04-21 14:56:08 -04:00
SoftFever 75cc0de071 Merge branch 'main' into belt/baseChanges 2026-04-17 15:44:03 +08:00
Clifford GarwoodandClaude Sonnet 4.6 2cfdddf8b0 fix: crash on Multimaterial tab for all printers (Windows-only)
The "Pre-slice warnings" line in the IDEX/IQEX Configuration section
was a widget-only line (no options) with full_width left at the default
of 0. activate_line() only skips the option_set.front() call when
full_width=1; without it, the code falls through to:

    bool is_legend_line = option_set.front().opt.gui_type == ...

Calling front() on an empty std::vector is undefined behavior. On
Windows/MSVC release builds this dereferences a null pointer and reads
at offset 0x30 (where ConfigOptionDef::gui_type lands), producing an
ACCESS_VIOLATION at 0x30. On Linux/GCC the same UB happens to be
harmless, so the crash is Windows-only and cannot be reproduced on
Linux.

Fix: set line.full_width = 1, matching the pattern used by the "Modes"
(IMEXModesCtrl) line. This takes the early-return widget path in both
append_line and activate_line, bypassing option_set.front() entirely.

Reported by tester: crash on clicking Multimaterial tab with any
printer (K3D VOSTOK confirmed), build af59501f.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 16:45:25 -04:00
Clifford GarwoodandClaude Sonnet 4.6 af59501f4a feat: firmware-agnostic per-tool PA emission for IMEX parallel modes
Extends set_pressure_advance() with an optional tool index (default -1,
preserving existing behavior for all non-IMEX call sites). Per-firmware:
- Klipper: EXTRUDER=extruder[N] when tool >= 0, bare command otherwise
- RRF: M572 D<N> when tool >= 0, bare M572 otherwise (no D0 fallback)
- Marlin 2: M900 K<X> T<N> when tool >= 0, bare M900 otherwise
- Marlin Legacy / fallback: M900 K<X> always

Adds m_imex_parallel_mode to GCode, set once per export from the active
plate mode. PA and layer-transition temperature tool-qualification are
gated on this being a non-primary parallel mode — primary mode prints
use regular tool-change PA exactly like any non-IMEX printer. Secondary
active tools in parallel modes receive explicit per-tool PA at print
start since they never go through a tool-change sequence.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 09:17:39 -04:00
Clifford GarwoodandClaude Sonnet 4.6 84fc851846 feat: add "Don't show again" to IMEX pre-slice warnings
Replaces MessageDialog with RichMessageDialog to show a suppress
checkbox on both slice-plate and slice-all warning paths. Persists
the choice to app_config as imex_pre_slice_warnings=false. Adds a
re-enable toggle in Printer Settings → Multimaterial → IDEX/IQEX
Configuration so the warnings can be restored if suppressed accidentally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 09:17:29 -04:00
Clifford GarwoodandClaude Sonnet 4.6 857bfbfb82 fix: guard against empty option_set in OG_CustomCtrl widget-only lines
update_visibility() and render() both called front() on an empty vector
when a CtrlLine had no options (pure widget lines). Added an early-return
path in update_visibility() and a null-guard in render().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-14 09:17:23 -04:00
Joseph Robertson bc6d0ef0fb Add first layer detection and fan control - prototype 2026-04-13 22:29:23 -05:00
Joseph Robertson c17ae25bbc Merge branch 'main' into belt/baseChanges 2026-04-13 21:34:34 -05:00
Clifford GarwoodandClaude Sonnet 4.6 ad0b42fd37 feat: iMEX temperature handling and pre-slice warnings
Temperature emission (GCode.cpp, PrintConfig.cpp, ConfigManipulation.cpp):
- Set temperatures for all active tools in layer_change_gcode for iMEX
  parallel modes (primary + secondary carriages)
- Fix filament temperature commands so Layer 1 temperatures only emit on
  the first layer; subsequent layers use normal layer-change temperatures
- Remove stray temperature commands that fired outside the intended context
- Consolidate iMEX temperature handling into the second-layer transition;
  clean up emission logic and naming throughout

Pre-slice warning dialogs (PartPlate.cpp/hpp, Plater.cpp):
- Collect per-plate IMEX warnings before slicing: multi-material conflict,
  bed temperature mismatch between carriages (>5 °C delta), and filament
  type incompatibility
- Show a dismissible Yes/No dialog from both "Slice Plate" and "Slice All"
  actions; No returns to 3D view, Yes proceeds to slice
- Refresh plate thumbnails after the panel switch so previously-generated
  thumbnails are not left black

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 17:45:11 -04:00
Clifford Garwood 5aa624b025 fix: Mark secondary carriages as used in iMEX parallel modes
In iMEX (IDEX/IQEX) parallel printing modes (copy/mirror), only the
primary extruder generates toolpaths. The firmware duplicates the
primary's movements for secondary carriages, so they don't appear in
tool_ordering.all_extruders().

This caused is_extruder_used[N] to return false for secondary tools
even though they're physically active and moving.

The fix adds logic to parse the active mode's tool assignments from
imex_mode_active_tools config and marks all assigned tools as used.

Changes:
- Added null checks and bounds validation for config options
- Skip empty tool strings to avoid unnecessary parsing
- Reordered bounds checks for defensive programming
- Added clarifying comments for exception handling

This ensures is_extruder_used[N] is true for all tools in a parallel
mode, allowing printer profiles to correctly enable heaters and
emit cleanup G-code for all active carriages.

Fixes: is_extruder_used[1] returns false in copy/mirror modes (#13086)

Related: Comment [28]/[30], Comment [18] (is_extruder_used in G-code header)
2026-04-11 02:00:02 -04:00
harrierpigeon e981a517cd Merge branch 'belt/global-mesh-transform' into temp-pr19-merge 2026-04-10 11:49:37 -05:00
harrierpigeon 0703728e56 add global mesh transform option 2026-04-10 11:39:08 -05:00
Clifford GarwoodandClaude Sonnet 4.6 73b5b750c6 feat: iMEX multi-material warning badge, conflict dialog, and mode row UI
- Add warning badge (obj_warning.svg overlay) to the iMEX plate icon when
  a parallel mode is active alongside multi-material objects on the same plate
- Add has_imex_multimaterial_conflict() using get_extruders(true) so only
  filaments actually used on the plate are checked
- Move multi-material caution dialog from reslice() into on_action_slice_plate /
  on_action_slice_all so it fires exactly once per user action and does not
  disrupt GL thumbnail generation during Slice All
- Fix is_imex missing from p->config init key list so on_config_change()
  diff detection correctly triggers refresh_imex_icons()
- Defer imex_changed handling until after set_bed_shape() so m_shape is current
- Replace plain remove button with ScalableButton (imex_remove.svg) in mode rows
- Add EditGCodeDialog launch button per mode row for placeholder browsing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 00:10:03 -04:00
SoftFever 1e9ee0c120 add a generic belt printer 2026-04-09 23:07:08 -05:00
harrierpigeon e9a579b604 switch default shear axis, swap to tan(a) instead of cot(a) 2026-04-09 23:07:07 -05:00
harrierpigeon 783acd932a revert CLAUDE.md 2026-04-09 23:07:07 -05:00
harrierpigeon c8a1bf3a99 Part 3.2: decouple axis remapping, enable viewing settings in Developer mode or when Belt mode is active 2026-04-09 23:07:07 -05:00
harrierpigeon 2facaac9e8 Part 3.1: refactor BeltTransform pipeline
add BeltGCodeWriter

add BeltGCode

consolidate changes into shared classes for BeltGcode
2026-04-09 23:07:07 -05:00
harrierpigeon 9bbac19de4 Part 2.7: Add G-code back-transform and tree support belt floor clipping
- Add BeltBackTransform class that inverts the shear/scale matrix and
  applies it in GCodeWriter::to_machine_coords() so G-code outputs in
  the machine's physical coordinate space, gated by new
  belt_gcode_back_transform config option
- Extend belt floor clipping to all three tree support pipelines
  (Prusa-style, Orca organic, TreeModelVolumes) with per-layer polygon
  clipping, anti-overhang integration, and belt raft extension layers
- Fix tree drop_nodes() belt termination, organic support global Z
  offset, collision calculation index bug, and first-layer brim/empty
  layer checks for belt printers

two-shot - first build built but didn't plumb to UI.  Woah.

add pre-slice axis remap, because Y needs to be Z

going to change tactic and move based on bbox min

switch to per axis snapping

per axis swap snap now per object

build plate tilt wasn't invalidating slicer settings

support upper bound now correct, need to get lower bound corrected

axis swapped support termination corrected

Z Shear works with and without pre-slice remap now
2026-04-09 23:07:07 -05:00
harrierpigeon ea5c6776b3 Part 2.6: Add belt floor support clipping for all support types
- Fix support clipping z-shift calculation by removing coordinate-space
  mismatch and sync belt_floor_z_shift with global_z_offset; fix
  invalidation so posSupportMaterial no longer resets slicing params
- Add belt floor polygon clipping to non-organic tree support
  (slim/strong/hybrid) with collision surface integration in
  TreeSupportData, belt extension layers, and first-layer brim
  suppression
- Add belt floor clipping to organic tree support pipeline with virtual
  belt raft layers, per-layer polygons in TreeModelVolumes, and
  post-generation layer trimming; fix pre-existing processing_last_mesh
  bug in calculateCollision()

Fix belt floor support clipping: z-shift, invalidation, and global offset

- Fix support clipping z-shift calculation by removing coordinate-space
  mismatch (raw_bounding_box min.z vs trafo_centered m_belt_min_z) and
  sync belt_floor_z_shift with global_z_offset in global shear mode
- Fix invalidation so posSupportMaterial no longer resets slicing params,
  preventing the exact posSlice z-shift from being overwritten by the
  bounding-box approximation on support-only setting changes
- Remove double-counting of global z_offset on support layers — support
  already inherits the offset from object layers during generation

This Work Was Co-Authored-By Claude Opus 4.6 (1M context) <noreply@anthropic.com>

UI: gray out inactive belt sub-options, rename to mesh transforms, move to Advanced

Fix mesh clipping through build plate after belt shear/scale transform

Generalize G-code viewer designed-view toggle for full belt transform

Clip support layers to transformed belt floor plane

Supports below the tilted build plate (Z = shear_factor * from_axis - min_z)
are now clipped via half-plane intersection after generation. Belt floor
parameters stored in SlicingParameters and populated in both update_slicing_parameters()
and the static slicing_parameters() overload.

Make belt G-code viewer toggle more prominent, add B keyboard shortcut

- Add separator + teal "Belt Printer" header in legend panel
- Append [B] hint to checkbox label
- Add B key shortcut in GLCanvas3D to toggle designed/machine view
- Read belt_printer_angle from loaded G-code headers to enable belt view

Add per-axis global transform option for belt printer shear

New belt_shear_{x,y,z}_global bool configs. When enabled, shear incorporates
instance shift so objects at different bed positions get position-aware
transform (Z += factor * instance_shift_on_from_axis).

Fix global shear: use layer Z offset instead of mesh transform, add config invalidation

- Global shear offset applied as post-slicing layer print_z adjustment
  instead of mesh transform (which was absorbed by min_z normalization
  or shifted mesh out of slice range)
- Register all belt transform options in Print::invalidate_state_by_config_options
  to trigger posSlice re-slicing (the fallback only invalidated Print steps,
  not PrintObject steps — belt changes had no effect without manual re-slice)
- Belt gcode remap options added to steps_gcode (gcode-export only)
- Skip empty-first-layer check for belt objects with global Z offset

WIP: split instances for global shear, relative Z offsets, debug logging

- PrintApply: when belt global mode active, prevent instance grouping by
  adding unique Z perturbation to trafo — each copy becomes its own
  PrintObject with independent layers
- PrintObjectSlice: compute global Z offset relative to minimum Y shift
  across all PrintObjects (lowest-Y object stays at Z=0)
- Debug logging (warning level) for belt global shift values and offsets

Known issues:
- Cached posSlice results cause stale offsets when mixing copies with
  individually-added objects — need to compute min baseline outside slice()
- Supports still generate to Z=0 instead of object's global Z offset

Fix global shear for copied objects: disable shared-object layer optimization

When belt global Z shear is active, each object needs unique layer Z
values based on its bed position. The shared-object optimization was
causing copies to reuse the source object's layers (and its Z offset)
instead of computing their own position-based offset.

started work on getting supports to work properly

one step forward, one step back

this version didn't quite work.  Getting somewhere though

about to add UI controllable tests

added configuration options for supports

tweak CLAUDE.md to be more aggressive for my machine.  This commit should probably be pulled out before contributing upstream

still chasing down some bugs

moving objects between slices no longer results in improper Z-height because of caching

added more data to the debug logs

Z offset is getting more global again

still not quite there, I think there's a fundamental logic flaw?

hunting for bugs

finally have a functional fix

Add belt floor clipping to tree supports (organic and non-organic)

- Add belt floor polygon clipping to non-organic tree support
  (slim/strong/hybrid) in draw_circles() and terminate nodes at the
  belt surface instead of the horizontal build plate
- Add belt floor clipping to organic tree support pipeline with virtual
  belt raft layers for sub-floor branch generation, per-layer belt
  floor polygons in TreeModelVolumes, and post-generation layer trimming
- Fix pre-existing processing_last_mesh bug in TreeModelVolumes that
  prevented m_anti_overhang (support blockers) from ever being applied;
  skip empty first layer check for belt printers

Commits:

current approach: make a face surface to build supports to

closer!

supports now terminate on shear plane, now need to get shear plane to correct Z height

nearly there

chasing down logic issues still

committing for checkpoint, this still does not work

still got logic problems...

cull support clipping

stashing changes for now.  Going to focus on getting the global shear OFF support generation dialed first.

beginning per object shear calcs

Local shear transform is on correct Z offset now

local shear finally works now and needs more testing

global shear works now, needs thorough testing

debugging non-45 degree angles

debugging part 2

supports at all angles work now

remove debug logging

Add belt floor collision to non-organic tree support pipeline

- Integrate belt floor as a collision surface in TreeSupportData so
  branches route around the belt naturally, replacing the explicit
  termination checks in drop_nodes()
- Add belt extension layers below the object after draw_circles() to
  allow support geometry to extend to the diagonal belt surface instead
  of terminating at a horizontal first layer
- Fix coordinate overflow in belt floor polygons (scale_(1e4) exceeds
  int32), skip first-layer brim expansion for belt printers, and
  extend empty first layer check bypass to all belt modes

add debug logging, Z translate for tree supports

still not seeing any cutoff surface yet

adding debug options

attempt #2 at trees

if hit Z buildplate stop but don't set to_buildplate true

getting closer

tree support almost there, just need to get rid of the circles at the beginning

getting closer

belt / shear plane clip works, need to figure out the buidlplate plane issues

more logic, added debugging logs

supports now extend somewhat below Z=0 in global shear mode

fix bad alloc, add 10mm below build plate

fully works now

shear transform + prusa tree support generation works now.

pull out debug logging
2026-04-09 23:07:07 -05:00
harrierpigeon 98f4d34dcb Part 2.5: Add global shear transform, support clipping, and belt UI improvements
- Implement per-object global shear transform in PrintObject with
  layer Z-offset calculation, config invalidation, and fix for
  shared-object layer optimization breaking copied objects
- Clip support layers to the transformed belt floor plane and begin
  work on tree support adaptation for sheared coordinate space
- Improve belt UI: gray out inactive sub-options, add B keyboard
  shortcut for G-code viewer design-view toggle, fix mesh clipping
  through build plate after shear/scale transform

y' = y + z·cot(α),
  while x' = x and z' = z

getting closer to customizable variant

getting closer

X/Y/Z shear initial

clean up UI

add 1/sin(a) transform, idea taken from blackbelt cura plugin

Things work now (turns out I've been using the wrong set of  transforms)
2026-04-09 23:07:07 -05:00
harrierpigeon 501aff7e53 Part 2: Replace belt rotation w/ per-axis shear transforms and G-code axis remap
- Replace monolithic belt rotation transform with independent per-axis
    shear controls (mode/angle/source-axis for X, Y, Z) and G-code axis
    remapping, giving full flexibility to match any belt printer's
    coordinate system
  - Remove all rotation mode logic and intermediate type+axes dropdowns,
    simplifying the pipeline to pure shear matrices while preserving the
    default behavior (Y += Z*cot(45deg) with identity remap)
  - Clean up GCodeWriter, GCodeProcessor, and GCodeViewer for the new
    shear-only model; expose 12 new settings in printer UI via
    Tab.cpp/Preset.cpp

Implement belt printer tilted slicing

Implement the core belt slicing pipeline that makes the slicer
tilt-aware:

Step 1: GCodeWriter::to_machine_coords() - R(+alpha, X) rotation
  from slicing frame to machine frame
Step 2: PrintObject - belt-rotated object height calculation
  (y*sin(a) + z*cos(a)) for correct layer count
Step 3: PrintObjectSlice - apply R(-alpha, X) rotation trafo so
  horizontal slice planes correspond to belt-parallel planes,
  with Z-shift computed from model volumes
Step 4: GCodeProcessor - machine-frame preview (no transform needed)
Step 5: 3DBed - rotate bed visualization about X by belt angle

Fix: belt surface IS the build plate, no mesh rotation

Currently still slicing perpendicular to the belt normal.  Need to figure out why.

Fix G-code Z sign: use R(-alpha, X) so Z+ is away from belt

The previous R(+alpha, X) transform produced negative Z values
(-y*sin(a) term dominated). Changed to R(-alpha, X) which gives
machine_z = y*sin(a) + z*cos(a), always positive for points
above the belt surface. Z increases with each layer as expected.

reverting and changing slice methodology

Add pink slicing direction arrow from origin

Shows the effective slicing direction (gantry normal) as a pink
arrow from the origin. Shorter and wider than the gravity arrow.
Direction: R(+alpha, X) * Z = (0, -sin(a), cos(a)), which is
the layer stacking direction in the original mesh frame.

Fix slicing arrow visibility and add raw G-code toggle

- Disable depth test for pink slicing arrow so it renders on top of
  the tilted bed geometry (was being occluded)
- Remove unnecessary 5mm Z-offset from arrow position
- Add m_belt_show_raw toggle to GCodeViewer
- Add "Show raw G-code (slicing frame)" checkbox in legend when
  belt mode is active

Implement to_machine_coords inverse rotation for belt printer G-code

The slicing pipeline rotates the mesh by R(-alpha, X) and shifts Z to
start at 0. The G-code output now undoes this transform via
to_machine_coords: R(+alpha, X) * T(0,0,+z_shift), recovering the
original machine-frame coordinates where Y is horizontal and Z is
vertical.

Changes:
- GCodeWriter: implement to_machine_coords with inverse rotation + Z-shift
- GCodeWriter: add belt_z_shift member and setter/getter
- GCode.cpp: compute Z-shift from print objects (same logic as
  PrintObjectSlice) and pass to writer; write z_shift to G-code header
- GCodeProcessor: parse belt_z_shift from G-code header
- GCodeViewer: store belt_z_shift from processor result

Wire raw G-code toggle to apply slicing-frame view transform

When "Show raw G-code (slicing frame)" is checked in the preview
legend, the view matrix is modified to apply R(-alpha, X) * T(0,0,-z_shift)
to the toolpath rendering. This shows the G-code as it was during
slicing: rotated part with horizontal layers.

Default (unchecked): machine-frame view — upright part with tilted layers.

Remove belt printer placeholder comment from GCodeProcessor

The preview now correctly displays machine-frame G-code with the
optional raw view toggle. No transform is needed in the processor.
2026-04-09 23:07:06 -05:00
harrierpigeon c808653565 Add belt printer transform pipeline: slicing rotation, G-code coords, preview
- Implement core belt slicing pipeline: R(-alpha, X) mesh rotation in PrintObjectSlice with corrected object height calculation for proper layer count
Add to_machine_coords() in GCodeWriter to convert slicing-frame coordinates back to machine-frame, propagated through GCode,
GCodeProcessor, and GCodeViewer
Add belt-mode UI: tilted bed visualization, slicing-direction arrow, and raw G-code toggle to switch between machine-frame and slicing-frame views

This is a combination of 6 commits.

checkpoint 1: initial MVP.  Slicing functions, but rotates instead of skews are happening and a lot of other stuff too

getting somewhere, getting to the point where I need to figure out how to verify this stuff

this appears to be a dead end.

getting somewhere I think maybe

I'm pretty sure we've completely lost the plot at this point and need to restart this process...

remove slice logic in preparation for new, more invasive plan
2026-04-09 23:07:06 -05:00
harrierpigeon a7441c7f48 stage in changes from off-plate-gravity and remove stuff I didn't need 2026-04-09 23:07:06 -05:00
Clifford GarwoodandClaude Sonnet 4.6 1d68f917b0 feat: add imex_mode/imex_mode_index/imex_mode_gcode placeholders with global var flow
Register imex_mode (string), imex_mode_index (int), and imex_mode_gcode
(string) in OtherSlicingStatesConfigDef so they appear in the placeholder
search UI under Slicing State.

Set all three via placeholder_parser().set() before any script processing
in _do_export(). Process imex_mode_gcode first so {global} declarations
defined there flow forward into machine_start_gcode.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 19:08:57 -04:00
Clifford GarwoodandClaude Sonnet 4.6 9a87363044 feat: 4-tool support, primary row, zone sizing, and stability fixes
IMEXModesCtrl:
- Primary mode is now a non-deletable first row stored as sentinel
  "primary" in imex_mode_names; older configs load cleanly
- New rows default T0 → Primary when no tool assignment is stored
- Filter "primary" from plater popup/cycle list to prevent double entry
- imex_tools_per_gantry cap raised 2 → 4

Zone sizing:
- Zone width/height now based on active tool count only; inactive tools
  donate their bed share to active neighbors (fixes 4-tool layout)
- Active col/row maps (col_to_zone/row_to_zone) applied consistently
  across zone fills, collision strips, and primary zone box

GCodeViewer animation:
- Mirror position formula fixed: left-of-copy reflects across copy
  zone's left edge; right-of-copy reflects across right edge
  (T3 was rendering on top of T1)
- strip_width/row_strip_height use active counts, matching PartPlate

Stability:
- Early return in calc_imex_zones() when tool_states is empty; prevents
  OOB crash on new printer with stale process-preset mode name
- is_imex toggle in on_config_change calls refresh_imex_icons() so the
  plate mode icon appears without requiring a new project

GCode:
- Remove "primary" guard so Primary mode gcode field is emitted at
  start of print

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 19:08:57 -04:00
Clifford GarwoodandClaude Sonnet 4.6 0d73cd13ba refactor: move IDEX/IQEX settings from standalone tab into Multimaterial section
- Remove standalone IDEX/IQEX printer tab
- Add IDEX/IQEX Configuration and Parallel Modes sections to the
  Multimaterial page, gated behind is_imex
- Carriage config options hidden via toggle_options() when is_imex is off
- Custom widgets (layout combo, theme combo, modes ctrl) show/hide accordingly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 19:08:57 -04:00
Clifford GarwoodandClaude Sonnet 4.6 461c69c83e refactor: rename iXex → IMEX throughout; use IDEX/IQEX as user-facing label
- All config keys: ixex_* → imex_* (is_imex, imex_gantry_count, etc.)
- All C++ identifiers: IXexModesCtrl → IMEXModesCtrl, m_ixex_* → m_imex_*, etc.
- 3MF serialization key: ixex_parallel_mode → imex_parallel_mode
- UI strings: iXex → IDEX/IQEX
- SVG icons: plate_ixex_mode*.svg → plate_imex_mode*.svg
- Remove unused ixex_mode.svg

Breaking change for existing printer configs — acceptable pre-merge.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 19:08:57 -04:00
Clifford Garwood 2934b18d2a Merge remote-tracking branch 'upstream/main' 2026-04-09 00:22:06 -04:00
Clifford GarwoodandClaude Sonnet 4.6 f540cf87a8 fix: preserve iXex tool assignments across gantry count changes
- Add all_tool_states map to IXexModesCtrl Row, storing all tool
  assignments including those not currently visible due to grid size
- active_tools_string() now serializes from all_tool_states so
  off-screen assignments survive the round trip through a smaller grid
- Button clicks keep all_tool_states in sync with visible btn_states
- Display anchors to the gantry row containing the Primary assignment
  so reducing gantry count keeps the meaningful row visible rather
  than always defaulting to row 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-08 23:35:32 -04:00
Clifford Garwood 48b9e6ab26 Merge branch 'main' into feedback 2026-04-07 23:56:30 -04:00
Clifford GarwoodandClaude Sonnet 4.6 5fa6480f61 feat: per-plate iXex mode selection with undo, dark mode icons, and menu fixes
- Add per-plate iXex mode icon to the plate toolbar (normal, hover, dark, dark-hover SVG variants)
- Left-click cycles through available modes; right-click shows a popup menu with all modes as radio items
- Mode changes are recorded in the undo/redo snapshot system
- Fix double context menu: suppress EVT_GLCANVAS_PLATE_RIGHT_CLICK and EVT_GLCANVAS_RIGHT_CLICK when the iXex icon popup was already shown
- Remove ixex_parallel_mode combo from Print Settings > Other > Special mode (superseded by per-plate icon)
- Remove dead code: refresh_ixex_mode_combo(), m_ixex_mode_combo member, related Tab reload hook
- iXex mode persisted in 3MF project files via existing plate metadata serialization

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 23:25:38 -04:00
Clifford GarwoodandClaude Sonnet 4.6 ec40172019 feat: per-plate iXex mode selection with undo, dark mode icons, and menu fixes
- Add per-plate iXex mode icon to the plate toolbar (normal, hover, dark, dark-hover SVG variants)
- Left-click cycles through available modes; right-click shows a popup menu with all modes as radio items
- Mode changes are recorded in the undo/redo snapshot system
- Fix double context menu: suppress EVT_GLCANVAS_PLATE_RIGHT_CLICK and EVT_GLCANVAS_RIGHT_CLICK when the iXex icon popup was already shown
- Remove ixex_parallel_mode combo from Print Settings > Other > Special mode (superseded by per-plate icon)
- Remove dead code: refresh_ixex_mode_combo(), m_ixex_mode_combo member, related Tab reload hook
- iXex mode persisted in 3MF project files via existing plate metadata serialization

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 23:25:22 -04:00
Clifford f25a433ec7 Merge branch 'OrcaSlicer:main' into main 2026-04-07 22:50:22 -04:00
SoftFever 3bc13e5cfd add a generic belt printer 2026-04-07 10:37:34 +08:00
Clifford Garwood f8565df113 Merge branch 'feedback' into main
Includes two feature sets on top of the base iXex implementation:

Auto-arrange iXex zone constraint:
  Constrains placement to the primary zone when a parallel mode is active.
  Collision strips registered as hard obstacles via m_unselected so the
  NFP placer correctly excludes them.

Nozzle clearance rename + carriage box visualization overhaul:
  ixex_carriage_width_x/y → ixex_nozzle_clearance_x/y (breaking rename).
  Collision strip width now uses the literal clearance value, not half.
  Carriage footprint boxes place the nozzle at the correct physical edge
  for all printer types (IDEX, IQEX 2x2) and modes (copy, mirror).
  Fixed GLModel::reset() bug causing stale mesh on config change.
2026-04-06 15:06:49 -04:00
Clifford GarwoodandClaude Sonnet 4.6 167211c85c refactor: rename ixex_carriage_width → ixex_nozzle_clearance; fix strip math and carriage box visualization
Config key rename (breaking for saved profiles — call out in PR):
  ixex_carriage_width_x/y → ixex_nozzle_clearance_x/y
  Labels updated to "Nozzle Clearance X/Y" with consistent tooltips
  describing the measurement as nozzle-to-collision-side-edge distance.

Strip math fix:
  Previously halved the clearance value (× 0.5) under the assumption
  the nozzle was centered in the carriage. The measurement is now the
  literal nozzle-to-edge distance, so the × 0.5 factor is removed.
  The collision strip width now equals the configured value directly.

Carriage box visualization (GCodeViewer):
  - Add per-carriage box_offset_x/y so the nozzle marker sits at the
    physically correct edge of the footprint box rather than centered.
  - X: zone-based by default (nozzle at inner edge facing bed center).
    Copy secondaries inherit the primary's X orientation (same movement
    direction). Mirror secondaries use the collision-side edge.
  - Y: always row-based regardless of copy/mirror mode. Gantry is always
    behind the nozzle (high-Y); front-row primaries with a back-row
    secondary override to place nozzle at the low-Y edge.
  - Fix stale mesh bug: GLModel::init_from() is a no-op when already
    initialized. Call reset() before init_from() so mesh rebuilds
    correctly when nozzle clearance values change in config.
  - Remove m_ixex_toolhead_box_dims (was the now-unnecessary cache key).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 14:57:57 -04:00
Clifford GarwoodandClaude Sonnet 4.6 a9ad17cea2 fix: route iXex collision zones through m_unselected for effective exclusion
params.excluded_regions flows into libnest2d PlacementConfig.m_excluded_regions
which is declared but never consumed by the NFP placer — the field is dead code.
m_unselected is the working path: it becomes fixeditems that are preloaded as
physical fixed obstacles in the NFP computation, same as the wipe tower.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 14:57:57 -04:00
Clifford GarwoodandClaude Sonnet 4.6 1cacc6d02d fix: exclude iXex collision strips from auto-arrange placement
Auto-arrange now injects the carriage collision zones as hard excluded
regions (is_virt_object) when an iXex parallel mode is active, using the
same BoundingBoxf3 data already stored for rendering and violation checks.
Objects will no longer be placed in the mirror-edge danger strips.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 14:57:57 -04:00
Clifford GarwoodandClaude Sonnet 4.6 b9f7148080 fix: constrain auto-arrange to iXex primary zone when parallel mode is active
Adds std::optional<BoundingBoxf> m_ixex_primary_zone_box to PartPlate,
populated by calc_ixex_zones() alongside the existing secondary/collision
zone geometry. The new ixex_primary_zone() getter calls ensure_ixex_zones()
so callers always get fresh data. ArrangeJob::process() replaces the
full-bed bedpts with the primary zone corners when the getter returns a
value, so auto-arrange no longer drops objects into the bed center.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 14:57:56 -04:00
Clifford GarwoodandClaude Sonnet 4.6 a3cac6c139 fix: remove iXex zone outline borders to eliminate aliasing flicker
The thin rectangular outlines around copy/mirror zones caused visible
aliasing and appeared to flash during interaction. Removed the border
GLModel, its build code in calc_ixex_zones(), the render block, and
the unused border field from IXexTheme.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 14:57:40 -04:00
Clifford GarwoodandClaude Sonnet 4.6 185b7084d2 fix: null m_ixex_mode_combo in TabPrint::clear_pages() to prevent dangling pointer crash
Same class of bug as the TabPrinter fix: clear_pages() destroys all
page widgets but didn't null m_ixex_mode_combo, causing a SIGSEGV in
refresh_ixex_mode_combo() when load_current_preset() ran after a
page rebuild (e.g. triggered by Plater::reset()).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 14:57:39 -04:00
Clifford GarwoodandClaude Sonnet 4.6 f4808ae7ba fix: avoid capturing C array in lambda for ixex_tool_layout combo
Arrays cannot be captured by value in C++ lambdas; replaced with a
static local declared inside the event handler.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 14:57:39 -04:00
Clifford GarwoodandClaude Sonnet 4.6 5297417518 fix: remove phantom phys_row variable reference from iXex button layout comment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 14:57:39 -04:00
Clifford GarwoodandClaude Sonnet 4.6 a03d2cd566 fix: restore info log level in two commented-out PartPlate log lines
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 14:57:39 -04:00
Clifford GarwoodandClaude Sonnet 4.6 c75231ec03 fix: remove spurious blank lines in PrintConfig.hpp
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 14:57:39 -04:00
Clifford GarwoodandClaude Sonnet 4.6 e786fabb6a fix: restore info log levels in PartPlate accidentally raised to warning
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 14:57:39 -04:00
Clifford Garwood 1e56b65146 feat: Add iXex parallel printing support for IDEX and IQEX printers
Introduces first-class parallel printing (copy/mirror modes) for printers
with multiple independent X-axis carriages. Branded iXex (independent X
extruder), targeting Klipper firmware with a firmware-agnostic design.

- PrintConfig: new printer options declaring iXex capability and geometry
  (is_ixex, ixex_gantry_count, ixex_tools_per_gantry, carriage dims,
  tool layout, and per-mode name/role/gcode arrays)
- Preset: iXex keys registered in printer and process preset option lists
- Tab: IXexModesCtrl visual grid editor in Printer preset tab; mode
  dropdown in Process → Others tab; clear_pages() nulls iXex pointers to
  prevent dangling-pointer crash on preset save
- PartPlate: 2D zone visualization (active/dimmed/dividers) and
  placement-violation detection (has_ixex_placement_violations) that
  blocks slicing when objects fall outside the primary zone
- Plater: violation detection wired into update_background_process so
  the Slice button is disabled with an error notification on violation
- GCode: mode-activation G-code injected before machine_start_gcode
- GCodeViewer: multi-carriage toolhead markers in sequential preview,
  filament legend annotated with active carriage count and mode name
2026-04-06 14:57:39 -04:00
SoftFever 141749a6f2 Merge branch 'main' into belt/baseChanges 2026-04-06 22:52:31 +08:00
harrierpigeon 4634a5dfd7 switch default shear axis, swap to tan(a) instead of cot(a) 2026-03-30 13:25:40 -05:00
harrierpigeon 372139c770 revert CLAUDE.md 2026-03-30 13:25:40 -05:00
harrierpigeon 44eebdb8ad Part 3.2: decouple axis remapping, enable viewing settings in Developer mode or when Belt mode is active 2026-03-30 13:25:40 -05:00
harrierpigeon c7aa4ca3ef Part 3.1: refactor BeltTransform pipeline
add BeltGCodeWriter

add BeltGCode

consolidate changes into shared classes for BeltGcode
2026-03-30 13:25:40 -05:00
harrierpigeon b297f68921 Part 2.7: Add G-code back-transform and tree support belt floor clipping
- Add BeltBackTransform class that inverts the shear/scale matrix and
  applies it in GCodeWriter::to_machine_coords() so G-code outputs in
  the machine's physical coordinate space, gated by new
  belt_gcode_back_transform config option
- Extend belt floor clipping to all three tree support pipelines
  (Prusa-style, Orca organic, TreeModelVolumes) with per-layer polygon
  clipping, anti-overhang integration, and belt raft extension layers
- Fix tree drop_nodes() belt termination, organic support global Z
  offset, collision calculation index bug, and first-layer brim/empty
  layer checks for belt printers

two-shot - first build built but didn't plumb to UI.  Woah.

add pre-slice axis remap, because Y needs to be Z

going to change tactic and move based on bbox min

switch to per axis snapping

per axis swap snap now per object

build plate tilt wasn't invalidating slicer settings

support upper bound now correct, need to get lower bound corrected

axis swapped support termination corrected

Z Shear works with and without pre-slice remap now
2026-03-30 13:25:40 -05:00
harrierpigeon 7ff6bc42b1 Part 2.6: Add belt floor support clipping for all support types
- Fix support clipping z-shift calculation by removing coordinate-space
  mismatch and sync belt_floor_z_shift with global_z_offset; fix
  invalidation so posSupportMaterial no longer resets slicing params
- Add belt floor polygon clipping to non-organic tree support
  (slim/strong/hybrid) with collision surface integration in
  TreeSupportData, belt extension layers, and first-layer brim
  suppression
- Add belt floor clipping to organic tree support pipeline with virtual
  belt raft layers, per-layer polygons in TreeModelVolumes, and
  post-generation layer trimming; fix pre-existing processing_last_mesh
  bug in calculateCollision()

Fix belt floor support clipping: z-shift, invalidation, and global offset

- Fix support clipping z-shift calculation by removing coordinate-space
  mismatch (raw_bounding_box min.z vs trafo_centered m_belt_min_z) and
  sync belt_floor_z_shift with global_z_offset in global shear mode
- Fix invalidation so posSupportMaterial no longer resets slicing params,
  preventing the exact posSlice z-shift from being overwritten by the
  bounding-box approximation on support-only setting changes
- Remove double-counting of global z_offset on support layers — support
  already inherits the offset from object layers during generation

This Work Was Co-Authored-By Claude Opus 4.6 (1M context) <noreply@anthropic.com>

UI: gray out inactive belt sub-options, rename to mesh transforms, move to Advanced

Fix mesh clipping through build plate after belt shear/scale transform

Generalize G-code viewer designed-view toggle for full belt transform

Clip support layers to transformed belt floor plane

Supports below the tilted build plate (Z = shear_factor * from_axis - min_z)
are now clipped via half-plane intersection after generation. Belt floor
parameters stored in SlicingParameters and populated in both update_slicing_parameters()
and the static slicing_parameters() overload.

Make belt G-code viewer toggle more prominent, add B keyboard shortcut

- Add separator + teal "Belt Printer" header in legend panel
- Append [B] hint to checkbox label
- Add B key shortcut in GLCanvas3D to toggle designed/machine view
- Read belt_printer_angle from loaded G-code headers to enable belt view

Add per-axis global transform option for belt printer shear

New belt_shear_{x,y,z}_global bool configs. When enabled, shear incorporates
instance shift so objects at different bed positions get position-aware
transform (Z += factor * instance_shift_on_from_axis).

Fix global shear: use layer Z offset instead of mesh transform, add config invalidation

- Global shear offset applied as post-slicing layer print_z adjustment
  instead of mesh transform (which was absorbed by min_z normalization
  or shifted mesh out of slice range)
- Register all belt transform options in Print::invalidate_state_by_config_options
  to trigger posSlice re-slicing (the fallback only invalidated Print steps,
  not PrintObject steps — belt changes had no effect without manual re-slice)
- Belt gcode remap options added to steps_gcode (gcode-export only)
- Skip empty-first-layer check for belt objects with global Z offset

WIP: split instances for global shear, relative Z offsets, debug logging

- PrintApply: when belt global mode active, prevent instance grouping by
  adding unique Z perturbation to trafo — each copy becomes its own
  PrintObject with independent layers
- PrintObjectSlice: compute global Z offset relative to minimum Y shift
  across all PrintObjects (lowest-Y object stays at Z=0)
- Debug logging (warning level) for belt global shift values and offsets

Known issues:
- Cached posSlice results cause stale offsets when mixing copies with
  individually-added objects — need to compute min baseline outside slice()
- Supports still generate to Z=0 instead of object's global Z offset

Fix global shear for copied objects: disable shared-object layer optimization

When belt global Z shear is active, each object needs unique layer Z
values based on its bed position. The shared-object optimization was
causing copies to reuse the source object's layers (and its Z offset)
instead of computing their own position-based offset.

started work on getting supports to work properly

one step forward, one step back

this version didn't quite work.  Getting somewhere though

about to add UI controllable tests

added configuration options for supports

tweak CLAUDE.md to be more aggressive for my machine.  This commit should probably be pulled out before contributing upstream

still chasing down some bugs

moving objects between slices no longer results in improper Z-height because of caching

added more data to the debug logs

Z offset is getting more global again

still not quite there, I think there's a fundamental logic flaw?

hunting for bugs

finally have a functional fix

Add belt floor clipping to tree supports (organic and non-organic)

- Add belt floor polygon clipping to non-organic tree support
  (slim/strong/hybrid) in draw_circles() and terminate nodes at the
  belt surface instead of the horizontal build plate
- Add belt floor clipping to organic tree support pipeline with virtual
  belt raft layers for sub-floor branch generation, per-layer belt
  floor polygons in TreeModelVolumes, and post-generation layer trimming
- Fix pre-existing processing_last_mesh bug in TreeModelVolumes that
  prevented m_anti_overhang (support blockers) from ever being applied;
  skip empty first layer check for belt printers

Commits:

current approach: make a face surface to build supports to

closer!

supports now terminate on shear plane, now need to get shear plane to correct Z height

nearly there

chasing down logic issues still

committing for checkpoint, this still does not work

still got logic problems...

cull support clipping

stashing changes for now.  Going to focus on getting the global shear OFF support generation dialed first.

beginning per object shear calcs

Local shear transform is on correct Z offset now

local shear finally works now and needs more testing

global shear works now, needs thorough testing

debugging non-45 degree angles

debugging part 2

supports at all angles work now

remove debug logging

Add belt floor collision to non-organic tree support pipeline

- Integrate belt floor as a collision surface in TreeSupportData so
  branches route around the belt naturally, replacing the explicit
  termination checks in drop_nodes()
- Add belt extension layers below the object after draw_circles() to
  allow support geometry to extend to the diagonal belt surface instead
  of terminating at a horizontal first layer
- Fix coordinate overflow in belt floor polygons (scale_(1e4) exceeds
  int32), skip first-layer brim expansion for belt printers, and
  extend empty first layer check bypass to all belt modes

add debug logging, Z translate for tree supports

still not seeing any cutoff surface yet

adding debug options

attempt #2 at trees

if hit Z buildplate stop but don't set to_buildplate true

getting closer

tree support almost there, just need to get rid of the circles at the beginning

getting closer

belt / shear plane clip works, need to figure out the buidlplate plane issues

more logic, added debugging logs

supports now extend somewhat below Z=0 in global shear mode

fix bad alloc, add 10mm below build plate

fully works now

shear transform + prusa tree support generation works now.

pull out debug logging
2026-03-30 13:25:40 -05:00
harrierpigeon 719af2d81d Part 2.5: Add global shear transform, support clipping, and belt UI improvements
- Implement per-object global shear transform in PrintObject with
  layer Z-offset calculation, config invalidation, and fix for
  shared-object layer optimization breaking copied objects
- Clip support layers to the transformed belt floor plane and begin
  work on tree support adaptation for sheared coordinate space
- Improve belt UI: gray out inactive sub-options, add B keyboard
  shortcut for G-code viewer design-view toggle, fix mesh clipping
  through build plate after shear/scale transform

y' = y + z·cot(α),
  while x' = x and z' = z

getting closer to customizable variant

getting closer

X/Y/Z shear initial

clean up UI

add 1/sin(a) transform, idea taken from blackbelt cura plugin

Things work now (turns out I've been using the wrong set of  transforms)
2026-03-30 13:25:40 -05:00
harrierpigeon cb13a22e57 Part 2: Replace belt rotation w/ per-axis shear transforms and G-code axis remap
- Replace monolithic belt rotation transform with independent per-axis
    shear controls (mode/angle/source-axis for X, Y, Z) and G-code axis
    remapping, giving full flexibility to match any belt printer's
    coordinate system
  - Remove all rotation mode logic and intermediate type+axes dropdowns,
    simplifying the pipeline to pure shear matrices while preserving the
    default behavior (Y += Z*cot(45deg) with identity remap)
  - Clean up GCodeWriter, GCodeProcessor, and GCodeViewer for the new
    shear-only model; expose 12 new settings in printer UI via
    Tab.cpp/Preset.cpp

Implement belt printer tilted slicing

Implement the core belt slicing pipeline that makes the slicer
tilt-aware:

Step 1: GCodeWriter::to_machine_coords() - R(+alpha, X) rotation
  from slicing frame to machine frame
Step 2: PrintObject - belt-rotated object height calculation
  (y*sin(a) + z*cos(a)) for correct layer count
Step 3: PrintObjectSlice - apply R(-alpha, X) rotation trafo so
  horizontal slice planes correspond to belt-parallel planes,
  with Z-shift computed from model volumes
Step 4: GCodeProcessor - machine-frame preview (no transform needed)
Step 5: 3DBed - rotate bed visualization about X by belt angle

Fix: belt surface IS the build plate, no mesh rotation

Currently still slicing perpendicular to the belt normal.  Need to figure out why.

Fix G-code Z sign: use R(-alpha, X) so Z+ is away from belt

The previous R(+alpha, X) transform produced negative Z values
(-y*sin(a) term dominated). Changed to R(-alpha, X) which gives
machine_z = y*sin(a) + z*cos(a), always positive for points
above the belt surface. Z increases with each layer as expected.

reverting and changing slice methodology

Add pink slicing direction arrow from origin

Shows the effective slicing direction (gantry normal) as a pink
arrow from the origin. Shorter and wider than the gravity arrow.
Direction: R(+alpha, X) * Z = (0, -sin(a), cos(a)), which is
the layer stacking direction in the original mesh frame.

Fix slicing arrow visibility and add raw G-code toggle

- Disable depth test for pink slicing arrow so it renders on top of
  the tilted bed geometry (was being occluded)
- Remove unnecessary 5mm Z-offset from arrow position
- Add m_belt_show_raw toggle to GCodeViewer
- Add "Show raw G-code (slicing frame)" checkbox in legend when
  belt mode is active

Implement to_machine_coords inverse rotation for belt printer G-code

The slicing pipeline rotates the mesh by R(-alpha, X) and shifts Z to
start at 0. The G-code output now undoes this transform via
to_machine_coords: R(+alpha, X) * T(0,0,+z_shift), recovering the
original machine-frame coordinates where Y is horizontal and Z is
vertical.

Changes:
- GCodeWriter: implement to_machine_coords with inverse rotation + Z-shift
- GCodeWriter: add belt_z_shift member and setter/getter
- GCode.cpp: compute Z-shift from print objects (same logic as
  PrintObjectSlice) and pass to writer; write z_shift to G-code header
- GCodeProcessor: parse belt_z_shift from G-code header
- GCodeViewer: store belt_z_shift from processor result

Wire raw G-code toggle to apply slicing-frame view transform

When "Show raw G-code (slicing frame)" is checked in the preview
legend, the view matrix is modified to apply R(-alpha, X) * T(0,0,-z_shift)
to the toolpath rendering. This shows the G-code as it was during
slicing: rotated part with horizontal layers.

Default (unchecked): machine-frame view — upright part with tilted layers.

Remove belt printer placeholder comment from GCodeProcessor

The preview now correctly displays machine-frame G-code with the
optional raw view toggle. No transform is needed in the processor.
2026-03-30 13:25:40 -05:00
harrierpigeon ed6ea086a2 Add belt printer transform pipeline: slicing rotation, G-code coords, preview
- Implement core belt slicing pipeline: R(-alpha, X) mesh rotation in PrintObjectSlice with corrected object height calculation for proper layer count
Add to_machine_coords() in GCodeWriter to convert slicing-frame coordinates back to machine-frame, propagated through GCode,
GCodeProcessor, and GCodeViewer
Add belt-mode UI: tilted bed visualization, slicing-direction arrow, and raw G-code toggle to switch between machine-frame and slicing-frame views

This is a combination of 6 commits.

checkpoint 1: initial MVP.  Slicing functions, but rotates instead of skews are happening and a lot of other stuff too

getting somewhere, getting to the point where I need to figure out how to verify this stuff

this appears to be a dead end.

getting somewhere I think maybe

I'm pretty sure we've completely lost the plot at this point and need to restart this process...

remove slice logic in preparation for new, more invasive plan
2026-03-30 13:25:40 -05:00
harrierpigeon 08aa277974 stage in changes from off-plate-gravity and remove stuff I didn't need 2026-03-30 13:25:40 -05:00
3596 changed files with 2656843 additions and 2544567 deletions
+75
View File
@@ -0,0 +1,75 @@
# clang-tidy configuration, enforced by the clang-tidy CI job on the lines a pull
# request changes (scripts/clang_tidy_diff.py). Two things are reported.
# Missing includes: a file should include the header for every symbol it uses, not
# rely on the precompiled header or another header's includes. Run with --fix to
# add them.
# Using-directives and using-declarations in the global namespace of a header:
# they reach every file that includes the header, and a using-declaration also
# makes the header look like the one to include for that name. Qualify the name
# in the header, and put the using in the source files that want it.
# Every check listed here gates pull requests, so enable a new one only once the
# code it flags on touched lines is reasonable to fix in passing.
Checks: '-*,misc-include-cleaner,google-global-names-in-headers'
WarningsAsErrors: '*'
CheckOptions:
# Missing includes only. Builds without the precompiled header break on these.
misc-include-cleaner.UnusedIncludes: false
# Headers that declare a symbol but are not the one to include: per-platform
# implementations of wxWidgets and Boost.Thread (a Linux run would suggest the
# GTK or pthread one), library internals and forward declarations (MSVC's STL
# __msvc_* and the Windows UCRT's corecrt_* included), CPython's headers behind
# Python.h (python3.x/ on Linux and macOS, libpython/include/ on Windows),
# curl's behind curl.h, oneTBB's behind tbb/, and
# admesh's stl.h, which the include path also exposes without its directory.
# Clipper's own clipper.hpp is only included through libslic3r/clipper.hpp or
# clipper_z.hpp, which configure it first, and Boost.Polygon's headers only
# work through boost/polygon/polygon.hpp or voronoi.hpp. Clipper2's headers
# are included through clipper2/clipper.h, or clipper2_z.hpp, which defines
# USINGZ first. minilzo's config
# headers are internal to minilzo.h.
# FFmpeg's C headers are left alone because they are only included inside
# extern "C", which an inserted include would miss. OS-specific headers (GLib,
# GTK, D-Bus, POSIX, the Windows SDK) are only used inside platform #if blocks,
# and an include added at the top of the file would break the other platforms'
# builds. A symbol these provide is not reported missing.
# Write every / as [/\\] so the patterns also match Windows paths.
# No comments inside the block below, because a # there silently becomes part of a pattern.
misc-include-cleaner.IgnoreHeaders: >-
wx[/\\](gtk|gtk1|msw|osx|unix|x11|motif|univ|qt|dfb|generic|private)[/\\].*;
.*[/\\]detail[/\\].*;
.*[/\\]impl[/\\].*;
.*_fwd\.hpp;
python3\.[0-9]+[/\\].*;
libpython[/\\]include[/\\].*;
bits[/\\].*;
corecrt_.*\.h;
__msvc_.*\.hpp;
boost[/\\]multiprecision[/\\]fwd\.hpp;
imconfig\.h;
expat_external\.h;
admesh[/\\]stl\.h;
boost[/\\]thread[/\\](pthread|win32)[/\\].*;
boost[/\\]regex[/\\]v[0-9]+[/\\].*;
curl[/\\](easy|multi|system|urlapi|header|options|websockets|mprintf)\.h;
oneapi[/\\]tbb[/\\].*;
opencv2[/\\]core[/\\]hal[/\\].*;
openssl[/\\]ossl_typ\.h;
libav[a-z]+[/\\].*;
libsw[a-z]+[/\\].*;
glib-2\.0[/\\].*;
gtk-3\.0[/\\].*;
dbus-1\.0[/\\].*;
sys[/\\].*;
unistd\.h;
strings\.h;
fcntl\.h;
termios\.h;
[/\\](um|shared)[/\\].*;
sal\.h;
tchar\.h;
clipper[/\\]clipper\.hpp;
png(lib)?conf\.h;
mcut[/\\]platform\.h;
boost[/\\]polygon[/\\].*;
clipper2[/\\]clipper\.(core|engine|offset|minkowski|rectclip|export|triangulation|version)\.h;
lzo(conf|defs)\.h
@@ -105,11 +105,12 @@ Rules:
`single_extruder_multi_material` off, and in the app when the printer tab loads a printer with a
different number of extruders, the pair is rebuilt from `extruder_variant_list` (one
`Direct Drive Standard` per extruder when the list is absent) and the variant arrays are resized to
the rebuilt pair, padded with their first value or cut. The resize skips the `machine_max_*` limits
and `hotend_heating_rate` / `hotend_cooling_rate`: they keep their width, and an extruder beyond it
reads their first value, so extruder 2 and up of a list-less printer take extruder 1's normal limit
as their silent one too. With the three in agreement that changes nothing; a pair written without
the list is replaced. A listed variant the pair lacks is a menu choice that reads variant index 0.
the rebuilt pair, padded with their first value or cut. The `machine_max_*` limits are padded the
same way, so extruder 2 and up of a list-less printer take extruder 1's normal limit as their
silent one too. The resize skips `hotend_heating_rate` / `hotend_cooling_rate`: they keep their
width, and an extruder beyond it reads their first value. With the three in agreement that changes
nothing; a pair written without the list is replaced. A listed variant the pair lacks is a menu
choice that reads variant index 0.
- The pair without `extruder_variant_list` slices, but the sidebar offers no variant switch and
the app cannot add variants to a list-less process: nothing is lost while every extruder
has exactly one variant, and every further variant is unreachable.
+253
View File
@@ -0,0 +1,253 @@
---
name: orca-wxwidgets
description: Use when writing, modifying, reviewing or debugging any OrcaSlicer GUI code under src/slic3r/GUI, or when deciding how a wxWidgets API behaves in OrcaSlicer — dialogs, frames, panels, the sidebar, Preferences, device/monitor pages, custom widgets, popups and menus, sizers and layout, painting, DPI scaling, dark mode, colours, icons, fonts, translated strings, settings fields, keyboard shortcuts, mouse capture and focus, events, CallAfter, threads and timers, WebView, the OpenGL canvas, AUI docking, clipboard, drag and drop, file dialogs — and for platform-specific UI bugs on Windows, macOS or Linux GTK/X11/Wayland such as popups that close at once, a frozen or unclickable UI, collapsed or clipped dialogs, invisible dark-mode icons, or crashes on close, even when the task never mentions wxWidgets.
---
# OrcaSlicer wxWidgets GUI
This skill is how OrcaSlicer's wxWidgets GUI is written, changed, fixed and reviewed. Its core is
**wxWidgets 3.3.2 API usage** — the documented contracts, the per-platform behaviour and the known
limitations of the wx version Orca pins — layered with the OrcaSlicer conventions, wrappers, custom
widget library and stable component designs a contributor must follow. Every reference file opens
with a numbered **Rules** checklist (what a diff is checked against), followed by sections that give
the wx contract, the correct code shape, platform differences, the Orca layer, and pitfalls as
wrong → right pairs with the commit that fixed each one.
## Ground truth: the wx tree in deps/
Orca pins **wxWidgets 3.3.2** from the fork `github.com/SoftFever/Orca-deps-wxWidgets` (tag `v3.3.2`,
`deps/wxWidgets/wxWidgets.cmake`), built static (Flatpak: shared). Facts about this build that change
how you read the wx docs:
- **Linux builds against GTK3** (`DEP_WX_GTK3` defaults ON in `deps/CMakeLists.txt`). Target GTK3 on
both X11 and Wayland; GTK-guarded code must still compile on GTK2, an opt-out build Orca does not ship.
- **wx asserts never fire.** wx is built with `wxBUILD_DEBUG_LEVEL=0` and `libslic3r_gui` with
`wxDEBUG_LEVEL=0`. Wherever the docs say a call "asserts", Orca silently ignores it, returns early or
corrupts state. Check preconditions yourself; do not expect a debug build to catch misuse.
- **No SVG in wx** (`wxUSE_NANOSVG=OFF`): `wxBitmapBundle::FromSVG*` does not exist. Orca rasterises
its SVG icons itself (`BitmapCache`, `create_scaled_bitmap`, `ScalableBitmap`).
Look things up in the source the app is built from — it beats memory, and 3.3 changed real behaviour:
```bash
WX=$(find -L deps -maxdepth 5 -type d -path '*dep_wxWidgets-prefix/src/dep_wxWidgets' 2>/dev/null | head -1)
# macOS: deps/build/<arch>/dep_wxWidgets-prefix/src/dep_wxWidgets Linux, Windows: deps/<tree>/dep_wxWidgets-prefix/...
# -L follows a worktree's deps/<tree> symlinked to the main checkout. Not a glob: zsh aborts on one that matches nothing.
# If deps are not built: git clone --depth 1 -b v3.3.2 https://github.com/SoftFever/Orca-deps-wxWidgets
grep -n "CaptureMouse" -A 30 $WX/interface/wx/window.h # documented contract (doxygen source)
grep -rn "@onlyfor\|not implemented" $WX/interface/wx/popupwin.h # documented platform limits
ls $WX/docs/doxygen/overviews/ # eventhandling.h, sizer.h, high_dpi.md, windowdeletion.h, ...
grep -n "IsDark" $WX/docs/changes.txt # what changed in 3.3 (changes_32.txt for 3.2)
grep -n "NotifyCaptureLost" -r $WX/src/osx $WX/src/gtk $WX/src/msw # what each port actually does
```
On Windows these lookups are bash: run them from Git Bash. PowerShell has no `grep`, and its `find` is
Windows' text-search `find.exe`. To locate the wx tree from PowerShell:
```powershell
$WX = Resolve-Path deps\*\dep_wxWidgets-prefix\src\dep_wxWidgets, deps\*\*\dep_wxWidgets-prefix\src\dep_wxWidgets -ErrorAction SilentlyContinue | Select-Object -First 1 -ExpandProperty Path
```
`interface/wx/<class>.h` is the documentation; `src/common` holds shared behaviour and
`src/{msw,osx,gtk,unix,generic}` the per-port implementation. When the docs and the source disagree,
the source is what runs — the references mark such facts **[source]**. Orca-side design docs live in
`docs/HLSD/` (`keyboard-shortcuts.md`, `deferred-page-construction.md`, `design-tab.md`,
`printer-agent.md`).
## Golden rules
1. **Interactive controls are Orca widgets** (`Button` + `SetStyle(...)`, `::CheckBox`, `::ComboBox`,
`::TextInput`, `SpinInput`, `SwitchButton`, `RadioGroup`, `TabCtrl`); the bottom row of every dialog is
`DialogButtons`. Never `wxButton`, `wxSpinCtrl`, `wxCheckBox`, `wxChoice` or a single-line `wxTextCtrl`
in new code; multi-line text is a raw `wxTextCtrl`. Inside `Slic3r::GUI` write `::CheckBox` etc. —
`Field.hpp` has classes with the same names. → `orca-widgets.md`
2. **DPI.** Layout pixel values go through `FromDIP(n)` (or `n * em_unit()`); `wxBitmap` constructor
sizes, image-list sizes and GL viewports are physical and are not `FromDIP`'d (icon heights passed to
`create_scaled_bitmap`/`ScalableBitmap`/`Button` are DIP). Top-level windows are `DPIDialog`/
`DPIFrame`; `on_dpi_changed` re-rasterises bitmaps (and re-sets them on the controls showing them),
calls each widget's `Rescale()`, re-applies stored sizes and finishes with
`GetSizer()->SetSizeHints(this)`; it never runs on macOS. → `dpi-bitmaps-fonts.md`, `sizers-layout.md`
3. **Dark mode.** Ask `wxGetApp().dark_mode()`, never `wxSystemSettings::GetAppearance().IsDark()`. Use
palette colours through `StateColor` (specific states first, `Normal` last) and pass a literal light
colour through `StateColor::darkModeColorFor()`; give every panel you create an explicit palette
background before creating widgets in it; end each dialog constructor with
`wxGetApp().UpdateDlgDarkUI(this)`; re-apply hand-picked colours and name-selected icons in
`on_sys_color_changed()` (a cached or long-lived window must also be reached from
`MainFrame::on_sys_color_changed`). → `colours-dark-mode.md`
4. **Strings.** Mark user text with `_L` / `_u8L` / `L` (and the `_CONTEXT` / `_L_PLURAL` forms) — never
`_()`, which xgettext does not extract. Build messages with `format_wxstr(_L("… %1% …"), arg)`;
convert with `from_u8()` / `into_u8()` (`from_path()` / `into_path()` for paths), never `ToStdString()`
or an implicit `std::string` → `wxString`. → `strings-i18n-files.md`
5. **Messages to the user** use the `MsgDialog` family (`MessageDialog`, `RichMessageDialog`,
`WarningDialog`, `ErrorDialog`, `InfoDialog`, `show_error`/`show_info`), never `wxMessageBox` once the
GUI exists. ESC and the close box return `wxID_CANCEL`, so test for the positive answer
(`== wxID_YES`); `show_error` is asynchronous. → `windows-dialogs.md`
6. **Events.** `Bind()` with handlers taking the event **by reference**; no new static event tables.
`Skip()` every non-command event you do not fully replace (focus, size, key, DPI, colour change, mouse
on custom widgets), and any event — command events included, e.g. `::CheckBox`'s
`wxEVT_TOGGLEBUTTON` — bound on an Orca widget, wx control or window whose own class also handles it:
your later-bound handler runs first. Unbind lambdas bound on other objects. → `events.md`
7. **Threads.** Only the main thread touches wx. Workers marshal with `wxGetApp().CallAfter([by-value
captures]{ … })` and the lambda re-checks liveness (a `std::shared_ptr<std::atomic<bool>>` alive flag,
`is_closing()`) before touching anything — `wxWeakRef` is main-thread only, never created, copied or
tested on a worker; never `wxPostEvent` from a worker. UI-initiated background work is a `Job` on a
`Worker`. → `threads-timers-app.md`, `events.md`
8. **Lifetime.** Heap windows die by `Destroy()`, not `delete`; modal dialogs end with
`EndModal(wxID_*)` (an id, never a `wxOK`/`wxCANCEL` style bit); ESC and the close box never run an
Orca Cancel button's handler, so cancel cleanup goes where every path ends. No window work on the stack
of a mouse handler or a WebView script-message callback — `CallAfter` it. → `windows-dialogs.md`
9. **Layout.** `SetSizerAndFit(sizer)` on top-level windows (AGENTS.md rule), plain `SetSizer` on child
panels; proportion is the second `Add` argument; a scrolled window needs `SetScrollRate` and
`FitInside()` after content changes; re-wrap a label with `Wrap(-1); Wrap(w);` or use `Label`. Never
commit a size or wrap from a width not laid out yet: a `wxDefaultSize` child is 20×20 on every port
until the first sizer layout. → `sizers-layout.md`
10. **Mouse capture.** `if (!HasCapture()) CaptureMouse();` / `if (HasCapture()) ReleaseMouse();` at every
site; `wxEVT_MOUSE_CAPTURE_LOST` *cancels* the gesture (never commits); release before a modal, hide or
destroy. macOS never sends capture-lost, and a leaked capture there leaves the app alive but
unclickable (keyboard still works). → `mouse-keyboard-focus.md`
11. **Popups.** Derive from Orca's `PopupWindow` and pass `wxPU_CONTAINS_CONTROLS` when it hosts
controls; size it before `Position()`; on macOS anchor a hover-driven popup flush to its opener; on
MSW call `BindUnfocusEvent()` when it must close with the frame; use a frameless
`wxDialog` for content that needs typing focus or hosts a WebView. → `popups-menus.md`
12. **Painting.** Draw only in the `wxEVT_PAINT` handler, change state then `Refresh()`; never draw
through `wxClientDC` (no effect on macOS and Wayland). → `painting-custom-widgets.md`
13. **Cross-platform.** Every change works on Windows, macOS and Linux GTK3 under X11 and Wayland. No
global pointer coordinates (`wxGetMousePosition()`) in logic that must work on Wayland; decide
X11/Wayland at runtime with `is_running_on_wayland()`. → `platforms.md`
14. **Registration.** New sources go into `SLIC3R_GUI_SOURCES` in `src/slic3r/CMakeLists.txt` (platform-only
files into the `if (WIN32)` / `if (APPLE)` blocks, CAD UI into `if (SLIC3R_CAD)`, `GUI/DeviceCore` /
`GUI/DeviceTab` files into their own `CMakeLists.txt`), and a new file with translatable strings into
`localization/i18n/list.txt`. → `orca-architecture.md`, `strings-i18n-files.md`
## The standard dialog
Exemplars: `src/slic3r/GUI/CloneDialog.cpp` (minimal), `FilamentPickerDialog.cpp` (larger, `Create*()`
helpers), `PurgeModeDialog.cpp` (custom-painted clickable cards). Full conventions, `DialogButtons` id
semantics and the `MsgDialog` API are in `windows-dialogs.md` §8–§9.
```cpp
class MyDialog : public DPIDialog
{
public:
explicit MyDialog(wxWindow* parent)
: DPIDialog(parent ? parent : static_cast<wxWindow*>(wxGetApp().mainframe), wxID_ANY,
_L("My Dialog"), wxDefaultPosition, wxDefaultSize,
wxCAPTION | wxCLOSE_BOX) // always pass a style: DPIAware's default is wxDEFAULT_FRAME_STYLE
{
SetBackgroundColour(*wxWHITE); // light palette colour, dark-mapped by UpdateDlgDarkUI
SetFont(Label::Body_14);
auto* sizer = new wxBoxSizer(wxVERTICAL);
// ... Orca widgets, sizes via FromDIP(n), text via _L() ...
sizer->Add(content_sizer, 1, wxEXPAND | wxALL, FromDIP(10));
auto* btns = new DialogButtons(this, {"OK", "Cancel"}); // untranslated: DialogButtons calls _L() and assigns wxID_OK/wxID_CANCEL
btns->GetOK()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { /* apply */ EndModal(wxID_OK); });
btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { EndModal(wxID_CANCEL); });
sizer->Add(btns, 0, wxEXPAND);
// cancel cleanup that must also run on ESC / close box: after ShowModal() returns, or in wxEVT_CLOSE_WINDOW
SetSizerAndFit(sizer);
CenterOnParent(); // after fitting: DPIAware centred the empty window
wxGetApp().UpdateDlgDarkUI(this); // last, after every child exists
}
protected:
void on_dpi_changed(const wxRect&) override
{
// msw_rescale() ScalableBitmaps and re-SetBitmap() them, Rescale() Orca widgets, re-apply FromDIP/em
// sizes (no msw_buttons_rescale(): it would override the DialogButtons' style height), then:
GetSizer()->SetSizeHints(this);
Refresh();
}
};
MyDialog dlg(this); // modal on the caller's stack
if (dlg.ShowModal() == wxID_OK) { /* read results */ }
```
## Orca widgets at a glance
| Orca widget (`src/slic3r/GUI/Widgets/`) | Instead of | Must know |
|---|---|---|
| `Button` | `wxButton` | ctor `(parent, text, icon_name, style, iconSize, id)` — id is last; style with `SetStyle(ButtonStyle::…, ButtonType::…)`; emits `wxEVT_BUTTON`; not a `wxButton` (no dialog default/escape emulation) |
| `::CheckBox` | `wxCheckBox` | no label parameter — pair with a `wxStaticText`/`Label`; emits `wxEVT_TOGGLEBUTTON` (never `wxEVT_CHECKBOX`), and a handler bound on it must `Skip()` or the bitmap is not refreshed |
| `::ComboBox` (+ `DropDown`) | `wxComboBox`/`wxChoice` | `wxCB_READONLY` for choices; `SelectAndNotify(n)` fires the event; ignores the ctor id; `void*` client data only |
| `::TextInput` | single-line `wxTextCtrl` | text via `GetTextCtrl()`; bind `wxEVT_TEXT_ENTER`/`wxEVT_KILL_FOCUS` on the widget, never on a parent |
| `SpinInput` | `wxSpinCtrl` | non-negative integers only (`-` cannot be typed); commits on Enter, kill-focus and arrow steps with `wxEVT_SPINCTRL`, a plain `wxCommandEvent` — bind a `wxCommandEvent&` handler and read `GetValue()` |
| `DialogButtons` | `wxStdDialogButtonSizer` | untranslated labels (custom ones written `L("…")`); OK/Cancel close by id, Yes/No/Apply/Confirm do not |
| `Label` | `wxStaticText` | `LB_AUTO_WRAP`, `LB_HYPERLINK` (only through `SetWindowStyleFlag`); also hosts the font table `Label::Head_*`/`Label::Body_*` |
| `SwitchButton`, `RadioGroup`, `TabCtrl`, `LabeledStaticBox`, `StaticLine`, `ProgressBar`, `PopupWindow` | toggle, `wxRadioBox`, notebook bar, `wxStaticBox`, `wxStaticLine`, `wxGauge`, `wxPopupTransientWindow` | quirks per widget in `orca-widgets.md` |
Disabling a parent does not repaint Orca widgets as disabled — enable/disable them individually (`::CheckBox`, a native
button, is the exception: it greys with its parent).
Containers stay raw (`wxPanel`, `wxBoxSizer`, `wxScrolledWindow`, `wxSimplebook`).
## Where to read next
| You are … / the symptom is … | Read |
|---|---|
| creating or closing a dialog or frame, `Destroy`/`delete`, modal results, liveness of a window pointer, message boxes | `references/windows-dialogs.md` |
| binding or emitting events, `Skip()`, propagation, custom events, `CallAfter`, `UPDATE_UI`, idle | `references/events.md` |
| worker-thread callbacks, background jobs, timers, `wxYield`/nested loops, progress dialogs, startup/shutdown | `references/threads-timers-app.md` |
| a sizer, a dialog that is collapsed, too big or clipped, a scrolled list, label wrapping, relayout after DPI change | `references/sizers-layout.md` |
| a custom-drawn control or a new widget in `Widgets/`, flicker, paint/erase, `wxGCDC`, best size | `references/painting-custom-widgets.md` |
| `FromDIP`/`em_unit`, rescale on DPI change, icons and bitmaps, image lists, fonts | `references/dpi-bitmaps-fonts.md` |
| colours, dark mode, theme toggle, `StateColor`, icons invisible in dark mode | `references/colours-dark-mode.md` |
| drag gestures and mouse capture, a frozen/unclickable UI on macOS, hover, wheel, keyboard shortcuts, focus, tooltips, cursors | `references/mouse-keyboard-focus.md` |
| popups and dropdowns (closing at once, not closing), context menus, the menu bar, `MenuFactory` | `references/popups-menus.md` |
| text/combo/check/radio/spin controls, book controls, `wxGrid`, `wxDataViewCtrl`, the object list | `references/controls-dataview.md` |
| WebView pages and dialogs, JS ↔ C++ messages, the GL canvas, ImGui overlays, docking panes, the top bar, camera view | `references/webview-gl-aui-media.md` |
| translations, string conversion and formatting, file/dir dialogs, clipboard, drag and drop, logging, `AppConfig` | `references/strings-i18n-files.md` |
| where new code goes, `GUI_App`/`MainFrame`/`Plater` structure, lazy pages, Preferences, notifications | `references/orca-architecture.md` |
| which Orca widget to use and its quirks | `references/orca-widgets.md` |
| adding or changing a print/filament/printer setting, a settings field, per-object overrides | `references/orca-settings-ui.md` |
| platform `#ifdef`s, wx build options, Wayland gaps, title bars, a bug on one platform only | `references/platforms.md` |
| old code, a wx call that behaves differently than you remember, wx-version migration | `references/wx-33-changes.md` |
**Reviewing a GUI diff:** for each area the diff touches, check it against that file's `## Rules`
list. **Debugging a UI bug:** find the symptom in the table above; most recurring Orca UI bugs are a
known class with a pitfall entry and a fixing commit.
## Platform gotchas worth memorising
- **macOS:** capture-lost is never sent (a leaked capture freezes all clicks); transient popups hover-
dismiss across a gap — anchor flush and re-verify the cursor; native modals (file/dir dialogs, native
message boxes) and generic progress dialogs re-activate the main window, so re-raise a secondary window
afterwards with a deferred, liveness-guarded `Raise()` — but never `Raise()` a `wxPopupWindow`, which makes
it the key window; a live menu accelerator consumes the key before
any wx key event; Control+click arrives as a right-click.
- **Windows:** `IsDark()` and `wxSYS_COLOUR_*` follow the system app mode, not Orca's theme — use
`dark_mode()`; menu bitmaps follow `check_dark_mode()`; windows are not double-buffered by default in
3.3.2; `ProcessLeftDown` is never called for popups; a popup that must close with the frame calls
`BindUnfocusEvent()`.
- **Linux GTK3:** dialogs without size hints collapse (only sizer-fitting calls, not `Fit()`, are
replayed at the first `Show()`); command events from a popup's children are not stopped at the popup
(MSW/macOS stop them); chained popups need `transient_for` set to the mapped parent right before
showing; native borders leak through custom widgets (`RemoveButtonBorder`/`RemoveInputBorder`).
- **Wayland:** no global pointer position, no window positioning, `wxClientDC`, `Update()` and `SetIcon`
do nothing, no floating AUI panes, GL is EGL only.
## Editing this skill
The skill describes how the pinned wxWidgets behaves and how Orca GUI code must be written, not what
the GUI tree currently contains. State rules, contracts, mechanisms, stable component designs and code
shapes; never usage counts, census lists, lists of today's offenders or dated measurements. Test each
sentence: if a change to Orca code the sentence does not name, with the wx pin unchanged, could make it
false, state the rule behind it or give a generic example instead, and cut it if there is no rule
behind it. Example files named as models to copy, and commit hashes cited as the reason for a rule,
are fine.
Keep the skill in step with what it describes. A change that alters a contract the skill states (an
Orca widget's API or quirk, a shared helper such as `DPIAware` or `UpdateDlgDarkUI`, the Shortcuts
registry) updates the skill in the same change. Moving the wx pin (`GIT_TAG` in
`deps/wxWidgets/wxWidgets.cmake`) means re-verifying every wx citation and **[source]** fact against
the new tree, and adding that version's behaviour changes the way `wx-33-changes.md` records 3.3's.
Cite wx by path relative to the wx tree root with line (`interface/wx/window.h:3805`), which stays
valid because the version is pinned. Cite Orca by file and symbol, never by line, and commits as the
rationale for a rule. Mark behaviour the wx docs don't state, or contradict, as **[source]**. Each
concept lives in one reference file and the others point to it. Verify every new claim in the wx tree
or the Orca code before adding it, and never drop a fact, qualifier or example to save words.
`evals/evals.json` holds the regression tasks (planted-defect review patches in `evals/files/`); rerun
them with and without the skill after substantial edits.
@@ -0,0 +1,204 @@
{
"skill_name": "orca-wxwidgets",
"evals": [
{
"id": 1,
"name": "report-issue-dialog",
"prompt": "In the OrcaSlicer repo, add a new dialog that opens from the Help menu called 'Report Issue'. It needs a dropdown to pick the issue type (Bug / Feature request / Question), a multiline text box for the description, a checkbox 'Include system info', and OK/Cancel buttons at the bottom. It has to look correct in dark mode and on high-DPI screens.",
"files": [],
"assertions": [
"The dialog class derives from DPIDialog and overrides on_dpi_changed(const wxRect&) with a body that rescales its custom widgets (Rescale()/msw_rescale) and re-lays out (Layout/Fit/Refresh or equivalent)",
"The dropdown is Orca's custom ComboBox (::ComboBox from Widgets/ComboBox.hpp), not wxComboBox/wxChoice",
"The checkbox is Orca's custom ::CheckBox (constructed without a label) paired with a separate wxStaticText/Label for the text",
"The bottom row is DialogButtons constructed with untranslated labels such as {\"OK\", \"Cancel\"} (not _L(\"OK\")), and handlers end the dialog with EndModal(wxID_OK/wxID_CANCEL), not Destroy()",
"The multiline description is a raw wxTextCtrl created with wxTE_MULTILINE, not TextInput (TextInput::DoSetSize keeps the inner control at its single-line height), explicitly made dark-safe (UpdateDarkUI/UpdateDlgDarkUI or StateColor::darkModeColorFor colours), and its text is read via GetValue()",
"Every hard-coded pixel size/padding goes through FromDIP(n) (or em_unit multiples); no raw pixel literals in sizes/borders",
"All user-visible strings are wrapped in _L(...)",
"The top-level sizer is installed with SetSizerAndFit (or SetSizer followed by SetSizeHints)",
"wxGetApp().UpdateDlgDarkUI(this) is called as the last step of the constructor, after all children exist",
"The new .cpp/.hpp are added to SLIC3R_GUI_SOURCES in src/slic3r/CMakeLists.txt",
"A Help-menu item is added in MainFrame's menu construction (append_menu_item or equivalent) that opens the dialog with ShowModal()"
]
},
{
"id": 2,
"name": "new-print-setting",
"prompt": "Add a new print setting to OrcaSlicer: 'seam_transition_gap', a float in mm, default 0.1, range 0-2, shown on the Quality page in the Seam group with a proper tooltip. It should be saved with process presets and searchable.",
"files": [],
"assertions": [
"A ConfigOptionDef is added in PrintConfig.cpp via this->add(\"seam_transition_gap\", coFloat) with label, tooltip, sidetext \"mm\", min 0, max 2, a mode, and default ConfigOptionFloat(0.1)",
"label/tooltip/sidetext in PrintConfig.cpp use the L(\"...\") extraction marker, not _L/_u8L",
"A ((ConfigOptionFloat, seam_transition_gap)) entry is added to the matching PRINT_CONFIG_CLASS_DEFINE block in PrintConfig.hpp",
"The key is appended to s_Preset_print_options in Preset.cpp",
"TabPrint::build() gets optgroup->append_single_option_line(\"seam_transition_gap\", ...) inside the Quality page's Seam option group",
"The answer states that search indexing comes automatically from get_option/append_single_option_line (no manual search registration)",
"Slicing invalidation is handled: the key is added to PrintObject::invalidate_state_by_config_options (or Print::invalidate_state_by_config_options) under an appropriate step"
]
},
{
"id": 3,
"name": "macos-hover-popup-dismiss",
"prompt": "OrcaSlicer bug in a feature branch: the sidebar's filament-sync button now opens a small hover menu (code below). On macOS the menu appears and then disappears before the user can click either item — moving the mouse from the button down into the menu closes it, and sometimes it closes while the cursor is already over it. It works on Windows. Find the causes and fix the code.\n\n```cpp\n// Widgets/FilamentSyncMenu.hpp/.cpp (new, opened when the cursor enters the sidebar's sync button)\nclass FilamentSyncMenu : public PopupWindow\n{\npublic:\n explicit FilamentSyncMenu(wxWindow* parent) : PopupWindow(parent, wxBORDER_NONE)\n {\n auto* sizer = new wxBoxSizer(wxVERTICAL);\n auto* colours = new Button(this, _L(\"Sync colours only\"));\n auto* all = new Button(this, _L(\"Sync colours and types\"));\n colours->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { Dismiss(); wxGetApp().sidebar().sync_ams_list(); });\n all->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { Dismiss(); wxGetApp().sidebar().sync_ams_list(true); });\n sizer->Add(colours, 0, wxEXPAND | wxALL, FromDIP(4));\n sizer->Add(all, 0, wxEXPAND | wxALL, FromDIP(4));\n SetSizerAndFit(sizer);\n\n m_timer.SetOwner(this);\n Bind(wxEVT_TIMER, [this](wxTimerEvent&) { Dismiss(); });\n Bind(wxEVT_LEAVE_WINDOW, [this](wxMouseEvent&) { m_timer.StartOnce(300); });\n Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent&) { m_timer.Stop(); });\n }\n\n void popup_under(wxWindow* btn)\n {\n wxPoint pos = btn->ClientToScreen(wxPoint(0, 0));\n Position(pos, {0, btn->GetSize().y + FromDIP(6)});\n Popup();\n }\n\nprivate:\n wxTimer m_timer;\n};\n\n// Sidebar constructor:\nm_sync_menu = new FilamentSyncMenu(ams_btn);\nams_btn->Bind(wxEVT_ENTER_WINDOW, [this](wxMouseEvent& e) { m_sync_menu->popup_under(ams_btn); e.Skip(); });\n```",
"files": [],
"assertions": [
"Identifies the opener-to-popup gap as a cause: the FromDIP(6) offset leaves a dead zone, and crossing it from the button into the menu ends the hover (LEAVE / hover-timer path) and dismisses the menu on macOS",
"Identifies that on macOS ENTER/LEAVE events around a wxPopupTransientWindow arrive spuriously or out of order (capture handling), so the hover timer must not trust them: re-verify the real cursor position with GetClientRect().Contains(ScreenToClient(wxGetMousePosition())) (or equivalent geometry) before starting/stopping the timer or dismissing",
"Fix anchors the menu flush with (or slightly overlapping) the button instead of leaving a gap, at least on macOS",
"Adds wxPU_CONTAINS_CONTROLS to the PopupWindow style because the menu hosts interactive Button children",
"Guards against calling Popup() again while the menu is already shown (re-entering the button) and/or debounces reopening right after a dismissal",
"The fix is gated to macOS where needed (#ifdef __WXOSX__/__APPLE__) or argued harmless elsewhere, and does not rely on unexplained SetFocus/CallAfter/Raise hacks"
]
},
{
"id": 4,
"name": "dark-mode-icons",
"prompt": "Users report that a couple of the AMS status icons in OrcaSlicer are nearly invisible when dark mode is on, but fine in light mode. What's going on and how do we fix it properly?",
"files": [],
"assertions": [
"Explains that BitmapCache::load_svg recolors dark-mode icons by literal substitution of a fixed palette (e.g. #262E30 -> #EFEFF0) and off-palette colors pass through unchanged",
"Offers the asset fix: re-author the SVG fills in palette colors (e.g. near-black line art as #262E30)",
"Offers the variant fix: a *_dark asset selected by name from wxGetApp().dark_mode()",
"States that name-selected variants must be re-picked/reloaded on a runtime theme switch (on_sys_color_changed / sys_color_changed / msw_rescale path)",
"Investigates the actual AMS icon code/assets (e.g. checks both branches of a dark_mode() ternary for a copy-paste _light/_light bug, or inspects the SVG fills)"
]
},
{
"id": 5,
"name": "custom-painted-drag-widget",
"prompt": "In OrcaSlicer, add a small owner-drawn widget for the filament area of the sidebar: a horizontal strip of colour swatches (one per filament) where the user can click-and-drag across swatches to select a contiguous range. Hovered swatches should highlight, it must emit an event with the selected range when the drag ends, and it must look right with high-DPI and dark mode. Write the widget.",
"files": [],
"assertions": [
"Paints only inside a wxEVT_PAINT handler using wxAutoBufferedPaintDC/wxBufferedPaintDC/wxPaintDC with SetBackgroundStyle(wxBG_STYLE_PAINT); never draws through wxClientDC; triggers repaint with Refresh()/RefreshRect()",
"CaptureMouse() on press is guarded (if (!HasCapture())) and ReleaseMouse() is guarded (if (HasCapture()))",
"Handles wxEVT_MOUSE_CAPTURE_LOST by ending the gesture: drag state reset and repaint, no recapture (cancelling rather than committing the selection is what the wx contract asks for)",
"Ensures capture cannot leak: release on every end-of-drag path regardless of drag flags (and/or in the destructor), mentioning the macOS consequence (UI unclickable) or the wx contract",
"Sizes use FromDIP (or em_unit) and the widget reports a size via DoGetBestSize/DoGetBestClientSize or SetMinSize, rescaled on DPI change (Rescale/msw_rescale/wxEVT_DPI_CHANGED)",
"Colours go through StateColor / StateColor::darkModeColorFor or are re-derived from wxGetApp().dark_mode() and re-applied on theme change (sys_color_changed/on_sys_color_changed)",
"Defines a custom event with wxDECLARE_EVENT/wxDEFINE_EVENT (e.g. a wxCommandEvent carrying the range) and uses Bind(), not a new static event table",
"Hover handling uses event-relative coordinates (evt.GetPosition()), not wxGetMousePosition(), and only refreshes when the hovered index actually changes"
]
},
{
"id": 6,
"name": "worker-thread-progress",
"prompt": "In OrcaSlicer, we're adding a non-modal 'Firmware download' dialog. A network library calls our progress callback (int percent, std::string status) on its own worker thread, and a completion callback (bool ok, std::string error) at the end. The dialog shows a progress bar and status text and has a Cancel button; the user may also close the dialog at any time while the download continues. Implement the dialog and the callback wiring.",
"files": [],
"assertions": [
"No wx/GUI calls are made on the worker thread; callbacks marshal to the main thread via CallAfter (wxGetApp().CallAfter / window CallAfter) or wxQueueEvent",
"Deferred lambdas capture data by value (copies of percent/status strings), not references to worker-owned data",
"Deferred work re-checks the dialog's liveness inside the lambda (shared_ptr<atomic<bool>> alive flag, wxWeakRef, or registry lookup) because the dialog can be closed before the callback runs",
"If events are used across threads, they are heap-allocated/owned (wxQueueEvent with new/Clone, or wxThreadEvent) — not wxPostEvent/AddPendingEvent with wxString payload from the worker",
"Closing the dialog cancels or detaches the download callbacks and the non-modal dialog is destroyed with Destroy() (not delete), with any wxTimer stopped first",
"Uses Orca UI conventions: DPIDialog base, Orca ProgressBar or custom widgets, DialogButtons/Button for Cancel, _L strings, FromDIP sizes, UpdateDlgDarkUI",
"Strings crossing to the GUI are converted with from_u8() (UTF-8 std::string -> wxString), not implicit/ToStdString conversions"
]
},
{
"id": 7,
"name": "scrolled-dynamic-layout",
"prompt": "OrcaSlicer bug on Linux: in a settings dialog, a wxScrolledWindow holds a list of rows that the user can add with a '+' button. After adding rows, the scrollbar doesn't appear/update and new rows are cut off; when the dialog first opens on GTK it's sometimes collapsed to a tiny size; and some German labels in the rows are clipped. Explain the causes and give the correct wx code patterns to fix all three.",
"files": [],
"assertions": [
"For the scroll issue: after adding rows, call FitInside() on the scrolled window (or SetVirtualSize/Layout so the virtual size updates), with SetScrollRate set so scrollbars are enabled",
"Explains that changing children doesn't change the scrolled window's size, so automatic layout doesn't run — Layout() (and FitInside) must be called explicitly after adding content",
"For the collapsed dialog: uses SetSizerAndFit on the top-level dialog or SetSizer + sizer->SetSizeHints(dialog) so the min size is set (GTK needs the min-size hint), and avoids an unconditional Fit() in on_dpi_changed/refresh paths",
"For clipped labels: avoids fixed widths/heights on wxStaticText (use -1 / best size) and re-Wrap()s after SetLabel or uses Label with LB_AUTO_WRAP; mentions translations being longer",
"Mentions InvalidateBestSize()/Layout() of the containing hierarchy (or parent->Layout()) after dynamic content changes, and/or Freeze()/Thaw() around bulk row creation",
"Notes GTK specifics: Linux builds wx against GTK3 by default (deps/CMakeLists.txt DEP_WX_GTK3 ON) and/or the first GTK size pass can run with a bogus tiny client width, so wrap/height calculations need a guard"
]
},
{
"id": 8,
"name": "keyboard-shortcut",
"prompt": "Add a global keyboard shortcut Ctrl+Shift+E (Cmd+Shift+E on macOS) to OrcaSlicer that exports the current plate's G-code (same as the existing export action). It must work on all three platforms and show up wherever OrcaSlicer documents shortcuts.",
"files": [],
"assertions": [
"Explains that wxACCEL_CTRL / 'Ctrl' in menu accelerator strings maps to Cmd on macOS (and WXK_RAW_CONTROL / wxACCEL_RAW_CTRL is the real Control key)",
"Registers the shortcut through Orca's shortcut registry (Shortcut enum + shortcut_table in src/slic3r/GUI/Shortcuts.cpp, Global context dispatched from MainFrame's wxEVT_CHAR_HOOK) and/or the menu item whose accelerator text is derived from it, dispatching to the existing export handler",
"Checks for conflicts with existing shortcuts (registry defaults, GLCanvas3D/ObjectList contexts, menu items) before choosing the binding",
"The shortcut appears in KBShortcutsDialog by virtue of the registry (or is explicitly added there), and the answer checks/handles a conflict with an existing default chord",
"Accounts for platform differences in Orca's menus: native wxMenuBar on macOS vs BBLTopbar/custom menus on Windows/Linux, so the shortcut works where no native menubar exists"
]
},
{
"id": 9,
"name": "webview-dialog",
"prompt": "Add a dialog to OrcaSlicer that shows a local HTML page from resources/web/ in a WebView. The page has a 'Done' button that posts a JS message; when it arrives the dialog must close and return a result to the caller. It must work on Windows (Edge), macOS (WKWebView) and Linux (WebKitGTK).",
"files": [],
"assertions": [
"Creates the browser through Orca's WebView wrapper (WebView::CreateWebView in Widgets/WebView.hpp), not wxWebView::New directly",
"Receives messages via wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED using the 'wx' handler the wrapper registers (window.wx.postMessage / postMessage), without adding a duplicate script message handler (which throws an uncatchable NSException on WKWebView)",
"Closing/ending the dialog from the script-message handler is deferred with CallAfter (not done synchronously inside the WebView callback stack)",
"Builds the local page URL from Orca's resources dir (resources_dir()/from_u8 + file:// URL or wxFileName::FileNameToURL), handling paths portably",
"Accounts for asynchronous backend creation (Edge): no script runs/calls before the page is loaded (wxEVT_WEBVIEW_LOADED) or the wrapper's deferral",
"Dialog follows Orca conventions: DPIDialog base, on_dpi_changed, UpdateDlgDarkUI/dark-mode handling, EndModal with a result id"
]
},
{
"id": 10,
"name": "macos-frozen-ui",
"prompt": "On macOS, sometimes after dragging the handle of the new ratio bar widget inside a dialog in OrcaSlicer, the whole app stops responding to mouse clicks — even the window's close button — but Cmd+S still saves, timers keep running and the 3D view still repaints. Windows is fine. What is going on, how do we confirm it, and how should widget code like this be written?",
"files": [],
"assertions": [
"Identifies a leaked wx mouse capture (CaptureMouse without matching ReleaseMouse) as the cause, explicitly not a deadlock/hang",
"Explains why only the mouse is dead: on macOS wx routes all mouse events to the capturing window while key events are unaffected",
"Explains why macOS differs: wxEVT_MOUSE_CAPTURE_LOST is not delivered on macOS (wxOSX never generates it; the docs' @onlyfor{wxmsw} is stale, wxGTK sends it too) so nothing ever unwinds the leaked capture",
"Fix: guard CaptureMouse with !HasCapture() and ReleaseMouse with HasCapture(), release on every exit path (mouse-up independent of drag flags, capture-lost handler, before the dialog closes/destructor)",
"Handles wxEVT_MOUSE_CAPTURE_LOST (required by the wx contract wherever it can fire) without recapturing",
"Gives a way to confirm/locate it (e.g. sampling the process / checking GetCapture(), or auditing every CaptureMouse call site)"
]
},
{
"id": 11,
"name": "review-planted-wx-defects",
"prompt": "Review this proposed OrcaSlicer change before it is merged: the patch at .claude/skills/orca-wxwidgets/evals/files/filament-notes.patch adds a non-modal 'Filament notes' dialog (src/slic3r/GUI/FilamentNotesDialog.hpp/.cpp). It is not applied to the tree; read it from that path and use the repository for context. Report every correctness, cross-platform (Windows/macOS/Linux GTK, X11 and Wayland), threading, lifetime, DPI, dark-mode and wxWidgets API-misuse problem you find, each with its consequence and the fix. Do not report style nits.",
"files": [
"files/filament-notes.patch"
],
"assertions": [
"D1: Flags FromDIP() called on the SwatchPreview object inside its own base-class initializer (before wxPanel is constructed) as invalid/UB, and fixes it (parent->FromDIP / static wxWindow::FromDIP(sz, parent) / SetMinSize after construction)",
"D2: Flags the mouse-capture handling: CaptureMouse unguarded, ReleaseMouse gated on m_dragging rather than HasCapture(), and no wxEVT_MOUSE_CAPTURE_LOST handler — with the consequence (leaked capture freezes mouse input on macOS; wx's capture asserts are compiled out in Orca, so elsewhere the misuse fails silently) and the guarded pattern",
"D3: Flags hit-testing with ScreenToClient(wxGetMousePosition()) in the motion handler (unreliable on Wayland, stale vs the event) and fixes with e.GetPosition()",
"D4: Flags drawing the hover outline through wxClientDC outside the paint handler (no effect on macOS or GTK3 Wayland; on MSW and X11 it races and is overwritten by the next paint) and fixes by storing m_hover and Refresh()/RefreshRect() + drawing in the paint handler",
"D5: Flags the hard-coded wxColour(\"#009688\") pen as not dark-mode aware (should go through StateColor::darkModeColorFor or a dark-mode branch, re-derived on theme change)",
"D6: Flags SetValue() inside the wxEVT_TEXT handler (SetValue emits wxEVT_TEXT again: recursion/re-entrancy and caret reset) and fixes with ChangeValue() (plus an insertion-point restore or guard)",
"D7: Flags the wxEVT_KILL_FOCUS handler that does not call e.Skip() (breaks native focus handling of the text control)",
"D8: Flags that the wxStaticBoxSizer's child wxTextCtrl is created with the dialog as parent instead of box->GetStaticBox()",
"D9: Flags the raw pixel size wxSize(400, 160) (needs FromDIP)",
"D10: Flags m_notes->GetValue().ToStdString() as lossy for non-ASCII text and fixes with into_u8()/ToUTF8()",
"D11: Flags `delete this` in the wxEVT_CLOSE_WINDOW handler and fixes with Destroy() (deferred deletion after pending events)",
"D12: Flags SetSizer()+Layout() on the top-level dialog without SetSizerAndFit/SetSizeHints/Fit (no initial size / min size; GTK collapse) per the project rule",
"D13: Flags m_status->SetLabel() called on the worker thread (GUI call off the main thread)",
"D14: Flags the worker's wxPostEvent(this, evt) with a wxString payload: posting a wxString-carrying event from a worker thread is unsafe (use wxQueueEvent with a heap event / CallAfter), and `this` may be destroyed because the destructor detaches the thread — needs an alive flag/join/cancellation",
"D15: Flags that wxEVT_MENU is bound on m_more_btn although PopupMenu() is called on the dialog (menu events go to the invoking window and propagate up, never to a child), and that Bind runs on every popup (accumulating handlers); fix: bind on the dialog once or use GetPopupMenuSelectionFromUser",
"D16: Flags the heap wxTimer that is never stopped or deleted (leak; a pending tick can fire into the destroyed dialog) and fixes with Stop()+delete in the destructor or a by-value member",
"D17: Flags that start_sync() move-assigns m_sync_thread while the previous std::thread is still joinable (never joined or detached; a finished thread stays joinable), so the second sync ('Reload from cloud', live once the D15 routing is fixed) calls std::terminate; fix: no reassigned thread member (a detached worker per request behind the D14 alive flag) or detach/join the previous thread before reassigning, and/or refuse a reload while a sync is in flight",
"D18: Flags that SwatchPreview never releases the capture if it is destroyed while holding it (no guarded ReleaseMouse() in its destructor or on the dialog's close path): e.g. Esc, which DPIDialog maps to Close(), pressed with the button held on the strip destroys the panel while it is still on wx's capture stack (the 'Destroying window before releasing mouse capture' assert, or in Orca's assert-free build a dangling capture that freezes or crashes mouse input, notably on macOS); fix: drop the capture, or if (HasCapture()) ReleaseMouse() in the destructor / before close",
"Does not report false defects about correct code (e.g. DialogButtons' untranslated labels, wxBG_STYLE_PAINT + wxAutoBufferedPaintDC, Bind with lambdas)"
]
},
{
"id": 12,
"name": "review-subtle-wx-defects",
"prompt": "Review this proposed OrcaSlicer change before it is merged: the patch at .claude/skills/orca-wxwidgets/evals/files/preset-note.patch adds a sidebar 'note chip' widget and a modal 'Preset note' editor dialog (src/slic3r/GUI/PresetNoteDialog.hpp/.cpp). It is not applied to the tree; read it from that path and use the repository and the wxWidgets source for context. Report every correctness, cross-platform, i18n, DPI, dark-mode, lifetime and wxWidgets/Orca API-misuse problem you find, each with its consequence and the fix. Do not report style nits.",
"files": [
"files/preset-note.patch"
],
"assertions": [
"P1: Flags the StateColor built with the Normal entry first: colorForStates returns the first matching entry and Normal (mask 0) matches every state, so the Hovered/Pressed colours never show; fix: list Pressed, Hovered, then Normal last",
"P2: Flags the wxEVT_SIZE lambda taking wxSizeEvent by value: Skip() on the copy does not reach the original, so the event counts as handled and default/base size handling stops; fix: take the event by reference",
"P3: Flags that the capture-lost handler routes through on_left_up and therefore fires EVT_NOTE_CHIP_CLICKED (commits a click) when capture is lost; per the wx contract capture loss must cancel the operation: reset m_pressed/visual state without emitting the event and without recapturing",
"P4: Flags _(\"Preset note\") as never extracted for translation (Orca's xgettext keywords are L/_L/_u8L/..., not _), fix: _L",
"P5: Flags wxBitmapBundle::FromSVGFile as unavailable in Orca's wx build (NanoSVG off / no wxHAS_SVG — does not compile) and replaces it with Orca's ScalableBitmap/create_scaled_bitmap/ScalableButton icon loading by name",
"P6: Flags Bind(wxEVT_TEXT_ENTER, ..., m_input->GetId()) on the dialog as never firing: TextInput re-dispatches TEXT_ENTER only to the wrapper (ProcessEventLocally, no propagation to parents) and the inner control's handler does not Skip; fix: bind on m_input (the wrapper) or on GetTextCtrl()",
"P7: Flags RichMessageDialog::SetYesNoLabels as having no effect in Orca (labels are stored but never applied to the buttons), fix: MsgDialog::SetButtonLabel(wxID_YES/wxID_NO, ...) or a dialog with custom buttons",
"P8: Flags EndModal(wxCANCEL): wxCANCEL is a style flag, not the return code wxID_CANCEL, so ShowModal returns the wrong value; fix: EndModal(wxID_CANCEL)",
"P9: Flags that restore_original() runs only in the Cancel button handler: ESC (DPIDialog's char hook -> Close()) and the close box end the dialog with wxID_CANCEL without running the custom Button's handler, so the draft is not cleared; fix: do cleanup on every non-OK exit (after ShowModal returns, or in a close/EndModal path)",
"P10: Flags m_preview->SetLabel(...) followed by Wrap(FromDIP(360)) on every update: in wx 3.3.2 Wrap() is a no-op when the width equals the last wrap width, so updated labels stay unwrapped; fix: Wrap(-1) then Wrap(w), or wxST_WRAP / Orca Label with LB_AUTO_WRAP",
"P11 (not a defect): Does not claim that m_options->Enable(...) leaves the ::CheckBox or its wxStaticText label looking enabled: ::CheckBox is a native wxBitmapToggleButton, so the ancestor's disable greys it on every port without calling its Enable() override (MSW: NotifyWindowOnEnableChange -> DoEnable -> EnableWindow, and the owner-drawn button paints its SetBitmapDisabled art; GTK3: native insensitivity, and the button's wxGtkImage draws a greyed copy of its current bitmap while !IsEnabled(); macOS: setEnabled:NO, and AppKit dims the image), and wxStaticText greys natively. Suggesting pin->Enable() to get the designed disabled art on GTK is at most a nit",
"P12: Flags that the patch registers neither new file: they are not in SLIC3R_GUI_SOURCES in src/slic3r/CMakeLists.txt (never compiled), and PresetNoteDialog.cpp is not in localization/i18n/list.txt (run_gettext scans only the listed files, so its _L strings never reach OrcaSlicer.pot); fix: add both entries",
"Does not report false defects about correct code: the weak_ptr-guarded CallAfter is not a use-after-free (alive is checked before `this` is used), GetSizer()->SetSizeHints(this) in on_dpi_changed is fine, DialogButtons' untranslated labels are correct"
]
}
]
}
@@ -0,0 +1,252 @@
diff --git a/src/slic3r/GUI/FilamentNotesDialog.hpp b/src/slic3r/GUI/FilamentNotesDialog.hpp
new file mode 100644
--- /dev/null
+++ b/src/slic3r/GUI/FilamentNotesDialog.hpp
@@ -0,0 +1,56 @@
+#pragma once
+
+#include "GUI_Utils.hpp"
+
+#include <wx/timer.h>
+
+#include <string>
+#include <thread>
+#include <vector>
+
+class Button;
+class TextInput;
+
+namespace Slic3r { namespace GUI {
+
+// Strip of filament colour swatches; hovering a swatch outlines it.
+class SwatchPreview : public wxPanel
+{
+public:
+ SwatchPreview(wxWindow* parent);
+ void set_colours(const std::vector<wxColour>& colours);
+
+private:
+ void on_left_down(wxMouseEvent& e);
+ void on_left_up(wxMouseEvent& e);
+ void on_motion(wxMouseEvent& e);
+ void draw_hover(int index);
+
+ std::vector<wxColour> m_colours;
+ bool m_dragging{false};
+ int m_hover{-1};
+};
+
+// Non-modal editor for the user's notes on a filament, synced with the cloud.
+class FilamentNotesDialog : public DPIDialog
+{
+public:
+ FilamentNotesDialog(wxWindow* parent, const std::string& filament_id);
+ ~FilamentNotesDialog() override;
+
+protected:
+ void on_dpi_changed(const wxRect& suggested_rect) override;
+
+private:
+ void start_sync();
+ void on_sync_done(wxCommandEvent& e);
+ void show_more_menu();
+
+ std::string m_filament_id;
+ TextInput* m_name_input{nullptr};
+ wxTextCtrl* m_notes{nullptr};
+ wxStaticText* m_status{nullptr};
+ SwatchPreview* m_preview{nullptr};
+ Button* m_more_btn{nullptr};
+ wxTimer* m_autosave_timer{nullptr};
+ std::thread m_sync_thread;
+};
+
+}} // namespace Slic3r::GUI
diff --git a/src/slic3r/GUI/FilamentNotesDialog.cpp b/src/slic3r/GUI/FilamentNotesDialog.cpp
new file mode 100644
--- /dev/null
+++ b/src/slic3r/GUI/FilamentNotesDialog.cpp
@@ -0,0 +1,170 @@
+#include "FilamentNotesDialog.hpp"
+
+#include "GUI_App.hpp"
+#include "I18N.hpp"
+#include "Widgets/Button.hpp"
+#include "Widgets/DialogButtons.hpp"
+#include "Widgets/TextInput.hpp"
+
+#include <wx/dcbuffer.h>
+#include <wx/dcclient.h>
+#include <wx/menu.h>
+#include <wx/statbox.h>
+
+namespace Slic3r {
+// Provided by the cloud layer; blocking network call.
+std::string fetch_remote_note(const std::string& filament_id);
+
+namespace GUI {
+
+wxDEFINE_EVENT(EVT_NOTES_SYNC_DONE, wxCommandEvent);
+
+SwatchPreview::SwatchPreview(wxWindow* parent)
+ : wxPanel(parent, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(240), FromDIP(28)))
+{
+ SetBackgroundStyle(wxBG_STYLE_PAINT);
+ Bind(wxEVT_PAINT, [this](wxPaintEvent&) {
+ wxAutoBufferedPaintDC dc(this);
+ dc.SetBackground(wxBrush(GetBackgroundColour()));
+ dc.Clear();
+ const wxSize sz = GetClientSize();
+ const int w = sz.x / std::max<int>(1, m_colours.size());
+ dc.SetPen(*wxTRANSPARENT_PEN);
+ for (size_t i = 0; i < m_colours.size(); ++i) {
+ dc.SetBrush(wxBrush(m_colours[i]));
+ dc.DrawRectangle(int(i) * w, 0, w, sz.y);
+ }
+ });
+ Bind(wxEVT_LEFT_DOWN, &SwatchPreview::on_left_down, this);
+ Bind(wxEVT_LEFT_UP, &SwatchPreview::on_left_up, this);
+ Bind(wxEVT_MOTION, &SwatchPreview::on_motion, this);
+}
+
+void SwatchPreview::set_colours(const std::vector<wxColour>& colours)
+{
+ m_colours = colours;
+ Refresh();
+}
+
+void SwatchPreview::on_left_down(wxMouseEvent& e)
+{
+ m_dragging = true;
+ CaptureMouse();
+}
+
+void SwatchPreview::on_left_up(wxMouseEvent& e)
+{
+ if (m_dragging) {
+ m_dragging = false;
+ ReleaseMouse();
+ }
+}
+
+void SwatchPreview::on_motion(wxMouseEvent& e)
+{
+ const wxPoint p = ScreenToClient(wxGetMousePosition());
+ const int w = GetClientSize().x / std::max<int>(1, m_colours.size());
+ const int idx = w > 0 ? p.x / w : -1;
+ if (idx != m_hover) {
+ m_hover = idx;
+ draw_hover(idx);
+ }
+ e.Skip();
+}
+
+void SwatchPreview::draw_hover(int index)
+{
+ wxClientDC dc(this);
+ dc.SetPen(wxPen(wxColour("#009688"), 2));
+ dc.SetBrush(*wxTRANSPARENT_BRUSH);
+ const int w = GetClientSize().x / std::max<int>(1, m_colours.size());
+ dc.DrawRectangle(index * w, 0, w, GetClientSize().y);
+}
+
+FilamentNotesDialog::FilamentNotesDialog(wxWindow* parent, const std::string& filament_id)
+ : DPIDialog(parent, wxID_ANY, _L("Filament notes"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE)
+ , m_filament_id(filament_id)
+{
+ SetBackgroundColour(*wxWHITE);
+ auto* sizer = new wxBoxSizer(wxVERTICAL);
+
+ m_name_input = new TextInput(this, wxEmptyString, wxEmptyString, wxEmptyString, wxDefaultPosition, wxSize(FromDIP(300), -1));
+ m_name_input->GetTextCtrl()->Bind(wxEVT_TEXT, [this](wxCommandEvent&) {
+ wxString v = m_name_input->GetTextCtrl()->GetValue();
+ v.Trim(false);
+ m_name_input->GetTextCtrl()->SetValue(v.Upper());
+ m_autosave_timer->StartOnce(1000);
+ });
+ m_name_input->GetTextCtrl()->Bind(wxEVT_KILL_FOCUS, [this](wxFocusEvent&) {
+ m_autosave_timer->StartOnce(1);
+ });
+ sizer->Add(m_name_input, 0, wxEXPAND | wxALL, FromDIP(10));
+
+ auto* box = new wxStaticBoxSizer(wxVERTICAL, this, _L("Notes"));
+ m_notes = new wxTextCtrl(this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(400, 160), wxTE_MULTILINE);
+ box->Add(m_notes, 1, wxEXPAND | wxALL, FromDIP(6));
+ sizer->Add(box, 1, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(10));
+
+ m_preview = new SwatchPreview(this);
+ sizer->Add(m_preview, 0, wxEXPAND | wxALL, FromDIP(10));
+
+ m_status = new wxStaticText(this, wxID_ANY, wxEmptyString);
+ sizer->Add(m_status, 0, wxLEFT | wxRIGHT, FromDIP(10));
+
+ m_more_btn = new Button(this, _L("More..."));
+ m_more_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { show_more_menu(); });
+ sizer->Add(m_more_btn, 0, wxALL, FromDIP(10));
+
+ auto* btns = new DialogButtons(this, {"OK", "Cancel"});
+ btns->GetOK()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { Close(); });
+ btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { Close(); });
+ sizer->Add(btns, 0, wxEXPAND);
+
+ m_autosave_timer = new wxTimer(this);
+ Bind(wxEVT_TIMER, [this](wxTimerEvent&) {
+ wxGetApp().app_config->set("filament_note_" + m_filament_id, m_notes->GetValue().ToStdString());
+ wxGetApp().app_config->save();
+ });
+
+ Bind(EVT_NOTES_SYNC_DONE, &FilamentNotesDialog::on_sync_done, this);
+ Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent&) { delete this; });
+
+ SetSizer(sizer);
+ Layout();
+ wxGetApp().UpdateDlgDarkUI(this);
+
+ start_sync();
+}
+
+FilamentNotesDialog::~FilamentNotesDialog()
+{
+ if (m_sync_thread.joinable())
+ m_sync_thread.detach();
+}
+
+void FilamentNotesDialog::start_sync()
+{
+ m_sync_thread = std::thread([this]() {
+ m_status->SetLabel(_L("Syncing..."));
+ const std::string remote = fetch_remote_note(m_filament_id);
+ wxCommandEvent evt(EVT_NOTES_SYNC_DONE);
+ evt.SetString(wxString::FromUTF8(remote));
+ wxPostEvent(this, evt);
+ });
+}
+
+void FilamentNotesDialog::on_sync_done(wxCommandEvent& e)
+{
+ m_notes->SetValue(e.GetString());
+ m_status->SetLabel(_L("Synced"));
+}
+
+void FilamentNotesDialog::show_more_menu()
+{
+ wxMenu menu;
+ menu.Append(wxID_CLEAR, _L("Clear notes"));
+ menu.Append(wxID_REVERT, _L("Reload from cloud"));
+ m_more_btn->Bind(wxEVT_MENU, [this](wxCommandEvent& e) {
+ if (e.GetId() == wxID_CLEAR)
+ m_notes->Clear();
+ else
+ start_sync();
+ });
+ PopupMenu(&menu, m_more_btn->GetPosition() + wxPoint(0, m_more_btn->GetSize().y));
+}
+
+void FilamentNotesDialog::on_dpi_changed(const wxRect&)
+{
+ m_more_btn->Rescale();
+ Layout();
+ Refresh();
+}
+
+}} // namespace Slic3r::GUI
@@ -0,0 +1,242 @@
diff --git a/src/slic3r/GUI/PresetNoteDialog.hpp b/src/slic3r/GUI/PresetNoteDialog.hpp
new file mode 100644
--- /dev/null
+++ b/src/slic3r/GUI/PresetNoteDialog.hpp
@@ -0,0 +1,57 @@
+#pragma once
+
+#include "GUI_Utils.hpp"
+#include "Widgets/StaticBox.hpp"
+#include "Widgets/StateColor.hpp"
+
+#include <memory>
+
+class TextInput;
+
+namespace Slic3r { namespace GUI {
+
+wxDECLARE_EVENT(EVT_NOTE_CHIP_CLICKED, wxCommandEvent);
+
+// Rounded chip that shows a preset's note in the sidebar; clicking it opens the editor.
+class NoteChip : public StaticBox
+{
+public:
+ NoteChip(wxWindow* parent, const wxString& text);
+ void set_text(const wxString& text);
+ void Rescale();
+
+protected:
+ void doRender(wxDC& dc) override;
+
+private:
+ void on_left_down(wxMouseEvent& e);
+ void on_left_up(wxMouseEvent& e);
+ void on_capture_lost(wxMouseCaptureLostEvent& e);
+
+ wxString m_text;
+ bool m_pressed{false};
+ StateColor m_bg;
+};
+
+// Modal editor for the note attached to a preset.
+class PresetNoteDialog : public DPIDialog
+{
+public:
+ PresetNoteDialog(wxWindow* parent, const wxString& preset_name, const wxString& note);
+ wxString get_note() const;
+
+protected:
+ void on_dpi_changed(const wxRect& suggested_rect) override;
+
+private:
+ void update_preview();
+ void restore_original();
+
+ TextInput* m_input{nullptr};
+ wxStaticText* m_preview{nullptr};
+ wxPanel* m_options{nullptr};
+ wxString m_original;
+ std::shared_ptr<bool> m_alive;
+};
+
+}} // namespace Slic3r::GUI
diff --git a/src/slic3r/GUI/PresetNoteDialog.cpp b/src/slic3r/GUI/PresetNoteDialog.cpp
new file mode 100644
--- /dev/null
+++ b/src/slic3r/GUI/PresetNoteDialog.cpp
@@ -0,0 +1,178 @@
+#include "PresetNoteDialog.hpp"
+
+#include "GUI_App.hpp"
+#include "I18N.hpp"
+#include "MsgDialog.hpp"
+#include "Widgets/CheckBox.hpp"
+#include "Widgets/DialogButtons.hpp"
+#include "Widgets/Label.hpp"
+#include "Widgets/TextInput.hpp"
+
+#include <wx/bmpbndl.h>
+#include <wx/statbmp.h>
+
+namespace Slic3r { namespace GUI {
+
+wxDEFINE_EVENT(EVT_NOTE_CHIP_CLICKED, wxCommandEvent);
+
+NoteChip::NoteChip(wxWindow* parent, const wxString& text)
+ : StaticBox(parent, wxID_ANY)
+ , m_text(text)
+{
+ m_bg = StateColor(std::pair{wxColour("#F1F1F1"), (int) StateColor::Normal},
+ std::pair{wxColour("#DBDBDB"), (int) StateColor::Hovered},
+ std::pair{wxColour("#CECECE"), (int) StateColor::Pressed});
+ SetBackgroundColor(m_bg);
+ SetCornerRadius(FromDIP(4));
+ SetMinSize(wxSize(-1, FromDIP(24)));
+
+ Bind(wxEVT_LEFT_DOWN, &NoteChip::on_left_down, this);
+ Bind(wxEVT_LEFT_UP, &NoteChip::on_left_up, this);
+ Bind(wxEVT_MOUSE_CAPTURE_LOST, &NoteChip::on_capture_lost, this);
+ Bind(wxEVT_SIZE, [this](wxSizeEvent e) {
+ Refresh();
+ e.Skip();
+ });
+}
+
+void NoteChip::set_text(const wxString& text)
+{
+ m_text = text;
+ Refresh();
+}
+
+void NoteChip::Rescale()
+{
+ SetMinSize(wxSize(-1, FromDIP(24)));
+ Refresh();
+}
+
+void NoteChip::doRender(wxDC& dc)
+{
+ StaticBox::doRender(dc);
+ dc.SetFont(Label::Body_12);
+ dc.SetTextForeground(StateColor::darkModeColorFor(wxColour("#262E30")));
+ const wxSize ext = dc.GetTextExtent(m_text);
+ dc.DrawText(m_text, FromDIP(8), (GetSize().y - ext.y) / 2);
+}
+
+void NoteChip::on_left_down(wxMouseEvent& e)
+{
+ m_pressed = true;
+ if (!HasCapture())
+ CaptureMouse();
+ Refresh();
+}
+
+void NoteChip::on_left_up(wxMouseEvent& e)
+{
+ if (HasCapture())
+ ReleaseMouse();
+ if (m_pressed) {
+ m_pressed = false;
+ wxCommandEvent evt(EVT_NOTE_CHIP_CLICKED, GetId());
+ evt.SetEventObject(this);
+ GetEventHandler()->ProcessEvent(evt);
+ }
+ Refresh();
+}
+
+void NoteChip::on_capture_lost(wxMouseCaptureLostEvent& e)
+{
+ wxMouseEvent up(wxEVT_LEFT_UP);
+ on_left_up(up);
+}
+
+PresetNoteDialog::PresetNoteDialog(wxWindow* parent, const wxString& preset_name, const wxString& note)
+ : DPIDialog(parent ? parent : static_cast<wxWindow*>(wxGetApp().mainframe), wxID_ANY,
+ _("Preset note"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX)
+ , m_original(note)
+ , m_alive(std::make_shared<bool>(true))
+{
+ SetBackgroundColour(*wxWHITE);
+ SetFont(Label::Body_14);
+ auto* sizer = new wxBoxSizer(wxVERTICAL);
+
+ auto* icon = new wxStaticBitmap(this, wxID_ANY,
+ wxBitmapBundle::FromSVGFile(from_u8(resources_dir() + "/images/note.svg"), wxSize(16, 16)));
+ sizer->Add(icon, 0, wxALL, FromDIP(10));
+
+ m_input = new TextInput(this, note, wxEmptyString, wxEmptyString, wxDefaultPosition,
+ wxSize(FromDIP(360), -1), wxTE_PROCESS_ENTER);
+ sizer->Add(m_input, 0, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(10));
+ m_input->GetTextCtrl()->Bind(wxEVT_TEXT, [this](wxCommandEvent& e) {
+ update_preview();
+ e.Skip();
+ });
+ Bind(wxEVT_TEXT_ENTER, [this](wxCommandEvent&) { EndModal(wxID_OK); }, m_input->GetId());
+
+ m_preview = new wxStaticText(this, wxID_ANY, wxEmptyString);
+ sizer->Add(m_preview, 0, wxEXPAND | wxALL, FromDIP(10));
+
+ m_options = new wxPanel(this);
+ m_options->SetBackgroundColour(*wxWHITE);
+ auto* opt_sizer = new wxBoxSizer(wxHORIZONTAL);
+ auto* pin = new ::CheckBox(m_options);
+ opt_sizer->Add(pin, 0, wxALIGN_CENTER_VERTICAL);
+ opt_sizer->Add(new wxStaticText(m_options, wxID_ANY, _L("Pin note to sidebar")), 0,
+ wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(6));
+ m_options->SetSizer(opt_sizer);
+ m_options->Enable(!note.empty());
+ sizer->Add(m_options, 0, wxALL, FromDIP(10));
+
+ auto* btns = new DialogButtons(this, {"OK", "Cancel"});
+ btns->GetOK()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
+ if (m_input->GetTextCtrl()->GetValue().length() > 500) {
+ RichMessageDialog dlg(this, _L("The note is very long. Keep it anyway?"), _L("Preset note"),
+ wxYES_NO | wxICON_QUESTION);
+ dlg.SetYesNoLabels(_L("Keep"), _L("Shorten"));
+ if (dlg.ShowModal() != wxID_YES)
+ return;
+ }
+ EndModal(wxID_OK);
+ });
+ btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
+ restore_original();
+ EndModal(wxCANCEL);
+ });
+ sizer->Add(btns, 0, wxEXPAND);
+
+ SetSizerAndFit(sizer);
+ update_preview();
+ CenterOnParent();
+ wxGetApp().UpdateDlgDarkUI(this);
+}
+
+wxString PresetNoteDialog::get_note() const { return m_input->GetTextCtrl()->GetValue(); }
+
+void PresetNoteDialog::update_preview()
+{
+ m_preview->SetLabel(wxString::Format(_L("Shown in the sidebar as: %s"), get_note()));
+ m_preview->Wrap(FromDIP(360));
+
+ // Persist the draft a moment later, once typing settles.
+ std::weak_ptr<bool> alive = m_alive;
+ wxGetApp().CallAfter([this, alive] {
+ if (alive.expired() || IsBeingDeleted())
+ return;
+ wxGetApp().app_config->set("preset_note_draft", into_u8(get_note()));
+ });
+}
+
+void PresetNoteDialog::restore_original()
+{
+ m_input->GetTextCtrl()->ChangeValue(m_original);
+ wxGetApp().app_config->set("preset_note_draft", "");
+}
+
+void PresetNoteDialog::on_dpi_changed(const wxRect&)
+{
+ m_input->Rescale();
+ GetSizer()->SetSizeHints(this);
+ Refresh();
+}
+
+}} // namespace Slic3r::GUI
@@ -0,0 +1,858 @@
# Colours and dark mode
Covers `wxColour`, system colours and the appearance API, `wxEVT_SYS_COLOUR_CHANGED`, wxMSW's own dark
mode, colour inheritance and the places native controls ignore colours, `StateColor`, Orca's dark-mode
machinery (the dark-mode state, the `Update*DarkUI` walk, NppDarkMode, the runtime switch per platform) and
dark-mode icons. Read it before setting any colour, adding a dialog/panel, or debugging "wrong colour in
dark (or light) mode".
Contents: [Rules](#rules) · [wxColour](#wxcolour) · [System colours and appearance](#system-colours-and-appearance) ·
[wxEVT_SYS_COLOUR_CHANGED](#wxevt_sys_colour_changed) · [wxMSW dark mode](#wxmsw-dark-mode-mswenabledarkmode-setappearance-wxdarkmodesettings) ·
[Window colours and native-control limits](#window-colours-inheritance-and-native-control-limits) ·
[StateColor](#statecolor) · [Orca dark-mode state](#orca-dark-mode-state) ·
[The Update*DarkUI walk](#the-updatedarkui-walk) · [NppDarkMode](#nppdarkmode-windows) ·
[Runtime theme switch](#runtime-theme-switch-and-re-applying-colours) · [Dark-mode icons](#dark-mode-icons)
## Rules
1. Ask Orca, not wx, whether the UI is dark: `wxGetApp().dark_mode()`. Never `wxSystemSettings::GetAppearance().IsDark()`
or `SelectLightDark()` in GUI code — on Windows they report the system app mode — and do not read the
`dark_color_mode` key directly (macOS `dark_mode()` ignores it). → [Orca state](#orca-dark-mode-state)
2. Theme with palette colours, not `wxSYS_COLOUR_*`: on Windows wx serves its dark palette whenever the
system app mode is dark, and macOS system colours are dynamic so the dark map cannot key them. → [System colours](#system-colours-and-appearance)
3. Write colours as `wxColour("#RRGGBB")` or `wxColour(r, g, b)`. A packed integer `wxColour(0xRRGGBB)` is
read as `0x00BBGGRR`; `StateColor` integers are the opposite (`0xRRGGBB`). → [wxColour](#wxcolour)
4. Give every `wxPanel`/`wxScrolledWindow` you create an explicit palette background (`*wxWHITE`, `#F8F8F8`, …),
and set it **before** creating Orca widgets inside it. → [Window colours](#window-colours-inheritance-and-native-control-limits), [Update*DarkUI](#the-updatedarkui-walk)
5. Background colour is never inherited; foreground only at creation, only from the immediate parent, only for
`wxControl`-like classes. Set colours on the window that shows them. → [Window colours](#window-colours-inheritance-and-native-control-limits)
6. In a `StateColor`, list specific states first and `Normal` last; negate with `Not*`/`Disabled`, never `~X`. → [StateColor](#statecolor)
7. A literal light colour given to `SetForegroundColour`/`SetBackgroundColour`/`wxPen`/`wxBrush` goes through
`StateColor::darkModeColorFor()` when it is a `gDarkColors` key, else branch on `dark_mode()`. → [Re-applying colours](#runtime-theme-switch-and-re-applying-colours)
8. End every dialog constructor (after all children exist, before `ShowModal()`) with `wxGetApp().UpdateDlgDarkUI(this)`;
frames use `UpdateFrameDarkUI`; a subtree built after the app-wide pass ends with `UpdateDarkUIWin(this)`.
`UpdateDarkUI(win)` themes one window only. → [Update*DarkUI](#the-updatedarkui-walk)
9. Apply deliberate non-palette colours **after** the walk, and again on theme change; the dark walk rewrites every
visited window's foreground. → [Update*DarkUI](#the-updatedarkui-walk)
10. Re-apply every construction-time colour and every name-selected icon in `on_sys_color_changed()` (DPIDialog/DPIFrame)
or a `sys_color_changed()` chained from the owner, ending with `Refresh()`. A long-lived (cached, lazily built,
hidden) window must be reached from `MainFrame::on_sys_color_changed`. → [Runtime switch](#runtime-theme-switch-and-re-applying-colours)
11. A `wxEVT_SYS_COLOUR_CHANGED` handler on a TLW or container calls `Skip()` and is idempotent. Do not rely on the
event reaching nested children in Orca. → [wxEVT_SYS_COLOUR_CHANGED](#wxevt_sys_colour_changed)
12. Do not call `wxApp::SetAppearance()` or change the `MSWEnableDarkMode(DarkMode_Auto)` → `NppDarkMode::InitDarkMode()`
order in `GUI_App::on_init_inner`. → [wxMSW dark mode](#wxmsw-dark-mode-mswenabledarkmode-setappearance-wxdarkmodesettings)
13. An explicit `dark_color_mode` ("0" or "1") wins in both directions; `check_dark_mode()` is only the fallback for an
unset key. → [Orca state](#orca-dark-mode-state)
14. A new theme-switch path first sets the `dark_color_mode` key (Windows: `app_config->set` + `save()`; macOS/Linux:
`update_dark_config()`), then refreshes the state computed from `dark_mode()`: `m_is_dark_mode` (`Update_dark_mode_flag()`,
which `update_dark_config()` already calls), StateColor's `gDarkMode` and the label colours (`init_label_colours()`)
and, on Windows, NppDarkMode's `g_darkModeEnabled` (`force_colors_update()`). `dark_mode()` itself is live; wx's own
MSW mode follows the system and is not Orca's to set. → [Orca state](#orca-dark-mode-state)
15. Author single-tone SVGs in the substitution palette (uppercase hex, `#262E30` for near-black line art); anything
else needs a `*_dark` asset chosen by name in code that re-runs on theme change. → [Icons](#dark-mode-icons)
16. Never use a native `wxButton`'s `SetBackgroundColour` for styling (MSW turns it owner-drawn, macOS ignores it):
use Orca `Button` + `SetStyle()`. → [Window colours](#window-colours-inheritance-and-native-control-limits)
17. Re-apply text-control colours after every `Enable()` (macOS resets them). → [Window colours](#window-colours-inheritance-and-native-control-limits)
18. Windows menu bitmaps follow wx's menu state (`check_dark_mode()`), not `dark_mode()`. → [Icons](#dark-mode-icons)
## wxColour
**Contract.** Constructors: `()`, `(r, g, b, a = wxALPHA_OPAQUE)`, `(unsigned long|long|int|unsigned int)` ("A
packed RGB value", `interface/wx/colour.h:98-101`), `(const wxString&|const char*|const wchar_t*)`;
`wxColour(bool) = delete` (`include/wx/colour.h:225-240`; `docs/changes.txt:246-248`: code "unintentionally
and mistakenly using wxColour ctor from bool … doesn't compile any longer").
| Fact | Cite |
|---|---|
| Packed integers are `0x00BBGGRR` (`0xAABBGGRR` for `SetRGBA`): "Notice the right-to-left order of components!" `wxColour(0x009688)` is R=0x88 G=0x96 B=0x00, not Orca teal. Only symmetric greys (`0xEEEEEE`) read the same both ways. wx writes its own MSW dark palette this way (`wxColour(0x9e5315)` is a blue). | `interface/wx/colour.h:179-183`; `include/wx/colour.h:77-83`; `src/msw/darkmode.cpp` `wxDarkModeSettings::GetColour` |
| `Set(const wxString&)` accepts colour-database names, CSS `rgb(r,g,b)`/`rgba(r,g,b,a)` (case-insensitive) and `#` + 6 hex digits; returns `false` on failure. **[source]** the parser also takes `#rgb`, `#rgba`, `#rrggbbaa`, but the documented form (and XRC, "but not "#rgb"") is `#RRGGBB` — write that. | `interface/wx/colour.h:288-301`; `src/common/colourcmn.cpp` `FromString`; `docs/doxygen/overviews/xrc_format.h:229` |
| The string ctor is `{ Set(colourName); }`: a typo silently yields `IsOk() == false`. An invalid colour passed to `SetBackgroundColour`/`SetForegroundColour` means "reset to the default colour". | `include/wx/colour.h:235`; `interface/wx/colour.h:239-243`; `interface/wx/window.h:2438-2439` |
| 3.3 changed `wxColourDatabase` to CSS values ("GREEN" is `#008000` in the CSS scheme, `#00ff00` traditionally; wxGTK already used CSS); `UseScheme()` reverts. **[source]** stock objects did not change: `*wxGREEN` is still (0,255,0), so `*wxGREEN != wxColour("green")`. | `docs/changes.txt:31-33`; `interface/wx/gdicmn.h:999-1013`; `src/common/gdicmn.cpp:516`, `:805-807` |
| `wxTransparentColour` = `wxColour(0,0,0,wxALPHA_TRANSPARENT)`: valid, black, alpha 0. `IsTransparent()/IsOpaque()/IsTranslucent()` are new in 3.3.1. `Alpha()` returns `wxALPHA_OPAQUE` "on platforms where alpha is not yet supported". | `include/wx/colour.h:31`; `interface/wx/colour.h:110-114`, `:260-282` |
| `GetLuminance()` = 0.299R + 0.587G + 0.114B on 0..1. | `interface/wx/colour.h:212-222` |
| `ChangeLightness(ialpha)`: 0 = black, 100 = unchanged, 200 = white; returns a copy. **[source]** values are clamped to 0..200 and the result is built as `wxColour(r,g,b)` — alpha is dropped. | `interface/wx/colour.h:365-377`; `src/common/colourcmn.cpp` `wxColourBase::ChangeLightness` |
| `MakeDisabled(brightness = 255)` "modifies the object in place and returns the object itself". **[source]** each channel becomes `brightness + 0.4·(c − brightness)` — it keeps 40% of the colour and moves 60% toward `brightness`, so on a dark background the default makes a "disabled" colour light. | `interface/wx/colour.h:336-342`; `src/common/colourcmn.cpp` `MakeDisabled`, `AlphaBlend` |
Alpha in window colours: **[source]** MSW brushes are `CreateSolidBrush(COLORREF)` (alpha dropped) and pen/brush
transparency is style-based, so a solid brush of a transparent colour paints **black** through GDI. Use
`*wxTRANSPARENT_BRUSH` or `wxGCDC`/`wxGraphicsContext` (`src/msw/brush.cpp:187`; `include/wx/brush.h:60-68`).
Background styles and transparent windows: see `references/painting-custom-widgets.md`.
**OrcaSlicer.** Build colours from hex strings or RGB triples, never from names. Text over user/filament
swatches is chosen by luminance (`clr.GetLuminance() < 0.51 ? *wxWHITE : *wxBLACK`, `wxExtensions.cpp` swatch
helpers, `PresetComboBoxes.cpp`). A packed `wxColour(0x…)` literal is harmless only for a symmetric grey
(`wxColour(0xEEEEEE)`); integer literals in `StateColor` contexts are RGB.
**Pitfalls**
- **Rule:** Never write a packed integer `wxColour`; keep `StateColor` integers as they are.
**Why:** `wxColour(unsigned long)` is BGR; `StateColor(unsigned long)`/`append(unsigned long, …)` byte-swap the value
so it is RGB (`Widgets/StateColor.cpp` `StateColor::append`). "Fixing" one to look like the other inverts R and B.
```cpp
// Wrong: R=0x88 G=0x96 B=0x00 (olive), not #009688
label->SetForegroundColour(wxColour(0x009688));
// Right:
label->SetForegroundColour(wxColour("#009688"));
// Also right (StateColor integers are 0xRRGGBB, alpha byte 0 = opaque):
box->SetBorderColor(StateColor(std::make_pair(0x009688, (int) StateColor::Hovered),
std::make_pair(0xDBDBDB, (int) StateColor::Normal)));
```
Cite: `interface/wx/colour.h:179-183`; `Widgets/StateColor.cpp` `StateColor::append(unsigned long, int)`.
- **Rule:** Validate colours from external data with `Set()`.
**Why:** an invalid colour reaching a setter silently resets the window to its default colour.
```cpp
// Wrong: wxColour c(user_str); win->SetBackgroundColour(c);
// Right:
wxColour c; if (!c.Set(user_str)) c = fallback; win->SetBackgroundColour(c);
```
Cite: `include/wx/colour.h:235`; `interface/wx/window.h:2438-2439`.
- **Rule:** `ChangeLightness(80)` darkens by 20%; there are no negative arguments. On dark backgrounds pass a dark
`brightness` to `MakeDisabled` or use a palette disabled colour (`#6B6B6B`/`#ACACAC`, mapped in dark).
Cite: `interface/wx/colour.h:365-377`, `:336-342`.
## System colours and appearance
**Contract.** `wxSystemSettings::GetColour(index)`: the values "map 1:1 the native values supported by the Windows'
GetSysColor function. Note that other ports … usually map the same colour to various wxSYS_COLOUR_* values"; "The
returned colour is always valid" (`interface/wx/settings.h:44-48`, `:398-407`). New in 3.3.2:
`wxSYS_COLOUR_GRIDLINES`, `wxSYS_COLOUR_LISTBOXHIGHLIGHT` (`interface/wx/settings.h:120-141`). `wxSYS_COLOUR_FRAMEBK` = BTNFACE.
`wxSystemSettings::GetAppearance()` returns `wxSystemAppearance` (`interface/wx/settings.h:288-371`):
| Method | Contract |
|---|---|
| `IsDark()` | "checks the appearance of the current application and not the other applications on the system, so under MSW … will return false even if dark mode is used system-wide unless the application opted in using dark mode using wxApp::MSWEnableDarkMode()" (`:332-346`). An incompatible 3.3 change (`docs/changes.txt:71-73`). |
| `AreAppsDark()` (3.3.0) | system-wide app dark mode "even if it's not enabled for this particular application"; same as `IsDark()` off MSW (`:308-321`). |
| `IsSystemDark()` (3.3.0) | the "Windows mode", which can differ from the "app mode" (`:348-358`). |
| `IsUsingDarkBackground()` | luminance fallback, "generally not very useful to call directly" (`:360-370`). |
| `GetName()` | "only implemented for macOS", e.g. "NSAppearanceNameAqua"; empty elsewhere (`:323-330`). |
| `wxSystemSettings::SelectLightDark(light, dark)` (3.3.0) | "just a convenient helper using wxSystemAppearance::IsDark()" (`:460-473`); literally `GetAppearance().IsDark() ? dark : light` (`include/wx/settings.h:234-237`). |
**Platforms** (all **[source]**):
| Port | `IsDark()` | `GetColour()` |
|---|---|---|
| MSW | `wxMSWDarkMode::IsActive()` ‖ luminance fallback (`src/msw/settings.cpp:431-442`). Under `DarkMode_Auto`, `IsActive()` is uxtheme's `ShouldAppsUseDarkMode()` (`src/msw/darkmode.cpp` `ShouldUseDarkMode`) — the **system app mode**, not the app's own choice. `AreAppsDark()/IsSystemDark()` read `AppsUseLightTheme`/`SystemUsesLightTheme` from the registry. | `GetSysColor`, except GRIDLINES→BTNFACE, LISTBOXTEXT→WINDOWTEXT, LISTBOXHIGHLIGHT→HIGHLIGHT, LISTBOX→WINDOW, MENUBAR→MENU unless flat menus (`src/msw/settings.cpp:99-148`). LISTBOXHIGHLIGHTTEXT maps only to a raw index `GetSysColor` does not define — use HIGHLIGHTTEXT. **When wx dark mode is active every index is answered by `wxDarkModeSettings::GetColour()` first**; indices it leaves invalid (GRAYTEXT, 3DLIGHT, borders, …) fall back to the light `GetSysColor` value. |
| macOS | `[NSApp effectiveAppearance]` best match is DarkAqua (`src/osx/cocoa/settings.mm` `IsDark`). | WINDOW/LISTBOX = `controlBackgroundColor`, BTNFACE = `windowBackgroundColor` (≥10.14), caption/border/MENU/MENUBAR = `windowFrameColor`, BTNTEXT/WINDOWTEXT/MENUTEXT/CAPTIONTEXT/INACTIVECAPTIONTEXT/INFOTEXT/LISTBOXTEXT = `controlTextColor` (GRAYTEXT = `disabledControlTextColor`, HIGHLIGHTTEXT = `selectedTextColor`), GRIDLINES = `gridColor`, INFOBK/APPWORKSPACE = `windowBackgroundColor` (commented "bogus"), HOTLIGHT = `linkColor` (`src/osx/cocoa/settings.mm` `GetColour`). The result wraps a **dynamic NSColor**: components resolve at call time under the effective appearance (`src/osx/cocoa/colour.mm`), and as a view background it adapts by itself. |
| GTK3 (the default Linux build) | `IsUsingDarkBackground()`: luminance(WINDOWTEXT) − luminance(WINDOW) > 0.2 (`src/common/settcmn.cpp:96-112`); `AreAppsDark()/IsSystemDark()` = `IsDark()` (`:71-84`). wxGTK3 follows the freedesktop portal `org.freedesktop.appearance` `color-scheme` (GNOME's dark style) by setting `gtk-application-prefer-dark-theme` and stripping a `-dark`/`-Dark` theme-name suffix; no portal when `GTK_THEME` is set (`src/gtk/settings.cpp:251-334`, `:369-400`, `:1426-1450`). | From synthetic `GtkStyleContext`s (button, textview, treeview, headerbar, tooltip, menu), **cached** in `gs_systemColorCache` until "notify::gtk-theme-name" or a colour-scheme change (`src/gtk/settings.cpp:728-880`). |
| GTK2 (opt-out build, `-DDEP_WX_GTK3=OFF`) | as GTK3 (luminance of the GTK2 theme); no portal/colour-scheme support (`#ifdef __WXGTK3__`). | `GtkStyle` of helper widgets (`src/gtk/settings.cpp:913ff`). |
| Wayland | no Wayland-specific branch in colour/appearance code. | — |
MSW dark palette (`wxDarkModeSettings::GetColour`, `src/msw/darkmode.cpp:300-370`; "not documented and are subject
to change", `interface/wx/msw/darkmode.h:74-84`): WINDOW/LISTBOX/INFOBK/APPWORKSPACE/ACTIVECAPTION `0x202020`;
the *TEXT indices `0xe0e0e0` except INACTIVECAPTIONTEXT `0xaaaaaa` (GRAYTEXT is left invalid); BTNFACE/GRIDLINES `0x333333`; MENU/INACTIVECAPTION `0x2b2b2b`; MENUBAR/LISTBOXHIGHLIGHT
`0x626262`; HIGHLIGHT/MENUHILIGHT `0x9e5315` (a blue — packed BGR); HOTLIGHT `0xe48435`.
**OrcaSlicer.** Orca does not theme with system colours or with `wxVisualAttributes`/`GetClassDefaultAttributes()`
(the stock advice for matching native controls): it uses a fixed palette with a hand-written dark twin map
(`StateColor`), because the exact-match map needs deterministic RGB that dynamic macOS colours and the MSW dark
palette do not give, and because Windows needs an app-level live toggle wx cannot do (see
[wxMSW dark mode](#wxmsw-dark-mode-mswenabledarkmode-setappearance-wxdarkmodesettings)). `GUI_App::init_label_colours`
still reads `wxSYS_COLOUR_WINDOWTEXT`/`wxSYS_COLOUR_WINDOW` for two light-mode values — on Windows with a dark system
and Orca light those come from wx's dark palette. macOS: Orca's wx is patched to read NSColor components in sRGB
instead of `NSCalibratedRGBColorSpace` (`deps/wxWidgets/0001-macos-use-srgb-colour-components.patch`, commit
a7775296b0 "Fix macOS custom color accuracy"), so RGB read back from native colours matches the expected hex.
**Pitfalls**
- **Rule:** Never branch on wx's appearance in Orca GUI code.
**Why:** Orca calls `MSWEnableDarkMode(DarkMode_Auto)`, so on Windows `IsDark()`/`SelectLightDark()` follow the
system app mode — Windows dark + Orca light reports dark.
```cpp
// Wrong:
auto c = wxSystemSettings::SelectLightDark(wxColour("#FFFFFF"), wxColour("#2D2D31"));
// Right:
auto c = StateColor::darkModeColorFor(wxColour("#FFFFFF")); // #FFFFFF is a gDarkColors key
auto d = wxGetApp().dark_mode() ? wxColour("#EFEFF0") : wxColour("#333333"); // unmapped colour
```
Cite: `src/msw/settings.cpp:431-442`; `src/msw/darkmode.cpp` `ShouldUseDarkMode`; 7d7f26ed69.
- **Rule:** Theme with palette colours, not `wxSystemSettings::GetColour(wxSYS_COLOUR_*)`.
**Why:** MSW returns wx's dark palette whenever the system app mode is dark, regardless of Orca's setting
(`src/msw/settings.cpp:99-107`); on macOS the value is a dynamic colour no `gDarkColors` key matches; on GTK it
is whatever the theme says. Do not cache macOS system-colour RGB across a theme change.
- **Rule:** Raw `wxButton` code that branches on `__WXMAC__` to `wxSYS_COLOUR_BTNFACE`/`BTNTEXT` only keeps the text
in the system colour: NSButton ignores the background colour anyway (see
[native limits](#window-colours-inheritance-and-native-control-limits)). New code uses Orca `Button`.
## wxEVT_SYS_COLOUR_CHANGED
**Contract.** "generated when the user changes the colour settings or when the system theme changes (e.g. automatic
dark mode switching on macOS)". "The default event handler for this event propagates the event to child windows,
since the system events are only sent to top-level windows. If intercepting this event for a top-level window,
remember to either call wxEvent::Skip() on the event, call the base class handler, or pass the event on to the
window's children explicitly" (`interface/wx/event.h:1942-1966`). **[source]** the default handler
(`wxWindowBase::OnSysColourChanged`, `src/common/wincmn.cpp:3011-3027`) sends a fresh event to every
**non-top-level** child and calls `Refresh()`; a child whose own handler does not `Skip()` stops it for its subtree.
**Who sends it** (all **[source]**):
| Port | Trigger |
|---|---|
| MSW | `WM_SYSCOLORCHANGE`, and `WM_SETTINGCHANGE` with "ImmersiveColorSet" (light/dark/accent switch) → `HandleSysColorChange()` on each TLW (`src/msw/window.cpp:3542`, `:5092`, `:5197-5200`). The default MSW handler re-sends a real `WM_SYSCOLORCHANGE` to native children (`wxWindowMSW::OnSysColourChanged`, `:5269-5293`); `wxFrame` also resets its background to `wxSYS_COLOUR_APPWORKSPACE` if `!UseBgCol()` and re-themes the menubar (`src/msw/frame.cpp:476-503`). |
| macOS | per-NSWindow KVO on `effectiveAppearance`, **and** `windowDidChangeBackingProperties` when the window's colour space changes (dragging to a display with another profile) — no theme change involved (`src/osx/cocoa/nonownedwnd.mm:684-740`). Popups are `wxNonOwnedWindow`s and get it too. No defined order across windows. |
| GTK3 | every TLW connects "notify::gtk-theme-name" with `g_signal_connect_after` so the colour cache is cleared before user handlers run (`src/gtk/toplevel.cpp:555-563`, `:951-955`); the portal colour-scheme handler `DoUpdateColorScheme` also loops over `wxTopLevelWindows` (`src/gtk/settings.cpp:251-334`). A dark switch that also renames the theme delivers the event **twice** per TLW. |
| GTK2 (opt-out build) | "notify::gtk-theme-name" only. |
`wxDialogBase::OnSysColourChanged` exists (`src/common/dlgcmn.cpp:561`) but no event table references it.
**Usage.**
```cpp
Bind(wxEVT_SYS_COLOUR_CHANGED, [this](wxSysColourChangedEvent& e) {
e.Skip(); // keep propagation to children (and the wxFrame/wxWindowMSW base work)
recolor(); // idempotent and cheap: may fire twice (GTK3) or without any theme change (macOS)
});
```
**OrcaSlicer.** `DPIAware<P>` (`GUI_Utils.hpp`) binds the event on every platform:
- macOS/Linux: `update_dark_config()` (writes `dark_color_mode` from `GetAppearance().IsDark()` and calls
`Update_dark_mode_flag()`), then the virtual `on_sys_color_changed()`, then `Skip()`.
- Windows: the body is empty and deliberately does not `Skip()` ("Not calling Skip() is what stops the event
propagating on Windows"). That consumes the event at every DPIAware TLW, so wx's
`wxWindowMSW::OnSysColourChanged` and every child handler (Plater's included) never run there. On Windows the
theme switch comes only from Preferences ([runtime switch](#runtime-theme-switch-and-re-applying-colours)).
Elsewhere containers that bind the event without `Skip()` stop it for their subtree (`ButtonsListCtrl` in
`Notebook.cpp` binds an empty handler; `Plater::priv::on_apple_change_color_mode` updates the GL canvases and does
not skip). Long-lived UI is therefore refreshed through the `MainFrame::on_sys_color_changed` fan-out, not by
child handlers.
**Pitfalls**
- **Rule:** A TLW or container handler calls `Skip()`.
**Why:** without it wx's default handler never propagates to the children (`interface/wx/event.h:1951-1956`).
Cite: `src/common/wincmn.cpp:3011-3027`.
- **Rule:** Make the handler idempotent and cheap; never assume one event per theme change.
**Why:** macOS fires on display colour-space changes; GTK3 can fire twice per TLW.
- **Rule:** Never put Orca re-theming in a child's SYS_COLOUR handler; implement `on_sys_color_changed()` /
`sys_color_changed()` and make sure the owner calls it.
**Why:** on Windows the event never reaches children; elsewhere any non-skipping ancestor swallows it.
## wxMSW dark mode: MSWEnableDarkMode, SetAppearance, wxDarkModeSettings
**Contract.** `bool wxApp::MSWEnableDarkMode(int flags = 0, wxDarkModeSettings* settings = nullptr)` (3.3.0,
`@onlyfor{wxmsw}`, `interface/wx/app.h:1418-1465`):
- "experimental"; uses "undocumented, and unsupported by Microsoft, functions"; works on Windows 10 later than
v1809 (including LTSC 2019) and all Windows 11; testing before 20H1 (v2004) "has been limited".
- Flags: default follows the system ("dark mode is only used if it is the default mode for the applications on
the current system"); `DarkMode_Always` forces dark. **[source]** `DarkMode_Auto = 0` exists in
`include/wx/msw/app.h:48` although only `DarkMode_Always` is documented.
- Returns `true` if enabled, `false` "most likely because the system doesn't support dark mode".
- Alternatives: the `msw.dark-mode` system option (1 = `MSWEnableDarkMode()`, 2 = `DarkMode_Always`, settable by
environment variable from outside the app, `interface/wx/sysopt.h:85-89`), or `SetAppearance(System|Dark)`.
- Known limitations (`interface/wx/app.h:1434-1448`): anything `TaskDialog()`-based has no dark mode —
`wxMessageBox()`, `wxMessageDialog`, `wxRichMessageDialog`, `wxProgressDialog`, simple `wxAboutBox()` (wx suggests
`wxGenericMessageDialog`/`wxGenericProgressDialog`); common-dialog wrappers `wxColourDialog`, `wxFindReplaceDialog`,
`wxFontDialog`, `wxPageSetupDialog`, `wxPrintDialog`; `wxTimePickerCtrl`, `wxDatePickerCtrl`, `wxCalendarCtrl`
stay light; toolbar items with `wxToolBar::SetDropdownMenu()` draw the drop-down "almost invisible".
`AppearanceResult wxApp::SetAppearance(Appearance::System|Light|Dark)` (3.3.0, `interface/wx/app.h:1152-1190`):
GTK/macOS follow the system by default and the call is immediate and "affects all the existing windows as well
as any windows created after this call"; "Under MSW, the default appearance is always light" and an app that
wants to follow the system must call it with `Appearance::System`; "the appearance can be only set before any
windows are created and calling this function too late will return AppearanceResult::CannotChange" (only wxMSW
returns it);
`Failure` e.g. "because `GTK_THEME` is defined".
`wxDarkModeSettings` (`interface/wx/msw/darkmode.h:30-111`), passed to `MSWEnableDarkMode()`: `GetColour(wxSystemColour)`
(defaults "not documented and are subject to change"; the doc example names `0x202020` as the default background);
`GetMenuColour(wxMenuColour)` — menu-bar colours match no `wxSystemColour`, affect top-level menus only (items use
`wxOwnerDrawn::SetTextColour()`), "must be valid"; `GetBorderPen()` — invalid pen = system `wxStaticBox` border,
which "doesn't look very well in dark mode"; the base returns grey.
**[source] facts the docs do not state:**
- `SetAppearance` on MSW returns `CannotChange` when any TLW exists **or `MSWEnableDarkMode` was already called**
(`gs_appMode != AppMode_Default`); `SetAppearance(Light)` returns `Ok` without doing anything
(`src/msw/darkmode.cpp` `wxApp::SetAppearance`). macOS `System` sets `NSApp.appearance` to
`[NSAppearance currentAppearance]` rather than nil, pinning the current look (`src/osx/cocoa/utils.mm:485-513`);
GTK3 maps to the portal colour-scheme machinery; GTK2 always returns `Failure` (`src/gtk/app.cpp:355-381`).
- `MSWEnableDarkMode` may be called any time, but dark title bars are applied only to TLWs **created** afterwards
(`EnableForTLW` at the end of TLW creation, `src/msw/toplevel.cpp:513`) and controls are dark-enabled at creation
(`MSWCreateControl` → `AllowForWindow` and, for some controls, `SetForegroundColour(LISTBOXTEXT)`,
`src/msw/control.cpp:133-140`). wx cannot flip existing windows — the reason `SetAppearance` refuses late calls.
- wx takes ownership of the settings pointer (`wxDarkModeModule::SetSettings`) — allocate it with `new`.
- `msw.dark-mode` is read in `wxApp::Initialize` (`src/msw/app.cpp:492-494`): a user's `WX_MSW_DARK_MODE`
environment variable enables wx dark mode before Orca's own call.
- wx's owner-drawn menu path keys on `wxMSWDarkMode::IsActive()` (`src/msw/menuitem.cpp` `wxMenuItem::OnDrawItem`,
`GetColourToUse`; `src/msw/menu.cpp`; menu-bar UAH drawing in `src/msw/darkmode.cpp`).
3.3.x dark-mode fix log: 3.3.2 wxMSW (`docs/changes.txt:297-308`: checkbox accessibility in dark mode, rendering of
several controls, toolbar, menus; also "Revert use of WS_EX_COMPOSITED"), 3.3.1 (`:358-372`: wxStaticBitmap-in-notebook
crash, disabled wxButton bitmaps and wxStaticText, notebook high-contrast background, wxDataViewCtrl light-mode
border regression, selected toolbar buttons, wxComboCtrl, wxTE_RICH wxTextCtrl), 3.3.0 (`:385` "Add experimental dark
mode support to wxMSW"); XRC dark colour variants (`:494`).
**OrcaSlicer.** `GUI_App::on_init_inner` (`#ifdef __WINDOWS__`) calls `MSWEnableDarkMode(DarkMode_Auto)` and then
`NppDarkMode::InitDarkMode(init_dark_color_mode, init_sys_menu_enabled)`. wx's call exists only so that wx-drawn
menus get dark borders; NppDarkMode does the theming (title bars, explorer theme, scrollbars, list headers) because
it can switch live and wx cannot. The code comment "Orca: todo switch to native dark mode support in wxWidgets and
remove NppDarkMode" records the intent; the blocker is that a live Preferences toggle would become restart-only
(`SetAppearance` returns `CannotChange` after startup, existing windows cannot be restyled) and TaskDialog/common
dialogs/pickers stay light anyway — which is also why Orca has its own `MsgDialog` family and `ProgressDialog`.
**[source]** consequences of `DarkMode_Auto`: wx's internal "dark" stays `AllowDark` and follows the **system app
mode**; NppDarkMode's later `SetPreferredAppMode` call does not touch wx's `gs_appMode`. So with Windows light +
Orca dark, wx's dark machinery (menus, `GetColour()`, `IsDark()`) stays off; with Windows dark + Orca light it is
on: `GetColour()` returns the dark palette, new native controls are dark-enabled at creation, `IsDark()` is true.
**Pitfalls**
- **Rule:** Call `MSWEnableDarkMode(DarkMode_Auto)` before `NppDarkMode::InitDarkMode(...)`.
**Why:** wx 3.3 draws some native chrome (context/file menus) itself; without wx dark mode those menus keep a
light white border in dark mode. **[source]** both calls end in the same undocumented uxtheme ordinal 135
(`SetPreferredAppMode`; wx `src/msw/darkmode.cpp` `InitDarkMode`, Orca `dark_mode/dark_mode.hpp`
`AllowDarkModeForApp`) and the last call wins at OS level: NppDarkMode then sets **ForceDark or ForceLight** per
Orca's setting, overriding wx's AllowDark in both directions. In the other order wx's AllowDark would replace
Orca's forced mode. wx's own state still follows the system apps-dark setting; the two coincide only when the
system theme matches what Orca forces.
```cpp
// Wrong: only NppDarkMode knows about dark mode; wx-drawn menus stay light-bordered
NppDarkMode::InitDarkMode(init_dark, sys_menu);
// Right:
MSWEnableDarkMode(DarkMode_Auto); // enable wx 3.3's dark machinery
NppDarkMode::InitDarkMode(init_dark, sys_menu); // then ForceDark/ForceLight overrides AllowDark
```
Cite: bf397a0632 (`GUI_App.cpp`, `GUI_App::on_init_inner`).
- **Rule:** Do not call `wxTheApp->SetAppearance(...)` in Orca.
**Why:** MSW returns `CannotChange` (Orca already called `MSWEnableDarkMode`), GTK2 fails, and macOS pins
`NSApp.appearance` while `GUI_App::dark_mode()` keeps reading the system `AppleInterfaceStyle` — the two would
disagree.
- **Rule:** Do not show wx's TaskDialog-based or common dialogs where dark mode matters; use `MessageDialog` and
friends (`MsgDialog.hpp`, see `references/windows-dialogs.md`).
## Window colours, inheritance and native-control limits
**Contract** (`interface/wx/window.h`):
| Call | Effect |
|---|---|
| `SetBackgroundColour(c)` | "may not affect the entire control and could be not supported at all depending on the control and platform"; does not refresh ("you may wish to call wxWindow::ClearBackground or wxWindow::Refresh"); "will disable attempts to use themes for this window"; returns `false` if the colour was already set (`:2427-2461`). |
| `SetOwnBackgroundColour(c)` | same, "but prevents it from being inherited by the children" (`:2588-2593`). |
| `SetForegroundColour(c)` | "not all native controls support changing their foreground colour so this method may change their colour only partially or even not at all" (`:2561-2585`). |
| `SetOwnForegroundColour(c)` | non-inheritable foreground (`:2622-2627`). |
| `UseBgCol()`/`UseBackgroundColour()`, `UseForegroundColour()` | whether a colour was set for this window (`:2600-2632`). |
| `InheritsBackgroundColour()`/`InheritsForegroundColour()` | the inheritable flag (`:2595-2639`). |
| `ShouldInheritColours()` | "base class version returns false, but … overridden in wxControl where it returns true" (`:2645-2653`). |
| `InheritAttributes()` | called during creation; a child takes an attribute only if the parent set it explicitly (not via `SetOwn*`) and the child did not (`:4052-4075`). |
| `GetClassDefaultAttributes(variant)` → `wxVisualAttributes{font, colFg, colBg}` | "colBg may be wxNullColour if the controls background colour is not solid"; "All of them may be invalid if it was not possible to determine the default control appearance" (`:135-150`, `:4245-4273`). |
**[source] facts the docs do not spell out** (`src/common/wincmn.cpp`):
- **Background is never inherited.** The background branch of `InheritAttributes()` is `#if 0` ("inheriting (solid)
background colour is wrong as it totally breaks any kind of themed backgrounds", `:1524-1553`). Only font and
foreground are copied, only at creation time (`InheritAttributes()` runs from the ports' creation code), only
from the **immediate** parent.
- `ShouldInheritColours()` is false for `wxWindow`/`wxPanel` (`include/wx/window.h:1682`), `wxAnyButton`
(`include/wx/anybutton.h:105`), `wxTextCtrl` (`include/wx/textctrl.h:872`), `wxControlWithItems` (`wxChoice`,
`wxListBox`, …; `include/wx/ctrlsub.h:447`), `wxTreeCtrl` (`include/wx/treectrl.h:399`); true for other
`wxControl`s (`wxStaticText`, `wxCheckBox`, …; `include/wx/control.h:98`).
So `panel->SetForegroundColour(x)` reaches only static-text-like direct children created afterwards.
- `GetBackgroundColour()`/`GetForegroundColour()` **never return an invalid colour**: unset, they return
`GetDefaultAttributes()`, falling back to `GetClassDefaultAttributes()` = `wxSYS_COLOUR_BTNFACE` /
`wxSYS_COLOUR_WINDOWTEXT` (`:1556-1605`). Test `UseBgCol()` to know whether a colour was really set.
- `SetBackgroundColour`/`SetForegroundColour` call `SetThemeEnabled(!hasBg && !fg.IsOk())` /
`SetThemeEnabled(!hasFg && !bg.IsOk())`: once either colour is set, theming is off (`:1640-1661`).
**How children still look like their parent** (visual inheritance ≠ `GetBackgroundColour()`; **[source]**):
- MSW: a child without its own background paints the nearest ancestor's **explicit** brush while
`HasTransparentBackground()` holds (`src/msw/window.cpp` `wxWindowMSW::MSWGetBgBrush`). Containers (`wxPanel`)
report transparent if an ancestor has an inheritable background (`SetBackgroundColour`, not
`SetOwnBackgroundColour`; `src/common/containr.cpp:162-175`); `wxStaticText`, `wxCheckBox`, `wxStaticBox`,
`wxStaticBitmap`, `wxHyperlinkCtrl`, book controls, MSW `wxRadioButton`/`wxSlider` always do.
- macOS: a bare window with the default `wxBG_STYLE_ERASE` is cleared with its **own** `GetBackgroundColour()`
(`wxWindowMac::MacDoRedraw`, `src/osx/window_osx.cpp:1950-1983`; the `wxWindowDC` background is
`GetBackgroundColour()`, `src/osx/carbon/dcclient.cpp:89`) — the class default `windowBackgroundColor`, a dynamic
system grey.
- GTK3: a bare `wxPanel` is theme-enabled (`src/common/panelcmn.cpp:100`) and renders its own GTK style-context
background (`gtk_render_background` in `wxWindowGTK::GTKSendPaintEvents`, `src/gtk/window.cpp`), which depends on
the theme.
This is why "dark-theme bugs that only show on macOS" exist: MSW hides an unset panel behind the ancestor's brush,
macOS paints the system grey.
**Native controls that ignore colours** (**[source]**):
| Port | Behaviour |
|---|---|
| MSW | `wxButton`/`wxToggleButton` `SetBackgroundColour`/`SetForegroundColour` switch the native button to `BS_OWNERDRAW` (`src/msw/anybutton.cpp` `wxAnyButton::MakeOwnerDrawn`, `:1324-1390`) — the colour works but native theming is gone. `wxCheckBox`/`wxRadioButton` foreground makes them owner-drawn when themes are active (`src/msw/control.cpp` `MSWMakeOwnerDrawnIfNecessary`); 3.3.2 fixed checkbox accessibility in that mode (`docs/changes.txt:299-300`). Static-text-like children paint the ancestor brush only if that ancestor's background is inheritable. |
| macOS | `SetBackgroundColour` reaches the NSView only if it `respondsToSelector:setBackgroundColor:` and the style is not `wxBG_STYLE_TRANSPARENT` (`src/osx/cocoa/window.mm:3514-3531`): **NSButton (`wxButton`) ignores the background colour**. Foreground uses `setTextColor:` when the view has it (`:3885-3887`); stock NSButton has none. `wxStaticText` is an NSTextField with `setDrawsBackground:NO` — its background is never painted (`src/osx/cocoa/stattext.mm:154`). `wxTextCtrl`'s `setEnabled:` resets the text colour: multi-line (`wxNSTextView`) always, to `controlTextColor`/`disabledControlTextColor`; single-line (`wxNSTextField`) when it does not draw its background, to `controlTextColor`/`secondarySelectedControlColor` (`src/osx/cocoa/textctrl.mm`). |
| GTK3 | `SetBackgroundColour/SetForegroundColour/SetFont` install a per-widget CSS provider `*{color:..;background:..;font:..}` at `GTK_STYLE_PROVIDER_PRIORITY_APPLICATION` (`src/gtk/window.cpp` `wxWindowGTK::GTKApplyStyle`); `wxButton`/`wxCheckBox` apply it to their inner label too (`src/gtk/button.cpp:325-335`, `src/gtk/checkbox.cpp:233-237`). User CSS (priority USER) can still override. |
| GTK2 | colours applied with `gtk_widget_modify_style` — pixmap-engine themes may ignore them. |
**OrcaSlicer — `Utils/MacDarkMode.mm`** installs process-wide Objective-C categories/swizzles that affect *every*
window, so stock-wx advice about NSTextField/NSButton colours differs in Orca:
- every `NSTextField` is created with `drawsBackground = false` (category `NSTextField (drawsBackground)`), so with
the `textctrl.mm` rule above a custom text colour is lost on every `Enable()` and the background is not painted;
- `NSButton (NSButton_Extended)` adds `textColor`/`setTextColor:` (via the attributed title), which is what makes
`wxButton`/`wxCheckBox` foreground colours work on macOS; `setBezelStyle:` forces bordered (except shadowless
square); focus rings are off for NSButton and NSTextField; `NSTableHeaderCell`'s font is forced to `Label::sysFont(13)`;
- the main-frame title text colour is forced (`set_title_colour_after_set_title`; `NSTextField (textColor)` swizzle
for that one field), and `set_miniaturizable` makes the titlebar transparent over a dark NSWindow background.
Orca's widgets work with these limits: `Label` sets keyed colours (`SetForegroundColour("#262E30")`, background =
`StaticBox::GetParentBackgroundColor(parent)`) so the walk can map them — commit d408db2fde replaced `wxStaticText`
with `Label` to fix Linux dark mode this way; `TextInput::Enable` re-applies the inner control's background and
foreground from its `StateColor`s after enabling; `StaticBox::Create` copies the parent background into its own wx
background (a `StaticBox` parent's `background_color.defaultColor()`, the midpoint of a gradient, else
`parent->GetBackgroundColour()`), used for the corners outside the rounded rectangle.
**Pitfalls**
- **Rule:** Give every `wxPanel`/`wxScrolledWindow` you create an explicit palette background (typically
`SetBackgroundColour(*wxWHITE)`), even when white "already looks right".
**Why:** the walk re-keys a background only if it is a `gDarkColors` key. A bare panel paints its class default
(`wxSYS_COLOUR_BTNFACE`; on macOS the appearance-dependent `windowBackgroundColor`), which is never a key, so it
shows as a system-grey patch against Orca's palette in either theme. MSW hides this by painting the ancestor's
explicit brush, so it surfaces on macOS. An explicit palette colour is the key that lets the walk re-map it in
both directions.
```cpp
wxPanel* p = new wxPanel(parent); // Wrong: unkeyed; macOS shows system grey, not #2D2D31 / #FFFFFF
wxPanel* p = new wxPanel(parent);
p->SetBackgroundColour(*wxWHITE); // Right: #FFFFFF is a key → #2D2D31 in dark
```
Cite: f7f0c82abb / 90ecdb8f03 (`HMSPanel.cpp`, `StatusPanel.cpp`, `UpgradePanel.cpp`,
`DeviceTab/wgtDeviceNozzleRack*.cpp`, `SelectMachine.cpp`); `src/common/wincmn.cpp:1524-1553`.
- **Rule:** Set the parent's background before creating Orca widgets in it.
**Why:** `StaticBox`, `Label`, `CheckBox`, `SwitchButton`, `RadioGroup` snapshot the parent background at construction; a later
change leaves wrong-coloured corners/label backgrounds.
- **Rule:** Set colours on the control itself, not on an ancestor.
**Why:** background never inherits; foreground inherits only at creation, from the immediate parent, and not for
`wxPanel`, `wxTextCtrl`, buttons or item controls.
```cpp
// Wrong: dlg->SetForegroundColour(c); // after children exist, or on a wxPanel above them
// Right: label->SetForegroundColour(c); // per control
```
- **Rule:** Re-apply text-control colours after every `Enable()`.
**Why:** macOS NSTextField/NSTextView reset their text colour in `setEnabled:`, and Orca's swizzle makes every
NSTextField non-background-drawing. `TextInput::Enable` is the model.
- **Rule:** Never style a native `wxButton` with `SetBackgroundColour`; use `Button` + `SetStyle(...)`.
**Why:** MSW turns the native button owner-drawn (theming gone); macOS ignores the background colour.
## StateColor
`StateColor` (`Widgets/StateColor.hpp/.cpp`) is an ordered list of `(wxColour, int mask)` pairs plus the global
light→dark map `gDarkColors`. Orca widgets (`StaticBox` and subclasses) take `StateColor`s and resolve them at paint
time.
**Contract** (from the code):
- State bits: `Normal = 0`, `Enabled = 1`, `Checked = 2`, `Focused = 4`, `Hovered = 8`, `Pressed = 16`; the negations
`Disabled`, `NotChecked`, `NotFocused`, `NotHovered`, `NotPressed` are the same bits `<< 16` and mean "bit must be
off". Masks OR together (`StateColor::Checked | StateColor::Enabled`). The `(int)` cast on the enum is required
for `std::pair` deduction.
- `colorForStates(states)` returns the **first** entry whose on-bits are all set and off-bits all clear;
`takeFocusedAsHovered_` (default true) lets a `Hovered` entry also match `Focused` (`setTakeFocusedAsHovered(false)`
to style focus separately). No match → `wxColour(0, 0, 0, 0)` (transparent black — black through GDI).
- `colorForStates()` passes the result through the dark map when StateColor's `gDarkMode` is set, **at paint time**:
owner-drawn widgets switch theme on `Refresh()` with no extra code. `colorForStatesNoDark()` skips mapping;
`defaultColor()` = `colorForStates(0)`.
- Single-colour ctors `StateColor(wxColour)`, `(wxString)`, `(unsigned long)` store a `Normal` entry. Integer forms
(`StateColor(unsigned long)`, `append(unsigned long, int)`, `std::make_pair(0x009688, …)`) are **`0xRRGGBB`**; an
alpha byte of 0 is treated as opaque — the opposite of `wxColour(unsigned long)`.
- `gDarkColors` is a `std::map` ordered by `wxColour::GetRGBA()`: an **exact RGBA** match. Non-opaque or near-miss
colours never map. Examples: `#FFFFFF→#2D2D31`, `#009688→#00675b`, `#262E30→#EFEFF0`, `#DFDFDF→#3E3E45`,
`#D4D4D4→#4D4D54`, `#DBDBDB→#4A4A51`, `#000000→#FFFFFE`, `#F8F8F8→#36363C`, `#F1F1F1→#36363B`, `#EEEEEE→#4C4C55`,
`#6B6B6B→#818183`, `#ACACAC→#65656A`, `#363636→#B2B3B5`, `#F0F0F1→#333337`. Near-white variants (`#FFFFFE` as the
twin of `#000000`, `#FEFFFF`, `#FFFEFE`) exist so the reverse map stays usable. `#F0F0F0`, `#333333`, `#5C5C5C`
are not keys.
- Static helpers: `SetDarkMode`, `darkModeColorFor` (returns its input unless `gDarkMode` is set), `lightModeColorFor`
(the reverse map, built once with `emplace` so the smallest `GetRGBA()` key wins; not gated by `gDarkMode` — it
maps in either mode), `GetDarkMap()`, LAB math
`GetLAB`, `GetLightness`, `SetLightness`, `LightenDarkenColor`, `GetColorDifference`/`LAB_Delta_E`.
**Reverse-map and chaining hazards** (exact consequences of the table):
- duplicate dark targets collapse: `#E8E8E8 → #3E3E45 → #DFDFDF`; `#EDFAF2 → #283232 → #E5F0EE`;
- legitimate light colours that equal some dark twin are rewritten by the light walk: `#909090 → #6B6A6A`,
`#D9D9D9 → #FFFEFE`, `#808080 → #2B3436`, `#FFFFFE → #000000`;
- the dark map chains when a value is also a key: a second dark pass takes `#FFFEFE → #D9D9D9 → #27272A`.
**Usage.**
```cpp
StateColor bg(std::pair{wxColour("#DFDFDF"), (int) StateColor::Disabled},
std::pair{wxColour("#D4D4D4"), (int) StateColor::Pressed},
std::pair{wxColour("#D4D4D4"), (int) StateColor::Hovered},
std::pair{wxColour("#FFFFFF"), (int) StateColor::Normal}); // Normal LAST
box->SetBackgroundColor(bg); // StaticBox API ("Color"), not wxWindow::SetBackgroundColour
// in doRender(wxDC& dc): dc.SetBrush(background_color.colorForStates(state_handler.states()));
```
How a widget attaches and repaints its `StateColor`s (`StateHandler`, `doRender`) is in
`references/painting-custom-widgets.md`; per-widget colour setters are in `references/orca-widgets.md`.
**OrcaSlicer.** Pick existing keys for new UI. `HyperLink` deliberately uses `#009687` (not a key) so its teal is
*not* mapped. `Button::SetStyle` detects dark mode with `darkModeColorFor("#FFFFFF") != "#FFFFFF"` for the focus
border, and `Button::Rescale()` re-runs `SetStyle` (when a style was set), so calling `Rescale()` on a theme change
refreshes it. A `Button` never given a style keeps its constructor colours, whose `*wxLIGHT_GREY` hover/disabled
entries are not keys — always call `SetStyle()`.
`DialogButtons` sets its own background with `darkModeColorFor(wxColour("#FFFFFF"))`. Widgets that take a plain
`wxColour` (e.g. `ProgressBar`) paint it unmapped — map it yourself. `StaticLine` maps line/text colours at paint time.
**Pitfalls**
- **Rule:** Most specific entries first, `Normal` last.
**Why:** `Normal` (mask 0) matches every state, so entries after it are dead. Check the order of any table you copy;
existing tables are not guaranteed correct (an `Enabled` entry after `Normal` never matches).
```cpp
// Wrong: hover never shows
StateColor c(std::pair{wxColour("#FFFFFF"), (int) StateColor::Normal},
std::pair{wxColour("#D4D4D4"), (int) StateColor::Hovered});
// Right:
StateColor c(std::pair{wxColour("#D4D4D4"), (int) StateColor::Hovered},
std::pair{wxColour("#FFFFFF"), (int) StateColor::Normal});
```
Cite: `Widgets/StateColor.cpp` `StateColor::colorForStates`.
- **Rule:** Negate with the `Not*` enumerators, never `~`.
**Why:** `(int) Hovered | NotFocused` = `0x40008` means "hovered and not focused"; `(int) Hovered | ~Focused` =
`0xFFFFFFFB` sets every on-bit and never matches.
- **Rule:** Do not pass data colours (filament/extruder/user colours) through `darkModeColorFor` or leave them as
window backgrounds the walk visits; paint them in a paint handler or re-set them after the walk.
**Why:** a white filament `#FFFFFF` maps to `#2D2D31`.
- **Rule:** Never use a dark-twin value as a light-mode colour (`#FFFFFE`, `#D9D9D9`, `#909090`, `#808080`, …).
**Why:** the light walk rewrites it through the reverse map.
## Orca dark-mode state
| State | Set by | Read by |
|---|---|---|
| app_config `dark_color_mode` ("1"/"0") | Windows: the Preferences checkbox (`PreferencesDialog::create_item_darkmode`, `#ifdef _WIN32`); `AppConfig::set_defaults` sets `"0"` when empty (`#ifdef _WIN32`). macOS/Linux: overwritten from `GetAppearance().IsDark()` at startup (`GUI_App::on_init_inner`, `#ifndef __WINDOWS__`) and by `update_dark_config()` on every SYS_COLOUR event. | `GUI_App::dark_mode()` (non-macOS); `Plater::priv::on_change_color_mode` and other GL/web code that reads the key directly — new code calls `dark_mode()` instead |
| `GUI_App::m_is_dark_mode` | `GUI_App::Update_dark_mode_flag()` (= `dark_mode()`) | `GUI_App::UpdateDarkUI` (the walk) only |
| `StateColor` file-static `gDarkMode` | `StateColor::SetDarkMode`, called only from `GUI_App::init_label_colours()` | `darkModeColorFor`, `colorForStates` |
| NppDarkMode `g_darkModeEnabled` (Windows) | `NppDarkMode::InitDarkMode` / `SetDarkMode` | title bars, explorer theme, scrollbars, DVC header |
| wx's own MSW mode | `MSWEnableDarkMode(DarkMode_Auto)` → follows the **system app mode** | wx internals, `IsDark()`, `GetColour()` |
`GUI_App::dark_mode()` (static, recomputed every call) is the only query GUI code uses:
- macOS: `wxPlatformInfo::Get().CheckOSVersion(10, 14) && mac_dark_mode()` (10.12/10.13 gave false positives);
`mac_dark_mode()` (`Utils/MacDarkMode.mm`) reads the `AppleInterfaceStyle` user default == "Dark" — the **system**
preference. The config key is ignored. wx's `IsDark()` (which feeds `update_dark_config()`) reads
`[NSApp effectiveAppearance]`; the two normally agree.
- elsewhere: `"1"` → true, `"0"` → false, otherwise `check_dark_mode()` (`GUI_Utils.cpp`:
`wxSystemSettings::GetAppearance().IsDark()`). On Windows the default makes the fallback effectively unreachable;
on Linux the key is only empty before `on_init_inner` writes it, so "dark" means "the GTK theme's window colours
are dark".
`check_dark_mode()` keeps `IsDark()` deliberately; switching it to `AreAppsDark()`/`IsSystemDark()` is not a required
3.3 migration: off MSW the three are identical (`src/common/settcmn.cpp:71-84`), and on MSW it is reached only for an
unset key and for Windows menu bitmaps, where matching wx's own menu state is the point.
There is **no "follow system" setting and no in-app override on macOS/Linux**: the app mirrors the OS and re-syncs
the config on each SYS_COLOUR event. `SUPPORT_DARK_MODE` is defined unconditionally in `libslic3r/AppConfig.hpp`, and
`_MSW_DARK_MODE` is defined to 1 on every platform in `GUI_App.hpp` — neither is a platform gate. Windows-only code
sits under `__WINDOWS__`/`_WIN32` (NppDarkMode sources are added only `if (WIN32)` in `src/slic3r/CMakeLists.txt`).
The other theme-dependent `GUI_App` colours (`m_color_label_modified`, `m_color_label_sys` `#363636`/`#B2B3B5`,
`m_color_label_default`, `m_color_highlight_default` `#F1F1F1`/`#36363B`, `m_color_window_default`, button
label/background) are computed by `init_label_colours()`.
**Pitfalls**
- **Rule:** An explicit `dark_color_mode` wins in both directions; `check_dark_mode()` is a last-resort fallback for
an unset key.
**Why:** on Windows, once `MSWEnableDarkMode(DarkMode_Auto)` is active, `IsDark()` reports the system app mode, so
an explicit "0" falling through to it made dark → light switching silently fail.
```cpp
// Wrong: explicit "0" falls through to the contaminated system query
return app_config->get("dark_color_mode") == "1" ? true : check_dark_mode();
// Right: explicit choice wins in both directions
const auto& val = app_config->get("dark_color_mode");
if (val == "1") return true;
if (val == "0") return false;
return check_dark_mode(); // unset key only
```
Cite: 7d7f26ed69 (`GUI_App.cpp`, `GUI_App::dark_mode`, the non-Apple branch).
- **Rule:** Any new code path that changes the theme first sets `dark_color_mode` (Windows: `app_config->set` +
`save()`; macOS/Linux: `update_dark_config()`), then refreshes the cached state: `wxGetApp().Update_dark_mode_flag()`
(`m_is_dark_mode`, read by the walk), `wxGetApp().init_label_colours()` (StateColor's `gDarkMode` and the label
colours) and, on Windows, `wxGetApp().force_colors_update()` (NppDarkMode's `g_darkModeEnabled` via
`NppDarkMode::SetDarkMode(dark_mode())`, the main frame's title bar, and the walk flag `update_ui_from_settings()`
consumes). `dark_mode()` itself is live; wx's own MSW mode follows the system and is not set by Orca.
**Why:** they are updated at different moments; `update_dark_config()` does not touch StateColor's flag — for the
main frame that happens in `MainFrame::on_sys_color_changed` — and after startup only `force_colors_update()` calls
`NppDarkMode::SetDarkMode`.
## The Update*DarkUI walk
`GUI_App` helpers (`GUI_App.hpp/.cpp`):
| Helper | Does |
|---|---|
| `UpdateDarkUI(win, highlited = false, just_font = false)` | One window. `just_font` is unused. Skips `wxBU_AUTODRAW` buttons (`wxButton` or Orca `Button`). Windows only: a `wxButton` with id `wxID_OK`/`wxID_CANCEL` gets `wxNO_BORDER`, palette background/foreground (→ owner-drawn) and four hover/focus handlers **bound again on every call**. Then, using `m_is_dark_mode` (not `dark_mode()`): **dark** — `bg = darkModeColorFor(GetBackgroundColour())`, set only if it changed (exact key); `fg = darkModeColorFor(GetForegroundColour())`, then if ΔE(bg, fg) < 10 → LAB L = 90, if L(fg) < 45 → L = 70, and **fg is always set**; **light** — `lightModeColorFor` on background and foreground, each set only if changed. |
| `update_dark_children_ui(win)` (file-static) | recursive over `GetChildren()` at call time: a `ScalableButton` gets `ScalableButton::UpdateDarkUI()` (= `msw_rescale()`: `UpdateDarkUI(this, m_has_border)` plus re-rasterized icons), anything else `UpdateDarkUI(child)`. `GetChildren()` includes owned TLWs, so dialogs parented to the window are walked too. |
| `UpdateDarkUIWin(win)` | the walk. |
| `UpdateDlgDarkUI(dlg)` / `UpdateFrameDarkUI(frame)` | Windows: `NppDarkMode::SetDarkExplorerTheme` + `SetDarkTitleBar` on the HWND (both follow Orca's mode in both directions); then the walk. |
| `UpdateDVCDarkUI(dvc, highlited)` | **Windows-only body** (no-op on macOS/Linux): `UpdateDarkUI`, dark list header via `NppDarkMode::SetDarkListViewHeader`, header attr text colour `NppDarkMode::GetTextColor()`, `SetAlternateRowColour(m_color_highlight_default)` for `wxDV_ROW_LINES`, forces `wxBORDER_SIMPLE`. |
| `UpdateAllStaticTextDarkUI(parent)` | **Windows-only body**: `UpdateDarkUI(parent)` and `m_color_label_default` on direct `wxStaticText` children. |
Consequences: an unmapped background stays as it is; an unmapped dark foreground becomes grey (L≈70) instead of
dark-on-dark; every visited window ends with an explicit foreground (on MSW that makes raw buttons/checkboxes
owner-drawn); dark → light is lossy (lifted greys are not restored; shared twins collapse, see
[StateColor](#statecolor)); the walk only calls `Set{Background,Foreground}Colour`, so `wxPaintDC` painting is
unreachable by it. The source comment on `UpdateDarkUIWin` ("Don't use this function for Dialog contains
ScalableButtons") is stale: all three entry points run the same `update_dark_children_ui`, which already handles
`ScalableButton`; the only difference is the Windows HWND theming. Use `UpdateDlgDarkUI` for dialogs because of
that theming.
App-wide passes: `MainFrame`'s constructor ends with `UpdateDarkUIWin(this)` (on all platforms); a theme switch runs
`update_dark_children_ui(mainframe)` from `GUI_App::update_ui_from_settings`; the lazily built main-window tabs re-run
`UpdateDarkUIWin(this)` after insertion. Everything created later themes itself.
Raw wx controls do not follow Orca's mode by themselves (on Windows wx themes native controls by the *system* app
mode, see [wxMSW dark mode](#wxmsw-dark-mode-mswenabledarkmode-setappearance-wxdarkmodesettings)). When one cannot
be replaced by an Orca widget, make it dark-safe through the walk (`UpdateDarkUI(ctrl)` for a single control created
after the pass, `UpdateDlgDarkUI(dlg)` for its dialog) or with explicit `darkModeColorFor()` colours.
**Pitfalls**
- **Rule:** Theme a dialog with `UpdateDlgDarkUI`, not `UpdateDarkUI`.
**Why:** `UpdateDarkUI` touches one window; children keep light defaults and the Windows title bar stays light.
```cpp
// Wrong:
wxGetApp().UpdateDarkUI(this);
// Right:
wxGetApp().UpdateDlgDarkUI(this);
```
Cite: 465f634988 (`AMSMaterialsSetting.cpp` `AMSMaterialsSetting::Show`).
- **Rule:** After building a runtime-created subtree (widgets constructed after the initial pass), end the
constructor with one `wxGetApp().UpdateDarkUIWin(this)`; for an ad-hoc `wxDialog`, call
`wxGetApp().UpdateDlgDarkUI(&dlg)` after all children exist and before `ShowModal()` (the order relative to `Fit()`
does not matter).
**Why:** the app-wide pass themes only windows that existed when it ran; HMS notify items, device
firmware/nozzle panels, one-off confirmation dialogs appear with light defaults in dark mode on every platform.
One subtree walk also themes child labels and beats sprinkling per-widget `darkModeColorFor` calls.
```cpp
// Wrong: dialog built and shown with hard-coded light colours only
dlg.SetSizer(main_sizer); dlg.Fit(); dlg.ShowModal();
// Right:
dlg.SetSizer(main_sizer); dlg.Fit();
wxGetApp().UpdateDlgDarkUI(&dlg);
dlg.ShowModal();
```
Cite: f7f0c82abb (`HMSPanel.cpp`, `DeviceTab/uiDeviceUpdateVersion.cpp`, `DeviceTab/wgtDeviceNozzleSelect.cpp`,
`SelectMachine.cpp` `SelectMachineDialog::show_timelapse_storage_dialog`).
- **Rule:** Apply deliberate non-palette colours after the walk (and again on theme change).
**Why:** in dark mode the walk rewrites every visited window's foreground — white text on an accent chip becomes
mid-grey.
```cpp
wxGetApp().UpdateDlgDarkUI(this);
m_badge->SetForegroundColour(*wxWHITE); // after the walk
```
- **Rule:** Run the dialog walk once per theme state; do not call it from paint or size handlers.
**Why:** the map chains (`#FFFEFE → #D9D9D9 → #27272A`) and the Windows OK/Cancel branch stacks handlers per call.
## NppDarkMode (Windows)
Vendored Notepad++ dark-mode code in `src/slic3r/GUI/dark_mode.cpp/.hpp` and `src/slic3r/GUI/dark_mode/*.hpp`,
compiled only on Windows. Namespace `NppDarkMode`:
| Function | Does |
|---|---|
| `InitDarkMode(bool dark, bool sys_menu)` | loads the uxtheme entry points, records the system-menu setting, `SetDarkMode(dark)`. |
| `SetDarkMode(bool)` | sets `g_darkModeEnabled`; `AllowDarkModeForApp(dark)` → `SetPreferredAppMode(ForceDark|ForceLight)` (Windows 1903+) or `AllowDarkModeForApp` (1809); flushes menu themes when the system-menu setting is on; scrollbar fix. |
| `SetDarkTitleBar(HWND)` | allows dark for the window per `IsEnabled()`, refreshes the title-bar colour, applies the explorer theme. |
| `SetDarkExplorerTheme(HWND)` | `SetWindowTheme(hwnd, IsEnabled() ? L"DarkMode_Explorer" : nullptr, nullptr)`. |
| `SetDarkListViewHeader(HWND)` | dark `ItemsView` theme on a list header. |
| `GetTextColor()` | `0xF0F0F0` when enabled, else `wxSYS_COLOUR_WINDOWTEXT` — which is wx's dark-palette `0xe0e0e0` when the Windows app mode is dark and Orca is light **[source]**. |
Unlike wx's MSW dark mode it switches live: every function follows `g_darkModeEnabled` in both directions, so
re-running them on existing HWNDs re-themes them. `GUI_App::force_colors_update()` re-arms it
(`NppDarkMode::SetDarkMode(dark_mode())`, `SetDarkTitleBar(mainframe)`); `UpdateDlgDarkUI`/`UpdateFrameDarkUI`
apply it per TLW. Its `#if wxVERSION_NUMBER < 3300` block that themed the tooltip window is dead:
`wxToolTip::GetToolTipCtrl()` is private in 3.3 (`include/wx/msw/tooltip.h:92`), and wx dark-enables the
tooltip window itself through `wxMSWDarkMode::AllowForWindow`, so tooltips follow wx's mode (the system app
mode), not Orca's **[source]** (`src/msw/tooltip.cpp:321`). `update_dark_ui(wxWindow*)` (`GUI_Utils.cpp`, `_WIN32`), which `DPIAware`'s constructor and
`force_color_changed()` call, has an empty body.
## Runtime theme switch and re-applying colours
```text
Startup (GUI_App::on_init_inner): init_label_colours() [StateColor::SetDarkMode] → Update_dark_mode_flag()
→ (non-Windows) dark_color_mode := GetAppearance().IsDark()
→ (Windows) MSWEnableDarkMode(DarkMode_Auto); NppDarkMode::InitDarkMode(dark_mode(), sys_menu)
MainFrame ctor ends with UpdateDarkUIWin(this)
Windows — Preferences "Enable dark Mode" (PreferencesDialog::create_item_darkmode, the only toggle):
1 set + save dark_color_mode 2 Update_dark_mode_flag()
3 force_colors_update(): NppDarkMode::SetDarkMode(dark_mode()), SetDarkTitleBar(mainframe), m_force_colors_update
4 update_ui_from_settings():
mainframe->force_color_changed() [_WIN32: update_dark_ui (empty) + MainFrame::on_sys_color_changed()]
update_scrolls(mainframe), update_scrolls(&m_settings_dialog)
update_dark_children_ui(mainframe) (all platforms when m_force_colors_update)
5 PreferencesDialog::set_dark_mode() → UpdateDlgDarkUI(this)
6 wxPostEvent(plater, EVT_GLCANVAS_COLOR_MODE_CHANGED) → Plater::priv::on_change_color_mode (GL canvases, sidebar)
macOS / Linux — system switch: wxEVT_SYS_COLOUR_CHANGED at every DPIAware TLW (order undefined)
→ update_dark_config() [config + m_is_dark_mode] → on_sys_color_changed() → Skip()
MainFrame::on_sys_color_changed(): init_label_colours() → force_colors_update() → update_ui_from_settings()
[update_dark_children_ui(mainframe)] → fan-out below
Plater::priv::on_apple_change_color_mode → GL canvases
```
At startup `init_label_colours()` and `Update_dark_mode_flag()` run before the key is rewritten from the system, and
the later re-check only fires when `dark_mode()` changes after that rewrite. On Linux (where `dark_mode()` reads the
key) a start after the GTK theme changed while Orca was closed therefore leaves StateColor's flag and
`m_is_dark_mode` on the previous session's value — including for the `MainFrame` constructor's walk — until the next
`MainFrame::on_sys_color_changed`. **[source; consequence inferred from the call order in
`GUI_App::on_init_inner`, not observed]**
`MainFrame::on_sys_color_changed` is the **registry for long-lived UI**: `DiffPresetDialog::if_built()`,
`m_tabpanel->Rescale()`, `m_param_panel->msw_rescale()`, `plater()->sys_color_changed()` (→ `Sidebar::sys_color_changed`
→ …), `MonitorPanel::when_built`, `CalibrationPanel::when_built`, every `Tab::sys_color_changed()` (tabs, model tabs,
plate tab), `MenuFactory::sys_color_changed(m_menubar)` (its body is compiled out with `#if 0`, so menu-bar item
icons are not re-rasterized; the cached context menus are, via `Plater::sys_color_changed` →
`MenuFactory::sys_color_changed()`), `WebView::RecreateAll()`, then `Refresh()`. A cached,
hidden or lazily built window must be added here or chained from something here: the walk reaches dialogs parented
to the main frame (they are in `GetChildren()`), but only their colours (and `ScalableButton` icons) — not their
title bar, `ScalableBitmap`-based images or `on_sys_color_changed()`. Commit bab3c72e4f fixed the compare dialog keeping old row colours by calling
`diff_dialog.on_sys_color_changed()` from this fan-out (the dialog is not destroyed on close).
Who gets what on a switch:
| Window | Windows (Preferences) | macOS/Linux (system) |
|---|---|---|
| MainFrame subtree | walk + fan-out | walk + fan-out |
| Open dialog parented to the main frame | walk only (Preferences re-themes itself) | its own `on_sys_color_changed()` via the OS event + the walk |
| Hidden cached dialog parented to the main frame | walk only, unless chained in the fan-out | walk + its own handler (the OS event reaches every TLW) |
| Modal dialogs (`dialogStack`, via `DPIAware::ShowModal`) | cannot be open across a Preferences change | live |
**What a dialog must do to survive a toggle:**
1. Derive from `DPIDialog` (`DPIAware<wxDialog>`, `GUI_Utils.hpp`; `on_sys_color_changed()` is a protected virtual
no-op by default) and use palette colours / Orca widgets.
2. End the constructor with `wxGetApp().UpdateDlgDarkUI(this)` — it also sets the Windows dark title bar and
explorer theme the children walk alone does not.
3. Override `on_sys_color_changed()` to re-create `ScalableBitmap`s (`msw_rescale()` + re-`SetBitmap`), re-pick
`*_dark` icon names, re-apply construction-time and owner-drawn colours, call widget `Rescale()` where styles
depend on the theme, then `Refresh()`. It runs on macOS/Linux from the OS event; on Windows only if the main-frame
fan-out calls it.
4. If it outlives a show (cached singleton, lazily built), register it in `MainFrame::on_sys_color_changed`.
**Pitfalls**
- **Rule:** Never hand a literal light-theme colour straight to `SetForegroundColour`/`SetBackgroundColour`/`wxPen`/`wxBrush`;
wrap it in `StateColor::darkModeColorFor(...)` when it is a `gDarkColors` key, or branch on
`wxGetApp().dark_mode()` with an explicit dark counterpart when it is not.
**Why:** `darkModeColorFor` is an exact RGBA lookup; an unmapped colour (`#F0F0F0`, `#333333`, `#5C5C5C`) passes
through unchanged and renders as a light patch, or — set directly with no walk after it — dark-on-dark text. This
also applies to owner-drawn `wxPaintDC` painting (popup borders/fills), which the walk cannot reach.
```cpp
label->SetForegroundColour(wxColour("#009688")); // Wrong: stays light-theme teal
label->SetForegroundColour(StateColor::darkModeColorFor("#009688")); // Right: → #00675b in dark
// unmapped colour: branch explicitly
msg->SetForegroundColour(wxGetApp().dark_mode() ? wxColour("#EFEFF0") : wxColour(0x33, 0x33, 0x33));
```
Cite: f7f0c82abb (`SelectMachine.cpp` `SelectMachineDialog::Enable_Auto_Refill`, `show_timelapse_folder_popup`;
`Plater.cpp` `HoverLabel`).
- **Rule:** Any colour chosen at construction time (chip backgrounds, per-state label colours, `dark_mode()`-dependent
picks) is re-applied on a live switch: override `on_sys_color_changed()` (DPIDialog/DPIFrame) or add a
`sys_color_changed()` method that the owner's `sys_color_changed()` chains to, ending with `Refresh()`.
**Why:** `dark_mode()` is live but a value computed once in a constructor is not; the walk fixes mapped colours only,
not unmapped colours or platform-conditional picks.
```cpp
// Wrong: colours set once in the ctor, never again
HoverLabel(...) { SetBackgroundColour(extruder_group_chip_bg()); ... }
// Right: also re-apply on theme switch, chained from the parent
void HoverLabel::sys_color_changed() { SetBackgroundColour(extruder_group_chip_bg()); /* label fg */ Refresh(); }
void ExtruderGroup::sys_color_changed() { if (hover_label) hover_label->sys_color_changed(); ...; Refresh(); }
void Sidebar::sys_color_changed() { ...; for (auto* ext : extruders) ext->sys_color_changed(); }
```
Cite: f7f0c82abb (`Plater.cpp`: `HoverLabel::sys_color_changed`, `ExtruderGroup::sys_color_changed`,
`Sidebar::sys_color_changed`).
- **Rule:** In a dialog's `on_sys_color_changed()` that derives colours through `darkModeColorFor`/`StateColor`,
call `wxGetApp().init_label_colours()` first.
**Why:** `darkModeColorFor` uses StateColor's flag, which only `init_label_colours()` refreshes (startup and
`MainFrame::on_sys_color_changed`). On macOS each NSWindow delivers the event from its own KVO with no defined
order, so a dialog handler can run before the main frame's and see the old flag. **[source; the ordering risk is
inferred, not observed]**
## Dark-mode icons
Icons are SVGs in `resources/images/` (a PNG of the same name is only a fallback, with no dark substitution), named
without extension, rasterized by Orca itself (wx has no SVG
support in Orca's build — see `references/dpi-bitmaps-fonts.md` for sizing, `ScalableBitmap` and `BitmapCache`
mechanics). Dark mode reaches icons in two regimes:
1. **Palette substitution.** `create_scaled_bitmap()` (`wxExtensions.cpp`) passes `wxGetApp().dark_mode()` to
`BitmapCache::load_svg`, which text-replaces palette colours in the SVG before nanosvg parses it. An SVG drawn
purely in substitution colours is dark-correct automatically.
2. **`*_dark` asset variants**, chosen by name in code, for everything else.
**Substitution table** (`BitmapCache::load_svg`; replacement by `BitmapCache::nsvgParseFromFileWithReplace`):
| Mode | Replacements |
|---|---|
| light | `"#00FF00"` → `"#52c7b8"`; unquoted `#949494` → `#7C8282` (icon line colour) |
| dark | `"#262E30"` → `"#EFEFF0"` and unquoted `#262E30` → `#EFEFF0`; `"#323A3D"` → `"#B3B3B5"`; `"#808080"` → `"#818183"`; `"#CECECE"` → `"#54545B"`; `"#6B6B6B"` → `"#818182"`; `"#909090"` → `"#FFFFFF"`; `"#00FF00"` → `"#FF0000"`; `"#009688"` → `"#00675b"`; `"#F1F1F1"` → `"#36363B"`; unquoted `#DBDBDB` → `#4A4A51` (border), `#F0F0F1` → `#333337` (disabled background) |
| dark, name contains `toggle_on` | additionally unquoted `#009688` → `#00675b` |
| `new_color` argument | replaces the `"#009688"` slot in both modes (in dark mode it overrides the `#00675b` mapping too) |
| name contains `printer_thumbnail` | no replacement at all |
| both modes | the key `"#0x00AE42"` is malformed (contains `0x`) and matches nothing real — dead |
Mechanics: literal, **case-sensitive** `boost::replace_all` over the raw file text, applied in `std::map` key order,
so all quoted keys (`"` = 0x22) run before unquoted ones (`#` = 0x23). Quoted keys match only a full attribute value
written `="#RRGGBB"` (`fill="#262E30"`, `stroke="…"`); they never match CSS `style="fill:#…"` or single quotes. Only the
unquoted keys (`#262E30`, `#DBDBDB`, `#F0F0F1` in dark; `#949494` in light) reach style attributes. The cache key
contains size, scale, `-dm`, `-gs` and the `new_color` string, so light and dark rasterizations are cached
separately. This SVG map is separate from, and not identical to, `gDarkColors`.
`create_scaled_bitmap` re-queries the mode on every call, with two exceptions: on Windows `menu_bitmap = true`
(`create_menu_bitmap`) uses `check_dark_mode()`; `bitmap2 = true` routes to `create_scaled_bitmap2` →
`BitmapCache::load_svg2`, which applies **no** palette substitution (only `#D9D9D9`/`fill-opacity` from
`array_new_color`). Re-rasterizing is therefore the theme hook: `ScalableBitmap::msw_rescale()` and
`ScalableButton::msw_rescale()` re-run `create_scaled_bitmap`, so the DPI path doubles as the theme path
(`Tab::sys_color_changed` calls `msw_rescale()` on every cached button/bitmap and rebuilds its `wxImageList`). The
walk reaches `ScalableButton`s (`ScalableButton::UpdateDarkUI` = `msw_rescale()`), but not `ScalableBitmap`
members — they are not windows; their owner calls `msw_rescale()` and re-`SetBitmap`s. `ScalableBitmap::msw_rescale()`
re-creates from name, size, grayscale and resize only: `new_color` and `bitmap2` are not re-applied.
**When a `*_dark` variant + explicit re-pick is required:** whenever the icon's colours are not in the substitution
table (multi-colour artwork, brand colours, off-palette greys like `#1F1F1F`), or the dark rendition is not a 1:1
colour mapping of the light one. The code chooses the `_light`/`_dark` name from `wxGetApp().dark_mode()` **in code
that re-runs on theme change**:
```cpp
// in the ctor AND in on_sys_color_changed():
m_icon = ScalableBitmap(this, wxGetApp().dark_mode() ? "icon_dark" : "icon", 20);
m_static_bmp->SetBitmap(m_icon.bmp());
```
The substitution still runs on whichever file is loaded, so variant files must use
off-palette colours or the `_dark` asset is recoloured a second time (a `#262E30` in a `_dark` file still becomes
`#EFEFF0`). Re-rasterizing a stored name (`ScalableBitmap::msw_rescale`) does **not** switch variants. Reference
pattern: `AmsHumidityLevelList` (`AmsMappingPopup.cpp`) preloads both variants as `ScalableBitmap`s and picks
`hum_level_img_dark`/`hum_level_img_light` by `dark_mode()` inside its render path, so a `Refresh()` re-picks.
Alternatively re-run the name selection inside `on_sys_color_changed()`/`msw_rescale()`.
**Menu bitmaps.** Menu items built with `append_menu_item(..., icon_name, ...)` use `create_menu_bitmap` (16 px,
no window, `menu_bitmap = true`), and the icon name is remembered per item id (not on GTK). On Windows the SVG substitution
then uses `check_dark_mode()` as its dark flag — wx's own answer,
which is what wx uses to draw menus (its owner-drawn menu path keys on `wxMSWDarkMode::IsActive()`), so the icon
matches the menu background even when Orca's mode differs from the Windows app mode. On a theme change Windows menu
icons are re-rasterized by `msw_rescale_menu` (a no-op elsewhere) from the stored icon names;
`MenuFactory::sys_color_changed()` does this for the cached context menus (the menu-bar overload is compiled out). Menus themselves are covered in
`references/popups-menus.md`.
**Pitfalls**
- **Rule:** Author single-tone SVG icons in the colours `load_svg` substitutes — for near-black line art use
`#262E30` (uppercase), not an arbitrary near-black like `#1F1F1F` or `#333333`.
**Why:** recolouring is a literal, case-sensitive string replacement on a fixed palette; an off-palette fill (or
lowercase `#262e30`) passes through untouched and the icon disappears against the dark background. The fix is a
colour change in the asset; no code change.
```xml
<path d="..." fill="#1F1F1F"/> <!-- Wrong: not substituted, invisible in dark mode -->
<path d="..." fill="#262E30"/> <!-- Right: → #EFEFF0 in dark mode -->
```
Cite: f658aad7ca (`resources/images/ams_drying.svg`); `BitmapCache::load_svg`, `BitmapCache::nsvgParseFromFileWithReplace`.
- **Rule:** When an icon exists as `*_light`/`*_dark` variants, select the variant matching the mode — `_dark` in
dark mode — and make the selection run inside code that re-executes on theme change, not once.
**Why:** the classic copy-paste bug returned the `_light` name in both branches, so dark mode showed
near-invisible light-theme humidity glyphs; nothing else corrects it because the variant files are authored
off-palette. A selection made only when the data changes keeps the old variant after a live switch until the data
changes again.
```cpp
// Wrong: light asset in dark mode
if (wxGetApp().dark_mode()) return "hum_level" + std::to_string(hum_level) + "_no_num_light";
else return "hum_level" + std::to_string(hum_level) + "_no_num_light";
// Right:
if (wxGetApp().dark_mode()) return "hum_level" + std::to_string(hum_level) + "_no_num_dark";
else return "hum_level" + std::to_string(hum_level) + "_no_num_light";
```
Cite: 668654da5f (`AMSDryControl.cpp` `get_humidity_level_img_path`); live-switch shape: `AmsHumidityLevelList`
(`AmsMappingPopup.cpp` `AmsHumidityLevelList::doRender`).
- **Rule:** Do not "fix" a Windows menu icon by passing `wxGetApp().dark_mode()`.
**Why:** `create_menu_bitmap` deliberately follows `check_dark_mode()` (wx's menu state = the Windows app mode),
not Orca's setting; the icon must match the background wx draws.
Cite: `wxExtensions.cpp` `create_scaled_bitmap`, `create_menu_bitmap`.
- **Rule:** Pass the real window to `create_scaled_bitmap`/`ScalableBitmap` and re-create bitmaps in
`on_sys_color_changed()` as well as `on_dpi_changed()`; a missing icon name throws `Slic3r::RuntimeError`.
Cite: `wxExtensions.cpp` `create_scaled_bitmap`.
@@ -0,0 +1,967 @@
# Standard controls, data views and Orca's widget event contracts
The value and event contracts of wx's standard controls (text, choice/combo, check/radio, spin, slider, gauge,
static text, hyperlink, static bitmap, book controls, splitter, collapsible pane, list/tree/grid), the
`wxDataViewCtrl` model/renderer machinery with its native-vs-generic limits, `wxVariant` and validators; then
which events Orca's replacement widgets emit and where to bind them, and the `ObjectList` / `ObjectGrid` designs.
Read it before writing or reviewing code that sets a control's value, reacts to its events, or touches a data view.
Contents: [Rules](#rules) · [Native vs generic](#native-vs-generic-and-silent-asserts) ·
[Programmatic changes](#events-from-programmatic-changes) · [wxTextCtrl](#wxtextctrl--wxtextentry) ·
[wxStaticText](#wxstatictext-labels-mnemonics-markup) · [Item containers](#item-containers) ·
[Check and radio](#check-boxes-and-radio-buttons) · [Spin, slider, gauge](#spin-controls-slider-gauge) ·
[Book controls](#book-controls) · [Splitter, pane, hyperlink, bitmap](#splitter-collapsible-pane-hyperlink-static-bitmap) ·
[List and tree](#wxlistctrl-wxtreectrl-image-lists) · [wxGrid](#wxgrid) · [Validators](#validators) ·
[DVC per port](#wxdataviewctrl-native-vs-generic) · [DVC model](#wxdataviewmodel-contract) ·
[DVC control API](#wxdataviewctrl-control-api) · [Renderers and editing](#custom-renderers-and-in-place-editing) ·
[wxVariant](#wxvariant-with-custom-objects) · [Orca widgets](#orca-replacement-widgets-event-contracts) ·
[ObjectList](#objectlist-objectdataviewmodel-extrarenderers) · [ObjectGrid](#objectgrid-gui_objecttable)
## Rules
1. Model→view refreshes use the quiet setters (`ChangeValue`, `ChangeSelection`). `wxTextEntry::SetValue`,
`wxBookCtrlBase::SetSelection`, `wxTreeCtrl::SelectItem` and macOS `wxDataViewCtrl::Select` *do* emit events.
→ [Programmatic changes](#events-from-programmatic-changes)
2. Never call `SetLabel`/`SetLabelText`/`GetLabel` on a `wxTextCtrl` to set or read its text; the setters are
silent no-ops in 3.3 and `GetLabel` returns the label, not the text. → [wxTextCtrl](#wxtextctrl--wxtextentry)
3. To consume Enter, handle `wxEVT_TEXT_ENTER` (needs `wxTE_PROCESS_ENTER`) and do not `Skip()`; skipping lets
Enter activate the dialog's default button. → [wxTextCtrl](#wxtextctrl--wxtextentry)
4. Call `OSXDisableAllSmartSubstitutions()` (wxOSX-only API, so inside `#ifdef __WXOSX__`) on every text control
that holds G-code, paths, URLs or code. → [wxTextCtrl](#wxtextctrl--wxtextentry)
5. Do not rely on `SetHint`/`SetMaxLength` on multi-line controls (hints: MSW and GTK2 only; max length: MSW and
GTK only). → [wxTextCtrl](#wxtextctrl--wxtextentry)
6. Show user data (preset, filament, file names) in labels and page titles with `SetLabelText` /
`wxControl::EscapeMnemonics`; quote it with `wxMarkupParser::Quote` inside markup.
→ [wxStaticText](#wxstatictext-labels-mnemonics-markup)
7. Typed `wxClientData` belongs to the control (never delete it); `::ComboBox` supports only untyped `void*`
data, which the caller owns. → [Item containers](#item-containers), [::ComboBox](#combobox)
8. A 3-state checkbox is read with `Get3StateValue()`; every radio group starts with `wxRB_GROUP`.
→ [Check and radio](#check-boxes-and-radio-buttons)
9. Read spin values after the commit event (`wxEVT_SPINCTRL`), not from `wxEVT_TEXT`.
→ [Spin](#spin-controls-slider-gauge)
10. Book pages are created with the book as parent; in `PAGE_CHANGED` use `event.GetSelection()`.
→ [Book controls](#book-controls)
11. Pixel-valued setters take `FromDIP(n)` (`SetMinimumPaneSize`, `SetRowHeight`, column widths), and are
re-applied on rescale. → [Splitter](#splitter-collapsible-pane-hyperlink-static-bitmap), [DVC API](#wxdataviewctrl-control-api)
12. Never dereference `begin()` of a wx selection range (`wxGrid::GetSelectedBlocks()`) without comparing it to
`end()`; never assume `wxDataViewCtrl::GetSelection()` is valid. → [wxGrid](#wxgrid)
13. A `wxGridCellChoiceEditor` subclass whose `m_control` is not a `wxComboBox` overrides `Reset()`,
`GetValue()` and `SetParameters()` too. → [wxGrid](#wxgrid)
14. Validators only filter keystrokes; parse and range-check values on commit. → [Validators](#validators)
15. Never `delete` a `wxDataViewModel`; keep exactly the references you mean to own. → [DVC model](#wxdataviewmodel-contract)
16. Change the model first, then notify (`ItemAdded`/`ItemDeleted`); after deletion use the pointer only as an
ID. Use `Cleared()` only when everything changed. → [DVC model](#wxdataviewmodel-contract)
17. `GetValue` fills the variant type the column's renderer expects; a mismatch shows nothing.
→ [DVC model](#wxdataviewmodel-contract)
18. With `wxDV_MULTIPLE` use `GetSelections()`/`HasSelection()`; `event.GetItem()` of `SELECTION_CHANGED` may be
invalid on GTK and macOS. → [DVC API](#wxdataviewctrl-control-api)
19. Bracket programmatic `Select`/`UnselectAll`/model mutations with a suppress flag that the
`SELECTION_CHANGED` handler checks. → [DVC API](#wxdataviewctrl-control-api)
20. Give a data view with custom renderers an explicit `SetRowHeight` sized for the tallest content; repeat it
after `SetFont` and in the rescale handler. → [DVC API](#wxdataviewctrl-control-api)
21. Never carry a `wxDataViewItem` across a deferred call without re-validating it against the model.
→ [DVC model](#wxdataviewmodel-contract), [ObjectList](#objectlist-objectdataviewmodel-extrarenderers)
22. Check `!v.IsNull() && v.GetType() == "<Class>"` before every `obj << variant`; `dynamic_cast` the editor in
`GetValueFromEditorCtrl`. → [wxVariant](#wxvariant-with-custom-objects), [Renderers](#custom-renderers-and-in-place-editing)
23. A compound in-place editor (TextInput, `::ComboBox`) commits by calling the renderer's `FinishEditing()` /
`CancelEditing()` itself. → [Renderers](#custom-renderers-and-in-place-editing)
24. On macOS, `CreateEditorCtrl` never runs natively: veto `START_EDITING`, `CallAfter` the renderer's own
`StartEditing`, then `SetCustomRendererPtr/Item`, all under `#ifdef __WXOSX__`.
→ [Renderers](#custom-renderers-and-in-place-editing)
25. Bind `::TextInput`/`::SpinInput` `wxEVT_TEXT_ENTER`/`wxEVT_KILL_FOCUS` on the widget itself (or its
`GetTextCtrl()`), never on a parent filtered by the widget's id. `::ComboBox` ignores the id passed to its
constructor, so bind on the combo (or `SetId()` after construction if an id filter is unavoidable).
→ [Orca widgets](#orca-replacement-widgets-event-contracts)
26. `::CheckBox`/`SwitchButton` emit `wxEVT_TOGGLEBUTTON`, and a handler bound on the widget itself must `Skip()`;
`RadioGroup::SetSelection` always emits; `SpinInput`'s `wxEVT_SPINCTRL` is a plain `wxCommandEvent`.
→ [Orca widgets](#orca-replacement-widgets-event-contracts)
27. Expect Orca's colours on every generic data view and `RenderText` on MSW: `ObjectList` installs a
process-global `wxRendererNative`. → [ObjectList](#objectlist-objectdataviewmodel-extrarenderers)
## Native vs generic, and silent asserts
Orca builds wx with `wxBUILD_DEBUG_LEVEL=0` and `libslic3r_gui` with `wxDEBUG_LEVEL=0`, so `wxASSERT`/`wxFAIL`
vanish and `wxCHECK_*` returns early without a message (`include/wx/debug.h:229-231, 340-368`). Every "asserts" in
the docs below means, in Orca, that the call silently does nothing (a `wxCHECK`) or carries on with bad state (a
`wxASSERT`). Linux builds target GTK3 (GTK2 is only the `-DDEP_WX_GTK3=OFF` opt-out); GTK2-only behaviour is noted
where it differs.
Which implementation a control uses decides most platform differences:
| Control | MSW | GTK | macOS | Cite |
|---|---|---|---|---|
| `wxDataViewCtrl` | generic | native `GtkTreeView` | native `NSOutlineView` | `include/wx/dataview.h:36-44` |
| `wxBitmapComboBox` | native (owner-drawn CB) | native | generic `wxOwnerDrawnComboBox` | `include/wx/bmpcbox.h:27-28,112-119` |
| `wxTreeCtrl` | native | generic | generic | `include/wx/treectrl.h:27-28` |
| `wxListCtrl` | native | generic | generic | `include/wx/listctrl.h:29-34` |
| `wxHyperlinkCtrl` | native | native | generic | `interface/wx/hyperlink.h:90` |
| `wxGrid` | generic | generic | generic | `src/generic/grid.cpp` |
## Events from programmatic changes
The general wx rule is that setters do not send events. The exceptions are the bugs:
| Call | Event emitted? | Cite |
|---|---|---|
| `wxTextEntry::SetValue(s)` | **yes**, one `wxEVT_TEXT`, even when `s` equals the current text | `interface/wx/textentry.h:539-542`; [source] `src/common/textentrycmn.cpp:236-254`, `src/msw/textctrl.cpp:1133-1145` |
| `wxTextEntry::ChangeValue(s)` | no | `interface/wx/textentry.h:177-178` |
| `wxTextEntry::Clear()` | yes (= `SetValue("")`) | `interface/wx/textentry.h:193-194` |
| `wxTextCtrl::SetLabel/SetLabelText` | nothing at all (3.3) | `docs/changes.txt:111-113`; `src/common/textcmn.cpp:934-937` |
| `SetSelection/SetStringSelection` on wxChoice, wxComboBox, wxListBox, wxRadioBox | no | `interface/wx/ctrlsub.h:102-103,126` |
| `wxComboBox::SetValue` | `wxEVT_TEXT` if editable; none with `wxCB_READONLY` | `interface/wx/combobox.h:256-272` |
| `wxComboBox::Popup()/Dismiss()` | DROPDOWN/CLOSEUP, except on wxOSX | `interface/wx/combobox.h:279-281,292-294` |
| `wxCheckBox::SetValue/Set3StateValue`, `wxRadioButton::SetValue`, `wxCheckListBox::Check` | no | `interface/wx/checkbox.h:170-180`, `interface/wx/radiobut.h:117-118`, `interface/wx/checklst.h:135-137` |
| `wxSpinCtrl[Double]::SetValue/SetRange` | no; `SetRange` may silently clamp the value | `interface/wx/spinctrl.h:186-197,220-231,432-441` |
| `wxBookCtrlBase::SetSelection` | **yes**, PAGE_CHANGING (vetoable) + PAGE_CHANGED | `interface/wx/bookctrl.h:175-183` |
| `wxBookCtrlBase::ChangeSelection` | no | `interface/wx/bookctrl.h:192-199` |
| `AddPage/InsertPage(select=true)` | yes, except for the very first page | `interface/wx/bookctrl.h:256-259` |
| `DeletePage/RemovePage` | yes if the selection shifts, except when deleting the last page | `interface/wx/bookctrl.h:286-295` |
| `wxTreeCtrl::SelectItem` | **yes**, SEL_CHANGING (vetoable) + SEL_CHANGED | `interface/wx/treectrl.h:866-868` |
| `wxDataViewCtrl::Select/SetSelections/UnselectAll` | generic: no; GTK: suppressed; **macOS: `SELECTION_CHANGED`** | [source] `src/generic/datavgen.cpp:6396-6412`; `src/gtk/dataview.cpp:5268-5282`; `src/osx/dataview_osx.cpp:687-738`, `src/osx/cocoa/dataview.mm:1824-1832` |
| `wxDataViewModel::ItemChanged/ValueChanged/ChangeValue` | `wxEVT_DATAVIEW_ITEM_VALUE_CHANGED` | `interface/wx/dataview.h:116-117,331,392` |
| generic `wxDataViewCtrl::Expand/Collapse` | EXPANDING/EXPANDED, COLLAPSING/COLLAPSED; `Collapse` can also send SELECTION_CHANGED when it hides selected rows | [source] `src/generic/datavgen.cpp:4110-4150` |
Orca's widgets add their own rows; see [Orca widgets](#orca-replacement-widgets-event-contracts).
Pitfalls:
- **Rule:** A model→view refresh must not loop back through the view's change handler.
**Why:** `SetValue` and `SetSelection` on a book fire synchronously inside the refresh; the handler writes the
value back, marks the preset dirty, or rebuilds the page it is running in. On macOS a programmatic DVC
`Select` re-enters the selection handler.
```cpp
// Wrong
text->SetValue(v); book->SetSelection(i); dvc->Select(item);
// Right
text->ChangeValue(v); book->ChangeSelection(i);
m_suppress = true; dvc->Select(item); m_suppress = false; // handler: if (m_suppress) return;
```
Cite: the table above; `ObjectList` uses `m_prevent_list_events` for the same reason.
## wxTextCtrl / wxTextEntry
Contract:
- `wxTE_PROCESS_ENTER` is required for `wxEVT_TEXT_ENTER` (`interface/wx/textctrl.h:1562-1564`). If no handler
exists, *or the handler calls `Skip()`*, Enter is processed internally or "used to activate the default button
of the dialog" (`interface/wx/textctrl.h:1324-1331`). `wxComboBox` has the same flag and semantics
(`interface/wx/combobox.h:42-48`); for `wxBitmapComboBox` it is documented as Windows-only
(`interface/wx/bmpcbox.h:29-33`).
- `wxTE_PROCESS_TAB` has no effect on single-line controls under wxGTK (`interface/wx/textctrl.h:1332-1338`).
- `wxEVT_TEXT` "is however not sent during the control creation" (`interface/wx/textctrl.h:1556-1560`). A
`SetValue` right after the constructor does send it, which is harmless only while nothing is bound.
- `IsModified()` is true only after user edits; `SetValue`/`ChangeValue` reset it (`interface/wx/textentry.h:170-172`,
`interface/wx/textctrl.h:1880-1886`). Use it on commit to skip writing back untouched values.
- Styles fixed at creation: `wxTE_READONLY`, `wxTE_PASSWORD` and the wrap styles can change later on GTK but not
on MSW; every other style except alignment is creation-time only (`interface/wx/textctrl.h:1398-1402`). Toggle
editability with `SetEditable(bool)`, not `SetWindowStyleFlag`.
- Multi-line positions are not string indices on `\r\n` platforms: use `GetRange()`, not
`GetValue().Mid(GetInsertionPoint())` (`interface/wx/textctrl.h:1405-1418`).
- `SetMaxLength(n)` works on single-line controls everywhere, on multi-line ones only in wxMSW and wxGTK; extra
input is discarded and `wxEVT_TEXT_MAXLEN` sent (`interface/wx/textentry.h:398-416`). It does not filter
`SetValue`.
- `SetHint()` is native on MSW, macOS and GTK ≥ 3.2. Without native support (GTK2) wx's fallback requires you to
`Skip()` focus and `wxEVT_TEXT` events and avoid `WriteText`/`Replace` while the control is empty. Hints are
ignored with `wxTE_PASSWORD`, and on multi-line controls they work only on MSW and GTK2 — so not on macOS or on
Orca's GTK3 Linux build (`interface/wx/textentry.h:467-486`).
- `wxTE_RICH` is MSW-only (prefer `wxTE_RICH2`); `wxTE_NOHIDESEL` is MSW-only (`interface/wx/textctrl.h:1347-1363`).
Platforms — macOS: quote and dash smart substitution are "enabled by default" (`interface/wx/textctrl.h:2114-2134`);
`OSXDisableAllSmartSubstitutions()` turns them off (`:2136-2144`). A single-line control also replaces pasted
newlines with spaces unless `OSXEnableNewLineReplacement(false)` (`:2094-2113`). These `OSX*` methods are
`@onlyfor{wxosx}` and declared only in `include/wx/osx/textctrl.h`, so calls must sit under `#ifdef __WXOSX__` or
the MSW and GTK builds fail to compile.
OrcaSlicer: interactive single-line text uses `::TextInput` ([below](#textinput)); raw `wxTextCtrl` remains for
multi-line text. Field's `TextCtrl::BUILD` calls `OSXDisableAllSmartSubstitutions()` under `#ifdef __WXOSX__`; copy
that for any G-code or path editor.
Pitfalls:
- **Rule:** Never use `SetLabel`, `SetLabelText` or `GetLabel` for a text control's content.
**Why:** `wxTextCtrlBase::SetLabel` is `wxFAIL_MSG("Use SetValue() or ChangeValue() instead.")` and nothing else
(`src/common/textcmn.cpp:934-937`); `SetLabelText` calls the virtual `SetLabel` (`include/wx/control.h:64-67`);
`GetLabel()` returns `m_labelOrig`, not the text (`include/wx/control.h:61`). The assert is compiled out, so the
control silently never updates, on every platform (3.3 made this consistent; MSW used to treat it as `SetValue`).
```cpp
ctrl->GetTextCtrl()->SetLabel(s); // Wrong: no-op
ctrl->GetTextCtrl()->ChangeValue(s); // Right (or SetValue if listeners must react)
```
Cite: `docs/changes.txt:111-113`.
- **Rule:** Handle `wxEVT_TEXT_ENTER` without `Skip()` unless you want the default button activated as well.
**Why:** a skipped Enter falls through to the dialog's default button and closes it.
```cpp
txt->Bind(wxEVT_TEXT_ENTER, [](wxCommandEvent& e) { commit(); e.Skip(); }); // Wrong in a dialog
txt->Bind(wxEVT_TEXT_ENTER, [](wxCommandEvent&) { commit(); }); // Right
```
Cite: `interface/wx/textctrl.h:1324-1331`.
- **Rule:** Disable macOS smart substitutions on code, path and URL editors.
**Why:** typed `"` becomes `”` and `--` becomes `—`, silently corrupting G-code macros and paths.
```cpp
auto* ed = new wxTextCtrl(this, wxID_ANY, gcode, wxDefaultPosition, sz, wxTE_MULTILINE); // Wrong alone
#ifdef __WXOSX__ // Right: add this
ed->OSXDisableAllSmartSubstitutions();
#endif
```
Cite: `interface/wx/textctrl.h:2114-2144`; `include/wx/osx/textctrl.h:154`; Field `TextCtrl::BUILD`.
- **Rule:** Don't put the only explanation of a multi-line field in `SetHint()`, and don't count on
`SetMaxLength()` there.
**Why:** multi-line hints are ignored on macOS and GTK3; multi-line max length is ignored on macOS.
Cite: `interface/wx/textentry.h:414-415, 485-486`.
## wxStaticText: labels, mnemonics, markup
Contract:
- `SetLabel` treats every `&` as a mnemonic marker; a literal ampersand must be `&&`. `SetLabelText()` shows text
verbatim, and `wxControl::EscapeMnemonics()` escapes it (`interface/wx/control.h:174-195, 379-387`). This applies
to `wxStaticText`, `wxCheckBox`, `wxRadioButton`, `wxButton` labels and to book page titles: all `wx*book`
classes interpret mnemonics in page text (`interface/wx/bookctrl.h:141-147`), and since 3.3 wxListbook and
wxChoicebook do too (`docs/changes.txt:128-130`).
- `SetLabelMarkup()` needs well-formed markup or the label "won't be shown at all" (returns false, keeps the old
label). A bare `&` is still a mnemonic. Multi-line markup works only on GTK and macOS; the generic version used on
MSW handles single lines (`interface/wx/control.h:338-347`). Quote untrusted text with
`wxMarkupParser::Quote()` (`include/wx/private/markupparser.h:137-141`); the header is private, but Orca's wx
install copies `include/wx/private` (`deps/wxWidgets/wxWidgets.cmake`, `copy_private_headers`).
- Without `wxST_NO_AUTORESIZE`, `SetLabel` resizes the control to its best size but never re-lays-out the parent;
call the parent's or sizer's `Layout()` after a size-changing label. `SetLabel` is a no-op when the text is
unchanged (`interface/wx/stattext.h:30-36, 107-117`; `src/common/stattextcmn.cpp:334-352`).
- Ellipsizing (`wxST_ELLIPSIZE_START/MIDDLE/END`): the best size is the full text extent on every port (GTK even
switches ellipsizing off to measure, `src/gtk/stattext.cpp:239-295`), so a label ellipsizes only when something
constrains its width — an explicit min/initial width, or `wxST_NO_AUTORESIZE` plus a sizer-assigned size.
- Wrapping (`Wrap(w)` caches its width in 3.3.2, so `SetLabel(new); Wrap(sameWidth)` leaves the new label
unwrapped — [source] `src/common/stattextcmn.cpp:259-264`; `wxST_WRAP`; Orca's `::Label`): see
`references/sizers-layout.md` §wxStaticText wrapping.
Pitfalls:
- **Rule:** User-provided names go through `SetLabelText` / `EscapeMnemonics`, and through
`wxMarkupParser::Quote` inside markup.
**Why:** an `&` in a preset or file name disappears or underlines the next character; a `<` makes the markup
invalid and the label shows nothing.
```cpp
new wxStaticText(p, wxID_ANY, preset_name); // Wrong
auto* st = new wxStaticText(p, wxID_ANY, ""); st->SetLabelText(preset_name); // Right
book->AddPage(pg, wxControl::EscapeMnemonics(filament_name)); // Right for page titles
lbl->SetLabelMarkup("<b>" + wxMarkupParser::Quote(name) + "</b>"); // Right for markup
```
Cite: `interface/wx/control.h:174-195, 338-347`. Escaping translated strings: `references/strings-i18n-files.md`.
## Item containers
`wxChoice`, `wxComboBox`, `wxBitmapComboBox`, `wxListBox`, `wxCheckListBox`, and Orca's `::ComboBox` derive from
`wxItemContainer`.
Contract:
- Client data: the control *owns* typed `wxClientData*` and deletes it in `Delete()`, `Clear()` and its destructor;
untyped `void*` is never touched. All items of one control use one kind, fixed by the first `Append(..., data)`
or `SetClient*` call (`interface/wx/ctrlsub.h:172-185, 466-480`). Asking for the other kind is a silent `wxCHECK`
returning `nullptr` (`src/common/ctrlsub.cpp:182-191, 221-230`); mixing kinds fails only a compiled-out
`wxASSERT` (`:213-214`).
- `SetStringSelection` matches case-insensitively and takes the first hit (`interface/wx/ctrlsub.h:128-131`); MSW
wxComboBox "doesn't behave correctly" with items differing only in case (`interface/wx/combobox.h:24-28`).
- While the dropdown is open only `GetCurrentSelection()` reflects the highlighted item
(`interface/wx/choice.h:148-161`, `interface/wx/combobox.h:200-208`). In a `wxEVT_COMBOBOX` handler `GetValue()`
already returns the new value (`interface/wx/combobox.h:53-56`).
- `wxComboBox::IsEmpty()` is ambiguous and does not compile; use `IsListEmpty()` or `IsTextEmpty()`
(`interface/wx/combobox.h:219-249`).
- With `wxCB_READONLY`, `SetValue(s)` requires `s` to be in the list (case-insensitive) (`interface/wx/combobox.h:264-267`).
- `wxComboBox` DROPDOWN/CLOSEUP exist on wxMSW, GTK ≥ 2.10 and wxOSX/Cocoa (`interface/wx/combobox.h:64-73`), but
`Popup()`/`Dismiss()` never send them on wxOSX (`:279-281`).
- `wxChoice::SetColumns` is GTK-only (`interface/wx/choice.h:164-172`). `wxLB_MULTIPLE` equals `wxLB_EXTENDED`
on GTK2 (`interface/wx/listbox.h:34`). In `wxEVT_CHECKLISTBOX`, `event.IsChecked()` is invalid; use `GetInt()`
with `wxCheckListBox::IsChecked(i)` (`interface/wx/checklst.h:18-23`).
Platforms — macOS: `wxBitmapComboBox` is a `wxOwnerDrawnComboBox`; its `Select()` uses `ChangeValue` and sends no
event (`src/generic/odcombo.cpp:1041-1060`), and all bitmaps must share one size (`interface/wx/bmpcbox.h:12-13`).
OrcaSlicer: `Slic3r::GUI::BitmapComboBox` (`BitmapComboBox.hpp`) wraps `wxBitmapComboBox` where a native bitmap
combo is still used (e.g. `apply_extruder_selector` in `wxExtensions.cpp`, `PrintHostDialogs`); on macOS it overrides `OnAddBitmap`/`OnDrawItem`
because the generic owner-drawn base would size items from Retina-scaled bitmaps. Settings fields use
`::ComboBox`, which has different client-data rules ([below](#combobox)).
Pitfalls:
- **Rule:** Never delete typed client data yourself; free `void*` data yourself (fetch the pointers before
`Clear()`/`Delete()`, which only drop them).
```cpp
combo->Append(name, new wxStringClientData(id)); // typed: the control owns and deletes it
delete combo->GetClientObject(i); // Wrong: double free on Delete()/Clear()/destruction
auto* d = static_cast<wxStringClientData*>(combo->GetClientData(i)); // Wrong kind: silent nullptr
auto* d = static_cast<wxStringClientData*>(combo->GetClientObject(i)); // Right; never delete d
```
Cite: `interface/wx/ctrlsub.h:172-185`; `src/common/ctrlsub.cpp:221-230`.
## Check boxes and radio buttons
Contract:
- 3-state checkboxes need `wxCHK_3STATE`; users reach the third state only with `wxCHK_ALLOW_3RD_STATE_FOR_USER`
(`interface/wx/checkbox.h:50-56`). Read them with `Get3StateValue()`. `IsChecked()` asserts on a 3-state box
(`include/wx/checkbox.h:57-62`), so in Orca it silently returns `GetValue()`; `Set3StateValue(wxCHK_UNDETERMINED)`
on a 2-state box is likewise only an assert (`interface/wx/checkbox.h:179-185`).
- Radio groups form from consecutive sibling buttons; `wxRB_GROUP` starts a new group and that button is the
initial selection (`interface/wx/radiobut.h:15-21`). `wxRB_SINGLE` takes a button out of any group, but only on
MSW and GTK (≥ 3.3.0); elsewhere such a button "can't be turned off" (`:31-39`). `SetValue(false)` on a grouped
button is invalid — select another. On MSW the focused radio button is always the selected one (`:120-130`).
- `wxRadioBox::SetSelection(n)` needs a valid `n`, never `wxNOT_FOUND` (`interface/wx/radiobox.h:292-295`), and
sends no event.
OrcaSlicer: interactive check boxes are `::CheckBox`/`SwitchButton`, radio rows are `::RadioGroup`; their event
contracts differ from these ([below](#checkbox-and-switchbutton)).
Pitfalls:
- **Rule:** Put `wxRB_GROUP` on the first button of *every* group.
**Why:** without it, adjacent groups merge into one and selecting in one clears the other.
Cite: `interface/wx/radiobut.h:15-21`.
## Spin controls, slider, gauge
Contract:
- Typed spin text "is not validated until the control loses focus"; it is then clamped and `wxEVT_SPINCTRL` is
sent only if the value differs from the last one sent. Raw typing produces `wxEVT_TEXT`
(`interface/wx/spinctrl.h:36-45`). `wxSpinCtrlDouble` sends `wxEVT_SPINCTRLDOUBLE` instead and also commits on
Enter (`:276-279`). Add `wxTE_PROCESS_ENTER` for `wxEVT_TEXT_ENTER` (`:18-22`).
- `wxEVT_SPINCTRL` is declared with the `wxSpinEvent` class (`include/wx/spinctrl.h:22`); `wxSpinEvent::GetValue()`
and `GetPosition()` both return `GetInt()` (`include/wx/spinbutt.h:104-107`).
- `wxSlider`: handle `wxEVT_SLIDER`. `wxEVT_SCROLL_CHANGED` exists on MSW only (`interface/wx/slider.h:35-38, 103`).
`wxSL_LEFT/TOP` work on Windows and GTK3 only; `wxSL_BOTH` and `wxSL_SELRANGE` on Windows only; tick marks
(`wxSL_AUTOTICKS`) need Windows or GTK ≥ 2.16 (`interface/wx/slider.h:32-66`).
- `wxGauge`: `Pulse()` switches to indeterminate mode until the next `SetValue()`; under wxMSW `SetRange` in
indeterminate mode controls the bounce span (`interface/wx/gauge.h:141-161`).
OrcaSlicer: integer spinners are `::SpinInput` ([below](#spininput)); progress bars are Orca's `ProgressBar`
(`references/orca-widgets.md`). Raw `wxSlider` remains in Field's `SliderCtrl`.
## Book controls
Contract:
- A page must be created with the book as its parent and added once; the book owns and deletes it
(`interface/wx/bookctrl.h:253-254, 273`). `RemovePage` detaches without deleting, and you then own it (`:324-330`).
- Removing the selected page selects the page before it (the new first page if it was first) through `SetSelection`,
so that page is shown and PAGE_CHANGING/CHANGED are sent; removing a page before the selection only shifts the
index. This is `wxBookCtrlBase::DoSetSelectionAfterRemoval` (`src/common/bookctrl.cpp:477-495` **[source]**),
called from `DoRemovePage` by `wxSimplebook`, `wxChoicebook`, `wxListbook`, `wxToolbook` and Orca's `Notebook`.
- `GetSelection()` inside a `PAGE_CHANGED` handler may return the old or the new page depending on the platform; use
`event.GetSelection()` (`interface/wx/bookctrl.h:160-166`).
- `wxSimplebook` has no UI; switch with `ChangeSelection()`. `SetSelection()` sends PAGE_CHANGING/CHANGED
(`interface/wx/simplebook.h:17-31`); `ShowNewPage()` adds and selects (`:130-138`).
- `wxNotebook`: `wxNB_LEFT/RIGHT/BOTTOM` are unsupported on themed MSW (`interface/wx/notebook.h:60-63`); the themed
MSW page background is disabled with `wxNB_NOPAGETHEME`, an explicit page `SetBackgroundColour`, or app-wide with
`wxSystemOptions::SetOption("msw.notebook.themed-background", 0)` (`:76-108`).
OrcaSlicer: three tab mechanisms, not interchangeable —
- `Notebook` (`Notebook.hpp`): a custom `wxBookCtrlBase` with a `ButtonsListCtrl` header, used for MainFrame's
Home/Prepare/Preview/Device tabs. Header clicks arrive internally as `wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED`; the book
itself emits `wxEVT_BOOKCTRL_PAGE_CHANGING/CHANGED` from `SetSelection` and nothing from `ChangeSelection`,
matching wx.
- `::TabCtrl` (`Widgets/TabCtrl`): a tab *bar* only; content switching is the caller's ([below](#radiogroup-tabctrl)).
- `wxSimplebook`: headerless page stacks (e.g. `StatusPanel`, `SelectMachine`, `ReleaseNote`).
Pitfalls:
- **Rule:** Sync code uses `ChangeSelection`; a `PAGE_CHANGED` handler reads `event.GetSelection()`.
```cpp
book->SetSelection(i); // Wrong in a sync routine whose PAGE_CHANGED handler rebuilds UI
book->ChangeSelection(i); // Right
int sel = book->GetSelection(); // Wrong inside PAGE_CHANGED
int sel = event.GetSelection(); // Right
auto* pg = new MyPage(dialog); book->AddPage(pg, t); // Wrong: page must be the book's child
auto* pg = new MyPage(book); book->AddPage(pg, t); // Right
```
Cite: `interface/wx/bookctrl.h:160-199, 253-254`.
## Splitter, collapsible pane, hyperlink, static bitmap
- `wxSplitterWindow`: the default minimum pane size is 0, so the user can drag a pane shut. `SetMinimumPaneSize()`
takes pixels — pass `FromDIP(n)` (`interface/wx/splitter.h:300-317`). `Unsplit()` only hides the removed pane
(`:452-466`). Sashes resize at idle time; `UpdateSize()` forces it before `Show` (`:468-479`).
- `wxCollapsiblePane`: put children on `GetPane()`, not on the pane control; re-layout on
`wxEVT_COLLAPSIBLEPANE_CHANGED`; the pane resizes its top-level window unless `wxCP_NO_TLW_RESIZE`
(`interface/wx/collpane.h:54-96`).
- `wxHyperlinkCtrl`: if the `wxEVT_HYPERLINK` handler `Skip()`s, or there is none, wx calls
`wxLaunchDefaultBrowser` (`interface/wx/hyperlink.h:55-58`). Orca's `Slic3r::GUI::HyperLink` and `::Label` with
`LB_HYPERLINK` are the styled alternatives (`references/orca-widgets.md`).
- `wxStaticBitmap`: native versions are meant for small icons and only the generic one supports every
`SetScaleMode`; use `wxGenericStaticBitmap` for large images. MSW centres a smaller bitmap, other ports draw it at
the origin (`interface/wx/statbmp.h:11-23, 146-152`). `SetBitmap` takes a `wxBitmapBundle` (`:132`); Orca builds
bundles from its own SVG rasteriser (`references/dpi-bitmaps-fonts.md`).
## wxListCtrl, wxTreeCtrl, image lists
- Virtual list (`wxLC_REPORT|wxLC_VIRTUAL`): call `SetItemCount()` and override `OnGetItemText` (optionally
`OnGetItemImage`/`OnGetItemAttr`) (`interface/wx/listctrl.h:128-134`). `EditLabel()` asserts without
`wxLC_EDIT_LABELS` (`docs/changes.txt:68-69`).
- Images: prefer `SetImages(std::vector<wxBitmapBundle>)`; `wxImageList` is discouraged
(`interface/wx/withimages.h:21-40`) and is measured in physical pixels in 3.3 (`docs/changes.txt:85-88`); calling
its methods on an invalid (unsized) list now asserts, i.e. fails silently in Orca (`docs/changes.txt:53-56`).
`Assign*` transfers ownership, `Set*ImageList` does not (`interface/wx/withimages.h:90-110`). Sizing:
`references/dpi-bitmaps-fonts.md`.
- `wxTreeCtrl`: `SelectItem` emits events ([table](#events-from-programmatic-changes)).
`Delete/DeleteChildren/DeleteAllItems` send `wxEVT_TREE_DELETE_ITEM` for every item; `DeleteChildren` does not
clear `SetItemHasChildren` (`interface/wx/treectrl.h:318-344`). The tree owns `wxTreeItemData`.
## wxGrid
Contract:
- `GetSelectedBlocks()` returns a range of unordered, possibly overlapping blocks (`interface/wx/grid.h:5090-5109`).
[source] It is empty when nothing is selected, and the grid cursor cell is not part of it
(`src/generic/grid.cpp:11295-11302` returns an empty `wxGridBlocks()` without a selection object, otherwise the
selection's blocks).
- `FreezeTo(row, col)` returns false (an assert, silent in Orca) for out-of-range values, merged cells or the native
header (`interface/wx/grid.h:5747-5772`); [source] also when rows/columns were reordered or drag-moving is enabled
(`src/generic/grid.cpp:5742-5750`). In 3.3 it freezes even when the grid is too small (`docs/changes.txt:48-51`).
- Editor contract: `EndEdit` must not modify the grid — it stores the value and returns true if it changed;
`ApplyEdit` writes it after `wxEVT_GRID_CELL_CHANGING` was not vetoed (`interface/wx/grid.h:610-638`). Editors,
renderers and attrs are ref-counted and the setters take ownership (`:1263-1305, 1739-1770, 2681-2701`).
- A custom table sends `wxGridTableMessage` via `ProcessTableMessage()` whenever rows or columns are added or
removed (`interface/wx/grid.h:1832-1840`). `AssignTable()` takes ownership and may be called once (`:3088-3108`).
- `wxGridCellChoiceEditor::Combo()` is a C-cast of `m_control` to `wxComboBox*`
(`include/wx/generic/grideditors.h:394`); `Reset()`, `GetValue()`, `EndEdit()` and `SetParameters()` use it
(`src/generic/grideditors.cpp:1538-1605`), and Esc in any editor calls `Reset()` (`:87-93`).
Pitfalls:
- **Rule:** Treat wx selection ranges as possibly empty: compare `begin()` with `end()` before dereferencing, and
fall back to the (row, col) that triggered the event.
**Why:** the docs never promised a non-empty range; with no selection 3.3 returns an empty range, and the
unchecked `begin()->GetLeftCol()` in `ObjectGrid` crashed on cell deselect after the 3.3 upgrade. The data-view
analogue: `wxDataViewCtrl::GetSelection()` is invalid with no selection *or* with more than one.
```cpp
auto left = grid->GetSelectedBlocks().begin()->GetLeftCol(); // Wrong
auto blocks = grid->GetSelectedBlocks(); // Right
auto it = blocks.begin();
int left = (it == blocks.end()) ? col : it->GetLeftCol();
```
Cite: 46e47cec0a (`GUI_ObjectTable.cpp`, `GridCellSupportEditor::DoActivate`).
- **Rule:** A `wxGridCellChoiceEditor` subclass that puts any other control (e.g. `::ComboBox`) in `m_control` must
also override `Reset()`, `GetValue()` and `SetParameters()` — or derive from `wxGridCellEditor` instead.
**Why:** shadowing the non-virtual `Combo()` does not change the base methods, which still C-cast `m_control` to
`wxComboBox*`; Esc calls `Reset()` on it.
Cite: `include/wx/generic/grideditors.h:394`, `src/generic/grideditors.cpp:87-93, 1561-1605`.
- **Rule:** Clamp `FreezeTo` arguments to `[0, GetNumberRows()]` / `[0, GetNumberCols()]` and check its result.
Cite: `src/generic/grid.cpp:5742-5750`.
## Validators
- `SetValidator` stores a `Clone()` (`interface/wx/window.h:3369-3371`). `wxTextValidator` filters `wxEVT_CHAR` and
pasted text (`src/common/valtext.cpp:60-61, 277-301`) but never `SetValue`. `wxFILTER_DIGITS` rejects `-`, `.` and
`+`; `wxFILTER_NUMERIC` allows `.`, signs and `e/E` but "is not the same behaviour of wxString::IsNumber()"
(`interface/wx/valtext.h:43-52`).
- Data transfer is automatic only for dialogs: `ShowModal()/Show()` → `InitDialog()` → `TransferDataToWindow()`;
the default `wxID_OK` handler runs `Validate() && TransferDataFromWindow()`. Panels must call `InitDialog()`
themselves (`docs/doxygen/overviews/validator.h:93-131`). A custom OK handler that calls `EndModal` skips
validation.
OrcaSlicer: validators are keystroke filters only — a `wxTextValidator` with `wxFILTER_DIGITS`, `wxFILTER_NUMERIC` or
`wxFILTER_INCLUDE_CHAR_LIST` set on `TextInput::GetTextCtrl()` (e.g. `Preferences.cpp`, `calib_dlg.cpp`) and inside
`SpinInput`. `TransferDataTo/FromWindow` is not used: values are read, parsed and range-checked explicitly on commit.
Keep that pattern.
Pitfalls:
- **Rule:** Pick the filter for the full value range, and still parse and range-check on commit.
```cpp
ctrl->GetTextCtrl()->SetValidator(wxTextValidator(wxFILTER_DIGITS)); // Wrong if negatives are valid
ctrl->GetTextCtrl()->SetValidator(wxTextValidator(wxFILTER_NUMERIC)); // Right, plus ToDouble + range check
```
Cite: `interface/wx/valtext.h:43-52`.
## wxDataViewCtrl: native vs generic
All cites `interface/wx/dataview.h`. MSW uses the generic implementation; wxGTK and wxOSX use native controls
(915-917, 1039-1041, 3953-3955). Orca's macOS wx has `wxUSE_NATIVE_DATAVIEWCTRL 1`, so `ObjectList`,
`DiffViewCtrl`, `ParamsViewCtrl` and the other subclasses are `NSOutlineView`s there.
| Feature | Generic (MSW) | GTK | macOS | Cite |
|---|---|---|---|---|
| Multi-column sort (`AllowMultiColumnSort()` reports it) | yes | no | no | 915-926, 1039-1041 |
| `wxDataViewVirtualListModel` truly virtual | yes | yes | no ("not supported by macOS") | 604-614 |
| Item attr `SetBackgroundColour` | since 2.9.4 | since 3.1.1 | since 3.1.4 | 714-720 |
| Item attr `SetStrikethrough` | yes | yes | ignored | 729-733 |
| Current item | may be unselected | may be unselected | always selected; `SetCurrentItem` selects in multi-selection | 1456-1458, 1655-1665 |
| `IsExpanded()` | correct | correct | may be true for leaves (documented bug) | 1597-1603 |
| `CreateEditorCtrl()` | called | called | **never called** | 2626-2629 |
| `wxDataViewRenderer::SetAlignment()` | yes | yes | ignored; aligns as the column header | 2079-2085 |
| `IsEditCancelled()` / veto `EDITING_DONE` | yes | documented as unavailable; [source] text commits (`src/gtk/dataview.cpp:2240-2246`) and custom-editor commits (`src/common/datavcmn.cpp:791-853`) do go through `DoHandleEditingDone`, so veto works | documented as unavailable; [source] native text commit reaches the model before `EDITING_DONE` | 3942-3958 |
| Icon+text markup (`EnableMarkup` on `wxDataViewIconTextRenderer`) | no | yes | no | 2215-2218 |
| `EnableDropTargets` | full | first format only | full | 1377-1385 |
| `SetDragFlags()` | honoured | ignored | ignored | 4012-4018 |
| `GetDropEffect()` | real | `wxDragNone` | `wxDragNone` | 4030-4037 |
| `GetProposedDropIndex()` from `ITEM_DROP_POSSIBLE` | yes | no | yes (all ports from `ITEM_DROP`) | 4052-4060 |
| Generic mouse events (`wxEVT_LEFT_DOWN`…) | yes (bind on `GetMainWindow()`) | "notably it doesn't work in wxGTK" | not all | 994-997 |
| Explorer theme (`wxSystemThemedControl`) | on by default since 3.1.0; `EnableSystemTheme(false)` disables it | — | — | 1000-1003 |
| `GetMainWindow()` ≠ the control | yes | no | no | 1505-1513 |
| `SetAlternateRowColour`, `SetHeaderAttr` | yes | no | no | 1636-1646, 1675-1689 |
| `SetRowHeight` (uniform rows, raise only) | yes | yes | yes (3.1.1+) | 1715-1733 |
| `GetCountPerPage()` | yes | needs ≥ 1 item | yes | 1746-1751 |
| `GetTopItem()` | yes | may be unimplemented | may be unimplemented | 1755-1761 |
| `wxEVT_DATAVIEW_COLUMN_REORDERED` | yes | not sent | yes | 3863-3866 |
| `wxDataViewColumn::SetWidth()` | immediate | applied "only slightly later" (`GetWidth()` returns the old width, 0 initially; widths set before showing apply when visible) | immediate | 2793-2797 |
Other per-port facts: editing starts on a slow double-click or a platform key — "F2 is typical on Windows, Space
and/or Enter is common elsewhere" (2604-2607, 1915-1918); a custom renderer's `StartDrag()` is "Not yet supported"
(2717-2719); `RenderText()` should be used inside `Render()` so text matches native renderers (2708-2714). Calling
`Collapse()` from an event handler was fixed in 3.3.1 (`docs/changes.txt:346`; the generic re-check is described
under [DVC control API](#wxdataviewctrl-control-api)).
OrcaSlicer: `ParamsViewCtrl` (`EditGCodeDialog`) and `DiffViewCtrl` (`UnsavedChangesDialog`) use
`wxDataViewIconTextRenderer::EnableMarkup` only under `#ifdef __linux__`, matching the GTK-only markup row; on MSW
and macOS they use Orca's `BitmapTextRenderer(use_markup = true)`, which handles markup itself. `ObjectList`
force-sets column widths on macOS because the column constructor's width is not applied on 4K/5K screens (see
`ObjectList::create_objects_ctrl`).
## wxDataViewModel contract
**You implement** `IsContainer`, `GetParent`, `GetChildren`, `GetValue` and `SetValue`
(`interface/wx/dataview.h:16-19, 383-385`).
**Ownership.** The model is a `wxRefCounter` and "cannot be deleted directly" (its destructor is protected,
`include/wx/dataview.h:291-294`). `AssociateModel` adds a reference (`interface/wx/dataview.h:1341-1345`;
`src/common/datavcmn.cpp:1248-1263`), and the control drops it in its destructor or when another model (or
`nullptr`) is associated. Either `DecRef()` once after associating, or hold it in `wxObjectDataPtr`
(`interface/wx/dataview.h:68-92`), or keep one deliberate owning reference and `DecRef()` it in your destructor.
Detaching with `AssociateModel(nullptr)` is safe only while you hold your own reference — with the
DecRef-after-associate pattern it destroys the model.
**`wxDataViewItem` is an opaque `void*`.** It must be unique and stable for the item's whole life; `nullptr` means
both "invalid" and "the invisible root" (`interface/wx/dataview.h:788-800`). The ports keep the pointer: GTK stores
it in `GtkTreeIter::user_data` (`src/gtk/dataview.cpp:1843-1845`), the generic control in its tree nodes, Cocoa in
its buffers. Once the node is deleted every copy dangles — copies captured in lambdas and "last selected" members
included — and a later allocation can reuse the address, so a liveness scan by pointer can be fooled.
**Notification ordering (all ports):**
```cpp
// add: insert into the model first, so GetChildren(parent) already returns it
parent_node->Append(node); model->ItemAdded(wxDataViewItem(parent_node), wxDataViewItem(node));
// delete: remove from the model first, then notify (pointer used only as an ID), then free
parent_node->Remove(node); model->ItemDeleted(wxDataViewItem(parent_node), wxDataViewItem(node)); delete node;
// IsContainer(parent) must already be correct after the removal
```
Why each port needs it [source]:
- Generic `ItemDeleted` scans its own nodes because the item "was already removed from the model by the time
ItemDeleted() is called", then asks `IsContainer(parent)` (`src/generic/datavgen.cpp:3260-3310`).
- GTK `ItemAdded` looks the item up in `GetChildren(parent)` and silently returns ("adding non-existent item?") if
it is not there (`src/gtk/dataview.cpp:3995-4010`); GTK `ItemDeleted` checks `IsContainer(parent)` (`:1885-1895`).
- macOS `ItemAdded/ItemDeleted` re-query the parent's children with `reloadItem:reloadChildren:`; for the root parent
they call `reloadData`, reloading the whole outline (`src/osx/cocoa/dataview.mm:2284-2300, 2393-2400`).
- Notifications for an item the control never realised (collapsed parent) are ignored on purpose
(`src/generic/datavgen.cpp:3127-3147`, `src/gtk/dataview.cpp:1830-1838`).
**`Cleared()`** means "everything changed", not "emptied" (`interface/wx/dataview.h:54-56, 141-151`). Every port
discards its tree: generic resets selection and the current row (`src/generic/datavgen.cpp:3404-3425`), GTK does
BeforeReset+AfterReset (`src/gtk/dataview.cpp:1998-2001`), macOS also scrolls to the top
(`src/osx/cocoa/dataview.mm:2384-2391`). Save and restore expansion and selection around it. Re-associating a model
on macOS likewise reloads the outline from scratch ([source] `wxCocoaDataViewControl::AssociateModel` replaces the
data source).
**ItemChanged vs ValueChanged.** Both end in `wxEVT_DATAVIEW_ITEM_VALUE_CHANGED` (`interface/wx/dataview.h:328-336,
388-393`). `ChangeValue()` is `SetValue()` + `ValueChanged()` (`:116-117, 376-381`). On macOS `ValueChanged` calls
`model->GetParent(item)` (`src/osx/dataview_osx.cpp:263-268`), so `GetParent` must work for any live item.
**GetValue/HasValue types.** `GetValue` must fill the type the renderer expects; on a mismatch "nothing will be
shown and a debug error message will be logged" (`interface/wx/dataview.h:258-267`) — concretely
`CheckedGetValue` nulls the value when `!IsCompatibleVariantType()` (`src/common/datavcmn.cpp:854-885`). Container
rows show only column 0 unless `HasContainerColumns()` returns true or `HasValue()` is overridden
(`interface/wx/dataview.h:271-314`).
Pitfalls:
- **Rule:** Mutate the model, then notify; never notify about a node you have already freed or not yet inserted.
**Why:** GTK silently drops an `ItemAdded` the model does not report; generic and GTK call `IsContainer(parent)`
during `ItemDeleted`; a freed node read during notification is a use-after-free.
```cpp
delete node; model->ItemDeleted(parent, wxDataViewItem(node)); // Wrong order (and any read is UAF)
model->ItemAdded(parent, wxDataViewItem(node)); parent_node->Append(node); // Wrong order
```
Cite: `src/gtk/dataview.cpp:3995-4010`, `src/generic/datavgen.cpp:3260-3310`.
- **Rule:** Use `ItemAdded/ItemDeleted/ItemChanged` for local changes, not `Cleared()`.
**Why:** `Cleared()` drops selection and expansion on every port and scrolls to the top on macOS.
- **Rule:** Moving a subtree is delete + re-add, and the re-add must announce every descendant again (see
`ObjectDataViewModel::AddAllChildren`).
**Why:** the control discarded the subtree with the deleted node; native GTK otherwise shows the moved node
childless ("just to add a deleted item is not enough on Linux").
## wxDataViewCtrl control API
Contract:
- `GetSelection()` returns an invalid item when nothing *or more than one item* is selected
(`interface/wx/dataview.h:1532-1540`; `src/common/datavcmn.cpp:1329-1337`). With `wxDV_MULTIPLE` use
`HasSelection()`/`GetSelections()`. GTK and macOS build `SELECTION_CHANGED` from `dv->GetSelection()`
(`src/gtk/dataview.cpp:4507-4516`, `src/osx/cocoa/dataview.mm:1824-1832`), so in multi-selection
`event.GetItem()` can be invalid there; generic passes a concrete row — the clicked one, or the first selected
row of a Shift range (`src/generic/datavgen.cpp:4822-4827`).
- `UnselectAll()` "only has effect if multiple selections are allowed" (`interface/wx/dataview.h:1709-1713`);
`SetSelections` silently ignores invalid items (`:1699`).
- [source] `Select()` and `EnsureVisible()` expand ancestors themselves on every port
(`src/generic/datavgen.cpp:6396-6398, 6503-6505`, `src/gtk/dataview.cpp:5272, 5335`,
`src/osx/dataview_osx.cpp:584-591, 687-694`). On GTK calling them before `AssociateModel` is a silent
`wxCHECK_RET` (`src/gtk/dataview.cpp:5270, 5332`).
- [source] Selection events differ: GTK suppresses them for programmatic selection
(`SelectionEventsSuppressor`, `src/gtk/dataview.cpp:5268-5322`) but deleting a selected row emits
`SELECTION_CHANGED` through GTK's "changed" signal (`:4507-4516`); macOS emits `SELECTION_CHANGED` for
`Select`/`SetSelections`/`UnselectAll` (`src/osx/cocoa/dataview.mm:2504-2540, 1824-1832`).
- `SetRowHeight(h)` works on generic, GTK and macOS, only for uniform rows, and only *raises* the height above the
renderers' minimum (`interface/wx/dataview.h:1715-1733`). [source] On macOS the floor is the font's line height
and renderer `GetSize()` is not consulted (`src/osx/cocoa/dataview.mm:2615-2628`); per-item row height is
unsupported (`:2630-2633`); and the control's `SetFont` resets the row height to that default (`:2645-2651`).
- `EditItem(item, col)` "doesn't do anything if the item or this column is not editable"
(`interface/wx/dataview.h:1361-1369`).
- On MSW bind mouse and motion events on `GetMainWindow()`; generic mouse events do not work on wxGTK
(`interface/wx/dataview.h:994-997, 1505-1513`). Use a custom renderer's `ActivateCell` instead (`:2593`).
- [source] The generic `Collapse` re-checks whether a `SELECTION_CHANGED` handler already collapsed the node
(`src/generic/datavgen.cpp:4119-4129`).
Pitfalls:
- **Rule:** With `wxDV_MULTIPLE`, never take `GetSelection()` or `event.GetItem()` as "the" selection.
```cpp
auto it = dvc->GetSelection(); if (it.IsOk()) apply(it); // Wrong: invalid once two are selected
wxDataViewItemArray sels; dvc->GetSelections(sels); // Right
auto* node = (Node*)event.GetItem().GetID(); if (node) ... // Right: tolerate a null item on GTK/macOS
```
Cite: `interface/wx/dataview.h:1532-1540`.
- **Rule:** Do not assume a native `wxDataViewCtrl` grows its rows to fit a custom renderer's `GetSize()` — set an
explicit `SetRowHeight` for the tallest custom content, on all platforms, after any `SetFont` on the control,
and again in the rescale handler with the new `em`.
**Why:** on macOS the native row is the font line height, so custom-drawn content (the filament colour badge)
overflows into adjacent rows; `SetRowHeight` can only raise it, a later `SetFont` resets it, and wx never
rescales a value passed to a setter, so after a DPI or theme change the badge stops fitting. Setting it on every
platform keeps spacing consistent (MSW is the generic control, Linux the native GTK one).
```cpp
// create_objects_ctrl(): SetRowHeight(2 * em + FromDIP(2));
// msw_rescale(): SetRowHeight(2 * em + FromDIP(2)); // repeat with the new em
```
Cite: d5638273c6 (`ObjectList::create_objects_ctrl`, `ObjectList::msw_rescale`). General rescale rule:
`references/dpi-bitmaps-fonts.md`.
- **Rule:** Before moving a selected item (delete + re-add), `Unselect` it and re-`Select` it afterwards.
**Why:** the control's selection is not reliably updated by `ItemDeleted`; `ObjectList::update_plate_values_for_items`
does this ("hotfix for wxDataViewCtrl selection not updated after wxDataViewModel::ItemDeleted()").
## Custom renderers and in-place editing
**Renderer contract.** Implement `Render(rect, dc, state)`, `GetSize()`, `SetValue(variant)` and `GetValue(variant)`;
call `RenderText()` inside `Render` (`interface/wx/dataview.h:2702-2718`). The constructor's `varianttype` is checked
against model values by `IsCompatibleVariantType()` (`:1965-1980, 2063-2076`). Editing needs `HasEditorCtrl()` →
true, `CreateEditorCtrl()` and `GetValueFromEditorCtrl()`.
**Editing flow** (common code: generic, GTK custom renderers, and any explicit `renderer->StartEditing()`):
1. `wxEVT_DATAVIEW_ITEM_START_EDITING` is sent; a veto aborts (`src/common/datavcmn.cpp:715-725`).
2. `CreateEditorCtrl(GetMainWindow(), rect, CheckedGetValue(...))` runs; the value is **null** after a type
mismatch or when `HasValue(item, col)` is false (`:854-885`). The returned control is stored as `m_editorCtrl`
(`:733`); returning `nullptr` cancels.
3. wx pushes a `wxDataViewEditorCtrlEvtHandler` onto *the returned control only* and focuses it — on native GTK on
idle (`:742-751`). That handler commits on Enter, cancels on Esc and commits on kill-focus unless focus moved to
a child of the editor (`src/common/datavcmn.cpp:1129-1198`). Key and focus events of a compound editor's children
never reach it.
4. `FinishEditing()` calls `GetValueFromEditorCtrl(m_editorCtrl, value)` (`:799`), then hides the editor and deletes
it **later** via `wxPendingDelete` (`:765-785`). `Validate()` runs after the editor is gone
(`interface/wx/dataview.h:2125-2135`). `wxEVT_DATAVIEW_ITEM_EDITING_DONE` follows; if allowed,
`model->ChangeValue()` raises `VALUE_CHANGED` (`src/common/datavcmn.cpp:783-853`).
**macOS (native).** `CreateEditorCtrl` "will be never called there" (`interface/wx/dataview.h:2626-2629`).
[source] `EditItem` → `StartEditor` → `[NSOutlineView editColumn:row:…]` starts native text editing of the cell
(`src/osx/dataview_osx.cpp:757-760`, `src/osx/cocoa/dataview.mm:2564-2566`); `START_EDITING` comes from
`textShouldBeginEditing:`, and vetoing it returns NO to native editing (`src/osx/cocoa/dataview.mm:1885-1904`).
`wxEVT_DATAVIEW_ITEM_EDITING_STARTED` follows from `textDidBeginEditing:` (`:1936`) and is not vetoable
(`wxDataViewRendererBase::NotifyEditingStarted` never checks `IsAllowed()`, `src/common/datavcmn.cpp:756-763`).
A custom cell's `objectValue` is a `wxCustomRendererObject` (`wxDataViewCustomRenderer::MacRender`,
`src/osx/cocoa/dataview.mm:2926-2929`), so the field shows its description `wxCustomRendererObject: 0x…`
(`src/osx/cocoa/dataview.mm:117-160, 1112-1123`). On commit `wxDataViewRenderer::OSXOnCellChanged` builds a
**"string"** variant and calls `model->ChangeValue()` directly (`src/osx/cocoa/dataview.mm:2766-2797`), before
`EDITING_DONE` is sent (`:1944-1955`) — the model's `SetValue` must type-check, and vetoing `EDITING_DONE` cannot
stop it. `SetCustomRendererPtr`/`SetCustomRendererItem` are undocumented wxOSX-only members
(`include/wx/osx/dataview.h:245-259`); `wxDataViewCtrl::FinishCustomItemEditing` reads them to close a custom
editor when native editing starts (`src/osx/dataview_osx.cpp:779-786`, called from `src/osx/cocoa/dataview.mm:1930`).
Pitfalls:
- **Rule:** In `GetValueFromEditorCtrl()`, `dynamic_cast` the editor to the expected type and return `false` on
mismatch; in `CreateEditorCtrl()`, check the incoming variant before extracting.
**Why:** wx only ever passes the control your `CreateEditorCtrl` returned, so the cast is defence in depth against
app-side bookkeeping confusion — cheap insurance where a wrong `static_cast` reads garbage and crashes on commit.
It matters most on macOS, where native editing never calls `CreateEditorCtrl` at all and Orca's editor exists only
because `start_filament_editor` calls `StartEditing` directly.
```cpp
auto* c = static_cast<::ComboBox*>(ctrl); // Wrong
auto* c = dynamic_cast<::ComboBox*>(ctrl); // Right
if (!c || c->GetSelection() < 0) return false;
```
Cite: c965b2a5b3 (`ExtraRenderers.cpp`, `BitmapChoiceRenderer::GetValueFromEditorCtrl`;
`BitmapTextRenderer::GetValueFromEditorCtrl` uses `wxDynamicCast`); `src/common/datavcmn.cpp:733, 799`.
- **Rule:** On macOS, do not open a custom-renderer column editor through `EditItem()` or let the native
start-editing path run: `Veto()` `wxEVT_DATAVIEW_ITEM_START_EDITING`, `CallAfter` a call to the renderer's own
`StartEditing(item, GetItemRect(item, column))`, then `SetCustomRendererPtr`/`SetCustomRendererItem`. Guard against
re-entry and an already-open editor (`renderer->GetEditorCtrl()`), re-validate the item inside the lambda, and keep
it all under `#ifdef __WXOSX__`.
**Why:** `EditItem()` on Cocoa enters native text editing of the `wxCustomRendererObject` instead of your editor,
glitching the cell and crashing on commit. The re-entry guard is needed because `StartEditing` itself sends
`START_EDITING` (`src/common/datavcmn.cpp:716-725`), which the same handler would otherwise veto.
```cpp
void ObjectList::OnStartEditing(wxDataViewEvent& event) {
#ifdef __WXOSX__
if (event.GetColumn() == colFilament) {
if (m_starting_filament_editor) return; // our own StartEditing re-entering
event.Veto();
CallAfter([this, item = event.GetItem()] {
if (!is_live_model_item(item)) return;
start_filament_editor(item); // StartEditing + SetCustomRendererPtr/Item
});
return;
}
#endif
```
Cite: c965b2a5b3 (`ObjectList::OnStartEditing`, `ObjectList::start_filament_editor`).
- **Rule:** A compound editor commits by calling the renderer's `FinishEditing()` (or `CancelEditing()`) from its
own commit event.
**Why:** wx's Enter/Esc/kill-focus handler sits only on the top editor window; a `::ComboBox` or TextInput editor's
inner controls never reach it, and kill-focus commit is unreliable on Linux.
```cpp
c_editor->Bind(wxEVT_COMBOBOX, [this](wxCommandEvent& e) { e.StopPropagation(); FinishEditing(); });
```
Cite: `BitmapChoiceRenderer::CreateEditorCtrl`; `src/common/datavcmn.cpp:742-751, 1129-1198`.
- **Rule:** Do not touch the editor after `FinishEditing()`/`CancelEditing()`, and do not rely on vetoing
`EDITING_DONE` outside MSW.
**Why:** the editor is queued on `wxPendingDelete` and dies at the next idle or yield; on macOS the value is
already in the model when `EDITING_DONE` arrives.
## wxVariant with custom objects
`wxIMPLEMENT_VARIANT_OBJECT(T)` (and the older `IMPLEMENT_VARIANT_OBJECT`) generates `T& operator<<(T&, const
wxVariant&)`, which only `wxASSERT`s the type and then C-casts `GetData()` (`include/wx/variant.h:525-532`). The
custom type name is the wxObject class name (`GetType()` → `GetClassInfo()->GetClassName()`, `:515-518`); a null
variant's type is `"null"` (`interface/wx/variant.h:420-425`). wx's own `wxBitmap << variant` extraction is generated
the same way.
Pitfalls:
- **Rule:** Check `!v.IsNull() && v.GetType() == "<Class>"` before every `obj << v` extraction.
**Why:** the assert is compiled out of Orca in every configuration, so a wrong-typed variant is read as garbage and
a null one dereferences `nullptr`. Wrong types really happen: `CheckedGetValue` passes a null variant to
`CreateEditorCtrl` after a model/renderer type mismatch, and native macOS editing hands the model's `SetValue` a
`"string"` variant built from the field text (`src/osx/cocoa/dataview.mm:2766-2797`).
```cpp
DataViewBitmapText data; data << variant; // Wrong: blind extraction
if (variant.IsNull() || variant.GetType() != wxT("DataViewBitmapText")) // Right
return false;
DataViewBitmapText data; data << variant;
```
Cite: c965b2a5b3 (`ExtraRenderers.hpp`, `ObjectDataViewModelNode::SetValue`).
Note: `wxObject`'s default copy and assignment share ref-data with correct refcounting
(`include/wx/object.h:324-338`); `DataViewBitmapText`'s field-copying operators are harmless but not required.
## Orca replacement widgets: event contracts
Orca's interactive controls replace raw wx ones (catalog, constructors and quirks: `references/orca-widgets.md`).
They emit the native event *types* through `GetEventHandler()->ProcessEvent`, so command events propagate to parents
like native ones and stop at dialogs (`wxWS_EX_BLOCK_EVENTS`) and, on MSW and macOS, at popups (not on wxGTK,
`references/events.md` §5) — but ids, event objects, event classes and setter side effects differ. "Ancestor" in the
Bind column means an ancestor bound by event type; binding an ancestor by the widget's id is discouraged
(`references/events.md` §10):
| Widget | Replaces | Emits (user action) | Id / object | Setter side effects | Bind |
|---|---|---|---|---|---|
| `::TextInput` | `wxTextCtrl` | inner `wxEVT_TEXT` (propagates); `wxEVT_TEXT_ENTER`, `wxEVT_KILL_FOCUS` re-sent to the wrapper only | TEXT: inner ctrl's id/object; ENTER/KILL_FOCUS: wrapper's id, inner object | `GetTextCtrl()->SetValue` → `wxEVT_TEXT` | TEXT: wrapper or `GetTextCtrl()`; ENTER/KILL_FOCUS: wrapper or `GetTextCtrl()`, never a parent |
| `::ComboBox` | `wxComboBox`/`wxChoice` | `wxEVT_COMBOBOX` (int = index, string = text); DROPDOWN/CLOSEUP | COMBOBOX: combo's auto-generated id (the ctor `id` is ignored) and object; DROPDOWN/CLOSEUP: id 0, no object | `SetSelection`/`SetValue` → `wxEVT_TEXT` when the text ctrl is shown or `CB_NO_TEXT`; `SelectAndNotify` → `wxEVT_COMBOBOX` | COMBOBOX on the combo (or an ancestor); DROPDOWN/CLOSEUP on the combo without id |
| `::SpinInput` | `wxSpinCtrl` | `wxEVT_SPINCTRL` (a `wxCommandEvent`); `EVT_SPINCTRL_TEXT` per parsable keystroke; inner `wxEVT_TEXT` | SPINCTRL / SPINCTRL_TEXT: spinner id/object | `SetValue(int)` → `EVT_SPINCTRL_TEXT` + `wxEVT_TEXT`, no `wxEVT_SPINCTRL` | SPINCTRL on the spinner or an ancestor, handler takes `wxCommandEvent&` |
| `::CheckBox`, `SwitchButton` | `wxCheckBox` | `wxEVT_TOGGLEBUTTON` | native | `SetValue` silent | on the widget (with `Skip()`) or an ancestor; never `wxEVT_CHECKBOX` |
| `::RadioGroup` | `wxRadioBox` | `wxEVT_COMMAND_RADIOBOX_SELECTED` | group id, no object | **every** `SetSelection` emits | on the group or an ancestor |
| `::TabCtrl` | tab bar | `wxEVT_TAB_SEL_CHANGING` then `wxEVT_TAB_SEL_CHANGED` (plain `wxCommandEvent`) | tab ctrl id/object; CHANGING carries the old index | `SelectItem(i)` emits both when `i` changes | on the ctrl or an ancestor; cannot veto |
| `Button` | `wxButton` | `wxEVT_BUTTON` | button id/object | — | on the button or an ancestor |
### TextInput
`::TextInput` (`Widgets/TextInput.cpp`, `TextInput::Create`; ctor `TextInput(parent, text, label = "", icon = "",
pos, size, style)`) is a `StaticBox` frame around a real `wxTextCtrl` (on MSW the `TextCtrl` subclass in
`Widgets/TextCtrl.h`). Style flags pass through to the inner control (alignment flags are stripped); tooltips forward.
- It always ORs in `wxTE_PROCESS_ENTER`, and its `wxEVT_TEXT_ENTER` handler does not `Skip()`: Enter in a TextInput
never activates a dialog's default button and never propagates to parents.
- The inner control's `wxEVT_TEXT_ENTER` and `wxEVT_KILL_FOCUS` are re-dispatched with the wrapper's id via
`ProcessEventLocally`, which runs the wrapper's own handlers but [source] never `TryAfter`, so never the parents
(`src/common/event.cpp:1582-1589`; the doc at `interface/wx/event.h:626-650` says otherwise).
- `wxEVT_TEXT` is a command event from the inner control and propagates normally, carrying the inner control's id
and object. Key, char and focus-in events exist only on `GetTextCtrl()`.
- The value API lives on `GetTextCtrl()`. `TextInput::SetLabel()` sets the painted side label, not the text.
- A handler bound later on `GetTextCtrl()` for ENTER or KILL_FOCUS runs before the wrapper's internal one
(dynamic handlers run most-recently-bound first, `docs/doxygen/overviews/eventhandling.h:475-482`) and must
`Skip()` so `OnEdit()` and the re-dispatch still run.
- `TextInput` has no id parameter: its `StaticBox` is created with `wxID_ANY`, so the wrapper id seen by
ENTER/KILL_FOCUS handlers is auto-generated.
### ComboBox
`::ComboBox` (`Widgets/ComboBox.cpp`) is `wxWindowWithItems<TextInput, wxItemContainer>` with an owned `DropDown`
popup; ctor `ComboBox(parent, id, value = "", pos, size, n, choices[], style)`. The `id` argument is ignored:
the constructor calls `TextInput::Create`, which creates the window with `wxID_ANY`, so `GetId()` is auto-generated
and a parent bound with the id you passed never fires. `wxCB_READONLY` hides the text control and paints the value;
`CB_NO_DROP_ICON`/`CB_NO_TEXT` are Orca style flags.
- `SetSelection(n)` sends no `wxEVT_COMBOBOX` (matching wx) and returns early when `n` is already selected. When the
inner text control is shown (editable) or `CB_NO_TEXT` is set, it — and `SetValue` — go through
`GetTextCtrl()->SetValue()`, which sends a propagating `wxEVT_TEXT`. `SelectAndNotify(n)` selects and sends
`wxEVT_COMBOBOX`.
- `SetLabel`/`GetLabel` are the displayed value; `SetTextLabel` writes the wrapper's painted label directly.
- Only untyped `void*` client data is supported: every `Append` overload calls
`SetClientDataType(wxClientData_Void)`, and the caller owns and frees the data. `DeleteOneItem()` skips the base
class's client-object reset.
- `Clear()`, `Insert()`, `Set()` and `Delete()` (via `DoClear`/`DoInsertItems`/`DoDeleteOneItem` →
`DropDown::Invalidate(true)`) reset the selection to -1 without clearing the shown text; call
`SetSelection`/`SetValue` afterwards.
- Mouse-wheel selection is disabled. `GetDropDown()` exposes the popup (e.g. `SetUseContentWidth(true)`).
### SpinInput
`::SpinInput` (`Widgets/SpinInput.cpp`; ctor `SpinInput(parent, text, label = "", pos, size, style, min = 0,
max = 100, initial = 0, step = 1)`) is integer-only: an inner `TextCtrl` with `wxTextValidator(wxFILTER_DIGITS)` plus
two arrow `Button`s with key-repeat; `SetValue/GetValue/SetRange/SetStep`.
- It commits — clamps, then sends `wxEVT_SPINCTRL` — on Enter, kill-focus and arrow keys only if the value changed,
but on every arrow-button press and key-repeat tick even when the value is pinned at `min`/`max`
(`SpinInput::createButton`, `onTimer`); mouse-wheel stepping is disabled (`EVT_MOUSEWHEEL` is commented out of
the event table, so `mouseWheelMoved` never runs). `EVT_SPINCTRL_TEXT` (int + string) is the live
per-keystroke event.
- `wxEVT_SPINCTRL` is built as a `wxCommandEvent` with no `SetInt`: bind with a `wxCommandEvent&` handler and read
`GetValue()` from the spinner; a `wxSpinEvent&` handler would read `GetPosition()` == 0 from a mis-typed object.
- `SetValue(int)` clamps and sends no `wxEVT_SPINCTRL`, but sends `EVT_SPINCTRL_TEXT` and a propagating `wxEVT_TEXT`
(it uses the inner `SetValue`). `SetRange` does not re-clamp the current value — set the range before the value.
- Typing accepts digits only; `-` cannot be typed, so negative ranges need another control.
- Enter and kill-focus are re-dispatched locally, as in TextInput. Dialogs sometimes `Disable()` a SpinInput to
force a commit before reading; that depends on the platform delivering `wxEVT_KILL_FOCUS` to the focused inner
control when it is disabled, which wx does not promise — read the inner text and commit explicitly when it matters.
### CheckBox and SwitchButton
`::CheckBox` (ctor `CheckBox(parent, id = wxID_ANY)`, no label — pair it with a `wxStaticText`/`Label`, as
`CloneDialog` does) and `SwitchButton` are `wxBitmapToggleButton`s. They emit `wxEVT_TOGGLEBUTTON`, never
`wxEVT_CHECKBOX`; `SetValue` sends nothing. The half state is drawn only (`SetHalfChecked`/`IsHalfChecked`); any click
clears it via the widget's own `wxEVT_TOGGLEBUTTON` handler (bound in the constructor), which also swaps the on/off
bitmap. Handlers bound later on the same widget run first, so they must `Skip()`, or the bitmap keeps showing the old
state and the half state is never cleared (`PreferencesDialog::create_item_bambu_cloud` marks this with "let
CheckBox::update() refresh the bitmap"). Inside `Slic3r::GUI` write `::CheckBox` — `Field.hpp` declares a `Slic3r::GUI::CheckBox` field class.
### RadioGroup, TabCtrl
- `::RadioGroup::SetSelection(i)` **always** sends `wxEVT_COMMAND_RADIOBOX_SELECTED` — for programmatic calls and
for an unchanged index too — the opposite of `wxRadioBox`. Guard the handler or set a flag around sync code.
- `::TabCtrl::SelectItem(i)` sends `wxEVT_TAB_SEL_CHANGING` (cannot be vetoed; `sendTabCtrlEvent` always returns true)
then `wxEVT_TAB_SEL_CHANGED`; switching content is the caller's job. `TabCtrl::AssignImageList` is Orca's own
method, not `wxWithImages`. `SelectItem` also sends a synthetic `wxEVT_CHECKBOX` (id 0, object = the tab `Button`)
to the old and new tab buttons to toggle their `StateHandler` Checked state; the state handler `Skip()`s it, so it
propagates to the TabCtrl's ancestors — an ancestor bound to `wxEVT_CHECKBOX` without an id filter receives it.
### Field widgets
Settings fields (`Field.cpp`, built by `OptionsGroup::build_field`) wrap these widgets — `TextCtrl` → `::TextInput`
(a raw multi-line `wxTextCtrl` when `opt.multiline`), `CheckBox` → `::CheckBox`, `SpinCtrl` → `::SpinInput`,
`Choice` and `PrinterAgentChoice` → `::ComboBox` (`choice_ctrl`; a `Choice` is editable with `wxTE_PROCESS_ENTER`
for open-enum GUI types without a dynamic list, `wxCB_READONLY` otherwise), `ColourPicker` → `wxColourPickerCtrl`,
`PointCtrl` → two `::TextInput`, `StaticText` → `wxStaticText(wxST_ELLIPSIZE_MIDDLE)`, `SliderCtrl` → `wxSlider` +
`wxTextCtrl`, `PluginConfigField` → `Button`. Programmatic field updates use the
`m_disable_change_event` bracket around `SetValue`, which works because wx delivers `wxEVT_TEXT` synchronously. Field
machinery, pooling and the bind-with-id rule: `references/orca-settings-ui.md`.
Pitfalls:
- **Rule:** Bind TextInput/SpinInput ENTER and KILL_FOCUS on the widget (or its `GetTextCtrl()`), never on a parent;
bind `::ComboBox` events on the combo, never by the id passed to its constructor.
```cpp
dialog->Bind(wxEVT_TEXT_ENTER, &Dlg::on_enter, this, input->GetId()); // Wrong: never fires
input->Bind(wxEVT_TEXT_ENTER, &Dlg::on_enter, this); // Right
panel->Bind(wxEVT_TEXT, h, input->GetId()); // Wrong: TEXT carries the inner id
input->Bind(wxEVT_TEXT, h); // Right
auto* c = new ::ComboBox(this, ID_MODE); Bind(wxEVT_COMBOBOX, h, ID_MODE); // Wrong: ID_MODE is ignored
c->Bind(wxEVT_COMBOBOX, h); // Right
```
Cite: `TextInput::Create`; `ComboBox::ComboBox`; `src/common/event.cpp:1582-1589`.
- **Rule:** Bind `::CheckBox`/`SwitchButton` with `wxEVT_TOGGLEBUTTON` (and `Skip()` when bound on the widget itself),
and `SpinInput` with a `wxCommandEvent&` handler.
```cpp
cb->Bind(wxEVT_CHECKBOX, h); // Wrong: never sent
cb->Bind(wxEVT_TOGGLEBUTTON, [](wxCommandEvent& e) { apply(); }); // Wrong: stale bitmap
cb->Bind(wxEVT_TOGGLEBUTTON, [](wxCommandEvent& e) { apply(); e.Skip(); }); // Right
spin->Bind(wxEVT_SPINCTRL, [](wxSpinEvent& e) { use(e.GetPosition()); }); // Wrong
spin->Bind(wxEVT_SPINCTRL, [spin](wxCommandEvent&) { use(spin->GetValue()); }); // Right
```
Cite: `CheckBox::CheckBox`, `SwitchButton::SwitchButton`; `SpinInput::sendSpinEvent`.
- **Rule:** Treat an editable `::ComboBox`'s `SetSelection`/`SetValue` as emitting `wxEVT_TEXT`, and a
`RadioGroup::SetSelection` as emitting its selection event.
**Why:** both fire synchronously inside model→view refreshes and re-enter change handlers.
## ObjectList, ObjectDataViewModel, ExtraRenderers
`ObjectList` (`GUI_ObjectList.cpp/.hpp`) is the sidebar's `wxDataViewCtrl` (`wxDV_MULTIPLE | wxNO_BORDER |
wxDV_NO_HEADER`) over `ObjectDataViewModel` (`ObjectDataViewModel.cpp/.hpp`), a custom `wxDataViewModel` of
`ObjectDataViewModelNode`s typed by the `ItemType` bitmask (`itPlate, itObject, itVolume, itInstanceRoot, itInstance,
itSettings, itLayerRoot, itLayer, itInfo`) with columns `ColumnNumber` (`colName, colHeight, colPrint, colFilament,
colSupportPaint, colColorPaint, colSinking, colEditing`). Per-object, per-part and per-layer overrides appear as an
`itSettings` child; selecting it opens the model-scope tabs (`TabPrintModel` → `TabPrintPlate/Object/Part/Layer`),
which reuse the `OptionsGroup`/`Field` machinery bound to the items' `ModelConfig`s instead of a preset config. Which
options are offered: `references/orca-settings-ui.md`.
**Model design.**
- Ownership: `ObjectList::create_objects_ctrl` does `new ObjectDataViewModel; AssociateModel(m_objects_model);`
without an immediate `DecRef`, and `ObjectList::~ObjectList` calls `m_objects_model->DecRef()` — ObjectList owns one
reference for its lifetime. That is what makes the macOS bulk-update pattern safe: `add_objects_to_list` and
`update_plate_values_for_items` detach with `AssociateModel(nullptr)` and reattach afterwards, so the outline
reloads once [source] instead of once per notification.
- Each `ObjectDataViewModelNode*` *is* its `wxDataViewItem` ID; children live in the parent's pointer array.
`ObjectDataViewModel::Delete` removes the node from its parent (or from `m_plates`/`m_objects`), calls
`ItemDeleted(parent, item)`, then deletes it.
- `HasContainerColumns()` returns true so container rows draw their icon columns; `IsContainer(invalid)` is true for
the root.
- Re-parenting (`ReparentObject`, `ReorganizeChildren`, `ReorganizeObjects`) is remove → `ItemDeleted` → insert →
`ItemAdded`; `ReorganizeChildren` and `ReorganizeObjects` then call `AddAllChildren`, which re-announces the subtree
and expands the moved node. `ReparentObject` (plate change) does not; `ObjectList::update_plate_values_for_items`
re-expands and re-selects the item itself.
- `GetColumnType` returns `"DataViewBitmapText"` for `colName`/`colFilament`, but wx 3.3 never calls it (deprecated,
`include/wx/dataview.h:285-289`); what wx checks is the renderer's `varianttype`, which both ExtraRenderers set to
`"DataViewBitmapText"`. `ObjectDataViewModelNode::SetValue` type-checks `"DataViewBitmapText"` for those columns.
**Renderers** (`ExtraRenderers.cpp/.hpp`; `ENABLE_NONCUSTOM_DATA_VIEW_RENDERING` is 0, so both are
`wxDataViewCustomRenderer`s):
- `BitmapTextRenderer` (name column): editor is a `wxTextCtrl` with `wxTE_PROCESS_ENTER`; editing is gated by
`set_can_create_editor_ctrl_function`; `GetValueFromEditorCtrl` refuses names with illegal filename characters
and `ObjectList::OnEditingDone` reports `WasCanceled()` through a deferred warning.
- `BitmapChoiceRenderer` (filament column): editor is a `::ComboBox` (`wxCB_READONLY | CB_NO_DROP_ICON |
CB_NO_TEXT`) filled from `get_extruder_color_icons()`. It force-opens the popup on focus — on GTK deferred with
`CallAfter` and an `IsShownOnScreen()` check, because the editor "may receive focus before its native window is
mapped" (popup parenting: `references/popups-menus.md`) — and calls `FinishEditing()` itself on `wxEVT_COMBOBOX`.
**macOS editing.** The filament editor is opened by the veto + `CallAfter` + `start_filament_editor` pattern
([above](#custom-renderers-and-in-place-editing)); `wxEVT_DATAVIEW_ITEM_ACTIVATED` on `colFilament` calls
`start_filament_editor` on macOS and `EditItem` elsewhere. While starting, `m_filament_editor_item` tells the
renderer's callbacks which item is being edited (selection may differ). The bitmap columns are created
`wxDATAVIEW_CELL_EDITABLE` on macOS only, so a click starts native editing, and `ObjectList::OnEditingStarted`
(non-MSW branch) treats the resulting `EDITING_STARTED` as a per-cell click and runs the column's action
(printable toggle, paint gizmos, sinking, settings reset). Its `event.Veto()` there has no effect — only
`START_EDITING` is vetoable ([above](#custom-renderers-and-in-place-editing)); stopping native editing needs a veto in
`OnStartEditing`, as the filament column does.
**Selection and events.**
- `m_prevent_list_events` brackets programmatic `Select`/`UnselectAll`/model mutation; the `SELECTION_CHANGED`
handler returns early on macOS when it is set, and `ObjectList::selection_changed` checks it on every port —
covering GTK's selection events from drag-and-drop and row deletion.
- With Shift held the handler recovers the last-clicked item from `GetSelections()`, because the event item is not
reliable in multi-selection; a null `event.GetItem()` is tolerated.
- `is_live_model_item` (macOS only) re-validates a deferred item by scanning `GetAllChildren` for its pointer. It
cannot detect a freed node whose address was reused; prefer re-resolving from object/volume indices when you can.
**Row height and fonts.** `SetRowHeight(2 * em + FromDIP(2))` in `create_objects_ctrl` and in `msw_rescale`
(d5638273c6). `ObjectList::ObjectList` calls `SetFont(Label::sysFont(13))` on every platform, before
`create_objects_ctrl` — necessary on macOS because the control's `SetFont` resets the row height. The macOS-only
"don't `SetFont`" guard lives in `DPIAware`'s constructor and concerns only a top-level window's default font
(`references/dpi-bitmaps-fonts.md`).
**Global renderer on MSW.** `ObjectList::ObjectList` calls `wxRendererNative::Set(new wxRenderer)` — a
`wxDelegateRendererNative` overriding `DrawItemSelectionRect`, `DrawFocusRect`, `DrawTreeItemButton` and
`DrawItemText` with Orca colours (through `StateColor::darkModeColorFor`). `Set` replaces "the global renderer"
(`interface/wx/renderer.h:651-657`): once `ObjectList` exists, every generic `wxDataViewCtrl` on MSW
(`src/generic/datavgen.cpp:2735-2974`) and every `RenderText` call (`src/common/datavcmn.cpp:1102`) draws with them.
`DrawItemText` draws at the rect's top-left without alignment or ellipsis. Expect this when a new MSW data view looks
"wrong". Dark styling of data views (`UpdateDVCDarkUI`): `references/colours-dark-mode.md`.
Pitfalls:
- **Rule:** Every deferred lambda that holds a `wxDataViewItem` re-validates it before use.
**Why:** the model can be rebuilt between queueing and execution; the item is a raw node pointer.
```cpp
CallAfter([this, item] { start_filament_editor(item); }); // Wrong
CallAfter([this, item] { if (!is_live_model_item(item)) return; // Right (or re-resolve by index)
start_filament_editor(item); });
```
Cite: c965b2a5b3 (`ObjectList::is_live_model_item`). Liveness rules for deferred calls: `references/events.md`
§CallAfter.
## ObjectGrid (GUI_ObjectTable)
`ObjectTableDialog` (a `DPIDialog`, opened by `Plater::PopupObjectTable`) hosts `ObjectGrid` (a `wxGrid`) with
`ObjectGridTable` (a `wxGridTableBase` set with `AssignTable`).
- Per-cell editors and renderers are installed with `SetCellEditor`/`SetCellRenderer`, which take ownership.
- `GridCellFilamentsEditor` and `GridCellChoiceEditor` derive from `wxGridCellChoiceEditor` but create a
`::ComboBox` as `m_control` and shadow `Combo()`; they override `BeginEdit`/`EndEdit`. Any change to them, or a new
editor of this shape, must account for the base `Reset()`/`GetValue()`/`SetParameters()` cast
([wxGrid](#wxgrid)).
- `GridCellSupportEditor::DoActivate`, the copy path in `ObjectGrid::OnKeyDown` and `ObjectGrid::paste_data` handle
an empty `GetSelectedBlocks()` (46e47cec0a).
@@ -0,0 +1,958 @@
# DPI, bitmaps and fonts
How wx 3.3.2 maps DIP, logical and physical pixels on each platform, how DPI changes reach a window,
and how OrcaSlicer sizes layout (`FromDIP`, `em_unit`), rescales (`DPIAware`), rasterizes icons
(`BitmapCache`, `create_scaled_bitmap`, `ScalableBitmap`) and chooses fonts (`Label` table). Read it
for any fixed size, icon, bitmap, image list, font, or `on_dpi_changed` work, and when a bug looks
like "too small / too big / blurry / clipped on another monitor".
Contents: [Rules](#rules) · [Pixel kinds per platform](#pixel-kinds-per-platform) ·
[FromDIP / ToDIP / FromPhys](#fromdip--todip--fromphys--tophys) ·
[Scale factors and GetDPI](#scale-factors-and-getdpi) ·
[Choosing a size unit](#choosing-a-size-unit-fromdip-em_unit-text-metrics) ·
[wxEVT_DPI_CHANGED](#wxevt_dpi_changed) · [DPIAware rescale path](#dpiaware-rescale-path-orca) ·
[wxBitmap](#wxbitmap-physical-size-scale-factor-logical-size) ·
[wxBitmapBundle](#wxbitmapbundle-and-its-limits-in-orcas-build) ·
[Orca icon pipeline](#orca-icon-pipeline) ·
[Image lists, art provider, wxImage](#image-lists-art-provider-wximage) ·
[Window icons](#window-icons) · [Displays](#displays-and-ppi) · [wxFont](#wxfont) ·
[Orca fonts](#orca-fonts-label-table-sysfont-initsysfont)
## Rules
1. Hard-coded layout pixel values (sizes, min sizes, borders, gaps, spacers) go through
`FromDIP(n)` (or `n * em_unit(this)` in em-based code), never raw ints. §Choosing a size unit
2. Do not `FromDIP` values that are not logical pixels: `wxBitmap` ctor sizes and `GetSize()`,
`wxImageList` sizes and `wxBitmapBundle::GetBitmap(size)` are physical; bundle default sizes
and `wxArtProvider::GetBitmapBundle` sizes are DIP. §wxBitmap, §wxBitmapBundle
3. On Linux, widths that must hold text come from text metrics, best sizes or `em_unit`, not
from a fixed `FromDIP` width. §Choosing a size unit
4. Call `FromDIP` on a created window, or on `parent` in base-ctor arguments; when the window
may be null use the static `wxWindow::FromDIP(x, win)`, never `win->FromDIP(x)` (fd80ded5a8).
§FromDIP
5. Pick bitmap resolution with `GetDPIScaleFactor()`; never with `GetContentScaleFactor()`
(always 1 on MSW) or `GetDPI().x / 96.0` (72-based on macOS). §Scale factors
6. Every top-level window is a `DPIDialog`/`DPIFrame` whose `on_dpi_changed` re-rasterizes named
bitmaps and re-sets them on controls, calls each Orca widget's `Rescale()`, re-applies every
size stored from `em_unit`/`FromDIP`, then re-establishes the minimum and fits. An empty
override is acceptable only for a trivial dialog with no bitmaps and no stored sizes.
§DPIAware rescale path
7. Every `wxEVT_DPI_CHANGED` handler you bind — on a child, a control, or on the TLW from a
component — calls `Skip()`; only DPIAware's own TLW handler deliberately does not.
§wxEVT_DPI_CHANGED
8. In a DPIAware window, never rely on wx's MSW top-level auto-resize; size the window in
`on_dpi_changed`. §wxEVT_DPI_CHANGED, §DPIAware rescale path
9. `DPIAware::scale_factor()` is the display scale only on MSW (Orca's `get_dpi_for_window` is a
96 stub elsewhere); use `GetDPIScaleFactor()`/`FromDIP` for anything else. §DPIAware
10. On GTK `em_unit` is measured from the font, with the same formula in the ctor and the
rescale path (40eab797c6). §DPIAware
11. Icons are SVG resource names (no path, no extension) passed to `create_scaled_bitmap`,
`ScalableBitmap` or `Button`, with the real window and an explicit size;
`wxBitmapBundle::FromSVG*` does not exist in Orca's wx. §Orca icon pipeline
12. Layout that depends on a bitmap uses its logical size (`ScalableBitmap::GetBmpSize()`,
`wxBitmap::GetLogicalSize()`), not `GetSize()`/`GetWidth()`. §wxBitmap
13. Owner-drawn offscreen bitmaps use `CreateWithDIPSize(sz, GetDPIScaleFactor())` or
`CreateWithLogicalSize(GetClientSize(), GetDPIScaleFactor())`, not `wxBitmap(FromDIP(sz))`.
§wxBitmap
14. Draw a bundle with `GetBitmapFor(win)`, never `GetBitmap(GetDefaultSize())`; bundle size
queries take a created, non-null window. §wxBitmapBundle
15. `wxImageList` sizes are physical and must equal the added bitmaps' sizes; prefer
`SetImages()` with bundles. §Image lists
16. Fonts come from `Label::Head_*`/`Label::Body_*`; never literal point sizes. §Orca fonts
17. After `wxFont::SetFaceName` check `IsOk()` and fall back. §wxFont
18. Private fonts are registered only by `Label::initSysFont()`, early in
`GUI_App::on_init_inner`; never `AddPrivateFont` on macOS. §Orca fonts
19. On MSW a `wxMemoryDC` sizes text for its bitmap's scale factor: give the bitmap the window's
`GetDPIScaleFactor()` before selecting it; any `Label` font then renders correctly. §wxFont
20. Never index `wxDisplay` with an unchecked `GetFromWindow()` result. §Displays
## Pixel kinds per platform
Contract (`docs/doxygen/overviews/high_dpi.md:139-149`): "Under MSW, logical pixels are always
the same as physical pixels, but are different from DIPs, while under all the other platforms
with DPI scaling support (currently only GTK 3 and macOS), logical pixels are the same as DIP, but
different from physical pixels." Conversions: DIP↔logical with `FromDIP/ToDIP`, physical↔logical
with `FromPhys/ToPhys`, DIP↔physical by multiplying/dividing by `GetDPIScaleFactor()`.
| | MSW | macOS | GTK3 (Linux default; X11 and Wayland) | GTK2 (opt-out build) |
|---|---|---|---|---|
| logical (all window/DC API) | = physical | = DIP (points) | = DIP | = physical |
| `FromDIP(x)` | x·DPI/96, rounded | identity | identity | identity [source] |
| `GetContentScaleFactor()` | always 1 | backing scale (1 or 2) | integer GDK scale | 1 |
| `GetDPIScaleFactor()` | DPI/96 (1.25, 1.5, 1.75…) | = content scale | = content scale | 1 |
| `GetDPI()` | per window, 96-based | 72 × scale | 96 × scale | 96 |
| bitmap scale factor | stored; drives bundle selection and `wxMemoryDC` text size; never changes drawn size [source] | stored; drawn size = physical / scale | stored; drawn size = physical / scale | not stored |
| `wxEVT_DPI_CHANGED` | PMv2 manifest + Win10 1703 | on backing-scale change [source] | GTK ≥ 3.10, wx ≥ 3.3.0 | never |
| app-level HiDPI | per-monitor v2 manifest | `NSHighResolutionCapable`, `NSPrincipalClass` | automatic; fractional scales rounded to an integer | only global `GDK_SCALE`/`GDK_DPI_SCALE` |
Platforms:
- **MSW.** Orca ships its own manifest, `src/dev-utils/platform/msw/OrcaSlicer.manifest.in`:
`<dpiAware>true/pm</dpiAware>` and `<dpiAwareness>permonitorv2,permonitor</dpiAwareness>`. That
is the per-monitor v2 awareness wx needs to send DPI events (`interface/wx/event.h:3585-3590`).
On Windows versions that only honour the `permonitor` (v1) fallback, wx's
`IsPerMonitorDPIAware()` accepts only PMv2, so no DPI handling runs there [source]
`src/msw/nonownedwnd.cpp:IsPerMonitorDPIAware, wxNonOwnedWindow::HandleDPIChange`.
- **macOS.** `src/dev-utils/platform/osx/Info.plist.in` (the template `src/CMakeLists.txt`
configures) sets `NSPrincipalClass=NSApplication` (the key `docs/doxygen/overviews/high_dpi.md:339-341` requires) and
`NSHighResolutionCapable=true`. Its standard PPI is 72, not 96 (`include/wx/display.h`
`wxDisplay::GetStdPPIValue`; documented `interface/wx/display.h:196-211`).
- **GTK3** (the Linux build: `option(DEP_WX_GTK3 … ON)` in `deps/CMakeLists.txt`, Flatpak too).
"wxGTK only supports integer scaling factors currently and fractional scales are rounded to
the closest integer" (`docs/doxygen/overviews/high_dpi.md:348-351`). A Wayland compositor's fractional scale therefore
reaches wx as an integer GDK scale.
- **GTK2** (only with `-DDEP_WX_GTK3=OFF`). `wxHAS_DPI_INDEPENDENT_PIXELS` is defined only for
`__WXGTK3__ || __WXMAC__ || __WXQT__` (`include/wx/features.h:115-120`), so GTK2 takes the
"real conversion" branch of `FromDIP`. But the GTK2 `wxDisplayImplGTK` does not override
`GetScaleFactor()` (only under `GTK_CHECK_VERSION(3,10,0)`, `src/gtk/display.cpp`), and the base
`GetPPI()` is `GetStdPPI()*GetScaleFactor()` = 96 (`include/wx/private/display.h:99-103`). Net
effect [source]: `FromDIP` is the identity on GTK2 too, and GTK2 HiDPI exists only through the
global env vars (`docs/doxygen/overviews/high_dpi.md:353-355`). Code guarded for GTK must still compile there.
Exceptions to "every API takes logical pixels" (`docs/doxygen/overviews/high_dpi.md:169-183`): sizes passed to `wxBitmap`
constructors and returned by `GetWidth/GetHeight/GetSize` are **physical**;
`wxBitmapBundle::GetPreferredBitmapSizeFor()` is physical (`GetPreferredLogicalSizeFor()` is the
logical twin); the bundle **default size** (`FromSVG` argument, `GetDefaultSize()`) is **DIP**.
`wxGLCanvas` drawing is also physical (see `references/webview-gl-aui-media.md`).
## FromDIP / ToDIP / FromPhys / ToPhys
**Contract** (`interface/wx/window.h:1089-1121`): "A DPI-independent pixel is just a pixel at the
standard 96 DPI resolution … this scaling may be already done by the underlying toolkit (GTK+,
Cocoa, ...) automatically. This method performs the conversion only if it is not already done by
the lower level toolkit." It "is only needed when using hard coded pixel values. It is not
necessary if the sizes are already based on the DPI-independent units such as dialog units or if
you are relying on the controls automatic best size determination and using sizers". A component
equal to `-1` is returned unchanged, so `wxSize(FromDIP(490), -1)` keeps "unspecified"
(`interface/wx/window.h:1113-1116`). `ToDIP` is the inverse; the doc's use case is persisting window geometry
DPI-independently (`interface/wx/window.h:1166-1189`).
**Static overloads** `FromDIP(sz|pt|d, const wxWindow* w)` (`interface/wx/window.h:1141-1163`) accept
`w == nullptr`, but are "discouraged as passing NULL will prevent your application from correctly
supporting monitors with different resolutions". [source] With null on MSW the DPI comes from
`wxDisplay().GetPPI()`, the primary display (`src/common/wincmn.cpp` `GetDPIHelper`); on
macOS/GTK the conversion is the identity regardless.
**FromPhys/ToPhys** (`interface/wx/window.h:1233-1321`): physical↔logical; "does nothing under MSW, but divides
the input value by the content scale factor under the other platforms", rounding to the closest
integer ("15 physical pixels are translated to 8"). The static form with a null window uses "the
content scale factor of the main screen if supported" (`interface/wx/window.h:1270-1282`); [source] that is
macOS only, 1 elsewhere (`src/common/wincmn.cpp` `GetContentScaleFactorFor`). Use them for genuinely physical quantities only (bitmap pixel
sizes, GL viewports), never for layout constants.
**Platforms** [source]:
- MSW rounds per call (`wxMulDivInt32`, `include/wx/private/rescale.h`), so
`FromDIP(a) + FromDIP(b)` can differ from `FromDIP(a + b)` by 1 px at 125 %/175 %. Convert the
sum when two values must line up.
- MSW `GetDPI()` on a window without an HWND (two-step creation, or `this` inside a base-class
argument) falls back to the top-level parent's HWND, else to the screen DC — the primary
monitor (`src/msw/window.cpp` `wxWindowMSW::GetDPI`; the "possibly wrong DPI" log is compiled
out in Orca).
- The `interface/wx/window.h:1101-1106` example `wxBitmap bmp(FromDIP(32, 32))` contradicts the physical-bitmap
rule (`docs/doxygen/overviews/high_dpi.md:173-178`) and gives a 1x bitmap on macOS/GTK3; use the `wxBitmap` creation helpers instead
(§wxBitmap).
**OrcaSlicer.** `FromDIP(n)` is the convention for every fixed size in new code. Shared
macros build on the member form and expand only inside a `wxWindow` member function:
`ICON_SINGLE_SIZE`/`ICON_SIZE` (`GUI_Utils.hpp`, `FromDIP(16)`; their comment says not to change
them and to define new sizes locally) and `MSG_DIALOG_BUTTON_SIZE` (`MsgDialog.hpp`).
`create_scaled_bitmap` uses the static form because its `win` argument may be null.
**Pitfalls**
- **Rule:** Never call a member function through a `wxWindow*` that is allowed to be null; use
the static null-safe overload.
**Why:** `win->FromDIP()` on null is UB; clang assumes `this != nullptr` and deletes later
`win ? … : …` checks, turning the fallback into a call through a null vtable. This crashed
LLVM/clang-cl builds at startup while MSVC survived by luck. The static overload falls back to
the primary-display DPI on MSW and is the identity on macOS/GTK.
```cpp
unsigned h = win->FromDIP(px_cnt); // Wrong: UB when win == nullptr
unsigned h = wxWindow::FromDIP(px_cnt, win); // Right: static, null-safe
```
Cite: fd80ded5a8 (`src/slic3r/GUI/wxExtensions.cpp` `create_scaled_bitmap`).
- **Rule:** In base-class constructor arguments convert through the parent, not `this`.
**Why:** `this` is not yet a constructed window there; on MSW (and GTK2) the member form calls
the virtual `GetDPI()` on it, which is UB, and even a constructed window without an HWND
reports the primary monitor's DPI. On macOS/GTK3 the member form is the identity, so the bug
shows only on Windows.
```cpp
MyPanel(wxWindow* p) : wxPanel(p, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(300), -1)) {} // Wrong
MyPanel(wxWindow* p) : wxPanel(p, wxID_ANY, wxDefaultPosition, wxSize(p->FromDIP(300), -1)) {} // Right
```
Cite: [source] `src/msw/window.cpp` `wxWindowMSW::GetDPI`.
## Scale factors and GetDPI
**Contract.**
- `GetDPIScaleFactor()` (`interface/wx/window.h:1605-1626`): "1 for standard DPI screens or 2 for '200%
scaling' and, unlike for GetContentScaleFactor(), is the same under all platforms. This factor
should be used to increase the size of icons and similar windows whose best size is not based
on text metrics … should *not* be used for window sizes expressed in pixels, as they are
already scaled by this factor by the underlying toolkit under some platforms. Use FromDIP() for
anything window-related instead." It answers "how many physical pixels per DIP", i.e. which
raster resolution to produce.
- `GetContentScaleFactor()` (`interface/wx/window.h:1576-1603`): "the factor mapping logical pixels of this
window to physical pixels"; on platforms without pixel mapping (MSW) it "always returns 1.0".
Note in the doc: it equalled `GetDPIScaleFactor()` in wx 3.1.0–3.1.3 only. Use it for physical
buffers (GL, `FromPhys`), not to choose icon sizes.
- `GetDPI()` (`interface/wx/window.h:2285-2295`): per window, can differ between windows on Windows 10;
`wxSize(0,0)` if unavailable. On macOS it is 72-based: `wxWindowMac::GetDPI()` is
`MakeDPIFromScaleFactor(GetDPIScaleFactor())` = 72 × scale [source] `src/osx/window_osx.cpp`.
**Platforms** [source]: macOS `GetContentScaleFactor()` is the `NSWindow`'s
`backingScaleFactor`, or the main screen's when the view has no window yet
(`src/osx/cocoa/window.mm` `wxWidgetCocoaImpl::GetContentScaleFactor`). GTK returns
`gtk_widget_get_scale_factor` (an integer; 1 on GTK2), and `GetDPIScaleFactor()` is the same value
(`src/gtk/window.cpp` `wxWindowGTK::GetContentScaleFactor/GetDPIScaleFactor`).
**Pitfalls**
- **Rule:** Derive a scale with `GetDPIScaleFactor()` or `wxDPIChangedEvent::Scale*`, never by
dividing a DPI by 96.
**Why:** macOS's standard PPI is 72 (`interface/wx/display.h:203-205`), so `GetDPI().x / 96.0`
is 1.5 on a 2x Retina screen and 0.75 on a 1x one.
```cpp
double s = GetDPI().x / 96.0; // Wrong on macOS
double s = GetDPIScaleFactor(); // Right
```
- **Rule:** Choose icon/raster resolution from `GetDPIScaleFactor()`, not
`GetContentScaleFactor()`.
**Why:** content scale is always 1 on MSW (`interface/wx/window.h:1587-1592`), so icons never grow there.
## Choosing a size unit: FromDIP, em_unit, text metrics
Two scaling currencies coexist in Orca, plus the text metrics wx recommends:
| Unit | Use for | Value per platform |
|---|---|---|
| `FromDIP(n)` | fixed sizes in new code: icon sizes, borders, gaps, control heights, min sizes not driven by text | MSW n·DPI/96; macOS/GTK identity |
| `em_unit` (`em_unit(this)`, `wxGetApp().em_unit()`, `DPIAware::em_unit()`) | the settings code (`Tab`, `OptionsGroup`, `Field`, `ObjectList` columns) and any size that must follow text on Linux; sizes written as multiples, `wxSize(65 * em, 30 * em)` | MSW `max(10, 10 × scale_factor)`; macOS always 10; GTK width of "m" in the window font − 1 (min 10) |
| text metrics (`GetTextExtent`, best sizes, `ConvertDialogToPixels`) | widths that hold translated text | follow the font everywhere |
The overview prefers text metrics or dialog units over pixel values and calls `FromDIP` "the
simplest change" (`docs/doxygen/overviews/high_dpi.md:71-78`).
**Platforms.** On GTK, text follows the font DPI and the desktop text-scaling factor (Xft DPI,
`GDK_DPI_SCALE`) while `FromDIP` stays the identity at GDK scale 1, so a fixed `FromDIP` width
that fits a label on MSW/macOS can clip it on Linux. That is why `DPIAware` measures `em_unit`
from the font on GTK (§DPIAware rescale path).
**OrcaSlicer.** `em_unit(wxWindow*)` (`wxExtensions.cpp`) walks to the top-level parent
(`find_toplevel_parent`) and returns that `DPIDialog`'s or `DPIFrame`'s own `em_unit()`, else
`wxGetApp().em_unit()`; the per-window value matters when windows sit on monitors with different
DPI. `wxGetApp().em_unit()` is 10 until `GUI_App::update_fonts` copies the main frame's value
(called from `MainFrame::on_dpi_changed` and at main-frame setup).
**Pitfalls**
- **Rule:** Size boxes that contain text from the text, not from a fixed `FromDIP` width.
```cpp
label->SetMinSize(wxSize(FromDIP(120), -1)); // Wrong: clips on GTK text scaling
label->SetMinSize(wxSize(label->GetTextExtent(text).x + FromDIP(8), -1)); // Right (or N * em, or -1 + sizer)
```
Cite: `docs/doxygen/overviews/high_dpi.md:71-78`; [source] `DPIAware::update_em_unit` comment (`GUI_Utils.hpp`).
## wxEVT_DPI_CHANGED
**Contract** (`interface/wx/event.h:3560-3593`): sent "to each wxTopLevelWindow affected by the
change, and all its children recursively (post-order traversal)" — on a move to a monitor with a
different DPI or a system DPI change. "You should almost always call event.Skip() … as many
controls rely on processing this event in order to update their appearance". The TLW's default
handler "only sets the new window size, by scaling the current size by the DPI ratio … and also
ensuring that the window is still bigger than its best size"; to prevent it, handle the event on
the TLW, `SetSize()` there and do *not* Skip. Documented generators: wxMSW "if and only if" Windows
10 1703+ with a PerMonitorV2 manifest; wxGTK "when using GTK 3.10 or later and only since
wxWidgets version 3.3.0".
Helpers (`interface/wx/event.h:3610-3660`): `GetOldDPI()`, `GetNewDPI()`, `Scale(wxSize)`, and since 3.3.0
`Scale(wxPoint)`/`Scale(wxRect)`, `ScaleX/ScaleY` — old-DPI→new-DPI via `wxMulDivInt32`. Prefer
them to `GetNewDPI()/96` (72-based on macOS).
**Platforms** [source unless noted]:
| Port | Who generates it | What wx itself rescales |
|---|---|---|
| MSW | `WM_DPICHANGED` → `wxNonOwnedWindow::HandleDPIChange` (`src/msw/nonownedwnd.cpp`), only for PMv2-aware windows | `wxWindowMSW::MSWUpdateOnDPIChange` (`src/msw/window.cpp`) recurses from the TLW through every non-TLW child; for each window it first rescales `m_min/maxWidth/Height`, invalidates best size, re-creates the window font at the new PPI (`MSWUpdateFontOnDPIChange`; the TLW's override `wxTopLevelWindowMSW::MSWUpdateFontOnDPIChange` only re-selects its icons and leaves the TLW font alone), rescales sizer borders, spacer sizes and nested-sizer min sizes (`UpdateSizerOnDPIChange`; window items keep their min size because each window scales its own), then recurses into its children, then sends the event to that window. If the TLW's event was **not processed**, wx `SetSize()`s the TLW to the suggested rect inflated to the sizer's min size |
| macOS | `windowDidChangeBackingProperties` when the backing scale changes (`src/osx/cocoa/nonownedwnd.mm`), DPIs built from the 72-based std PPI (`wxWindowBase::WXNotifyDPIChange`). Undocumented. The same notification sends `wxSysColourChangedEvent` when the colour space changes | nothing (logical = DIP) |
| GTK3 | TLW configure event when `GetContentScaleFactor()` changed (`src/gtk/toplevel.cpp` `wxTopLevelWindowGTK::GTKConfigureEvent`, `__WXGTK3__` only); the initial scale is captured at creation, so there is no event at startup; DPIs are 96 × integer scale | nothing (logical = DIP) |
| GTK2 | never | — |
The default TLW resize exists only on MSW; on macOS/GTK3 logical sizes do not change with DPI.
Handler order: children receive the event before their TLW (post-order, documented; [source]
`src/common/wincmn.cpp` `NotifyAboutDPIChange`; MSW recursion in `MSWUpdateOnDPIChange`).
Dynamic handlers run most recently bound first, before static tables
(`docs/doxygen/overviews/eventhandling.h:475-482`), so a handler you `Bind` on
a control runs before the control's own (`wxBookCtrlBase`, `wxComboCtrlBase`, `wxTreeCtrlBase` bind
one; wxMSW `wxStaticBitmap` uses a static table entry, `src/msw/statbmp.cpp`, and MSW button
bitmaps bind one, `src/msw/anybutton.cpp`).
**What wx rescales vs what Orca must re-apply:**
| Item | MSW (done by wx before the event) | macOS / GTK3 | Orca's `on_dpi_changed` must |
|---|---|---|---|
| window min/max sizes | rescaled by the DPI ratio | unchanged (logical = DIP) | re-set only if it recomputes them anyway |
| window font set by `SetFont` | re-created at the new PPI for non-TLW windows; the TLW keeps its old-PPI font [source] | unchanged (points) | nothing: a `wxFont` stores points and every window or DC `SetFont` re-adjusts it to its target's PPI; `DPIAware::rescale` re-reads it and updates `em_unit` |
| sizer borders, spacers, nested-sizer min sizes | rescaled | unchanged | nothing |
| TLW size | resized only if the TLW event is unprocessed — never for DPIAware windows | unchanged | re-establish min size, `Fit()`/`SetSize()` |
| values given to setters (`SetRowHeight`, column widths, `SetItemMinSize`, custom-widget sizes, cached pixel members) | never | never | re-apply from `FromDIP`/`em_unit` |
| bitmaps on native controls | reselected from the control's bundle; for Orca's single bitmaps that is the old raster, unscaled or integer-upscaled | GTK never; macOS from the bundle | `msw_rescale()` + `SetBitmap()` |
| Orca widgets (cached measures, named icons) | never | never | call `Rescale()` |
**Pitfalls**
- **Rule:** Call `Skip()` in every `wxEVT_DPI_CHANGED` handler you bind, on a child, a control or
the TLW (DPIAware's own handler is the one exception).
**Why:** your dynamically bound handler runs first; without `Skip()` the control's own handler
(book controls, combo controls, MSW static bitmaps and button images) never runs and keeps its
old-DPI appearance. Bound on a `DPIDialog`/`DPIFrame` (as `m_parent` is here), it also starves
DPIAware's own handler, so `on_dpi_changed` never runs.
```cpp
m_parent->Bind(wxEVT_DPI_CHANGED, [this](wxDPIChangedEvent& e) { UpdateButtons(); }); // Wrong
m_parent->Bind(wxEVT_DPI_CHANGED, [this](wxDPIChangedEvent& e) { UpdateButtons(); e.Skip(); }); // Right
```
Cite: `interface/wx/event.h:3572-3575`; `src/slic3r/GUI/Widgets/DialogButtons.cpp`
`DialogButtons::on_dpi_changed`.
- **Rule:** Do not add a second `wxEVT_DPI_CHANGED` handler on a `DPIDialog` expecting wx to
resize the dialog; put the sizing in `on_dpi_changed()`.
**Why:** DPIAware's TLW handler never Skips, so the event counts as processed and wxMSW skips
its "scale size / ensure ≥ best size" resize.
Cite: [source] `src/msw/nonownedwnd.cpp` `wxNonOwnedWindow::HandleDPIChange`.
## DPIAware rescale path (Orca)
`template<class P> DPIAware : public P` (`src/slic3r/GUI/GUI_Utils.hpp`) wraps `wxDialog`/`wxFrame`;
`DPIFrame` (typedef) and `DPIDialog` (subclass) are the instantiations every Orca top-level window
uses. Its modal, ESC and dark-mode parts are in `references/windows-dialogs.md` and
`references/colours-dark-mode.md`; this section is the DPI part.
**Constructor.**
- `m_scale_factor = get_dpi_for_window(this) / 96` and `m_prev_scale_factor` = the same.
`get_dpi_for_window` (`GUI_Utils.cpp`) is real only on Windows (`GetDpiForWindow`, falling back
to `GetDpiForMonitor` or the DC); on Linux and macOS it is a `// TODO` stub returning
`DPI_DEFAULT` (96), so `m_scale_factor` starts at 1.0 there.
- `m_normal_font = get_default_font_for_dpi(this, dpi)` (MSW: `SystemParametersInfoForDpi`
message font for that DPI; elsewhere `wxSYS_DEFAULT_GUI_FONT`), applied with `SetFont` except
on macOS (`#ifndef __WXOSX__`, comment "Don't call SetFont under OSX to avoid name cutting in
ObjectList"). The window font is set before `em_unit` is measured because the default window
font is the primary display's.
- `update_em_unit()`:
```cpp
#if !defined(__WXGTK__)
m_em_unit = std::max<size_t>(10, 10.0f * m_scale_factor); // MSW: DPI-based; macOS: always 10
#else
m_em_unit = std::max<size_t>(10, this->GetTextExtent("m").x - 1); // GTK: from the font
#endif
```
**Bindings.**
- `wxEVT_DPI_CHANGED`, non-macOS only (`#ifndef __WXOSX__`): stores
`GetNewDPI().x / 96` in `m_scale_factor` and calls `rescale(wxRect())` if
`m_can_rescale && (m_force_rescale || is_new_scale_factor())`. It does **not** `Skip()`: the
dialog sizes itself, so wx's MSW TLW resize is suppressed. On GTK3 this handler fires (wx 3.3)
and sets a real integer scale from the 96-based DPI; on macOS it is not bound, so
`on_dpi_changed` never runs there; on GTK2 there is no event.
- `wxEVT_MOVE_START`/`wxEVT_MOVE_END` (wxMSW-only events, `interface/wx/event.h:5006-5014`): START
clears `m_can_rescale`, so a DPI event during an interactive drag only records the new factor;
END rescales with the move rect if the factor changed, else re-arms `m_can_rescale`. The DPI
handler itself is what rescales; MOVE_END only performs a rescale that was deferred.
- `enable_force_rescale()` makes the next DPI event rescale even if the factor is unchanged.
**`rescale(suggested_rect)`**: `Freeze()` → `m_normal_font = GetFont()` (the TLW font, which wxMSW
does not re-create on a DPI change; harmless, because a `wxFont` stores points and is re-adjusted
to the PPI of whatever window or DC it is set on) → `update_em_unit()` → pure virtual `on_dpi_changed(suggested_rect)`
→ `Layout()` → `Thaw()` → `m_prev_scale_factor = m_scale_factor`. `suggested_rect` is empty on the
DPI-event path and the moved window rect on the MOVE_END path; do not rely on it.
**What `on_dpi_changed` does** (the canonical shape):
```cpp
void MyDialog::on_dpi_changed(const wxRect&) {
m_logo.msw_rescale(); // ScalableBitmap: new raster at the new DPI
m_logo_ctrl->SetBitmap(m_logo.bmp()); // the control holds its own copy
m_ok_btn->Rescale(); // every Orca widget (Button, TextInput, ComboBox, ...)
const int em = em_unit();
msw_buttons_rescale(this, em, {wxID_CLOSE}); // native stock-id wxButtons only
m_list->SetMinSize(wxSize(-1, 16 * em)); // re-apply stored sizes
SetMinSize(wxSize(65 * em, 30 * em)); // explicit minimum + Fit(), or
Fit(); // GetSizer()->SetSizeHints(this) in place of both
Refresh();
}
```
`AboutDialog::on_dpi_changed` is this shape (ScalableBitmap + `SetBitmap`, html fonts re-derived
from `GetFont()`, `msw_buttons_rescale`, min sizes, `Fit()`, `Refresh()`).
`PreferencesDialog::on_dpi_changed` shows the child walk: recurse `GetChildren()` and call
`Rescale()` on each Orca widget found by `dynamic_cast`; inside `namespace Slic3r::GUI` write the
types qualified (`::CheckBox`, whose method is `Rescale()`, not `msw_rescale()`), because
unqualified `CheckBox` names the `Field` class there (`references/orca-widgets.md`). wx 3.3's
`CallForEachChild(functor)` (`interface/wx/window.h:602-624`) does the same recursive walk, the
window itself included; it also descends into owned top-level children ([source]
`include/wx/window.h` `wxWindowBase::CallForEachChild`).
**Cascade.** `MainFrame::on_dpi_changed` is the root for the main window: `update_fonts`, the
tab panel / top bar / buttons `Rescale()`, `plater()->msw_rescale()`, the param panel, lazily
built pages through `when_built`, then a `SetSize(sz + 1)`/`SetSize(sz)` jiggle (with
un-maximize/re-maximize) to force a full redraw. Child panels expose `msw_rescale()`/`Rescale()`
and are called from their owner; they do not get `on_dpi_changed`.
**Self-rescaling components.** `DialogButtons` binds its parent's `wxEVT_DPI_CHANGED` in the ctor,
unbinds in the dtor, restyles and `Skip()`s. Being bound after DPIAware's handler, it runs first.
This is the model for a component that must rescale without its owner's help. Keep one-time
`Bind` calls out of the restyle function such a handler runs: `Bind` does not deduplicate, so a
handler bound there runs once more after every DPI change.
**Helpers.**
- `msw_buttons_rescale(dlg, em, ids)` (`wxExtensions.cpp`; all platforms despite the name) calls
`SetMinSize(wxSize(-1, 2.5 * em))` on whatever window has each id. Meant for native `wxButton`s;
an Orca `Button` with that id (every `DialogButtons` OK/Cancel) takes it too and loses its style
height through `Button::SetMinSize`, so leave it out for Orca buttons
(`references/sizers-layout.md` §Layout on DPI change).
- `scale_factor()`/`prev_scale_factor()` are meaningful only on MSW (and on GTK3 after a DPI
event); `em_unit()`, `normal_font()` are the per-window values.
**Pitfalls**
- **Rule:** On GTK derive `em_unit` from the current font, and use the same computation in the
ctor and the rescale path (one helper).
**Why:** on GTK `DPIAware` starts with scale 1.0 because Orca's `get_dpi_for_window` is a stub
there; only the GTK3 `wxEVT_DPI_CHANGED` sets a real (integer) scale. The defect 40eab797c6 fixed: the ctor
measured the font while `rescale()` used `max(10, 10 × scale)` — e.g. 20 at 2× — so controls laid
out with a different em after a Linux DPI change than at construction.
```cpp
m_em_unit = std::max<int>(10, 10.0f * m_scale_factor); // Wrong: in rescale(), on every platform
update_em_unit(); // Right: same platform-branched helper as the ctor
```
Cite: 40eab797c6 (`src/slic3r/GUI/GUI_Utils.hpp` `DPIAware::update_em_unit`).
- **Rule:** In `on_dpi_changed`/`msw_rescale`, re-apply every size computed from `em_unit` or
`FromDIP` at construction (row heights, column widths, min sizes, cached pixel members).
**Why:** wx never rescales values passed to setters (`SetRowHeight`, column widths) on any
port; wxMSW only rescales stored min/max sizes and sizer spacers. Stale values clip or
overflow after a DPI or theme change (the object-list filament badge stopped fitting its row).
```cpp
// ctor: SetRowHeight(2 * em + FromDIP(2));
// msw_rescale(): SetRowHeight(2 * em + FromDIP(2)); // must repeat with the new em
// GetColumn(cn)->SetWidth(m_columns_width[cn] * em);
```
Cite: d5638273c6 (`src/slic3r/GUI/GUI_ObjectList.cpp` `ObjectList::create_objects_ctrl`,
`ObjectList::msw_rescale`). Layout side: `references/sizers-layout.md`.
- **Rule:** In `on_dpi_changed`, re-establish the minimum and resize: `GetSizer()->SetSizeHints(this)`
(does both), or `SetMinSize(...)` then `Fit()`/`SetSize()`; never leave a dialog without a
minimum that re-`Fit()`s on refresh paths.
**Why:** on MSW nothing else resizes a DPIAware dialog (wx's TLW resize is suppressed), so
`Refresh()` alone leaves it at its old physical size. A `Fit()` on a dialog without size hints
can collapse it on GTK when children are transiently zero-sized (after iconizing the main
window); with hints in place `Fit()` is safe. `wxSizer::SetSizeHints(win)` "first calls Fit()
and then wxTopLevelWindow::SetSizeHints()" (`interface/wx/sizer.h:937-970`), so a `Fit()` right
before it is redundant, and one after it re-applies the best size without the display clamp
(`references/sizers-layout.md` §Fitting functions); plain `Fit()` sets no minimum.
```cpp
Layout(); Fit(); // Wrong: ctor, no enforced minimum
void on_dpi_changed(const wxRect&) override { Refresh(); Fit(); }
Layout(); Fit(); v_sizer->SetSizeHints(this); // Right: ctor (the Fit() is redundant)
void on_dpi_changed(const wxRect&) override { GetSizer()->SetSizeHints(this); Refresh(); }
```
Cite: f760f4e462 (`src/slic3r/GUI/calib_dlg.cpp` `FlowRateCalibrationDialog`; the GTK collapse
mechanism is from the commit message, not visible in source); mechanism in
`references/sizers-layout.md`.
- **Rule:** Do not size anything from `scale_factor()`/`prev_scale_factor()` off MSW.
**Why:** `get_dpi_for_window()` returns a hard-coded 96 on Linux and macOS, so the factor is 1
at construction there (on GTK3 it can later jump to the event's integer scale; on macOS it
never changes).
```cpp
int w = int(120 * scale_factor()); // Wrong: 120 px on a Retina Mac / 2x GTK3 at startup
int w = FromDIP(120); // Right
```
## wxBitmap: physical size, scale factor, logical size
**Contract.**
- Size arguments of `wxBitmap` constructors and `GetWidth/GetHeight/GetSize` are physical
(`docs/doxygen/overviews/high_dpi.md:173-178`, `interface/wx/bitmap.h:788-800`).
- `CreateWithDIPSize(size, scale)` (`interface/wx/bitmap.h:486-521`): physical size = `size × scale`, rounded;
afterwards `GetDIPSize() == size`, `GetScaleFactor() == scale`. For fixed (compile-time) sizes.
`CreateScaled` is its older synonym (`interface/wx/bitmap.h:579`).
- `CreateWithLogicalSize(size, scale)` (since 3.3.0, `interface/wx/bitmap.h:523-558`): for sizes from
`GetClientSize()` etc. with `scale = GetDPIScaleFactor()`; physical = `size` on MSW,
`size × scale` where `wxHAS_DPI_INDEPENDENT_PIXELS` is defined.
- `GetLogicalSize()` (`interface/wx/bitmap.h:685-706`): physical / scale factor on DPI-independent ports,
`GetSize()` elsewhere; "must be used in any computations involving the sizes expressed in
logical units" (`docs/doxygen/overviews/high_dpi.md:176-178`). `GetScaledSize/Width/Height` are its older synonyms.
`GetDIPSize()` (`interface/wx/bitmap.h:645-659`) is the same value on all platforms and "should not be used
as window or device context coordinates".
- `SetScaleFactor(scale)` (`interface/wx/bitmap.h:951-966`) changes no pixels, only the apparent drawn size,
"in the ports in which logical and physical pixels differ (i.e. wxOSX and wxGTK3, but not
wxMSW)". The doc of `GetScaleFactor()` says it "always returns 1 under the other platforms"
(`interface/wx/bitmap.h:744-751`) — **[source] contradicted on MSW**: `wxGDIImage` stores the factor "to use
the correct sizes in the code which uses it to decide on the bitmap size to use"
(`src/msw/gdiimage.cpp` `wxGDIImage::SetScaleFactor`, `GetDIPSize`); bundle selection reads
it (§wxBitmapBundle) and the MSW memory DC sizes text by it (§wxFont). GTK2 has no scale storage (`include/wx/gtk/bitmap.h`, `__WXGTK3__` only).
- `wxBitmap(const wxImage&, int depth, double scale)` exists on all three ports [source], but the
MSW one ignores `scale` (`double WXUNUSED(scale)`, `include/wx/msw/bitmap.h:68`); call
`SetScaleFactor()` afterwards on MSW. The `scale` argument does not resize: it declares that
the image is already sized for that backing scale (Orca's comments in `BitmapCache.cpp`
`wxImage_to_wxBitmap_with_alpha` and `BitmapComboBox.cpp` say the same). `wxBitmap(img, dc)`
inherits the DC's scale (`interface/wx/bitmap.h:370-385`).
- `wxBitmap(const wxCursor&)` is invalid on GTK under Wayland (`interface/wx/bitmap.h:388-401`).
- wxMSW `wxBitmap::Create(size, dc)` no longer multiplies by the DC's content scale
(`docs/changes.txt:94-96`).
**Offscreen drawing.** "The scaling factor of the bitmap determines the scaling factor used by
this device context" (`interface/wx/dcmemory.h:41-58`); `wxMemoryDC(wxDC*)` does **not** inherit
the DC's scaling (`interface/wx/dcmemory.h:80-89`). The cross-platform shape needs no `#ifdef`:
```cpp
wxBitmap bmp;
bmp.CreateWithDIPSize(wxSize(24, 24), GetDPIScaleFactor()); // fixed-size art
{ wxMemoryDC mdc(bmp); mdc.SetFont(GetFont()); /* draw in logical coords: FromDIP() values */ }
dc.DrawBitmap(bmp, pos); // 24 DIP, sharp on Retina/GTK3
// back buffer: bmp.CreateWithLogicalSize(GetClientSize(), GetDPIScaleFactor());
```
[source] On MSW the memory DC does not scale coordinates (logical = physical there, so a
`CreateWithDIPSize` bitmap is drawn with `FromDIP` coordinates), but it does size text by the
bitmap: `wxMemoryDCImpl::DoSelect` records the selected bitmap's scale factor and
`wxMemoryDCImpl::SetFont` adjusts every font to `GetPPI()` = 96 × that factor
(`src/msw/dcmemory.cpp`). The macOS memory DC applies the bitmap scale to its graphics context
(`src/osx/core/dcmemory.cpp`). Back-buffering and DC coordinates are in
`references/painting-custom-widgets.md`.
**OrcaSlicer.** `SwitchButton::Rescale` is the legacy manual HiDPI pattern: on macOS it measures
with `dc.GetFont().Scaled(scale)`, draws into a `scale ×` image and wraps it with
`wxBitmap(img, -1, scale)`, using `mac_max_scaling_factor()`; on MSW it draws into a scale-1
bitmap with `GetFont().Scaled(GetDPIScaleFactor())` (compensating the memory DC's 96-PPI text) and
tags the result with `SetScaleFactor` afterwards. New owner-drawn caches use `CreateWithDIPSize`/`CreateWithLogicalSize` +
`wxMemoryDC` instead.
**Pitfalls**
- **Rule:** Create drawn bitmaps with a DIP size and the window's scale.
**Why:** bitmap sizes are physical and `FromDIP` is the identity on macOS/GTK3, so the first
form is a 1x bitmap upscaled (blurry) on Retina and 2x GTK3; on MSW its scale factor stays 1, so
text drawn into it through a `wxMemoryDC` comes out at 100 % size.
```cpp
wxBitmap bmp(FromDIP(wxSize(32, 32))); // Wrong
wxBitmap bmp; bmp.CreateWithDIPSize(wxSize(32, 32), GetDPIScaleFactor()); // Right
```
Cite: `docs/doxygen/overviews/high_dpi.md:173-178`, `interface/wx/dcmemory.h:41-56`.
- **Rule:** Lay out from logical bitmap sizes.
**Why:** physical ≠ logical off MSW; `GetSize()` of a 2x bitmap is twice its drawn size.
```cpp
int w = bmp.GetWidth() + FromDIP(4); // Wrong: double width on Retina
int w = bmp.GetLogicalSize().x + FromDIP(4); // Right (ScalableBitmap::GetBmpWidth() for Orca icons)
```
Cite: `interface/wx/bitmap.h:685-706`.
## wxBitmapBundle and its limits in Orca's build
**Contract.**
- Any API taking `const wxBitmapBundle&` accepts a `wxBitmap` through the implicit converting
constructor (`interface/wx/bmpbndl.h:110-116`). This is how every Orca bitmap reaches wx
controls: Orca code does not build bundles itself.
- Selection (`docs/doxygen/overviews/high_dpi.md:245-255`, `interface/wx/bmpbndl.h:52-62`): use the closest existing bitmap without
scaling; scale only when the mismatch is large. The overview says "equal or greater than 1.5";
**[source]** the code scales only when the target scale is **greater than** 1.5 × the largest
available, and then by an integer factor (or rounds the target scale)
(`src/common/bmpbndl.cpp` `wxBitmapBundleImpl::DoGetPreferredSize`).
- Single-bitmap bundle [source] (`bmpbndl.cpp` `wxBitmapBundleImplSet::Init`,
`GetNextAvailableScale`): default size = `GetDIPSize()` of the smallest bitmap; its available
scale = (DIP size / default size) × `GetScaleFactor()`. Consequences: a 16 px bitmap with
scale 1 is shown unscaled at 150 % and upscaled to 32 px at 175 %/200 %; a 24 px bitmap tagged
`SetScaleFactor(1.5)` has DIP size 16 and is used as-is at 150 %.
- `GetBitmap(size)` (`interface/wx/bmpbndl.h:415-428`): size "in physical pixels"; dynamically created sizes
are cached until exit ("avoid calling it for many different sizes"). [source] the result gets
`SetScaleFactor(size.y / GetDefaultSize().y)` (`bmpbndl.cpp` `wxBitmapBundle::GetBitmap`), so
`GetBitmap(GetDefaultSize())` always yields a scale-1 bitmap at the DIP size — a downscaled 1x
bitmap on HiDPI.
- `GetBitmapFor(win)`, `GetPreferredBitmapSizeFor(win)` (physical),
`GetPreferredLogicalSizeFor(win)` (logical) take a "Non-null and fully created window"
(`interface/wx/bmpbndl.h:392-441`); null hits a `wxCHECK` and returns `wxDefaultSize` silently in Orca.
- `FromBitmaps(vec)` / `FromBitmaps(b1, b2)` (`interface/wx/bmpbndl.h:158-169`): all bitmaps valid, sizes
physical, the smallest defines the default size. `FromImpl(new MyImpl)` takes ownership ("must
not call DecRef()", `interface/wx/bmpbndl.h:200-212`). `FromFiles` also looks in a `2.0x` subdirectory since
3.3.2 (`interface/wx/bmpbndl.h:231-247`). A custom `wxBitmapBundleImpl` implements `GetDefaultSize()` (DIP),
`GetPreferredBitmapSizeAtScale()` (physical; may defer to `DoGetPreferredSize()` when
`GetNextAvailableScale()` is overridden) and non-const `GetBitmap(size)` (`interface/wx/bmpbndl.h:522-560`).
- Auto-update on DPI change happens only on MSW and macOS (`docs/doxygen/overviews/high_dpi.md:205-209`); GTK controls
keep the bitmap chosen at set time.
- The overview asks for art usable unscaled at least at 100 % and 200 % (or a single SVG), and
advises against shipping only a high-resolution version to be downscaled on 1x displays
("contours become more blurry", `docs/doxygen/overviews/high_dpi.md:189-197`). In Orca the SVG
route is `BitmapCache`, not a bundle (below).
**Orca's build.** `deps/wxWidgets/wxWidgets.cmake` passes `-DwxUSE_NANOSVG=OFF` (7658cf9076,
duplicate symbols with Orca's own nanosvg) and LunaSVG stays off, so `wxHAS_SVG` — defined only for
`wxHAS_RAW_BITMAP && (wxUSE_NANOSVG || wxUSE_LUNASVG)` (`include/wx/features.h:96-98`) — is
undefined. `wxBitmapBundle::FromSVG`, `FromSVGFile` and `FromSVGResource` do not exist (compile
error; `interface/wx/bmpbndl.h:272-275` says to check `wxHAS_SVG`). Knock-on effects [source]: the Tango art
provider returns empty bundles (`src/common/arttango.cpp`, `!wxHAS_SVG` branch), the std
provider's SVG logo is absent (`src/common/artstd.cpp`), and wxAUI tab/dock buttons fall back to
1-bit XBM art (`src/aui/tabart.cpp`, `src/aui/dockart.cpp`). Orca rasterizes SVG itself
(`BitmapCache::load_svg`) and recolours it for dark mode, which a stock bundle could not do.
**Why Orca keeps single bitmaps + explicit rescale** rather than bundles: no SVG bundles in this
build; GTK does not auto-update bundles anyway; owner-drawn widgets must re-measure on DPI change;
and the icon raster must also change on a theme switch. The MSW `SetScaleFactor` tagging in
`create_scaled_bitmap` makes the implicit single-bitmap bundle report the intended DIP size
(§Orca icon pipeline).
If a stock wx control ever needs auto-updating multi-resolution art, the Orca-compatible shape
is a bundle implementation backed by `BitmapCache` (not existing Orca code; a sketch):
```cpp
struct OrcaSvgBundleImpl : wxBitmapBundleImpl {
std::string name; wxSize def; // def in DIP
wxSize GetDefaultSize() const override { return def; }
wxSize GetPreferredBitmapSizeAtScale(double s) const override { return def * s; }
wxBitmap GetBitmap(const wxSize& sz) override { // sz is physical
static Slic3r::GUI::BitmapCache cache;
wxBitmap* b = cache.load_svg(name, 0, sz.y, false, wxGetApp().dark_mode());
return b ? *b : wxBitmap();
}
};
// wxBitmapBundle::FromImpl(new OrcaSvgBundleImpl{...}); // takes ownership
```
On macOS `BitmapCache` already multiplies by its own `m_scale`; such an impl would need a cache
whose scale is 1.
**Pitfalls**
- **Rule:** Never call `wxBitmapBundle::FromSVG*` in Orca.
```cpp
auto b = wxBitmapBundle::FromSVGFile(path, wxSize(16, 16)); // Wrong: does not compile here
ScalableBitmap icon(this, "cog", 16); // Right (or create_scaled_bitmap("cog", this, 16))
```
Cite: `include/wx/features.h:96-98`, `deps/wxWidgets/wxWidgets.cmake`.
- **Rule:** Draw a bundle at the bitmap the window needs.
**Why:** the size argument of `GetBitmap` is physical and the result is forced to scale 1.
```cpp
wxBitmap b = bundle.GetBitmap(bundle.GetDefaultSize()); // Wrong: 1x, downscaled on HiDPI
wxBitmap b = bundle.GetBitmapFor(this); // Right; draw at b.GetLogicalSize()
```
Cite: [source] `src/common/bmpbndl.cpp` `wxBitmapBundle::GetBitmap`; `interface/wx/bmpbndl.h:425`.
- **Rule:** Pass a created, non-null window to bundle size queries.
```cpp
bundle.GetPreferredBitmapSizeFor(nullptr); // Wrong: wxDefaultSize, silently
bundle.GetPreferredBitmapSizeFor(this); // Right, after Create()
```
Cite: `interface/wx/bmpbndl.h:399`.
## Orca icon pipeline
Icons are SVG files in `resources/images/`, referenced by **name string without extension**
(`BitmapCache` resolves `Slic3r::var(name + ".svg")`, then `".png"`). The entry points live in
`src/slic3r/GUI/wxExtensions.hpp/.cpp` and `BitmapCache.hpp/.cpp`.
**`create_scaled_bitmap(name, win = nullptr, px_cnt = 16, grayscale, new_color, menu_bitmap,
resize, bitmap2, array_new_color)`**:
```cpp
static BitmapCache cache; // process-wide, never cleared
unsigned h = wxWindow::FromDIP(px_cnt, win) + 0.5f; // static overload: win may be null
bool dark = menu_bitmap (MSW only) ? check_dark_mode() : wxGetApp().dark_mode();
wxBitmap* b = cache.load_svg(name, 0, h, grayscale, dark, new_color, resize ? em_unit(win) * 0.1f : 0);
if (!b) b = cache.load_png(name, 0, h, grayscale, ...); // neither found: throws Slic3r::RuntimeError
#ifdef __WXMSW__
b->SetScaleFactor(win ? win->GetDPIScaleFactor() : wxWindow::FromDIP(100, nullptr) / 100.0);
#endif
return *b;
```
`px_cnt` is the icon height in DIP. A missing icon name throws. `bitmap2 = true` routes to
`create_scaled_bitmap2`/`load_svg2` (semi-transparent filament art, no dark recolour).
Raster per platform [source]:
| Platform | Physical height | Scale factor | Drawn (logical) size |
|---|---|---|---|
| MSW | `FromDIP(px, win)` | `win->GetDPIScaleFactor()` (primary-display ratio when `win` is null) | `FromDIP(px)` px; the tag makes the implicit bundle's DIP size `px`, so wx uses it unscaled |
| macOS | `px × BitmapCache::m_scale` (SVG) | `m_scale`, via `wxBitmap(image, -1, m_scale)` | `px` points |
| GTK3 | `px` | 1 | `px`; at GDK scale 2 it is drawn upscaled (no HiDPI raster on GTK3) |
| GTK2 | `px`, round-tripped through PNG to fix broken alpha (`wxImage_to_wxBitmap_with_alpha`) | — | `px` |
Why the MSW tag matters: at 200 % `FromDIP(16)` is a 32 px raster; untagged (scale 1) its
implicit bundle has a 32-DIP default size and wx doubles it again to 64 px, while tagged 2.0 its
DIP size is 16 and it is used as-is. A raster kept from an older DPI is reselected by the same
rule after a DPI change: a 16 px / scale-1 bitmap stays 16 px at 150 % and is upscaled to 32 px
at 200 % — hence the re-`SetBitmap` in `on_dpi_changed` (§wxBitmapBundle selection).
`BitmapCache` [source]:
- `m_scale` (macOS only) is `mac_max_scaling_factor()` read when the cache is constructed — for
the static cache in `create_scaled_bitmap`, at the first icon load. Despite its name,
`mac_max_scaling_factor()` (`src/slic3r/Utils/MacDarkMode.mm`) loops over the screens but
reads `objectAtIndex:0` each time, i.e. it returns the backing factor of the first screen (the
one with the menu bar). Icons are therefore rasterized once for that screen: 1x on a Retina
laptop whose primary display is a 1x external monitor, with no re-rasterization when windows
move.
- `load_svg` keys the cache by name, height, `m_scale`, `-dm` (dark), `-gs` (grayscale) and
`new_color`; dark-mode recolouring by palette substitution is in
`references/colours-dark-mode.md`.
- `load_png` never applies the Retina factor (`wxImage_to_wxBitmap_with_alpha(image)` with scale
1; resized with `wxIMAGE_QUALITY_BILINEAR`) and gets no dark recolour.
**`ScalableBitmap(parent, icon_name = "", px_cnt = 16, grayscale, resize, bitmap2, new_color)`**
holds `{m_parent, m_icon_name, m_px_cnt, m_grayscale, m_resize, m_bmp}`.
- `msw_rescale()` re-runs `create_scaled_bitmap(m_icon_name, m_parent, m_px_cnt, m_grayscale,
"", false, m_resize)` — it is the DPI path on every platform and also the theme-switch path,
because it re-reads the dark flag. It does **not** re-apply `new_color` or `bitmap2`.
- `m_parent` is a raw pointer; the parent must outlive the `ScalableBitmap`.
- `GetBmpSize()/GetBmpWidth()/GetBmpHeight()` return the scaled (logical) size on Apple and
`GetSize()` elsewhere — equivalent to `GetLogicalSize()` given the scales above.
- `bmp()` returns the bitmap; wx controls that were given it keep their own copy.
**`ScalableButton(parent, id, icon_name, label, size, pos, style = wxBU_EXACTFIT | wxNO_BORDER,
use_default_disabled_bitmap, bmp_px_cnt = 16)`** is a native `wxButton` with a scaled bitmap. An
explicit `size` is stored in em/10 units (`size * 10 / em`) and re-applied as `m * em / 10` in
`msw_rescale()`; `UpdateDarkUI()` is `msw_rescale()`; on GTK it calls `RemoveButtonBorder`. New
code uses `Widgets/Button` instead.
**Orca widgets.** `Button(parent, text, icon = "", style = 0, iconSize = 0, id)` keeps its icon
as a `ScalableBitmap` (20 px when `iconSize <= 0`). `Button::Rescale()` re-rasterizes a **named**
icon, re-measures and re-applies the style; an icon set through `SetIcon(const wxBitmap&)` has no
name and cannot be re-rasterized, so prefer `SetIcon(const wxString&)`. Other widgets' `Rescale()`
follow the same idea (`references/orca-widgets.md`, `references/painting-custom-widgets.md`).
**Menu icons.** `create_menu_bitmap(name)` = `create_scaled_bitmap(name, nullptr, 16, false, "",
true)`: created without a window, so at primary-display DPI on MSW, and on MSW the dark variant
follows `check_dark_mode()`. `msw_rescale_menu` exists only on MSW. Menus
are in `references/popups-menus.md`.
**Pitfalls**
- **Rule:** Pass the real window (not `nullptr`) and re-create the bitmap in `on_dpi_changed`.
**Why:** a null window means primary-display DPI on MSW (wrong raster and wrong scale tag on a
secondary monitor); `em_unit(nullptr)` falls back to the main frame's em for `resize`.
```cpp
m_icon = ScalableBitmap(nullptr, "cog", 16); // Wrong
m_icon = ScalableBitmap(this, "cog", 16); // Right; m_icon.msw_rescale() in on_dpi_changed
```
- **Rule:** After `msw_rescale()`, hand the new bitmap to every control that displays it.
**Why:** `msw_rescale()` replaces only the `ScalableBitmap`'s own `m_bmp`; a `wxStaticBitmap` or
native button keeps the old copy (on MSW wx merely rescales that old raster).
```cpp
m_icon.msw_rescale(); // Wrong alone
m_icon.msw_rescale(); m_bmp_ctrl->SetBitmap(m_icon.bmp()); // Right
```
Cite: `src/slic3r/GUI/AboutDialog.cpp` `AboutDialog::on_dpi_changed`.
- **Rule:** Pass a bare resource name and an explicit DIP height.
**Why:** the third argument is `px_cnt`, not a bitmap type, and the name is resolved as
`var(name + ".svg"|".png")`. `px_cnt = 0` means "the asset's own height": it dereferences
`parent`, and on Retina macOS it stores the physical height, which doubles the icon [source]
`ScalableBitmap::ScalableBitmap`.
```cpp
ScalableBitmap(this, Slic3r::var("logo.png"), wxBITMAP_TYPE_PNG); // Wrong: path + type as px_cnt
ScalableBitmap(this, "logo", 16); // Right
```
- **Rule:** Author new icons as SVG.
**Why:** `load_png` is never Retina-scaled and never dark-recoloured.
- **Rule:** Re-apply `new_color`/`bitmap2` art yourself on rescale.
**Why:** `ScalableBitmap::msw_rescale()` drops both, so a recoloured icon reverts to its default
colours after a DPI or theme change. Keep the colour and rebuild with the full constructor.
## Image lists, art provider, wxImage
**`wxImageList`** (`interface/wx/imaglist.h:37-39, 60-63`): "Use of this class is not recommended
in the new code as it doesn't support showing DPI-dependent bitmaps. Please use
wxWithImages::SetImages() instead"; "the size is specified in physical pixels and must correspond
to the size of bitmaps … that will be added". 3.3 made the size physical and makes calls on an
invalid list assert (`docs/changes.txt:53-56, 85-88`) — silently in Orca's assert-free build.
When a list is unavoidable, `wxBitmapBundle::CreateImageList(win, bundles)` builds one at the
consensus size (public but undocumented, [source] `include/wx/bmpbndl.h`).
```cpp
auto* il = new wxImageList(FromDIP(16), FromDIP(16)); il->Add(bmp_of_other_size); // Wrong
auto sz = bmps[0].GetSize(); auto* il = new wxImageList(sz.x, sz.y); // Right (or SetImages(bundles))
```
**`wxArtProvider`** (`interface/wx/artprov.h:285-330`): `GetBitmap(id, client, size)` returns
that physical size; "applications using wxWidgets 3.1.6 or later should prefer calling
GetBitmapBundle()". `GetBitmapBundle(id, client, size)` takes the DIP default size — "this
implies that wxWindow::FromDIP() must not be used with it". The provider stack is native → Tango
→ std [source] `src/common/artprov.cpp`; in Orca's build Tango contributes nothing (no SVG), so
non-native ids come from low-resolution XPMs. `GetBitmap(id, client, FromDIP(wxSize(16, 16)))` is a
16-physical-pixel bitmap on Retina/GTK3 (drawn upscaled); hand the bundle to the control instead.
```cpp
wxArtProvider::GetBitmapBundle(wxART_WARNING, wxART_OTHER, FromDIP(wxSize(16, 16))); // Wrong: double-scaled on MSW
wxArtProvider::GetBitmapBundle(wxART_WARNING, wxART_OTHER, wxSize(16, 16)); // Right; give the bundle to the control
```
**`wxImage`** resizing (`interface/wx/image.h:28-89`): `wxIMAGE_QUALITY_NEAREST` is no longer an
alias of `NORMAL` since 3.3.0 (`docs/changes.txt:98-99`; `wxIMAGE_QUALITY_FAST` is the speed
synonym). `NORMAL` (default) = bilinear down to an integer multiple, then box average; `HIGH` =
box average when shrinking, bicubic when enlarging; `BILINEAR`, `BICUBIC`, `BOX_AVERAGE` explicit.
High-quality scaling "may not work as expected when using a single mask colour for
transparency" — use alpha (`interface/wx/image.h:1016-1019`). `Rescale` mutates and returns `*this`; `Scale`
returns a copy. `wxInitAllImageHandlers()` registers the compiled handlers: in Orca there is no
TIFF (`wxUSE_LIBTIFF=OFF`), WebP is built in, and SVG is not an image handler.
`wxImage::SetDefaultLoadFlags(0)` drops `Load_Verbose` warnings for images created afterwards
(`interface/wx/image.h:1825-1838`). For pixel-exact glyphs choose `NEAREST`; for icons `BILINEAR` (what
`load_png` uses) or `HIGH`; for photos `HIGH`.
## Window icons
`wxTopLevelWindow::SetIcon` (`interface/wx/toplevel.h:505-525`): "In wxMSW, icon must be either
16x16 or 32x32"; under Wayland it "doesn't do anything … create a `.desktop` file".
`SetIcons(wxIconBundle)` (`interface/wx/toplevel.h:527-546`): MSW wants 16 and 32, "preferably both"; also a
no-op on Wayland, where the icon comes from the desktop file matched by app id
(`wxApp::SetClassName`, `interface/wx/app.h:761-768`). [source] wxMSW picks the small and big
icon from the bundle at the window's DPI-aware system-metric sizes with `FALLBACK_NEAREST_LARGER`
and re-picks them on every DPI change (`src/msw/toplevel.cpp` `wxTopLevelWindowMSW::DoSetIcons`,
`MSWUpdateFontOnDPIChange`), so a bundle with 16/20/24/32/48 px entries stays sharp at any
scale; `SetIcon` is a one-icon bundle. `wxIconBundle(file,
type)` loads every icon in the file (`interface/wx/iconbndl.h:53`); `GetIcon(size, flags)` falls
back per `FALLBACK_SYSTEM` (default), `FALLBACK_NEAREST_LARGER` or `FALLBACK_NONE`
(`interface/wx/iconbndl.h:29-42, 141-158`).
Orca: the main frame takes its icon from the executable's resource on MSW and from
`OrcaSlicer_128px.png` elsewhere (`MainFrame.cpp` `main_frame_icon`).
```cpp
SetIcon(wxIcon(path_to_multi_size_ico, wxBITMAP_TYPE_ICO)); // Wrong: one size, scaled for both slots
SetIcons(wxIconBundle(path_to_multi_size_ico, wxBITMAP_TYPE_ICO)); // Right
```
## Displays and PPI
Window placement on displays is in `references/windows-dialogs.md`; the resolution side:
- `wxDisplay(const wxWindow*)` (since 3.1.2) is the display showing the window, "falling back to
the default display if it is not shown at all or positioned outside of any display"
(`interface/wx/display.h:35-50`). `GetFromWindow(win)` returns `wxNOT_FOUND` when the window is
on no display (`interface/wx/display.h:115-126`). [source] on macOS it picks an intersecting display with the
same backing scale as the window, else `wxNOT_FOUND` (`src/osx/core/display.cpp`
`wxDisplayFactoryMacOSX::GetFromWindow`).
- `GetPPI()` is the scaled resolution, `wxSize(0,0)` if unknown (`interface/wx/display.h:158-168`);
`GetRawPPI()` is unscaled, new in 3.3.2 (`interface/wx/display.h:170-181`); `GetScaleFactor()` = PPI / std
PPI (`interface/wx/display.h:183-194`); `GetStdPPIValue()` is 96, 72 on Apple (`interface/wx/display.h:196-221`).
- `IsConnected()` (3.3.0): objects go stale after a display configuration change; recreate them on
`wxEVT_DISPLAY_CHANGED`, do not cache `wxDisplay` (`interface/wx/display.h:223-241`).
- [source] `wxDisplay(unsigned n)` only `wxASSERT`s the index and then indexes a vector
(`src/common/dpycmn.cpp`), so `(unsigned)wxNOT_FOUND` reads out of bounds in Orca's
assert-free build. `GUI_App::window_pos_sanitize`/`window_pos_center` show the checked pattern.
```cpp
wxDisplay(wxDisplay::GetFromWindow(win)).GetClientArea(); // Wrong: wxNOT_FOUND → out of bounds
wxDisplay(win).GetClientArea(); // Right (or check != wxNOT_FOUND first)
```
## wxFont
**Contract.**
- Sizes are points (1/72 in): `wxFontInfo(double pointSize)` (fractional since 3.1.2,
`interface/wx/font.h:323-330`), or pixels via `wxFontInfo(wxSize)` / `SetPixelSize`, which is
"directly supported only under wxMSW and wxGTK currently; under other platforms a font with the
closest size … is found using binary search" (`interface/wx/font.h:1124-1138`). Prefer
`SetFractionalPointSize` to the legacy integer `SetPointSize` (`interface/wx/font.h:1100-1123`).
- `MakeBold/MakeLarger/MakeSmaller/Scale` mutate; `Bold/Larger/Smaller/Scaled` return copies
(`interface/wx/font.h:853-999`); Larger/Smaller use a factor of 1.2.
- `SetFaceName(face)` (`interface/wx/font.h:1021-1036`): if the face does not exist "the font is invalidated (so
that IsOk() will return false) and false is returned" ([source] `src/common/fontcmn.cpp`
`wxFontBase::SetFaceName` → `UnRef()`; the check is `wxFontEnumerator::IsValidFacename`, which
caches the face list on first use for the session (`src/common/fontenumcmn.cpp`); only the
Unix `AddPrivateFont` invalidates that cache).
- `wxFont::AddPrivateFont(path)` (`interface/wx/font.h:719-751`):
- macOS: does nothing but check that the file exists inside `Resources/Fonts` of the bundle;
the app must ship it there and set `ATSApplicationFontsPath`. [source] it compares the path
with `GetResourcesDir() + "/Fonts"` and `wxLogError`s otherwise (`src/osx/fontutil.cpp`).
- MSW: "must be called before any wxGraphicsContext objects have been created";
[source] `AddFontResourceEx(FR_PRIVATE)`, remembered for GDI+ (`src/msw/font.cpp`).
- Unix: needs Pango ≥ 1.38, else returns false and logs. [source] creates one fontconfig config
on the first call, then on **every** call adds the file, re-installs the config into Pango's
font map (`pango_fc_font_map_set_config`) and invalidates the face-name cache
(`src/gtk/font.cpp` `wxFontBase::AddPrivateFont`).
**DPI** [source]:
- On MSW a `wxFont` stores points; `SetFractionalPointSize` computes `lfHeight` at the primary
screen PPI and relies on `WXAdjustToPPI()` later (`src/msw/font.cpp`
`wxNativeFontInfo::SetFractionalPointSize`).
- `wxWindow::SetFont/GetFont` adjust the window's copy to its own PPI
(`src/common/wincmn.cpp` `wxWindowBase::SetFont/GetFont` → `WXAdjustFontToOwnPPI`), and
non-TLW window fonts are re-adjusted on DPI change (`src/msw/window.cpp`
`MSWUpdateFontOnDPIChange`; `src/msw/toplevel.cpp` overrides it for TLWs to re-select icons
only).
- `wxMSWDCImpl::SetFont` adjusts to the DC's window PPI when it has a window (`src/msw/dc.cpp`).
A `wxMemoryDC` has none; `wxMemoryDCImpl` overrides `SetFont`/`GetPPI` instead and adjusts every
font to 96 × the scale factor of the bitmap selected into it, re-applied at each `SelectObject`
(`src/msw/dcmemory.cpp` `wxMemoryDCImpl::DoSelect/SetFont/GetPPI`). With a scale-1 bitmap text is
laid out at 100 % whatever the monitor and whatever font object is passed: `GetFont()` of a
150 % window is re-adjusted down to 96 PPI too. A `wxGCDC`/`wxGraphicsContext` created from that
memory DC follows the same rule: the GDI+ context has no window, and its DPI is 96 × the bitmap's
scale factor (`src/msw/graphics.cpp` `wxGDIPlusRenderer::CreateContext(const wxMemoryDC&)`,
`wxGDIPlusContext::GetDPI`) — which is what `StaticBox::render`'s MSW anti-aliasing block hands to
`doRender` (`references/painting-custom-widgets.md`).
- So a global `wxFont` such as `Label::Body_14` is DPI-correct on any monitor when set on a window,
a window DC, or a memory DC whose bitmap was given the window's `GetDPIScaleFactor()` before it
was selected (`CreateWithDIPSize`/`CreateWithLogicalSize`, or `SetScaleFactor` then
`SelectObject`, as `get_extruder_color_icon` in `wxExtensions.cpp` does).
- On macOS and GTK a point is a fixed number of logical pixels (1 on macOS, 4/3 at 96 DPI on
GTK), so fonts need no DPI handling.
**Pitfalls**
- **Rule:** Check `IsOk()` after `SetFaceName` and fall back.
```cpp
font.SetFaceName("X"); dc.SetFont(font); // Wrong: invalid font if X is missing
if (!font.SetFaceName("X")) font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); // Right (Label::sysFont pattern)
```
Cite: `interface/wx/font.h:1030-1032`.
- **Rule:** On MSW, give a memory-DC bitmap the window's scale factor before selecting it; the
font object does not fix memory-DC text size.
**Why:** the MSW memory DC sizes text for 96 × the selected bitmap's scale factor, so text drawn
into a scale-1 `FromDIP`-sized bitmap comes out at 100 % size, too small at 125–200 %, even with
the window's own `GetFont()`.
```cpp
wxBitmap bmp(FromDIP(wxSize(24, 24))); // Wrong: scale 1
wxMemoryDC mdc(bmp); mdc.SetFont(GetFont()); // text at 96 PPI on a 150 % monitor
wxBitmap bmp; bmp.CreateWithDIPSize(wxSize(24, 24), GetDPIScaleFactor()); // Right
wxMemoryDC mdc(bmp); mdc.SetFont(Label::Body_12); // text at 144 PPI
```
Cite: [source] `src/msw/dcmemory.cpp` `wxMemoryDCImpl::DoSelect`, `wxMemoryDCImpl::SetFont`;
`src/slic3r/GUI/wxExtensions.cpp` `get_extruder_color_icon` (`SetScaleFactor` before
`SelectObject`).
## Orca fonts: Label table, sysFont, initSysFont
**The table** (`src/slic3r/GUI/Widgets/Label.hpp/.cpp`): static fonts `Label::Head_48, 32, 24, 20,
18, 16, 15, 14, 13, 12, 11, 10` (bold) and `Label::Body_16, 15, 14, 13, 12, 11, 10, 9, 8` (regular),
built by `Label::initSysFont()`. Convention: `Head_*` for titles and section headers, `Body_*` for
content; `Body_14` is the dialog workhorse (`SetFont(Label::Body_14)` on dialogs and controls),
`Body_12`/`Body_13` for dense secondary text. `Label` widgets default to `Body_14`.
**`Label::sysFont(size, bold)`**:
```cpp
#ifndef __APPLE__
size = size * 4 / 5; // integer arithmetic
#endif
wxString face = "HarmonyOS Sans SC";
if (wxLocale::GetSystemLanguage() == wxLANGUAGE_KOREAN) face = "NanumGothic";
wxFont font{size, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, bold ? wxFONTWEIGHT_BOLD : wxFONTWEIGHT_NORMAL, false, face};
font.SetFaceName(face);
if (!font.IsOk()) { font = wxSystemSettings::GetFont(wxSYS_DEFAULT_GUI_FONT); if (bold) font.MakeBold(); font.SetPointSize(size); }
```
- The 4/5 factor approximates the 72-vs-96 PPI difference (a point is 1 logical px on macOS,
4/3 px at 96 DPI elsewhere), so the same `Body_N` looks alike on macOS and MSW/Linux. That is
why literal point sizes are wrong: 12 pt is 12 logical px on macOS but 16 px at 100 % on MSW, so
`wxFont(12, …)` looks a third larger there.
- Integer truncation makes some entries identical on MSW/Linux: `Body_16`/`Body_15` and
`Head_16`/`Head_15` are 12 pt, `Body_11`/`Body_10` and `Head_11`/`Head_10` are 8 pt. Pick the
next step down when a visible difference matters.
- The Korean face is keyed on the **system** language (`wxLocale::GetSystemLanguage()`), not on
Orca's UI language.
- The statics are created once and stay DPI-correct on MSW because wx adjusts fonts per window,
window DC and memory-DC bitmap scale (§wxFont).
**`Label::initSysFont()`** runs near the start of `GUI_App::on_init_inner()` (after the log
target and the macOS deep-link handler), before any window or `wxGraphicsContext` exists, which
satisfies the MSW `AddPrivateFont` rule. On MSW and Linux it registers
`resources/fonts/HarmonyOS_Sans_SC_{Bold,Regular}.ttf` and `NanumGothic-{Regular,Bold}.ttf`
with `wxFont::AddPrivateFont` (`wxUSE_PRIVATE_FONTS=ON` in `deps/wxWidgets/wxWidgets.cmake`). On
Linux it skips all four calls when fontconfig already knows both families (e.g. installed
system-wide in a Flatpak): Orca's comment records that `AddPrivateFont` triggers a Pango crash in
`ensure_faces()` on Pango ≥ 1.48 because `FcConfigAppFontAddFile` invalidates Pango's cached font
map. macOS never calls it: the bundle plist sets `ATSApplicationFontsPath = fonts/`, and wx's
macOS `AddPrivateFont` would reject any path outside `Resources/Fonts`.
**App fonts.** `GUI_App::init_fonts()`/`update_fonts()` derive `normal_font()`, `small_font()`,
`bold_font()`, `link_font()` and `code_font()` from the `Label` statics (`update_fonts` uses
`Body_14`; `init_fonts` overrides small/bold sizes on macOS; the code font is
`wxFONTFAMILY_TELETYPE` at the small size). The splash screen's `scale_font` works around MSW
`SetFractionalPointSize` using the primary PPI by computing `lfHeight` for the splash's own DPI.
**macOS notes.** `DPIAware` and the main frame skip their default-font `SetFont` on macOS ("name
cutting in ObjectList"); that is about the window default font, not a ban — dialogs set
`Label::Body_14` normally. `OG_CustomCtrl` draws a focused URL label underlined but not bold on
macOS (workaround for a Big Sur bold-font rendering issue).
**Pitfalls**
- **Rule:** Use the `Label` table, never hard-coded point sizes or ad-hoc faces.
```cpp
title->SetFont(wxFont(12, wxFONTFAMILY_SWISS, wxFONTSTYLE_NORMAL, wxFONTWEIGHT_BOLD)); // Wrong
title->SetFont(Label::Head_12); // Right
```
Cite: `src/slic3r/GUI/Widgets/Label.cpp` `Label::sysFont`.
- **Rule:** Do not call `wxFont::AddPrivateFont` for Orca's fonts outside `Label::initSysFont`.
**Why:** macOS rejects paths outside `Resources/Fonts` (fonts load through the plist); MSW
requires the call before any graphics context, and [source] does not invalidate the
session-cached face list, so a `SetFaceName` check made before registration keeps rejecting the
private face; on Linux every call re-installs Pango's fontconfig map, which crashes Pango ≥ 1.48.
Cite: `interface/wx/font.h:719-751`; `Label::initSysFont`.
@@ -0,0 +1,958 @@
# Events: binding, dispatch, posting and deferred calls
How wx 3.3.2 finds, runs, propagates, queues and drops event handlers, and how OrcaSlicer code
binds, emits and defers. Read it before writing any `Bind`/`Unbind`, `Skip()`, `ProcessEvent`,
`wxPostEvent`/`wxQueueEvent` or `CallAfter`, before defining a custom event, and when debugging a
handler that never runs, runs twice, runs on a dead object, or swallows a widget's own behaviour.
Contents: [Rules](#rules) · [1 Dispatch order](#1-dispatch-order) ·
[2 Bind and Unbind](#2-bind-and-unbind) · [3 Static event tables](#3-static-event-tables) ·
[4 Skip discipline](#4-skip-discipline) · [5 Propagation](#5-propagation) ·
[6 Emitting events synchronously](#6-emitting-events-synchronously) ·
[7 Posting and queueing](#7-posting-and-queueing) · [8 CallAfter](#8-callafter-and-the-liveness-rule) ·
[9 Custom events and payloads](#9-custom-events-and-payload-ownership) · [10 Ids](#10-window-and-event-ids) ·
[11 UPDATE_UI](#11-wxevt_update_ui) · [12 Idle events](#12-idle-events) ·
[13 Event filters](#13-event-filters) · [14 Pushed handlers and blockers](#14-pushed-handlers-wxeventblocker-setevthandlerenabled) ·
[15 Exceptions](#15-exceptions-in-handlers) · [16 How Orca widgets emit events](#16-how-orca-widgets-emit-events)
Build fact that shapes every pitfall below: OrcaSlicer builds wx with `-DwxBUILD_DEBUG_LEVEL=0`
(`deps/wxWidgets/wxWidgets.cmake`) and `libslic3r_gui` with `-DwxDEBUG_LEVEL=0`
(`src/slic3r/CMakeLists.txt`). `wxASSERT`/`wxFAIL` compile to nothing and `wxCHECK_*` return silently
(`include/wx/debug.h:314-324, 356-382`). Every event misuse wx would assert on in a debug build
(pushed handler not popped, `Unbind` of an unknown entry, `RemoveFilter` of an unknown filter,
out-of-range window id, bad `FilterEvent` return) is a silent no-op or a later crash in Orca.
## Rules
1. New code binds dynamically (`Bind` with a lambda or method + handler); never add a static event
table (existing widget-internal tables stay where they are). Take the event by reference (`auto&`,
`wxXxxEvent&`). → §2, §3, §4
2. Later-bound handlers run first, and every dynamic handler runs before the static table. A handler
you bind on an Orca widget, a wx control or a window whose base class already bound the same
event must `Skip()` or the internal handler never runs. → §4, §16
3. `Skip()` every non-command event you do not fully replace: focus, size, key-down, unhandled
`wxEVT_CHAR_HOOK` keys, DPI and system-colour changes, TLW activation, mouse events on custom
widgets. Command events are normally not skipped. → §4
4. A binding on an object other than `this` (parent, TLW, canvas, app) must not outlive the handler:
method + `wxEvtHandler` sink is removed automatically but late; lambdas and non-`wxEvtHandler`
sinks are never removed. Unbind in the destructor, or use `EventGuard`. → §2
5. `Unbind` with the same emitter, event type, id range and the same functor object (or the same
method + handler). A lambda literal never matches. → §2
6. `Bind`/`Unbind` on the main thread only; binding twice registers twice. → §2
7. Only command events, `wxEVT_CHAR_HOOK` and Orca's `SimpleEvent`/`Event<T>` family propagate; they
stop at dialogs, and at popups on MSW and macOS but not on wxGTK. Bind on the emitting control, or
on the dialog/popup itself. → §5
8. `wxEVT_DESTROY` bubbles from children: compare `GetEventObject()` with the window. A TLW's destroy
event arrives after its derived members are destroyed. → §5
9. Emit a window's event with `ProcessWindowEvent()` (or `HandleWindowEvent()` from native
callbacks), never `win->ProcessEvent()`; forward to another handler with `ProcessEventLocally()`. → §6
10. Construct the event class declared for the type, and set the event object and id. → §6, §9
11. A handler may destroy the emitter: never touch `this` after a synchronous emit that can lead to
destruction; defer destruction instead. → §6
12. `AddPendingEvent`/`wxPostEvent` only on the main thread; `QueueEvent`/`wxQueueEvent` with a heap
event from any thread. A posted event is dispatched on the object you posted to, bypassing its
pushed handlers. → §7
13. Events and `CallAfter`s queued on a handler are deleted with it. A worker must only post to a
target that outlives the worker (in Orca: `wxGetApp()`), or be stopped first. → §7
14. Order is FIFO only per target; a `CallAfter` that re-queues itself starves the UI. Use `wxTimer`
for retries and polling. → §7
15. Deferred work runs inside any nested loop (`ShowModal`, `wxYield`) and is held back by some
native modal loops; design for reentrancy. → §7
16. Every deferred lambda re-checks the liveness of everything it touches, except the handler it was
queued on. Never capture a bare `this` or `wxDataViewItem` and trust it. → §8
17. `CallAfter` captures are copied: capture by value, `shared_ptr` for move-only state; by-reference
captures only in a blocking marshal. → §8
18. Window work requested from a mouse handler or a webview script-message callback goes through
`CallAfter`. → §8
19. `wxDECLARE_EVENT` in the header, `wxDEFINE_EVENT` in exactly one `.cpp`. → §9
20. A custom event class derived from a concrete wx event overrides `Clone()` and copies every
payload member. Client objects on a `wxCommandEvent` are not owned by the event. → §9
21. Use `wxID_ANY` for controls and bind on the control; do not filter by id on an ancestor. → §10
22. `wxEVT_UPDATE_UI` handlers run every idle pass for every window: keep them trivial. → §11
23. Do not use idle events for periodic work; hidden panels still receive them. → §12
24. `FilterEvent` runs for every event: return `Event_Skip` fast. → §13
25. Pop or remove every pushed handler before its window dies; nest `wxEventBlocker` scopes and push
nothing else inside one. → §14
26. Catch exceptions inside handlers; one that escapes ends the session. → §15
---
## 1. Dispatch order
**Contract.** `wxEvtHandler::ProcessEvent()` searches in this order (`interface/wx/event.h:567-625`,
`docs/doxygen/overviews/eventhandling.h:451-515`):
| Step | What runs | Notes |
|---|---|---|
| 0 | `wxApp::FilterEvent()` and other `wxEventFilter`s (LIFO) | Anything but `Event_Skip` (-1) stops here. Called once per event, not again as it propagates ([source] `src/common/event.cpp:1537-1552`) |
| 1 | `TryBefore()` | Validators on windows |
| 2 | — | If `SetEvtHandlerEnabled(false)`, skip to step 5 (the overview's wording; the interface doc's "skips to step (7)" is inaccurate — [source] `TryHereOnly` returns `false` and `DoTryChain` still runs, `src/common/event.cpp:1582-1591, 1644-1648`) |
| 3 | Dynamic table (`Bind`) | **Most recently bound first**, before the static table (`docs/doxygen/overviews/eventhandling.h:474-483`) |
| 4 | Static event table | Macro order, derived class before base class |
| 4a | Implicit `CallAfter` entry | Runs a queued `wxAsyncMethodCallEvent` only when its event object is this handler ([source] `src/common/event.cpp:1644-1667` `TryHereOnly`) |
| 5 | Next handlers in the chain | For windows: the pushed-handler stack (§14) |
| 6 | `TryAfter()` | Windows propagate to the parent (§5); finally `wxTheApp->ProcessEvent()` |
`ProcessEvent` returns `true` iff some handler ran and did not call `Skip()` (`interface/wx/event.h:619-622`).
Before each handler call wx resets the flag with `event.Skip(false)` ([source]
`src/common/event.cpp:1443-1475` `ProcessEventIfMatchesId`), so a skip in one handler does not carry over to the
next: every handler that wants processing to continue must call `Skip()` itself.
A handler entry matches when the event type matches and the bound id is `wxID_ANY`, or equals the
event id, or the event id falls in `[id, lastId]` (`src/common/event.cpp:1443-1475`).
---
## 2. Bind and Unbind
**Contract.**
- Forms: `Bind(tag, functor, id = wxID_ANY, lastId = wxID_ANY, userData = nullptr)` and
`Bind(tag, &Class::method, handlerPtr, id, lastId, userData)` (`interface/wx/event.h:876-957`). The
method form accepts "an arbitrary method (doesn't need to be from a wxEvtHandler derived class)";
the handler pointer "must always be specified". `userData`: "wxWidgets will take ownership of
this pointer" — deleted when the handler is unbound or at program termination.
- Handlers can be bound at any time and removed with `Unbind`
(`docs/doxygen/overviews/eventhandling.h:237-252`). `Connect()` is the legacy form: "please use
[Bind] in any new code" (`interface/wx/event.h:705-706`).
- Lifetime (`docs/doxygen/overviews/eventhandling.h:332-335`), for a handler object not derived from `wxEvtHandler`: "the
lifetime of `myFrameHandler` must be greater than that of `MyFrame` object -- or at least it needs
to be unbound before being destroyed".
- `Unbind` "can only unbind functions, functors or methods which have been added using the Bind<>()
method. There is no way to unbind functions bound using the (static) event tables." Its note:
"functors are compared by their address which, unfortunately, doesn't work correctly if the same
address is reused for two different functor objects. Because of this, using Unbind() is not
recommended if there are multiple functors using the same eventType and id and lastId as a wrong
one could be unbound" (`interface/wx/event.h:958-997`).
**Mechanics** [source]:
- `Bind` takes `const Functor&`, stores a **copy** of the functor and records the **address of the
object you passed** (`include/wx/event.h:524-570` `wxEventFunctorFunctor`, `:3951-3961`). `Unbind`
matches on that address plus the functor type. So a lambda is unbindable when the same lvalue
(a member `std::function`, a named lambda that stays at one address, heap storage) is passed to
both calls; an inline lambda literal never matches. Method + handler pairs match by value and
always work. Captures must be copyable.
- `DoUnbind` requires `entry->m_id == id`; only `lastId == wxID_ANY` and `eventType == wxEVT_NULL`
act as wildcards (`src/common/event.cpp:1806-1824`). A handler bound with `ctrl->GetId()` is not
removed by `Unbind(evt, fn)` (id defaults to `wxID_ANY`), and vice versa. `Unbind` on a different
emitter than the one you bound on returns `false` silently.
- `DoBind` always appends (`src/common/event.cpp:1769-1803`): binding the same handler twice runs it twice.
- No locking in `DoBind`/`DoUnbind`: main thread only.
**Lifetime by handler kind** [source]:
| Handler | Removed automatically when the handler object dies? |
|---|---|
| `src->Bind(evt, &C::m, sink)` where `sink` is a `wxEvtHandler` (any window) other than `src` | Yes. `DoBind` registers a `wxEventConnectionRef` on the sink (`src/common/event.cpp:1793-1802`); the sink's `~wxTrackable` calls `OnSinkDestroyed`, which deletes the entries (`include/wx/event.h:4184-4200`, `src/common/event.cpp:2022-2042`) |
| Lambda or functor (`[this]{…}`), free function | No. `GetEvtHandler()` is null for functors |
| Method of a class not derived from `wxEvtHandler` (`GLCanvas3D`, `Plater::priv`) | No |
| Handlers bound on `this` itself | Deleted with `this` (`~wxEvtHandler`, `src/common/event.cpp:1204-1245`) |
The automatic removal runs **late**: `wxEvtHandler` derives from `wxObject, wxTrackable`
(`include/wx/event.h:3705-3706`), so `~wxTrackable` runs after the derived destructor, after member
destruction, and after the port destructor has destroyed the native window and the children
(`src/osx/window_osx.cpp` `~wxWindowMac`, `src/msw/window.cpp` `~wxWindowMSW`). Events the source
emits during that teardown (activation, focus, size, show) still reach the half-destroyed sink.
Unbind explicitly in the sink's destructor whenever the source can fire while the sink dies.
**Usage.**
```cpp
// Method + wxEvtHandler sink on another window: removed on sink death (late) — unbind early anyway
m_parent->Bind(wxEVT_DPI_CHANGED, &DialogButtons::on_dpi_changed, this);
DialogButtons::~DialogButtons() { m_parent->Unbind(wxEVT_DPI_CHANGED, &DialogButtons::on_dpi_changed, this); }
// Lambda on another object: unbindable only through the same stored object
std::function<void(wxShowEvent&)> m_on_show = [this](wxShowEvent& e) { e.Skip(); /* ... */ };
top->Bind(wxEVT_SHOW, m_on_show);
top->Unbind(wxEVT_SHOW, m_on_show); // same object → matches
```
**OrcaSlicer.**
- `EventGuard` (`src/slic3r/GUI/GUI_Utils.hpp`) is the RAII form: it stores the functor (or method +
handler) on the heap, so its address is stable and the destructor's `Unbind` matches. Use it when
the emitter outlives the handler object, or to drop a binding before the owner's base destructor
runs. The emitter must still be alive when the guard dies.
```cpp
EventGuard on_idle_evt; // member of PlaterWorker (src/slic3r/GUI/Jobs/PlaterWorker.hpp)
, on_idle_evt(plater, wxEVT_IDLE, [this](wxIdleEvent&) { process_events(); })
EventGuard on_progress_evt; // PrintHostQueueDialog binds on itself, unbinds during member destruction
, on_progress_evt(this, EVT_PRINTHOST_PROGRESS, &PrintHostQueueDialog::on_progress, this)
```
- `GLCanvas3D` is not a `wxEvtHandler`: its method bindings on its `wxGLCanvas` are never removed
automatically, so `GLCanvas3D::bind_event_handlers`/`unbind_event_handlers` are a mandatory pair,
and `Plater::priv::set_current_panel` unbinds the canvas of the panel being left before binding
the active one.
- **Binding on another window (popups and child widgets).** When an object binds on a window other
than itself — typically the top-level parent — unbind in its destructor.
`PopupWindow::Create` binds `wxEVT_ACTIVATE` on its top parent (wxGTK), `BindUnfocusEvent()` binds
`wxEVT_ACTIVATE`/`wxEVT_ICONIZE`/`wxEVT_SHOW` (wxMSW), and `PopupWindow::~PopupWindow` unbinds them
(`src/slic3r/GUI/Widgets/PopupWindow.cpp`). The static `GetTopParent` there returns the first
`wxNonOwnedWindow` strictly above its argument (or the root), so for a popup inside another popup
the handlers sit on the outer popup. Because `Create` passes `parent` and the destructor passes
`this`, the two calls resolve to different windows when `parent` is itself a TLW or popup that has a
parent: the wxGTK `Unbind` then misses silently (§2 Mechanics). These method + `this` bindings
would be removed by wx when the popup dies, but only after the teardown window described above; a
lambda binding would never be removed. Popup dismissal itself: see `references/popups-menus.md`.
**Pitfalls.**
- **Rule:** A lambda that captures `this` and is bound on a longer-lived object must be unbound
before `this` dies.
**Why:** functor bindings are not tracked; the emitter later calls into freed memory.
```cpp
// Wrong: dangles after this panel is destroyed
GetParent()->Bind(wxEVT_SHOW, [this](wxShowEvent& e) { e.Skip(); refresh(); });
// Right: method + wxEvtHandler sink, and unbind in the destructor
GetParent()->Bind(wxEVT_SHOW, &MyPanel::on_parent_show, this);
MyPanel::~MyPanel() { GetParent()->Unbind(wxEVT_SHOW, &MyPanel::on_parent_show, this); }
```
Cite: `src/common/event.cpp:1793-1802, 2022-2042`; `docs/doxygen/overviews/eventhandling.h:332-335`.
- **Rule:** Unbind with the same object, id and emitter you bound with.
**Why:** a temporary lambda has a new address; a missing id does not match an id-bound entry. Both
return `false` and leave the handler bound — silently.
```cpp
// Wrong
btn->Bind(wxEVT_BUTTON, fn, btn->GetId()); btn->Unbind(wxEVT_BUTTON, fn);
win->Unbind(wxEVT_SIZE, [this](wxSizeEvent& e) { e.Skip(); });
// Right
btn->Unbind(wxEVT_BUTTON, fn, btn->GetId());
```
Cite: `include/wx/event.h:524-570`; `src/common/event.cpp:1806-1824`.
- **Rule:** Move-only captures do not compile in `Bind` or `CallAfter`; use `std::shared_ptr`.
Cite: `include/wx/event.h:524-570` (functor stored by copy).
---
## 3. Static event tables
**Contract.** `wxDECLARE_EVENT_TABLE()` in the class, `wxBEGIN_EVENT_TABLE(Class, Base)` …
`wxEND_EVENT_TABLE()` in the `.cpp`; entries are searched in macro order, then the base class table
(`interface/wx/event.h:567-625` step 5). They run **after** every dynamic handler for the same event.
**Multiple inheritance.** "it is imperative that the wxEvtHandler(-derived) class is the first class
inherited such that the `this` pointer for the overall object will be identical to the `this`
pointer of the wxEvtHandler portion" (`interface/wx/event.h:376-380`). Put the wx base first in
`class X : public wxPanel, public Other`.
**OrcaSlicer.** New code binds with lambdas (`[this](auto& e)`) or method + `this`. The core widgets
keep their internal handlers in static tables (`DECLARE_EVENT_TABLE()` in `Widgets/StaticBox.hpp`,
`Widgets/Button.hpp`; tables in `Button.cpp`, `TextInput.cpp`, `SpinInput.cpp`, `ComboBox.cpp`,
`DropDown.cpp`, `TabCtrl.cpp`). That is why `Skip()` in user handlers matters (§4): a user `Bind` on
`Button` for `wxEVT_LEFT_DOWN` runs before `Button::mouseDown`. Do not add new static tables; when
changing such a widget, keep its internal handlers where they are.
---
## 4. Skip discipline
**Contract.** `wxEvent::Skip` (`interface/wx/event.h:238-252`): "Without Skip() (or equivalently if
Skip(false) is used), the event will not be processed any more. If Skip(true) is called, the event
processing system continues searching for a further handler function for this event, even though it
has been processed already in the current handler. In general, it is recommended to skip all
non-command events to allow the default handling to take place. The command events are, however,
normally not skipped as usually a single command such as a button click or menu item selection must
only be processed by one handler."
| Event | Rule | Cite |
|---|---|---|
| `wxEVT_SET_FOCUS` / `wxEVT_KILL_FOCUS` | "should almost invariably call wxEvent::Skip()"; a KILL_FOCUS handler "must not call wxWindow::SetFocus()" — defer it (`CallAfter`) | `interface/wx/event.h:3410-3416` |
| `wxEVT_SIZE` | "Sizers … rely on size events to function correctly … call Skip on all size events you catch" | `interface/wx/event.h:5058-5060` |
| `wxEVT_KEY_DOWN` | Not skipping suppresses `wxEVT_CHAR` for that key and "may also prevent accelerators … from working" | `interface/wx/event.h:1454-1461` |
| `wxEVT_CHAR_HOOK` | Propagates upward; handled (not skipped) → no `KEY_DOWN`/`CHAR`. Skip every key you do not handle | `interface/wx/event.h:1485-1508`; keyboard order: `references/mouse-keyboard-focus.md` |
| `wxEVT_PAINT` | The handler "must create a wxPaintDC"; skip only if default painting must also run | `interface/wx/event.h:2274-2285`; `references/painting-custom-widgets.md` |
| `wxEVT_DPI_CHANGED` | "should almost always call event.Skip() … as many controls rely on processing this event"; a TLW handler may deliberately not skip to suppress the default resize | `interface/wx/event.h:3571-3583` |
| `wxEVT_SYS_COLOUR_CHANGED` | The default handler propagates it to children; a TLW handler must Skip, call the base, or forward | `interface/wx/event.h:1950-1955` |
| `wxEVT_ACTIVATE` on a TLW | MSW: `wxTopLevelWindowMSW::OnActivate` (static table) saves and restores the last focused child; macOS: `wxFrame::OnActivate` (static table) installs the frame's menubar. A non-skipping dynamic handler disables both | [source] `src/msw/toplevel.cpp` `wxTopLevelWindowMSW::OnActivate`, `src/osx/carbon/frame.cpp` `wxFrame::OnActivate`; `references/popups-menus.md` §15 |
| Mouse/key events on custom widgets | Dynamic handlers run before the widget's static table: not skipping disables the widget's own press/release/capture logic | [source] `src/common/event.cpp:1644-1667` |
| Command events | Normally not skipped; `Skip()` lets the event continue to the static table, the next handler, then the parent | `interface/wx/event.h:247-251` |
wx's own controls bind some internals dynamically in their constructors — e.g.
`wxBookCtrlBase`, `wxComboCtrlBase` and `wxTreeCtrlBase` bind `wxEVT_DPI_CHANGED`
(`src/common/bookctrl.cpp:60`, `src/common/combocmn.cpp:839`, `src/common/treebase.cpp:172`) — so a
non-skipping handler you bind on
such a control later starves its rescale. DPI propagation order: `references/dpi-bitmaps-fonts.md`.
**OrcaSlicer — deliberate deviations.**
- `DPIAware<P>` (`src/slic3r/GUI/GUI_Utils.hpp`) binds `wxEVT_DPI_CHANGED` on the TLW (not on macOS)
**without** Skip: Orca rescales itself in `rescale()` and suppresses wx's default TLW resize, which
the doc allows. Its `wxEVT_SYS_COLOUR_CHANGED` handler Skips on macOS and Linux but not on Windows,
where the theme is app-forced.
- Because `DPIAware` binds in its own constructor, everything a derived dialog binds on itself later
runs first. `DialogButtons` binds the parent's `wxEVT_DPI_CHANGED` and calls `Skip()`, so it runs
before `DPIAware`'s handler and lets it run. Likewise `DPIAware` maps Esc to `Close()` in a
`wxEVT_CHAR_HOOK` handler on every `DPIDialog`: a derived dialog's own `wxEVT_CHAR_HOOK` handler must
`Skip()` every key it does not consume, or Esc stops closing the dialog (`CloneDialog`'s Enter→OK
hook is the model).
**Pitfalls.**
- **Rule:** Take the event parameter by reference.
**Why:** the functor form compiles with a by-value parameter; the lambda then receives a copy
(`include/wx/event.h:534-545` calls `m_handler(static_cast<EventArg&>(event))`), and `Skip()` on the
copy does nothing — the original counts as handled.
```cpp
// Wrong
ctrl->Bind(wxEVT_KILL_FOCUS, [](wxFocusEvent e) { e.Skip(); commit(); });
// Right
ctrl->Bind(wxEVT_KILL_FOCUS, [](wxFocusEvent& e) { e.Skip(); commit(); });
```
- **Rule:** A handler you add to an Orca widget, for any event the widget handles internally, calls
`Skip()`.
**Why:** your later `Bind` runs first (LIFO, dynamic before static); without Skip the widget's own
handler never runs.
```cpp
// Wrong: CheckBox's internal toggle handler never runs, the bitmap and half-state go stale
cb->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent&) { save(); });
// Right
cb->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) { e.Skip(); save(); });
```
The same applies to `GetTextCtrl()->Bind(wxEVT_TEXT_ENTER / wxEVT_KILL_FOCUS / wxEVT_TEXT, …)` on
`TextInput`/`SpinInput`/`ComboBox`, and to raw mouse/key binds on `Button` and other `StaticBox`
widgets. Cite: `src/slic3r/GUI/Widgets/CheckBox.cpp` `CheckBox::CheckBox`;
`src/slic3r/GUI/Preferences.cpp` ("let CheckBox::update() refresh the bitmap").
- **Rule:** Never call `Skip()` (or touch members) after the handler deleted `this` (§6).
---
## 5. Propagation
**Contract** (`docs/doxygen/overviews/eventhandling.h:536-568`):
- "the events of the classes deriving from wxCommandEvent are propagated by default to the parent
window if they are not processed in this window itself … all event classes not deriving from
wxCommandEvent … do not propagate upward." Mouse, motion, enter/leave, size, paint and key events
stay at the window — except `wxEVT_CHAR_HOOK`, which propagates (`interface/wx/event.h:1485-1494`).
- "the event propagation stops when it reaches the parent dialog, if any … The events do propagate
beyond the frames, however." `SetExtraStyle(wxWS_EX_BLOCK_EVENTS)` blocks at any window, or clears
the default on a dialog (`interface/wx/window.h:264-270`).
- The mechanism is `m_propagationLevel` (`interface/wx/event.h:263-279`): `wxEVENT_PROPAGATE_NONE` by default,
`wxEVENT_PROPAGATE_MAX` for command events; any event class may set it in its constructor.
`StopPropagation()` returns the old level for `ResumePropagation()` (`interface/wx/event.h:255-260`);
`wxPropagationDisabler` and `wxPropagateOnce` are RAII helpers (`interface/wx/event.h:346-367`).
**Source facts** [source]:
- **Popups block propagation on MSW and macOS, not on wxGTK.** `wxPopupWindowBase::Create` sets
`wxWS_EX_BLOCK_EVENTS` (`src/common/popupcmn.cpp:129-138`; not in the overview). The MSW and macOS
`wxPopupWindow::Create` call it (`src/msw/popupwin.cpp`, `src/osx/carbon/popupwin.cpp`, which the
Cocoa build uses); the wxGTK one never does (`src/gtk/popupwin.cpp` `wxPopupWindow::Create`). So a
`wxEVT_BUTTON` from a control inside a popup (`wxPopupTransientWindow`, Orca `PopupWindow`,
`DropDown`) that no handler inside consumes stops at the popup on MSW/macOS but bubbles on to the
popup's parent and its ancestors on GTK.
- `wxWindowBase::TryAfter` does not propagate to a parent that `IsBeingDeleted()`
(`src/common/wincmn.cpp:3499-3522`). After a block or the top of the chain, the event still goes
to `wxTheApp` — except `wxEVT_IDLE` (`src/common/event.cpp:1483-1498` `DoTryApp`).
- `wxWindowDestroyEvent` and `wxWindowCreateEvent` derive from `wxCommandEvent`
(`interface/wx/event.h:4554, 2255`): a `wxEVT_DESTROY` handler on a dialog also receives every
child's destroy event (unless the dialog itself is being deleted).
- `wxUpdateUIEvent` is a command event: every unhandled update-UI event walks the parent chain up to
the first blocking window (a dialog; a popup on MSW/macOS) or the root, and then `wxApp` (§11).
**Destroy-event timing** [source]. `wxWindowBase::Destroy()` of a child sends `wxEVT_DESTROY` before
`delete this` (`src/common/wincmn.cpp:559-573`). For a TLW, and for any `delete`, the event is sent
from a base destructor (`src/common/framecmn.cpp` `~wxFrameBase`, `src/msw/toplevel.cpp`
`~wxTopLevelWindowMSW`, `src/gtk/toplevel.cpp` `~wxTopLevelWindowGTK`, `src/osx/dialog_osx.cpp`
`~wxDialog`, `src/osx/nonownedwnd_osx.cpp` `~wxNonOwnedWindow`, `src/osx/window_osx.cpp` `~wxWindowMac`)
— after the derived class destructor and its members are gone. A `wxEVT_DESTROY` handler may clear
an outside pointer to the window, but must not call into the derived object. Cleanup that needs the derived members belongs in the
derived destructor (`WebDialog::~WebDialog` in `src/slic3r/GUI/WebDialog.cpp` documents this choice).
**OrcaSlicer.** The payload events in `src/slic3r/GUI/Event.hpp` — `SimpleEvent`, `IntEvent`,
`Event<T>`, `ArrayEvent<T, N>` — derive from `wxEvent` but set
`m_propagationLevel = wxEVENT_PROPAGATE_MAX`, so they bubble like command events and, like them,
stop at dialogs and (on MSW/macOS) popups.
**Pitfalls.**
- **Rule:** In a `wxEVT_DESTROY` handler bound on a window with children, check the event object.
```cpp
// Wrong: the first child destroyed clears the pointer
m_plugins_dlg->Bind(wxEVT_DESTROY, [this](wxWindowDestroyEvent&) { m_plugins_dlg = nullptr; });
// Right (GUI_App::open_plugins_dialog)
m_plugins_dlg->Bind(wxEVT_DESTROY, [this](wxWindowDestroyEvent& e) {
if (e.GetEventObject() == m_plugins_dlg) m_plugins_dlg = nullptr;
e.Skip();
});
```
- **Rule:** Do not expect events from inside a dialog or popup at its opener — and do not rely on
popup events *not* arriving there either.
**Why:** `wxWS_EX_BLOCK_EVENTS` (`src/common/dlgcmn.cpp` `wxDialogBase::wxDialogBase`,
`src/common/popupcmn.cpp`) blocks at every dialog, but at popups only on MSW/macOS; on wxGTK an
unconsumed command event from inside a popup reaches the opener's ancestors and any unfiltered
handler there. Bind on the dialog/popup or on the control, consume the event there, or re-emit
explicitly.
- **Rule:** An ancestor that binds a command event without an id filter receives that event from
every descendant control of that type.
---
## 6. Emitting events synchronously
| Call | Use for | Note |
|---|---|---|
| `win->ProcessWindowEvent(e)` = `win->GetEventHandler()->ProcessEvent(e)` | Emitting a window's own event | "ProcessEvent() itself can't be called for wxWindow objects as it ignores the event handlers associated with the window; use this function instead" (`interface/wx/window.h:2736-2744`) |
| `win->HandleWindowEvent(e)` = `GetEventHandler()->SafelyProcessEvent(e)` | Same, from code that must not leak exceptions (native callbacks) | `interface/wx/window.h:2726-2734` |
| `win->ProcessWindowEventLocally(e)` | This window and its pushed handlers only, no propagation | `interface/wx/window.h:2746-2757` |
| `handler->ProcessEventLocally(e)` | Forwarding an event to another handler | "should, be called to forward an event to another handler instead of ProcessEvent() which would result in a duplicate call to TryAfter()" (`interface/wx/event.h:627-651`) |
| `handler->SafelyProcessEvent(e)` | Catches exceptions → `wxApp::OnExceptionInMainLoop` | `interface/wx/event.h:653-666` |
| `handler->ProcessEvent(e)` | Non-window handlers | On a window object it bypasses pushed handlers (Orca `StateHandler`) |
**Emitting shape** (`docs/doxygen/overviews/eventhandling.h:660-671`): construct the event with the
type and `GetId()`, `SetEventObject(this)`, fill the payload, `ProcessWindowEvent(event)`.
**Programmatic changes.** wx controls normally send command events only for user actions. The
documented exceptions include `wxNotebook::AddPage/AdvanceSelection/DeletePage/SetSelection`,
`wxTreeCtrl::Delete/DeleteAllItems/EditLabel` and "All wxTextCtrl methods" — use
`wxTextCtrl::ChangeValue` instead of `SetValue`; `Replace`/`WriteText` have no event-free form
(`docs/doxygen/overviews/eventhandling.h:796-815`). `wxBitmapToggleButton::SetValue` "does not cause a EVT_TOGGLEBUTTON event
to be emitted" (`interface/wx/tglbtn.h:169`). Orca widget setters: §16.
**Reentrancy.** A synchronous emit runs every handler before it returns. If a handler destroys the
emitter, the emitter's code after `ProcessEvent` runs on freed memory. Non-TLW `Destroy()` deletes
immediately (`src/common/wincmn.cpp:559-573`).
**OrcaSlicer models.** `Button::sendButtonEvent` (`Widgets/Button.cpp`): `wxCommandEvent` of
`wxEVT_BUTTON` with `GetId()`, `SetEventObject(this)`, `GetEventHandler()->ProcessEvent`.
`MsgDialog::show_dsa_button` (`MsgDialog.cpp`) makes a label click behave like a checkbox click:
`SetValue(!GetValue())`, then emits `wxEVT_TOGGLEBUTTON` with the checkbox's id and object through its
`GetEventHandler()`. `CloneDialog` (`CloneDialog.cpp`) turns Enter in its spin box into an OK click
from its `wxEVT_CHAR_HOOK` handler by emitting `wxEVT_BUTTON` with `ok_btn->GetId()` through
`ok_btn->GetEventHandler()`, and Skips other keys.
**Pitfalls.**
- **Rule:** Never emit a window's event with `win->ProcessEvent()`.
```cpp
// Wrong: skips handlers pushed on the window (StateHandler, wxEventBlocker)
wxCommandEvent e(wxEVT_BUTTON, GetId()); e.SetEventObject(this); this->ProcessEvent(e);
// Right
wxCommandEvent e(wxEVT_BUTTON, GetId()); e.SetEventObject(this); ProcessWindowEvent(e);
```
Cite: `interface/wx/window.h:2736-2744`.
- **Rule:** Construct the event class the type was declared with.
**Why:** the `Bind` tag ties type and class at compile time, but nothing checks the object you
construct. `wxCommandEvent e(wxEVT_LEFT_DOWN, id)` compiles; handlers then `static_cast` it to
`wxMouseEvent&` (undefined behaviour), and it propagates to parents like a command event. To make
a whole card clickable, call the action directly or emit a semantic event.
```cpp
// Wrong: a "mouse" event that is a wxCommandEvent, emitted past the handler stack
auto forward = [this](wxMouseEvent&) {
wxCommandEvent click(wxEVT_LEFT_DOWN, GetId()); click.SetEventObject(this); this->ProcessEvent(click);
};
// Right: a semantic command event through the handler stack (or call select() directly)
auto forward = [this](wxMouseEvent&) {
wxCommandEvent click(wxEVT_BUTTON, GetId()); click.SetEventObject(this); ProcessWindowEvent(click);
};
child->Bind(wxEVT_LEFT_DOWN, forward);
```
Cite: `src/slic3r/GUI/PurgeModeDialog.cpp` `PurgeModeBtnPanel` (re-dispatches child clicks; not a
model to copy).
- **Rule:** Defer destroying the emitter out of its own handler.
**Why:** `Button::mouseReleased` is still on the stack when your `wxEVT_BUTTON` handler runs.
```cpp
// Wrong
btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { m_panel->Destroy(); }); // m_panel contains btn
// Right
btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) {
CallAfter([w = wxWeakRef<wxWindow>(m_panel)] { if (w) w->Destroy(); });
});
```
Alternative: `wxTheApp->ScheduleForDestruction(win)` (`interface/wx/app.h:191-212`). Deletion
rules: `references/windows-dialogs.md`.
---
## 7. Posting and queueing
**Contract.**
- `QueueEvent(wxEvent*)` is asynchronous and "takes ownership of the event parameter, i.e. it will
delete it itself … the pointer can't be used any more after the function returns". It "can be used
for inter-thread communication from the worker threads to the main thread. It is safe in the sense
that it uses locking internally", and wakes the idle loop via `wxWakeUpIdle()`
(`interface/wx/event.h:409-466`). `wxQueueEvent(dest, evt)` wraps it (`interface/wx/event.h:5368-5382`).
- `AddPendingEvent(const wxEvent&)` copies the event via `Clone()`, so the original may be on the
stack, but it "can't be used to post events from worker threads for the event objects with
wxString fields (i.e. in practice most of them)"; "Use QueueEvent() to avoid this"
(`interface/wx/event.h:468-488`). The overview adds that you "will need to use the latter [QueueEvent] when
doing inter-thread communication; when you use only the main thread you can also safely use the
former" (`docs/doxygen/overviews/eventhandling.h:618-620`). `wxPostEvent(dest, evt)` = `dest->AddPendingEvent(evt)`, "not
thread-safe for event objects having wxString fields, use wxQueueEvent() instead"
(`interface/wx/event.h:5355-5366`).
- Every posted event class "must implement" `Clone()` (`interface/wx/event.h:125-146`).
- `wxThreadEvent`: `Clone()` unshares the string; its category is `wxEVT_CATEGORY_THREAD`, which
keeps it out of `YieldFor()` calls that do not ask for that category; `SetPayload<T>` needs a
copy constructor that is thread-safe, "i.e. create a copy that doesn't share anything with the
original" (`interface/wx/event.h:3733-3790`). [source] Plain `wxYield()` is `YieldFor(wxEVT_CATEGORY_ALL)`, which
includes `THREAD`; the protection applies to masked yields such as the generic `wxProgressDialog`'s
`YieldFor(wxEVT_CATEGORY_UI|wxEVT_CATEGORY_USER_INPUT)` (`src/generic/progdlgg.cpp`).
**Source facts** [source]:
- `AddPendingEvent` is literally `QueueEvent(event.Clone())` (`include/wx/event.h:3780-3789`). wxString
is always a deep-copy `std::wstring` in 3.3 (`include/wx/string.h:121-132`), so the copy-on-write
rationale is historical; follow the documented rule anyway.
- **Where** a posted event is dispatched: the queue belongs to the handler you posted to, and
`ProcessPendingEvents` calls `SafelyProcessEvent` on that object (`src/common/event.cpp:1368-1440`).
Posting to a `wxWindow*` processes it on the window object itself, **bypassing pushed handlers**
(`StateHandler`, `wxEventBlocker`). Post to `win->GetEventHandler()` if they must see it.
- **When**: pending events run before idle (`src/common/evtloopcmn.cpp:258-330`
`wxEventLoopManual::DoRunLoop`, the MSW loop; per-port table below); a nested loop (`ShowModal()`,
`wxYield()`) processes them too (`src/common/evtloopcmn.cpp:172-192` `DoYieldFor`). An exiting loop
drains them before it returns on MSW (`DoRunLoop` tail) and in macOS modal loops
(`src/osx/core/evtloop_cf.cpp` `wxCFEventLoop::OSXDoRun`, run by `wxModalEventLoop::OSXDoRun` in
`src/osx/cocoa/evtloop.mm`), but not on wxGTK (`src/gtk/evtloop.cpp` `wxGUIEventLoop::DoRun` just
leaves `gtk_main()`), where they run in the enclosing loop's next idle pass. A callback can run inside any
`ShowModal()` or `wxYield()` reached from your code. Nested loops: `references/threads-timers-app.md`.
- **Order**: FIFO only per target handler. The app keeps a list of handlers with pending events and
drains `handler[0]` completely — including events added to it meanwhile — before the next
(`src/common/appbase.cpp:561-603`, `src/common/event.cpp:1368-1440`). `A->CallAfter(f1); B->CallAfter(f2);
A->CallAfter(f3);` runs f1, f3, f2. A callback that keeps queueing new work on any handler keeps
`ProcessPendingEvents` looping and native input and paint starve.
- **Destruction**: `~wxEvtHandler` removes itself from the app's list and deletes its queued events
(`src/common/event.cpp:1234-1238`) — events posted to a destroyed handler are dropped, not delivered.
`DeletePendingEvents` does not take `m_pendingEventsLock` (`src/common/event.cpp:1361-1366`): a worker
posting to an object being destroyed races its destructor, and a worker holding a raw pointer to a
dead target is undefined behaviour.
**Platforms** [source]:
| Port | Pending events (`CallAfter`, posted) | Idle events (`wxEVT_IDLE`, update-UI, deferred TLW deletes) |
|---|---|---|
| MSW | Keep running inside native modal loops (menu tracking, window move/size, `MessageBox`, common dialogs) through a `WH_GETMESSAGE` hook (`src/msw/window.cpp` `wxIdleWakeUpModule::MsgHookProc` → `wxApp::MSWProcessPendingEventsIfNeeded`, `src/msw/app.cpp:717-730`) | Not processed inside those native loops — do not defer repaint/layout to idle if it must happen during a live resize |
| macOS | Processed by a run-loop observer at `kCFRunLoopBeforeTimers` in common modes (`src/osx/core/evtloop_cf.cpp:86-112`) | At `kCFRunLoopBeforeWaiting`. While a native `wxMessageDialog`, `wxFileDialog` or `wxDirDialog` is modal, `wxCFEventLoopPauseIdleEvents` stops **both** (`src/osx/cocoa/msgdlg.mm:62`, `src/osx/cocoa/filedlg.mm:601`, `src/osx/cocoa/dirdlg.mm:123`, `src/osx/core/evtloop_cf.cpp:362-379`): a `CallAfter` queued before or during such a dialog runs only after it closes |
| GTK3 (default build, X11 and Wayland) / opt-out GTK2 | Pending events and idle run from one GLib idle source at `G_PRIORITY_LOW` (`src/gtk/app.cpp:102-153, 631`, `wxApp::DoIdle`): they starve while higher-priority sources (timers, redraw, input floods) are busy; a masked `YieldFor` removes that source before each iteration, so nothing queued runs inside it (`references/threads-timers-app.md` §Event categories and yields) | same source; `RequestMore()` or remaining pending events keep it installed — a busy loop |
**OrcaSlicer.** `GLCanvas3D::post_event` sets the event object and `wxPostEvent(m_canvas, …)` —
asynchronous, dispatched on the `wxGLCanvas`, where `Plater::priv` binds the canvas events.
`BackgroundSlicingProcess` posts with `wxQueueEvent(wxGetApp().mainframe->m_plater, evt.Clone())` and
`new wxCommandEvent(...)`; `SlicingProcessCompletedEvent` carries the worker's exception as a
`std::exception_ptr`. Worker → GUI patterns, `set_queue_on_main_fn`, Jobs:
`references/threads-timers-app.md`.
**Pitfalls.**
- **Rule:** From a worker, post only to an object that outlives the worker.
```cpp
// Wrong: races ~wxEvtHandler, or uses a dangling pointer
std::thread([panel] { wxQueueEvent(panel, new wxThreadEvent(EVT_DONE)); }).detach();
// Right: post to the app and re-validate on the main thread
std::thread([this, alive = m_alive] {
wxGetApp().CallAfter([this, alive] { if (alive->load()) on_done(); });
}).detach();
```
Cite: `src/common/event.cpp:1234-1238, 1361-1366`.
- **Rule:** Use one target for steps that must run in order.
**Why:** `this->CallAfter(a); wxGetApp().CallAfter(b);` gives no order between `a`'s handler queue
and the app's.
- **Rule:** Retry and polling go through `wxTimer` (`StartOnce`), not a self-re-queuing `CallAfter`
or `RequestMore()` (§12). Timers: `references/threads-timers-app.md`.
---
## 8. CallAfter and the liveness rule
**Contract** (`interface/wx/event.h:490-564`). `CallAfter(&T::method, args…)` — 0, 1 or 2 arguments;
"The method being called must be the method of the object on which CallAfter() itself is called" —
and `CallAfter(functor)`. "it is safe to use CallAfter() from other, non-GUI, threads, but … the
method will be always called in the main, GUI, thread context." Its documented purpose: actions that
"can't be performed inside their handlers, e.g. you shouldn't show a modal dialog from a mouse click
event handler as this would break the mouse capture state". The overview adds the alternative of
breaking the capture with `dialog.CaptureMouse(); dialog.ReleaseMouse();` when a dialog really must
be shown from such a handler (`docs/doxygen/overviews/eventhandling.h:878-901`). [source] That pair does not
break a capture taken through wx's capture stack: `ReleaseMouse` re-captures the previous holder
(`src/common/wincmn.cpp:3412-3416`), so release your own capture and `CallAfter` the dialog instead. Capture
rules: `references/mouse-keyboard-focus.md` §Mouse capture.
**Mechanics** [source]. Every overload is `QueueEvent(new wxAsyncMethodCallEvent…(this, …))` on the
object it is called on (`include/wx/event.h:3815-3855`), executed by the implicit entry in
`TryHereOnly` only when the event object is that handler (`src/common/event.cpp:1644-1667`).
Consequences:
- The functor is copied; method-form arguments are stored by value. Captures must be copyable.
- **Target destroyed first → the call is silently dropped**, and its captures are destroyed in
`DeletePendingEvents` (`src/common/event.cpp:1234-1238`). A `CallAfter` queued from the main thread on a window
needs no guard for that window itself.
- **`SetEvtHandlerEnabled(false)` drops pending calls**: `TryHereOnly` returns before the implicit
entry (`src/common/event.cpp:1646-1648`), and the event is consumed.
- Pushed handlers and `wxEventBlocker` do not block `CallAfter` (it is dispatched on the object).
- A TLW after `Destroy()` is not deleted yet: it is put on `wxPendingDelete` and hidden (off macOS, unless
it is the last visible TLW) (`src/common/toplvcmn.cpp:102-142`; wxOSX `src/osx/toplevel_osx.cpp`); pending events run before
the idle-time delete, so `CallAfter`s queued on it still execute on the hidden window.
`IsBeingDeleted()` is documented to cover "scheduled for destruction" (`interface/wx/window.h:3620-3633`)
but stays `false` until the real delete, because `m_isBeingDeleted` is set only by
`SendDestroyEvent` (`src/common/wincmn.cpp:535-557`). Use `wxTheApp->IsScheduledForDestruction(w)`
(`interface/wx/app.h:221`) or an alive flag.
- `wxApp::CallAfter` (Orca: `wxGetApp().CallAfter`) always runs — the app outlives every window — so
every object captured by pointer must be re-validated inside.
**The liveness rule.** Every `CallAfter` or other deferred lambda that touches a window, a model item
or any object other than the handler it was queued on must re-check liveness **inside** the lambda:
capture a `std::shared_ptr<std::atomic<bool>>` alive flag (set to `false` in the destructor), a
`std::weak_ptr` token or a `wxWeakRef`, or re-validate the target (registry lookup, model-item scan)
before touching it. Never capture a bare `this` or `wxDataViewItem` and trust it.
**Why:** the window or item can die between queueing and execution — dialog closed, plugin unloaded,
model rebuilt — and the deferred body then dereferences freed memory. Three fixes independently
added this guard. Self-queued work is the exception: `window->CallAfter(...)` is discarded unexecuted
when that window is destroyed, so the check is needed only for `wxGetApp().CallAfter(...)` and for
objects other than the window it was queued on. When a lambda touches only one window, queuing it on
that window gives the guard for free.
```cpp
// Wrong
CallAfter([this, item] { start_filament_editor(item); });
// Right — re-validate inside the lambda
CallAfter([this, item] {
if (!is_live_model_item(item)) return; // or: if (!alive->load()) return;
start_filament_editor(item);
});
// Right — app-queued work with an alive flag
wxGetApp().CallAfter([this, alive = m_alive, payload]() {
if (alive->load(std::memory_order_acquire))
handle_web_command(payload);
});
```
Cite: 0a0d59b76b (`src/slic3r/GUI/PluginsDialog.cpp/.hpp`, `PluginsDialog::m_alive`,
`PluginsDialog::on_script_message`), c965b2a5b3 (`src/slic3r/GUI/GUI_ObjectList.cpp`,
`ObjectList::is_live_model_item`), b779a7bfed (`src/slic3r/plugin/host/PluginHostUi.cpp`, the
`UiRegistry::is_open` re-check in `ui_create_window`).
**Liveness tools.**
| Tool | Use when | Limits |
|---|---|---|
| Queue on the target itself (`win->CallAfter`) | The lambda touches only `win` | Main thread; still runs for a `Destroy()`ed TLW until the idle delete |
| `std::shared_ptr<std::atomic<bool>>` alive flag (`PluginsDialog::m_alive`) | App-queued work, any thread | Flips at the start of the derived destructor |
| `std::weak_ptr` token (`MachineObject::add_command_error_code_dlg` with `m_token`, `DeviceManager.cpp`) | Non-window objects | — |
| `wxWeakRef<T>` (`interface/wx/weakref.h`) | Main-thread checks of a `wxEvtHandler`/window | Reset in `~wxTrackable`, i.e. after the derived destructor and `DestroyChildren`; tracker list unsynchronised (`include/wx/tracker.h`) — create and test on the main thread only |
| Registry / model re-validation (`UiRegistry::is_open`, `ObjectList::is_live_model_item` in macOS-only code) | Ids or items that may be rebuilt | `is_live_model_item` compares node pointers, so it cannot detect a freed node whose address was reused |
| `wxTheApp->IsScheduledForDestruction(w)` | Detect a `Destroy()`ed TLW still in `wxPendingDelete` | — |
| `GUI_App::is_closing()` | Callbacks during shutdown | Check before posting and again inside the lambda |
Deletion and `wxWeakRef` details: `references/windows-dialogs.md`.
**OrcaSlicer.**
- `run_on_ui_blocking` (`src/slic3r/plugin/host/PluginHostUi.cpp`) captures `fn` and the promise **by
reference** in `wxGetApp().CallAfter` and runs `fn` inline when `wxIsMainThread()` (a `CallAfter` +
`future.get()` on the main thread would deadlock). It is safe only because the caller blocks until
the lambda has run; never copy by-reference captures into a fire-and-forget `CallAfter`.
- Webview script messages arrive synchronously inside the native callback on WebKitGTK and WKWebView;
defer window work from them with `CallAfter` plus an alive check, and in a `WebViewHostDialog`
subclass do it in your own `on_script_message`, because the base dispatches synchronously. Details
and cites (b779a7bfed, f2ccbfc8b5, 0a0d59b76b): `references/webview-gl-aui-media.md`.
**Pitfalls.**
- **Rule:** Do not use `IsBeingDeleted()` to detect a `Destroy()`ed TLW. Use an alive flag or
`IsScheduledForDestruction`.
- **Rule:** Do not disable a handler with `SetEvtHandlerEnabled(false)` while it still has
`CallAfter`s it needs (§14).
---
## 9. Custom events and payload ownership
**Declaring.** `wxDECLARE_EVENT(NAME, Class)` in the header, `wxDEFINE_EVENT(NAME, Class)` in exactly
one `.cpp` — "this is a definition so can't be in a header" (`docs/doxygen/overviews/eventhandling.h:634-640`).
[source] `wxDEFINE_EVENT` expands to `const wxEventTypeTag<T> NAME(wxNewEventType())`
(`include/wx/event.h:105-106`). A namespace-scope `const` has internal linkage unless an `extern`
declaration (`wxDECLARE_EVENT`) precedes it, so a header definition alone gives every translation unit
a **different** event type: binds and posts silently never meet, and nothing fails to link.
`wxDECLARE_EXPORTED_EVENT` exists only for DLL export (`include/wx/event.h:110-115`).
**Choosing the class** (`docs/doxygen/overviews/eventhandling.h:600-613`): `wxEvent` (no payload, no propagation) or
`wxCommandEvent` (int, long, string, client data; propagates). For richer payloads derive a class,
add members and implement `Clone() { return new MyEvent(*this); }` (`docs/doxygen/overviews/eventhandling.h:686-741`);
event-table macros need extra boilerplate, `Bind` does not. `wxEvent::Clone()` is pure virtual
(`include/wx/event.h:1009`), so a direct `wxEvent` subclass without `Clone` does not compile — but
a subclass of a concrete event (`wxCommandEvent::Clone` returns `new wxCommandEvent(*this)`,
`include/wx/event.h:1655`) silently inherits a slicing `Clone`.
**Payload ownership.**
| Payload | Ownership | Cite |
|---|---|---|
| `wxCommandEvent::SetString/SetInt/SetExtraLong` | Copied / by value | `interface/wx/event.h` wxCommandEvent |
| `wxCommandEvent::SetClientData(void*)` | Raw pointer, never owned | — |
| `wxCommandEvent::SetClientObject(wxClientData*)` | "not owned by the event … must be owned and deleted by another object (e.g. a control) that has longer life time than the event object" | `interface/wx/event.h:2210-2216` |
| `wxEvtHandler::SetClientObject(wxClientData*)` | Owned by the handler: "Any previous object will be deleted"; deleted in `~wxEvtHandler`; do not mix with `SetClientData` on the same handler | `interface/wx/event.h:1062-1074`; [source] `src/common/event.cpp:1240-1242` |
| `Bind(…, userData)` | Owned by the binding, deleted on unbind or at exit | `interface/wx/event.h:900-905` |
| `wxThreadEvent::SetPayload<T>` | Copied; T's copy must not share state | `interface/wx/event.h:3775-3790` |
[source] For a `wxEVT_TEXT` event whose event object is a text-entry window,
`wxCommandEvent::GetString()` always returns the control's **current** value and ignores the stored
string (`src/common/event.cpp:430-447`). The copy constructor materialises the stored string
(`include/wx/event.h:1622-1632`), but a queued or cloned copy still has the event object, so its
`GetString()` reads the control live at handling time. If the value at emit time matters, capture it
yourself.
**OrcaSlicer.**
- Custom event types are declared in headers (`GLCanvas3D.hpp` and `Plater.hpp` in
`Slic3r::GUI`; widget events such as `EVT_SPINCTRL_TEXT`, `wxEVT_TAB_SEL_CHANGED` at global scope)
and defined once in the matching `.cpp`.
- Payload classes in `src/slic3r/GUI/Event.hpp`: `SimpleEvent`, `IntEvent` (`get_data()`),
`Event<T>` (`.data`), `ArrayEvent<T, N>` (`.data`). All derive from `wxEvent`, propagate (§5), and
their `Clone()` copies type, data and event object (not the id). `LoadPrinterViewEvent` there shows
the `wxCommandEvent`-derived shape: a copy constructor that copies the extra member, and
`Clone() { return new LoadPrinterViewEvent(*this); }`.
```cpp
wxDECLARE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event<int>); // GLCanvas3D.hpp
wxDEFINE_EVENT(EVT_GLCANVAS_INCREASE_INSTANCES, Event<int>); // GLCanvas3D.cpp
post_event(Event<int>(EVT_GLCANVAS_INCREASE_INSTANCES, +1)); // GLCanvas3D, posts on m_canvas
view3D_canvas->Bind(EVT_GLCANVAS_INCREASE_INSTANCES, [this](Event<int>& e) { /* e.data */ });
```
- Simple dialog-level events use `wxCommandEvent` with `SetInt`/`SetString`/`SetEventObject`
(`ReleaseNote.cpp` `EVT_SECONDARY_CHECK_*`; `MsgDialog::show_dsa_button` posts
`EVT_CHECKBOX_CHANGE` with `wxPostEvent(this, event)`).
**Pitfalls.**
- **Rule:** Never put `wxDEFINE_EVENT` in a header.
```cpp
// Wrong (header): one event type per .cpp that includes it
wxDEFINE_EVENT(EVT_MY_THING, wxCommandEvent);
// Right
wxDECLARE_EVENT(EVT_MY_THING, wxCommandEvent); // header
wxDEFINE_EVENT(EVT_MY_THING, wxCommandEvent); // one .cpp
```
- **Rule:** A posted custom event class overrides `Clone()` and copies every payload member.
**Why:** `wxPostEvent`/`wxQueueEvent(evt.Clone())`/`AddPendingEvent` clone the event; an inherited
`Clone` slices it to the base class, and the handler's cast to the derived type reads garbage.
---
## 10. Window and event ids
**Contract.**
- `wxID_ANY` in a constructor asks wx for an id; automatic ids "are always negative and so will never
conflict with the user-specified identifiers which must be always positive". Custom ids belong
above `wxID_HIGHEST` or below `wxID_LOWEST` (`docs/doxygen/overviews/eventhandling.h:863-875`),
"should not have values 0 or 1", and "they are not needed when using wxEvtHandler::Bind()" if you
bind on the control (`docs/doxygen/overviews/windowids.h:32-42`). Stock ids span `wxID_LOWEST`
(5000) to `wxID_HIGHEST` (6000) (`include/wx/defs.h:1786-1787, 1943`).
- `wxNewId()`: "@deprecated Ids generated by it can conflict with the Ids defined by the user code,
use wxID_ANY …" (`interface/wx/utils.h:433-444`). The header does not mark it deprecated, so there is
no compiler warning. [source] It starts at 100, skips the stock range and never reuses an id
(`src/common/utilscmn.cpp:654-663`).
- `wxWindow::NewControlId(count)` reserves auto ids (`interface/wx/window.h:4348-4376`; main thread).
**Platforms** [source]. `wxUSE_AUTOID_MANAGEMENT` defaults ON for Windows builds and OFF elsewhere
(`build/cmake/options.cmake:520-528`). With it (MSW), auto ids are reference-counted in
`-32000..-2000`, handed out upward from -32000, and once that range has been used up the allocator
scans for ids whose window or menu item is gone and **reuses** them — contradicting
`docs/doxygen/overviews/windowids.h:28-30` ("an ID that had never been returned by this function
before"); without it (macOS, GTK) they count down from -2000 to -1000000 and then wrap unchecked
(`src/common/windowid.cpp:187-251`, `include/wx/defs.h:1755-1772`). `wxMenuItem` holds its id as a `wxWindowIDRef`
(`include/wx/menuitem.h`). MSW native ids are 16-bit: `CreateBase` accepts `wxID_ANY`, `0..32766` or
the auto range (`src/common/wincmn.cpp:369-375`; the assert is compiled out in Orca, so an
out-of-range id fails silently).
**OrcaSlicer.**
- `append_menu_item`/`append_submenu` (`src/slic3r/GUI/wxExtensions.cpp`) assign `wxNewId()` for
`wxID_ANY` and bind handlers filtered by that id: `append_menu_item` binds `wxEVT_MENU` on the menu
(dies with it; on MSW on a non-null `event_handler` instead), and with a non-null `parent` both helpers bind `wxEVT_UPDATE_UI` on `parent`, never
unbound. That is safe only because `wxNewId` ids are never reused and the menus are built once; switching them
to `NewControlId` (recycled on MSW) would let stale handlers fire for new items. Menu event routing:
`references/popups-menus.md`.
- **Field window pools** (`src/slic3r/GUI/Field.cpp`, `Builder::build`, `free_window`; not on GTK,
where windows are deleted): option widgets are reused across page rebuilds, and `free_window`
unbinds every dynamic entry that was bound with an explicit id (on the widget and its inner
`wxTextCtrl`). Convention: `Field` handlers on pooled widgets pass the control id as the `Bind` id
(`…, temp->GetId())`); a handler bound without an id survives into the next `Field` that reuses the
widget and dangles. Widget-internal handlers use `wxID_ANY` and survive by design.
`GUI_App::recreate_GUI` releases the old pools through a `wxClientData` it hands to the old
`MainFrame` with `SetClientObject`, so they are freed when that frame's `~wxEvtHandler` runs (§9).
**Pitfalls.**
- **Rule:** Bind on the emitting control, not on an ancestor filtered by id.
**Why:** with recycled ids (MSW) a stale ancestor binding fires for an unrelated new control; an
unfiltered ancestor binding catches every descendant.
```cpp
// Wrong
panel->Bind(wxEVT_BUTTON, &MyPanel::on_ok, this, ok_btn->GetId());
// Right
ok_btn->Bind(wxEVT_BUTTON, &MyPanel::on_ok, this);
```
---
## 11. wxEVT_UPDATE_UI
**Contract** (`interface/wx/event.h:2444-2523`). Update-UI pseudo-events are sent "in idle time" from
`wxWindow::OnInternalIdle`; handlers call `evt.Enable/Check/Show/SetText`. Popup menus are updated
just before showing (`wxMenu::UpdateUI`). "On Windows and GTK+, events for menubar items are only
sent when the menu is about to be shown, and not in idle time." Default mode
`wxUPDATE_UI_PROCESS_ALL`, interval 0. Throttle with `wxUpdateUIEvent::SetMode(wxUPDATE_UI_PROCESS_SPECIFIED)`
plus `wxWS_EX_PROCESS_UI_UPDATES` on the windows that need it, or `SetUpdateInterval(ms)` with
`UpdateWindowUI()` at critical points (`interface/wx/event.h:2470-2479`; `interface/wx/window.h:286-288`).
**Source facts** [source].
- Each idle pass calls `UpdateWindowUI` for every window whose parent is shown on screen
(`src/common/wincmn.cpp:2810-2814`, `src/common/event.cpp:499-530` `wxUpdateUIEvent::CanUpdate`).
The event is a command event: unhandled, it bubbles to the TLW and `wxApp`, and every hop scans
that handler's whole dynamic table. Many `parent->Bind(wxEVT_UPDATE_UI, …, id)` entries on a frame
tax every idle pass.
- Menubar update-on-open applies to macOS too: `wxUSE_IDLEMENUUPDATES` is 0 for MSW, GTK and OSX
(`include/wx/platform.h:535-545`), except wxGTK with a global menu bar, which falls back to idle
updates (`src/common/framecmn.cpp:66-79`).
**Pitfall.**
- **Rule:** Keep update-UI handlers to cheap state reads; never do layout, I/O or model scans in them.
---
## 12. Idle events
**Contract** (`interface/wx/event.h:4383-4441`). Idle events are sent once when the loop becomes idle;
a continuous stream needs `RequestMore()` or `wxWakeUpIdle()`, and "both of these approaches (and
especially the first one) increase the system load". `wxIdleEvent::SetMode(wxIDLE_PROCESS_SPECIFIED)`
plus `wxWS_EX_PROCESS_IDLE` limits recipients. Documented: "The children of hidden windows do not
receive idle events". The "delayed action" idiom binds an idle handler that unbinds itself;
`CallAfter` is simpler.
**Source facts** [source].
- **Contradicts the doc:** `wxWindowBase::SendIdleEvents` recurses into every child without a
visibility check (`src/common/wincmn.cpp:2783-2808`); only update-UI skips children of hidden
windows (`src/common/event.cpp:508-513`). Idle handlers on hidden panels run.
- One `wxIdleEvent` object is reused for all windows in a pass; TLWs pending deletion are skipped,
and `DeletePendingObjects` runs from the app's idle processing (`src/common/appcmn.cpp:408-429`,
`src/common/appbase.cpp:442-462`). `wxYield()` runs pending events and one idle pass
(`src/common/evtloopcmn.cpp:172-192`) — so it can delete `Destroy()`ed TLWs under your feet.
**OrcaSlicer.** `DropDown::Create` binds an empty, non-skipping `wxEVT_IDLE` handler on macOS.
Because dynamic handlers run before the static table, this shadows `wxPopupTransientWindow::OnIdle`
(static table, `src/common/popupcmn.cpp:108-112, 439-471`) and its idle-time capture juggling, so the
capture taken in `Show` is held until the drop-down hides. `FanControlPopupNew` binds the same no-op,
but it is a `wxDialog`, which has no such idle handler to shadow. Popup mechanics:
`references/popups-menus.md`.
**Pitfall.**
- **Rule:** Use `wxTimer` (`StartOnce`) or `CallAfter` instead of idle handlers for periodic or
delayed work.
**Why:** idle fires once per wake-up, does not run inside MSW native modal loops (§7), runs for
hidden panels, and `RequestMore()` busy-loops a core.
---
## 13. Event filters
**Contract.** `wxApp::FilterEvent` and any `wxEventFilter` registered with
`wxEvtHandler::AddFilter` run for every event before anything else, in LIFO order, with wxApp
registered by default (`interface/wx/event.h:1195-1214`). Return `Event_Skip` (-1) to continue,
`Event_Ignore` (0) or `Event_Processed` (1) to stop (`interface/wx/eventfilter.h:85-93`). "having event
filters adds additional overhead to every event … return as quickly as possible"
(`interface/wx/eventfilter.h:18-20`). A standalone filter must be removed with `RemoveFilter` before it is
destroyed (`interface/wx/eventfilter.h:108-114`; misuse is silent in Orca).
**OrcaSlicer.**
- `GUI_App::FilterEvent` only timestamps user input (non-command `wxEVT_CATEGORY_USER_INPUT` events
and main-frame size events) and always returns `Event_Skip` — keep it that cheap.
- `SplashScreen::FilterEvent` (in `GUI_App.cpp`) returns `wxEventFilter::Event_Skip` to disable
`wxSplashScreen`'s own filter, which `Close()`s (and so `Destroy()`s) the splash on any key or mouse
press (`src/generic/splash.cpp:40-45, 105-126`) while `GUI_App::on_init_inner` still holds it
(as a `wxWeakRef<SplashScreen>`). Cite: 4088a36095.
---
## 14. Pushed handlers, wxEventBlocker, SetEvtHandlerEnabled
**Contract.**
- `PushEventHandler(h)`: `h` must not be part of another chain; events reach the most recently pushed
handler first and the window last (`interface/wx/window.h:2779-2809`). `PopEventHandler(deleteHandler
= false)` — an error with nothing pushed (`interface/wx/window.h:2759-2777`); `RemoveEventHandler(h)` removes from
the middle (`interface/wx/window.h:2811-2827`). `SetNextHandler` on a window is not supported — windows use the
stack (`interface/wx/window.h:2843-2851`).
- `wxEventBlocker(win, type = wxEVT_ANY)` discards events of the given types directed to `win`; `win`
"must remain alive until the wxEventBlocker object destruction" (`interface/wx/event.h:287-337`).
- `SetEvtHandlerEnabled(false)`: the handler's dynamic and static tables are skipped, processing
resumes at the chain step (`docs/doxygen/overviews/eventhandling.h:466-471`).
**Source facts** [source].
- **Pop before destroy.** `~wxWindowBase` asserts "any pushed event handlers must have been removed"
(`src/common/wincmn.cpp:468-472`) — compiled out in Orca, so a forgotten pushed handler is a
dangling pointer, not an assert.
- `wxEventBlocker` is a pushed handler whose `ProcessEvent` returns `true` for blocked types
(`src/common/event.cpp:2075-2103`). Its destructor pops whatever is on top; pushing another handler
inside its scope corrupts the stack. It only sees events dispatched through `GetEventHandler()` —
not posted events, `CallAfter`, or a direct `win->ProcessEvent`. Blocked command events count as
processed and do not propagate.
- `SetEvtHandlerEnabled(false)` does not affect handlers bound on other objects for this window's
events, validators, the chain, or propagation — and it drops queued `CallAfter`s (§8).
`wxWindow::Enable(false)` only stops native input; posted and synthetic events still reach the
handlers (`src/common/event.cpp:1644-1648` checks only the handler flag).
**OrcaSlicer.**
- `StateHandler` (`src/slic3r/GUI/Widgets/StateHandler.cpp`) is a `wxEvtHandler` pushed on every
`StaticBox`-based widget (member `StaticBox::state_handler`) and on attached children
(`attach_child`). It binds on itself, tracks enabled/checked/focused/hovered/pressed, always
Skips, and removes itself with `RemoveEventHandler` in its destructor. Member destruction runs
before `~wxWindowBase`, so it is gone in time. Call `remove_child(child)` before destroying an
attached child separately. Emitting through `GetEventHandler()` (§6) is what lets it see
synthesized events such as `EVT_ENABLE_CHANGED`.
- `PrinterWebView::~PrinterWebView` (`PrinterWebView.cpp`) and `WebViewPanel::~WebViewPanel`
(`WebViewDialog.cpp`) call
`SetEvtHandlerEnabled(false)` first, so events raised while the webview and members are torn down
do not reach handlers that touch freed members.
**Pitfall.**
- **Rule:** Pop pushed handlers in reverse push order, before the window dies.
```cpp
// Wrong: blocker2 pops blocker1's entry (or a foreign handler) — stack corrupted, no assert in Orca
auto* b1 = new wxEventBlocker(win); wxEventBlocker b2(win); delete b1;
// Right: nested scopes
{ wxEventBlocker b1(win); { wxEventBlocker b2(win, wxEVT_TEXT); /* … */ } }
```
---
## 15. Exceptions in handlers
**Contract.** `wxApp::OnExceptionInMainLoop` returns `true` to continue the loop or `false` to exit;
the default is to exit "in all ports except under Windows where a dialog is shown"; if it rethrows and
the exception cannot be stored, the program terminates (`interface/wx/app.h:465-480`). Since 3.3.0 the
system option `catch-unhandled-exceptions` set to 0 (environment variable
`wx_catch_unhandled_exceptions=0`) stops wx from catching unhandled exceptions, so the default abort
happens and the backtrace or crash dump is more likely to show where the exception came from
(`interface/wx/sysopt.h:17-23, 40-49`).
[source] If `OnExceptionInMainLoop` throws, `wxEvtHandler::WXConsumeException` exits the current loop
and stores the exception or aborts; the wx comment explains that exceptions "can't propagate through
the C GTK+ code and corrupt the stack" (`src/common/event.cpp:1688-1749`).
**OrcaSlicer.** `GUI_App::OnExceptionInMainLoop` calls `generic_exception_handle()`, whose every
path terminates or rethrows, so its `return false` is never reached: `std::bad_alloc` and
`boost::io::bad_format_string` show a message box and terminate; other `std::exception`s are logged,
reported with `wxLogError` and rethrown (non-`std` exceptions escape unlogged), so the main loop exits
and `wxEntry` ends with the fatal exit code. An exception that escapes any handler ends the session.
Unwinding order and exit code: `references/threads-timers-app.md` §Exceptions in the main loop.
**Pitfall.**
- **Rule:** Catch inside any handler (and any `CallAfter` lambda) that can throw — file, network,
JSON, config parsing — and report the error there; never let an exception cross a native callback.
---
## 16. How Orca widgets emit events
The per-widget table (event types, ids and event objects, which setters emit, where to bind) is
`references/orca-widgets.md` §Event semantics, with per-widget binding pitfalls in
`references/controls-dataview.md` §Orca replacement widgets. The event mechanics behind them:
- **Synchronous, through the handler stack.** Orca widgets emit with `GetEventHandler()->ProcessEvent` (§6), so
the pushed `StateHandler` (§14) sees the event and command events propagate to ancestors (§5). Exceptions:
`SwitchBoard` posts `wxCUSTOMEVT_SWITCH_POS` with `wxPostEvent(this, …)` from `SwitchBoard::on_left_down`
(asynchronous: the handler runs after the click handler returns; no event object, id 0), and `TempInput` posts
`wxCUSTOMEVT_SET_TEMP_FINISH` to its parent (`TempInput::SetFinish`).
- **Local re-dispatch.** `TextInput` and `SpinInput` catch the inner control's `wxEVT_TEXT_ENTER`/
`wxEVT_KILL_FOCUS` and re-send them with the wrapper's id through `ProcessEventLocally` (§6): the wrapper's
handlers run, its parents never see them. The inner `wxEVT_TEXT` propagates normally, with the inner control's id
and object, so bind it on the wrapper with `wxID_ANY`, not the wrapper's id.
- **Internal handlers run after yours** (§4). `Button` keeps LEFT_DOWN/LEFT_UP/MOUSE_CAPTURE_LOST/KEY_DOWN/KEY_UP/
PAINT in its static table (`wxEVT_BUTTON` comes from `Button::mouseReleased`; Space/Enter synthesize LEFT_DOWN/UP
in `Button::keyDownUp`); `::CheckBox` and `SwitchButton` bind their own `wxEVT_TOGGLEBUTTON` handler in the
constructor (clears the half state, refreshes the bitmap, Skips); `TextInput`'s handlers on the inner control run
`OnEdit` and the re-dispatch. A user handler bound on the widget (or on `GetTextCtrl()`) for those events must
`Skip()`. Capture-lost handling: `references/mouse-keyboard-focus.md`.
- **`EVT_ENABLE_CHANGED`** (`StateHandler.hpp`, a `wxCommandEvent` with id 0) comes from the `Enable` overrides of
`Button`, `SpinInput`, `RadioGroup` and the other `StateHandler` widgets. `StateHandler` Skips it, so it
propagates to ancestors like any command event.
- **Id-less events still propagate.** `ComboBox`'s `wxEVT_COMBOBOX_DROPDOWN`/`_CLOSEUP` (id 0, no event object),
`RadioGroup`'s `wxEVT_RADIOBOX` (`wxEVT_COMMAND_RADIOBOX_SELECTED` is an alias, `include/wx/event.h:4910`; no
event object) and `EVT_ENABLE_CHANGED` reach every ancestor bound by type. Bind on the specific widget, never on
an ancestor filtered by its id (§10).
- **Popup relay.** `DropDown` emits `wxEVT_COMBOBOX` on itself (`sendDropDownEvent`, its own id and object); as a
`PopupWindow` it stops an unconsumed event on MSW/macOS only (§5). `ComboBox` binds it on the `DropDown`,
consumes it and re-emits it with its own id and object.
- **Emitting setters.** `RadioGroup::SetSelection` (every call, same index included), `MultiSwitchButton::SetSelection`,
`TabCtrl::SelectItem` (CHANGING carries the old index and cannot veto: `sendTabCtrlEvent` always returns `true`),
`SpinInput::SetValue` (`EVT_SPINCTRL_TEXT` + `wxEVT_TEXT`), `Notebook::SetSelection` (PAGE_CHANGING/CHANGED, as in
wx) and, in an editable or `CB_NO_TEXT` combo, `ComboBox::SetSelection`/`SetValue` (`wxEVT_TEXT` through
`ComboBox::SetLabel`) emit synchronously. Guard model→view refreshes (`references/controls-dataview.md` §Events from programmatic changes).
@@ -0,0 +1,968 @@
# Mouse, keyboard and focus
How input reaches wx windows in the wxWidgets 3.3.2 build Orca ships, and the Orca conventions on
top: mouse capture, mouse and key events, accelerators and Orca's shortcut registry, focus,
tooltips and cursors. Read it when a widget captures the mouse or tracks hover, when adding or
changing a keyboard shortcut, when touching focus, tooltip or cursor code, and when debugging an
"alive but unclickable" UI, lost or phantom clicks, or shortcuts that fire while typing.
wx asserts are compiled out in Orca (`wxDEBUG_LEVEL=0`), so every misuse below that wx documents as
an assert fails silently. "GTK" means wxGTK3, Orca's Linux default (X11 and Wayland); GTK2 is only
an opt-out build (`-DDEP_WX_GTK3=OFF`), noted where it differs. Paths starting `interface/`,
`include/`, `src/`, `docs/` are in the wx tree
(located as in `SKILL.md` §Ground truth); Orca paths are
relative to `src/slic3r/GUI/`.
Contents: [Rules](#rules) · [Mouse capture](#mouse-capture) · [Mouse events](#mouse-events) ·
[Global pointer position and Wayland](#global-pointer-position-and-wayland) ·
[Hover handlers and enter/leave feedback loops](#hover-handlers-and-enterleave-feedback-loops) ·
[Keyboard events](#keyboard-events) · [Accelerators and menu shortcuts](#accelerators-and-menu-shortcuts) ·
[Orca's shortcut registry](#orcas-shortcut-registry) · [Focus](#focus) · [Tooltips](#tooltips) ·
[Cursors](#cursors)
## Rules
1. Capture only with `if (!HasCapture()) CaptureMouse();` and release only with
`if (HasCapture()) ReleaseMouse();`, at every site. → [Capture stack](#the-capture-stack)
2. On button-up, release whenever `HasCapture()` is true, never gated on a gesture flag that other
code can clear; decide whether to *commit* separately. → [Cancel-on-lost pattern](#the-cancel-on-lost-pattern)
3. Every window that captures handles `wxEVT_MOUSE_CAPTURE_LOST` by *cancelling*: reset state and
`Refresh()`. No commit, no `Skip()`, no `CaptureMouse()`, no unguarded `ReleaseMouse()`. Never route it
through the mouse-up/commit path. → [Cancel-on-lost pattern](#the-cancel-on-lost-pattern)
4. Release capture before `Hide()`, `Destroy()`/`delete` and in the destructor. →
[While captured](#modal-dialogs-popups-and-destruction-while-captured)
5. Never show a modal dialog, popup or message box from a handler that runs while the mouse is
captured: release your own capture, then `CallAfter` the dialog. →
[While captured](#modal-dialogs-popups-and-destruction-while-captured)
6. Write capture code as if `wxEVT_MOUSE_CAPTURE_LOST` did not exist on macOS (it is never sent there);
a leaked capture freezes every click in the app. → [macOS](#macos-rerouting-and-the-frozen-ui-diagnosis)
7. Mouse handlers take `wxMouseEvent&` (never by value) and `Skip()` `wxEVT_LEFT_DOWN` so focus still
moves. → [Button state and clicks](#button-state-and-clicks)
8. A handler that counts presses binds `wxEVT_LEFT_DCLICK` too: the second press of a double click
is a DCLICK, not a DOWN, on every port. → [Button state and clicks](#button-state-and-clicks)
9. Wheel: divide `GetWheelRotation()` by `GetWheelDelta()`, filter `GetWheelAxis()`, accumulate for
discrete steps. → [Wheel](#wheel)
10. On macOS, read button state on non-button events (motion, enter/leave, up, wheel) from
`wxGetMouseState()`, not the event. → [Button state and clicks](#button-state-and-clicks)
11. No global screen coordinates in logic that must work on Linux: no `wxGetMousePosition()`,
`wxFindWindowAtPoint()` or cross-window screen-rect tests. Use the event's client position,
`GetClientRect()` and focus tracking; guard unavoidable uses with `is_running_on_wayland()`. →
[Global pointer](#global-pointer-position-and-wayland)
12. Never use `wxGetKeyState()` for non-modifier keys (always false on Wayland); track
`wxEVT_KEY_DOWN`/`wxEVT_KEY_UP`. → [Global pointer](#global-pointer-position-and-wayland)
13. Enter/leave handlers only record hover state, per child window. Apply `Show()`/`Hide()` +
`Layout()` from `wxEVT_IDLE` or `CallAfter`, only when the state differs. No `wxFindWindowAtPoint()`
there, and no `IsShownOnScreen()` as a guard. → [Hover](#hover-handlers-and-enterleave-feedback-loops)
14. To see a child's keys, bind `wxEVT_CHAR_HOOK` on the parent and `Skip()` everything not
handled; `wxEVT_KEY_DOWN`/`wxEVT_CHAR` do not propagate. → [wxEVT_CHAR_HOOK](#wxevt_char_hook)
15. A top-level `wxEVT_CHAR_HOOK` must not consume printable keys, Space or text-editing chords
(Ctrl+A/C/V/X/Z, Delete, Backspace, Home/End, arrows) while a text entry has focus. →
[wxEVT_CHAR_HOOK](#wxevt_char_hook)
16. Test modifiers with `GetModifiers() == wxMOD_…`, not `ControlDown()`; on macOS `wxMOD_CONTROL` is
Cmd and `wxMOD_RAW_CONTROL` is the Control key. → [Modifiers](#modifiers-and-the-cmd-mapping)
17. Match letters, digits and special keys on `wxEVT_KEY_DOWN` and punctuation on `wxEVT_CHAR`;
build chords with `KeyChord::from_event`. → [Key codes](#key-codes)
18. New user-facing shortcuts go through the registry (`Shortcut` enum + `shortcut_table` row + a case in
the context's dispatcher). No raw `wxAcceleratorTable`, hard-coded key test or literal key name in a
label. → [Registry](#orcas-shortcut-registry)
19. Global chords need Ctrl/Alt or a key that types nothing. On macOS a live menu accelerator is
consumed by the menu bar before any wx key event, so the menu handler and the dispatcher case
must do the same thing. → [Accelerators](#accelerators-and-menu-shortcuts)
20. `SetAcceleratorTable()` replaces the window's table; build one table per window. It fires only
while focus is inside that window and stops at the top-level window. → [Accelerators](#accelerators-and-menu-shortcuts)
21. `SetFocus()` only on a user action or when the top-level window `IsActive()`; never from hover or
timers unconditionally, never inside `wxEVT_KILL_FOCUS` (defer with `CallAfter`). Focus handlers
`Skip()`. → [Focus](#focus)
22. Clear a tooltip with `UnsetToolTip()`, not `SetToolTip("")`. A composite that forwards tooltips
overrides `DoSetToolTip` as well as `DoSetToolTipText`. → [Tooltips](#tooltips)
23. Tooltips on disabled controls only through the MSW-gated parent-motion hack
(`Button::EnableTooltipEvenDisabled`); never install parent-motion forwarding on macOS. →
[Tooltips](#tooltips-on-disabled-controls)
24. Set a window cursor once with `SetCursor()`, reset with `wxNullCursor`, never toggle it on
enter/leave. Busy cursors via `wxBusyCursor` RAII, on the main thread. → [Cursors](#cursors)
## Mouse capture
### Contract
`CaptureMouse()` "Directs all mouse input to this window". wx "maintains the stack of windows having
captured the mouse … you must release the mouse as many times as you capture it, unless the window
receives the wxMouseCaptureLostEvent event. Any application which captures the mouse in the
beginning of some operation must handle wxMouseCaptureLostEvent and cancel this operation when it
receives the event. The event handler must not recapture mouse." (`interface/wx/window.h:3805-3819`)
`wxMouseCaptureLostEvent` goes to **all windows on the capture stack** when capture is lost to an
"external" event (a dialog box shown, another application capturing the mouse). It is "not sent if
the capture changes because of a call to CaptureMouse or ReleaseMouse"
(`interface/wx/event.h:3496-3514`). The doc's "currently emitted under Windows only" /
`@onlyfor{wxmsw}` is stale: wxGTK sends it too, wxOSX never does (table below).
`wxMouseCaptureChangedEvent` is MSW-only and is sent to a window that loses capture "even if
wxWindow::ReleaseMouse was called by the application code"; the doc's purpose: "allows an application
to cater for unexpected capture releases" (`interface/wx/event.h:4659-4680`; only `src/msw/window.cpp`
`wxWindowMSW::HandleCaptureChanged` builds it).
### The capture stack
[source] `src/common/wincmn.cpp:3322-3456` (namespace `wxMouseCapture`; the stack is a
`wxVector`, i.e. `std::vector`):
| Call | Behaviour |
|---|---|
| `CaptureMouse()` (:3352) | Asserts (compiled out) if `this` is already anywhere on the stack. Natively releases the current holder, `DoCaptureMouse()`, pushes `this`. A second call on the same window pushes it **twice**. |
| `ReleaseMouse()` (:3371) | Calls `DoReleaseMouse()` **first**, dropping the native capture whoever owns it. Then `wxCHECK_RET(stack.back() == this)` returns silently if this window is not on top. Pops, and if the stack is non-empty **re-captures the new top** (:3412-3416). |
| `HasCapture()` | `AsWindow() == GetCapture()` (`include/wx/window.h:1125`): true only for the top of the stack. |
| `NotifyCaptureLost()` (:3438) | Does nothing while a wx `Capture/ReleaseMouse` is in progress. Otherwise sends the lost event to each stacked window, top first, popping each. The stack ends empty, so no `ReleaseMouse()` is owed afterwards. A handler that leaves the event unprocessed (`Skip()`) hits a compiled-out `wxFAIL` (:3423-3436). |
| `~wxWindowBase` (:452) | Asserts (compiled out) if the window is still on the stack, and does **not** remove it: the stack keeps a dangling pointer. |
Consequences:
- Double capture followed by one release leaves the **same window captured** (the restore step
re-captures it). That is a leaked capture.
- An unguarded `ReleaseMouse()` from a window that is not on top frees the real owner's native
capture and leaves the stack out of step; a later release can re-capture a stale window.
- An unguarded `ReleaseMouse()` inside the lost handler pops the stack while `NotifyCaptureLost`
is iterating it. The loop then pops an entry it never notified, or calls `pop_back()` on an
empty vector (undefined behaviour). A `HasCapture()`-guarded release is a harmless no-op there
(next table).
- The documented way to break capture before a modal, `dialog.CaptureMouse(); dialog.ReleaseMouse();`
(`docs/doxygen/overviews/eventhandling.h:878-901`), is undone by the restore step when the
current holder captured through wx: the release re-captures the previous holder. It only breaks
a capture taken natively, outside wx's stack.
### Per-port delivery
| | MSW | GTK3 / GTK2 | macOS |
|---|---|---|---|
| Native capture | `SetCapture` | `gdk_seat_grab` (GTK ≥ 3.20) or `gdk_pointer_grab`. On an unrealized window `DoCaptureMouse` fails a silent `wxCHECK_RET`, but `wincmn` still pushes it (`src/gtk/window.cpp:6732-6765`) | None. `DoCaptureMouse` sets `wxApp::s_captureWindow` and wx reroutes events (`src/osx/window_osx.cpp:596-612`) |
| Capture-lost sent | `WM_CAPTURECHANGED` → `HandleCaptureChanged` → `NotifyCaptureLost`, then `wxEVT_MOUSE_CAPTURE_CHANGED` (`src/msw/window.cpp:5176-5192`) | GTK `grab-broken-event` (`src/gtk/window.cpp:2636-2646`). `wxDialog::ShowModal` and `wxMessageDialog::ShowModal` release the grab and notify (`src/gtk/dialog.cpp:137`, `src/gtk/msgdlg.cpp:285` → `GTKReleaseMouseAndNotify`) | **Never.** `src/osx` has no `NotifyCaptureLost` call |
| `HasCapture()` inside the lost handler | false: `GetCapture()` returns null while `gs_insideCaptureChanged` (`src/msw/window.cpp:757-768`) | false: `g_captureWindow` is cleared before notifying (`src/gtk/window.cpp:6794-6815`) | n/a |
| Modal dialog shown while captured | Lost event only if something takes the native capture (`WM_CAPTURECHANGED`) | Capture released, lost event sent | Capture kept; the dialog cannot be clicked (`src/osx/dialog_osx.cpp` `ShowModal` has no capture code) |
| Captor destroyed | wx stack dangles | `g_captureWindow` cleared (`src/gtk/window.cpp:3088-3089`); wx stack dangles | `s_captureWindow` dangles; the next mouse event dereferences it |
| `wxEVT_CHAR_HOOK` while captured | Not generated (any native `::GetCapture()`) | Not generated | Generated |
| Enter/leave while captured | Synthesized for the captor only (`src/msw/window.cpp:6019-6074`) | Synthesized for the captor only; grab crossings ignored (`src/gtk/window.cpp:2073-2117, 2384-2456`) | Other views' enter/exit events are rerouted to the captor |
### The cancel-on-lost pattern
```cpp
// EVT_MOUSE_CAPTURE_LOST(MyWidget::mouseCaptureLost) in the event table, or Bind(...)
void MyWidget::mouseDown(wxMouseEvent& e)
{
e.Skip(); // let focus move
m_pressed = true;
if (!HasCapture()) CaptureMouse(); // a second button mid-press must not push twice
Refresh();
}
void MyWidget::mouseReleased(wxMouseEvent& e)
{
e.Skip();
if (HasCapture()) ReleaseMouse(); // always, not only when m_pressed
if (!m_pressed) return;
m_pressed = false;
Refresh();
if (GetClientRect().Contains(e.GetPosition()))
sendButtonEvent(); // commit only on a real button-up inside
}
void MyWidget::mouseCaptureLost(wxMouseCaptureLostEvent&)
{
m_pressed = false; // cancel: no commit, no Skip(),
Refresh(); // no CaptureMouse(), no unguarded ReleaseMouse()
}
MyWidget::~MyWidget() { if (HasCapture()) ReleaseMouse(); }
```
Copy the `HasCapture()` guards from `Widgets/Button.cpp` `Button::mouseDown`/`Button::mouseReleased`,
but not the rest of that class's shape: `Button::mouseReleased` releases only inside its
`pressedDown` branch, and `Button::mouseCaptureLost` routes through `mouseReleased` (the commit
path, see Pitfalls). Take the cancel shape from `Widgets/SwitchButton.cpp`
`ModeSwitchButton::mouseCaptureLost`, minus its `Skip()`.
`Widgets/SpinInput.cpp` `SpinInput::createButton` shows the guards on `Bind` lambdas, with
`wxEVT_LEFT_DCLICK` bound next to `wxEVT_LEFT_DOWN`.
macOS never sends the lost event, so a widget that must survive an interrupted gesture there needs
stand-ins. Cancel and release when the top-level window reports `wxEVT_ACTIVATE` with
`GetActive() == false`, or when a `wxEVT_MOTION` arrives during the gesture while
`!wxGetMouseState().LeftIsDown()`.
### Modal dialogs, popups and destruction while captured
- "you shouldn't show a modal dialog from a mouse click event handler as this would break the mouse
capture state" (`interface/wx/event.h:490-499`). The overview adds that a modal shown while
captured "won't receive any mouse input and appear unresponsive"
(`docs/doxygen/overviews/eventhandling.h:878-901`). Release your own capture, then
`CallAfter([…]{ dlg.ShowModal(); })`. The same applies to events a capturing control emits mid-drag
(sash moves, list selection), and to `wxPopupTransientWindow::Popup()`, which takes capture itself on
macOS (see `references/popups-menus.md`).
- `Destroy()` or `delete` of a capturing window leaves a dangling stack entry (`~wxWindowBase`
never removes it), and on macOS a dangling `s_captureWindow`. `Hide()` leaves the hidden window
holding capture; on macOS every click then goes to it. Release first.
- Moving a top-level window from a custom title bar: prefer the window manager's drag over capture.
`BBLTopbar::OnMouseLeftDown` (`BBLTopbar.cpp`) posts `WM_NCLBUTTONDOWN`/`HTCAPTION` on MSW after a
`CaptureMouse(); ReleaseMouse();` pair and calls `gtk_window_begin_move_drag` on GTK. Its fallback
branch (capture, then `Move()` the frame from `OnMouseMotion`) never runs, because `MainFrame`
creates `BBLTopbar` only off macOS (`#ifndef __APPLE__` in the `MainFrame` ctor).
### macOS rerouting and the frozen-UI diagnosis
[source] `wxNSWindow`/`wxNSPanel` `sendEvent:` calls `WX_filterSendEvent:` first
(`src/osx/cocoa/nonownedwnd.mm:141-165`, called at :188 and :293). While `wxWindow::GetCapture()` is
non-null, every NSEvent of type `NSLeftMouseDown` … `NSMouseExited` goes straight to the capture
window's `wxWidgetCocoaImpl::DoHandleMouseEvent`, and `[super sendEvent:]` never runs. That covers
left/right down/up, moved, left/right dragged, entered and exited (types 1–9), on any wx window. AppKit
does no hit-testing at all, not even for the title-bar buttons. Scroll-wheel and other-button
(middle) events are not rerouted. Key events (type ≥ 10) never are.
Symptom of a leaked capture: the app repaints, logs and runs timers, but no click works anywhere,
including the window's own traffic-light buttons and any modal dialog. The keyboard still works:
Cmd+S and Cmd+Q still save and quit, so nothing needs to be force-killed. The capture is permanent,
because nothing on macOS unwinds the stack.
Diagnosis: run `sample <pid> 3` while moving the mouse over the window.
`WX_filterSendEvent:` → `wxWidgetCocoaImpl::DoHandleMouseEvent` on the main thread is the proof,
because that path is only reachable while a capture is held. Otherwise the main thread idles in
`mach_msg` with all threads clean, so do not hunt for a deadlock. Then audit every `CaptureMouse()`
site:
- the capture is guarded by `HasCapture()`;
- it is released whenever held, never behind a drag flag something else can clear;
- the window has a lost handler, plus macOS stand-ins where gestures can be interrupted;
- the window releases before hide/destroy and in its destructor.
Typical causes: an unguarded capture pushed twice (a second button, or a re-entered DOWN path);
a dialog opened mid-press; a stack-allocated dialog or popup destroyed while a child holds capture.
### OrcaSlicer
- `Button`, `DropDown` and `StepCtrl` in `Widgets/` capture in their own mouse handlers and declare
`EVT_MOUSE_CAPTURE_LOST` in their event tables, but their lost handlers share the
route-through-mouse-up shape the Pitfalls below call wrong; follow the cancel pattern above, not
them.
- `GLCanvas3D` guards every capture with `has_mouse_capture()` and releases in
`GLCanvas3D::mouse_up_cleanup()` (`if (m_canvas->HasCapture())`). `GLCanvas3D` is not a
`wxEvtHandler`: it binds on `m_canvas` in `bind_event_handlers()` and must unbind in
`unbind_event_handlers()`.
### Pitfalls
- **Rule:** Guard both calls.
**Why:** An unguarded `ReleaseMouse()` drops whichever window holds the native capture, then
silently skips the stack bookkeeping. An unguarded `CaptureMouse()` in a second DOWN branch pushes
the window twice, and the single release restores it: a leak, which on macOS is the frozen UI.
```cpp
// Wrong:
void up(wxMouseEvent&) { ReleaseMouse(); }
void down(wxMouseEvent&) { CaptureMouse(); } // in each of LEFT/RIGHT/MIDDLE_DOWN
// Right:
void up(wxMouseEvent& e) { e.Skip(); if (HasCapture()) ReleaseMouse(); }
void down(wxMouseEvent& e) { e.Skip(); if (!HasCapture()) CaptureMouse(); }
```
Cite: `src/common/wincmn.cpp:3352-3421`.
- **Rule:** Cancel on capture-lost; never commit.
**Why:** A default `wxMouseEvent` is at (0,0) (`src/common/event.cpp:576-581`), which is inside
`wxRect({0,0}, GetSize())`. Routing the lost event through mouse-up therefore fires `wxEVT_BUTTON`
when capture is lost mid-press (any `wxDialog::ShowModal` on GTK, an external capture change on
MSW), and in a list or dropdown it commits the hovered item.
```cpp
// Wrong: the lost event runs the commit path
void W::mouseCaptureLost(wxMouseCaptureLostEvent&) { wxMouseEvent e; mouseReleased(e); }
// Right:
void W::mouseCaptureLost(wxMouseCaptureLostEvent&) { pressedDown = false; Refresh(); }
```
Cite: `interface/wx/window.h:3814-3817`.
- **Rule:** Never `Skip()` in a lost handler.
**Why:** The event then counts as unprocessed, which is the case wx asserts on in debug builds
(`src/common/wincmn.cpp:3423-3436`).
- **Rule:** Never assume the lost handler will run.
**Why:** macOS never sends it. The `HasCapture()` guards and the up handler carry the whole release
logic there.
## Mouse events
### Coordinates
The position is in client coordinates "of the window which generated the event". Convert with
`ClientToScreen()` and then the other window's `ScreenToClient()` (`interface/wx/event.h:2782-2786`). While a window holds
capture, positions are relative to the capturing window and can be negative or outside its client
rectangle. On macOS they are converted from the event's NSWindow (`src/osx/cocoa/window.mm`
`wxSetupCoordinates`). `GetLogicalPosition(dc)` applies the DC's device origin, e.g. scrolling
(`interface/wx/event.h:3003`). A `wxGLCanvas` works in physical pixels, so `GLCanvas3D::on_mouse`
multiplies by the retina scale under `ENABLE_RETINA_GL`; see `references/webview-gl-aui-media.md`.
### Enter and leave
"the mouse is considered to be inside the window if it is over the window and not inside one of its
children … the parent window receives wxEVT_LEAVE_WINDOW event not only when the mouse leaves the
window entirely but also when it enters one of its children" (`interface/wx/event.h:2776-2780`).
Per port [source]:
- **MSW:** ENTER is synthesized on the first `WM_MOUSEMOVE`, so a click can arrive with no prior
ENTER; LEAVE comes from `TrackMouseEvent` (`src/msw/window.cpp:6019-6074`). Orca's
`GLCanvas3D::on_mouse` handles this as "Workaround for SPE-832" (`on_enter_workaround`): on MSW, a
non-enter event while the cached position is invalid is treated as the enter.
- **GTK:** crossing events with a grab/ungrab mode are ignored; outside capture wx re-derives the
window under the pointer on each motion (`src/gtk/window.cpp:2073-2117, 2384-2456`; fixes #24339
and #24931–#24933 are in this release, `docs/changes.txt:537, 547`).
- **macOS:** each view's `NSTrackingArea` uses `NSTrackingInVisibleRect`
(`src/osx/cocoa/window.mm:3924`) and covers its children, so do not rely on the parent getting
LEAVE when the pointer moves onto a child. `NSMouseMoved` is delivered only to the deepest view
under the pointer (`src/osx/cocoa/window.mm:1508-1517`).
A composite's hover state must therefore track its children. Orca's `StateHandler`
(`Widgets/StateHandler.cpp`, `StateHandler::attach_child`) keeps per-child state for this; see
`references/painting-custom-widgets.md`. Inside a `wxPopupTransientWindow` on macOS, Orca's
`PopupWindow` (created with `wxPU_CONTAINS_CONTROLS`) synthesizes ENTER/LEAVE itself; re-check geometry there
(`references/popups-menus.md` §6).
### Button state and clicks
- `LeftDown()` means "this event is the press"; `LeftIsDown()` means "the button is held now"; "if
wxMouseEvent::LeftDown returns true, wxMouseEvent::LeftIsDown will also"
(`interface/wx/event.h:2788-2799`). `Dragging()` is MOTION with any button down
(`include/wx/event.h:1850-1853`).
- **macOS:** button flags are filled only for Down/Dragged NSEvents (`mouseChord`,
`src/osx/cocoa/window.mm:645-700`). ENTER/LEAVE, plain motion, UP and wheel events report every
button as up. Use `wxGetMouseState()`, which reads `[NSEvent pressedMouseButtons]`
(`src/osx/cocoa/utils.mm` `wxGetMouseState`). `GLCanvas3D::on_mouse` back-fills the event from
`wxGetMouseState()` only when the event carries no button, "to preserve wx's synthetic right button
for Ctrl+left".
- **macOS Ctrl+click is a right click:** button 0 with `NSControlKeyMask` becomes the right button for
the whole down/drag/up sequence (`src/osx/cocoa/window.mm:664-686`; documented as an emulation hint at
`interface/wx/event.h:2772-2774`). `RawControlDown()` stays true on that `wxEVT_RIGHT_DOWN`, so a
`GetModifiers() == wxMOD_NONE` test fails on it.
- `wxEVT_LEFT_DOWN` handlers "should normally call event.Skip() … otherwise the window under mouse
wouldn't get the focus" (`interface/wx/event.h:2803-2805`). `Skip()` on a by-value copy is lost,
because dispatch checks the original event (`src/common/event.cpp:1459-1477`; see
`references/events.md`).
- **Double click:** on every port the second press arrives as `wxEVT_LEFT_DCLICK` **instead of**
`wxEVT_LEFT_DOWN`, giving DOWN, UP, DCLICK, UP. MSW maps `WM_LBUTTONDBLCLK` (window classes use
`CS_DBLCLKS`, `src/msw/app.cpp:584`). GTK drops the surplus press before `GDK_2BUTTON_PRESS`
(`src/gtk/window.cpp:1798-1817`), and GTK2 also suppresses triple clicks (`:1818-1829`). On wxOSX the
third press of a triple click is a DOWN again, as on MSW (`src/osx/cocoa/window.mm:4105-4140`;
#25886, `docs/changes.txt:316`). `GetClickCount()` is "implemented only in wxMac and returns -1
for the other platforms" (`interface/wx/event.h:2965-2974`).
- **Context menu:** "under MSW the context menu event is generated after EVT_RIGHT_UP … but under
GTK … after EVT_RIGHT_DOWN", so a window handling `wxEVT_CONTEXT_MENU` must not handle (or must
`Skip()`) both right-button DOWN and UP (`interface/wx/event.h:3306-3312`).
### Wheel
`GetWheelDelta()` is "normally 120", and "you shouldn't assume that one event is equal to 1 line"
(`interface/wx/event.h:3020-3055`). [source]:
| Port | `GetWheelDelta()` | `GetWheelRotation()` |
|---|---|---|
| MSW | `WHEEL_DELTA` | raw (`src/msw/window.cpp:6117-6118`) |
| GTK3 | 120 | `120 × delta` from `GDK_SCROLL_SMOOTH`, fractional streams (`src/gtk/window.cpp:2177, 2207-2241`) |
| GTK2 | 120 | ±120 per notch |
| macOS | **10** | precise scrolling deltas from trackpads; non-precise wheels ×10 (`src/osx/cocoa/window.mm:800-850`) |
A diagonal trackpad scroll on macOS sends two events, vertical first, then one with
`GetWheelAxis() == wxMOUSE_WHEEL_HORIZONTAL`. GTK3 smooth scrolling splits it too, horizontal first
(`src/gtk/window.cpp:2207-2241`).
### Pitfalls
- **Rule:** Take mouse events by reference.
**Why:** `Skip()` on a copy does not reach the dispatcher, so the event counts as handled; focus
and default processing stop.
```cpp
// Wrong:
w->Bind(wxEVT_LEFT_DOWN, [](wxMouseEvent e) { /*...*/ e.Skip(); });
// Right:
w->Bind(wxEVT_LEFT_DOWN, [](wxMouseEvent& e) { /*...*/ e.Skip(); });
```
- **Rule:** Bind DCLICK wherever presses are counted (spin arrows, steppers, toggles).
**Why:** every second press of a fast pair is a DCLICK; a DOWN-only handler loses it.
Cite: `Widgets/SpinInput.cpp` `SpinInput::createButton`.
- **Rule:** Don't use `evt.LeftIsDown()` in LEAVE, plain MOTION or UP handlers on macOS.
**Why:** the flags are only filled for Down/Dragged NSEvents.
```cpp
// Wrong: if (evt.Leaving() && evt.LeftIsDown()) keep_drag();
// Right: if (evt.Leaving() && wxGetMouseState().LeftIsDown()) keep_drag();
```
- **Rule:** Normalise wheel steps.
**Why:** the macOS delta is 10, and GTK3/trackpads send fractional streams. Dividing by a
hard-coded 120 makes a macOS wheel notch (rotation 10) 12× too slow; a fixed step per event races
or jitters on trackpad streams.
```cpp
// Wrong:
zoom += evt.GetWheelRotation() / 120.0; // or: rot > 0 ? step : -step per event
// Right:
if (evt.GetWheelAxis() != wxMOUSE_WHEEL_VERTICAL) return evt.Skip();
m_acc += double(evt.GetWheelRotation()) / evt.GetWheelDelta();
while (std::abs(m_acc) >= 1.0) { step(m_acc > 0 ? 1 : -1); m_acc -= (m_acc > 0 ? 1 : -1); }
```
## Global pointer position and Wayland
| API | Contract | GTK on Wayland [source] |
|---|---|---|
| `wxGetMousePosition()` | screen coordinates (`interface/wx/utils.h:360-365`) | `gdk_device_get_position` with no Wayland handling (`src/gtk/window.cpp:7026-7042`). Wayland exposes no global pointer position to clients [external]; Orca's comments record (0,0) |
| `wxGetMouseState()` | position, buttons and modifiers (`interface/wx/utils.h:367-375`) | position unreliable, same path (`src/gtk/window.cpp:2827-2867`) |
| `ClientToScreen()`, `ScreenToClient()`, `GetScreenRect()` | — | add the top-level window's `gdk_window_get_origin()` (`src/gtk/window.cpp:4630-4698`). Without a global origin the result is only meaningful relative to the same top-level window: fine for deltas and hit tests inside one window, wrong across windows or for absolute placement |
| `wxFindWindowAtPoint(pt)` | deepest window at a screen point; disabled children count, hidden ones are skipped (`interface/wx/utils.h:385-394`) | built on screen rectangles, so unreliable. On GTK it is `wxGenericFindWindowAtPoint`, a walk over every top-level window and child (`src/gtk/utilsgtk.cpp:98-101`, `src/common/utilscmn.cpp:1294-1345`) |
| `wxGetKeyState(key)` | "In wxGTK, this function can be only used with modifier keys … when not using X11 backend" (`interface/wx/utils.h:352-358`) | Ctrl/Alt/Shift and Caps/Num/Scroll Lock only; any other key returns false (`src/unix/utilsx11.cpp:2596-2662`) |
| `WarpPointer()` | Apple's HIG forbids it; on Wayland it works only with a compositor implementing the pointer-warp protocol, and mutter also needs a pressed button (`interface/wx/window.h:3895-3914`; `docs/changes.txt:294`) | — |
| `wxUIActionSimulator` | "doesn't work when using Wayland" (`interface/wx/uiaction.h:20`) | — |
| `PopupMenu(x, y)` | — | GTK ≥ 3.22 positions it relative to the window (`gtk_menu_popup_at_rect`, `src/gtk/window.cpp:6520-6565`), so it is safe |
On wxOSX `WarpPointer` synthesizes a `wxEVT_MOTION` to the window (`src/osx/window_osx.cpp:1370-1398`),
so warping from a motion handler recurses. On GTK2, pointer queries and warps go straight to X11.
**OrcaSlicer.** Detect the backend with `Slic3r::GUI::is_running_on_wayland()` / `is_running_on_x11()`
(`LinuxDisplayBackend.hpp`), never with environment variables (`references/platforms.md`). Models:
- `SearchDialog` (`Search.cpp`) dismisses by focus on Wayland (`focus_left_popup`) instead of
comparing `wxGetMousePosition()` with its screen rectangle.
- `BBLTopbar::FindToolByCurrentPosition` (`BBLTopbar.cpp`) uses the last event position and returns
null on Wayland rather than query the global pointer.
- `BBLTopbar::OnMouseMotion` and `Button::OnParentMotion` use `ClientToScreen(event.GetPosition())`
within one window ("wxGetMousePosition() … returns (0,0) on Wayland").
### Pitfalls
- **Rule:** On Linux, never rely on global screen coordinates.
**Why:** wx delegates straight to GDK and does not compensate on Wayland, so hit tests against
`wxGetMousePosition()` silently fail or misfire.
```cpp
// Wrong:
if (!GetScreenRect().Contains(wxGetMousePosition())) Dismiss();
if (wxFindWindowAtPoint(wxGetMousePosition()) != this) /* ... */;
// Right: event-relative, same window (convert through the event's GetEventObject() window)
if (!GetClientRect().Contains(evt.GetPosition())) Dismiss();
// or, for popups and dialogs, focus tracking (focus_left_popup is file-local to Search.cpp):
if (focus_left_popup(this, wxWindow::FindFocus(), related)) Dismiss();
```
- **Rule:** Track held keys with KEY_DOWN/KEY_UP, not `wxGetKeyState(letter)`.
**Why:** it is always false for letters on Wayland, so held-key and repeat detection built on it
(including pruning a held-key record with it) breaks there. Clear such a record from KEY_UP.
## Hover handlers and enter/leave feedback loops
**Rule.** In `wxEVT_ENTER_WINDOW`/`wxEVT_LEAVE_WINDOW` handlers, only record hover state, per child
window, because enter and leave fire per window. Apply `Show()`/`Hide()` + `Layout()` from
`wxEVT_IDLE` or `CallAfter`, and only when the desired state differs from the current one.
`Skip()` the events.
**Why.** A layout change inside the handler moves windows under the pointer, which re-fires
enter/leave. On Wayland compositors that keep hidden-workspace surfaces mapped (Hyprland), GTK
delivers a stream of synthetic leave events, and a handler that re-layouts pegs a CPU core
(69e16cd7ef). The fix that moved show/hide out of the handlers removed the freeze caused by the
dynamically hidden printer edit button (e87625e023). Two wx facts block the obvious guards:
- `wxFindWindowAtPoint()` is a full window-tree walk on GTK and unreliable on Wayland (table above).
- `IsShownOnScreen()` is not a visibility test on any platform. It only checks `IsShown()` up the
parent chain (`src/common/wincmn.cpp:1205-1212`, `interface/wx/window.h:3101-3102`), plus "surface
exists" for `wxGLCanvas` on Unix. It never reflects minimised, occluded or other-workspace state,
which is why `Plater::priv::set_current_panel` says "wxWidgets IsShownOnScreen() is buggy and
cannot be used reliably".
**OrcaSlicer.** The Sidebar printer, nozzle and bed panels (`Plater.cpp`, `Sidebar` ctor) follow
this design:
- each child's ENTER/LEAVE lambda inserts or erases the window in a per-group
`std::shared_ptr<std::unordered_set<wxWindow*>>` (e.g. `printer_preset_hovered`) and sets the border
colour;
- a `wxEVT_IDLE` handler compares `!hovered->empty()` with `btn_edit_printer->IsShown()` and calls
`Show()`/`Hide()` + `Layout()` only on a difference (keep such a handler a cheap comparison: it runs on every
idle pass, hidden panels included, `references/events.md` §12);
- clicking the edit button clears the set inside `CallAfter`, because opening the preset tab sends
no LEAVE (ff4147ede3), and because hiding a button from inside its own event handler crashed on
wxGTK.
```cpp
// Wrong: walks the tree and re-layouts inside the event (feedback loop)
w->Bind(wxEVT_LEAVE_WINDOW, [=](wxMouseEvent& e) {
if (!hit(wxFindWindowAtPoint(wxGetMousePosition()))) { btn->Hide(); panel->Layout(); }
e.Skip(); });
// Right: record, then apply once from idle
w->Bind(wxEVT_ENTER_WINDOW, [=](wxMouseEvent& e) { hovered->insert(w); e.Skip(); });
w->Bind(wxEVT_LEAVE_WINDOW, [=](wxMouseEvent& e) { hovered->erase(w); e.Skip(); });
Bind(wxEVT_IDLE, [=](wxIdleEvent& e) {
if (btn->IsShown() != !hovered->empty()) { btn->Show(!hovered->empty()); panel->Layout(); }
e.Skip(); });
```
Cite: 69e16cd7ef, e87625e023, ff4147ede3 (`Plater.cpp`).
## Keyboard events
### Event order per port
[source] One key press, in order:
| Port | Sequence |
|---|---|
| MSW | thread keyboard hook `wxKeyboardHook` → `wxEVT_CHAR_HOOK` to the focus window (or the active window) — skipped while any native `::GetCapture()` exists or a non-wx modal (IME) is open; with IME open a handled hook does not stop the key reaching the IME (`src/msw/window.cpp:7320-7395`) → `wxGUIEventLoop::PreProcessMessage`: accelerator walk from the focus up to the first `IsTopNavigationDomain(Navigation_Accel)` window, with the text-entry exemption (`src/msw/evtloop.cpp:62-119`) → `IsDialogMessage`, which is never given `VK_ESCAPE` (`src/msw/window.cpp:2769-2778`) → `wxEVT_KEY_DOWN` → `wxEVT_CHAR` |
| GTK | `wxEVT_CHAR_HOOK`, skipped while `g_captureWindow` is set → accelerator walk to the top-level window, no text-entry exemption → `wxEVT_KEY_DOWN` → input method → `wxEVT_CHAR` (`src/gtk/window.cpp:1266-1420`) |
| macOS | the main menu's `performKeyEquivalent:`: a menu-bar key equivalent consumes the key before wx sees it (`src/osx/cocoa/window.mm:1611-1644`) → `wxEVT_CHAR_HOOK`, also during capture → accelerator walk, only if no `wxEVT_CHAR_HOOK` handler processed the event (`src/osx/window_osx.cpp:2547-2590`) → Tab navigation by the first `wxTAB_TRAVERSAL` ancestor unless the window has `wxWANTS_CHARS` (`src/osx/cocoa/window.mm:4009-4046`) → `interpretKeyEvents` (IME) → `wxEVT_KEY_DOWN` → `wxEVT_CHAR` (`:4048-4101`). A disabled window gets no key events (`:1616-1617`) |
### wxEVT_CHAR_HOOK
Contract (`interface/wx/event.h:1487-1508`): "Unlike all the other key events, this event is
propagated upwards the window hierarchy … generated before any other key events". If a handler
processes it without `Skip()`, "neither wxEVT_KEY_DOWN nor wxEVT_CHAR events will be generated
(although wxEVT_KEY_UP still will be)". `DoAllowNextEvent()` handles it and still lets normal events
through (`:1683-1702`). It "is not generated when the mouse is captured". [source] Exceptions and
details:
- **macOS** has no capture check, so the hook fires during drags.
- **wxGTK** reads the allow flag from the original event rather than the hook event, so
`DoAllowNextEvent()` has no effect there (`src/gtk/window.cpp:1266-1282`).
- Propagation stops only at windows with `wxWS_EX_BLOCK_EVENTS` (`src/common/wincmn.cpp:3498-3522`).
`wxDialog` sets it (`src/common/dlgcmn.cpp:127`), and so does `wxPopupWindow` on MSW and macOS
(`wxPopupWindowBase::Create`, `src/common/popupcmn.cpp:135`). wxGTK's `wxPopupWindow::Create`
never calls the base, so a GTK popup does not block (`src/gtk/popupwin.cpp`). Frames do not
either: keys typed in a modeless frame (or a GTK popup) parented to `MainFrame` reach
`MainFrame`'s hook.
`wxEVT_KEY_DOWN`/`wxEVT_CHAR` are not command events and do not propagate
(`docs/doxygen/overviews/eventhandling.h:536-544`). If `wxEVT_KEY_DOWN` is handled without `Skip()`, "the
corresponding char event … will not happen … Not doing may also prevent accelerators defined using
this key from working" (`interface/wx/event.h:1456-1463`).
**OrcaSlicer — dialog keys.** `DPIAware<wxDialog>` (`GUI_Utils.hpp`) binds `wxEVT_CHAR_HOOK`: Esc
calls `Close()`, everything else is skipped. A dialog that needs Esc or Enter itself binds its own
hook. Later `Bind`s run first (`docs/doxygen/overviews/eventhandling.h:474-482`), so the dialog's
hook runs before `DPIAware`'s. Orca's `::Button` is not a `wxButton`, so wx's default-button and
Esc emulation (`EmulateButtonClickIfPresent`) never finds it. Dialogs synthesize the click instead,
as the `CloneDialog` ctor does for Enter in its count field:
```cpp
Bind(wxEVT_CHAR_HOOK, [this, ok_btn](wxKeyEvent& e) {
const int key = e.GetKeyCode();
if ((key == WXK_RETURN || key == WXK_NUMPAD_ENTER) && m_count_spin->GetTextCtrl()->HasFocus()) {
wxCommandEvent evt(wxEVT_BUTTON, ok_btn->GetId());
ok_btn->GetEventHandler()->ProcessEvent(evt);
} else
e.Skip();
});
```
The full Esc/close path is in `references/windows-dialogs.md`.
### Key codes
- Use `GetUnicodeKey()` for printable characters and `GetKeyCode()` for `WXK_*` specials
(`interface/wx/event.h:1329-1346`).
- `wxEVT_KEY_DOWN`/`UP`: ASCII letters are their upper-case code; other Latin-1 characters (`ù`,
`ö`, `²`) are the character itself, not upper-cased; keys producing non-Latin printable characters
report "the ASCII code of the character the same key would produce in the standard US keyboard
layout"; specials are their `WXK_*` (`:1371-1394`). A Cyrillic `ц` gives `'W'`, so Ctrl+letter
shortcuts work across layouts, but an AZERTY key reports its own label (`$` where US has `]`), so
`Ctrl-;`-style punctuation accelerators may be untypeable on some layouts (`:1396-1407`). wxGTK
got the non-Latin mapping in 3.3.0 (#23379, `docs/changes.txt:540`).
- `wxEVT_CHAR` reflects Shift and the layout. Ctrl+letter gives 1..26 (`WXK_CONTROL_A` …,
`:1409-1421`). Exception: on macOS Cmd+letter's CHAR is the letter itself (`'a'`/`'A'`); only the
physical Control key yields 1..26 (`src/osx/cocoa/window.mm:304-307`).
- Documented inconsistencies: Ctrl-Backspace, Ctrl-Enter, and on GTK no CHAR for Ctrl + a letter
mapped to a non-Latin one (`:1423-1433`). Modifier keys generate no CHAR (`:1435`).
- `IsAutoRepeat()` is `@onlyfor{wxosx,wxmsw,wxQt}`, not GTK (`:1595-1601`).
`KeyChord::from_event` (`KeyChord.cpp`) normalises all of this: it up-cases letters, folds numpad
keys onto the main keyboard, maps CHAR control codes 1..26 back to letters, and drops Shift from
CHAR punctuation, because the character already reflects it. Use it rather than testing raw codes.
### Modifiers and the Cmd mapping
| Query | MSW / GTK | macOS |
|---|---|---|
| `ControlDown()`, `wxMOD_CONTROL`, `wxACCEL_CTRL`, `"Ctrl+"` in accelerator strings | Ctrl | **Cmd** |
| `RawControlDown()`, `wxMOD_RAW_CONTROL`, `WXK_RAW_CONTROL`, `wxACCEL_RAW_CTRL`, `"RawCtrl+"` | Ctrl (same value as the above) | the physical Control key (a distinct bit) |
| `CmdDown()`, `wxMOD_CMD`, `wxACCEL_CMD` | deprecated aliases of `ControlDown()`/`wxMOD_CONTROL`/`wxACCEL_CTRL` | same |
Sources: `interface/wx/kbdstate.h:38-130`, `interface/wx/accel.h:15-30`, `include/wx/defs.h:2372-2388`,
`include/wx/accel.h:29-41`.
`interface/wx/kbdstate.h:45` claims `wxMOD_CMD` is `wxMOD_META` on Mac; the code defines `wxMOD_CMD = wxMOD_CONTROL`
everywhere [source]. Prefer `GetModifiers() == wxMOD_CONTROL`: `ControlDown()` alone is also true for
Ctrl+Shift, and for AltGr, which reports as Ctrl+Alt (`interface/wx/kbdstate.h:38-70`).
### wxWANTS_CHARS and navigation
- `wxWANTS_CHARS`: the window "get[s] all char/key events for all keys — even for keys like TAB or
ENTER"; call `Navigate()` yourself for Tab (`interface/wx/window.h:225-232`). On macOS wx's Tab
navigation is skipped for such windows, and otherwise Tab is consumed by the first
`wxTAB_TRAVERSAL` ancestor before `wxEVT_KEY_DOWN` (`src/osx/cocoa/window.mm:4009-4046`). The GL
canvas is created with it (`OpenGLManager::create_wxglcanvas`), so Tab and arrows arrive.
`GLCanvas3D::on_key` does not `Skip()` Tab and arrows and skips everything else "to have EVT_CHAR
generated".
- `wxTAB_TRAVERSAL` "should almost never be used in the application code"
(`interface/wx/window.h:221-224`). A composite with focusable children derives from
`wxNavigationEnabled<Base>` (`interface/wx/containr.h:10-46`).
- `Navigate(flags)` "is equivalent to calling NavigateIn() method on the parent"
(`interface/wx/window.h:2968-2988`). `HandleAsNavigationKey(evt)` (`:2715-2724`) and
`MoveAfterInTabOrder`/`MoveBeforeInTabOrder` (`:2949-2966`) complete the set.
- `DisableFocusFromKeyboard()` removes a window from the Tab chain but keeps click focus
(`interface/wx/window.h:497-506`). `SpinInput`'s arrow buttons use it.
**OrcaSlicer — `::Button`.** `Button::keyDownUp` turns Space/Return into LEFT_DOWN/UP and passes Tab
and arrows to `HandleAsNavigationKey`. The synthesized LEFT_DOWN runs `Button::mouseDown`, so a held
Space/Return holds the mouse capture until its KEY_UP reaches the button. On MSW
`Button::MSWWindowProc` answers `WM_GETDLGCODE` with `DLGC_WANTMESSAGE`.
### Pitfalls
- **Rule:** Catch children's keys with `wxEVT_CHAR_HOOK` on the parent.
```cpp
// Wrong: never sees keys typed in child controls
panel->Bind(wxEVT_KEY_DOWN, &P::on_key, this);
// Right:
panel->Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& e) { if (!handle(e)) e.Skip(); });
```
- **Rule:** A top-level hook lets text-editing keys through to a focused text entry.
**Why:** the hook runs before the text control and before MSW's text-entry accelerator
exemption. Eating bare printable keys, Space, Ctrl+C/V/X/A/Z, Delete, Home/End or arrows breaks
them in every field of the window.
```cpp
// Wrong:
Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& e) { if (e.GetKeyCode() == WXK_DELETE) delete_selection(); else e.Skip(); });
// Right:
Bind(wxEVT_CHAR_HOOK, [this](wxKeyEvent& e) {
if (e.GetKeyCode() == WXK_DELETE && !dynamic_cast<wxTextEntryBase*>(wxWindow::FindFocus())) delete_selection();
else e.Skip(); });
```
Cite: `MainFrame.cpp` `focus_keeps_space` (text entries, `wxWebView`, `::Button`, `StaticBox`
composites and `wxControl`s keep Space).
- **Rule:** Don't expect hook-based shortcuts during a drag on MSW/GTK, and guard drag state on
macOS, where they do fire.
- **Rule:** Ctrl+letter in `wxEVT_CHAR` is a control code.
```cpp
// Wrong: if (e.GetEventType() == wxEVT_CHAR && e.ControlDown() && e.GetKeyCode() == 'a')
// Right: if (KeyChord::from_event(e) == KeyChord{'A', wxMOD_CONTROL}) // or match on KEY_DOWN
```
- **Rule:** Modifier tests are exact.
```cpp
// Wrong: if (e.ControlDown() && e.GetKeyCode() == 'C') // also AltGr+C, Ctrl+Shift+C
// Right: if (e.GetModifiers() == wxMOD_CONTROL && e.GetKeyCode() == 'C')
```
## Accelerators and menu shortcuts
### Contract
- "An accelerator takes precedence over normal processing" (`interface/wx/accel.h:178`), but it
runs after `wxEVT_CHAR_HOOK` on every port (table above).
- [source] On GTK and macOS a hit is sent as `wxEVT_MENU` with the entry's id and, if unprocessed,
retried as `wxEVT_BUTTON`; macOS treats the key as consumed either way
(`src/gtk/window.cpp:1340-1366`, `src/osx/window_osx.cpp:2563-2590`). On MSW the `WM_COMMAND` goes to
a child window with that id if one exists (a `wxButton` clicks), otherwise it becomes `wxEVT_MENU`
(`src/msw/window.cpp` `wxWindowMSW::HandleCommand`).
- Matching up-cases a–z (`src/generic/accel.cpp:86-88`, `src/osx/accel.cpp:59`) and needs an
**exact** modifier match (`src/generic/accel.cpp:155-180`; `src/osx/accel.cpp:69-84`, which also
matches RawCtrl). Ctrl+Shift+Z needs its own entry.
- `SetAcceleratorTable()` **replaces** the window's single table; nothing merges. The walk runs from
the focused window up to the top-level window, so a table works only while focus is inside its
window, and a dialog never sees its parent frame's table [source].
- Menu strings take `"Label\tCtrl+X"`: modifiers `CTRL`/`RAWCTRL`/`ALT`/`SHIFT` joined by `+` or `-`,
plus the special key names listed in `interface/wx/menuitem.h:469-555`.
`wxAcceleratorEntry::FromString` accepts the bare accelerator or the legacy `"Label\tAccel"`.
`ToRawString()` is untranslated, for config files (`interface/wx/accel.h:124-146`).
`AddExtraAccel` is `@onlyfor{wxmsw,wxgtk}` (`interface/wx/menuitem.h:643-647`).
### Platforms
- **Menu accelerators exist only for menus attached to a frame's `wxMenuBar`** [source]. MSW merges
them in `wxMenuBar::RebuildAccelTable` (`src/msw/menu.cpp:1264-1292`); GTK adds the menu's accel
group to the top-level window in `AttachToFrame` (`src/gtk/menu.cpp:239-247`). A menu shown with
`PopupMenu` treats `"\tCtrl+X"` as display text.
- **MSW:** accelerators are not translated while a `wxTextCtrl`/`wxComboBox`/`wxSpinCtrl` has focus and
the key is a text-editing key: Ctrl+A/C/V/X/Ins/Del/Home/End/Left/Right, Shift+those navigation
keys, bare Del/Home/End, Alt+Backspace (`src/msw/textentry.cpp:1073-1150`). Multi-line controls also
keep Enter (`src/msw/textctrl.cpp:2110-2135`).
- **GTK:** the accelerator walk has no such exemption (`src/gtk/window.cpp:1340-1366`). As menu
accelerators, Shift with non-alphabetic keys does not work, bare arrow keys do not work, and the
listed keys (Tab, the modifiers, locks …) are unsupported (`interface/wx/menuitem.h:562-575`).
- **macOS:** menu-bar items become `NSMenuItem` key equivalents
(`src/osx/cocoa/menuitem.mm` `wxMacCocoaMenuItemSetAccelerator`). They run before any wx key event,
including while a text field has focus. A menu-bar accelerator on a printable key without a
modifier, or on a text-editing chord such as Cmd+C, takes that key from typing.
### OrcaSlicer menus
The native `wxMenuBar` exists only on macOS (`MainFrame::init_menubar_as_editor`, Preferences under
`OSXGetAppleMenu()`). On Windows and Linux the same `wxMenu`s hang off `BBLTopbar`
(`GetTopMenu()`, `SetFileMenu`, `AddDropDownSubMenu`), so their labels are display-only and the keys
are dispatched by the registry (`MainFrame`'s hook, the canvases). Add shortcut-bearing items with
`MainFrame::append_shortcut_item(menu, Shortcut::X, accelerator, label, …)`:
- `accelerator == true` and the binding is menu-safe (`ShortcutRegistry::accelerator()` is non-empty,
i.e. `KeyChord::is_menu_accelerator()`): the label is `label + "\t" + accel`, a live key equivalent
on macOS.
- Otherwise the binding is appended as text. The separator is `" - "` on Apple, so the macOS menu bar
does not take it as a key equivalent, and `"\t"` elsewhere (`MainFrame::shortcut_label`). The source
comment cites #8152: the macOS menu bar "handles the key accelerators automatically and breaks key
handling in normal typing". The macOS Edit menu shows clipboard and undo this way, so that Cmd+C in
a text field copies text rather than objects.
- `MainFrame::update_shortcut_labels()` rewrites every tracked item after a rebinding
(`GUI_App::on_shortcuts_changed`).
On Apple `MainFrame`'s hook keeps Cmd+H (consumed; the app menu hides), Cmd+M (`Iconize()`), Cmd+Q
(posts `wxEVT_CLOSE_WINDOW`) and Cmd+Ctrl+F (`EnableFullScreenView(true)` + `ShowFullScreen`
toggle). Preferences (Cmd+, / Ctrl+P) is a Global registry shortcut.
### Pitfalls
- **Rule:** One table per window, built once from all entries.
```cpp
// Wrong: the second call discards the first table
SetAcceleratorTable(wxAcceleratorTable(1, &copy)); SetAcceleratorTable(wxAcceleratorTable(1, &paste));
// Right:
wxAcceleratorEntry e[] = { copy, paste }; SetAcceleratorTable(wxAcceleratorTable(2, e));
```
- **Rule:** Don't expect `MainFrame`'s keys inside a dialog: each top-level window needs its own
handling (dialogs block `wxEVT_CHAR_HOOK` propagation and the accelerator walk stops at them).
## Orca's shortcut registry
The registry is the one table every key dispatcher, menu label, tooltip and the shortcuts dialog
reads; the design is in `docs/HLSD/keyboard-shortcuts.md`.
**Data model.**
- `KeyChord` (`KeyChord.hpp`) is `{key, modifiers}`: the key as `wxEVT_KEY_DOWN` reports it, plus
`wxMOD_*` limited to CONTROL|SHIFT|ALT|RAW_CONTROL. Its canonical text (`to_string()`, e.g.
`Ctrl+Shift+S`) is platform-neutral and doubles as the wx accelerator string and the config format.
`display()` uses translated modifier names and the macOS glyphs. Predicates:
- `needs_char_event()`: a printable non-alphanumeric key with at most Shift, resolvable only from
the CHAR that follows.
- `is_punctuation()`: such a key with no modifier.
- `is_menu_accelerator()`: Ctrl, Alt or RawCtrl held, or a non-printable key other than Space.
- `is_system_shortcut()`: Alt+F4 and Alt+Space on Windows.
- `to_accelerator_entry()` maps RawCtrl to `wxACCEL_RAW_CTRL` on Apple.
- `Shortcut` (enum) and `shortcut_table` (`Shortcuts.cpp`): each row, written with
`SHORTCUT`/`REPEATING`/`STEPPING`, holds a config key, description, context mask, default chord, and
the `repeatable`/`modifier_variants` flags. `static_assert`s keep the table in enum order and
`section_table` ascending.
- `ShortcutRegistry` (`wxGetApp().shortcuts()`) overlays user overrides from the AppConfig section
`shortcuts`; only overrides are stored, and `none` records an unbound shortcut.
`ShortcutRegistry::load()` drops a Global override that fails `is_menu_accelerator()` ("A Global
chord is seen before any text field").
- Lookups: `lookup(context, chord)` matches exactly. `match()` falls back to `modifier_variants`
shortcuts with Shift/Ctrl added, which are reserved steps (`step_owner()`). `conflicts()` treats
Global as sharing every context.
**Contexts and dispatch.**
| Context | Dispatcher | Event |
|---|---|---|
| `Global` | `MainFrame::handle_global_shortcut`, from `MainFrame`'s `wxEVT_CHAR_HOOK`, before the focused child gets KEY_DOWN/CHAR (a child's own CHAR_HOOK handler still runs first); `Skip()` otherwise | CHAR_HOOK |
| `Plater`, `Preview` | `GLCanvas3D::handle_shortcut` (→ `ShortcutRegistry::match`) from `GLCanvas3D::on_key`; punctuation retried from `GLCanvas3D::on_char` | KEY_DOWN, CHAR |
| `ObjectList` | `ObjectList::dispatch_shortcut`, from `ObjectList::key_event` on CHAR (non-macOS); on macOS from a `wxAcceleratorTable` built by `ObjectList::update_shortcut_accelerators()` (ids from `wxWindow::NewControlId`), because the native data view delivers no keys there | CHAR / accelerator |
| `Painting` | a painting gizmo's `on_tool_shortcut` (`GLGizmosManager`) | KEY_DOWN |
- A Global chord needs Ctrl/Alt or a non-typing key, because it is seen before any text field. Space
counts as typing: the speed dial's bare-Space default is skipped when `focus_keeps_space(FindFocus())`.
- Shortcuts in other contexts may share a chord when their contexts do not overlap.
- The macOS ObjectList table is swapped to `wxNullAcceleratorTable` while a name is being edited and
restored in `ObjectList::OnEditingDone`.
- wxGTK reports no auto-repeat, so the canvases keep one shared record of keys seen going down
(`key_repeats`/`key_released`) and swallow repeats of non-`repeatable` shortcuts.
**Adding a shortcut.**
1. Add the `Shortcut` value and its row in `shortcut_table`, in dialog order (a new section also needs a
`section_table` entry). Pick a default that does not collide inside its contexts; the `[Shortcuts]`
tests check every default.
2. Handle it in the context's dispatcher: `MainFrame::handle_global_shortcut`,
`GLCanvas3D::handle_shortcut`, `ObjectList::dispatch_shortcut`, or a gizmo's `on_tool_shortcut`. A
gizmo opened by a key sets `m_shortcut` in its `on_init()` (e.g. `GLGizmoMove3D::on_init`).
3. Where the UI shows the key, ask the registry: `display()` for tooltips, `accelerator()` (via
`append_shortcut_item`) for menus. Never write a literal key name.
`KBShortcutsDialog::fill_pages` lists registry entries automatically. Only non-rebindable keys and
mouse actions are added there by hand (`fixed(...)`, `mouse(...)`). Edits go through
`GUI_App::on_shortcuts_changed()`, which saves and pushes to menus, canvas tooltips and the macOS
ObjectList table.
**Model for capturing raw chords:** `ShortcutCaptureDialog` (`KBShortcutsDialog.cpp`).
- A `wxWANTS_CHARS` `StaticBox` keeps focus, so the buttons never get the keys; focus is set by
`capture->CallAfter(... SetFocus())` after construction.
- `wxEVT_CHAR_HOOK` on the dialog handles Esc/Enter, records KEY_DOWN chords and `Skip()`s
`needs_char_event()` chords.
- A `wxEVT_CHAR` handler on the box records punctuation.
- Its comment notes that the hook runs before the window procedure, so Windows does not open its
window menu on Alt+Space.
```cpp
// Wrong: ad-hoc key test plus a hand-written label
if (evt.GetKeyCode() == 'E' && evt.ControlDown()) export_gcode();
append_menu_item(menu, wxID_ANY, _L("Export") + "\tCtrl+E", ...);
// Right: registry row + dispatcher case + registry-derived label
case Shortcut::ExportSlicedFile: if (can_export_gcode()) wxPostEvent(m_plater, SimpleEvent(EVT_GLTOOLBAR_EXPORT_SLICED_FILE)); return false;
append_shortcut_item(export_menu, Shortcut::ExportSlicedFile, true, _L("Export plate sliced file") + dots, ...);
```
## Focus
### Contract
- `SetFocus()` "sets the window to receive keyboard input" (`interface/wx/window.h:567-572`).
`FindFocus()` is static (`:4274-4283`). `HasFocus()` also covers a composite's main child
(`:529-536`).
- `AcceptsFocus()` returning false means the control "doesn't accept input at all" (`:472-480`).
`AcceptsFocusFromKeyboard()` controls Tab-chain membership (`:482-488`). `CanAcceptFocus()` is
`AcceptsFocusRecursively() && IsShown() && IsEnabled()` (`include/wx/window.h:751-766`).
- `SetCanFocus()` "is only implemented by ports which have support for native TAB traversal … A call
to this does not disable or change the effect of programmatically calling SetFocus()"
(`interface/wx/window.h:538-548`).
- Focus handlers "should almost invariably call wxEvent::Skip() … wxEVT_KILL_FOCUS handler must not
call wxWindow::SetFocus()" (`interface/wx/event.h:3405-3416`). The event's `GetWindow()` "may be
NULL" (`:3438-3445`). `wxEVT_CHILD_FOCUS` derives from `wxCommandEvent` and propagates, and its
window is the *direct* child (`:3453-3491`).
- `wxPanel::SetFocus` focuses the first child if "the control has at least one child";
`SetFocusIgnoringChildren()` focuses the panel itself (`interface/wx/panel.h:125-142`).
- `wxGetActiveWindow()` "always returns NULL in the other ports", i.e. on everything but MSW and GTK
(`interface/wx/window.h:4545-4551`).
### Platforms
[source]
- **GTK:** `SetFocus()` calls `gtk_window_present()` on a visible, inactive top-level window, i.e. it
**raises and activates** the window (`src/gtk/window.cpp:5137-5162`). On a not-yet-shown widget the
focus becomes "pending", and `FindFocus()` returns the pending window at once (`:5141-5155,
2777-2791`). While a popup menu is open, `FindFocus()` returns the invoking window.
- **MSW:** disabling the focused control first `Navigate()`s forward (`src/msw/window.cpp`
`MSWEnableHWND`). Any `WM_SETFOCUS`, `WM_KILLFOCUS` or button-down in a window outside
`wxCurrentPopupWindow` dismisses a transient popup that lacks `wxPU_CONTAINS_CONTROLS`
(`src/msw/window.cpp:3018-3033` → `MSWDismissUnfocusedPopup`, `src/msw/popupwin.cpp:218-229`).
- **Modeless windows:** focus requested right after `Show()` can be dropped while activation is still
in flight (Orca comment in `SpeedDialWebDialog`'s ctor). Set it from `CallAfter` or on
`wxEVT_ACTIVATE` (`SpeedDialWebDialog`'s activate handler, see `references/popups-menus.md`); the
modal `ShortcutCaptureDialog` likewise defers its first `SetFocus()` with `CallAfter`.
### OrcaSlicer
- `::Button` overrides `SetCanFocus` to store `canFocus`, which drives `Button::AcceptsFocus()` and
whether `Button::mouseDown` calls `SetFocus()`. In Orca `SetCanFocus(false)` therefore means "never
take focus", unlike the stock GTK-only hint.
- `GLCanvas3D::on_mouse` focuses the canvas on any button-down. On ENTER it focuses the canvas only if
the top-level window `IsActive()` and focus is not in a `wxTextCtrl`, and on MSW only while
`wxCurrentPopupWindow` is null. Stealing focus would trigger `MSWDismissUnfocusedPopup` and close
the search dropdown.
### Pitfalls
- **Rule:** No unconditional `SetFocus()` on hover, ENTER or timers.
**Why:** on GTK it raises and activates the window over whatever the user is doing; on MSW it
dismisses open transient popups; anywhere it steals the caret from a text field.
```cpp
// Wrong:
canvas->Bind(wxEVT_ENTER_WINDOW, [=](wxMouseEvent& e) { canvas->SetFocus(); e.Skip(); });
// Right (wxCurrentPopupWindow is in no wx header: declare
// extern wxPopupWindow* wxCurrentPopupWindow; as GLCanvas3D.cpp does):
canvas->Bind(wxEVT_ENTER_WINDOW, [=](wxMouseEvent& e) {
auto* tlw = dynamic_cast<wxTopLevelWindow*>(wxGetTopLevelParent(canvas));
if (tlw && tlw->IsActive() && !dynamic_cast<wxTextCtrl*>(wxWindow::FindFocus())
#ifdef __WXMSW__
&& !wxCurrentPopupWindow
#endif
) canvas->SetFocus();
e.Skip(); });
```
Cite: `GLCanvas3D::on_mouse`.
- **Rule:** Defer focus changes out of `wxEVT_KILL_FOCUS`, and always `Skip()` focus events.
```cpp
// Wrong: ctrl->Bind(wxEVT_KILL_FOCUS, [=](wxFocusEvent&) { other->SetFocus(); });
// Right: ctrl->Bind(wxEVT_KILL_FOCUS, [=](wxFocusEvent& e) { e.Skip(); other->CallAfter([other] { other->SetFocus(); }); });
```
## Tooltips
### Contract
- `SetToolTip(const wxString&)`, `SetToolTip(wxToolTip*)` and `UnsetToolTip()`
(`interface/wx/window.h:3238-3273`). Setting an **empty string does not remove** the tooltip; the
code says "use SetToolTip(nullptr)" (`src/common/wincmn.cpp:2245-2259`) [source].
- The string overload reaches the virtual `DoSetToolTipText`; the pointer overload and `UnsetToolTip`
reach `DoSetToolTip` (`include/wx/window.h:1484-1489`).
- Statics (`interface/wx/tooltip.h:26-85`): `Enable` "may not be supported on all platforms";
`SetAutoPop`/`SetReshow` "May not be supported (eg. wxCocoa, GTK)"; `SetMaxWidth` is wxMSW-only.
- `wxTipWindow::New()` (3.3.2) returns a `wxTipWindow::Ref` that becomes null when the tip closes
itself; never keep a raw pointer (`interface/wx/tipwin.h:20-110`).
- `wxRichToolTip` is not a window: each `ShowFor()` creates a new one, and the native MSW version
only applies to text controls (`interface/wx/richtooltip.h:70-192`).
| Static [source] | MSW | GTK | macOS |
|---|---|---|---|
| `Enable` | works | sets `GtkSettings` `gtk-enable-tooltips` | no-op |
| `SetDelay` | works | sets `gtk-tooltip-timeout` (`src/gtk/tooltip.cpp:74-128`) | writes `NSInitialToolTipDelay` into `[NSUserDefaults standardUserDefaults]`, the app's persistent defaults (`src/osx/cocoa/tooltip.mm:66-72`) |
| `SetAutoPop`, `SetReshow` | work; Orca's `MainFrame` ctor uses `SetAutoPop(32767)` because larger values fail | no-op | no-op |
### Tooltips on disabled controls
MSW tooltips subclass the tool's HWND (`TTF_SUBCLASS`, `src/msw/tooltip.cpp:131-135`), and a
disabled HWND receives no mouse messages [external], so a disabled control shows no tooltip.
`Button::EnableTooltipEvenDisabled()` (`Widgets/Button.cpp`) works around this:
- it binds the **parent's** `wxEVT_MOTION`/`wxEVT_LEAVE_WINDOW` (`Button::OnParentMotion`/
`Button::OnParentLeave`);
- it pops a `wxTipWindow::Ref` when the pointer is over the disabled button;
- it positions the tip from `ClientToScreen(wxPoint(0, 0))` rather than `wxGetMousePosition()`, which
returns (0,0) on Wayland;
- it is compiled only under `#if defined(_MSC_VER) || defined(_WIN32)`.
- **Rule:** Install parent-window mouse-tracking handlers that simulate tooltips on disabled controls
only on Windows; gate them with `#if defined(_WIN32)`.
**Why:** the hack froze the UI on macOS. The commit records only the symptom. Likely mechanism
[source]: `wxTipWindow` is a `wxPopupTransientWindow`. On wxOSX its `Show(true)` captures the mouse
on its child without a guard, and `OnIdle` keeps capture while the pointer is outside the popup
(`src/common/popupcmn.cpp:335-430, 439-471`). A tip popped from parent MOTION, beside the pointer,
therefore takes every click (see the macOS capture section), and a re-`Popup()` while shown
pushes the capture twice.
```cpp
// Wrong: unconditional
parent->Bind(wxEVT_MOTION, &Button::OnParentMotion, this);
// Right:
#if defined(_MSC_VER) || defined(_WIN32)
parent->Bind(wxEVT_MOTION, &Button::OnParentMotion, this);
parent->Bind(wxEVT_LEAVE_WINDOW, &Button::OnParentLeave, this);
#endif
```
Cite: d0cc4b35ee (`Widgets/Button.cpp` `Button::EnableTooltipEvenDisabled`).
### OrcaSlicer
- `TextInput`, `SpinInput` and `TempInput` override `DoSetToolTipText` to forward the text to the
inner `wxTextCtrl`. Only the string overload forwards; `UnsetToolTip()` and
`SetToolTip(wxToolTip*)` do not reach the inner control.
- `Sidebar::priv::show_rich_tip` (`Plater.cpp`, `_WIN32` only) uses `wxRichToolTip` and recolours
the shown popup's first child for dark mode.
- Option tooltips for settings are assembled by the `Field` machinery (`references/orca-settings-ui.md`).
### Pitfalls
- **Rule:** `UnsetToolTip()` to clear.
```cpp
// Wrong: btn->SetToolTip(""); // keeps an empty tooltip object
// Right: btn->UnsetToolTip();
```
- **Rule:** A composite forwarding tooltips overrides both virtuals, so `UnsetToolTip()` and the
`wxToolTip*` overload reach the inner children.
- **Rule:** Don't call `wxToolTip::SetDelay` on macOS casually: it persists in the user's defaults for
the app.
## Cursors
### Contract
- `SetCursor(c)` "also sets it for the children of the window implicitly"; `wxNullCursor` resets it to
the default (`interface/wx/window.h:3866-3882`). The system shows the window cursor whenever the
pointer is over the window, so set it **once**.
- For high DPI, use `SetCursorBundle()` (3.3.0) (`interface/wx/window.h:3884-3892`). A default `wxCursorBundle()` is
empty and means "no custom cursor", not a blank cursor (`interface/wx/cursor.h:305-317`).
- `wxSetCursor(bundle)` "Globally sets the cursor … overrides any cursor set for the individual
windows … until this function is called again with an empty cursor bundle"
(`interface/wx/gdicmn.h:1371-1389`). `wxSetCursor(wxNullCursor)` is that reset, through the implicit
`wxCursorBundle(const wxCursor&)` (`src/common/curbndl.cpp:174`) [source].
- `wxBusyCursor` is RAII around the nested `wxBeginBusyCursor`/`wxEndBusyCursor` counter, with
`wxIsBusy()` (`interface/wx/busycursor.h`). These are main-thread GUI calls.
- `wxBitmap(wxCursor)` is invalid on GTK/Wayland (`interface/wx/bitmap.h:393-395`).
### OrcaSlicer
- Dialog work uses `wxBusyCursor` RAII (e.g. `PhysicalPrinterDialog.cpp`).
- Jobs wrap their `process()` in `BusyCursored<Job>` (`Jobs/BusyCursorJob.hpp`). Its
`CursorSetterRAII` marshals `wxBeginBusyCursor`/`wxEndBusyCursor` to the main thread through
`ctl.call_on_main_thread`.
- Web dialogs bracket blocking work with `wxSetCursor(wxCURSOR_ARROWWAIT)` …
`wxSetCursor(wxNullCursor)` (`WebViewDialog.cpp`).
### Pitfalls
- **Rule:** Set the hover cursor once and reset with `wxNullCursor`.
**Why:** the system already switches cursors on enter/leave. Toggling by hand fights it, and an
explicit `wxCURSOR_ARROW` overrides the cursor inherited from the parent.
```cpp
// Wrong:
w->Bind(wxEVT_ENTER_WINDOW, [w](wxMouseEvent& e) { w->SetCursor(wxCURSOR_HAND); e.Skip(); });
w->Bind(wxEVT_LEAVE_WINDOW, [w](wxMouseEvent& e) { w->SetCursor(wxCURSOR_ARROW); e.Skip(); });
// Right:
w->SetCursor(wxCURSOR_HAND); // once; w->SetCursor(wxNullCursor) to drop it
```
- **Rule:** Busy cursors from worker threads go through the main thread
(`ctl.call_on_main_thread`, `CallAfter`); never call `wxBeginBusyCursor` from a worker.
@@ -0,0 +1,982 @@
# OrcaSlicer GUI architecture
The map of OrcaSlicer's GUI: which object owns what, how the app starts, rebuilds and shuts down,
how pages are built lazily, and where a new dialog, panel, sidebar control, setting, notification,
menu item or source file belongs. Read it before adding a component or when code has to reach
another part of the GUI; the API detail of each area lives in the file the section points to.
Contents: [The stack](#the-stack-orcas-gui-is-built-on) ·
[Component map](#component-map) · [GUI_App](#gui_app) ·
[Close and shutdown](#close-and-shutdown-sequence) · [MainFrame](#mainframe) ·
[Deferred construction](#deferred-construction-lazy-lazypage-stagedbuild-idlescheduler) ·
[Plater and Sidebar](#plater-and-sidebar) · [Settings placement](#settings-placement-paramspanel-paramsdialog-tabs) ·
[ObjectList](#objectlist) · [3D canvas and ImGui](#3d-canvas-imgui-layer-and-notificationmanager) ·
[Background work](#background-work) · [Device pages](#device-and-monitor-pages) ·
[Web UI](#web-based-ui) · [Preferences](#preferences) · [AppConfig](#appconfig) ·
[Where new code goes](#where-new-code-goes) · [Build registration](#build-registration) ·
[Design docs](#design-docs-docshlsd)
## Rules
1. Reach app-wide objects through `wxGetApp()`. `app_config` is non-null for the whole GUI
lifetime; `plater()` and `mainframe` can be null, and `sidebar()`, `obj_list()`, `model()`
dereference the plater unchecked — test `plater()` first on any path that can run before the
main frame exists or after it closes. → [Accessors](#accessors)
2. Deferred code (CallAfter bodies, agent callbacks, timers) that can run during shutdown checks
`!wxTheApp || wxGetApp().is_closing()` before touching the GUI. → [Close and shutdown](#close-and-shutdown-sequence)
3. Never keep a raw pointer to a `MainFrame` child, a `Tab`, a lazy panel or a cached dialog across
`GUI_App::recreate_GUI` (language switch); `is_closing()` stays false during it. → [recreate_GUI](#recreate_gui-language-switch)
4. A new top-level tab is a `LazyPage<Panel>` with a `LazyInstance<Panel>` panel; a heavy dialog
owned by the main frame is a `Lazy<Dlg>` member. Only the start page and the Prepare plater are
built before the first frame. → [Deferred construction](#deferred-construction-lazy-lazypage-stagedbuild-idlescheduler)
5. Outside `MainFrame`, reach a lazy object only through its statics: `if_built()` for work it can
live without, `ensure()` only to show or navigate to it, `when_built()` for state it would not
pull for itself (and for rescale/recolour of staged panels). → [Reaching a lazy object](#reaching-a-lazy-object)
6. In a `StagedBuild` panel, step-built members start null; timers, handlers and the destructor
check `built()` first; nothing takes focus while off screen. → [Staged construction](#staged-construction-stagedbuild)
7. Background UI construction is a prebuild task run by `IdleScheduler`, never a `wxEVT_IDLE` +
`RequestMore()` loop or a chain of posted events. → [The idle scheduler](#the-idle-scheduler-idlescheduler-prebuildqueue)
8. A component with Orca rescale / recolour hooks must be reached by the explicit fan-out
(`MainFrame::on_dpi_changed` / `on_sys_color_changed`, `Plater::msw_rescale` /
`sys_color_changed`, `Sidebar::msw_rescale` / `sys_color_changed`); nothing calls it otherwise. → [DPI and colour fan-out](#dpi-and-colour-fan-out)
9. Process and model-scope settings are in `ParamsPanel` inside the sidebar; filament and printer
settings are in the modeless `ParamsDialog`. `get_tab()` returns null until the tab is complete. → [Settings placement](#settings-placement-paramspanel-paramsdialog-tabs)
10. `Plater` and `Sidebar` are pimpl'd: new state goes into `Plater::priv` / `Sidebar::priv`. New
events are declared next to their emitter with the `Event.hpp` types; a short-lived listener on
the plater binds through `EventGuard`. → [Plater and Sidebar](#plater-and-sidebar)
11. Docked panes go through `Plater::add_dock_pane` with a stable, untranslated, delimiter-free name;
the window must be a child of the plater. → [Docking](#docking)
12. UI drawn over the 3D view is ImGui inside `GLCanvas3D`, never a wx child window over the GL
canvas; ask for a redraw with `set_as_dirty()` / `request_extra_frame()`. → [3D canvas](#3d-canvas-imgui-layer-and-notificationmanager)
13. `NotificationManager` is called on the UI thread only, and its notifications are visible only
while the plater is shown. → [NotificationManager](#notificationmanager)
14. UI-initiated background work is a `Job` on a `Worker`; slicing is `BackgroundSlicingProcess`;
network agents call back through `wxGetApp().CallAfter`. UI is touched only on the main thread. → [Background work](#background-work)
15. Device UI pulls state from `DeviceManager` on its own timer and mutates `MachineObject` only on
the UI thread. → [Device pages](#device-and-monitor-pages)
16. Web UI goes through Orca's hosts (`WebView::CreateWebView`, `WebViewHostDialog`, `WebPanel`,
`DockPanel`); window operations requested from a script message are deferred and liveness-checked. → [Web UI](#web-based-ui)
17. A preference is a `create_item_*` row in `PreferencesDialog::create_items` that writes
`app_config` and saves at once; its default goes in `AppConfig::set_defaults`; effects needed
after the dialog closes go in `GUI_App::open_preferences`. → [Preferences](#preferences)
18. `AppConfig` values are strings: match the key's own convention (`"true"/"false"` or `"1"/"0"`);
`save()` and every write run on the main thread. → [AppConfig](#appconfig)
19. Every new source file is registered in `src/slic3r/CMakeLists.txt`: `SLIC3R_GUI_SOURCES`, or the
`if (WIN32)` / `if (APPLE)` / `if (SLIC3R_CAD)` blocks, or the `GUI/DeviceCore` / `GUI/DeviceTab`
lists. → [Build registration](#build-registration)
20. A new subsystem whose design is not evident from the code gets `docs/HLSD/<subsystem>.md`; a
change that invalidates an existing HLSD doc updates it in the same PR. → [Design docs](#design-docs-docshlsd)
## The stack Orca's GUI is built on
- **wxWidgets 3.3.2, SoftFever fork.** `deps/wxWidgets/wxWidgets.cmake` fetches
`https://github.com/SoftFever/Orca-deps-wxWidgets` at tag `v3.3.2` and builds it static
(`-DwxBUILD_SHARED=OFF`); Flatpak builds build it shared. Linux builds against **GTK3** by default
(`option(DEP_WX_GTK3 "Build wxWidgets against GTK3" ON)` in `deps/CMakeLists.txt`, `SLIC3R_GTK`
default `"3"`, Flatpak uses gtk3). GTK2 exists only as an opt-out (`-DDEP_WX_GTK3=OFF`) and loses
EGL, WebKit2 and DIP pixels. Code guarded for GTK should still compile on GTK2, but GTK3 under X11
and Wayland is the target. Toolkit and build-option detail: `references/platforms.md`.
- **Asserts are compiled out.** wx is built with `-DwxBUILD_DEBUG_LEVEL=0` and `libslic3r_gui` adds
`wxDEBUG_LEVEL=0` (under `SLIC3R_STATIC`, `src/slic3r/CMakeLists.txt`). `wxASSERT`/`wxFAIL` vanish
and `wxCHECK*` return silently, so API misuse shows up as wrong pixels, dropped calls or corrupted
state, never as an assert dialog.
- **No wx SVG.** `-DwxUSE_NANOSVG=OFF`: `wxBitmapBundle::FromSVG*` does not exist; Orca rasterises
SVG itself (`BitmapCache`, `create_scaled_bitmap`) — `references/dpi-bitmaps-fonts.md`.
- **Orca's own widget library.** New UI code largely does not use raw wx controls: the owner-drawn
widgets in `src/slic3r/GUI/Widgets/` (`Button`, `CheckBox`, `ComboBox`, `TextInput`, `SpinInput`,
`SwitchButton`, `RadioGroup`, `Label`, `DialogButtons`, …) replace them. Reasons: native controls
cannot follow Orca's look or its app-level dark-mode toggle; on Windows wx's native dark mode does
not reach anything built on `TaskDialog()` (`wxMessageBox`, `wxMessageDialog`, `wxRichMessageDialog`,
`wxProgressDialog`) nor the wrapped common dialogs (`wxColourDialog`, `wxFontDialog`, …)
(`interface/wx/app.h:1434-1443`), so Orca shows the `MsgDialog` family and its own generic
`Widgets/ProgressDialog` instead; and on GTK the theme's borders bleed through wrapped native
controls (the widgets call `RemoveButtonBorder` / `RemoveInputBorder` under `__WXGTK__`).
Plain containers stay raw (`wxPanel`, `wxBoxSizer`, `wxScrolledWindow`).
- **Namespaces.** Most widgets are in the global namespace; a few (`DialogButtons`, `HyperLink`,
`ProgressDialog`, `RadioBox`, `WebViewHostDialog`, the AMS/device composites) are in
`Slic3r::GUI`. GUI code inside `Slic3r::GUI` writes `::CheckBox` because `Field.hpp` declares the
settings-field classes `Slic3r::GUI::CheckBox`, `TextCtrl`, `SpinCtrl`, `Choice`, `StaticText`,
which an unqualified name finds first once `Field.hpp` is reachable; `::TextInput` and
`::ComboBox` are qualified the same way by convention (no `Slic3r::GUI` class shadows them).
Catalog and quirks: `references/orca-widgets.md`.
## Component map
| Component | Type, file | Owns / does | Reach it with |
|---|---|---|---|
| `GUI_App` | `wxApp`; `GUI/GUI_App.hpp/.cpp` | process singletons, startup, `post_init`, app idle handler, dark-mode entry points, `recreate_GUI` | `wxGetApp()` |
| `MainFrame` | `DPIFrame`; `GUI/MainFrame.hpp/.cpp` | borderless main window, top bar / menu bar, tab book, preset tabs, idle prebuild, DPI/colour fan-out | `wxGetApp().mainframe` |
| `Plater` | `wxPanel`, pimpl `Plater::priv`; `GUI/Plater.hpp/.cpp` | the Prepare and Preview page: model, three canvases, AUI docking, slicing, job worker, notifications, context menus | `wxGetApp().plater()` |
| `Sidebar` | `wxPanel`, pimpl `Sidebar::priv`; `GUI/Plater.hpp/.cpp` | printer and filament blocks, `ParamsPanel`, object search + `ObjectList`, settings index | `wxGetApp().sidebar()` (unchecked) / `plater()->sidebar()` |
| `ParamsPanel` | `wxPanel`; `GUI/ParamsPanel.hpp` | process and model-scope `Tab`s, reparented into the sidebar | `wxGetApp().params_panel()` (null-safe) |
| `ParamsDialog` | `DPIDialog`; `GUI/ParamsDialog.hpp` | its own `ParamsPanel` with the filament and printer `Tab`s; modeless | `wxGetApp().params_dialog()` (null-safe) |
| `Tab` family | `GUI/Tab.hpp/.cpp` | preset editors (`TabPrint`, `TabPrintPlate/Object/Part/Layer`, `TabFilament`, `TabPrinter`) | `get_tab(Preset::Type)`, `get_plate_tab()`, `get_model_tab(part)`, `get_layer_tab()` |
| `ObjectList` | `wxDataViewCtrl`; `GUI/GUI_ObjectList.hpp` | plate/object/part tree | `wxGetApp().obj_list()` (unchecked) |
| `GLCanvas3D` | wraps a `wxGLCanvas`; `GUI/GLCanvas3D.hpp` | 3D, preview and assemble rendering, gizmos, ImGui overlays | `plater()->canvas3D()`, `get_current_canvas3D()` |
| `NotificationManager` | `GUI/NotificationManager.hpp` | ImGui notifications drawn in the canvas | `wxGetApp().notification_manager()` (null-safe) |
| Jobs | `GUI/Jobs/` | UI-initiated background tasks | `plater()->get_ui_job_worker()` |
| `MonitorPanel` / `StatusPanel` | `GUI/Monitor.hpp`, `GUI/StatusPanel.hpp` | Device tab | `MonitorPanel::if_built()` / `ensure()` |
| `DeviceManager` / `MachineObject` | `GUI/DeviceCore/DevManager.h` (`DeviceManager`), `GUI/DeviceManager.hpp` (`MachineObject`), parts in `GUI/DeviceCore/Dev*` | device state | `wxGetApp().getDeviceManager()` |
| `NetworkAgent` | `Utils/NetworkAgent.hpp` | printer agent + cloud agents façade | `wxGetApp().getAgent()` |
| Web hosts | `Widgets/WebView`, `Widgets/WebViewHostDialog`, `WebViewDialog.hpp` (`WebViewPanel`), `PrinterWebView`, `WebPanel`, `DockPanel`, `WebDialog` | HTML UI | per class |
| `PreferencesDialog` | `DPIDialog`; `GUI/Preferences.hpp` | app settings | `wxGetApp().open_preferences(tab, highlight)` |
| `AppConfig` | `libslic3r/AppConfig.hpp` | persisted app settings | `wxGetApp().app_config` |
| `PresetBundle` | `libslic3r/PresetBundle.hpp` | presets | `wxGetApp().preset_bundle` |
| `ShortcutRegistry` | `GUI/Shortcuts.hpp` | key bindings | `wxGetApp().shortcuts()` |
| `ActionRegistry` | `GUI/ActionRegistry.hpp` | Speed Dial actions | `wxGetApp().action_registry()` |
| `ImGuiWrapper` | `GUI/ImGuiWrapper.hpp` | the app's ImGui context | `wxGetApp().imgui()` |
## GUI_App
### Entry and construction
`GUI_Run` (`GUI/GUI_Init.cpp`) creates the app by hand: `new GUI_App()`, then
`Slic3r::instance_check(argc, argv, single_instance)` using `app_config`, then
`GUI_App::SetInstance(gui)`, `gui->init_params = &params`, and `wxEntry`. When there are command-line
arguments it passes **only `argv[0]`** to `wxEntry`, because wx reports errors for some file names; the real
arguments travel in `GUI_App::init_params` (`GUI_InitParams`). `IMPLEMENT_APP(GUI_App)` is in
`GUI_App.cpp` and `DECLARE_APP(GUI_App)` in `GUI_App.hpp` inside `Slic3r::GUI`, so `wxGetApp()` is
`Slic3r::GUI::wxGetApp()` — write `GUI::wxGetApp()` from `Slic3r` scope outside `GUI`. It expands to
`*static_cast<GUI_App*>(wxApp::GetInstance())` (`include/wx/app.h:941` **[source]**), so it is valid from
`SetInstance` until wx cleanup nulls the instance.
The constructor (`GUI_App::GUI_App`) runs before `wxEntry`, i.e. before wx is initialised. It creates
the `ImGuiWrapper`, `RemovableDriveManager`, `Downloader`, `OtherInstanceMessageHandler`, then calls
`init_app_config()` early (instance checking needs it) and loads the `ShortcutRegistry` from it.
Nothing that needs a running wx (timers, windows, modal prompts, WebView runtime checks) may go in
the constructor; those belong in `on_init_inner` or `post_init`.
### Startup sequence
`GUI_App::OnInit` wraps `on_init_inner()` in a try/catch (`generic_exception_handle`, then a `return false`
that is never reached because the handler terminates or rethrows — `references/threads-timers-app.md` §Startup).
The order inside `on_init_inner` that contributors depend on:
1. Log target, `::Label::initSysFont()` (the `Label::Head_*/Body_*` font table every widget uses),
wxInspector plugin registration, `wxInitAllImageHandlers()`, GTK menu-image and log-filter tweaks.
2. `wxEVT_QUERY_END_SESSION` bound on the app: it sends the main frame a vetoable `wxCloseEvent`
(so the save prompts run), vetoes the session end if that close was vetoed, then calls
`EndModal(wxID_ABORT)` on every dialog in the global `dialogStack`.
3. `init_label_colours()`, `init_fonts()`, `Update_dark_mode_flag()`; the editor's TLS
certificate prompt.
4. `load_language()` — language, colour mode and fonts must be initialised before the first UI
action; the app exits if loading the language fails.
5. Dark-mode initialisation (non-Windows writes `dark_color_mode` from the system appearance;
Windows calls `MSWEnableDarkMode(DarkMode_Auto)` before `NppDarkMode::InitDarkMode`) —
`references/colours-dark-mode.md`.
6. `SplashScreen` (if `show_splash_screen`), held in a `wxWeakRef` and advanced with
`SetText(text, progress)` + `wxYield()` — `references/threads-timers-app.md`.
7. `new PresetBundle`, `new PresetUpdater` and their event bindings; plugin GUI wiring
(`init_plugin_gui_wiring`); networking (`on_init_network`).
8. GTK with EGL: `wxGLCanvas::PreferGLX()` on X11, before any GL canvas exists —
`references/webview-gl-aui-media.md`.
9. `mainframe = new MainFrame()` (creates the plater, the tab book, the preset tabs and the lazy
pages), then `select_tab(TAB_ID_PREPARE or TAB_ID_HOME)` per `starts_on_prepare()`
(`default_page == "1"`).
10. `obj_list()->init()`, `SetTopWindow(mainframe)`, `plater_->init_notification_manager()`,
`load_current_presets()`, `mainframe->Show(true)`; the splash is destroyed; `update_mode()`.
11. The app-level `wxEVT_IDLE` handler is bound.
### post_init and the app idle handler
The idle handler bound at the end of `on_init_inner` runs `post_init()` exactly once (guarded by
`m_post_initialized`, and postponed while a WebView script handler is being added), then on every
idle **saves `app_config` if it is `dirty()`**. `post_init` initialises the WebView2 runtime on
Windows (`init_webview_runtime`, before the first WebView), opens command-line files, loads the GL
resources on the Prepare canvas when the app starts on Prepare (when it starts on Home they load
later as an idle task, so Home paints first), starts the idle prebuild
(`MainFrame::prebuild_pages_when_idle`), and `CallAfter`s the config wizard and update checks — the
code comment: on Mac this is "the only way to popup a modal dialog on start without screwing combo
boxes". If the GL context cannot be made current yet, Linux resets `m_post_initialized` so the next
idle retries (a Wayland surface commits late).
### Accessors
| Accessor | Null? |
|---|---|
| `app_config`, `preset_bundle`, `imgui()`, `shortcuts()`, `action_registry()` | created in the constructor or before `MainFrame`; non-null for the GUI lifetime (`preset_updater` is null in the G-code viewer) |
| `mainframe`, `plater()` | null before `MainFrame` exists |
| `sidebar()`, `obj_list()`, `model()` | dereference `plater_` **without a check** |
| `params_panel()`, `params_dialog()`, `notification_manager()` | null-safe (return null without a main frame / plater) |
| `get_tab(Preset::Type)` | null for a tab not found **or not yet `completed()`**; `tabs_list` / `model_tabs_list` are cleared by `MainFrame::shutdown` |
| `get_model_tab(part)`, `get_layer_tab()` | index `model_tabs_list` **without a bounds check** — undefined once `MainFrame::shutdown` has cleared it |
| `getDeviceManager()`, `getAgent()` | may be null; check before use |
| `em_unit()` | app-wide; per-window value via the free `em_unit(wxWindow*)` — `references/dpi-bitmaps-fonts.md` |
| `dark_mode()` | static, recomputed per call — `references/colours-dark-mode.md` |
| `is_closing()`, `is_recreating_gui()`, `input_idle_ms()` | state flags; `input_idle_ms` is fed by `GUI_App::FilterEvent` |
### recreate_GUI (language switch)
`GUI_App::recreate_GUI` sets `m_is_recreating_gui`, destroys the cached Speed Dial dialog (its
translated strings are injected once), calls `mainframe->shutdown()`, swaps the `Field` control pools
(`switch_window_pools()`; the old pools are released only when the old frame is destroyed), creates a
**new `MainFrame`**, `Destroy()`s the old one, reloads presets, shows the new frame and calls
`prebuild_pages_when_idle()` again. `GUI_App::shutdown` returns early while recreating, so
`is_closing()` never becomes true during a language switch.
Consequences: every `MainFrame` child, `Tab`, lazy panel and cached dialog is a new object afterwards.
The `LazyInstance` statics follow automatically (the new frame's holders replace the old ones); raw
pointers do not. Settings fields are recycled through pools: `references/orca-settings-ui.md`.
### Pitfalls
- **Rule:** On startup and shutdown paths, test `plater()` before `sidebar()` / `obj_list()` / `model()`.
**Why:** those accessors dereference `plater_` unchecked; before `MainFrame` exists they crash.
```cpp
// Wrong: reachable before the main frame exists
wxGetApp().sidebar().update_presets(Preset::TYPE_PRINTER);
// Right
if (Plater* plater = wxGetApp().plater())
plater->sidebar().update_presets(Preset::TYPE_PRINTER);
```
Cite: `GUI_App::sidebar`, `GUI_App::obj_list`, `GUI_App::model`.
- **Rule:** Do not null-check `app_config` inside the GUI; do keep it on the main thread.
**Why:** it is created in the `GUI_App` constructor, before `wxEntry`, so it exists for the whole GUI
lifetime; the hazard is threading ([AppConfig](#appconfig)), not null. Cite: `GUI_App::GUI_App`.
- **Rule:** Do not cache a pointer to a `MainFrame` child or lazy panel in a static or a long-lived
object. **Why:** `recreate_GUI` destroys the old frame; the cached pointer dangles and `is_closing()`
does not warn you.
```cpp
// Wrong
static MonitorPanel* s_monitor = MonitorPanel::ensure();
// Right: ask each time; the statics follow the new frame's holder
if (MonitorPanel* monitor = MonitorPanel::if_built()) monitor->jump_to_HMS();
```
Cite: `GUI_App::recreate_GUI`, `LazyInstance` (`Lazy.hpp`).
## Close and shutdown sequence
Orca's side of shutdown, in order:
1. `MainFrame`'s `wxEVT_CLOSE_WINDOW` handler (bound in the constructor) vetoes, when the event can
be vetoed, if a gizmo is in editing mode, if `Plater::close_with_confirm` (project and preset save
prompts) is cancelled, or if `GUI_App::check_print_host_queue` refuses.
2. Otherwise: `MarkdownTip::ExitTip()`, `wxGetApp().set_closing(true)` (so queued work is inert during
the reset), `m_plater->reset()` (which also saves the AUI perspective to the `window_layout` key),
`MainFrame::shutdown()`, `event.Skip()` (wx's default handler then `Destroy()`s the frame, or — for a
vetoable close while a modal dialog is open — vetoes it after this teardown, `references/windows-dialogs.md` §2).
3. `MainFrame::shutdown()`: stops the idle scheduler (`m_idle.stop()`), shuts down the built Project
panel and plugin pages, removes dock panes, clears the backup callback, cancels all UI jobs
(`get_ui_job_worker().cancel_all()`), unbinds the canvases' handlers (on macOS Cmd+Q delivers a mouse
event after the close handler), resets canvas volumes, **hides the frame** (paint messages into
dying windows crashed), stops the 3D-mouse controller and saves its config, shuts down the
other-instance listener, saves `app_config` if dirty, clears `tabs_list` / `model_tabs_list`, and
calls `GUI_App::shutdown()`.
4. `GUI_App::shutdown()`: removable-drive manager shutdown, login dialog deleted, then (unless
recreating the GUI) stop the HTTP server, `set_closing(true)`, plugin manager shutting down,
printer agent detached and the agent cache cleared.
5. wx deletes all remaining top-level windows, then calls `GUI_App::OnExit`, which stops the HTTP server
and preset sync, deletes `DeviceManager`, `UserManager` and the network agent.
`m_is_closing` is a `std::atomic<bool>`. There is no drain of queued `CallAfter`s at shutdown: queued
app calls are discarded with the app object, and those that still run see `is_closing()`. (The bounded
`drain_pending_events` belongs to `GUI_App::hot_reload_network_plugin`.) The wx side — windows deleted
before `OnExit` (`interface/wx/app.h:358-371`), `wxTheApp` null in `~GUI_App`, exception policy — is in
`references/threads-timers-app.md`.
- **Rule:** Guard deferred GUI work with `!wxTheApp || wxGetApp().is_closing()`.
**Why:** after wx cleanup `wxGetApp()` dereferences a null instance (`wxEntryCleanup` resets the
instance before deleting the app, `src/common/init.cpp:472-487` **[source]**); between the close
handler and `OnExit` the plater has been reset and windows are dying.
```cpp
// Wrong
wxGetApp().CallAfter([this, msg] { handle(msg); });
// Right (as ActionRegistry::init)
if (!wxTheApp || wxGetApp().is_closing()) return;
wxGetApp().CallAfter([this, msg] { if (wxGetApp().is_closing()) return; handle(msg); });
```
Cite: `ActionRegistry::init` (plugin source callbacks), `NetworkAgentFactory.cpp`
(`reject_conflicting_capability`); `GUI_App::init_networking_callbacks` (`message_arrive_fn`) runs
inside `GUI_App`, so it tests its own `is_closing()` before and inside the `CallAfter`.
- **Rule:** A component that owns a thread, timer, socket or dock pane stops it from
`MainFrame::shutdown()` (or its own `shutdown()` called from there), not from its destructor alone.
**Why:** by the time destructors run, the frame is hidden and the plater reset; a timer or thread that
fires in between touches half-destroyed state. `MainFrame::shutdown` is the one place that runs before
any window is deleted, both on exit and on a language switch.
## MainFrame
### Frame, top bar and menu bar
`MainFrame : DPIFrame` uses `BORDERLESS_FRAME_STYLE` (no `wxCAPTION`; no `wxRESIZE_BORDER` on macOS)
and draws its own title bar. Each platform restores the missing decoration differently (MSW strips
`WS_CAPTION` and handles non-client messages in `MainFrame::MSWWindowProc`; GTK adds
`ResizeEdgePanel`s that start a resize drag; macOS `set_miniaturizable` in `Utils/MacDarkMode.mm`) —
`references/platforms.md`.
Off macOS the title bar is `BBLTopbar` (a `wxAuiToolBar` in the frame's sizer, not an AUI pane) that
hosts the File menu, the Edit/View/Help drop-down submenus, the Calibration menu and undo/redo. On
macOS the same menus are attached to a native `wxMenuBar` (`m_menubar`), with Preferences under
`OSXGetAppleMenu()`. `MainFrame::init_menubar_as_editor` builds the `wxMenu`s once and branches only
where they are attached; `generate_help_menu` builds Help. Menu mechanics, `append_menu_item`,
`MenuFactory` and `BBLTopbar` events: `references/popups-menus.md`.
### The tab book
`m_tabpanel` is Orca's `Notebook` (`GUI/Notebook.hpp`, a `wxBookCtrlBase` with a `ButtonsListCtrl`
header that sends `wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED`). Pages are addressed by **string ids**, the
`TAB_ID_*` macros in `MainFrame.hpp` (`TAB_ID_HOME`, `TAB_ID_DESIGN`, `TAB_ID_PREPARE`,
`TAB_ID_PREVIEW`, `TAB_ID_MONITOR`, `TAB_ID_MONITOR_WEB`, `TAB_ID_MULTI_DEVICE`, `TAB_ID_PROJECT`,
`TAB_ID_CALIBRATION`): `AddPage(id, page, text, bmp_name)`, `InsertPage(n, id, …)`,
`FindPageByName`, `SelectPageByName`, `GetSelectedPageName`, `PositionAfter({ids})`. Use the ids, not
indices: pages come and go per printer and per feature flag.
- The **same `Plater` window is inserted twice**, as Prepare and Preview (`MainFrame::update_layout`).
The page-changed handler posts `EVT_GLVIEWTOOLBAR_3D` / `EVT_GLVIEWTOOLBAR_PREVIEW` to the plater, so
"which page" is resolved by id, never by `GetName()` of the window (`MainFrame::select_tab(wxPanel*)`).
- Every other page is a `LazyPage<…>` created in `MainFrame::init_tabpanel`: Home
(`WebViewPanel`), Device (`MonitorPanel`), web Device (`PrinterWebView`), Multi-device
(`MultiMachinePage`), Project (`ProjectPanel`), Calibration (`CalibrationPanel`), and Design
(`DesignPanel`, only under `SLIC3R_CAD` with the feature enabled, order −1 so it is never prebuilt).
- `MainFrame::show_device` inserts and removes the Device, web Device, Multi-device and Calibration
pages depending on the printer and on `use_printer_agents`; a removed page stays registered but is
not prebuilt (its `LazyPage::in_book()` is false).
- Plugin pages are appended by `PluginPages::initialize` (`plugin/host/PluginPages.hpp`) with
namespaced ids (`plugin.<plugin_key>.<name>`) that cannot collide with `TAB_ID_*`. Each is a
`LazyPage<PluginPage>` with order −1, destroyed when its capability goes away.
→ [Deferred construction](#deferred-construction-lazy-lazypage-stagedbuild-idlescheduler)
### Preset tabs
`MainFrame::create_preset_tabs` creates `TabPrint`, `TabPrintPlate`, `TabPrintObject`, `TabPrintPart`,
`TabPrintLayer` on `m_param_panel`, and `TabFilament`, `TabPrinter` on `m_param_dialog->panel()`.
`add_created_tab` moves the plate tab out of `tabs_list` into `plate_tab`, and the model tabs into
`model_tabs_list`, so `tabs_list` holds print, filament and printer. Placement and the settings
pipeline: [Settings placement](#settings-placement-paramspanel-paramsdialog-tabs),
`references/orca-settings-ui.md`.
### DPI and colour fan-out
`MainFrame::on_dpi_changed` and `MainFrame::on_sys_color_changed` call each component they own
explicitly: the tab book and top bar `Rescale()`, the action buttons, `plater()->msw_rescale()` /
`sys_color_changed()` (which go on to the preview, canvas, sidebar, `MenuFactory` and the cached
select-machine dialog), `m_param_panel->msw_rescale()`, every tab's `sys_color_changed()`,
`MenuFactory::sys_color_changed(m_menubar)`, `WebView::RecreateAll()`; lazy panels only through
`X::when_built(...)` and built dialogs through `X::if_built()` (`DiffPresetDialog`). A panel or cached
dialog that is not reached from this chain never runs its `msw_rescale` / `sys_color_changed`. The
DPI mechanics are in `references/dpi-bitmaps-fonts.md`; the colour path (and why Windows reaches it
through `force_color_changed`) is in `references/colours-dark-mode.md`.
- **Rule:** When you add a panel with `msw_rescale()` / `on_sys_color_changed()` hooks, add it to the
fan-out of its owner in the same change.
**Why:** child panels are not top-level windows and get no DPI handling of their own from
`DPIAware`; on Windows the dark-mode toggle reaches components only through this chain.
```cpp
// Right (MainFrame::on_dpi_changed): lazy panels through the statics
CalibrationPanel::when_built([](CalibrationPanel& calibration) { calibration.msw_rescale(); });
// Right (MainFrame::on_sys_color_changed): a lazily built dialog
if (DiffPresetDialog* dialog = DiffPresetDialog::if_built())
dialog->on_sys_color_changed();
```
Cite: `MainFrame::on_dpi_changed`, `MainFrame::on_sys_color_changed`, `Plater::msw_rescale`.
## Deferred construction (Lazy, LazyPage, StagedBuild, IdleScheduler)
Design doc: `docs/HLSD/deferred-page-construction.md`. Startup pays only for what the first frame
shows (the start page and the Prepare plater); every other tab, and heavy dialogs and GL resources,
build on first show or in small units while the user is idle. A click during the idle build waits for
one unit at most. The parts are independent and wx-free where possible (`Lazy`, `StagedBuild`,
`PrebuildQueue` are unit-tested in `tests/slic3rutils`: `test_lazy.cpp`, `test_staged_build.cpp`,
`test_prebuild_queue.cpp`).
### The holder: `Lazy<T>` and `LazyInstance<T>`
`Lazy<T>` (`GUI/Lazy.hpp`) holds a factory and the object it makes: `Lazy(name, order, factory)`.
| Member | Contract |
|---|---|
| `get()` | the object, **null until completely built** (a staged object mid-build is null) |
| `ensure()` | builds whatever is left now (busy cursor + log line) and returns the object; null if the factory returned null or a nested call finds it mid-build |
| `when_built(fn)` | runs `fn` now if built, otherwise once the build completes |
| `build_step()` | one unit: the factory first, then one `StagedBuild` step per call; a nested call (a unit that pumps the loop) does nothing |
| `prebuild_order()` | position in the idle queue; lower first; **negative = never prebuilt** |
The holder does not own the object — its wx parent does. A factory that returns null or a unit that
throws leaves the holder and scheduler able to carry on. `LazyInstance<Self>` is a mixin that gives a
type with one instance app-wide the statics `Self::if_built()`, `Self::ensure()`,
`Self::when_built(fn)`; the `Lazy<Self>` constructor registers itself, and a recreated `MainFrame`'s
holder replaces the old one. All statics are harmless (null / no-op) while no holder exists —
including `when_built`, which then drops `fn`.
### The placeholder page: `LazyPage<Panel>`
`LazyPage<Panel> : wxPanel, Lazy<Panel>` (`GUI/LazyPage.hpp`) is the notebook page (the book needs a
page object to insert and remove by pointer). `LazyPage(parent, name, order, factory)`; the default
factory is `new Panel(parent)`. Its `Show(true)` builds the panel the first time (only once the
top-level frame is shown — `MainFrame::Show` completes the start page on the frame's first show) and
forwards later shows/hides to the panel, so the panel's own `Show()` override stays its activation
hook. The build runs before the placeholder's own `wxPanel::Show(true)`, so an on-demand build creates
its controls in a hidden window as a prebuild does: on MSW each control created or moved inside a
shown window re-clips and repaints its shown siblings, which made a large panel's first show take
seconds. A lazy panel's constructor therefore runs off screen (except the start page's) and must not
rely on `IsShownOnScreen()`. A panel built while its page is hidden stays hidden, and `when_built`
gives it the dark-UI pass the frame ran before it existed (`apply_dark_ui_to_lazy_panel`).
`pending()` is true only while the page is in the book.
### Staged construction: StagedBuild
`StagedBuild` (`GUI/StagedBuild.hpp`) splits a constructor too big for one unit: the constructor builds
a skeleton and queues the rest with `add_build_step(fn)`; `add_build_steps_of(child)` forwards a child
panel's steps, and the parent is `built()` only once every child is. Constraints, all from the design:
- members created in steps start null, so a partly built panel can be destroyed;
- timers, event handlers and the destructor that touch step content check `built()` first;
- nothing takes focus while off screen (a unit may run while the user types elsewhere);
- a widget added by a step keeps its place through an empty sizer slot the skeleton creates.
### The idle scheduler: IdleScheduler, PrebuildQueue
`PrebuildQueue` (`GUI/PrebuildQueue.hpp`) orders `LazyBase` tasks by `prebuild_order()` (equal order:
insertion order) and runs one slice of units of the first pending task. `IdleScheduler`
(`GUI/IdleScheduler.hpp/.cpp`, `MainFrame::m_idle`) drives it from a self-owned `wxTimer`:
- it ticks every 250 ms and runs a slice only after 500 ms without user input (`GUI_App::input_idle_ms`,
stamped by `GUI_App::FilterEvent` for non-command user-input events and main-frame resizes);
- a slice spends at most 40 ms, then the next slice is `StartOnce(5)` — a separate timer message, so
paint, timers and input queued meanwhile run first. Posting slices as pending events would not do
that, because wx drains every pending event, including ones posted meanwhile, before the next
native message (`src/common/appbase.cpp` `wxAppConsoleBase::ProcessPendingEvents` loops until the
list is empty **[source]**);
- it skips while `wxEventLoopBase::GetActive()->IsYielding()` (a slice inside a `wxYield()` would build
pages in the middle of the code that yielded) and guards re-entry with `m_in_slice`;
- it stops its timer when nothing is pending, so it costs nothing afterwards.
`MainFrame::prebuild_pages_when_idle` (called from `post_init` and `recreate_GUI`) clears the queue
and registers the GL resources (`GLResourcesPrebuild`), the Prepare settings page one option group at
a time (`ParamsPanel::settings_page_prebuild`), the Prepare layout at the book's page size
(`m_prepare_layout_prebuild`), every lazy page with a non-negative order, and the lazily built
dialogs (`m_diff_dialog`); the queue then runs them by `prebuild_order()`. `MainFrame::shutdown`
stops it. Units should fit in one slice on a fast machine; a constructor over that is staged.
**Platforms.** GTK: a timer that is always due (`g_timeout_add`, default priority,
`src/gtk/timer.cpp`) runs ahead of the lower-priority GLib sources that repaint and that deliver
posted events and idle (wx's single `G_PRIORITY_LOW` idle source, `src/gtk/app.cpp`
`wxApp::WakeUpIdle` **[source]**) — hence the 5 ms gap rather than 0. macOS: wxOSX rejects a 0 ms
timer (`src/osx/core/timer.cpp:74` `wxCHECK_MSG(m_milli > 0, …)` **[source]**; with asserts compiled
out, `StartOnce(0)` silently never fires). Windows: a slice also waits while the native queue holds input
(`GetQueueStatus`), not counting mouse moves, which Windows synthesises when a window appears under the
cursor. GTK GL resources: the prebuild task `gtk_widget_realize`s the hidden canvas before making the
context current, since GTK creates the surface only on realize.
### Reaching a lazy object
| Need | Use |
|---|---|
| work the object can live without (refresh, status update) | `if (X* x = X::if_built()) x->…;` |
| navigating to it or showing it | `X::ensure()->…` (as `MainFrame::jump_to_monitor`) |
| state it would not fetch for itself when constructed; rescale/recolour of a staged panel (null from `if_built()` while mid-build) | `X::when_built([](X& x) { … });` |
A panel that pulls its own state in its constructor only ever needs `if_built()`.
### Usage
The shape to copy for a new tab (`MainFrame::init_tabpanel`):
```cpp
// Panel: one instance app-wide; heavy constructors also derive StagedBuild
class CalibrationPanel : public wxPanel, public StagedBuild, public LazyInstance<CalibrationPanel> { … };
// MainFrame::init_tabpanel: id, order (gaps leave room between neighbours; <0 = never prebuilt)
m_calibration_page = new LazyPage<CalibrationPanel>(m_tabpanel, TAB_ID_CALIBRATION, 30);
m_lazy_pages.push_back(m_calibration_page);
m_tabpanel->AddPage(TAB_ID_CALIBRATION, m_calibration_page, _L("Calibration"), "tab_calibration_active");
// MainFrame::on_dpi_changed / on_sys_color_changed
CalibrationPanel::when_built([](CalibrationPanel& calibration) { calibration.msw_rescale(); });
```
A lazily built dialog is a `Lazy<Dlg>` member of `MainFrame` with `Dlg : DPIDialog,
LazyInstance<Dlg>`, e.g. `m_diff_dialog("compare_presets", 100, [this] { return make_diff_dialog(); })`,
added to the queue in `prebuild_pages_when_idle` if it should prebuild. The panel's constructor must
cope with the main frame already existing and the user being busy elsewhere, and do all its own setup:
the main frame does nothing to a panel after creating it.
### Pitfalls
- **Rule:** Do not `ensure()` a lazy object for optional work.
**Why:** `ensure()` builds the whole object now under a busy cursor, defeating the deferral for a
page the user may never open.
```cpp
// Wrong: a DPI change builds the Device tab
MonitorPanel::ensure()->msw_rescale();
// Right
MonitorPanel::when_built([](MonitorPanel& monitor) { monitor.msw_rescale(); });
```
Cite: `MainFrame::on_dpi_changed`.
- **Rule:** In a staged panel, timer and event handlers return early until `built()`.
**Why:** a step-built member is null until its step runs; the timer can fire, or the book can select
the page, in between.
```cpp
// Right (MonitorPanel::update_all)
if (!built())
return;
```
Cite: `MonitorPanel::update_all`, `MonitorPanel::init_tabpanel` (steps queued before the page is
added, "where built() must already be false").
- **Rule:** Never take focus while built off screen.
**Why:** a unit can run while the user is typing in another control; `SetFocus` steals the
keystrokes.
```cpp
// Wrong
page->SetFocus();
// Right (MonitorPanel page-changed handler)
if (page->IsShownOnScreen())
page->SetFocus();
```
- **Rule:** Put background UI construction into the prebuild queue, not into idle events.
**Why:** an `wxEVT_IDLE` + `RequestMore()` loop busy-loops the CPU (wxGTK keeps its idle source
installed while more is requested, `src/gtk/app.cpp` `wxApp::DoIdle` **[source]**), runs inside
every `wxYield()` (a full yield calls `ProcessIdle()`, `src/common/evtloopcmn.cpp:182-191`
**[source]**), and builds even while the user is clicking or typing, so the input waits behind it.
```cpp
// Wrong
Bind(wxEVT_IDLE, [this](wxIdleEvent& e) { /* build the next part */ e.RequestMore(); });
// Right: a Lazy<…> holder (or a LazyBase task) registered in MainFrame::prebuild_pages_when_idle
m_idle.add(m_diff_dialog);
```
Cite: `IdleScheduler::tick`, `docs/HLSD/deferred-page-construction.md`.
- **Rule:** A lazy page that can be destroyed while the main frame lives takes a negative order and
stays out of `m_lazy_pages`.
**Why:** `m_lazy_pages` and `PrebuildQueue` hold raw `LazyBase*` and nothing removes one
(`PrebuildQueue` has only `add` and `clear`). The queue calls `pending()` on every task each slice,
and `prebuild_pages_when_idle` reads every entry of `m_lazy_pages`, so a page destroyed while still
listed can be read after it is freed. A page only taken out of the book is fine: it stays registered
and its `pending()` is false (`MainFrame::show_device`).
```cpp
// Right (PluginPages::create_page): order -1, and no m_lazy_pages.push_back
auto* page = new GUI::LazyPage<PluginPage>(m_parent, name, -1, [capability](wxWindow* parent) {
return new PluginPage(parent, capability);
});
```
Cite: `PluginPages::create_page`, `PluginPages::remove_page`.
- **Rule:** Remove several lazy pages from a book left to right.
**Why:** removing the selected page selects and shows the page before it
(`references/controls-dataview.md` §Book controls), and showing an unbuilt `LazyPage` while the frame
is shown builds it. In any other order the page before the selected one can be one removed next,
built only to be destroyed; left to right it is one that stays (unless the selected page is the
book's first).
```cpp
// Right (PluginPages::shutdown): m_order is the tabs' left-to-right order
for (const PluginCapabilityId& id : std::vector<PluginCapabilityId>(m_order))
remove_page(id);
```
Cite: `PluginPages::shutdown`, `PluginPages::relayout`, `PluginPages::on_plugin_deregister`.
## Plater and Sidebar
### Structure
`Plater` and `Sidebar` (`GUI/Plater.hpp/.cpp`) are pimpl'd (`std::unique_ptr<priv> p`); public methods
forward to `p->`. `Plater::priv` owns the model, `PartPlateList`, the three canvases (`view3D`,
`preview`, `assemble_view` in one sizer inside `panel_3d`), `BackgroundSlicingProcess
background_process`, `PlaterWorker<BoostThreadWorker> m_worker` (`Plater::get_ui_job_worker()`), the
`NotificationManager`, `Mouse3DController`, `MenuFactory menus` and the AUI manager. New private state
and helpers go into `priv` in `Plater.cpp`; the header changes only for a public entry point.
### Event hub and custom events
`Plater::priv::priv` is the hub: it binds Orca events on the canvases (`EVT_GLCANVAS_OBJECT_SELECT`,
`EVT_GLCANVAS_RIGHT_CLICK`, `EVT_GLCANVAS_ARRANGE`, …, posted by `GLCanvas3D::post_event`, which does
`wxPostEvent(m_canvas, …)`) and on the plater itself (`EVT_SLICING_UPDATE`, `EVT_SLICING_COMPLETED`,
`EVT_PROCESS_COMPLETED`, `EVT_EXPORT_BEGAN`, `EVT_GLCANVAS_COLOR_MODE_CHANGED`, …). Events are declared
in the header of the class that emits them (`GLCanvas3D.hpp`, `Plater.hpp`, `NotificationManager.hpp`,
`ParamsDialog.hpp`); `BackgroundSlicingProcess` is handed the ids to post (`set_finished_event`,
`set_export_began_event`).
Payload types are in `GUI/Event.hpp`: `SimpleEvent`, `IntEvent`, `Event<T>`, `ArrayEvent<T,N>`. They
derive from `wxEvent` but set `m_propagationLevel = wxEVENT_PROPAGATE_MAX` (a plain `wxEvent` does not
propagate, a command event does — `interface/wx/event.h:270-273`) and implement `Clone()`, so they can
be posted or queued and travel up to the plater.
```cpp
wxDECLARE_EVENT(EVT_GLCANVAS_ARRANGE, SimpleEvent); // GLCanvas3D.hpp, next to the emitter
wxDEFINE_EVENT(EVT_GLCANVAS_ARRANGE, SimpleEvent); // GLCanvas3D.cpp
post_event(SimpleEvent(EVT_GLCANVAS_ARRANGE)); // GLCanvas3D: wxPostEvent on the wxGLCanvas
view3D_canvas->Bind(EVT_GLCANVAS_ARRANGE, [this](SimpleEvent& evt) { … }); // Plater::priv::priv
wxQueueEvent(wxGetApp().plater(), new SimpleEvent(EVT_MODIFY_FILAMENT, filament_info)); // heap, owned (ParamsDialog)
```
Binding, `Skip`, `CallAfter` and cross-thread rules are in `references/events.md` and
`references/threads-timers-app.md`.
- **Rule:** A short-lived object that listens to plater (or canvas) events binds through `EventGuard`
(`GUI_Utils.hpp`) or unbinds in its destructor, and `Skip()`s.
**Why:** the plater outlives the listener; a handler left bound runs on a freed object. Dynamic
handlers run most recently bound first, so a handler that does not `Skip()` hides the event from the
plater's own handler (dynamically bound handlers are searched in reverse order of registration,
`docs/doxygen/overviews/eventhandling.h:480`). `EventGuard` stores the functor at a stable
address, which is what functor `Unbind` matches on (`interface/wx/event.h:967-970`).
```cpp
// Wrong
wxGetApp().plater()->Bind(EVT_SLICING_UPDATE, [this](SlicingStatusEvent& e) { refresh(); });
// Right: member EventGuard unbinds when the dialog dies
m_slicing_guard = EventGuard(wxGetApp().plater(), EVT_SLICING_UPDATE,
[this](SlicingStatusEvent& e) { refresh(); e.Skip(); });
```
Cite: `EventGuard` (`GUI_Utils.hpp`), `PlaterWorker` (binds the plater's idle/paint through it).
### Sidebar content
`Sidebar::Sidebar` builds, inside `p->scrolled` (a `wxPanel`; the sidebar is itself the AUI pane
`"sidebar"`):
1. the printer block — title bar, `PlaterPresetComboBox* combo_printer`, bed type
(`combo_printer_bed`), nozzle/extruder cards (`ExtruderGroup`), sync and connect buttons;
2. the filament block ("Project Filaments") — `combos_filament`, add / delete / edit, purge mode,
flushing volumes, AMS sync;
3. the **`ParamsPanel` top bar reparented in** (`params_panel->get_top_panel()->Reparent(p->scrolled)`:
"Process" title, global/object switch, mode view);
4. `p->sizer_params` (proportion 2): the object search box, `ObjectList` and the `ObjectLayers`
sizer (`ObjectSettings` is created on `p->scrolled`, but its sizer is added only in the
`#if !NEW_OBJECT_SETTING` branch);
5. the **`ParamsPanel` itself reparented in** with proportion 3.
So the process settings are the full `ParamsPanel` in the sidebar, not a summary group; the process
preset combo is the `TabPrint` page's own `TabPresetComboBox`. `Sidebar::update_presets(type)` refreshes
the combos after a preset change; `Sidebar::jump_to_option(...)` activates a tab row and blinks it;
`Sidebar::settings_index()` (`Search::SettingsIndex`) and `Sidebar::get_searcher()`
(`Search::OptionsSearcher`) are the settings search — `references/orca-settings-ui.md`.
`Sidebar::load_ams_list(obj)` is how device data reaches the filament block.
Spacing constants come from `SidebarProps` (`Plater.hpp`): `TitlebarMargin()`, `ContentMargin()`,
`ContentMarginV()`, `IconSpacing()`, `WideSpacing()`, `ElementSpacing()`, used as
`FromDIP(SidebarProps::ContentMargin())`. A new sidebar control uses them and is added to
`Sidebar::msw_rescale`, `Sidebar::sys_color_changed` and, if mode-dependent, `Sidebar::update_mode`.
### Docking
Docks are `AuiMgr`s (`AuiMgr.hpp`), a `wxAuiManager` subclass carrying Orca's dock art, theme and
Wayland rule (`references/webview-gl-aui-media.md`). `Plater::priv` owns `m_aui_mgr`, managing the
plater; the Design tab owns its own for its sidebar (`DesignPanel::m_aui`, layout in
`design_window_layout`). Plater panes: `"sidebar"` (left, no close
button, not top/bottom dockable), `"main"` (`CenterPane()`, the `panel_3d`), `"uv_editor"` (right,
hidden until the texture-displacement gizmo shows it), plus dynamic dock panes. The default perspective
is saved right after `AddPane`; the app-config `window_layout` is applied with
`LoadPerspective(layout, false)` and falls back to the default on failure; `Plater::priv::reset` saves
it back. On Wayland floating is disabled (`wxAUI_MGR_ALLOW_FLOATING` cleared,
`sanitize_window_layout_for_wayland` strips floating state). wx AUI contracts (`Update()` batching,
perspective semantics, floating-frame lifetime): `references/webview-gl-aui-media.md`.
`Plater::add_dock_pane(window, name, caption, dock, size, on_close)` adds a pane: `window` must be a
child of the plater; `dock` is `"left"`, `"right"`, `"bottom"` or `"float"`; `size` is in DIPs; the
name is made unique with `#2`, `#3`…; a saved per-pane layout entry restores its last place. A pane
closed by its own close button is destroyed after `on_close` runs; `remove_dock_pane(window)` destroys
it **without** calling `on_close`; `remove_dock_panes()` runs from `MainFrame::shutdown`.
`show_dock_pane(window, show)` toggles it. `DockPanel : WebPanel` is the plugin pane, named with
`plugin_pane_name(plugin_key, title)` ("stable across sessions … free of wxAuiManager layout
delimiters").
- **Rule:** Name a dock pane with a stable, untranslated identifier free of `|`, `;`, `=` and `\`.
**Why:** `LoadPerspective` restores only panes whose names match, and wx's own parser hides every
pane it does not find (`src/aui/framemanager.cpp:1906-1912` **[source]**, contrary to `interface/wx/aui/framemanager.h:562-565`).
A translated or reused name loses its layout after a language switch or collides.
```cpp
// Wrong
plater->add_dock_pane(panel, into_u8(caption), caption, "right", size, on_close);
// Right
plater->add_dock_pane(panel, plugin_pane_name(plugin_key, title), caption, "right", size, on_close);
```
Cite: `Plater::priv::add_dock_pane`, `DockPanel.hpp`.
### Context menus
Right-click menus are built and cached by `MenuFactory` (`GUI/GUI_Factories.hpp`, `Plater::priv::menus`)
and shown with `Plater::PopupMenu`, which suppresses background-processing updates while the menu tracks
and defers slicing error dialogs (`m_tracking_popup_menu`) to a `CallAfter` after the menu closes. Detail:
`references/popups-menus.md`.
## Settings placement: ParamsPanel, ParamsDialog, Tabs
- `m_param_panel` (a `ParamsPanel`) is created as a `m_tabpanel` child in `MainFrame::init_tabpanel`
and reparented into the sidebar by `Sidebar::Sidebar` (top bar and body separately). It hosts the
process tab and the model-scope tabs; `ParamsPanel::switch_to_object` / `switch_to_global` flip the
sidebar between object and global settings.
- `m_param_dialog` (a `ParamsDialog : DPIDialog`, parented to the plater) owns a second `ParamsPanel`
with the filament and printer tabs. It is **modeless with emulated modality** (a `wxWindowDisabler`
while shown); `Popup()`, the close/validation path and where post-edit work goes:
`references/orca-settings-ui.md` §Where the tabs live; the modality mechanics:
`references/windows-dialogs.md` §6.
The pipeline from `PrintConfigDef` to `Field`, adding a setting, toggles, search and per-object
overrides: `references/orca-settings-ui.md`.
- **Rule:** Null-check `get_tab()`.
**Why:** tabs complete after construction, and `MainFrame::shutdown` clears `tabs_list`.
```cpp
// Wrong
wxGetApp().get_tab(Preset::TYPE_PRINTER)->reload_config();
// Right
if (Tab* tab = wxGetApp().get_tab(Preset::TYPE_PRINTER)) tab->reload_config();
```
## ObjectList
`ObjectList : wxDataViewCtrl` (`GUI/GUI_ObjectList.hpp`) over `ObjectDataViewModel : wxDataViewModel`
(`GUI/ObjectDataViewModel.hpp`), whose nodes are typed by the `ItemType` bitmask (`itPlate`,
`itObject`, `itVolume`, `itInstanceRoot`, `itInstance`, `itSettings`, `itLayerRoot`, `itLayer`,
`itInfo`). It lives in the sidebar, is initialised by `obj_list()->init()` after the main frame is
created, gets keys through the shortcut registry (`ObjectList::dispatch_shortcut`; on macOS a
`wxAcceleratorTable` regenerated by `update_shortcut_accelerators`, because the native control
delivers no key events), and shows context menus through `MenuFactory` + `Plater::PopupMenu`. Per-object
overrides appear as `itSettings` children that open the model-scope tabs. Model ownership, renderers,
drag and drop, native-vs-generic data view: `references/controls-dataview.md`.
## 3D canvas, ImGui layer and NotificationManager
### GLCanvas3D
`GLCanvas3D` is **not a window**: it wraps a `wxGLCanvas* m_canvas` (`get_wxglcanvas()`) created by
`OpenGLManager`, binds its size/idle/key/mouse/paint/focus/timer handlers in `bind_event_handlers`, and
must be unbound before teardown (`Plater::unbind_canvas_event_handlers`, from `MainFrame::shutdown`).
Rendering is idle-driven: handlers mark `set_as_dirty()`, `request_extra_frame()` or
`schedule_extra_frame(ms)`, and `on_idle` renders. Outgoing events go through `GLCanvas3D::post_event`.
The view, preview and assemble canvases and the UV editor share one `wxGLContext`. Paint/idle/swap
details, the shared-context attribute rule and EGL/GLX: `references/webview-gl-aui-media.md`.
### What is ImGui and what is wx
| Drawn with ImGui inside the canvas | wx windows |
|---|---|
| gizmo panels (`GLGizmoBase::on_render_input_window`), `NotificationManager` and its hint / slicing-progress notifications, the preview layer slider (`IMSlider`), `IMToolbar`, the G-code legend (`GCodeViewer`), plate labels (`PartPlate`), and the overlays in `GLCanvas3D::_render_overlays` (plate-select toolbar, variable-layer-height dialog, 3D navigator, toolbar item windows) | everything outside the canvas: sidebar, tabs, dialogs, top bar, Home/Device/Project pages |
`GLToolbar` is not ImGui: its icons are OpenGL-textured quads; only its item option windows are
ImGui callbacks. ImGui input arrives only through the canvas's own handlers (`ImGuiWrapper::update_mouse_data` /
`update_key_data`), so text entry needs canvas focus; ImGui sizes are physical pixels (scale by
`GLCanvas3D::get_scale()`); ImGui strings are UTF-8 (`_u8L`). wx theming, `DPIDialog`, sizers and
`Widgets/` do not apply there.
- **Rule:** Never place a wx child window over the GL canvas; draw the overlay in ImGui or put a wx
window beside the canvas.
**Why:** on GTK the GL canvas is a native child window or, on Wayland, a subsurface drawn outside
GTK, and a wx child over it does not reliably stack above the GL content.
```cpp
// Wrong
auto* banner = new wxPanel(canvas->get_wxglcanvas());
// Right: ImGui from the gizmo / overlay pass, or a sibling of the canvas in the plater layout
void on_render_input_window(float x, float y, float bottom_limit) override; // GLGizmoBase
```
Cite: `docs/HLSD/design-tab.md` (sketch banner "a sibling of the canvas, not a child over it").
### NotificationManager
`NotificationManager` (`GUI/NotificationManager.hpp`) is owned by `Plater::priv` and initialised after
the canvas exists (`Plater::init_notification_manager`; notifications pushed before `init()` are
neither shown nor updated). Push with
`push_notification(NotificationType, NotificationLevel, text, hypertext, callback)`;
`NotificationType::CustomNotification` covers one-offs, and a new `NotificationType` is needed only
when the notification must be closed or updated by type (`close_notification_of_type`). Levels order
importance and fading (`RegularNotificationLevel` fades, `ErrorNotificationLevel` never does). It has
no locking, and it draws only while the plater's canvas renders.
- **Rule:** Push notifications from the UI thread, and use a dialog for messages that must be seen
while Home or Device is shown.
**Why:** the manager's containers are unsynchronised; a notification pushed while the plater is
hidden is not drawn until the user returns to Prepare/Preview.
```cpp
// Wrong: inside Job::process or an agent callback
wxGetApp().notification_manager()->push_notification(text);
// Right
wxGetApp().CallAfter([text] {
if (wxGetApp().is_closing()) return;
if (NotificationManager* nm = wxGetApp().notification_manager())
nm->push_notification(NotificationType::CustomNotification,
NotificationManager::NotificationLevel::RegularNotificationLevel, text);
});
```
## Background work
| Kind | Mechanism | Back to the UI |
|---|---|---|
| UI-initiated task (arrange, orient, fill bed, send) | `Job` subclass in `GUI/Jobs/`; `replace_job(plater->get_ui_job_worker(), std::make_unique<OrientJob>())`, or a dialog-owned `PlaterWorker<BoostThreadWorker>` | `Job::finalize` and `Ctl::call_on_main_thread`, delivered from the owner window's idle/paint |
| slicing and export | `BackgroundSlicingProcess` (`Plater::priv::background_process`) | `wxQueueEvent(plater, evt.Clone())`; `execute_ui_task` for a synchronous UI call |
| network agents, HTTP, preset sync | agent / io threads | `wxGetApp().CallAfter` + `is_closing()`; agents get `set_queue_on_main_fn` |
| geometry | TBB | no wx calls inside |
The contracts (which side runs what, cancellation, the `eptr` rethrow, deadlock rules, platform stalls)
are in `references/threads-timers-app.md`.
## Device and Monitor pages
Design doc: `docs/HLSD/printer-agent.md`; implementing a printer agent: the `orca-printer-communication`
skill. Data flow:
1. `NetworkAgent` (façade over the active `IPrinterAgent` and the cloud agents) calls the callbacks
installed by `GUI_App::init_networking_callbacks` (`set_on_message_fn`, `set_on_local_message_fn`,
`set_on_printer_connected_fn`, `set_queue_on_main_fn`, …) and by `GUI_App::post_init` /
`restart_networking` (`set_on_ssdp_msg_fn`) on its own threads.
2. Each callback returns if `is_closing()`, then `CallAfter`s a by-value lambda that re-checks
`is_closing()` and, on the UI thread, updates the `MachineObject` (`parse_json`), refreshes
`Sidebar::load_ams_list` and `Plater::update_machine_sync_status`. `MachineObject` and
`DeviceManager` state is main-thread-only.
3. The Device UI is **pull-based**: `MonitorPanel : wxPanel, StagedBuild, LazyInstance<MonitorPanel>`
(`GUI/Monitor.hpp`) starts its refresh `wxTimer` in its `Show(true)` override, stops it on hide, and
`on_timer` → `update_all()` reads `DeviceManager::get_selected_machine()` and pushes it into the
`StatusPanel` (`StatusBasePanel : wxScrolledWindow, StagedBuild`), HMS and media pages inside a
`Tabbook`. `DeviceManager::start_refresher` / `stop_refresher` follow the main frame's `wxEVT_SHOW`.
4. Camera playback: `MediaPlayCtrl` selects and tears down the stream backend; the wx parent owns the
rendering window.
New device UI goes inside `MonitorPanel` / `StatusPanel`, reads state on the timer, makes no network
call on the UI path, and stops its timers on hide.
## Web-based UI
| Host | Use |
|---|---|
| `WebView::CreateWebView(parent, url)` (`Widgets/WebView.hpp`) | the sanctioned way to make a browser: backend choice, handlers, user agent, `"wx"` script handler once per view, registration for `WebView::RecreateAll()` theming, a `FakeWebView` stub instead of null on failure. A raw `wxWebView::New` view gets none of these (no theming on colour change, no null safety) |
| `WebViewPanel` (`WebViewDialog.hpp`, `LazyInstance`) | the Home tab |
| `PrinterWebView` (`LazyInstance`) | the web Device tab (Fluidd/Mainsail/printer UIs) |
| `WebViewHostDialog : DPIDialog` (`Widgets/WebViewHostDialog.hpp`) | local-HTML dialogs: `create_webview(resource_path, …)`, pure-virtual `on_script_message(json)`, `handle_common_script_command`, theme user scripts registered once, `apply_theme_live`, `call_web_handler` (C++ → JS). Subclasses include `WebDialog`, `PluginsDialog`, `PluginsConfigDialog`, `SpeedDialWebDialog`, `TerminalDialog`, `PresetBundleDialog`, `ExportPresetBundleDialog` |
| `WebPanel`, `DockPanel : WebPanel` | plugin pages and docked plugin panes |
| `GuideFrame` (`WebGuideDialog.hpp`) | setup wizard |
Script messages arrive synchronously inside the native WebKit delegate / GTK signal on macOS and Linux
(**[source]**; Edge queues them), so a subclass defers every window operation (show, close, create, `EndModal`) with `CallAfter` and
re-checks liveness inside; `handle_common_script_command`'s `close_page` ends the dialog directly and
`call_web_handler` captures `this` in an app `CallAfter`, so a subclass whose lifetime can end first
adds its own guard. Backend rules, creation order, `RunScript` re-entrancy: `references/webview-gl-aui-media.md`.
## Preferences
`PreferencesDialog : DPIDialog` (`GUI/Preferences.hpp`) is a `TabCtrl m_pref_tabs` over the
`PreferencesTab` pages (`General`, `Control`, `Graphics`, `Online`) plus the Associate and Developer pages,
each a `wxFlexGridSizer` of rows built in `PreferencesDialog::create_items` with the
`create_item_title / label / checkbox / combobox / input / spinctrl / decimal_input / button / …`
helpers (title, tooltip, app-config key, …, `wiki_url`). Window focus follows creation order, so rows are
created in display order; an empty tooltip is filled from the title.
- Rows **write `app_config` and `save()` immediately** in their handler; side effects are `param == "…"`
branches inside the row's handler (`create_item_checkbox`).
- The dialog is opened only through `GUI_App::open_preferences(tab, highlight_option)`, which shows it
modally in an inner scope (it must be destroyed before `recreate_GUI`), then handles what must happen
after it closes: canvas focus, reloading the print when sequence options changed, file associations
on Windows, redraw when a render setting changed, a pending language switch (`load_language`,
`ActionRegistry::relocalize_builtins`, `recreate_GUI`).
- The Windows-only dark-mode row (`create_item_darkmode`) is described in
`references/colours-dark-mode.md`.
```cpp
// PreferencesDialog::create_items — a checkbox row bound to an app_config key
auto item_show_splash_scr = create_item_checkbox(_L("Show splash screen"),
_L("Show the splash screen during startup."), "show_splash_screen");
g_sizer->Add(item_show_splash_scr);
// AppConfig::set_defaults — the default for a fresh config
if (get("show_splash_screen").empty())
set_bool("show_splash_screen", true);
```
- **Rule:** Put a preference's runtime effect where it belongs: immediate effects in the row handler,
effects that need the dialog gone (rebuilding the GUI, reloading the print) in
`GUI_App::open_preferences`.
**Why:** `recreate_GUI` while the dialog is alive crashed in `~wxDialogBase` (the inner-scope comment
in `open_preferences`); work done from the row handler runs under the modal loop.
## AppConfig
`AppConfig` (`libslic3r/AppConfig.hpp`) is Orca's own string store, saved as JSON, not `wxConfig`
(comparison with `wxConfig`: `references/strings-i18n-files.md`). Keys live in sections (`"app"` by
default).
| Call | Behaviour |
|---|---|
| `get(key)` / `get(section, key)` | the string, `""` if missing |
| `get_bool(key)` | `get("app", key) == "true" \|\| get("app", key) == "1"` |
| `get_bool(section, key)` | `get(section, key) == "true" \|\| get("app", key) == "1"` — the `"1"` is read from **`"app"`** |
| `set(key, value)`, `set(section, key, value)`, `set_str(section, key, value)`, `set(section, key, bool)` | marks dirty only when the value changes; the `bool` overload writes `"true"`/`"false"`, and a bare `const char*` value selects it — pass a `std::string` or use `set_str` (`references/strings-i18n-files.md` §AppConfig) |
| `set_bool(key, value)` | `"true"`/`"false"` in `"app"` |
| `has(section, key)`, `dirty()`, `save()` | `save()` throws `CriticalException` off the main thread |
| `set_defaults()` | fills missing keys at load (`if (get("k").empty()) set…`) |
Persistence: the app idle handler saves whenever `dirty()` after `post_init`, and `MainFrame::shutdown`
saves if dirty, so a `set` persists on its own; an explicit `save()` is for immediate persistence
(Preferences rows, dark-mode init). Keys use both conventions — `set_bool` keys hold `"true"/"false"`,
others hold `"1"/"0"` (`dark_color_mode`, `default_page`, `sys_menu_enabled`) — so compare with the
key's own convention.
- **Rule:** Read a non-`"app"` boolean with `get(section, key)` and an explicit comparison.
**Why:** `get_bool(section, key)` accepts `"1"` only from the `"app"` section.
```cpp
// Wrong: false when section/key holds "1"
bool on = app_config->get_bool("section", "key");
// Right
bool on = app_config->get("section", "key") == "1";
```
- **Rule:** Write `app_config` on the main thread only.
**Why:** the storage map is unsynchronised and `save()` throws off the main thread.
```cpp
// Wrong: on a worker or agent thread
wxGetApp().app_config->set("key", value);
// Right
wxGetApp().CallAfter([value] { if (!wxGetApp().is_closing()) wxGetApp().app_config->set("key", value); });
```
Cite: `AppConfig::save`.
## Where new code goes
| You add | Put it | Must also |
|---|---|---|
| Modal dialog | `GUI/<Name>Dialog.hpp/.cpp`, `class X : public DPIDialog` | follow the dialog recipe in `references/windows-dialogs.md` (parent fallback `wxGetApp().mainframe`, `on_dpi_changed`, `SetSizerAndFit`, `UpdateDlgDarkUI` last) |
| Message / confirm box | `MessageDialog`, `RichMessageDialog`, `WarningDialog`, `ErrorDialog`, `InfoDialog` (`MsgDialog.hpp`), or `show_error` / `show_info` (`GUI.hpp`) | never `wxMessageBox`; `show_error` is asynchronous (an app `CallAfter` around an `ErrorDialog`), so pass a parent that outlives the call or none; `show_info` is a synchronous modal `MessageDialog` — `references/windows-dialogs.md` |
| Local-HTML dialog | subclass `WebViewHostDialog` | implement `on_script_message`; reuse `handle_common_script_command`; defer window operations; register user scripts once ([Web UI](#web-based-ui)) |
| Top-level tab | `LazyPage<Panel>` + `TAB_ID_*` in `MainFrame::init_tabpanel` | panel derives `LazyInstance<Panel>` (+ `StagedBuild` if heavy); fan-out hooks with `when_built`; statics outside `MainFrame`; no focus off screen |
| Heavy dialog owned by the frame | `Lazy<Dlg>` member of `MainFrame`, `Dlg : LazyInstance<Dlg>` | register in `prebuild_pages_when_idle` to prebuild; `if_built()` in the colour fan-out |
| Sidebar control | `Sidebar::Sidebar`, state in `Sidebar::priv` | `SidebarProps` spacing; add to `Sidebar::msw_rescale`, `sys_color_changed`, `update_mode` |
| Docked pane | `Plater::add_dock_pane` | window is a plater child; stable name; know that `remove_dock_pane` skips `on_close` |
| Print / filament / printer setting | def in `PrintConfig.cpp`, row in `Tab*::build` | the full checklist in `references/orca-settings-ui.md` |
| Per-object setting | `SettingsFactory::OBJECT_CATEGORY_SETTINGS` / `PART_CATEGORY_SETTINGS` | `references/orca-settings-ui.md` |
| Overlay or tool UI in the 3D view | gizmo `on_render_input_window` or `GLCanvas3D::_render_overlays` | ImGui + `_u8L`; redraw via `set_as_dirty()` / `request_extra_frame()`; GL only in the canvas's current context |
| Transient message about the 3D view | `NotificationManager::push_notification` | UI thread; new `NotificationType` only to close/update by type |
| Main-menu item | the shared `wxMenu` in `MainFrame::init_menubar_as_editor` / `generate_help_menu` | `append_menu_item`, or `append_shortcut_item` when it has a shortcut — `references/popups-menus.md` |
| Context-menu item | `MenuFactory` (`GUI_Factories.cpp`) | show with `Plater::PopupMenu` |
| Speed Dial command | the `NativeCommands` catalog (`NativeCommands.cpp`, `NativeCommand{key, title, group, input, icon, runner}`) | `ActionRegistry` stores and dispatches it |
| Keyboard shortcut | `Shortcut` enum + `shortcut_table` (`Shortcuts.cpp`) | handle in the context's dispatcher; labels from the registry — `references/mouse-keyboard-focus.md`, `docs/HLSD/keyboard-shortcuts.md` |
| Preference | a `create_item_*` row in `PreferencesDialog::create_items`; default in `AppConfig::set_defaults` | effects in the row handler; post-close effects in `GUI_App::open_preferences` |
| Background task | `Job` subclass in `GUI/Jobs/` | UI only in `finalize` / `call_on_main_thread`; poll `was_canceled()` — `references/threads-timers-app.md` |
| Device UI | inside `MonitorPanel` / `StatusPanel` | pull from `DeviceManager::get_selected_machine()` on the timer; no network calls on the UI path |
| Reusable control | `GUI/Widgets/` | `references/orca-widgets.md`, `references/painting-custom-widgets.md` |
| Source files | `src/slic3r/CMakeLists.txt` | [Build registration](#build-registration) |
| Tests for wx-free GUI logic | `tests/slic3rutils/test_<subsystem>.cpp` (as `test_lazy.cpp`, `test_shortcuts.cpp`) | list the file in that suite's `CMakeLists.txt` (`tests/AGENTS.md`) |
## Build registration
`src/slic3r/CMakeLists.txt` defines `SLIC3R_GUI_SOURCES`, the list compiled into `libslic3r_gui`. It
covers everything under `src/slic3r` (`GUI/`, `GUI/Widgets/`, `GUI/Jobs/`, `Utils/`, `Config/`,
`plugin/`), with paths relative to `src/slic3r`. Add a new `.cpp`/`.hpp` pair on consecutive lines
(the list is only roughly alphabetical). Additional places:
| File kind | Where |
|---|---|
| Windows-only sources | `if (WIN32) list(APPEND SLIC3R_GUI_SOURCES …)` (the vendored `GUI/dark_mode/` code lives there) |
| macOS Objective-C++ (`.mm`) and their headers | `if (APPLE) list(APPEND SLIC3R_GUI_SOURCES …)` |
| Design/CAD UI | the `if (SLIC3R_CAD) list(APPEND …)` block; shared code that references it is guarded with `#ifdef SLIC3R_CAD` (the root `CMakeLists.txt` adds the definition) |
| `GUI/DeviceCore/`, `GUI/DeviceTab/` | their own `CMakeLists.txt`, included with `add_subdirectory`, which `list(APPEND SLIC3R_GUI_SOURCES …)` and re-export it with `PARENT_SCOPE` |
- **Rule:** Keep platform-only sources out of the shared list.
**Why:** an `.mm` file or a Win32-only header in the shared list breaks the other platforms' builds.
```cmake
# Wrong: in the shared set(SLIC3R_GUI_SOURCES …) list
GUI/GUI_UtilsMac.mm
# Right
if (APPLE)
list(APPEND SLIC3R_GUI_SOURCES
GUI/GUI_UtilsMac.mm
)
endif ()
```
## Design docs (docs/HLSD)
Per `AGENTS.md`, the high-level design of a subsystem goes in `docs/HLSD/<subsystem>.md`, describes
the design as it stands (no phases or before/after framing), and is updated in the same PR when a change
invalidates it. Planning output stays in the gitignored `docs/superpowers/`. GUI-relevant documents:
| Doc | Covers |
|---|---|
| `docs/HLSD/deferred-page-construction.md` | `Lazy`, `LazyPage`, `StagedBuild`, `IdleScheduler`, `PrebuildQueue`, GL-resource prebuild; rules for reaching lazy objects, unit size, order |
| `docs/HLSD/keyboard-shortcuts.md` | `KeyChord`, `Shortcut` / `shortcut_table`, `ShortcutRegistry`, contexts and dispatchers, labels, the shortcuts dialog, "Adding a shortcut" |
| `docs/HLSD/design-tab.md` | the Design (CAD) tab: `SLIC3R_CAD` gate, null-guarded hooks in `GLCanvas3D`, Esc-level contract, generated offer table, project persistence |
| `docs/HLSD/printer-agent.md` | `NetworkAgent`, `IPrinterAgent`, `ICloudServiceAgent`, `DeviceManager` / `MachineObject` ownership, camera playback boundary |
The other HLSD documents cover slicing features and profile data (for example `preset-cache.md`, which
explains how system presets load at startup).
@@ -0,0 +1,707 @@
# OrcaSlicer settings UI: PrintConfig → Tab
How a `ConfigOptionDef` becomes a row in a settings tab, how edits flow back into the config, and everything
a print/filament/printer setting must touch to load, save, show, search, translate, toggle and override.
Read it before adding or changing a setting, writing a `Field` type or custom row widget, adding dependency
rules, or debugging a settings row that does not show, save, search, revert or translate.
Contents: [Rules](#rules) · [Pipeline](#pipeline-at-a-glance) · [ConfigOptionDef](#configoptiondef-the-data-side) ·
[Config classes and preset lists](#config-classes-preset-lists-and-variants) · [Tabs and placement](#tabs-pages-and-where-they-live) ·
[Groups, options, lines](#optionsgroup-option-and-line) · [build_field](#optionsgroupbuild_field) ·
[Field and value flow](#field-and-the-value-flow) · [Control pooling](#field-control-pooling) ·
[Lazy building and OG_CustomCtrl](#lazy-building-and-og_customctrl) · [ConfigManipulation](#configmanipulation-toggles-and-fix-ups) ·
[Search index](#search-index-registration) · [Localization](#localization-of-option-definitions) ·
[Per-object overrides](#per-object-part-layer-and-plate-overrides) · [Checklist](#checklist-adding-a-setting)
## Rules
1. Declare a setting once, in `PrintConfigDef::init_*_params()`, with `label`, `category`, `tooltip`, `sidetext`,
`mode`, limits and a default; mark every user-visible string with `L()`, never `_L()`. → [ConfigOptionDef](#configoptiondef-the-data-side)
2. Put the member in the `PRINT_CONFIG_CLASS_DEFINE` block of the scope it belongs to; the class decides whether
the setting can be overridden per object, part or layer range. → [Config classes](#config-classes-preset-lists-and-variants), [Overrides](#per-object-part-layer-and-plate-overrides)
3. List the key in the `Preset` option list of its preset type; without it the key is absent from the tab's
config and the row cannot be built. → [Config classes](#config-classes-preset-lists-and-variants)
4. A per-variant key is a vector option listed in the matching `*_options_with_variant` set, appended with index
`0`, and toggled with the variant index. → [Variants](#per-extruder-variant-options)
5. Add the row with `optgroup->append_single_option_line(key, wiki_path)` in `TabX::build()`; widget, tooltip,
undo/system icons, dirty tracking and search all come from the def. → [Groups](#optionsgroup-option-and-line)
6. Page titles, group titles, `Line` labels and tooltips are English `L()` strings; translation happens at
display time. → [Localization](#localization-of-option-definitions)
7. Do not rely on `L_CONTEXT` in a def: the display path translates without context. → [Localization](#localization-of-option-definitions)
8. A row built from a custom widget has no `Field`; give its key name branches in `Tab::decorate`,
`Tab::on_roll_back_value` and `ConfigOptionsGroup::back_to_config_value` (plus `Tab::options_list_storage_key`
for a vector key). → [Custom widgets](#custom-widgets-on-a-line)
9. In a `Field::BUILD()`, create controls through a `static Builder<T>` per construction style, re-set every
property, and bind every handler with the control's id. → [Pooling](#field-control-pooling)
10. Bind with an id only events the widget emits with its id; `::ComboBox` sends `wxEVT_COMBOBOX_DROPDOWN/CLOSEUP`
with id 0. → [Pooling](#field-control-pooling)
11. Fields exist only for the active page: null-check `get_field()`, and use `toggle_line` (not field state) for
anything that must hold on every page. → [Lazy building](#lazy-building-and-og_customctrl)
12. Dependent enable/hide rules go in `ConfigManipulation::toggle_*_options`; value fix-ups go in
`ConfigManipulation::update_*_config` through `apply()` under the `is_msg_dlg_already_exist` guard.
→ [ConfigManipulation](#configmanipulation-toggles-and-fix-ups)
13. Set field values from code with `set_value(value, false)`; the `boost::any` must hold the display type that
`ConfigOptionsGroup::get_config_value` produces for the option type. → [Field](#field-and-the-value-flow)
14. Teach `Print::invalidate_state_by_config_options` / `PrintObject::invalidate_state_by_config_options` which
steps the key invalidates; an unknown key reslices everything. → [Checklist](#checklist-adding-a-setting)
15. A setting is searchable, offered by the Speed Dial and listed in the unsaved-changes/compare dialogs only if a
settings tab registers it in a titled group and it has a label. → [Search index](#search-index-registration)
16. Filament and printer tabs live in the modeless `ParamsDialog`; code that must run after editing hooks its
close path, not the line after `Popup()`. → [Placement](#where-the-tabs-live)
17. Renaming or removing a key needs `PrintConfigDef::handle_legacy`, and a new key's default must reproduce the
old behaviour for existing profiles and projects. → [Checklist](#checklist-adding-a-setting)
## Pipeline at a glance
```
PrintConfigDef::init_*_params() def = this->add(key, coX) ... libslic3r/PrintConfig.cpp
PRINT_CONFIG_CLASS_DEFINE(...) ((ConfigOptionX, key)) libslic3r/PrintConfig.hpp
s_Preset_*_options key saved/loaded/diffed per preset libslic3r/Preset.cpp
TabX::build() add_options_page → Page::new_optgroup slic3r/GUI/Tab.cpp
ConfigOptionsGroup::append_single_option_line(key, wiki, idx)
get_option(): m_opt_map["key#idx"], settings_index().add_key(...) slic3r/GUI/OptionsGroup.cpp
create_single_option_line(): Line{label, formatted tooltip}
append_line(): index.set_path / set_line_label
page shown → Page::activate → OptionsGroup::activate → activate_line
→ OptionsGroup::build_field → Field::Create<T> → T::BUILD() slic3r/GUI/Field.cpp
→ OG_CustomCtrl paints labels, sidetext, undo icons slic3r/GUI/OG_CustomCtrl.cpp
edit → Field::on_change_field → OptionsGroup::on_change_OG → ConfigOptionsGroup::on_change_OG
→ change_opt_value(config) → group m_on_change (Page::new_optgroup)
→ Tab::update_dirty() + Tab::on_value_change() → TabX::update()
→ ConfigManipulation::update_*_config → toggle_options() → MainFrame::on_config_changed
```
## ConfigOptionDef: the data side
**Contract.** `ConfigOptionDef` (`src/libslic3r/Config.hpp`) is the static description of one key: type,
default, GUI presentation, limits and legacy names. Defs are registered with `ConfigDef::add(key, type)` (or
`add_nullable`) inside `PrintConfigDef::init_common_params` / `init_fff_params` / `init_sla_params`
(`src/libslic3r/PrintConfig.cpp`); the def map owns the default value object.
```cpp
def = this->add("brim_width", coFloat);
def->label = L("Brim width"); // row label; L() is an extraction marker (no-op)
def->category = L("Support"); // per-object settings grouping (not the tab page)
def->tooltip = L("This is the distance from the model to the outermost brim line.");
def->sidetext = L("mm"); // unit
def->min = 0; def->max = 100; // Field clamps to these
def->mode = comSimple; // visibility gate
def->set_default_value(new ConfigOptionFloat(0.));
```
Enums need three parts: the `enum class` plus `CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(Name)` in `PrintConfig.hpp`,
a `static t_config_enum_values s_keys_map_Name` plus `CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(Name)` in
`PrintConfig.cpp`, and in the def `enum_keys_map = &ConfigOptionEnum<Name>::get_enum_values()` with parallel
`enum_values` (serialized keys) and `enum_labels` (`L()` display labels) — see `wall_generator`
(`PerimeterGeneratorType`).
| Field | Meaning for the GUI |
|---|---|
| `type` | `coFloat/coFloats/coInt/coInts/coString/coStrings/coPercent(s)/coFloatOrPercent(s)/coBool(s)/coEnum(s)/coPoint(s)/…`; picks the `Field` when `gui_type` is `undefined`. |
| `gui_type` | `GUIType {undefined, i_enum_open, f_enum_open, color, select_open, slider, legend, one_string, plugin_picker, plugin_config, printer_agent_select}`; checked first by `build_field`. `slider` is marked "currently unused" in `Config.hpp`. |
| `gui_flags` | `"serialized"`: a vector edited as one `;`-separated string; `"show_value"`: show the value even when enum labels exist. |
| `label` / `full_label` | `label` is the short row label (a sub-label inside a multi-option row). `full_label`, when set, names the setting on its own: sidebar search titles, the per-object "Add Settings" menu, the compare dialogs. The Speed Dial titles a setting with the label its row draws and keeps `full_label`/`label` only as a search alias (`ActionRegistry`). |
| `category` | English group name for per-object settings (`SettingsFactory::get_bundle`, the settings menus, the ObjectList settings item) and the transfer view's fallback in `UnsavedChangesDialog`. Empty = left out of the ObjectList settings item and the "Add Settings" menus (the model tabs still show the key). Search uses the tab page title instead. |
| `tooltip`, `sidetext` | Translated at display; `sidetext` is drawn inside most inputs (see [Field](#field-and-the-value-flow)). |
| `min`, `max`, `max_literal` | `Field` clamps to `[min, max]` with "Value is out of range."; `max_literal` bounds the absolute (non-%) value of `coFloatOrPercent` keys whose `sidetext` contains `"mm "`. Both `min`/`max` bounded → "Range:" line in the tooltip. |
| `mode` | `comSimple < comAdvanced < comExpert < comDevelop`; a row shows when its **first** option's mode ≤ the tab's mode. |
| `nullable` | Vector values may hold nil ("N/A"); used by model-scope overrides. |
| `multiline`, `full_width`, `is_code`, `height`, `width`, `readonly` | Text box shape (in em units for `height`/`width`); `is_code` sets `normal_font()` (not a monospace font, despite the `Config.hpp` comment) and adds the "Edit Custom G-code" button when the group has `edit_custom_gcode`; disabled control (`readonly`). |
| `ratio_over` | For `coFloatOrPercent`: the key a percentage refers to. |
| `aliases`, `shortcut` | Legacy names; one value expanding to several keys. |
| `plugin_type` | Makes the option plugin-backed (`is_plugin_backed()`). |
An unadorned `coBool` def becomes a checkbox with no GUI code at all.
Pitfalls:
- **Rule:** Give `coFloatOrPercent` defs a `sidetext` of the form `"mm or %"` / `"mm/s or %"` and a sensible
`max_literal`.
**Why:** `Field::get_value_by_opt_type` asks "Is it N% or N mm?" by matching the English `sidetext`: a unitless
value above `max` when it contains `"mm/s"`, above `max_literal` when it contains `"mm "` (only that form also
clamps literal values to `max_literal`); another unit text skips the check.
Cite: `Field::get_value_by_opt_type`.
- **Rule:** Give every key that can be overridden per object a non-empty `category`.
**Why:** `is_improper_category` (`GUI_Factories.cpp`) drops empty categories (and `"Extruders"`/`"Wipe options"`
with one filament, `"Support material"` for parts), so the override never appears in the ObjectList settings item
and does not light the Objects switch (`ParamsPanel::notify_object_config_changed`).
Cite: `SettingsFactory::get_bundle`.
## Config classes, preset lists and variants
**Static config classes.** Every key that the slicing core reads is a member of a `PRINT_CONFIG_CLASS_DEFINE`
block in `src/libslic3r/PrintConfig.hpp`:
| Class | Scope | Overridable in model tabs |
|---|---|---|
| `PrintObjectConfig` | per object | object (`TabPrintObject`) |
| `PrintRegionConfig` | per region | object, part (`TabPrintPart`), layer range (`TabPrintLayer`) |
| `MachineEnvelopeConfig`, `GCodeConfig`, `PrintConfig` (derives from both) | global | no |
| `SLA*Config` | SLA | — |
`layer_height` is added for layer ranges, and the plate tab offers the fixed `plate_keys` list in `Tab.cpp`.
**Preset option lists.** `src/libslic3r/Preset.cpp` lists which keys each preset type owns: `s_Preset_print_options`,
`s_Preset_filament_options`, `s_Preset_printer_options` (+ `s_Preset_machine_limits_options` and the nozzle-sized
`PrintConfigDef::extruder_option_keys()`, joined in `Preset::printer_options()`). `PresetBundle` builds each
collection's default config from its list (`prints(Preset::TYPE_PRINT, Preset::print_options(), …)`), so the list
decides what is saved, loaded, diffed, inherited and shown in the tab.
### Per-extruder variant options
Multi-extruder printers store some vectors once per extruder variant. A key joins one of the sets in
`src/libslic3r/PrintConfig.cpp`: `print_options_with_variant`, `filament_options_with_variant`,
`printer_options_with_variant_1` (one value per variant) or `printer_options_with_variant_2` (a normal/silent pair
per variant, stride 2). Printer per-extruder keys sized to `nozzle_diameter` go in
`PrintConfigDef::init_extruder_option_keys` (`m_extruder_option_keys`; a retract key also joins
`m_extruder_retract_keys`, which is asserted sorted).
GUI mechanics: the row is appended with index 0 (`append_single_option_line("outer_wall_speed", wiki, 0)`, field id
`outer_wall_speed#0`). `Tab::switch_excluder` rewrites each group's `m_opt_map` index to the selected variant, so
edits write `values[variant]`, and fills `Page::m_opt_id_map` (`"key#<variant>"` → shown field id).
`Tab::get_config_manipulation` passes a variant index to `toggle_option`/`toggle_line`/`set_option_label` as
`index + 256`; `Page::get_field`/`Page::get_line` see `>= 256` and translate through `m_opt_id_map`. The print-side
variant speeds are nullable vectors (`nullable = true`, `ConfigOptionFloatsNullable`) so a model override can set
one variant's element and leave the others nil (`TabPrintModel::on_value_change`); copy the declaration of an
existing key in the same set.
Pitfalls:
- **Rule:** Add the key to the `Preset` list together with the def and the class member.
**Why:** the tab's config lacks the key, so `ConfigOptionsGroup::get_option` only prints
`No <key> in ConfigOptionsGroup config.` to stderr and the row's value read (`get_config_value`) dereferences a
missing option when the page builds [source].
Cite: `ConfigOptionsGroup::get_option`, `ConfigOptionsGroup::get_config_value`.
- **Rule:** In `ConfigManipulation`, toggle variant keys with the variant index.
```cpp
toggle_field("outer_wall_speed", have_perimeters); // Wrong: finds no field (the row's id is key#0)
toggle_field("outer_wall_speed", have_perimeters, variant_index); // Right
```
Cite: `ConfigManipulation::toggle_print_fff_options`, `Page::get_field`.
## Tabs, pages and where they live
**Classes.** `Tab : wxPanel` (`src/slic3r/GUI/Tab.hpp`) owns a `PresetCollection* m_presets`, the edited
`DynamicPrintConfig* m_config`, its `Page`s and a `ConfigManipulation`. Concrete tabs: `TabPrint`, `TabFilament`,
`TabPrinter`, and the model-scope `TabPrintModel` → `TabPrintPlate`, `TabPrintObject`, `TabPrintPart`,
`TabPrintLayer`. `MainFrame::create_preset_tabs` creates them and `MainFrame::add_created_tab` calls
`Tab::create_preset_tab()` (top bar + `build()`). `GUI_App::get_tab(type)` returns null until a tab is
`completed()`; `get_plate_tab()`, `get_model_tab(part)`, `get_layer_tab()` reach the model tabs.
**Declaring pages.** `TabX::build()` is declarative:
```cpp
auto page = add_options_page(L("Quality"), "custom-gcode_quality"); // English title, page icon
auto optgroup = page->new_optgroup(L("Layer height"), L"param_layer_height"); // L"..." is a wide literal, not L()
optgroup->append_single_option_line("layer_height", "quality_settings_layer_height");
```
`Page::new_optgroup(title, icon, noncommon_label_width, is_extruder_og)` creates a tab group
(`ConfigOptionsGroup(..., is_tab_opt = true)`, or `ExtruderOptionsGroup`), records the page title and preset type
for search (`set_config_category_and_type`), and installs the callbacks: `m_on_change` →
`Tab::update_dirty()` + `Tab::on_value_change()` (called directly; deferring it re-runs `update()`),
`m_get_initial_config` (selected preset), `m_get_sys_config` / `have_sys_config` (parent system preset). On
`TabPrint` pages (model tabs included) `m_split_multi_line` stacks a multi-option row's fields vertically and `m_option_label_at_right`
makes `OG_CustomCtrl` draw sub-labels to the right of the fields. `TabPrinter` creates its "Motion ability",
"Multimaterial" and `"Extruder N"` pages with `add_options_page(..., is_extruder_pages = true)`, which does not
append them to `m_pages`; the caller inserts each at its position.
### Where the tabs live
- **Process.** `TabPrint` and the model tabs sit on `MainFrame::m_param_panel`, a `ParamsPanel` whose top bar
(`get_top_panel()`) and body are reparented into the sidebar's scrolled panel in `Sidebar::Sidebar`: the top bar
above the object list, the body (the tab with its own `TabPresetComboBox`, `Tab::get_combo_box()`) below it.
Print parameters are therefore in the sidebar. `ParamsPanel::switch_to_global` / `switch_to_object` flip its
Global/Objects switch (`m_mode_region`).
- **Filament and printer.** `TabFilament` and `TabPrinter` live on `ParamsDialog::panel()`, a second
`ParamsPanel` inside `ParamsDialog : DPIDialog`, created once with the plater as parent. `ParamsDialog::Popup()`
applies `UpdateDlgDarkUI`, reparents to the main frame on MSW, centres and `Show()`s it — modeless. A
`wxWindowDisabler(this)` created in its `wxEVT_SHOW` handler disables every other shown top-level window while it
is visible and is deleted on hide; the close handler validates (`Tab::validate_filament_temperature_pairs`, may
veto), hides, queues `EVT_MODIFY_FILAMENT` when a filament was being edited, and calls
`Sidebar::finish_param_edit()`. It never destroys the dialog, so the panel and its tabs are reused across opens.
`MainFrame::select_tab(wxPanel*)` given a `ParamsPanel` other than `m_param_panel` opens the dialog.
Modality mechanics: `references/windows-dialogs.md` §6.
- **Sidebar map.** `Sidebar` (pimpl `Sidebar::priv`, `Plater.cpp`) holds the printer block (`combo_printer`,
nozzle/bed-type combos, `ExtruderGroup`s, sync buttons), the filament block (`combos_filament`, add/delete/edit,
flushing-volume button), the `ParamsPanel` top bar, the object-list block (the plate/object/part search bar,
`ObjectList`, `ObjectLayers`; `ObjectSettings` is created but not laid out under `NEW_OBJECT_SETTING`) and the
process `ParamsPanel`. There is no separate process combo
(`Sidebar::priv::combo_print` is never created). It owns the `Search::OptionsSearcher`.
`Sidebar::update_presets(type)` refreshes the combos after a preset change. Full component map:
`references/orca-architecture.md`.
- **No quick-settings group.** `Sidebar::og_freq_chng_params()` returns null in Orca (the frequently-changed
parameters group is compiled out); the process tab itself is the sidebar's settings UI.
Showing the tab of a preset type follows `PlaterPresetComboBox::switch_to_tab`:
```cpp
if (tab->GetParent() == wxGetApp().params_panel())
wxGetApp().mainframe->select_tab(TAB_ID_PREPARE); // process: it is in the sidebar
else {
wxGetApp().params_dialog()->Popup(); // filament/printer
tab->OnActivate();
}
```
**Contract (wx).** `wxWindowDisabler` disables all top-level windows except the skipped one in its constructor and
re-enables them in its destructor; it affects only windows shown and not already disabled at construction
(`interface/wx/utils.h:59-69`, `:87-110`).
Pitfalls:
- **Rule:** Run post-edit work from the `ParamsDialog` close path (or the tab's value-change path), never after
`Popup()`.
```cpp
wxGetApp().params_dialog()->Popup(); refresh_after_edit(); // Wrong: Popup() returns at once
// Right: react in the dialog's close handler / Sidebar::finish_param_edit / EVT_MODIFY_FILAMENT
```
**Why:** the dialog is shown modeless and only emulates modality with `wxWindowDisabler`; the main frame stays
disabled until it hides.
## OptionsGroup, Option and Line
**`Option`** (`OptionsGroup.hpp`) is a *copy* of the def plus the field id (`opt_id`, `"key"` or `"key#idx"`) and
an optional `side_widget`. **`Line`** holds `label`, `label_tooltip`, `label_path` (wiki path), one or more
`Option`s, and optional widgets: `widget` (replaces the fields), `append_widget` extras, `near_label_widget`, plus
`full_width`, `toggle_visible`, `undo_to_sys`. `Line(label, tooltip)` applies `_()` to both, so pass English `L()`
strings. `Line()` is a separator (`OptionsGroup::append_separator()`).
`ConfigOptionsGroup` binds a group to a `DynamicPrintConfig` (or a `ModelConfig`, then `ModelConfig::touch()` runs
after each change). Its API:
- `get_option(key, idx = -1)` → `Option` with id `key` or `key#idx`; records `m_opt_map[id] = {key, idx}`; for tab
groups registers the key in the search index (see [Search](#search-index-registration)).
- `append_single_option_line(key, wiki_path = "", idx = -1)` = `get_option` + `create_single_option_line` (label
`_(label)`, tooltip from `get_formatted_tooltip_text`) + `append_line`.
- `append_single_option_line(const Option&, wiki_path)` appends a modified copy:
```cpp
Option option = optgroup->get_option("small_area_infill_flow_compensation_model");
option.opt.full_width = true; option.opt.is_code = true; option.opt.height = 15; // changes this row only
optgroup->append_single_option_line(option, "quality_settings_wall_and_surfaces#small-area-flow-compensation");
```
**Multi-option rows** build the `Line` by hand (as the "Overhang speed" and "Bridge" rows in `TabPrint::build` and
"Recommended nozzle temperature" in `TabFilament::build`):
```cpp
Line line = { L("Bridge"), L("Set speed for external and internal bridges") };
line.append_option(optgroup->get_option("bridge_speed", 0));
line.append_option(optgroup->get_option("internal_bridge_speed", 0));
optgroup->append_line(line);
```
The row's mode is its first option's `mode`; each field gets a sub-label from its own `label`.
**Wiki link.** A non-empty `label_path` makes the row label a link: hovering highlights it and a click calls
`OptionsGroup::launch_browser` → `https://www.orcaslicer.com/wiki/<path>` with the path appended verbatim, so write
anchors as the wiki slugs them (`page#lowercase-hyphenated`). `append_line` also records the path for the Speed
Dial's "open wiki" action.
**Groups outside tabs.** A `ConfigOptionsGroup` created without `is_tab_opt` (`PhysicalPrinterDialog`,
`BedShapeDialog`) draws a `LabeledStaticBox` with a `wxFlexGridSizer` of `wxStaticText` labels and plain sizer
layout, no `OG_CustomCtrl`, no search registration. The owner calls `activate()`, adds `optgroup->sizer`, sets
`m_on_change`, and loads values (`reload_config()` / `set_value`).
### Custom widgets on a line
`Tab::create_line_with_widget(optgroup, key, wiki_path, widget)` makes a row whose `widget` (a
`std::function<wxSizer*(wxWindow*)>`) replaces the field — bed shape (`printable_area`), `compatible_printers`,
`compatible_prints`, `filament_ramming_parameters`. It presets white-bullet undo icons and the default label colour.
`Line::full_width` with `widget`/extra widgets builds a description row that `append_line` does not register as
options. `near_label_widget` draws a window before the label (in tab groups `activate_line` creates it as a child of
the `OG_CustomCtrl`, which positions it); the group's `rescale_near_label_widget` / `rescale_extra_column_item`
callbacks rescale them on DPI change.
Pitfalls:
- **Rule:** When a key is edited by a custom widget, add it to the name branches in `Tab::decorate` (the
`option_without_field` keys), `Tab::on_roll_back_value` (keyed by group title, then `load_key_value` to refresh
the widget), `ConfigOptionsGroup::back_to_config_value` and, for a vector key stored whole,
`Tab::options_list_storage_key`.
**Why:** `decorate` looks the key up with `get_field()` and skips it when there is none, so the row's modified
colour and undo/lock icons never update; the revert paths have no field to push the restored value into, so the
widget keeps showing the old value [source].
Cite: `Tab::decorate`, `Tab::on_roll_back_value` (`printable_area`, `compatible_prints`, `compatible_printers`).
## OptionsGroup::build_field
`OptionsGroup::build_field(id, def)` switches on `gui_type` first, then on `type`:
| `gui_type` | Field |
|---|---|
| `select_open` | `Choice` (read-only) |
| `i_enum_open`, `f_enum_open` | `Choice` (editable: any value, enum entries as presets) |
| `color` | `ColourPicker` |
| `slider` | `SliderCtrl` |
| `legend` | `StaticText` |
| `one_string` | `TextCtrl` (vector edited as one string) |
| `plugin_picker` / `plugin_config` / `printer_agent_select` | `PluginField` / `PluginConfigField` / `PrinterAgentChoice` (Orca) |
| `type` (when `gui_type` is `undefined`) | Field → widget |
|---|---|
| `coFloat(s)`, `coPercent(s)`, `coFloatOrPercent(s)`, `coString(s)` | `TextCtrl` → `::TextInput` (raw `wxTextCtrl` when `multiline`) |
| `coBool(s)` | `CheckBox` → `::CheckBox` |
| `coInt(s)` | `SpinCtrl` → `SpinInput` |
| `coEnum(s)` | `Choice` → `::ComboBox` (`choice_ctrl`) |
| `coPoint(s)` | `PointCtrl` → two `::TextInput` |
| `coNone` | nothing |
| anything else (`coPoint3`, `coIntsGroups`, …) | throws `Slic3r::LogicError("This control doesn't exist till now")` |
It then wires the field: `m_on_change` / `m_on_kill_focus` → the group (ignored while the group is `m_disabled`),
`m_back_to_initial_value` / `m_back_to_sys_value`, the edit button for `is_code` options when the group has
`edit_custom_gcode`, the plugin picker and the preset type of a `PluginConfigField`. Widget event contracts
(`wxEVT_TOGGLEBUTTON` for `::CheckBox`, commit events of `SpinInput`): `references/controls-dataview.md`
§Field widgets, `references/orca-widgets.md`.
**`Choice` specifics** (`Choice::BUILD`): read-only for plain enums, `select_open` and keys with a registered
`DynamicList`, editable (`wxTE_PROCESS_ENTER`) for open enums; entries are `_(enum_labels[i])`, or untranslated `enum_values` when there are
no labels; an entry gets an icon when `resources/images/param_<enum_value>.svg` exists. Lists computed at runtime
(filament pickers) register a `DynamicList` with `Choice::register_dynamic_list(key, list)` (done in
`Sidebar::Sidebar` for `support_filament`, `sparse_infill_filament_id`, …). A tab may narrow the offered entries per
state by rewriting the field's `m_opt.enum_values/enum_labels` and the combo items, as `TabPrint::toggle_options`
does for `support_style`.
Pitfall:
- **Rule:** A new option type or presentation needs a `gui_type` (or a custom-widget line), not a new `type` case
left unmapped.
**Why:** an unmapped type throws from `build_field` when the page activates, aborting the page build.
## Field and the value flow
`Field` (`src/slic3r/GUI/Field.hpp`, abstract, `Slic3r::GUI`) keeps a copy of the def (`m_opt`), the id
(`m_opt_id`), the vector index (`m_opt_idx`, parsed from `#idx` in `PostInitialize` for most vector types) and the
current `boost::any m_value`. Virtuals: `BUILD()`, `set_value(any, change_event)`, `get_value()`, `enable()`,
`disable()`, `msw_rescale()`, `sys_color_changed()` (MSW only: `UpdateDarkUI` on the window), `propagate_value()`;
`toggle(en)` enables only when not `readonly`. Subclasses: `TextCtrl`, `CheckBox`, `SpinCtrl`, `Choice`,
`ColourPicker`, `PointCtrl`, `StaticText`, `SliderCtrl`, `PrinterAgentChoice`, `PluginField`,
`PluginConfigField`. `Field::Create<T>(parent, def, id)` constructs, runs `PostInitialize()` (em unit,
`parent_is_custom_ctrl`, `BUILD()`, readonly → `disable()`, Ctrl+1..4 tab shortcuts on the window) and returns a
`std::unique_ptr<Field>` owned by `OptionsGroup::m_fields`. The subclass `CheckBox` shadows the global `::CheckBox`
widget inside `Slic3r::GUI` wherever `Field.hpp` is visible, which is why widget code there writes `::CheckBox`
(and qualifies `::TextInput` / `::ComboBox` the same way) (`references/orca-widgets.md`).
`Field` and `Line` derive from `UndoValueUIManager`: the per-row "revert to system" (lock) and "revert to saved"
(undo arrow) icons, their tooltips and the modified label colour (`#F1754E` by default, `label_clr_modified` in
app config) come for free; `Tab::update_changed_ui` / `Tab::decorate` set them from the option status.
**Value types in the `boost::any`** (`Slic3r::GUI::change_opt_value`, `GUI.cpp`):
These are the config-side values that `Field::get_value()`, `on_change_OG` and `Tab::on_value_change` carry.
`Field::set_value` takes the display form that `ConfigOptionsGroup::get_config_value` produces instead: a
`wxString` for float, percent, float-or-percent and string fields, `bool`/`unsigned char` for checkboxes, `int` for
ints and enums, `Vec2d` for points.
| Option type | `any` holds |
|---|---|
| `coFloat(s)`, `coPercent(s)` | `double` |
| `coFloatOrPercent(s)`, `coString`, single `coStrings` element | `std::string` (`"serialized"` `coStrings`: the whole `;`-joined string; `compatible_printers`/`compatible_prints`: `std::vector<std::string>`) |
| `coInt(s)`, `coEnum(s)` | `int` |
| `coBool` | `bool` |
| `coBools` (incl. nullable) | `unsigned char` (`ConfigOptionBoolsNullable::nil_value()` = nil) |
| `coPoint` / `coPoints` element | `Vec2d`; whole `printable_area`-style lists: `std::vector<Vec2d>` |
A wrong type throws `boost::bad_any_cast` inside `change_opt_value`, which logs "Internal error when changing value
for <key>" and leaves the config unchanged.
**Commit points.** Text fields commit on Enter or kill focus (`propagate_value`), not per keystroke; `TextCtrl`
ignores a kill focus raised while its Enter commit is still running (`EnterPressed` guard, e.g. a dialog the commit
opens). `Choice` commits on `wxEVT_COMBOBOX` (editable: also Enter/kill focus); `CheckBox` on `wxEVT_TOGGLEBUTTON`;
`SpinCtrl` on `wxEVT_SPINCTRL`, Enter and kill focus (skipping the first kill focus after an Enter). Validation
(`Field::get_value_by_opt_type`) clamps to the def's limits and reports through `show_error` (asynchronous).
**Flow after a commit.** `Field::on_change_field` (no-op while `m_disable_change_event`) → `OptionsGroup::on_change_OG`
→ `ConfigOptionsGroup::on_change_OG` (resolves `key#idx` through `m_opt_map`, `change_opt_value` on the group's
config, `ModelConfig::touch()` for model configs) → group `m_on_change` → `Tab::update_dirty()` +
`Tab::on_value_change()` (key-specific branches, then `update()`, `Page::update_visibility`, `Layout()`) →
`TabX::update()` (in `TabPrint::update`: `ConfigManipulation::update_print_fff_config`, then, when the update counter
`m_update_cnt` returns to zero, `toggle_options()`, the ObjectList settings refresh and `MainFrame::on_config_changed`
→ `Plater::on_config_change`).
Revert clicks go `OG_CustomCtrl::OnLeftDown` → `ConfigOptionsGroup::back_to_initial_value` / `back_to_sys_value`.
Pitfalls:
- **Rule:** Update a field from code with `set_value(value, false)`; to push a programmatic value into the config,
follow it with `field_changed()` (or `propagate_value()`).
**Why:** `set_value` only brackets the widget update with `m_disable_change_event = !change_event`, so the flag
decides whether events the setter itself emits (e.g. the `wxEVT_TEXT` of `SliderCtrl`'s text box) reach
`on_change_field`; where a setter does emit, `true` re-enters `on_value_change` → `update()`. Most Orca widget
setters emit nothing (`::ComboBox::SetValue`/`SetSelection`, `::CheckBox::SetValue`), so `set_value(v, true)`
alone usually leaves the config unchanged [source].
```cpp
m_optgroup->set_value("print_host", new_url, true); // Wrong: the widget shows it, the config is unchanged
m_optgroup->set_value("print_host", new_url, false); // Right: show it ...
m_optgroup->get_field("print_host")->field_changed(); // ... then commit through the group
```
Cite: `PhysicalPrinterDialog::build_printhost_settings`, `Tab::on_value_change` (`set_value` + `propagate_value`).
- **Rule:** Implement `msw_rescale()` in a new `Field` by calling `Field::msw_rescale()` first (refreshes
`m_em_unit`), then rescaling the widget (`Rescale()`), and size controls in em units. DPI fan-out:
`references/dpi-bitmaps-fonts.md`.
## Field control pooling
Field controls are recycled, not destroyed. `Builder<T>::build(parent, args...)` (`Field.cpp`) takes a window from
its pool when one exists (`Reparent(parent)`, `Enable()`, `Show()`) and otherwise constructs `T(parent, args...)` and
stores the pool pointer in the window's client data. When a page is cleared (`OptionsGroup::clear`), each field
window goes to `free_window`:
- non-GTK: unbind every dynamic handler whose id is a single explicit id (`m_id != wxID_ANY && m_lastId ==
wxID_ANY`) on the window and on its `wxTextCtrl` children, hide, clear the containing sizer, reparent to the main
frame, push back into the pool named by the client data;
- GTK: `delete` the window.
`GUI_App::recreate_GUI` calls `switch_window_pools()` (fresh pools for the new frame) and releases the old pools
when the old frame is destroyed (`release_window_pools()` from a client object on the old frame).
The unbinding walks `wxEvtHandler::GetFirstDynamicEntry/GetNextDynamicEntry`, which wx marks "for internal use
only" (`include/wx/event.h:4027-4033`), and unbinds each entry by its stored functor pointer, which sidesteps
"functors are compared by their address" (`interface/wx/event.h:967-970`) [source]. `Bind`'s `id` defaults to
`wxID_ANY` (`interface/wx/event.h:913-916`); the widgets' own internal handlers are bound that way, which is what
keeps them alive across reuse.
```cpp
// Right: shape of a Field::BUILD
static Builder<::CheckBox> builder; // one static builder per construction style
auto temp = builder.build(m_parent); // may be a reused window
temp->SetValue(check_value); // re-set every property you rely on
temp->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) { on_change_field(); e.Skip(); },
temp->GetId()); // explicit id: unbound by free_window
temp->SetToolTip(get_tooltip_text(check_value ? "true" : "false"));
window = temp;
```
Pitfalls:
- **Rule:** Bind every `Field` handler with the control's id.
**Why:** an id-less `Bind` survives `free_window`; when the pooled control is reused by another field, the old
lambda still fires with a dangling `this` (the old `Field` is gone) — a use-after-free on MSW/macOS — and one more
copy of the handler accumulates per reuse. GTK deletes instead, so the bug does not reproduce there.
```cpp
temp->Bind(wxEVT_TOGGLEBUTTON, [this](auto& e) { on_change_field(); }); // Wrong
temp->Bind(wxEVT_TOGGLEBUTTON, [this](auto& e) { on_change_field(); }, temp->GetId()); // Right
```
Cite: `free_window`, `CheckBox::BUILD`.
- **Rule:** Bind with the id only events the widget sends with its id.
**Why:** the id filter must match the event id. `::ComboBox` sends `wxEVT_COMBOBOX` with its id, and `::TextInput`
re-sends `wxEVT_TEXT_ENTER`/`wxEVT_KILL_FOCUS` with the wrapper's id, but `wxEVT_COMBOBOX_DROPDOWN/CLOSEUP` are
built as `wxCommandEvent e(type)` (id 0: `interface/wx/event.h:2137`), and an entry bound with one id matches
only an equal event id (`src/common/event.cpp:1454-1457`) [source], so an id-filtered bind never fires — and an
unfiltered one is never unbound. `Choice::BUILD`'s own `m_is_dropped` binds are such dead binds. Query
`ComboBox::is_drop_down()` instead of tracking those events.
Cite: `ComboBox::ComboBox` (`EVT_DISMISS` lambda), `ComboBox::mouseDown`, `ComboBox::keyDown`,
`ComboBox::ForceDropdownOpen`, `Choice::BUILD`.
- **Rule:** Keep one `static Builder<T>` per distinct constructor style, and re-apply size, label, value, colours
and tooltip in `BUILD()`.
**Why:** a reused window ignores the new constructor arguments (style, size, id, label) — `Choice::BUILD` keeps
separate builders for editable and `wxCB_READONLY` combos for this reason.
- **Rule:** Do not use `SetClientData` on a pooled control; it holds the pool pointer.
**Contract (wx).** `Reparent` removes the window from its parent and inserts it into another; a notebook page must be
removed from its book first (`interface/wx/window.h:731-742`).
## Lazy building and OG_CustomCtrl
**Lazy fields.** Rows are declared at tab build time; controls are created only when a page activates.
`Tab::activate_selected_page` → `Page::activate` → `Page::activate_group` per group (`OptionsGroup::activate` →
`activate_line` → `build_field`; then `update_visibility`, `reload_config`), followed by `update_changed_ui()`,
`toggle_options()` and `update_visibility()`. Switching pages clears the other pages' controls back to the pool
(`Tab::update_current_page_in_background` → `Page::clear`). The idle prebuild builds the selected settings page one
option group per slice (`Page::build_step`, `Tab::page_build_step`, `ParamsPanel::settings_page_prebuild()`; design
in `docs/HLSD/deferred-page-construction.md`, mechanics in `references/orca-architecture.md`). On GTK the page view is
hidden while a page builds, because GTK crashes when it desensitizes a multi-line text view built on screen and hidden
before its first size allocation (`Tab::activate_selected_page`). Building can be cancelled: `activate(throw_if_canceled)`
throws `UIBuildCanceled`, and the group clears itself.
Consequences: `Tab::get_field` / `Page::get_field` return null for any key not on the built active page;
`Tab::toggle_option` acts only on `m_active_page`; `Tab::toggle_line` and `Tab::set_option_label` write
`Line::toggle_visible` / `Line::label` on **every** page, so they persist and reach the search titles before a page
is ever shown.
**`OG_CustomCtrl`** (`OG_CustomCtrl.hpp/.cpp`, a `wxPanel`) hosts the fields of a tab group (`m_use_custom_ctrl`):
`activate_line` creates it with the first line and fields are parented to it. It paints labels (with the label
colour and blinking search highlight), sub-labels, sidetext of fields that do not combine it, the separator lines and
the undo/lock/edit icons in `OnPaint` (`CtrlLine::render`), positions field windows itself
(`correct_window_position`, `CtrlLine::correct_items_positions`; MSW re-fixes positions in a `CallAfter` after
`Page::activate`), shows/hides fields per line in `CtrlLine::update_visibility` (`toggle_visible && first option mode
<= mode`), and handles clicks (`OnLeftDown`: wiki link, revert to saved, revert to system, edit button).
Pitfall:
- **Rule:** Null-check every `get_field()` and never cache `Field*` across page switches.
```cpp
m_active_page->get_field("support_style")->m_opt; // Wrong: null when not on this page
if (auto f = dynamic_cast<Choice*>(m_active_page->get_field("support_style"))) { /* … */ } // Right
```
**Why:** the field object is destroyed with its page's controls; the window behind it is pooled for another key.
## ConfigManipulation: toggles and fix-ups
`ConfigManipulation` (`ConfigManipulation.hpp/.cpp`) is UI-agnostic dependency logic with two roles:
1. **Value fix-ups** — `update_print_fff_config(config, is_global_config, is_plate_config)` (and the filament/printer
`check_*` helpers) detect invalid combinations, warn with `MessageDialog`, and write corrections through
`apply(config, &new_conf)`, which copies the diff and calls the tab's `load_config` callback (`update_dirty()`,
`reload_config()`, `update()`).
2. **Visibility** — `toggle_print_fff_options(config, variant_index, is_global_config)` calls `toggle_field` (grey
out, → `Tab::toggle_option` → `Field::toggle`), `toggle_line` (hide the row, → `Tab::toggle_line`) and
`set_option_label` (rename a row at runtime, e.g. `brim_width` → "Brim ear radius").
`Tab::get_config_manipulation()` builds the callbacks (variant index → `+256`, see [Variants](#per-extruder-variant-options));
`TabX::toggle_options()` calls the `toggle_*` function and adds tab-local tweaks. They run on every page activation,
after every `update()`, and after a variant switch.
```cpp
// fix-up shape (update_print_fff_config)
if (config->opt_float("layer_height") < EPSILON) {
MessageDialog dialog(m_msg_dlg_parent, _L("Layer height too small\nIt has been reset to 0.2"), "", wxICON_WARNING | wxOK);
DynamicPrintConfig new_conf = *config;
is_msg_dlg_already_exist = true; // ShowModal's loop re-enters update() (field kill focus)
dialog.ShowModal();
new_conf.set_key_value("layer_height", new ConfigOptionFloat(0.2));
apply(config, &new_conf);
is_msg_dlg_already_exist = false;
}
// toggle shape (toggle_print_fff_options)
bool have_infill = config->option<ConfigOptionPercent>("sparse_infill_density")->value > 0;
toggle_line("infill_combination_max_layer_height", config->opt_bool("infill_combination") && have_infill);
```
Pitfalls:
- **Rule:** Guard every modal fix-up with `is_msg_dlg_already_exist` and write values only through `apply()`.
**Why:** `ShowModal` runs a nested event loop; focus leaving the edited field commits it again
(`propagate_value` on kill focus) and re-enters `update()` — the guard exists "to except the duplicate call of
the update() after dialog->ShowModal()". Without it the dialog repeats; a direct `set_key_value` on the tab config
skips `load_config`, leaving fields and dirty state stale. Modality rules: `references/windows-dialogs.md`.
- **Rule:** Hide with `toggle_line`, grey out with `toggle_field`; do not call `Show()` on field windows.
**Why:** `OG_CustomCtrl` re-applies `toggle_visible` and mode on every `update_visibility`, and only `Line` state
survives page rebuilds and feeds the Speed Dial (`Tab::setting_row_state`).
- **Rule:** Gate rules that only make sense for the global preset on `is_global_config`.
**Why:** the model tabs run the same `update_print_fff_config` / `toggle_print_fff_options` on an object's config
with `is_global_config == false` (`m_type < Preset::TYPE_COUNT` in `TabPrint`); see
`toggle_line("flush_into_objects", !is_global_config)` and the global-only support checks.
## Search index registration
`Sidebar` owns `Search::OptionsSearcher searcher`; its `Search::SettingsIndex` (`SettingsIndex.hpp`) is reached as
`wxGetApp().sidebar().settings_index()`. Registration is automatic for tab rows:
- `ConfigOptionsGroup::get_option` → `settings_index().add_key(opt_id, type, group title, page title, group icon)`
— **only when `m_use_custom_ctrl`** (tab groups);
- `OptionsGroup::append_line` → `set_path(opt_id, type, label_path)` and `set_line_label(...)` with the label the
row draws (`Search::compose_display_label`: row label, or "row – sub-label" for multi-option rows).
`SettingsIndex::apply/init` → `append_options` then builds two views from the tab config: `options()` filtered by
mode (sidebar search, `SearchDialog`) and `all_options()` for every mode (the Speed Dial, `ActionRegistry`). An
option enters only if its group and category are non-empty and it has a label (`full_label`, else `label`); vector
keys of print/printer presets get one entry per element (`key#i`), filament variant keys `key#0`. Each entry stores the
English and translated label/group/category, so search matches either. `UnsavedChangesDialog` and `DiffPresetDialog`
name settings from the same index and skip a changed key the index lacks; only the extruder-transfer view
(`UnsavedChangesDialog::update_tree(type, config, from, to)`) falls back to the def's label/category, then "Others".
`Sidebar::jump_to_option(key, type, category)` → (model tab if it has the key, else switch to global) →
`Tab::activate_option`, which selects the page by translated category, focuses the field and blinks it
(`get_custom_ctrl_with_blinking_ptr`). `Tab::apply_searcher()` refreshes the index for one tab.
Pitfalls:
- **Rule:** Create tab groups with a non-empty English title; a key that should be findable must be on a tab.
**Why:** `append_options` skips entries with an empty group or category, so `page->new_optgroup("")` rows and
dialog-only keys are invisible to search and the Speed Dial, and their changes are left out of the unsaved-changes
and compare dialogs.
- **Rule:** Keep `is_tab_opt = false` (the default) for `ConfigOptionsGroup`s outside tabs; the one-argument
`ConfigOptionsGroup(parent)` constructor sets it to `true`.
**Why:** a custom-ctrl group registers its keys with the index under its `config_type()`; outside a tab that type
and category are not set up.
## Localization of option definitions
In `PrintConfig.cpp`, `L(s)` is `(s)` and `L_CONTEXT(s, ctx)` returns `s` (`libslic3r/I18N.hpp`): they only mark the
string for xgettext. The defs live in the static `print_config_def`, built before the GUI installs its translate
callback (`Slic3r::I18N::set_translate_callback`) and reused across language switches, so defs, page titles and group
titles hold English and the GUI translates at display time:
| String | Translated in |
|---|---|
| Row label | `OptionsGroup::create_single_option_line` (`_(label)`) and `Line`'s constructor |
| Sub-labels of multi-option rows | `OG_CustomCtrl` / `activate_line` (`_(label)`; labels exactly `"Top"`/`"Bottom"` in the `"Layers"` context) |
| Tooltip | `get_formatted_tooltip_text` (`Field.cpp`): `_(tooltip)` + "parameter name: `key[idx]`" + for keys in the selected print preset's parent, "Default: value+sidetext" and, when `min`/`max` are both bounded, "Range: [min, max]" |
| Sidetext | `Field::BUILD` (`_L(sidetext)`, drawn inside the input when `m_combine_side_text`) or `OG_CustomCtrl::CtrlLine::render` / `activate_line` (`_(sidetext)`) |
| Enum labels | `Choice::BUILD` (`_(enum_labels[i])`) |
| Page titles | `Tab::translate_category` (`"Extruder N"` composed as `_("Extruder")` + N, or "Left/Right Extruder" on BBL printers) |
| Group titles | `OptionsGroup::activate` (`_(title)`) |
| Category (per object) | ObjectList settings item and menus (`_(category)`) |
Translator workflow and catalog rules: `references/strings-i18n-files.md` and the AGENTS.md localization section.
**Contract (wx).** A message with `msgctxt` is found only when the lookup passes the same context
(`interface/wx/translation.h:594-603`); the catalog keys context entries as `context + '\x04' + msgid`, so a
context-less lookup never sees them (`src/common/translation.cpp:1154-1157`) [source].
Pitfalls:
- **Rule:** Mark def strings with `L()`; never `_L()`/`_()` in `PrintConfig.cpp` or in tab titles.
```cpp
def->label = _L("Brim width"); // Wrong: _L is GUI-only; a def is built once, before any language is set
def->label = L("Brim width"); // Right: English marker, translated where it is displayed
```
**Why:** a translated tab or group title also breaks the English keys below (`translate_category`, the search
index); `PrintConfig.cpp`'s `_()` (`Slic3r::I18N::translate`) runs at static init with no callback installed.
- **Rule:** Do not expect `L_CONTEXT` in a def to select a context translation.
**Why:** the display paths call `_()`/`_L()` without context, so `L_CONTEXT("s", "second")` sidetext shows the
context-less `"s"` entry and the translator's context entry is never used; only the hard-coded `"Top"`/`"Bottom"`
`"Layers"` labels are looked up with context. To disambiguate, use a distinct English string or add a context
lookup in the GUI.
- **Rule:** Keep `category`, page and group titles in English and stable.
**Why:** they are keys: page titles match tab items through `translate_category`, categories key
`SettingsFactory::CATEGORY_ICON` (unknown → no icon) and the `*_CATEGORY_SETTINGS` maps, and the index stores the
English form for search.
## Per-object, part, layer and plate overrides
The model tabs reuse the process tab: `TabPrintModel::build()` runs `TabPrint::build()`, inserts a "Frequent" page
(`layer_height`, `sparse_infill_density`, `wall_loops`, `enable_support`), removes every option not in `m_keys`
(`remove_option_if`), and drops empty groups and pages. `m_keys` is `Preset::print_options()` ∩:
| Tab | Keys |
|---|---|
| `TabPrintObject` | `PrintObjectConfig().keys()` ∪ `PrintRegionConfig().keys()` |
| `TabPrintPart` | `PrintRegionConfig().keys()` |
| `TabPrintLayer` | `layer_height` + `PrintRegionConfig().keys()` |
| `TabPrintPlate` | `plate_keys` (`Tab.cpp`), appended whole, so plate-only keys such as `curr_bed_type` survive the intersection |
So any print setting added to `TabPrint::build()` and to `PrintObjectConfig`/`PrintRegionConfig` is overridable
with no extra GUI code. Selecting an object, part, layer range or plate in `ObjectList` runs
`ObjectSettings::update_settings_list` (`GUI_ObjectSettings.cpp`, `NEW_OBJECT_SETTING` path), which hands the
selected `ModelConfig`s to the tabs with `TabPrintModel::set_model_config`; `TabPrintModel::on_value_change` writes
only the edited key into each `ModelConfig` (nil elements for untouched variants) after a `take_snapshot`. The
`ObjectList` shows an `itSettings` child grouped by `def->category` (`SettingsFactory::get_bundle`), and
`ParamsPanel::notify_object_config_changed` highlights the Objects switch when any object or part has overrides.
ObjectList and its data model: `references/controls-dataview.md`.
`SettingsFactory` (`GUI_Factories.hpp/.cpp`):
- `get_options(is_part)` — `PrintRegionConfig` keys, plus `PrintObjectConfig` keys for objects (SLA: object keys
minus `layer_height`); feeds the "Add Settings" menus (`MenuFactory::append_menu_item_settings`) and
`get_bundle`.
- `OBJECT_CATEGORY_SETTINGS` / `PART_CATEGORY_SETTINGS` — curated category → `std::vector<SimpleSettingData>`
(`{name, label, priority}`, `name` = the option key) lists for
the Object Table dialog (`ObjectTableDialog`, `ObjectTableSettings` via `get_visible_options` /
`get_all_visible_options`). They do not decide whether a key can be overridden.
- `CATEGORY_ICON` — category → icon name.
## Checklist: adding a setting
1. **`src/libslic3r/PrintConfig.cpp`** — the `def = this->add("my_option", coX)` block in the right
`init_*_params()` (label, category, tooltip, sidetext, min/max, mode, default; `L()` strings; enum maps if an enum).
2. **`src/libslic3r/PrintConfig.hpp`** — `((ConfigOptionX, my_option))` in the `PRINT_CONFIG_CLASS_DEFINE` block of
its scope (`PrintObjectConfig` / `PrintRegionConfig` / `PrintConfig` / `GCodeConfig` / `MachineEnvelopeConfig`).
3. **`src/libslic3r/Preset.cpp`** — append the key to `s_Preset_print_options` / `s_Preset_filament_options` /
`s_Preset_printer_options` / `s_Preset_machine_limits_options`. Per-variant keys also go into the
`*_options_with_variant` sets in `PrintConfig.cpp`; nozzle-sized printer keys into
`PrintConfigDef::init_extruder_option_keys`.
4. **`src/slic3r/GUI/Tab.cpp`** — `optgroup->append_single_option_line("my_option", "wiki_page#anchor")` on the right
page and group of `TabPrint::build` / `TabFilament::build` / `TabPrinter::build_fff` (index `0` for variant keys).
That alone yields the widget, tooltip, undo/system decoration, dirty tracking, search and Speed Dial entries, and
(for object/region keys) the model-tab rows.
5. **Slicing invalidation** — add the key to the right step list in `Print::invalidate_state_by_config_options` or
`PrintObject::invalidate_state_by_config_options`; a key in neither falls through to `invalidate_all_steps()`
(correct, but every edit reslices everything).
6. **Optional GUI:** dependencies in `ConfigManipulation::toggle_*_options` / `update_*_config` (+ `TabX::toggle_options`);
Object Table exposure in `SettingsFactory::OBJECT_CATEGORY_SETTINGS` / `PART_CATEGORY_SETTINGS`; a runtime list
via `Choice::register_dynamic_list`; enum icons `resources/images/param_<value>.svg`; a custom widget row via
`Tab::create_line_with_widget` plus the name branches in [Custom widgets](#custom-widgets-on-a-line).
7. **Compatibility** — a renamed or removed key needs `PrintConfigDef::handle_legacy` (old key/value → new;
`PrintConfigDef::handle_legacy_composite` when the migration needs other keys of the loaded config); the
default must keep existing profiles and 3MF projects slicing as before; profile edits follow the `orca-profiles`
skill (vendor `version` bump).
@@ -0,0 +1,858 @@
# Orca widget library
The owner-drawn widgets in `src/slic3r/GUI/Widgets/`: which raw wx control each replaces, constructors, the events
each emits and where to bind them, style APIs, and the per-widget quirks behind real bugs. Read it before adding or
changing a control in Orca UI, or when a widget event "never fires", fires twice, or a widget looks wrong after a
DPI or theme change. Writing a *new* widget (StaticBox/StateHandler/render/Rescale) is in
`references/painting-custom-widgets.md`; `StateColor` semantics and the dark map in
`references/colours-dark-mode.md §StateColor`; the `Label` font table in `references/dpi-bitmaps-fonts.md`; raw wx
controls in `references/controls-dataview.md`.
Contents: [Rules](#rules) · [Why the library exists](#why-the-library-exists) ·
[Namespaces](#namespaces-and-the-checkbox-clash) · [Catalog](#catalog) · [Event semantics](#event-semantics) ·
[Custom vs raw](#custom-vs-raw) · [Shared lifecycle rules](#shared-lifecycle-rules) · [Button](#button) ·
[DialogButtons](#dialogbuttons) · [Label and HyperLink](#label-and-hyperlink) · [CheckBox](#checkbox-and-radiobox) ·
[SwitchButton family](#switchbutton-family) · [RadioGroup](#radiogroup) · [TextInput](#textinput) ·
[ComboBox and DropDown](#combobox-and-dropdown) · [SpinInput](#spininput-and-tempinput) · [Tab systems](#tab-systems) ·
[StaticBox, LabeledStaticBox, StaticLine](#staticbox-labeledstaticbox-staticline) · [ProgressBar](#progressbar) ·
[ScrolledWindow](#scrolledwindow) · [PopupWindow](#popupwindow) ·
[ProgressDialog, WebView, device composites](#progressdialog-webview-and-device-page-composites) ·
[Debugging widgets](#debugging-widgets)
## Rules
1. Interactive controls in new or modified UI are Orca widgets (`Button`, `::CheckBox`, `::ComboBox`, `::TextInput`,
`SpinInput`, `SwitchButton`, `RadioGroup`, `TabCtrl`); never `wxButton`, `wxSpinCtrl`, `wxCheckBox`, `wxChoice` or
a single-line `wxTextCtrl` in new code. → §Custom vs raw
2. Every dialog's bottom button row is `DialogButtons`. → §DialogButtons
3. Inside `namespace Slic3r::GUI` write `::CheckBox`, `::TextInput`, `::ComboBox` (also in `dynamic_cast` and
forward declarations, which go at global scope). → §Namespaces
4. Call `Button::SetStyle(ButtonStyle, ButtonType)` on every `Button` you create; re-apply size/font overrides after
`Rescale()`. → §Button
5. User handlers bound on a widget run **before** the widget's own handlers: `Skip()` in `wxEVT_TOGGLEBUTTON`
handlers on `::CheckBox`/`SwitchButton`, in mouse handlers on `Button`, and in ENTER/KILL_FOCUS handlers on
`GetTextCtrl()`, and take events by reference. → §Event semantics
6. Bind widget events on the widget itself (or by event type on an ancestor), not on an ancestor filtered by the
widget's id: `ComboBox` ignores the id you pass, `TextInput`/`SpinInput` take none, and several events carry id 0
or another window's id. → §Event semantics
7. Know which setters emit: `RadioGroup::SetSelection`, `MultiSwitchButton::SetSelection`, `TabCtrl::SelectItem`,
`Notebook::SetSelection`, an editable `ComboBox::SetSelection` (as `wxEVT_TEXT`) and `SpinInput::SetValue` (as
`wxEVT_TEXT` + `EVT_SPINCTRL_TEXT`) do. → §Event semantics
8. `::CheckBox` and `SwitchButton` emit `wxEVT_TOGGLEBUTTON`, never `wxEVT_CHECKBOX`. → §CheckBox and RadioBox
9. Set a container's background colour before creating widgets in it. → §Shared lifecycle rules
10. Enable/disable custom widgets individually; disabling an ancestor leaves them painted enabled (`::CheckBox`, a native button, greys with it). → §Shared lifecycle
rules
11. Call each widget's `Rescale()` from `on_dpi_changed` (types qualified); `RadioGroup`, `Label`, `HyperLink` and
`LabeledStaticBox` have none and `ProgressBar::Rescale()` does nothing. → §Shared lifecycle rules
12. Read and write `TextInput` text through `GetTextCtrl()` (`ChangeValue` for a silent update); bind
`wxEVT_TEXT_ENTER`/`wxEVT_KILL_FOCUS` on the TextInput or its `GetTextCtrl()`, never on a parent. → §TextInput
13. Multi-line text is a raw `wxTextCtrl` with `wxTE_MULTILINE`, not `TextInput`. → §TextInput
14. `ComboBox`: `wxCB_READONLY` for choice semantics, `SelectAndNotify(n)` when listeners must react, `void*` client
data only. → §ComboBox and DropDown
15. `SpinInput` is integer-only and cannot type `-`; set the range before the value; read `GetValue()` after the
commit; bind `wxEVT_SPINCTRL` with a `wxCommandEvent&` handler. → §SpinInput and TempInput
16. `TabCtrl`'s `wxEVT_TAB_SEL_CHANGING` cannot veto; switching content is your job. → §Tab systems
17. Custom `DialogButtons` labels must exist in the translation catalog (write them as `L("…")`). → §DialogButtons
18. `ProgressBar` colours are painted unmapped and its colour-setter names are swapped; use `SetHeight` for thin bars.
→ §ProgressBar
19. Transient popups derive from `PopupWindow`; on MSW call `BindUnfocusEvent()` when the popup must close with the
frame. → §PopupWindow
20. Children of a `LabeledStaticBox` are created with the box as parent; its label is fixed at `Create()`.
→ §StaticBox, LabeledStaticBox, StaticLine
21. Links are `HyperLink`, or a `Label` given `LB_HYPERLINK` through `SetWindowStyleFlag` (the constructor ignores it).
→ §Label and HyperLink
## Why the library exists
Native controls cannot carry Orca's flat, rounded, palette-coloured look, and they cannot follow all of Orca's
theming:
- On Windows the theme is an app-level setting (Preferences "Enable dark Mode", Windows-only) layered on
`MSWEnableDarkMode`, and wx's MSW dark mode does not reach `TaskDialog()`-based dialogs (`wxMessageBox`,
`wxMessageDialog`, `wxRichMessageDialog`, `wxProgressDialog`), the common dialogs or the date/time pickers
(`interface/wx/app.h:1436-1445`). Hence the `MsgDialog` family (`references/windows-dialogs.md`) and Orca's own
`ProgressDialog`.
- On macOS and Linux Orca follows the system appearance; native controls are themed by the toolkit, but any
light palette colour set on them still needs the `UpdateDarkUI` pass (`references/colours-dark-mode.md`).
- GTK theme borders bleed through native controls wrapped inside an owner-drawn frame, so wrappers strip them with
`Slic3r::GUI::RemoveInputBorder` (`TextInput`, `SpinInput`, `ComboBox`) or `RemoveButtonBorder` (`::CheckBox`,
`SwitchButton`) under `__WXGTK__` (`GUI_Utils.cpp`, a CSS provider on GTK3).
Design that every widget shares:
- **Colours are data.** Most widgets derive from `StaticBox` (a `wxWindow` painting a rounded rect, border, optional
vertical gradient and badge) whose colours are `StateColor`s resolved at paint time from state bits tracked by a
pushed `StateHandler` and from the current dark flag, so painted colours follow a theme switch on the next
`Refresh()`. `StaticBox::SetBackgroundColor(StateColor)` is the owner-drawn fill and is **not** wx's
`SetBackgroundColour`. Details: `references/painting-custom-widgets.md`, `references/colours-dark-mode.md §StateColor`.
- **Events mirror wx.** Widgets emit the native event types (`wxEVT_BUTTON`, `wxEVT_TOGGLEBUTTON`, `wxEVT_COMBOBOX`,
`wxEVT_SPINCTRL`, `wxEVT_RADIOBOX`) through `GetEventHandler()->ProcessEvent`, so command events propagate to
parents like native ones (`docs/doxygen/overviews/eventhandling.h:536-552`) and stop at dialogs
(`wxWS_EX_BLOCK_EVENTS`, `interface/wx/window.h:264-270`) and, on MSW and macOS, at popups ([source]
`src/common/popupcmn.cpp:135` sets the same flag; wxGTK's `wxPopupWindow::Create` never calls it,
`src/gtk/popupwin.cpp`). Propagation still works with the `StateHandler` pushed in front because the window's
`TryAfter` forwards to the parent ([source] `src/common/wincmn.cpp:3499-3522`). The differences from native
controls (ids, event objects, setter side effects) are in §Event semantics.
## Namespaces and the ::CheckBox clash
Widgets are in the **global namespace**, except these, which are in `Slic3r::GUI`: `DialogButtons`, `HyperLink`,
`ProgressDialog`, `RadioBox`, the `AMSControl`/`AMSItem` family, the `FanControl` family, `FilamentLoad`, the
`MultiNozzleSync` dialogs/tables, `SideTools`/`SideToolsPanel`, `WebViewHostDialog`, and the non-widget helpers of
`WebHosting.hpp` (namespace `Slic3r::GUI::web_hosting`) (check the header).
The `::` prefix matters because `Field.hpp` declares settings-field classes `Slic3r::GUI::CheckBox`, `TextCtrl`,
`SpinCtrl`, `Choice`, `StaticText` (plus `ColourPicker`, `PointCtrl`, `SliderCtrl`). Inside `namespace Slic3r::GUI`,
once `Field.hpp` is reachable (through `OptionsGroup.hpp`, `Tab.hpp`, …), unqualified `CheckBox` names the Field
class, which is not a `wxWindow`. `TextCtrl` also names the MSW `wxTextCtrl` subclass/typedef in
`Widgets/TextCtrl.h`.
- **Rule:** Qualify global widget types inside `Slic3r::GUI`, especially in `dynamic_cast`.
**Why:** `dynamic_cast<CheckBox*>(child)` compiles (the Field class has `msw_rescale()`), but never matches a
window, so the walk silently skips every `::CheckBox`. `PreferencesDialog::on_dpi_changed` has this shape — copy
its child walk, not its unqualified casts.
```cpp
// Wrong (in namespace Slic3r::GUI):
else if (auto* chk = dynamic_cast<CheckBox*>(child)) chk->msw_rescale(); // Field class: never matches
// Right:
else if (auto* chk = dynamic_cast<::CheckBox*>(child)) chk->Rescale();
```
- **Rule:** Forward-declare global widgets at global scope, before `namespace Slic3r {`.
**Why:** `class Button;` inside `namespace Slic3r::GUI` declares a distinct, never-defined `Slic3r::GUI::Button`;
members of that type cannot hold a `::Button*` and calls on them do not compile.
```cpp
class Button; class ComboBox; // Right: global, as MultiNozzleSync.hpp does
namespace Slic3r { namespace GUI {
class MyPanel : public wxPanel { ::Button* m_ok{nullptr}; ::ComboBox* m_combo{nullptr}; };
}}
```
## Catalog
| Widget (header) | Replaces | Base | Constructor | Must-know |
|---|---|---|---|---|
| `Button` | `wxButton`, `wxBitmapButton`, `ScalableButton` | `StaticBox` | `Button(parent, text, icon = "", style = 0, iconSize = 0, id = wxID_ANY)` | icon = SVG **name** from `resources/images/` (no path/extension), default 20 px; **id is last**; style with `SetStyle(...)`; emits `wxEVT_BUTTON`. §Button |
| `DialogButtons` (`Slic3r::GUI`) | `wxStdDialogButtonSizer`, `CreateStdDialogButtonSizer` | `wxPanel` | `DialogButtons(parent, {non-translated labels}, primary_translated_label = "", left_aligned_count = 0)` | labels → stock ids; styles primary/alert; self-rescales. §DialogButtons |
| `Label` | `wxStaticText` | `wxStaticText` | `Label(parent, text = "", style = 0, size)` (font `Body_14`), `Label(parent, font, text, style, size)` | `LB_AUTO_WRAP`, `LB_PROPAGATE_MOUSE_EVENT`, `LB_HYPERLINK` (via `SetWindowStyleFlag`); hosts the font table. §Label |
| `HyperLink` (`Slic3r::GUI`) | `wxHyperlinkCtrl` | `wxStaticText` | `HyperLink(parent, label = "", url = "", style = 0)` | opens `url` with `wxLaunchDefaultBrowser` on left-down. §Label |
| `::CheckBox` | `wxCheckBox` | `wxBitmapToggleButton` | `CheckBox(parent, id = wxID_ANY)` — **no label** | emits `wxEVT_TOGGLEBUTTON`; `SetHalfChecked`; `Rescale()`. §CheckBox |
| `SwitchButton` | on/off `wxCheckBox`, `wxToggleButton` | `wxBitmapToggleButton` | `SwitchButton(parent = nullptr, id = wxID_ANY)` | `SetLabels(on, off)`; track/thumb/text `StateColor`s; emits `wxEVT_TOGGLEBUTTON`. §SwitchButton family |
| `MultiSwitchButton` | segmented `wxRadioBox` | `StaticBox` | `MultiSwitchButton(parent, id, pos, size, style)` | emits `wxCUSTOMEVT_MULTISWITCH_SELECTION`, also from `SetSelection`. §SwitchButton family |
| `RadioGroup` | `wxRadioBox`, `wxRadioButton` rows | `wxPanel` | `RadioGroup(parent, std::vector<wxString> labels, wxHORIZONTAL/wxVERTICAL, row_col_limit = -1)` | emits `wxEVT_RADIOBOX` from **every** `SetSelection`. §RadioGroup |
| `::TextInput` | single-line `wxTextCtrl` | `wxNavigationEnabled<StaticBox>` | `TextInput(parent, text, label = "", icon = "", pos, size, style)` | value API on `GetTextCtrl()`; `label` is a painted side label. §TextInput |
| `::ComboBox` | `wxComboBox`, `wxChoice`, `wxBitmapComboBox` | `wxWindowWithItems<TextInput, wxItemContainer>` | `ComboBox(parent, id, value = "", pos, size, n = 0, choices = NULL, style = 0)` | `wxCB_READONLY` = choice; `SelectAndNotify`; `void*` client data only. §ComboBox |
| `DropDown` | native combo popup, menu used as a list | `PopupWindow` | `DropDown(parent, std::vector<Item>& items, style = 0)` | holds `items` **by reference**; `Invalidate()` after edits. §ComboBox |
| `SpinInput` | `wxSpinCtrl` | `wxNavigationEnabled<StaticBox>` | `SpinInput(parent, text, label = "", pos, size, style, min = 0, max = 100, initial = 0, step = 1)` | integer, non-negative typing; commits on Enter/kill-focus. §SpinInput |
| `TempInput` | temperature `wxTextCtrl` (device pages) | `wxNavigationEnabled<StaticBox>` | `TempInput(parent, type, text, TempInputType, label, normal_icon, active_icon, pos, size, style)` | warning icon + too-high/too-low states; posts `wxCUSTOMEVT_SET_TEMP_FINISH` to its **parent**. §SpinInput |
| `TabCtrl` | `wxNotebook` tab strip | `StaticBox` | `TabCtrl(parent, id, pos, size, style)` | one `Button` per tab; you switch the content. §Tab systems |
| `Notebook` (`GUI/Notebook.hpp`) | `wxNotebook` | `wxBookCtrlBase` | `Notebook(parent, id, pos, size, side_tools = NULL, style = 0)` | MainFrame's main tabs; pages addressed by name. §Tab systems |
| `LabeledStaticBox` | `wxStaticBox` | `wxStaticBox` | `LabeledStaticBox(parent, label = "", pos, size, style)` | usable as a `wxStaticBoxSizer` box; label fixed at `Create()`. §StaticBox… |
| `StaticBox` | plain bordered `wxPanel` | `wxWindow` | `StaticBox(parent, id, pos, size, style)` | rounded group frame; base of most widgets; `ShowBadge(bool)`. §StaticBox… |
| `StaticLine` | `wxStaticLine` | `wxWindow` | `StaticLine(parent, vertical = false, label = {}, icon = {})` | H/V separator with optional label + icon; `SetLineColour`. Avoid `wxStaticLine`. §StaticBox… |
| `ProgressBar` | `wxGauge` | `wxWindow` | `ProgressBar(parent, id = wxID_ANY, max = 100, pos, size, shown = false)` | plain `wxColour`s, unmapped; `Disable(wxString)`. §ProgressBar |
| `ScrolledWindow` + `MyScrollbar` | `wxScrolledWindow` with slim bars | `wxScrolled<wxWindow>` | `ScrolledWindow(parent, id, pos, size, style, marginWidth = 0, scrollbarWidth = 4, tipLength = 0)` | content on `GetPanel()`; vertical use. §ScrolledWindow |
| `PopupWindow` | `wxPopupTransientWindow` | `wxPopupTransientWindow` | `PopupWindow(parent, style = wxBORDER_NONE)` | per-platform dismissal hooks; MSW part opt-in. §PopupWindow |
| `ProgressDialog` (`Slic3r::GUI`) | `wxProgressDialog` | `wxDialog` | `ProgressDialog(title, message, maximum = 100, parent = NULL, style = wxPD_APP_MODAL \| wxPD_AUTO_HIDE, adaptive = false)` | styled copy of the generic dialog. §ProgressDialog… |
| `WebView` | `wxWebView::New` | static helpers | `WebView::CreateWebView(parent, url)` | see `references/webview-gl-aui-media.md`. §ProgressDialog… |
| `CheckList` | `wxCheckListBox` | `wxWindow` | `CheckList(parent, choices, scroll_style = wxVSCROLL)` | filterable multi-select list (`MultiChoiceDialog`). |
Field classes (`Field.cpp`) build their editors from these widgets (`TextCtrl` → `::TextInput`, `CheckBox` →
`::CheckBox`, `SpinCtrl` → `SpinInput`, `Choice` → `::ComboBox`); the field machinery is in
`references/orca-settings-ui.md`.
## Event semantics
| Widget | Emits on user action | Id / event object | Programmatic setters | Bind |
|---|---|---|---|---|
| `Button` | `wxEVT_BUTTON` (on release inside, Space/Enter) | button id / button | `SetValue(bool)` (Checked look) silent | on the button (Rule 6) |
| `::CheckBox`, `SwitchButton` | `wxEVT_TOGGLEBUTTON` | native id / widget | `SetValue`, `SetHalfChecked` silent (`interface/wx/tglbtn.h:98`) | on the widget; never `wxEVT_CHECKBOX` |
| `::ComboBox` | `wxEVT_COMBOBOX` (int = index, string = text) from popup pick, arrow keys (read-only combo), `SelectAndNotify`; `wxEVT_COMBOBOX_DROPDOWN`/`_CLOSEUP` | COMBOBOX: combo's auto id / combo; DROPDOWN/CLOSEUP: **id 0, no object** | `SetSelection`, `SetValue` silent for COMBOBOX; editable or `CB_NO_TEXT`: they send `wxEVT_TEXT` | on the combo |
| `::TextInput` | inner `wxEVT_TEXT` (propagates); `wxEVT_TEXT_ENTER`, `wxEVT_KILL_FOCUS` re-sent to the wrapper only | TEXT: **inner** id/object; ENTER/KILL_FOCUS: wrapper id, inner object | `GetTextCtrl()->SetValue` sends `wxEVT_TEXT`; `ChangeValue` silent | TEXT: wrapper, inner, or ancestor by type; ENTER/KILL_FOCUS: wrapper or inner |
| `SpinInput` | `wxEVT_SPINCTRL` (a plain `wxCommandEvent`, no int) on commit; `EVT_SPINCTRL_TEXT` (int + string) per parseable keystroke; inner `wxEVT_TEXT` | spinner id / spinner | `SetValue` sends `EVT_SPINCTRL_TEXT` + `wxEVT_TEXT`, no `wxEVT_SPINCTRL` | on the spinner; handler takes `wxCommandEvent&` |
| `RadioGroup` | `wxEVT_RADIOBOX` (int, string) | group id / **no object** | **every** `SetSelection(i)` emits, same index included | on the group |
| `TabCtrl` | `wxEVT_TAB_SEL_CHANGING` (int = old index, not vetoable), then `wxEVT_TAB_SEL_CHANGED` (int = new) | ctrl id / ctrl | `SelectItem(i)` emits both when `i` changes | on the ctrl |
| `Notebook` | tab click posts `wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED` (id = page index), then `wxEVT_NOTEBOOK_PAGE_CHANGING`/`_CHANGED` | — | `SetSelection` emits PAGE_*; `ChangeSelection` silent | on the notebook |
| `MultiSwitchButton` | `wxCUSTOMEVT_MULTISWITCH_SELECTION` (int, string) | id / widget | `SetSelection` emits when the index changes | on the widget |
| `SwitchBoard` | `wxCUSTOMEVT_SWITCH_POS` (int: 1 = left half, 0 = right half), **posted** | id 0 / none | — | on the board |
| `ModeSwitchButton` | none — writes the app mode (`wxGetApp().save_mode`) | — | `SetSelection` silent | — |
| `DropDown` (standalone) | `wxEVT_COMBOBOX` (int, string), `EVT_DISMISS` | COMBOBOX: popup id / popup; EVT_DISMISS: id 0, no object | — | on the DropDown (popups block propagation on MSW/macOS, not on GTK) |
| `TempInput` | `wxCUSTOMEVT_SET_TEMP_FINISH` on commit, **posted to the parent** (int = the ctor's `type`, string = the `TempInputType` number) | id 0 / none | — | on the **parent**; tell inputs apart by the int |
Consequences that recur:
- **Ids.** `ComboBox` calls `TextInput::Create`, which takes no id and creates the window with `wxID_ANY`, so the
combo's `id` argument is ignored; `TextInput` and
`SpinInput` have no id parameter. `parent->Bind(wxEVT_COMBOBOX, h, ID_MY_COMBO)` never fires. Bind on the widget,
or call `SetId()` after construction if an id filter is unavoidable.
- **Handler order.** Dynamic handlers run most-recently-bound first, before static event tables
(`interface/wx/event.h:592-593`). Widgets bind their internal handlers in the constructor (or use event tables),
so yours run first: a handler that does not `Skip()` cuts off the widget's own behaviour (bitmap refresh, commit,
click). A handler taking the event **by value** cannot `Skip()` the real event (`references/events.md §Skip
discipline`).
- **Setters vs wx.** wx's rule is that setters are silent (`interface/wx/ctrlsub.h:102-103`); `wxTextEntry::SetValue`
and `wxBookCtrlBase::SetSelection` are the documented exceptions (`references/controls-dataview.md §Events from
programmatic changes`). The table's emitting setters re-enter change handlers synchronously inside model→view
refreshes: guard with a flag or use the silent variant.
## Custom vs raw
Rules:
1. Interactive controls in new or modified UI: Orca widgets, always. Bottom button rows: `DialogButtons`. When you
modify an older dialog built from raw controls, convert the controls you touch; untouched raw controls are not a
model.
2. Static text: plain `wxStaticText` with a `Label::Body_*` font is fine; use `Label` for auto-wrap and the hyperlink
look, `HyperLink` for a link that opens a URL. Fonts come from the `Label` statics (`Head_10…Head_48` bold for
titles, `Body_8…Body_16` for content, `Body_14` the dialog default); never hardcode point sizes, because
`Label::sysFont` already scales them per platform (`references/dpi-bitmaps-fonts.md`).
3. Containers stay raw: `wxPanel`, `wxBoxSizer`, `wxScrolledWindow`, `wxSimplebook`. `StaticBox`/`LabeledStaticBox`
only for the rounded-border group look; `ScrolledWindow` only for the slim-scrollbar look.
4. If a raw control is unavoidable, make it dark-safe: `wxGetApp().UpdateDarkUI(ctrl)` / `UpdateDlgDarkUI(dlg)` or
explicit `StateColor::darkModeColorFor()` (`references/colours-dark-mode.md`).
5. Every custom widget inside a `DPIDialog`/`DPIFrame` gets its `Rescale()` called from `on_dpi_changed`
(§Shared lifecycle rules).
Mixed is normal: `CloneDialog::CloneDialog` pairs raw `wxStaticText` labels with `SpinInput`, `::CheckBox`,
`ProgressBar` and `DialogButtons` (copy its layout, not its OK handler, which runs a `wxYield()` loop inside a frozen
plater).
No Orca replacement exists — use raw wx (`references/controls-dataview.md`) for: multi-line text (`wxTextCtrl` +
`wxTE_MULTILINE`, as Field does for multi-line options), headerless page stacks (`wxSimplebook`), data views and grids
(`wxDataViewCtrl`, `wxGrid`), `wxSlider`, `wxColourPickerCtrl`, `wxSplitterWindow`, `wxStaticBitmap`, and the
`Slic3r::GUI::BitmapComboBox` wrapper where a `wxBitmapComboBox` is required.
## Shared lifecycle rules
**Background snapshot.** `StaticBox::Create`, `Label`, `SwitchButton` (through `StaticBox::GetParentBackgroundColor`:
a parent `StaticBox`'s default fill — the midpoint for a gradient — else `parent->GetBackgroundColour()`), and
`::CheckBox`, `RadioGroup` (`parent->GetBackgroundColour()`) copy the parent's background colour at construction;
wx itself never inherits a background colour ([source] `src/common/wincmn.cpp:1543-1552`).
- **Rule:** Set the container's background before creating widgets in it.
```cpp
auto* panel = new wxPanel(this);
auto* cb = new ::CheckBox(panel); // Wrong: snapshots the panel's default colour
panel->SetBackgroundColour(*wxWHITE);
// Right: SetBackgroundColour first, then create children
```
**Enable state.** A parent's `Enable()`/`Disable()` never calls a child's virtual `Enable()`: on MSW and macOS it
reaches children through `NotifyWindowOnEnableChange` → `DoEnable()`, on GTK the toolkit propagates sensitivity
natively ([source] `src/common/wincmn.cpp:1147-1201`). Orca widgets update their `Enabled` state bit only from
`EVT_ENABLE_CHANGED`, which their `Enable()` override emits — so after `panel->Disable()` they ignore input
(`IsEnabled()` is false, `interface/wx/window.h:3060-3069`) but the `StateColor`-painted ones (`Button`, `TextInput`,
`ComboBox`, `SpinInput`, `RadioGroup`'s labels, …) still paint enabled colours. [source] `::CheckBox`, a native
`wxBitmapToggleButton`, greys with its parent on every port: MSW `EnableWindow`s it and the owner-drawn button paints
its disabled bitmap (`src/msw/window.cpp:576-593`, `src/msw/anybutton.cpp:846-871`, `:969-972`, `:1502`); GTK3's
button image draws a greyed copy of the current bitmap while `!IsEnabled()` (`src/gtk/image_gtk.cpp:37-42`), though
the disabled bitmap itself needs `IsThisEnabled()` false (`src/gtk/anybutton.cpp:140-146`); macOS sends
`setEnabled:NO` (`src/osx/cocoa/window.mm:3784-3791`) and AppKit dims the image (`NSButtonCell`
`imageDimsWhenDisabled`, default YES).
- **Rule:** Enable/disable each custom widget directly (`RadioGroup::Enable` does this for its own buttons).
```cpp
m_options_panel->Enable(on); // Wrong: widgets keep the enabled look
for (wxWindow* w : std::initializer_list<wxWindow*>{m_combo, m_spin, m_check})
w->Enable(on); // Right: virtual Enable() per widget
```
**DPI.** Widgets size themselves from `FromDIP` values and cached bitmaps; after a DPI change the owning
`DPIDialog::on_dpi_changed` must call `Rescale()` on each (`Button`, `::CheckBox`, `::TextInput`, `::ComboBox`
(also rescales its `DropDown`), `SpinInput`, `SwitchButton`, `MultiSwitchButton`, `TabCtrl`, `StaticLine`,
`ModeSwitchButton`). Exceptions: `RadioGroup`, `Label`, `HyperLink` and `LabeledStaticBox` (its scale is fixed in
`Create()`) have no `Rescale()`; `ProgressBar::Rescale()` is empty; `DialogButtons` rescales itself. Pass sizes through
`FromDIP` (`references/dpi-bitmaps-fonts.md`).
```cpp
void MyDialog::on_dpi_changed(const wxRect&) {
m_ok_btn->Rescale(); m_combo->Rescale(); m_check->Rescale(); // ::CheckBox has Rescale(), not msw_rescale()
m_combo->SetMinSize(wxSize(FromDIP(160), -1)); // re-apply explicit sizes
GetSizer()->SetSizeHints(this); Refresh(); // resize + new minimum (sizers-layout.md)
}
```
**Theme switch.** `StateColor`s follow the dark flag at paint time. What was captured once does not: the wx
background copied at construction, colours baked into bitmaps (`SwitchButton` labels, `::CheckBox`/`RadioGroup`
SVGs), `ProgressBar` colours, `Label` foreground. The `Update*DarkUI` walk re-maps the wx colours among them that
are `gDarkColors` keys (`Label`'s `#262E30`, a palette background) when it reaches the widget, nothing else.
Re-apply them in `on_sys_color_changed()` (call `Rescale()` where
it re-reads colours, e.g. `SwitchButton`), and keep `UpdateDlgDarkUI(this)` as the last constructor line
(`references/colours-dark-mode.md §Runtime theme switch and re-applying colours`).
**Sizing.** No widget implements `DoGetBestSize`: the text/icon widgets set their min size in `messureSize()` whenever
a label, font, icon, style or DPI changes, and the bitmap toggles (`::CheckBox`, `SwitchButton`) size themselves
to their bitmap in `Rescale()`.
Nothing re-lays out the parent: call `Layout()` on the container after changing a widget's content at runtime
(`references/sizers-layout.md`).
**Mouse capture.** `Button`, `DropDown`, `SpinInput`'s arrows, `ModeSwitchButton`, `MyScrollbar`, `StepCtrl` and the
device-page `SideButton`, `ImageSwitchButton` and `AxisCtrlButton` capture the mouse while pressed; a capture leak
shows on macOS as a UI that is alive but unclickable (`references/mouse-keyboard-focus.md §Mouse capture`).
## Button
`Button` (`Widgets/Button.cpp`) is a `StaticBox` with label, optional icon (`ScalableBitmap` from an SVG name), focus
ring and an optional toggle look. `Create` sets `Label::Body_14` and measures; `messureSize()` sets the min size from
text + icon + padding, capped in width by `SetMaxSize` (the label then becomes the tooltip if none is set); a size
given to `Button::SetMinSize` is stored, floors the width and, when its height is > 0, replaces the measured height.
`SetStyle(ButtonStyle, ButtonType)` is the one call that makes it look like an Orca button — don't hand-roll button
colours in new code:
| `ButtonType` | Padding / min size / radius | Font | Use |
|---|---|---|---|
| `Compact` | padding 8×3, radius 8 | `Body_10` | tight spaces |
| `Window` | size and min 58×24, radius 12 | `Body_12` | buttons in windows, away from parameter boxes |
| `Choice` | min 100×32, padding 12×8, radius 4 | `Body_14` | dialog/window choice buttons (`DialogButtons` uses it) |
| `Parameter` | size and min 120×26, radius 4 | `Body_14` | buttons next to parameter boxes |
| `Icon` | padding 5×5, size and min 26×26, radius 4 | — | icon-only; create with `iconSize = 16` and a 16 px icon |
| `Expanded` | min height 32, padding 12×8, radius 4 | `Body_14` | full-width buttons, e.g. inside a static box |
All values go through `FromDIP`. `ButtonStyle::{Regular, Confirm, Alert, Disabled}` picks the background, border and
text `StateColor`s from the `btn_regular/btn_confirm/btn_alert/btn_disabled` tables in `Button.cpp` (palette colours
where the dark map should apply; the focus-border colour is chosen per theme when `SetStyle` runs). `ButtonStyle::Disabled` is a **look**; it does not call `Enable(false)`.
```cpp
auto* export_btn = new Button(this, _L("Export"));
export_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Choice);
export_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { on_export(); });
auto* reload_btn = new Button(parent, wxEmptyString, "refresh", 0, 16); // icon-only (PreferencesDialog)
reload_btn->SetStyle(ButtonStyle::Regular, ButtonType::Icon);
```
Behaviour (`Button::mouseDown`, `Button::mouseReleased`, `Button::keyDownUp`):
- Left-down focuses (if focusable) and captures; left-up releases and sends `wxEVT_BUTTON` (id = window id, event
object = button) when the pointer is inside. Space/Enter synthesise down/up, so a focused Button clicks on key-up;
on MSW it claims `WM_GETDLGCODE` so Enter reaches it instead of the dialog's default-button logic.
- Tab is turned into navigation; arrow keys are swallowed (`HandleAsNavigationKey` handles only Tab, [source]
`src/common/wincmn.cpp:3566-3583`), which is why `DialogButtons` adds its own arrow handler.
- `SetValue(bool)`/`GetValue()` drive the `Checked` state bit (used by `TabCtrl` and `MultiSwitchButton`); it is
visible only with `StateColor`s that have `Checked` entries — the `SetStyle` tables have none, the unstyled default
does. Clicks do not toggle it.
- `SetCanFocus(false)` keeps it out of focus (`AcceptsFocus()` returns the flag). `EnableTooltipEvenDisabled()` shows
the tooltip on a disabled button by watching the parent's motion — MSW only (elsewhere a no-op).
- `SetIndicator(bool)` draws a small dot after the label (TabCtrl uses it); `SetVertical`, `SetCenter`,
`SetPaddingSize`, `SetIconSpacing`, `SetTextColor(StateColor)`, `SetIcon(name | wxBitmap)`.
- `Rescale()` re-rasterises a **named** icon (one set from a `wxBitmap` has no source and stays as is), re-measures and
re-runs `SetStyle` with the stored style/type.
Pitfalls:
- **Rule:** Call `SetStyle` on every Button you create.
**Why:** the unstyled defaults are wx stock colours (`*wxLIGHT_GREY` hover, `*wxBLACK` text) chosen for no Orca
design and partly off the dark map, with generic metrics (padding 10×8, `StaticBox` radius 8, `Body_14`) instead of
a `ButtonType`'s, and no focus border.
- **Rule:** Re-apply your own min size, size or font after `Rescale()`.
**Why:** `Rescale()` re-runs `SetStyle`, which resets min size, padding, radius and font for the type.
```cpp
btn->SetStyle(ButtonStyle::Regular, ButtonType::Choice);
btn->SetMinSize(wxSize(FromDIP(160), FromDIP(32))); // lost on the next Rescale()…
// Right: in on_dpi_changed: btn->Rescale(); btn->SetMinSize(wxSize(FromDIP(160), FromDIP(32)));
```
- **Rule:** A `wxEVT_LEFT_DOWN`/`_UP` handler bound on a Button must `Skip()`.
**Why:** the click logic lives in the Button's static event table, which runs after dynamic handlers.
- **Rule:** Do not rely on Button's capture-lost handling to cancel a press.
**Why:** `Button::mouseCaptureLost` replays release with a default `wxMouseEvent` at (0,0), which is inside the
button, so losing capture mid-press **sends `wxEVT_BUTTON`**. The lost event comes on MSW and GTK and never on
macOS ([source] `src/msw/window.cpp:5186`, `src/gtk/window.cpp:6808-6814`; `interface/wx/event.h:3507` says
Windows only — `references/mouse-keyboard-focus.md §Per-port delivery`). wx's contract is to cancel the
operation (`interface/wx/window.h:3815-3817`). `ModeSwitchButton::mouseCaptureLost` is the correct shape (clear
the pressed flag, no action), minus its `Skip()` (a lost handler must not skip).
## DialogButtons
`Slic3r::GUI::DialogButtons` (`Widgets/DialogButtons.cpp`) is a `wxPanel` that builds the standard bottom row: one
`Button` per label, all `ButtonStyle::Regular` + `ButtonType::Choice`, gaps of `ButtonProps::ChoiceButtonGap()`.
```cpp
auto* dlg_btns = new DialogButtons(this, {"OK", "Cancel"}); // NOT pre-translated
dlg_btns->GetOK()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { apply(); EndModal(wxID_OK); }); // no Skip()
sizer->Add(dlg_btns, 0, wxEXPAND);
```
The full dialog recipe (DPIDialog, `SetSizerAndFit`, `CenterOnParent`, `UpdateDlgDarkUI`) is in
`references/windows-dialogs.md`.
Constructor contract:
- **Labels** are untranslated; the constructor shows `_L(label)` and matches `label` lower-cased against a stock-id
map: `ok`→`wxID_OK`, `yes`→`wxID_YES`, `apply` and `confirm`→`wxID_APPLY`, `no`→`wxID_NO`, `cancel`→`wxID_CANCEL`,
`open`→`wxID_PRINT` (the map's first `"open"` entry wins), `add`, `copy`, `new`, `save`, `save as`, `refresh`,
`retry`, `ignore`, `help`, `clone`/`duplicate`→`wxID_DUPLICATE`, `select all`, `replace`, `replace all`,
`return`→`wxID_BACKWARD`, `next`→`wxID_FORWARD`, `remove`, `delete`, `abort`, `stop`, `reset`, `clear`,
`exit`/`quit`→`wxID_EXIT`. Other labels keep an auto id.
- **Primary** (`ButtonStyle::Confirm`): the 2nd argument is a *translated* label (`_L("Create")`) naming it;
empty → the only button if there is one, else the first present of `{wxID_OK, wxID_YES, wxID_APPLY, wxID_SAVE,
wxID_PRINT}` in **numeric id order** (a `std::set`: `wxID_SAVE` < `wxID_PRINT` < `wxID_OK` < `wxID_APPLY` <
`wxID_YES`), so `{"Save", "OK"}` makes Save primary. A label that matches no button means no primary. The primary
takes focus only when nothing in the app has focus.
- **Alert** (`ButtonStyle::Alert`): only with ≥ 2 buttons, the first present (numeric order) of `wxID_EXIT`,
`wxID_CLEAR`, `wxID_DELETE`, `wxID_RESET`, `wxID_ABORT`, `wxID_REMOVE`, `wxID_STOP`; `SetAlertButton(translated)`
picks another.
- **`left_aligned_count`** pins the first N buttons to the left (`SetLeftAlignedButtonsCount` later).
- Getters: `GetOK/GetYES/GetAPPLY/GetCONFIRM (= wxID_APPLY)/GetNO/GetCANCEL/GetRETURN/GetNEXT/GetFIRST/GetLAST`,
`GetButtonFromID`, `GetButtonFromLabel(translated)`, `GetButtonFromIndex`.
How clicks close the dialog: a click sends `wxEVT_BUTTON` with the stock id; unhandled, it propagates to
`wxDialogBase::OnButton` (`src/common/dlgcmn.cpp:105,455-478`): the affirmative id (`wxID_OK` by default) runs
`AcceptAndClose()` = `Validate()` + `TransferDataFromWindow()` then `EndDialog(wxID_OK)` (:369-375); `wxID_APPLY`
validates and transfers without closing; `wxID_CANCEL` → `EndDialog(wxID_CANCEL)`; any other id is skipped. So
OK/Cancel close by themselves; Yes, No, Confirm/Apply and custom labels do nothing until you bind them. wx's
ESC→button emulation looks for a real `wxButton` (`EmulateButtonClickIfPresent`, :387-404) and never finds an Orca
`Button`; ESC handling is in `references/windows-dialogs.md`.
DPI: the constructor binds the **parent's** `wxEVT_DPI_CHANGED` (unbound in the destructor) and `Skip()`s it; being
bound after `DPIAware`'s handler it runs first, then the dialog's `on_dpi_changed` — the dialog does not rescale
DialogButtons itself.
Pitfalls:
- **Rule:** A handler that calls `EndModal` must not `Skip()`; bind every button whose default handling is not what
you want.
**Why:** `Skip()` lets the event reach `wxDialogBase::OnButton`, which validates and transfers again and ends the
dialog a second time with its own code (`EndDialog(affirmative id)` / `EndDialog(wxID_CANCEL)`). `EndDialog` only
hides a dialog that no longer reports `IsModal()` (GTK, macOS reset it in the first `EndModal`), but on MSW
`IsModal()` stays true until `ShowModal` returns, so `EndModal` runs again and replaces the return code you passed
([source] `src/common/dlgcmn.cpp` `wxDialogBase::EndDialog`, `include/wx/msw/dialog.h` `IsModal`;
`references/windows-dialogs.md`). Yes/No/Confirm/custom buttons never close on their own.
- **Rule:** Every custom label must be in the translation catalog: write it as `L("Skip for Now")` in the vector, or
make sure the same string appears in a `_L()` elsewhere.
**Why:** the constructor's `_L(label)` translates a variable, which xgettext cannot see, so a label used only there
never reaches `OrcaSlicer.pot` and always shows in English. `L()` is a no-op marker that xgettext extracts
(`references/strings-i18n-files.md`).
```cpp
new DialogButtons(this, {"Download and Install", "Skip for Now"}); // Wrong: untranslatable
new DialogButtons(this, {L("Download and Install"), L("Skip for Now")}); // Right
```
- **Rule:** Don't call `UpdateButtons()` (or `SetLeftAlignedButtonsCount`) repeatedly in your code.
**Why:** each call re-binds `wxEVT_KEY_DOWN` on every button (wx `Bind` does not de-duplicate) and the handler
`Skip()`s, so arrow-key focus moves repeat once per accumulated binding; DPI changes already add one each.
- **Rule:** Don't expect the row to take the dialog's background.
**Why:** the panel paints `darkModeColorFor("#FFFFFF")`, re-applied on every `UpdateButtons()` (DPI change); on a
non-white dialog the row stands out.
## Label and HyperLink
`Label` (`Widgets/Label.cpp`) is a `wxStaticText` with font `Body_14` (or the font passed), foreground `#262E30` and
the parent's background. Style bits (no clash with `wxST_*`):
| Bit | Value | Effect |
|---|---|---|
| `LB_HYPERLINK` | `0x20` | underlined font, `#009688`, hand cursor — **only** when set through `SetWindowStyleFlag` |
| `LB_PROPAGATE_MOUSE_EVENT` | `0x40` | left-down/up are forwarded to the parent's handler with `ProcessEventLocally` (label-relative position, label as event object) and consumed |
| `LB_AUTO_WRAP` | `0x80` | `Label::Wrap(GetSize().x)` on every `wxEVT_SIZE` |
`Label::Wrap(width)` is Orca's own wrapper over the stored text and has no width cache, unlike 3.3.2's
`wxStaticText::Wrap` (`references/controls-dataview.md §wxStaticText`). `Label::split_lines(dc, width, text, out,
max_count)` wraps text for owner-drawn code. The static fonts (`Head_*`, `Body_*`) are built by
`Label::initSysFont()` from `GUI_App::on_init_inner` (`references/dpi-bitmaps-fonts.md`).
`Slic3r::GUI::HyperLink` (`Widgets/HyperLink.cpp`) is a `wxStaticText` with `Head_14` (kept underlined by its
`SetFont`), colour `#009687` — deliberately one off the palette `#009688` so the dark map leaves it alone — hover
`#26A69A`, hand cursor, the URL as tooltip, and `wxLaunchDefaultBrowser(url)` on left-down when the URL is non-empty.
Its `style` argument is unused.
Pitfalls:
- **Rule:** Apply `LB_HYPERLINK` through `SetWindowStyleFlag`, not the constructor; it is a look, not a link.
**Why:** the constructor only stores the bit, and `SetWindowStyleFlag` returns early when the style is unchanged,
so a label created with the bit never gets the look by re-applying it. Clicks still need your handler (or use
`HyperLink`).
On macOS the hyperlink label is set with `SetLabelMarkup`, so quote user text (`wxMarkupParser::Quote`).
```cpp
new Label(this, _L("Learn more"), LB_HYPERLINK); // Wrong: stays plain text
auto* lbl = new Label(this, _L("Learn more")); // Right
lbl->SetWindowStyleFlag(lbl->GetWindowStyle() | LB_HYPERLINK);
lbl->Bind(wxEVT_LEFT_DOWN, [url](wxMouseEvent&) { wxLaunchDefaultBrowser(url); });
```
- **Rule:** For a `HyperLink` with a custom action, leave the URL empty and bind `wxEVT_LEFT_DOWN`.
**Why:** with a URL its own handler opens the browser; your later-bound handler runs first and would have to
`Skip()` to keep it.
## CheckBox and RadioBox
`::CheckBox` (`Widgets/CheckBox.cpp`) is a `wxBitmapToggleButton` (`wxBORDER_NONE`) showing 18 px SVGs
`check_{on,half,off}`, `…_disabled`, `…_focused`. It has **no label**: pair it with a `wxStaticText`/`Label`
(as `CloneDialog::CloneDialog` does). `GetValue()` is a 2-state `bool`.
- Emits `wxEVT_TOGGLEBUTTON` on click (native). `SetValue` emits nothing (`interface/wx/tglbtn.h:98`).
- `SetHalfChecked(true)` is a drawn-only third state (`IsHalfChecked()` exists for the inspector only); the widget's own toggle handler
clears it on any click. `SetValue` does **not** clear it — call `SetHalfChecked(false)` before showing a definite
state.
- `Rescale()` (no `msw_rescale()`) re-rasterises all nine bitmaps and resets size/min size.
- Platform paths: macOS emulates the disabled/focused/hover bitmaps (`CheckBox::Enable` override,
`DoGetBitmap`, `updateBitmap`), but wxOSX hands the `NSButton` `m_bitmaps[State_Current]`/`m_bitmaps[State_Normal]`
directly and calls `DoGetBitmap` only for an `IsOk()` test ([source] `src/osx/anybutton_osx.cpp:84-94`), so only
the hover (`_focused`) art shows and a disabled box shows its normal art dimmed by AppKit; MSW sets a focus bitmap;
GTK strips the theme border.
`Slic3r::GUI::RadioBox` is an older single bitmap radio (`wxBitmapToggleButton`); use `RadioGroup` for new radio
sets.
Pitfalls:
- **Rule:** Bind `wxEVT_TOGGLEBUTTON`, never `wxEVT_CHECKBOX`, and `Skip()` in the handler.
**Why:** `wxEVT_CHECKBOX` is never sent. The widget's own `wxEVT_TOGGLEBUTTON` handler (bound in the constructor)
runs **after** yours and is what swaps the bitmap (`CheckBox::update`); without `Skip()` the value changes but the
box keeps showing the old state.
```cpp
cb->Bind(wxEVT_CHECKBOX, h); // Wrong: never fires
cb->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) { apply(); }); // Wrong: bitmap not refreshed
cb->Bind(wxEVT_TOGGLEBUTTON, [this](wxCommandEvent& e) { e.Skip(); apply(); }); // Right
```
## SwitchButton family
All in `Widgets/SwitchButton.hpp/.cpp`:
- **`SwitchButton`** — `wxBitmapToggleButton` (`wxBORDER_NONE | wxBU_EXACTFIT`), font `Body_12`. Without labels it
shows the `toggle_on`/`toggle_off` SVGs; `SetLabels(on, off)` switches to a two-segment pill drawn into bitmaps from
the track/thumb/text `StateColor`s (`SetTrackColor`, `SetThumbColor`, `SetTextColor`, `SetTextColor2`), narrowed
to `GetMaxWidth()` by shrinking the font. Emits `wxEVT_TOGGLEBUTTON`; `SetValue` is silent; its own toggle handler
(refreshes the bitmap) runs after yours — `Skip()`, as for `::CheckBox`. Every colour/label setter and
`SetBackgroundColour` call `Rescale()`, which re-rasterises the SVGs or, with labels, re-reads the parent background
and re-bakes the pill bitmaps with the current dark mapping: call it on DPI **and** theme change.
- **`ModeSwitchButton`** — the 3-position Simple/Advanced/Expert control (`StaticBox`, `doRender`), plus `SetDevMode`.
It sends no event: a click calls `SelectAndNotify`, which writes the app mode through `wxGetApp().save_mode()` and
is ignored in dev mode or when disabled. `SetSelection` is silent and clamps to 0..2.
- **`MultiSwitchButton`** — a segmented control of `Button`s in an internal `wxScrolledWindow` (scrolls instead of
clipping when squeezed; `SetFitToOptions`). `AppendOption/SetOptions/DeleteAllOptions`, per-option text/client data,
`GetButton(i)`. Emits `wxCUSTOMEVT_MULTISWITCH_SELECTION` (int = index, string = text) whenever the selection
changes — **including programmatic `SetSelection`**.
- **`SwitchBoard`** — a two-label device-page switch (a plain `wxWindow`). It **posts** `wxCUSTOMEVT_SWITCH_POS`
(int = 1 for a click on the left half, 0 for the right half; no id, no event object). Its `Enable()` only flips a
private flag and repaints, and its non-virtual `IsEnabled()` hides the base one: the window stays enabled for wx.
`SetAutoDisableWhenSwitch()` makes a click set that flag off until you re-enable it.
## RadioGroup
`RadioGroup` (`Widgets/RadioGroup.cpp`) is a `wxPanel` of `wxStaticBitmap` radio icons plus `Button` labels in a
`wxFlexGridSizer`; `row_col_limit` (−1 = one row for `wxHORIZONTAL`, one column for `wxVERTICAL`) wraps the items
into a grid — check the row/column arithmetic in `RadioGroup::Create` before relying on a layout. Keyboard:
Right/Down and Left/Up on the focused label move the selection with wrap-around (`SelectNext`/`SelectPrevious`);
only the selected label button is focusable, so Tab enters the group once.
`SetRadioTooltip(i, tip)`, `Enable()` (also enables the label buttons and emits `EVT_ENABLE_CHANGED`). There is no
`Rescale()`.
- **Rule:** Treat every `SetSelection(i)` as an event source.
**Why:** it always sends `wxEVT_COMMAND_RADIOBOX_SELECTED` (= `wxEVT_RADIOBOX`), for programmatic calls and for the
current index too — the opposite of `wxRadioBox::SetSelection` (`interface/wx/ctrlsub.h:102-103`). The constructor
fires one before anyone can bind.
```cpp
m_group->SetSelection(cfg.mode); // Wrong: re-enters on_mode_changed
{ m_syncing = true; m_group->SetSelection(cfg.mode); m_syncing = false; } // Right: handler returns if m_syncing
```
- **Rule:** Don't read `GetEventObject()` in its handler.
**Why:** the event carries the group's id but no event object (null).
## TextInput
`::TextInput` (`Widgets/TextInput.cpp`, `TextInput::Create`) is a `wxNavigationEnabled<StaticBox>` frame around a
real `wxTextCtrl` (on MSW the `TextCtrl` subclass in `Widgets/TextCtrl.h`, which overrides `DoMSWControlColor`).
- `text` is the initial value; `label` is a **painted side label** (`Body_12`), `icon` a 16 px SVG name
(`SetIcon`, `SetIcon_1` for a second icon, `SetStaticTips` for a grey hint line under the label).
- `style` goes to the inner control with `wxTE_PROCESS_ENTER | wxBORDER_NONE` added and alignment bits stripped;
the wrapper keeps the alignment bits and uses them to place label and icons, so typed text is left-aligned unless
you set `wxTE_RIGHT`/`wxTE_CENTRE` on `GetTextCtrl()` afterwards (alignment can change after creation on MSW, GTK
and macOS, `interface/wx/textctrl.h:1398-1399`).
- Inner font `Body_14`, inner context menu disabled (empty `wxEVT_RIGHT_DOWN` handler), tooltips forwarded
(`DoSetToolTipText`), `Enable()` also enables the inner control and recolours it.
- `TextInput::SetLabel()` changes the side label, not the text. The value API is `GetTextCtrl()->GetValue()/
SetValue()/ChangeValue()`; `SetHint`, validators and `SetMaxLength` also go on `GetTextCtrl()`.
Event routing:
- `wxEVT_TEXT` is a command event from the inner control; it propagates to the wrapper and beyond, carrying the
**inner** control's id and event object.
- `wxEVT_TEXT_ENTER` and `wxEVT_KILL_FOCUS` are caught on the inner control, `OnEdit()` runs (ComboBox resolves the
typed text there), then they are re-sent with the wrapper's id via `ProcessEventLocally`, which runs the wrapper's
own handlers but [source] never `TryAfter`, so never the parents (`src/common/event.cpp:1582-1589`; the
`interface/wx/event.h:626-651` text says it calls `TryAfter`). The event object stays the inner control.
- The internal ENTER handler does not `Skip()`: Enter in a TextInput never activates a dialog's default button
(`interface/wx/textctrl.h:1324-1331`).
- Key, char and focus-in events exist only on `GetTextCtrl()`.
Pitfalls:
- **Rule:** Bind ENTER and KILL_FOCUS on the TextInput (or `GetTextCtrl()`), never on a parent; bind TEXT on the
input without an id filter.
```cpp
dialog->Bind(wxEVT_TEXT_ENTER, &Dlg::on_enter, this, input->GetId()); // Wrong: never reaches the dialog
input->Bind(wxEVT_TEXT_ENTER, &Dlg::on_enter, this); // Right
panel->Bind(wxEVT_TEXT, h, input->GetId()); // Wrong: TEXT carries the inner id
input->Bind(wxEVT_TEXT, h); // Right
```
Cite: `TextInput::Create`; `src/common/event.cpp:1582-1589`.
- **Rule:** A handler bound on `GetTextCtrl()` for ENTER or KILL_FOCUS must `Skip()`.
**Why:** it runs before the internal handler (most-recently-bound first), which does `OnEdit()` and the re-dispatch.
- **Rule:** Don't `static_cast` the event object to `TextInput*`.
**Why:** every TextInput event's object is the inner `wxTextCtrl`.
- **Rule:** Use `ChangeValue` for model→view updates.
**Why:** `wxTextEntry::SetValue` sends `wxEVT_TEXT` (`interface/wx/textentry.h:539-542`), [source] even for
identical text (`src/common/textentrycmn.cpp:236-254`).
- **Rule:** Use a raw `wxTextCtrl` with `wxTE_MULTILINE` for multi-line text.
**Why:** `TextInput::DoSetSize` sets only the inner control's width and keeps it vertically centred at the height
it got at creation (its initial best size), so a multi-line TextInput never grows its text area. Hints on
multi-line controls are ignored except on MSW and GTK2 (`interface/wx/textentry.h:485-486`), so not on macOS or
GTK3; on macOS call `OSXDisableAllSmartSubstitutions()`
on any control holding G-code, paths or URLs (`references/controls-dataview.md §wxTextCtrl`).
## ComboBox and DropDown
`::ComboBox` (`Widgets/ComboBox.cpp`) is `wxWindowWithItems<TextInput, wxItemContainer>` plus an owned `DropDown drop`
over its `std::vector<DropDown::Item> items`, so the `wxItemContainer` API (`Append/Insert/Set/Clear/Delete/
GetCount/GetString/FindString/GetSelection/SetSelection/GetStringSelection`) works, and `GetValue/SetValue` mirror the
wxTextEntry side of `wxComboBox`. `ComboBox::SetLabel/GetLabel` are the displayed value — the opposite of
`TextInput::SetLabel` — and `SetTextLabel/GetTextLabel` reach the painted side label.
- `wxCB_READONLY` hides the text control and paints the value (font `Body_14`, focused background `#E5F0EE`): choice
semantics. Without it the combo is editable.
- Orca style flags: `CB_NO_DROP_ICON` (no arrow), `CB_NO_TEXT` (icon-only items).
- Per-item data (`DropDown::Item`): text, `icon` (list), `icon_textctrl` (shown in the closed combo),
`text_static_tips`, `data` (`void*`), `group_key`/`group_label` (a group opens a sub-dropdown), `alias`, `tip`,
`flag`, `style` = `DD_ITEM_STYLE_SPLIT_ITEM` (separator-style header), `DD_ITEM_STYLE_DISABLED` (not selectable by
click), `DD_ITEM_STYLE_DIMMED` (grey, selectable). `Append(text, bitmap, group, clientData, item_style)` overloads,
`SetItems(std::vector<DropDown::Item>)`, `SetItemTooltip/Alias/Bitmap`, `SetFlag`.
- `GetDropDown()` exposes the popup (`SetUseContentWidth(true[, limit])`, `SetAlignIcon`, colours).
`SetKeepDropArrow(true)` keeps the arrow and shows the item icon as a second icon. `ForceDropdownOpen()` opens it
programmatically (data-view editors use this).
- Opening: click (debounced by `DropDown::HasDismissLongTime()`, ≥ 20 ms since the last dismissal, so the click that
dismissed the popup does not reopen it), Enter/Space. Up/Down/Left/Right on a focused read-only combo step the
selection and send `wxEVT_COMBOBOX` (in an editable combo the keys go to the inner text control). Mouse-wheel selection is disabled (handler commented out of the event table). Scrolling an
ancestor `wxScrollHelper` hides the popup.
Events: see §Event semantics. Picking an item sends `wxEVT_COMBOBOX` even when it is already selected. Typing into
an editable combo sends `wxEVT_TEXT` per keystroke and **no** `wxEVT_COMBOBOX`: the commit is `wxEVT_TEXT_ENTER`/
`wxEVT_KILL_FOCUS` on the combo, after `OnEdit()` has matched the text to an item (exact, case-sensitive) and re-set
it (one more `wxEVT_TEXT`).
Pitfalls:
- **Rule:** Use `SelectAndNotify(n)` when listeners must react; `SetSelection(n)` is silent for `wxEVT_COMBOBOX` and
returns early when `n` is already selected.
**Why:** matches wx (`interface/wx/ctrlsub.h:102-103`), but in an **editable** combo (or one with `CB_NO_TEXT`)
`SetSelection`/`SetValue` go through `GetTextCtrl()->SetValue()` and send a propagating `wxEVT_TEXT`; guard
`wxEVT_TEXT` handlers during sync.
- **Rule:** Store only untyped `void*` client data and free it yourself.
**Why:** every `Append` overload calls `SetClientDataType(wxClientData_Void)`, and declaring them hides every base
`wxItemContainer::Append` (the `wxClientData*` and `wxArrayString` ones included); `DeleteOneItem()` bypasses the
base client-object reset. Typed `wxClientData` ownership (`references/controls-dataview.md §Item containers`) does
not apply.
- **Rule:** After `Clear()`, `Insert()`, `Set()` or `Delete()`, call `SetSelection`/`SetValue`.
**Why:** they reset the selection to -1 (`DropDown::Invalidate(true)`) but leave the shown text.
- **Rule:** Bind `wxEVT_COMBOBOX_DROPDOWN`/`_CLOSEUP` on the combo without an id filter.
**Why:** they are created with id 0 and no event object; any ancestor bound without a filter receives them from
every combo below it.
`DropDown` (`Widgets/DropDown.cpp`) standalone: a `PopupWindow` (`wxPU_CONTAINS_CONTROLS`, `wxBG_STYLE_PAINT`,
`wxBufferedPaintDC`) drawing `items`, which it holds **by reference** — the vector must outlive the popup, and
`Invalidate()` must follow any edit of it. `Popup()` on GTK sets the toplevel as transient parent explicitly (a
data-view editor can get focus before wxGTK infers one); `Dismiss()` refuses while its sub-dropdown is shown;
`OnDismiss()` sends `EVT_DISMISS` and stamps the dismissal time; `ShouldDismissOnTopWindowDeactivate()` keeps chained
dropdowns open on Wayland, where mapping a grabbing child popup deactivates the toplevel; on macOS it binds an empty
`wxEVT_IDLE` handler to stop wx's idle-time capture release/re-capture ([source] `src/common/popupcmn.cpp:108-111,
439-472`). It captures without a `HasCapture()` guard, and its capture-lost handler replays release, which can commit
the hovered item. Popup mechanics: `references/popups-menus.md`.
## SpinInput and TempInput
`SpinInput` (`Widgets/SpinInput.cpp`) is a `wxNavigationEnabled<StaticBox>` with an inner `TextCtrl` validated by
`wxTextValidator(wxFILTER_DIGITS)`, two arrow `Button`s (not keyboard-focusable) and a repeat `wxTimer`.
- `text` (if it parses) overrides `initial`; `label` is a painted side label; `style` goes to the inner control
(`wxTE_PROCESS_ENTER` added).
- `SetValue(int|wxString)` clamps to `[min, max]` and stores; `GetValue()` returns the **last committed** value, not
the text being typed; `SetRange(min, max)` stores the bounds without re-clamping; `SetStep`.
- Commits — parse, clamp, `wxEVT_SPINCTRL` — on Enter, on kill-focus and on Up/Down keys (which do not step past a
bound) only if the value changed, but on **every** arrow-button press and auto-repeat tick (while held), even when
the value is pinned at `min`/`max` (`SpinInput::createButton`, `SpinInput::onTimer`). Mouse-wheel stepping is
disabled (handler commented out of the event table).
- `EVT_SPINCTRL_TEXT` (int + string) fires on every keystroke that leaves a parseable integer.
- Enter and kill-focus are re-dispatched to the spinner with `ProcessEventLocally`, as in TextInput. Unlike
TextInput, `SpinInput::onTextLostFocus` never `Skip()`s the inner control's `wxEVT_KILL_FOCUS` (it skips a local
copy), although focus handlers should (`interface/wx/event.h:3410-3412`); a `KILL_FOCUS` handler bound on
`GetTextCtrl()` runs before it and must `Skip()` to keep the commit.
Pitfalls:
- **Rule:** Bind `wxEVT_SPINCTRL` with a `wxCommandEvent&` handler and read `GetValue()` from the spinner.
**Why:** `wxEVT_SPINCTRL` is declared with `wxSpinEvent` (`include/wx/spinctrl.h:22`) but `SpinInput::sendSpinEvent`
sends a plain `wxCommandEvent` without `SetInt`; a `wxSpinEvent&` handler reads `GetPosition()` == 0 from an object
of the wrong type.
```cpp
spin->Bind(wxEVT_SPINCTRL, [](wxSpinEvent& e) { use(e.GetPosition()); }); // Wrong
spin->Bind(wxEVT_SPINCTRL, [spin](wxCommandEvent&) { use(spin->GetValue()); }); // Right
```
- **Rule:** Use `SpinInput` only for non-negative integer ranges.
**Why:** `wxFILTER_DIGITS` rejects `-` (and `.`, `+`) (`interface/wx/valtext.h:43-52`); a negative minimum can be
reached only with the arrow buttons or Up/Down keys.
- **Rule:** Set the range before the value.
**Why:** `SetRange` does not re-clamp an existing value.
- **Rule:** Commit explicitly before reading when the user may still be typing.
**Why:** `GetValue()` is the last committed value. Dialogs sometimes call `Disable()` to force the kill-focus
commit (`CloneDialog`), which depends on the platform delivering `wxEVT_KILL_FOCUS` to the disabled focused child
— not a wx contract. Parse `GetTextCtrl()->GetValue()` and call `SetValue` yourself when it matters.
- **Rule:** Expect `SetValue` to emit `EVT_SPINCTRL_TEXT` and a propagating `wxEVT_TEXT`.
**Why:** it writes through the inner `wxTextCtrl::SetValue`.
`TempInput` (`Widgets/TempInput.cpp`, device pages) is a separate `wxNavigationEnabled<StaticBox>` with normal/active
icons, target and current temperatures (`SetTagTemp`, `SetCurrTemp`) and a warning state (`Warning(bool,
WARNING_TOO_HIGH | WARNING_TOO_LOW | WARNING_UNKNOWN)`); on commit it **posts** `wxCUSTOMEVT_SET_TEMP_FINISH` to its
**parent** (`TempInput::SetFinish`; int = the constructor's `type`, no id, no event object), so bind it on the parent
and tell inputs apart by the int. It guards re-entry with `m_on_changing`, because a handler that opens a dialog
moves focus and re-triggers the kill-focus commit — copy that guard for any commit-on-kill-focus widget whose
handler can show UI.
## Tab systems
Three distinct mechanisms; don't confuse them:
| | `TabCtrl` (`Widgets/TabCtrl.cpp`) | `Notebook` (`GUI/Notebook.hpp`) | `wxSimplebook` |
|---|---|---|---|
| What | a bare tab **bar**: one `Button` per tab in a `StaticBox` | `wxBookCtrlBase` with a `ButtonsListCtrl` header (one `Button` per tab) | standard stacked-page container, no UI |
| Content | **caller** shows/hides it | owns pages | owns pages |
| Events | `wxEVT_TAB_SEL_CHANGING` (int = old) → `wxEVT_TAB_SEL_CHANGED` (int = new), plain `wxCommandEvent`s | posted `wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED` (id = page) → `wxEVT_NOTEBOOK_PAGE_CHANGING`/`_CHANGED` | PAGE_CHANGING/CHANGED from `SetSelection` only (`interface/wx/simplebook.h:28-31`) |
| Use | settings-style category strips (Preferences) | MainFrame's main tabs (Home, Prepare, Preview, Device, …) | headerless page switching (`SelectMachine.hpp`, `ReleaseNote.cpp`, `StatusPanel`) |
`TabCtrl`: `AppendItem(text[, image, selImage, clientData])`, `AppendItem(text, bitmap)`, `DeleteItem`,
`DeleteAllItems`, `SelectItem(i)`/`Unselect()`, `GetSelection`, `SetItemText/Bitmap/Data`, `SetItemBold(i, bool)`,
`SetItemTextColour(i, StateColor)`, `SetItemIndicator(i, bool)` (dot after the label), `SetFont`, `Rescale()`.
`AssignImageList` is Orca's own method (takes ownership), not `wxWithImages`, and nothing draws from that list:
`AppendItem` ignores its `image`, `selImage` and `clientData` arguments — give icons with `AppendItem(text, bitmap)`/
`SetItemBitmap` and data with `SetItemData`. Call `SetFont` before `SetItemBold`: it derives the bold font that
`SetItemBold` applies. Selecting a tab flips the tab Buttons'
`Checked` state by sending them `wxEVT_CHECKBOX` command events (id 0, object = the tab `Button`); those propagate up
the parent chain, so an ancestor bound to `wxEVT_CHECKBOX` without an id/object filter sees them. `PreferencesDialog::
create` is the canonical usage:
```cpp
m_pref_tabs = new TabCtrl(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE | wxWANTS_CHARS);
m_pref_tabs->SetFont(Label::Body_14); // before SetItemBold
m_pref_tabs->AppendItem(_L("General")); // one per page (create_items)
m_pref_tabs->Bind(wxEVT_TAB_SEL_CHANGED, [this](wxCommandEvent& e) {
Freeze();
const int sel = e.GetSelection(); // GetInt()
for (size_t i = 0; i < m_pref_tabs->GetCount(); ++i) {
m_pref_tabs->SetItemBold(i, int(i) == sel);
f_sizers[i]->Show(int(i) == sel); // the caller switches content
}
Layout(); Thaw();
});
StateColor item_color(std::make_pair(wxColour("#6B6B6C"), (int) StateColor::NotChecked),
std::make_pair(wxColour("#363636"), (int) StateColor::Normal));
for (size_t i = 0; i < m_pref_tabs->GetCount(); ++i) m_pref_tabs->SetItemTextColour(i, item_color);
m_pref_tabs->SelectItem(0);
```
- **Rule:** Don't try to veto a TabCtrl switch from `wxEVT_TAB_SEL_CHANGING`.
**Why:** it is a plain `wxCommandEvent` and `TabCtrl::sendTabCtrlEvent` always returns true; `SelectItem` continues.
`Notebook`: pages are inserted and addressed by a stable id (`AddPage(id, page, text, bmp_name)`,
`InsertPage(n, id, page, text, bmp_name, bSelect)`, `FindPageByName(id)`, `SelectPageByName(id)`, `GetPageName(n)`,
`GetSelectedPageName()`); a page inserted unselected is hidden at once; one window may sit under two tabs (Prepare
and Preview share the `Plater`). A header click posts `wxCUSTOMEVT_NOTEBOOK_SEL_CHANGED` to the notebook, whose own handler calls
`SetSelection(page)`; a handler bound later (MainFrame) runs first and must `Skip()` to let the switch happen.
`SetSelection` sends the vetoable PAGE_CHANGING and PAGE_CHANGED (`interface/wx/bookctrl.h:177-178`) and hides all
other pages; `ChangeSelection` is silent (`:194-195`). `wxEVT_BOOKCTRL_PAGE_CHANGED` is the same event type as
`wxEVT_NOTEBOOK_PAGE_CHANGED` in Orca's build (`include/wx/bookctrl.h:433-434`); `Notebook` sends the
`wxEVT_BOOKCTRL_*` names.
`wxSimplebook`: switch with `ChangeSelection()` (silent) or `SetSelection()` (events); pages must be created with
the book as parent; page titles are mnemonic-interpreted (`references/controls-dataview.md §Book controls`).
## StaticBox, LabeledStaticBox, StaticLine
`StaticBox` (`Widgets/StaticBox.cpp`) as a container: a rounded, bordered group frame. API: `SetCornerRadius`,
`SetBorderWidth`, `SetBorderColor(StateColor)`/`SetBorderColorNormal`, `SetBorderStyle(wxPenStyle)`,
`SetBackgroundColor(StateColor)`/`SetBackgroundColorNormal`, `SetBackgroundColor2` (vertical gradient),
`SetTopMargin`, `ShowBadge(bool)` (corner badge bitmap), static `GetParentBackgroundColor(parent)`. `wxBORDER_NONE`
makes the border 0. Its children inherit its fill through `GetParentBackgroundColor`. Authoring widgets on it:
`references/painting-custom-widgets.md`.
`LabeledStaticBox` (`Widgets/LabeledStaticBox.cpp`) is a real `wxStaticBox` subclass, so it works as the box of a
`wxStaticBoxSizer`; it paints a rounded border (`Head_14` label in the border gap, `#DBDBDB` border, white fill)
itself (`wxBG_STYLE_PAINT` except on macOS, where `staticbox_remove_margin` is applied and the
`GetBordersForSizer` override sets the side padding other platforms use). It is not focusable. `SetCornerRadius`, `SetBorderWidth`,
`SetBorderColor(StateColor)`, `SetFont`, `Enable`. There is no `StaticGroup` class.
- **Rule:** Create the controls inside the box as children of the box.
**Why:** since 2.9.1 wx "strongly recommends" box children over siblings to avoid repaint problems
(`interface/wx/statbox.h:16-24`); `sizer->GetStaticBox()` is the parent to use.
```cpp
auto* box = new LabeledStaticBox(this, _L("Network"));
auto* sizer = new wxStaticBoxSizer(box, wxVERTICAL);
sizer->Add(new ::CheckBox(box), 0, wxALL, FromDIP(5)); // parent = box
```
- **Rule:** To change the label, recreate the box.
**Why:** the label is captured and measured in `Create()`; there is no `SetLabel` override, so `SetLabel` changes the
native text the widget never paints.
- **Rule:** Don't expect a disabled look.
**Why:** its `StateColor`s list `Normal` before `Disabled`, and the first match wins
(`references/colours-dark-mode.md §StateColor`), so the disabled colours never apply.
`StaticLine` (`Widgets/StaticLine.cpp`): horizontal or vertical separator with optional label and icon;
`SetLineColour(wxColour)`, `SetLabel`, `SetIcon`, `Rescale()`; line and text colours go through `darkModeColorFor` at
paint time. Use it instead of `wxStaticLine`.
## ProgressBar
`ProgressBar` (`Widgets/ProgressBar.cpp`) is a `wxWindow` with a rounded track and fill (MSW draws through a memory
DC + `wxGCDC` for anti-aliasing). `SetValue(step)` (re-enables after `Disable(text)`), `SetProgress(step)` (ignores
negatives), `Reset()`, `ShowNumber(bool)`, `SetRadius`, `SetHeight(h)` (sets min height and radius `h/2`), and
`Disable(wxString text)`, which draws an orange "disabled" bar with `text` and hides `wxWindow::Disable()` (name
hiding: `bar->Disable()` does not compile).
Pitfalls:
- **Rule:** Pass dark-mapped colours and re-set them on theme change.
**Why:** track, fill and text are plain `wxColour`s painted as given; nothing maps them.
```cpp
bar->SetProgressBackgroundColour(StateColor::darkModeColorFor(wxColour("#009688"))); // sets the FILL
```
- **Rule:** Mind the swapped setter names.
**Why:** `SetProgressForedColour` sets the **track** (`m_progress_background_colour`) and
`SetProgressBackgroundColour` sets the **fill** (`m_progress_colour`).
- **Rule:** Use `SetHeight(FromDIP(h))` for a bar thinner than 14 px.
**Why:** `ProgressBar::SetMinSize` returns without doing anything (width included) when the height is below its
`miniHeight` of 14 — so `SetMinSize(wxSize(w, -1))` is ignored too.
- **Rule:** Use `ShowNumber` only with `max == 100`.
**Why:** it draws the raw step followed by `%`.
- `Rescale()` is empty: re-set the height in `on_dpi_changed`.
## ScrolledWindow
`ScrolledWindow` (`Widgets/ScrolledWindow.cpp`) is a `wxScrolled<wxWindow>` that hides the native scrollbars and
draws `MyScrollbar`s (`Widgets/Scrollbar.cpp`) of `scrollbarWidth` in a `marginWidth` strip; content goes on
`GetPanel()` (the scroll target). Its internal panels are sized from the constructor `size`, so pass a real size
(`FromDIP`/`em`-based, as `Search.cpp` does). Use raw `wxScrolledWindow` for ordinary scrolling.
- **Rule:** Create it with `wxVSCROLL`.
**Why:** the mouse-wheel handler forwards to the vertical bar unconditionally and `SetBackgroundColour` touches the
vertical-bar panels unconditionally, so a `wxHSCROLL`-only instance dereferences null pointers; with neither flag
`GetPanel()` is null. With both flags only the vertical bar is built.
- `MyScrollbar` paints on a `wxClientDC` and forces `Refresh(); Update();` — do not copy its painting
(`references/painting-custom-widgets.md`).
## PopupWindow
`PopupWindow` (`Widgets/PopupWindow.cpp`) derives `wxPopupTransientWindow` and adds per-platform dismissal hooks.
Use it for every transient popup, knowing which parts are automatic:
| Port | Added behaviour |
|---|---|
| GTK (X11 and Wayland) | `Create` binds `wxEVT_ACTIVATE` on the first top-level window strictly above the parent (`GetTopParent` starts at the parent's parent, so a popup parented directly to a dialog watches the dialog's own parent); deactivation → `DismissAndNotify()` unless `ShouldDismissOnTopWindowDeactivate()` (virtual) returns false — `DropDown` uses that to keep chained popups open on Wayland |
| MSW | dismissal on toplevel deactivate/iconize/hide is **opt-in**: call `BindUnfocusEvent()` (MSW-only member); the destructor unbinds. Its activate handler does not `Skip()`, so while the popup exists the toplevel's earlier-bound `wxEVT_ACTIVATE` handlers and wx's focus save/restore (`wxTopLevelWindowMSW::OnActivate`, `src/msw/toplevel.cpp:1326`) do not run [source] |
| macOS | with `wxPU_CONTAINS_CONTROLS` it hit-tests and forwards mouse and enter/leave events to child controls (`OnMouseEvent2`); wx documents the flag as MSW focus behaviour only (`interface/wx/popupwin.h:17-26`) |
Command events from controls inside the popup stop at the popup on MSW and macOS ([source]
`src/common/popupcmn.cpp:135`) but bubble on to the popup's parent on GTK, whose `wxPopupWindow::Create` never sets
`wxWS_EX_BLOCK_EVENTS` (`src/gtk/popupwin.cpp`; `references/events.md §5`); bind them on the popup or its children,
as `ComboBox` binds `drop`'s `wxEVT_COMBOBOX`, and don't let an ancestor's unfiltered handler also act on them. When a click on the opener both
dismisses and reopens the popup, gate the reopen on `DropDown::HasDismissLongTime()` or an equivalent timestamp.
Dismissal contracts, per-port mechanics and parenting rules: `references/popups-menus.md`.
## ProgressDialog, WebView and device-page composites
- **`Slic3r::GUI::ProgressDialog`** (`Widgets/ProgressDialog.cpp`): a `wxDialog` copy of wx's generic progress dialog
with Orca styling and a `Button` for Cancel; wx-compatible API (`Update(value, msg, &skip)`, `Pulse`,
`WasCancelled`, `WasSkipped`, `Resume`, `SetRange`). `Update` yields to the event loop
(`YieldFor(wxEVT_CATEGORY_UI | wxEVT_CATEGORY_USER_INPUT)`), so handlers can re-enter; usage rules and the Jobs
alternative: `references/threads-timers-app.md`.
- **`WebView`** (`Widgets/WebView.cpp`): static helpers `CreateWebView(parent, url)`, `LoadUrl`, `RunScript`,
`CheckWebViewRuntime`, `RecreateAll()` + `EVT_WEBVIEW_RECREATED`; `WebViewHostDialog` (`Slic3r::GUI`) is the base of
Orca's web dialogs: `references/webview-gl-aui-media.md`.
- **Device-page composites** (Monitor/Device UI; reuse inside those pages, don't copy their painting as a model):
`SideButton`, `SideTools`/`SideToolsPanel` (`Slic3r::GUI`), `SidePopup` (`SideMenuPopup.hpp`), `StepCtrl`/
`StepIndicator` (`EVT_STEP_CHANGING`/`EVT_STEP_CHANGED`), `TempInput`, `ImageSwitchButton`/`FanSwitchButton`,
`AxisCtrlButton`, `FanControl` family, `AMSControl`/`AMSItem` family/`FilamentLoad`, `MultiNozzleSync` tables
(`Slic3r::GUI`), `CheckList`, `ErrorMsgStaticText`, `AnimaIcon` (`AnimaController.hpp`), `RoundedRectangle`.
## Debugging widgets
wxInspector is compiled into Debug/RelWithDebInfo builds (the top-level `CMakeLists.txt` defines
`WXINSPECTOR_DISABLE` when `BBL_RELEASE_TO_PUBLIC` is set true or, when that variable is undefined, for the Release
configuration; `references/platforms.md §wxInspector`). Every `DPIAware` window installs its accelerator
(`SetupInspectorAccelerator`, Ctrl+Shift+I, Cmd+Shift+I on macOS); the plugins in `src/slic3r/Utils/wxInspectorPlugins/`
(`CustomWidgetsPlugin`) show Button style/type, CheckBox half state, TextInput, SwitchButton, ProgressBar, Label and
LabeledStaticBox properties. The accessors commented "only meant to be used by inspector" (`Button::GetStyle`,
`CheckBox::IsHalfChecked`, `TextInput::GetCornerRadius`, `LabeledStaticBox::GetBorderColor`, …) exist for it — don't
build features on them. A later `SetAcceleratorTable` on a DPIAware window replaces the inspector's table.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,940 @@
# Platforms, the wx build, and platform-specific code
Read this when code has to differ per platform, when a bug shows up on only one OS, toolkit or display
server, or when you need to know how Orca's wxWidgets is built. It covers the wx fork and its build
options, platform macros, per-platform summaries that point into the topic files, and where
platform-specific code lives. It also owns runtime X11/Wayland detection, the Wayland gap list, custom
title bars and window decoration, GTK native-chrome removal, and the cross-platform test checklist.
Contents: [Rules](#rules) · [The wx build Orca uses](#the-wx-build-orca-uses) ·
[Platform macros and native handles](#platform-macros-and-native-handles) ·
[Per-platform summaries](#per-platform-summaries) · [The ifdef landscape](#the-ifdef-landscape) ·
[Runtime X11/Wayland detection](#runtime-x11wayland-detection) · [Wayland gaps](#wayland-gaps) ·
[Window decoration and custom title bars](#window-decoration-and-custom-title-bars) ·
[GTK native chrome and GTK size calls](#gtk-native-chrome-and-gtk-size-calls) ·
[wx 3.3 migration notes](#wx-33-migration-notes) ·
[Cross-platform testing checklist](#cross-platform-testing-checklist)
## Rules
1. Every GUI change must work on Windows (wxMSW), macOS (wxOSX/Cocoa) and Linux wxGTK3, under both
X11 and Wayland. GTK-guarded code must still compile against GTK2, but GTK2 is an opt-out build and
not something Orca ships. → [The wx build](#the-wx-build-orca-uses), [Testing](#cross-platform-testing-checklist)
2. Use the wx toolkit macros (`__WXMSW__`, `__WXOSX__`, `__WXGTK__`, `__WXGTK3__`) when the behaviour
comes from wx. Use `_WIN32` / `__APPLE__` / `__linux__` only for OS APIs and in code that is not
built against wx (`src/libslic3r`). → [Platform macros](#platform-macros-and-native-handles)
3. Guard a declaration in the header exactly as its definition is guarded in the `.cpp`, and call a
guarded helper only under the same guard. → [Platform macros](#platform-macros-and-native-handles)
4. Decide X11 vs Wayland at runtime with `is_running_on_wayland()` / `is_running_on_x11()`, and only
after GTK is initialised. Read `WAYLAND_DISPLAY` / `DISPLAY` / `GDK_BACKEND` only before GTK starts
(`CLI::run`). → [Runtime detection](#runtime-x11wayland-detection)
5. On Wayland, none of these work: global pointer coordinates, positioning top-level windows,
drawing through `wxClientDC`, `Update()`, `SetIcon`, `WarpPointer` (outside narrow conditions),
AUI floating panes, `wxUIActionSimulator`. → [Wayland gaps](#wayland-gaps)
6. Orca's wx has no SVG support and no asserts. Never call `wxBitmapBundle::FromSVG*`. Wherever the
wx docs say a call "asserts", expect a silent failure in Orca and check the precondition yourself.
→ [The wx build](#the-wx-build-orca-uses)
7. Change wx build options or patches only in `deps/wxWidgets/wxWidgets.cmake`, and make the same
change in the wxWidgets module of the Flatpak manifest. → [The wx build](#the-wx-build-orca-uses)
8. On MSW, `MainFrame` draws its own non-client area. Mask `WS_CAPTION` out of every non-client
computation, and handle `WM_NCCALCSIZE` for the maximised case yourself.
→ [MSW title bar](#msw-the-mainframe-custom-title-bar)
9. On Linux, move and resize the borderless main frame through the window manager
(`gtk_window_begin_move_drag` / `gtk_window_begin_resize_drag`). Never call `Move()` or `SetSize()`
from mouse coordinates. → [GTK frame](#linux-gtk-the-borderless-mainframe)
10. To show an undecorated top-level window on Wayland, install an empty client-side titlebar and then
call `gtk_window_set_decorated(false)`, both in the constructor, before control returns to the
event loop.
→ [Undecorated windows on Wayland](#wayland-undecorated-top-level-windows-splash)
11. Remove GTK theme borders from custom-drawn controls with `RemoveButtonBorder` / `RemoveInputBorder`
(`__WXGTK__` only). Call raw GTK size functions only with strictly positive sizes.
→ [GTK native chrome](#gtk-native-chrome-and-gtk-size-calls)
12. Put platform glue where it already lives: Cocoa code in `.mm` files listed in the `APPLE` block of
`src/slic3r/CMakeLists.txt`, Win32 messages in `MSWWindowProc` overrides, and GDK/GTK calls behind
`__WXGTK__` with the GTK header included under the same guard. → [Ifdef landscape](#the-ifdef-landscape)
13. Before fixing something "for platform X", find the wx mechanism that differs there (the
per-platform summaries point to it) and check whether the same bug class exists on the other
platforms. → [Per-platform summaries](#per-platform-summaries)
## The wx build Orca uses
### Source, pin and local patch
- `deps/wxWidgets/wxWidgets.cmake` builds `https://github.com/SoftFever/Orca-deps-wxWidgets` at tag
**`v3.3.2`** (`GIT_SHALLOW ON`, submodules `3rdparty/catch`, `3rdparty/pcre` and `3rdparty/libwebp`
only). The fork carries Orca's build fixes, the clang-cl fix among them; do not duplicate
a fork fix as a local patch under `deps/wxWidgets/` (cc390f11ee removed the local
`0001-Clang-CL-fix.patch` once the fork carried the fix). The fork's clang-cl fix is the MSVC lib-dir
selection in the installed `wxWidgetsConfig.cmake`: it looks for `<prefix>_<arch>_lib` (or `_dll`)
under the consuming compiler's prefix first and then the sibling one (`clang` ↔ `vc`), because cl
and clang-cl share an ABI and either can consume either build
(`build/cmake/wxWidgetsConfig.cmake.in:53-73`).
- **The one local patch** is `deps/wxWidgets/0001-macos-use-srgb-colour-components.patch`, applied
only `if (APPLE)`. The `PATCH_COMMAND` first runs `git checkout -f -- src/osx/cocoa/colour.mm` and
then `git apply`, so the step can run again safely (a7775296b0). The patch makes the wxOSX
`wxColour` component getters (`wxNSColorRefData::Red/Green/Blue/Alpha` and `IsSolid`) convert the
`NSColor` with `[NSColorSpace sRGBColorSpace]` instead of `NSCalibratedRGBColorSpace`, so colours
read back on macOS match their sRGB values (custom-colour accuracy). Colour usage:
`references/colours-dark-mode.md`.
- The checked-out source is the tree that every wx citation in this skill refers to:
`deps/build/<arch>/dep_wxWidgets-prefix/src/dep_wxWidgets` on macOS and
`deps/<tree>/dep_wxWidgets-prefix/src/dep_wxWidgets` on Linux and Windows (`deps/build` for a
release build; `build_win.bat` names the others). Locate it with the bash or PowerShell lookup in
`SKILL.md` §Ground truth. On macOS its
`src/osx/cocoa/colour.mm` already has the patch applied.
- **Flatpak builds wx separately.** `deps/CMakeLists.txt` leaves `dep_wxWidgets` out of the deps
target when `FLATPAK` is set. Instead, `scripts/flatpak/com.orcaslicer.OrcaSlicer.yml` has its own
`wxWidgets` module whose config-opts "mirror deps/wxWidgets/wxWidgets.cmake with FLATPAK=ON,
DEP_WX_GTK3=ON": `-DwxBUILD_TOOLKIT=gtk3`, a shared build (`wxBUILD_SHARED=ON`,
`BUILD_SHARED_LIBS=ON`, `d` debug postfix), and `wxUSE_LIBWEBP=sys`, because the builtin webp
libraries are installed only by static builds. It links with lld and pins the fork's tag
`orca-3.3.2` at a fixed commit. Option and version changes must be made in both files.
### Toolkit per platform
| Platform | wx port | How it is selected |
|---|---|---|
| Windows | wxMSW | default port; the Edge WebView backend is built only for MSVC-family compilers |
| macOS | wxOSX/Cocoa | default port; wx's exported targets add `__WXOSX_COCOA__;__WXMAC__;__WXOSX__` |
| Linux | **wxGTK3** | `deps/CMakeLists.txt` declares `option(DEP_WX_GTK3 "Build wxWidgets against GTK3" ON)` (default ON since 026499c5b7, #10294), which gives `-DwxBUILD_TOOLKIT=gtk3`. The root `CMakeLists.txt` sets `SLIC3R_GTK "3"`, so `src/CMakeLists.txt` finds wx through `wx-config --toolkit=gtk${SLIC3R_GTK}` and `src/slic3r/CMakeLists.txt` links `GTK${SLIC3R_GTK}`. Flatpak uses gtk3 too. |
**GTK2 is an opt-out, not a default.** `wxWidgets.cmake` starts from `_gtk_ver 2` and switches to 3
when `DEP_WX_GTK3` is on, so you get GTK2 only by passing `-DDEP_WX_GTK3=OFF` (and `SLIC3R_GTK=2`).
A GTK2 build loses the following (wx `build/cmake/init.cmake`, `include/wx/features.h`):
- EGL: `wxUSE_GLCANVAS_EGL` is forced OFF unless GTK3 and EGL are both found (init.cmake:559-561), and
`wxHAS_EGL` is set only on GTK3 (:531-540). No EGL means no native Wayland GL.
- WebKit2: GTK2 gets WebKit1 (init.cmake:568-569); GTK3 uses webkit2gtk-4.1 and falls back to 4.0
(:571-577).
- DIP pixels: `wxHAS_DPI_INDEPENDENT_PIXELS` is defined only for `__WXGTK3__ || __WXMAC__ || __WXQT__`
(`include/wx/features.h:115-120`), so GTK2 uses physical pixels and gets no `wxEVT_DPI_CHANGED`.
- Backend detection: Orca's `wxHAVE_GDK_*` macros are not defined, so `get_linux_display_backend()`
always returns `Unknown` ([Runtime detection](#runtime-x11wayland-detection)).
- Border removal: `RemoveButtonBorder` / `RemoveInputBorder` fall back to a global `gtk_rc` style.
`src/slic3r/CMakeLists.txt` also requires `webkit2gtk-4.1`, a GTK3 library, so a GTK2 GUI would load
GTK2 and GTK3 into one process. Treat GTK2 as a compile-compatibility target only.
wx is linked statically (`wxBUILD_SHARED=OFF`) everywhere except Flatpak. On Windows and macOS,
`src/CMakeLists.txt` uses `find_package(wxWidgets 3.3 CONFIG … propgrid)` (`propgrid` is needed by
wxInspector). On Linux it uses the `wx-config` module mode.
### Build options
The options `deps/wxWidgets/wxWidgets.cmake` passes, with the consequence each one has for GUI code:
| Option | Value | Consequence for GUI code |
|---|---|---|
| `wxBUILD_DEBUG_LEVEL` | `0` | wx asserts are compiled out; see [Debug level 0](#debug-level-0-no-wx-asserts) |
| `wxBUILD_SHARED` | `OFF` (Flatpak: `ON`) | static; private wx globals such as `wxCurrentPopupWindow` can be reached with `extern` |
| `wxBUILD_PRECOMP` / `wxBUILD_SAMPLES` | `ON` / `OFF` | build speed only |
| `wxUSE_NANOSVG` | `OFF` | no SVG in wx; see [No SVG](#no-svg-in-orcas-wx). It was disabled to avoid duplicate symbols with Orca's own NanoSVG, which carries an `nsvgRasterizeXY` extension (7658cf9076) |
| `wxUSE_GLCANVAS_EGL` | `ON` | takes effect only on GTK3 with EGL found; with both EGL and GLX built, wx picks EGL even on X11 unless `PreferGLX()` is called → `references/webview-gl-aui-media.md` §EGL vs GLX |
| `wxUSE_OPENGL` | `ON` | `wxGLCanvas` |
| `wxUSE_WEBVIEW` / `wxUSE_WEBVIEW_EDGE` / `wxUSE_WEBVIEW_IE` | `ON` / `ON` only `if (MSVC)` / `OFF` | Edge (WebView2) on Windows, WKWebView on macOS, WebKit2GTK on Linux. `wxUSE_WEBVIEW_CHROMIUM` keeps its default `OFF` (`build/cmake/options.cmake:303`), so Chromium-backend notes never apply. `wxUSE_WEBVIEW_EDGE_STATIC` keeps its default `OFF` (`build/cmake/options.cmake:514`), so the root `CMakeLists.txt` ships `WebView2Loader.dll` from `deps/WebView2/lib/win-<arch>` next to the executable → `references/webview-gl-aui-media.md` §wxWebView backends |
| `wxUSE_WEBREQUEST` | `ON` | `wxWebSession`/`wxWebRequest` are available (used only for a few image downloads) |
| `wxUSE_MEDIACTRL` | `ON` | kept for `wxMediaState`; the camera view is Orca's `wxMediaCtrl3`, not a `wxMediaCtrl` |
| `wxUSE_PRIVATE_FONTS` | `ON` | `wxFont::AddPrivateFont` for the bundled fonts → `references/dpi-bitmaps-fonts.md` |
| `wxUSE_AUI` | `ON` | Plater docking and `BBLTopbar` (a `wxAuiToolBar`) |
| `wxUSE_STC` | `OFF` | no `wxStyledTextCtrl` |
| `wxUSE_DETECT_SM` | `OFF` | no X11 session-manager detection |
| `wxUSE_REGEX` | `builtin` | — |
| `wxUSE_LIBPNG` / `ZLIB` / `LIBJPEG` / `EXPAT` | `sys` (from the deps tree) | — |
| `wxUSE_LIBTIFF` | `OFF` | no TIFF image handler |
| `wxUSE_LIBWEBP` | `builtin` (Flatpak: `sys`) | WebP image handler is available |
| `wxUSE_LIBSDL` / `wxUSE_XTEST` | `OFF` | no SDL audio backend; `wxUIActionSimulator` on X11 uses its non-XTest path (`src/unix/uiactionx11.cpp`) |
Options left at wx defaults that matter: LunaSVG is off (`wxUSE_LUNASVG 0` in the installed
`setup.h`), `wxUSE_STD_CONTAINERS 1`, `WXWIN_COMPATIBILITY_3_0 0`, and `WXWIN_COMPATIBILITY_3_2 1`.
On macOS `wxUSE_NATIVE_DATAVIEWCTRL` is 1 (`references/controls-dataview.md`).
### Debug level 0: no wx asserts
wx is built with `-DwxBUILD_DEBUG_LEVEL=0`, which `build/cmake/init.cmake:245-246` turns into
`-DwxDEBUG_LEVEL=0`. `src/slic3r/CMakeLists.txt` also adds `wxDEBUG_LEVEL=0` to `libslic3r_gui` when
`SLIC3R_STATIC`. The Flatpak module and wxInspector use level 0 as well. At level 0, `wxASSERT`,
`wxFAIL` and `wxTrap` "do nothing at all", while "wxCHECK macros always check their conditions,
setting debug level to 0 only makes them silent in case of failure" (`include/wx/debug.h:229-231`,
`:342-382`).
In practice, misuse that a debug wx would report shows up in Orca as a silent no-op, an early return
or a wrong result: for example a second `ReleaseMouse`, a late `PreferGLX()`, a second `Destroy()` on
a transient popup, a window added to a second sizer, `SetCurrent` on a hidden GL canvas, or a
duplicate AUI pane name. Write "wx would assert in a debug build; in Orca it silently …". When the
wx docs state a precondition, check it in your own code.
### No SVG in Orca's wx
`wxHAS_SVG` is defined only when `wxHAS_RAW_BITMAP && (wxUSE_NANOSVG || wxUSE_LUNASVG)`
(`include/wx/features.h:96-97`), and Orca builds with both off. So `wxBitmapBundle::FromSVG`,
`FromSVGFile` and `FromSVGResource` do not exist (`include/wx/bmpbndl.h:81-101`), using them is a
compile error, the Tango art provider returns empty bundles (`src/common/arttango.cpp`, `!wxHAS_SVG`
branch), and wx's AUI tab and dock-art buttons use their non-SVG bitmap fallbacks
(`src/aui/tabart.cpp:92-137`, `src/aui/dockart.cpp:87-128`) [source]. Orca rasterises SVG with its own
NanoSVG in `BitmapCache::load_svg` (through `create_scaled_bitmap` / `ScalableBitmap`) →
`references/dpi-bitmaps-fonts.md`.
### Private headers
The wx 3.3 CMake install does not copy `wx/private`. The `copy_private_headers` step in
`wxWidgets.cmake` runs after install and copies `include/wx/private`, `include/wx/generic/private`
and `include/wx/gtk/private` to `include/wx` (MSVC) or `include/wx-3.3/wx` (elsewhere). The cmake
comment calls this "for accessibility support". The actual consumers are:
- `Widgets/WebView.cpp`: `wx/private/jsscriptwrapper.h` (Windows and macOS only).
- `ExtraRenderers.cpp`: `wx/generic/private/{markuptext,rowheightcache,widthcalc}.h`, under
`wxHAS_GENERIC_DATAVIEWCTRL`, so MSW only.
- `ExtraRenderers.cpp`: `wx/private/markupparser.h`, under `wxUSE_ACCESSIBILITY`.
Its `wx/gtk/private*` includes are commented out. The per-port headers outside those directories,
`wx/msw/private.h` (`BitmapComboBox.cpp`, `PresetComboBoxes.cpp`, Windows-guarded) and
`wx/osx/private.h` (`Utils/MacDarkMode.mm`), come with the regular wx install. Private headers are
port-specific and unversioned. Include one only under the same macro wx uses for that port or
feature, and re-check it whenever the fork is bumped.
### wxInspector
The deps also build wxInspector (`deps/wxInspector/wxInspector.cmake`, compiled with
`-DwxDEBUG_LEVEL=0`). `DPIAware<P>` derives from `wxInspector::wxInspectable`, and its constructor
calls `SetupInspectorAccelerator(this)`. That calls `SetAcceleratorTable` on the window with
`wxACCEL_CTRL | wxACCEL_SHIFT` + `I` (Cmd+Shift+I on macOS), which toggles an inspection frame showing
the window tree (`src/inspector.cpp` in the wxInspector tree); a later `SetAcceleratorTable` call on a
`DPIAware` window replaces the inspector shortcut. `GUI_App::on_init_inner` registers Orca plugins for it
(`RegisterOrcaInspectorPlugins`, `Utils/wxInspectorPlugins/`). The root `CMakeLists.txt` defines
`WXINSPECTOR_DISABLE` when `BBL_RELEASE_TO_PUBLIC` is set true, or, when that variable is not defined,
for the Release configuration; this turns the whole API into no-op stubs. Use it in
Debug/RelWithDebInfo builds to inspect layouts on each platform.
## Platform macros and native handles
| Macro | Defined when | Use for |
|---|---|---|
| `__WXMSW__` | wxMSW build | wx behaviour on Windows, `MSWWindowProc`, MSW-only wx API |
| `__WXOSX__` (also `__WXMAC__`, `__WXOSX_COCOA__`) | wxOSX build | Cocoa-specific wx behaviour |
| `__WXGTK__` | any wxGTK build | GTK/GDK calls, Linux toolkit behaviour |
| `__WXGTK3__` | GTK ≥ 3.0 | GTK3-only API (CSS providers, DPI events, Wayland) |
| `__WXGTK20__` | GTK ≥ 2.0, **also defined in GTK3 builds** (`build/cmake/setup.cmake:61-72` defines every version macro up to the toolkit version) | `defined(__WXGTK20__) \|\| defined(__WXGTK3__)` in `GUI_App.cpp` is the same as `__WXGTK__` |
| `__WINDOWS__` | defined by wx when `_WIN32`, `__WIN32__` or `__WXMSW__` is defined (`include/wx/platform.h:87-91`) | wx's own Windows checks; Orca's dark-mode code uses it |
| `_WIN32`, `__APPLE__`, `__linux__` | compiler | OS APIs (Win32, Cocoa frameworks, `/proc`), and all of `src/libslic3r` |
| `wxHAS_EGL`, `wxHAS_GLX` | wx `setup.h` (`build/cmake/setup.h.in:1144-1147`) when built with EGL / GLX | GL backend code; test them only after a wx header is included |
| `wxHAVE_GDK_WAYLAND`, `wxHAVE_GDK_X11` | Orca's `cmake/modules/FindGTK3.cmake` (`check_symbol_exists(GDK_WINDOWING_WAYLAND/X11 "gdk/gdk.h" …)`), passed by `src/slic3r/CMakeLists.txt` as PRIVATE definitions of `libslic3r_gui` | only `LinuxDisplayBackend.cpp` needs them; wx headers do not define them |
| `GTK_CHECK_VERSION(a,b,c)` | GTK headers, compile time | branching on GTK API version. `gtk_check_version()` and wx's internal `wx_is_at_least_gtk3(n)` check the runtime version |
The toolkit macros come from wx's compile definitions: `wxTOOLKIT_DEFINITIONS` in
`build/cmake/toolkit.cmake:57-87,157`, exported through the CMake targets and through
`wx-config --cxxflags`. They are therefore defined in every `libslic3r_gui` source, even before the
first wx include; `LinuxDisplayBackend.hpp` relies on this. `src/libslic3r` does not link wx and uses
none of them. Prefer the toolkit macro whenever the difference comes from wx: a later toolkit change
(for example wxGTK on another OS) then keeps the right branch.
**Native handles.** `wxWindow::GetHandle()` returns `WXWidget`:
- wxMSW: the `HWND` (`include/wx/msw/window.h:169`).
- wxOSX: the peer's `NSView*`. Reach the `NSWindow` with `[view window]`, as
`set_miniaturizable(GetHandle())` does.
- wxGTK: `m_widget` (`include/wx/gtk/window.h:140`). For a top-level window this is the `GtkWindow`.
`MainFrame`, `BBLTopbar` and `DropDown` also use `m_widget` directly; wxGTK declares it in a
`public:` implementation block (`include/wx/gtk/window.h:291`), so other classes can read another
window's `m_widget` (`m_frame->m_widget`).
Native calls on these handles bypass wx's bookkeeping. Keep them minimal, guard them with the toolkit
macro, and prefer an existing helper (`GUI_Utils`, `MacDarkMode.mm`, `GUI_UtilsMac.mm`) over new
inline native code.
- **Rule:** Keep the guard on a declaration identical to the guard on its definition (one toolkit
guard, version branches with `GTK_CHECK_VERSION` inside the definition), and include GTK headers
under the same guard as the code that uses them.
**Why:** a header declaring under `__WXGTK3__` while the `.cpp` defines under `__WXGTK__` (or the
reverse) breaks the build on the other GTK configuration or leaves an undefined symbol. The
wrong → right shape is under [GTK native chrome](#gtk-native-chrome-and-gtk-size-calls).
Cite: 477208a969 (`GUI_Utils.hpp` / `GUI_Utils.cpp`).
## Per-platform summaries
Each bullet names the mechanism and the file that owns it.
### MSW (wxMSW)
- **Pixels and DPI:** logical pixels equal physical pixels and `FromDIP` really scales. Per-monitor
DPI change events need the PMv2 manifest (`src/dev-utils/platform/msw/OrcaSlicer.manifest.in`
declares `permonitorv2,permonitor`); under the `permonitor` (V1) fallback on older Windows wx
generates no DPI events, because it accepts only PMv2 [source: `src/msw/nonownedwnd.cpp`
`IsPerMonitorDPIAware`]. wx rescales min sizes, fonts and sizer borders before
`wxEVT_DPI_CHANGED` [source], and `DPIAware` does not `Skip()` it. `wxEVT_MOVE_START/END` exist only on MSW
(DPIAware uses them to defer rescaling while a window is dragged) → `references/dpi-bitmaps-fonts.md`.
- **Dark mode:** `MSWEnableDarkMode(DarkMode_Auto)` runs before `NppDarkMode::InitDarkMode()`. After
that, `IsDark()` reports the OS apps setting, so `dark_color_mode` is consulted first. The runtime
dark-mode toggle is Windows-only. Menu bitmaps choose dark variants through `check_dark_mode()` →
`references/colours-dark-mode.md`.
- **Popups:** the current popup is the wx-internal global `wxCurrentPopupWindow`. Focus changes and
clicks outside dismiss popups that lack `wxPU_CONTAINS_CONTROLS` (`MSWDismissUnfocusedPopup`);
popups with it are dismissed on deactivation, deferred through `CallAfter`. No key dismisses a
popup, and `ProcessLeftDown` is never called. `PopupWindow::BindUnfocusEvent` is MSW-only →
`references/popups-menus.md` §5.
- **Painting:** in 3.3.2 windows are not double-buffered by default (the 3.3.0 global
`WS_EX_COMPOSITED` was reverted, `docs/changes.txt:308`). Custom widgets buffer by hand →
`references/painting-custom-widgets.md`.
- **Building large panels:** every control is a native child window, and outside a sizer pass a move
or resize is immediate, repainting when the window is shown [source: `src/msw/window.cpp:2036`
`DoMoveSibling` → `MSWMoveWindowToAnyPosition(..., IsShown())`]; `wxStaticText::SetLabel`/`SetFont`
resize the control that way (`src/common/stattextcmn.cpp:334` `AutoResizeIfNecessary`). Created
inside a shown parent, each control re-clips and erases its shown, overlapping siblings, so the
cost grows with the number already built and hundreds of controls take seconds. Build a large
panel while its parent is hidden and show it once complete — `LazyPage::Show` builds its panel
before showing the page for this reason → `references/orca-architecture.md` §Deferred construction.
- **Modal loops:** idle events do not run inside the Windows sizing/moving modal loop, so the 3D
canvas renders from `on_paint` on MSW (c06a0223a7) → `references/webview-gl-aui-media.md` §GLCanvas3D rendering.
- **Mouse capture:** `wxEVT_MOUSE_CAPTURE_LOST` and `wxEVT_MOUSE_CAPTURE_CHANGED` are delivered →
`references/mouse-keyboard-focus.md`.
- **Controls:** `wxDataViewCtrl` is the generic implementation (`references/controls-dataview.md`).
TaskDialog-based dialogs and common dialogs stay light in wx dark mode
(`interface/wx/app.h:1434-1448`); Orca's `MsgDialog` family is owner-drawn →
`references/windows-dialogs.md`.
- **WebView:** Edge (WebView2). It needs the runtime (checked by `GUI_App::init_webview_runtime`),
creates asynchronously, serves custom schemes as `https://<scheme>.wxsite`, and allows one script
handler → `references/webview-gl-aui-media.md`.
- **Window frame:** custom title bar and non-client handling → [MSW title bar](#msw-the-mainframe-custom-title-bar).
### macOS (wxOSX/Cocoa)
- **Pixels and DPI:** logical pixel = DIP = point. The standard PPI is 72, so `GetDPI()` and
`GetNewDPI()` are 72-based. `wxEVT_DPI_CHANGED` is generated on backing-scale changes even though
the docs don't say so [source: `src/osx/cocoa/nonownedwnd.mm` `windowDidChangeBackingProperties`],
but `DPIAware` binds it only off macOS. `DPIAware` and
`MainFrame::init_tabpanel` skip `SetFont` on macOS ("name cutting in ObjectList") →
`references/dpi-bitmaps-fonts.md`.
- **Menus and keys:** there is a native `wxMenuBar`, and Preferences goes into `OSXGetAppleMenu()`.
Menu key equivalents run before `wxEVT_CHAR_HOOK`, display-only shortcut text uses `" - "`,
`wxMOD_CONTROL` means Cmd, Ctrl+click arrives as a right click [source], and Cmd+letter char events
carry the plain letter [source] → `references/mouse-keyboard-focus.md`, `references/popups-menus.md`.
- **Mouse capture:** capture is a wx-level redirect of every left/right button, motion and
enter/exit event (not wheel or middle-button events), and capture-lost is never sent [source]. A
leaked capture looks like a frozen UI whose keyboard still works →
`references/mouse-keyboard-focus.md`.
- **Popups:** a transient popup toggles mouse capture on idle (since 3.1.7) and dismisses on an
outside click [source]. Since the 3.3 upgrade a hover-opened popup anchored with a gap below its
opener was dismissed as the cursor crossed the gap (#12936); the wx mechanism was not established,
and the fix is to anchor flush → `references/popups-menus.md` §3.
- **Controls:** the native `wxDataViewCtrl` (NSOutlineView) never calls `CreateEditorCtrl`, and the
current item is always selected (`references/controls-dataview.md`). `wxClientDC` cannot draw
(`references/painting-custom-widgets.md`).
- **Colours:** Orca's sRGB `wxColour` patch. Dark mode follows the system only (`mac_dark_mode()`) →
`references/colours-dark-mode.md`.
- **WebView:** WKWebView. Handlers must be registered before `Create`, and adding the same script
handler twice raises an uncatchable NSException → `references/webview-gl-aui-media.md`.
- **Window frame:** a native titled window with a transparent titlebar →
[macOS frame](#macos-a-native-titled-window).
### GTK3 (X11 and Wayland)
- **Pixels and DPI:** logical pixel = DIP, and the scale is an integer ("fractional scales are rounded
to the closest integer"). `wxEVT_DPI_CHANGED` needs GTK ≥ 3.10 and wx ≥ 3.3.0, so `DPIAware`'s
rescale path runs on Linux. `get_dpi_for_window()` is a fixed-96 stub on Linux (and macOS), so
`em_unit` is measured from the font (`DPIAware::update_em_unit`) →
`references/dpi-bitmaps-fonts.md`.
- **Dark mode:** follows the system appearance. `GUI_App::on_init_inner` (non-Windows) and
`update_dark_config` (called from `DPIAware`'s `wxEVT_SYS_COLOUR_CHANGED` handler off Windows)
overwrite `dark_color_mode` from `wxSystemSettings::GetAppearance().IsDark()` →
`references/colours-dark-mode.md`.
- **Sizing:** sizer-fitting calls made on a top-level window that is not yet shown are replayed at
`Show()` (`wxWindow::Fit()` is not), and dialogs collapse without size hints →
`references/sizers-layout.md`.
- **Native chrome:** GTK theme borders and padding show through custom-drawn controls →
[GTK native chrome](#gtk-native-chrome-and-gtk-size-calls).
- **Popups:** `Show()` grabs the pointer (`gdk_seat_grab`) [source]. `PopupWindow` dismisses when the
top-level window is deactivated, and popups are created with `GDK_WINDOW_TYPE_HINT_COMBO` [source] →
`references/popups-menus.md`.
- **Mouse capture:** a grab-broken event or a modal dialog delivers `wxEVT_MOUSE_CAPTURE_LOST`
[source; the docs mark the event MSW-only] → `references/mouse-keyboard-focus.md`.
- **Controls:** `wxDataViewCtrl` is the native GtkTreeView (`references/controls-dataview.md`). `Field`
control pools delete windows on GTK instead of recycling them (`references/orca-settings-ui.md`).
- **WebView:** WebKit2GTK delivers script messages synchronously, with an empty handler name and no
event object, and navigation events synchronously too [source] → `references/webview-gl-aui-media.md`.
- **GL:** EGL or GLX. Orca calls `PreferGLX()` on X11 → `references/webview-gl-aui-media.md` §EGL vs GLX.
- **App init:** `GUI_App::on_init_inner` sets `gtk-menu-images` to TRUE so menu icons show, and
installs a `g_log_set_handler("Gtk", G_LOG_LEVEL_CRITICAL, …)` filter. The filter drops known
harmless criticals (allocation on hidden widgets, events on unrealised widgets, style-context calls
before realisation), so GTK criticals not on that list still reach the log.
- **Window frame:** borderless with WM-driven move and resize → [GTK frame](#linux-gtk-the-borderless-mainframe).
### GTK2 (opt-out build)
Compile-compatibility only. See [Toolkit per platform](#toolkit-per-platform) for what it loses: EGL,
WebKit2, DIP pixels, DPI events and backend detection. GTK-guarded code must still compile here:
use `GTK_CHECK_VERSION` branches as `RemoveButtonBorder` does. No GTK2 runtime behaviour is supported.
### Wayland (GTK3 native backend)
The protocol gives clients no global pointer or window positions, and the compositor places
top-level windows. GL is EGL only, drawn into a subsurface. Popups are `xdg_popup` surfaces and must
form a chain of parents. Window icons come from the `.desktop` file. The full list is under
[Wayland gaps](#wayland-gaps). These Wayland rules are owned by other files:
- hover handlers must short-circuit, because on compositors that keep hidden-workspace surfaces
mapped (e.g. Hyprland) GTK sends a stream of synthetic leave events and `IsShownOnScreen()` stays
true there (69e16cd7ef) →
`references/mouse-keyboard-focus.md`;
- GL blending must keep destination alpha at 1 (d8369e5f75);
- GL post-init must retry until the surface is committed (d2c24fdabb) →
`references/webview-gl-aui-media.md`.
### XWayland (GTK3 X11 backend inside a Wayland session)
Users opt in with `GDK_BACKEND=x11…`. GTK then talks X11, so `is_running_on_x11()` is true and Orca
uses GLX through `PreferGLX()`. `CLI::run` prepares this path before GTK starts (PRIME variables,
`XInitThreads()`, no WebKit compositing workaround; the source comment says multi-monitor handling
is compromised there) → [Runtime detection](#runtime-x11wayland-detection),
`references/webview-gl-aui-media.md` §WebKitGTK on Linux sessions. Intel's XWayland GL exposes a
smaller `GL_MAX_TEXTURE_SIZE`, so the ImGui font atlas is re-packed to fit (22e121f4e4) →
`references/webview-gl-aui-media.md`.
## The ifdef landscape
Platform-specific GUI code falls into recurring categories. Most of it sits in `MainFrame`,
`GUI_App`, `GLCanvas3D`, `GUI_ObjectList`, `Plater`, `wxExtensions`, `Field` and `Widgets/AMSItem`;
start there when looking for prior art. The sites below are exemplars, cited by symbol.
**Focus, capture and popup dismissal** (the largest category) → `references/popups-menus.md`,
`references/mouse-keyboard-focus.md`
- `StatusPanel::on_switch_speed`: on `__WXOSX__` the speed popup gets a `nullptr` parent (the source
comment says "MacOS has focus problem"); elsewhere the parent is the control.
`popUp->BindUnfocusEvent()` runs only under `__WXMSW__` and binds the top parent's
`wxEVT_ACTIVATE` / `wxEVT_ICONIZE` / `wxEVT_SHOW` to `Dismiss()`.
- `GLCanvas3D::on_mouse`, `evt.Entering()` branch: on MSW the canvas does not `SetFocus()` while
`wxCurrentPopupWindow` is non-null. Stealing focus would trigger `MSWDismissUnfocusedPopup` and
close the search dropdown. `wxCurrentPopupWindow` is a wx-internal global (`src/msw/popupwin.cpp`)
that `GLCanvas3D.cpp` declares `extern` itself, which works only because wx is linked statically.
`SearchDialog` and `SearchObjectDialog` override the virtual `MSWDismissUnfocusedPopup`.
- `GLCanvas3D::on_mouse`: the MSW "on_enter workaround" (comment "SPE-832") handles a spurious mouse
event that arrives before `evt.Entering()`; `m_mouse.position` is reset at the end of the function.
- `SearchObjectDialog::Popup`: on `__WXOSX__`, focus moves to `m_object_list` before
`PopupWindow::Popup`, otherwise the text input becomes unusable.
- `SearchDialog::Dismiss`, `SearchObjectDialog::Dismiss`: on Wayland they dismiss by focus tracking
(`focus_left_popup(...)`) instead of hit-testing `wxGetMousePosition()`.
- `PopupWindow::Create`: on GTK it binds the top-level `wxEVT_ACTIVATE` to `topWindowActiavate` →
`DismissAndNotify()`, gated by the virtual `ShouldDismissOnTopWindowDeactivate()`, which `DropDown`
overrides for Wayland popup chains. On `__WXOSX__` with `wxPU_CONTAINS_CONTROLS`,
`PopupWindow::OnMouseEvent2` hit-tests children, re-dispatches mouse events and synthesises
enter/leave.
- `SidePopup::Popup` (`Widgets/SideMenuPopup.cpp`): on `__APPLE__` the menu is anchored flush against
the button with a slight overlap. Since the wx 3.3 upgrade, the transient popup was dismissed as
soon as the cursor entered the gap (#12936, 9a053f15eb). The wx mechanism was not established
(`references/popups-menus.md` §3): [source] `wxPopupTransientWindow::OnIdle` captures the mouse
whenever the cursor is outside the popup rect and releases it inside
(`src/common/popupcmn.cpp:438-471`, a 3.1.7 addition), and `wxPopupWindowHandler::OnLeftDown`
dismisses only on an outside click (`popupcmn.cpp:536+`), so neither explains a dismissal on hover.
**Menu bar and accelerators** → `references/mouse-keyboard-focus.md`, `references/popups-menus.md`
- `MainFrame::MainFrame`: `#ifndef __APPLE__` creates `m_topbar = new BBLTopbar(this)`, a
`wxAuiToolBar` with `BBLTopbarArt : wxAuiDefaultToolBarArt`. macOS gets a plain `wxPanel` top area
plus the native `wxMenuBar` that `MainFrame::init_menubar_as_editor` sets with `SetMenuBar`.
Preferences is added with `append_shortcut_item(..., Shortcut::Preferences, ...)` into
`OSXGetAppleMenu()` on macOS and into `m_topbar->GetTopMenu()` elsewhere.
- `MainFrame::shortcut_label`: items registered with `accelerator=true` whose binding is menu-safe
(`ShortcutRegistry::accelerator()` non-empty) get `"\t" + accelerator`. That is a live key
equivalent only in the macOS `wxMenuBar`; the Windows/Linux menus are popped up from `BBLTopbar`,
where accelerators are display-only and the registry dispatches the keys. Display-only shortcut
text uses the static `sep`, which is `" - "` on macOS and `"\t"` elsewhere, because the native menu
bar would otherwise grab keys that must reach text fields (#8152).
- `KeyChord::from_event` normalises char events: control codes 1–26 with `ControlDown()` become
`'A'..'Z'`, and lowercase becomes uppercase. Match shortcuts through it, never through raw char
codes for Ctrl/Cmd+letter. [source] `wxOSXTranslateCocoaKey` produces `WXK_CONTROL_A+n` only when
the physical Ctrl is held (`src/osx/cocoa/window.mm:305-307`), so a Cmd+letter char event carries
the plain letter. Control-code translation is documented at `interface/wx/event.h:1408-1421`.
- `MainFrame::MainFrame`, `wxEVT_CHAR_HOOK` lambda under `__APPLE__`: Cmd+H is swallowed, Cmd+M calls
`Iconize()`, Cmd+Q posts `wxEVT_CLOSE_WINDOW`, and Cmd+Ctrl+F calls `EnableFullScreenView(true)` and
toggles `ShowFullScreen` (`interface/wx/toplevel.h:700-728`, OSX only). Everything else goes to
`handle_global_shortcut(KeyChord::from_event(evt))`. Cmd+, is the registry's
`Shortcut::Preferences`, not part of the hook.
- `ObjectList::update_shortcut_accelerators`: the native macOS data view gets no key events, so on
macOS a `wxAcceleratorTable` is generated from the shortcut registry.
**Window decoration / custom title bar** → [Window decoration](#window-decoration-and-custom-title-bars)
- `MainFrame::MainFrame`: `set_miniaturizable` (OSX); `m_gdkDecor = 0` and three `ResizeEdgePanel`s
(GTK); the `WS_CAPTION` strip (MSW). `MainFrame::MSWWindowProc` and `AdjustWorkingAreaForAutoHide`
(MSW). `BBLTopbar::OnMouseLeftDown`, `BBLTopbar::OnFullScreen`, `BBLTopbar::MSWWindowProc`.
`SplashScreen::SplashScreen` (Wayland).
**Fonts, sizes, Retina** → `references/dpi-bitmaps-fonts.md`
- The `DPIAware` constructor and `MainFrame::init_tabpanel`: `#ifndef __WXOSX__` around `SetFont`,
"to avoid name cutting in ObjectList".
- `OG_CustomCtrl::CtrlLine::draw_text`: works around the Big Sur bold-font issue. Focused URL labels
are drawn underlined instead of bold and underlined.
- `SwitchButton::Rescale`: on `__WXOSX__` the measuring font is scaled by `mac_max_scaling_factor()`.
That helper reads screen 0's backing factor (`[[NSScreen screens] objectAtIndex:0]`, the menu-bar
screen) on every loop iteration, so despite its name it returns that screen's factor (at least 1),
not the maximum over all screens.
- `BitmapComboBox` (comment block under `#ifdef __APPLE__` in `BitmapComboBox.hpp`): the bitmap
`scale` argument means "the image is already sized for that backing scale". The `scale` parameter
of `wxBitmap(const wxImage&, int depth, double scale)` is not in the interface docs. It exists on
every port (`include/wx/osx/bitmap.h:115`, `include/wx/gtk/bitmap.h:77`), but wxMSW ignores it
(`include/wx/msw/bitmap.h:68`). The portable APIs are `CreateWithDIPSize` and `SetScaleFactor`
(`interface/wx/bitmap.h:491-520, 966`).
**GTK / Wayland** → this file, `references/webview-gl-aui-media.md`, `references/popups-menus.md`
- `CLI::run` (`src/OrcaSlicer.cpp`): sets backend-related environment variables before GTK starts.
- `GUI_App::on_init_inner`: calls `wxGLCanvas::PreferGLX()` when `is_running_on_x11()`, under
`#if defined(__WXGTK__) && wxHAS_EGL`.
- `OpenGLManager::detect_multisample`: on Wayland without `wxHAS_EGL` it skips `IsDisplaySupported()`,
which would go through GLX and crash on a missing X11 display. Multisampling is also off on ChromeOS
(`PlatformFlavor::LinuxOnChromium`).
- `OpenGLManager::init_gl`: loads GLAD through `eglGetProcAddress` on Wayland.
- `DropDown::messureSize`: positive-size `gtk_window_resize` on the GTK wrapper window, plus an idle
poll that synthesises `mouseMove` on the main dropdown while a submenu holds the grab (Mutter drops
motion events outside the grabbing surface). `DropDown::mouseMove` sets the submenu
`gtk_window_set_transient_for` to the mapped main popup at show time. `DropDown::Popup` gives
data-view cell editors an explicit top-level transient parent.
`DropDown::ShouldDismissOnTopWindowDeactivate` handles Wayland chains.
- `CheckBox::CheckBox`, `SwitchButton`, `RadioBox`, `ScalableButton` (`wxExtensions.cpp`),
`PresetComboBoxes` call `RemoveButtonBorder`; `TextInput` and `SpinInput` call
`RemoveInputBorder` on their inner `wxTextCtrl`.
- `Plater::priv::priv` together with `sanitize_window_layout_for_wayland`: AUI floating is disabled on
Wayland.
- `GUI_App::window_pos_restore`: skips `SetPosition` on Wayland.
- `BBLTopbar::OnMouseLeftDown` / `OnMouseMotion` convert event coordinates with `ClientToScreen`
instead of calling `wxGetMousePosition()`. `BBLTopbar::FindToolByCurrentPosition` returns null on
Wayland when the last event position is unknown or outside the bar.
- `LinuxDisplayBackend.{hpp,cpp}`: runtime X11/Wayland detection.
**Windows rendering and dark mode** → `references/colours-dark-mode.md`,
`references/painting-custom-widgets.md`
- `_MSW_DARK_MODE` is `#define`d to 1 **on every platform** (`GUI_App.hpp`), so it is not a platform
gate. The MSW-only calls inside those blocks sit under `__WINDOWS__` / `_WIN32`.
`dark_mode.cpp/.hpp` and `dark_mode/{dark_mode,IatHook,UAHMenuBar}.hpp` (the vendored Notepad++
dark mode, `NppDarkMode`) are compiled only under `if (WIN32)` in `src/slic3r/CMakeLists.txt`.
`SUPPORT_DARK_MODE` (`libslic3r/AppConfig.hpp`) gates `GUI_App::dark_mode`.
- `AMSExtText::render`: on `__WXMSW__` it blits the paint DC into a bitmap and renders through a
`wxGCDC` over the `wxMemoryDC` (GDI+ anti-aliasing), then `DrawBitmap`s the result. Other ports call
`doRender(dc)` directly. `StaticBox::render` uses a variant (rounded boxes only) that clears the
bitmap with the background colour instead of blitting. This is the recurring MSW pattern in custom
widgets.
- `GLCanvas3D::on_paint`: renders immediately on MSW (c06a0223a7).
**Native data view** → `references/controls-dataview.md`
- `GUI_ObjectList.cpp` has `__WXOSX__` editing and model paths, because the native macOS control never
calls a custom renderer's `CreateEditorCtrl`.
**macOS Objective-C++ glue** — `.mm` files listed in the `APPLE` block of `src/slic3r/CMakeLists.txt`,
for example:
- `Utils/MacDarkMode.mm`: `mac_dark_mode`, `mac_max_scaling_factor`, `set_miniaturizable`,
`set_title_colour_after_set_title`, the `WKWebView_*` helpers, `initGestures`.
- `GUI/GUI_UtilsMac.mm`: `dataview_remove_insets`, `staticbox_remove_margin`,
`set_window_corner_radius`, declared under `__WXOSX__` in `GUI_Utils.hpp`.
- `GUI/DeepLinkHandlerMac.mm`, `GUI/InstanceCheckMac.mm`, `GUI/Mouse3DHandlerMac.mm`,
`GUI/RemovableDriveManagerMM.mm`, `Utils/RetinaHelperImpl.mm`. (`libslic3r/MacUtils.mm` is registered
in `src/libslic3r/CMakeLists.txt` instead.)
Trackpad gestures: `GLCanvas3D::bind_event_handlers` calls the portable
`EnableTouchEvents(wxTOUCH_ZOOM_GESTURE | wxTOUCH_ROTATE_GESTURE)` plus
`initGestures(m_canvas->GetHandle(), m_canvas)` for the pan recogniser, and `unbind_event_handlers`
calls `initGestures(..., nullptr)`. Deep links: `GUI_App::on_init_inner` → `register_mac_deep_link_handler()`
re-registers the `kAEGetURL` handler after wx installs its own (1f2ed70288, #13119).
**WebView and media** → `references/webview-gl-aui-media.md`
- `WebView::CreateWebView`: `WebViewEdge` on Windows, `WebViewWebKit` on macOS, `wxWebView::New()` on
Linux.
- Camera: `wxMediaCtrl3`, a plain `wxWindow` that decodes on a worker thread and paints a `wxBitmap`
frame on Win32 and a `wxImage` frame elsewhere. No per-platform native player sits
behind it; new camera sources implement `IMediaController` (97955dbab8 and 7e3724b5f3 removed the
`wxMediaCtrl2.cpp/.mm` players).
## Runtime X11/Wayland detection
**API** (`src/slic3r/GUI/LinuxDisplayBackend.hpp`, declared only `#if defined(__WXGTK__)`, namespace
`Slic3r::GUI`):
```cpp
enum class LinuxDisplayBackend { X11, Wayland, Unknown };
LinuxDisplayBackend get_linux_display_backend(); // "Must be called after gtk_init() / wxWidgets initialization."
bool is_running_on_wayland();
bool is_running_on_x11();
```
**Mechanism.** `get_linux_display_backend()` tests `gdk_display_get_default()` with
`GDK_IS_WAYLAND_DISPLAY` and then `GDK_IS_X11_DISPLAY`. Each test is compiled only when
`wxHAVE_GDK_WAYLAND` / `wxHAVE_GDK_X11` is defined. The result is cached in a function-local static
on the **first** call. So:
- Called before GTK has a display, it caches `Unknown` for the rest of the process.
- In a GTK2 build neither macro is defined, so it always returns `Unknown`.
- `Unknown` makes both predicates false. Write every branch so that "neither" is safe. For example,
`GUI_App::on_init_inner` logs "Unknown display backend, defaulting to EGL" and does not call
`PreferGLX()`.
**Usage.**
```cpp
#ifdef __WXGTK__
#include "LinuxDisplayBackend.hpp"
#endif
...
#if defined(__WXGTK__)
if (Slic3r::GUI::is_running_on_wayland()) {
// Wayland-only path
}
#endif
```
**Before GTK starts**, only the environment exists. `CLI::run` (`src/OrcaSlicer.cpp`) branches on
`GDK_BACKEND` (an `x11` prefix means the X11 opt-in). On the default path it forces `GDK_BACKEND=x11`
when wx lacks EGL and `WAYLAND_DISPLAY` is set. It sets `WEBKIT_DISABLE_COMPOSITING_MODE=1`
(non-replacing) only when both `DISPLAY` and `WAYLAND_DISPLAY` are set, and calls `XInitThreads()`
only when `DISPLAY` is set. With neither variable set, the GUI refuses to start ("Neither DISPLAY nor
WAYLAND_DISPLAY set"). The details and the WebKit rule (c12912e0df) are in
`references/webview-gl-aui-media.md` §WebKitGTK on Linux sessions.
**wx's own detectors**, for reference:
- `wxGetDisplayInfo()` (`include/wx/utils.h:731-750`, public header but not in the interface docs)
returns `wxDisplayX11` / `wxDisplayWayland` / `wxDisplayNone` and the native display.
- `wxGTKImpl::IsWayland` / `IsX11` (`include/wx/gtk/private/backend.h`, GTK3 only) are private, and
each caches the answer of its first call.
Orca standardises on `LinuxDisplayBackend`, which keeps GDK headers out of callers.
**Pitfalls**
- **Rule:** Decide the backend with the GDK type check (`is_running_on_wayland()` /
`is_running_on_x11()`), not with `WAYLAND_DISPLAY` / `GDK_BACKEND` in GUI code. Environment
variables are appropriate only before GTK initialises.
**Why:** environment variables describe the session, not the backend GTK actually picked. An
XWayland run has both `DISPLAY` and `WAYLAND_DISPLAY` set while GTK talks X11, and a native Wayland
run usually has `DISPLAY` set too (XWayland available). GUI decisions keyed on the environment
misfire in both cases.
```cpp
// Wrong: GUI code reading the session environment
if (getenv("WAYLAND_DISPLAY"))
m_aui_mgr.SetFlags(m_aui_mgr.GetFlags() & ~wxAUI_MGR_ALLOW_FLOATING);
// Right (shape of Plater::priv::priv)
#if defined(__WXGTK__)
if (Slic3r::GUI::is_running_on_wayland())
m_aui_mgr.SetFlags(m_aui_mgr.GetFlags() & ~wxAUI_MGR_ALLOW_FLOATING);
#endif
```
Cite: 1b71835337 (`GUI_App.cpp`), `src/slic3r/GUI/LinuxDisplayBackend.cpp`.
- **Rule:** Never call the detectors from static initialisers or before `wxEntry` has initialised GTK.
**Why:** `gdk_display_get_default()` is null then, and the function-local cache keeps `Unknown`
forever, which silently disables every Wayland or X11 branch.
## Wayland gaps
wx has no dedicated Wayland document; the documented limits are scattered across the interface
headers. Rows marked [source] or "protocol" come from the implementation or from Wayland itself.
| Area | What happens on Wayland | Cite | Orca handling / owner |
|---|---|---|---|
| Global pointer position | `wxGetMousePosition()` / `wxGetMouseState()` call `gdk_device_get_position` [source], but Wayland gives clients no global pointer position, so the result is not a screen position | `src/gtk/window.cpp` `wxGetMousePosition` | use event coordinates + `ClientToScreen` (`BBLTopbar`); dismiss popups by focus tracking (`SearchDialog::Dismiss`) → `references/mouse-keyboard-focus.md` |
| Top-level position | the compositor places windows, and `SetPosition()` / `Move()` on a TLW have no effect (protocol) | — | `GUI_App::window_pos_restore` restores only size and maximised state; moves go through `gtk_window_begin_move_drag` |
| Window icon | `SetIcon()` / `SetIcons()` "doesn't do anything when using Wayland … create a `.desktop` file" | `interface/wx/toplevel.h:517-521, 538-542` | `src/dev-utils/platform/unix/com.orcaslicer.OrcaSlicer.desktop` (`Icon=OrcaSlicer`, `StartupWMClass=orca-slicer`) |
| App id | `wxAppConsole::SetClassName()` is the xdg `app_id` with GTK ≥ 3.24.22 (and the AUMID on Windows); it must be set before any TLW. wx applies it when a TLW is mapped, and only if it is non-empty [source: `wxTopLevelWindowGTK::GTKHandleMapped`] | `interface/wx/app.h:765-812` | Orca calls only `SetAppName(SLIC3R_APP_KEY)`, so GTK's default applies. On Windows, `SetClassName` would also change shell behaviour (MRU, Shift+middle-click) |
| `wxClientDC` | deprecated in 3.3 ("please use wxInfoDC instead for obtaining information", `interface/wx/dcclient.h:43-46`). Drawing through it "simply doesn't have any effect" on GTK3/Wayland or wxOSX. `CanBeUsedForDrawing()` [source]: false on Wayland only for wxGTK (`src/gtk/dc.cpp`), always false on wxOSX, always true on wxMSW (`include/wx/{osx,msw}/dcclient.h`), although its doc also lists "wxMSW when using double buffering" | `interface/wx/dcclient.h:48-53, 80-88` | draw only in `wxPaintDC` after `Refresh()`/`RefreshRect()` → `references/painting-custom-widgets.md` |
| `wxWindow::Update()` | "doesn't do anything in wxGTK port when using Wayland". [source] wx skips the GDK update calls there because they broke later updates (#25036) | `interface/wx/window.h:2405-2407` | never rely on `Update()` to paint synchronously |
| `WarpPointer()` | works only if the compositor implements the pointer-warp protocol; mutter also needs a mouse button held | `interface/wx/window.h:3902-3907`; `docs/changes.txt:294` | Orca never warps the pointer |
| `wxUIActionSimulator` | "currently doesn't work when using Wayland with wxGTK" | `interface/wx/uiaction.h:20` | not used; Orca also builds `wxUSE_XTEST=OFF` |
| `wxBitmap(const wxCursor&)` | creates an invalid bitmap | `interface/wx/bitmap.h:393-395` | — |
| OpenGL | only EGL; `PreferGLX()` has no effect. Without EGL in the build, wxGTK's `wxGLCanvas` shows a fatal message and refuses to work [source: `src/gtk/glcanvas.cpp` `IsAvailable`]. The EGL surface is a subsurface over the canvas, ready only after map and a frame callback [source: `src/unix/glegl.cpp`] | `interface/wx/glcanvas.h:1094-1095` | `CLI::run` forces X11 when wx lacks EGL; overlays are drawn in GL/ImGui, never as wx children over the canvas → `references/webview-gl-aui-media.md` |
| AUI | the doc note "live resize is always used … for wxOSX and wxGTK3 when using Wayland" is obsolete: "As of wxWidgets 3.3.0 this function always returns false", and `wxAUI_MGR_LIVE_RESIZE` is in the default flags. Floating panes need global positions | `interface/wx/aui/framemanager.h:336-345` | `Plater::priv::priv` clears `wxAUI_MGR_ALLOW_FLOATING`; `sanitize_window_layout_for_wayland` strips floating state from the saved layout → `references/webview-gl-aui-media.md` §wxAuiManager |
| Popups | a GTK popup is an `xdg_popup` only for COMBO/DROPDOWN/POPUP_MENU hints, so wx creates popups with `GDK_WINDOW_TYPE_HINT_COMBO` [source]. A chained popup's parent must be the mapped popup, and mapping it with a grab deactivates the toplevel (Orca's comments in `DropDown::mouseMove`, `DropDown::ShouldDismissOnTopWindowDeactivate`) | `src/gtk/popupwin.cpp:110-114` | `DropDown` transient-for chain, `ShouldDismissOnTopWindowDeactivate` → `references/popups-menus.md` |
| Fractional scale | arrives as an integer GDK scale | `docs/doxygen/overviews/high_dpi.md:348-351` | → `references/dpi-bitmaps-fonts.md` |
| Window decorations | some desktop environments draw a title bar on undecorated windows anyway | — | [Undecorated windows](#wayland-undecorated-top-level-windows-splash) |
| `wxWebViewChromium` | X11 only | `interface/wx/webview_chromium.h:133-146` | not built (`wxUSE_WEBVIEW_CHROMIUM` OFF) |
Wayland history in the change logs (`docs/changes_32.txt`): already in 3.1.5, Orca's previous wx,
were two-finger scrolling (703, 3.1.3), the EGL-based `wxGLCanvas` for Wayland (497) and `wxMediaCtrl`
support (498, both 3.1.5). New with the upgrade: "Many bug fixes for Wayland-specific problem" (397)
and a `wxMediaCtrl` fix (402) in 3.1.6, GDK errors from `PopupMenu()` avoided (317, 3.1.7),
`wxCURSOR_SIZING` fixed (261, 3.2.0). In 3.3, `WarpPointer()` on supported compositors
(`docs/changes.txt:294`) and the EGL/Wayland high-DPI scale fix (`changes.txt:538`).
## Window decoration and custom title bars
Only `MainFrame` replaces the native title bar. Dialogs keep native decorations, chosen through their
style flags (`references/windows-dialogs.md`). `MainFrame` is created with
```cpp
#ifndef __APPLE__
#define BORDERLESS_FRAME_STYLE (wxRESIZE_BORDER | wxMINIMIZE_BOX | wxMAXIMIZE_BOX | wxCLOSE_BOX)
#else
#define BORDERLESS_FRAME_STYLE (wxMINIMIZE_BOX | wxMAXIMIZE_BOX | wxCLOSE_BOX)
#endif
```
There is no `wxCAPTION` on any platform; each port then needs its own handling.
### MSW: the MainFrame custom title bar
**Contract** [source + documented]. Since wx **3.3.0**, `wxTopLevelWindowMSW::MSWGetStyle` adds
`WS_CAPTION` whenever any of `wxCAPTION | wxMINIMIZE_BOX | wxMAXIMIZE_BOX | wxCLOSE_BOX` is set
(`src/msw/toplevel.cpp:133-135`). The 3.3.0 wxMSW change list says "Turn wxCAPTION on automatically if
required by other styles (#23575)" (`docs/changes.txt:581`). Commit eefdabcd98 attributes this to
3.3.2, but it is a 3.3.0 change. `SetWindowStyleFlag()` recomputes the native style through
`MSWGetStyle` and turns the bits back on (`src/msw/window.cpp` `wxWindowMSW::MSWUpdateStyle`).
**OrcaSlicer design** (`MainFrame.cpp`, all under `__WXMSW__`):
- `MainFrame::MainFrame` strips `WS_CAPTION` with `SetWindowLongPtr` and applies it with
`SetWindowPos(..., SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE)`.
Without it Windows 10 showed the native frame behind the custom title bar, a "double window"
(f70d30bf79, #13074).
- `MainFrame::MainFrame` also binds `wxEVT_MAXIMIZE`: the handler clamps the frame to the display's
client area plus the border overshoot (`AdjustWindowRectEx`), moves it there and `Skip()`s, so a
maximised frame does not overlap the taskbar (restored by f70d30bf79).
- `MainFrame::MSWWindowProc`:
- `WM_NCACTIVATE`: sets `lParam = -1` so `DefWindowProc` does not repaint the non-client area, while
the window still receives activation.
- `WM_NCCALCSIZE` with `wParam` TRUE: computes the border with
`AdjustWindowRectEx(&r, GetWindowLongPtr(hWnd, GWL_STYLE) & ~WS_CAPTION, FALSE, 0)` and insets
left, right and bottom by it. When not maximised, the top grows by 1 px so the window can be
resized from its top edge. When maximised, the top is inset by the full border, because Windows
extends a maximised window beyond the screen by the border thickness. Then it returns 0.
- `WM_NCHITTEST`: returns `HTCAPTION` when maximised. Otherwise, over the top bar, points within the
border thickness give `HTTOP` / `HTTOPLEFT` / `HTTOPRIGHT` / `HTLEFT` / `HTRIGHT`, and the rest of
the bar gives `HTCAPTION`.
- `WM_GETMINMAXINFO`: `HandleGetMinMaxInfo` + `AdjustWorkingAreaForAutoHide`, which keeps a
maximised window off an auto-hide taskbar (#8085) and also masks `WS_CAPTION`.
- `BBLTopbar::MSWWindowProc` returns `HTTRANSPARENT` for `WM_NCHITTEST` over empty bar areas and the
title, and `CenteredTitle::MSWWindowProc` always returns it. The frame's `HTCAPTION` answer then
provides native dragging, double-click maximise and Snap. `BBLTopbar::OnMouseLeftDown` does
`CaptureMouse(); ReleaseMouse();` and posts `WM_NCLBUTTONDOWN` with `HTCAPTION`.
**Pitfall**
- **Rule:** For a Windows frame with a custom title bar, handle `WM_NCCALCSIZE` yourself. Mask
`WS_CAPTION` out of the style passed to every `AdjustWindowRectEx`
(`GetWindowLongPtr(hWnd, GWL_STYLE) & ~WS_CAPTION`). In the maximised branch, strip the full border
overshoot on all four sides instead of returning early.
**Why:** wx adds `WS_CAPTION` whenever a min/max/close box is requested, and puts it back when the
style is recomputed. Letting `DefWindowProc` (or an unmasked `AdjustWindowRectEx`) compute the
non-client area subtracts a caption you draw yourself, so a maximised window leaves a gap above the
taskbar.
```cpp
// Wrong: caption height included; maximised case left to DefWindowProc
AdjustWindowRectEx(&b, GetWindowLongPtr(hWnd, GWL_STYLE), FALSE, 0);
if (wPos.showCmd == SW_SHOWMAXIMIZED) break;
// Right
AdjustWindowRectEx(&b, GetWindowLongPtr(hWnd, GWL_STYLE) & ~WS_CAPTION, FALSE, 0);
b.left *= -1; b.top *= -1;
sz->rgrc[0].top += (wPos.showCmd == SW_SHOWMAXIMIZED) ? b.top : 1;
sz->rgrc[0].left += b.left; sz->rgrc[0].right -= b.right; sz->rgrc[0].bottom -= b.bottom;
return 0;
```
Cite: eefdabcd98 (`MainFrame::MSWWindowProc`).
### Linux (GTK): the borderless MainFrame
**wx mechanism** [source, `src/gtk/toplevel.cpp`]:
- At creation, wx turns the style into WM hints (`m_gdkDecor`, `m_gdkFunc`, :880-925).
`wxBORDER_NONE` / `wxSIMPLE_BORDER` call `gtk_window_set_decorated(false)`.
- On Wayland with GTK ≥ 3.10, a bordered window without `wxCAPTION` gets a `gtk_header_bar_new()`
titlebar (:900-906); `BORDERLESS_FRAME_STYLE` is such a style.
- On realise, `GTKHandleRealized` calls `gdk_window_set_decorations(window, m_gdkDecor)` (:400-444).
When a client-side titlebar exists it first sets `m_gdkDecor = 0`, because "Don't set WM
decorations when GTK is using Client Side Decorations".
**OrcaSlicer design:**
- **No WM decorations.** `MainFrame::MainFrame` sets `m_gdkDecor = 0` (a `public:` implementation
member of `wxTopLevelWindowGTK`, `include/wx/gtk/toplevel.h:108-109`) before the frame is
realised, so the window manager draws no title bar, while `m_gdkFunc` keeps move, resize, minimise,
maximise and close. `BBLTopbar` is the only title bar (d50b4cbf3d, #12600 "No more double title
bar").
- **Move.** `BBLTopbar::OnMouseLeftDown`, on empty bar areas and the title, calls
`gtk_window_begin_move_drag(GTK_WINDOW(m_frame->m_widget), 1, x, y, gtk_get_current_event_time())`
with `x, y` taken from `ClientToScreen(event.GetPosition())`. This is a compositor-driven move, and
on Wayland it is the only way a client can move its window.
- **Maximise.** `BBLTopbar::OnFullScreen`, which also handles a double-click on the title, uses
`gtk_window_is_maximized` / `gtk_window_maximize` / `gtk_window_unmaximize` on GTK.
- **Resize.** Three `ResizeEdgePanel`s (bottom, left, right; `BORDER_PX` = 5): transparent `wxPanel`s
(`wxBG_STYLE_TRANSPARENT`, empty `wxPaintDC` paint) that are `Raise()`d above all siblings, so their
GDK windows receive pointer events even over WebKit2GTK or GL surfaces. On motion they set a named
cursor (`gdk_cursor_new_from_name`, `"s-resize"`, `"sw-resize"`, …). On left-down they call
`gtk_window_begin_resize_drag` with the matching `GdkWindowEdge`, unless the frame is maximised or
fullscreen. `MainFrame::update_edge_panels` hides them while maximised or fullscreen, lays them out
along the client edges and raises them again. `MainFrame::shutdown` clears the pointers; wx
destroys the panels as children. They use event-relative coordinates, not a global event filter
keyed on `wxGetMousePosition()`, which cannot work on Wayland (6049c6e234, #12705).
**Pitfall**
- **Rule:** Move and resize the borderless frame through `gtk_window_begin_move_drag` /
`gtk_window_begin_resize_drag` with the current event time. Do not capture the mouse and call
`Move()` / `SetSize()` from global coordinates.
**Why:** on Wayland, clients cannot position top-level windows and get no global pointer position,
so a manual drag does nothing or jumps. On X11, the WM-driven drag also gives edge snapping and
correct multi-monitor behaviour.
```cpp
// Wrong (GTK): manual drag
CaptureMouse(); /* on motion: */ m_frame->Move(::wxGetMousePosition() - m_delta);
// Right (GTK)
wxPoint p = ClientToScreen(event.GetPosition());
gtk_window_begin_move_drag(GTK_WINDOW(m_frame->m_widget), 1, p.x, p.y, gtk_get_current_event_time());
```
Cite: `BBLTopbar::OnMouseLeftDown`; `ResizeEdgePanel::OnLeftDown` (`MainFrame.cpp`).
### macOS: a native titled window
- [source] wxOSX gives a window `NSTitledWindowMask` as soon as any of `wxMINIMIZE_BOX`,
`wxMAXIMIZE_BOX`, `wxCLOSE_BOX`, `wxSYSTEM_MENU` or `wxCAPTION` is set, and adds the
miniaturizable, resizable and closable masks for the matching boxes
(`src/osx/cocoa/nonownedwnd.mm:816-831`). `BORDERLESS_FRAME_STYLE` therefore still produces a titled
window with the standard window buttons. `wxRESIZE_BORDER` is left out on Apple, and the window is
still resizable because `wxMAXIMIZE_BOX` already adds `NSResizableWindowMask`.
- `set_miniaturizable(GetHandle())` (`Utils/MacDarkMode.mm`, called in `MainFrame::MainFrame` under
`__WXOSX__`) sets `titlebarAppearsTransparent`, sets a dark window background colour, ORs in
`NSMiniaturizableWindowMask`, and remembers the title `NSTextField`.
`set_title_colour_after_set_title` re-colours that field white after the title changes.
- There is no `BBLTopbar` on macOS: the top area is a plain `wxPanel`, and the menus live in the
native `wxMenuBar`.
- Fullscreen: Cmd+Ctrl+F calls `EnableFullScreenView(true)` and toggles `ShowFullScreen()`.
`EnableFullScreenView` is OSX-only, and the full-screen button is needed for the animated
fullscreen space (`interface/wx/toplevel.h:700-728`).
### Wayland: undecorated top-level windows (splash)
- **Rule:** To show a truly undecorated top-level window (the splash screen) on Wayland, don't rely on
wx style flags or window-type hints. Install an empty client-side titlebar and disable decoration
on the GTK window in the constructor, before control returns to the event loop. [source] The
`wxSplashScreen` base constructor has already called `Show(true)` (`src/generic/splash.cpp`), so in
`SplashScreen` these calls land right after the show request and before any event is processed; in
a window you show yourself, make them before `Show()`:
```cpp
#if defined(__WXGTK__)
if (Slic3r::GUI::is_running_on_wayland()) {
GtkWidget* empty = gtk_fixed_new();
gtk_widget_set_size_request(empty, 0, 0);
gtk_window_set_titlebar(GTK_WINDOW(GetHandle()), empty);
gtk_window_set_decorated(GTK_WINDOW(GetHandle()), false);
}
#endif
```
**Why:** some Wayland desktop environments ignore splash-typed window properties (wxGTK's
`wxSplashScreen` sets `GDK_WINDOW_TYPE_HINT_SPLASHSCREEN`) and draw a title bar anyway. This
happens even though `wxBORDER_NONE` already makes wx call `gtk_window_set_decorated(false)`. Forcing an empty client-side titlebar removes it on every
desktop environment.
Cite: 1b71835337 (`SplashScreen::SplashScreen` in `GUI_App.cpp`; the splash style is
`wxBORDER_NONE | wxFRAME_NO_TASKBAR`, plus `wxSTAY_ON_TOP` on Apple).
## GTK native chrome and GTK size calls
**Helpers** (`GUI_Utils.hpp`, declared under `__WXGTK__`):
- `RemoveButtonBorder(wxWindow*)` is "for wxButton/wxBitmapToggleButton based controls (SwitchButton,
CheckBox)".
- `RemoveInputBorder(wxWindow*)` is "for TextCtrl based controls (TextInput, ComboBox, SpinInput..)".
Both return immediately if `GetHandle()` is null, so call them after the control is created; the
widgets do it in their constructors.
- **GTK3** (and the GTK4 branch): a `GtkCssProvider` is added to the widget's own style context at
`GTK_STYLE_PROVIDER_PRIORITY_USER`, then released with `g_object_unref` (the context keeps its
reference). The CSS covers `button, button:hover, button:active, button:focus`, or
`entry, entry text, entry undershoot` for inputs, and zeroes `border`, `outline`, `box-shadow`,
`padding`, `margin`, `min-height` and `min-width`, with `background: none`.
- **GTK2**: `gtk_rc_parse_string` installs a **global** rc style keyed by widget path or class
(`"*.GtkBitmapToggleButton"`, class `"GtkEntry"`). The first call changes every matching widget in
the process, not just the one passed in.
**Callers:** `CheckBox::CheckBox`, `SwitchButton`, `RadioBox`, `ScalableButton` and
`PresetComboBoxes` (`RemoveButtonBorder`); the inner `wxTextCtrl` of `TextInput` and `SpinInput`
(`RemoveInputBorder`). A new owner-drawn control built on a native GTK widget needs the same call.
**Sizing a bitmap button on GTK.** A `wxBitmapToggleButton`/`wxButton`-based widget sized to exactly its
bitmap leaves no room for the theme's CSS padding, and GTK logs "negative content width" criticals.
Either strip the button CSS with `RemoveButtonBorder` and size to the bitmap (`CheckBox::Rescale`), or
size to `GetBestSize()` grown to the bitmap (`IncTo`; `RadioBox::Rescale` and `SwitchButton::Rescale`
do both). Cite: 6148ba16b3, 988b500f33.
**Pitfalls**
- **Rule:** To strip native GTK control chrome (entry and button borders, padding), attach a
`GtkCssProvider` at `GTK_STYLE_PROVIDER_PRIORITY_USER` to the widget's style context, and include the
pseudo-class states (`button, button:hover, button:active, button:focus { ... }`), plus the inner
subnodes for entries (`entry, entry text, entry undershoot`). Guard the code with `#ifdef __WXGTK__`,
branch GTK2/3/4 with `GTK_CHECK_VERSION`, and keep the `.hpp` declaration guard identical to the
`.cpp` definition guard.
**Why:** wx border-style flags don't remove GTK theme borders or padding, which show up as black
borders on Linux. CSS without `:hover` / `:focus` lets the border come back on interaction. Guards
that differ between header and implementation (`__WXGTK3__` vs `__WXGTK__`) break the build on other
GTK versions.
```cpp
// Wrong: wx flags alone, border reappears / theme padding stays
text_ctrl = new wxTextCtrl(this, wxID_ANY, text, pos, size, style | wxBORDER_NONE);
// Right
text_ctrl = new wxTextCtrl(this, wxID_ANY, text, pos, size, style | wxBORDER_NONE);
#ifdef __WXGTK__
Slic3r::GUI::RemoveInputBorder(text_ctrl);
#endif
```
Cite: 477208a969 (`GUI_Utils.cpp` `RemoveButtonBorder` / `RemoveInputBorder`, `GUI_Utils.hpp`).
- **Rule:** Guard direct `gtk_window_resize()` calls (and similar GTK size calls) so they run only with
strictly positive width and height.
**Why:** GTK checks the arguments (`gtk_window_resize: assertion 'width > 0'`) when a popup is
measured before it has content. This spams assertion errors on Linux and aborts when GTK criticals
are made fatal. `DropDown` resizes the GTK wrapper window itself (source comment: "Gtk has a
wrapper window for popup widget"), so its size can be zero before the items exist.
```cpp
// Wrong
gtk_window_resize(GTK_WINDOW(m_widget), szContent.x, szContent.y);
// Right
if (szContent.x > 0 && szContent.y > 0)
gtk_window_resize(GTK_WINDOW(m_widget), szContent.x, szContent.y);
```
Cite: dc12126b78 (`DropDown::messureSize`, `Widgets/DropDown.cpp`).
## wx 3.3 migration notes
Orca moved from **3.1.5** to 3.3.2 in 8248b06337 ("Updated wxWidgets to 3.3.2", #12941; build system
in 2d7e26292b), so the 3.1.6–3.2.0 incompatible changes (`docs/changes_32.txt`) apply as well as
`docs/changes.txt`. The version digest, every migration commit as a rule for new code, and the
post-upgrade regressions to watch are owned by `references/wx-33-changes.md` (§8). The
platform-specific ones are described in this file: the `MainFrame` `WS_CAPTION` handling
([MSW title bar](#msw-the-mainframe-custom-title-bar)), the macOS `kAEGetURL` re-registration and the
`SidePopup` anchoring ([ifdef landscape](#the-ifdef-landscape)), and the GTK criticals filter
([GTK3 summary](#gtk3-x11-and-wayland)).
## Cross-platform testing checklist
Because wx asserts are compiled out, a platform bug usually shows up as wrong pixels, a dropped
event or a frozen interaction, not a dialog. Exercise the change; a successful build proves little.
Say in the PR which platforms you exercised.
**Build**
- [ ] Every toolkit branch compiles: `__WXMSW__`, `__WXOSX__`, `__WXGTK__`. GTK code also compiles with
the GTK2 branch of any `GTK_CHECK_VERSION`, and header/implementation guards match.
- [ ] New Cocoa code is in a `.mm` file registered in the `APPLE` block, and new sources are added to
`src/slic3r/CMakeLists.txt`.
- [ ] No `FromSVG*`, no new wx private header without the matching port macro, no reliance on a wx
assert.
**Windows**
- [ ] 100 %, 125 %, 150 % and 175 % scaling, plus moving the window between monitors with different
scaling (PMv2 `wxEVT_DPI_CHANGED`, `DPIAware` rescale, `on_dpi_changed`).
- [ ] Light and dark, including switching dark mode at runtime from Preferences.
- [ ] `MainFrame` maximise/restore, maximise with an auto-hide taskbar, Snap, drag from the top bar,
resize from the top edge.
- [ ] Popups: open, then click elsewhere, Alt+Tab, minimise the main window; the popup must close
exactly once.
- [ ] Live window resize with the 3D view visible (no blank canvas); custom-painted widgets don't
flicker.
**macOS**
- [ ] A Retina and a non-Retina display, and moving windows between them.
- [ ] Switching system appearance while the app runs.
- [ ] Menu-bar shortcuts vs text fields (typing in a field must not trigger a menu shortcut), and
Cmd+H / Cmd+M / Cmd+Q / Cmd+Ctrl+F.
- [ ] Popups and dropdown menus: move the cursor from the anchor into the menu without it
dismissing; nothing stays captured after a drag (the UI must stay clickable).
- [ ] `ObjectList` editing and keyboard shortcuts (native data view).
**Linux, GTK3 on X11** (`GDK_BACKEND=x11` on a Wayland session, or an X11 session)
- [ ] Scale 1 and 2 (`GDK_SCALE=2`), light and dark GTK themes, large font settings (text-driven
sizes, `em_unit`).
- [ ] No native borders or padding around custom widgets; dialogs open at their fitted size and don't
collapse after minimising the main window.
- [ ] The GL views render (GLX path), and WebView pages load.
**Linux, GTK3 on native Wayland**: at least GNOME (mutter) and one wlroots compositor (Sway or
Hyprland); KDE is worth a run for decoration differences.
- [ ] Moving and resizing the main window through the top bar and the edges; maximise/restore; no
second title bar; the splash has no title bar.
- [ ] Popups, chained dropdown submenus and search dropdowns open in the right place, track hover and
dismiss correctly.
- [ ] Nothing depends on `wxGetMousePosition()`, `SetPosition()` on a top-level window, or
`wxClientDC` drawing. No CPU spin with the window on an inactive workspace.
- [ ] GL views render after startup (EGL post-init retry), and overlay icons are not translucent.
- [ ] Dock panes cannot float, and a layout saved on X11 loads.
**Packaging**
- [ ] The Flatpak build (shared wx, GTK3, sandbox) if the change touches wx options, file dialogs,
WebView or desktop integration.
**Debugging aids**
- [ ] In a Debug or RelWithDebInfo build, Ctrl+Shift+I (Cmd+Shift+I on macOS) on any `DPIAware` window
opens wxInspector to check the window tree, sizes and styles per platform.
- [ ] On Linux, remember that `GUI_App::on_init_inner` filters some GTK criticals. Check the terminal
output for the rest.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,822 @@
# Sizers and layout
How wx 3.3.2 computes window sizes and lays out children, and how OrcaSlicer builds layouts on top of
that: sizers and flags, best/min size, the fitting functions (`SetSizerAndFit`, `SetSizeHints`, `Fit`,
`Layout`, `FitInside`) per platform, show/hide relayout, Freeze/Thaw, scrolled windows, `wxStaticText`
wrapping, layout on DPI change, and Orca's layout idioms. Read it when building or reviewing any dialog
or panel layout, or when debugging a window that is collapsed, clipped, too large or not re-laid out.
Contents: [Rules](#rules) · [Build facts](#build-facts-that-change-how-layout-bugs-present) ·
[Size model](#the-size-model-best-min-effective-min-initial-virtual) ·
[Fitting functions](#fitting-functions-setsizer-setsizerandfit-setsizehints-fit-layout) ·
[Re-layout](#re-layout-after-content-or-visibility-changes) · [Adding items](#adding-items-proportion-flags-wxsizerflags) ·
[Ownership](#ownership-and-removal) · [Specific sizers](#specific-sizers) ·
[Size events](#wxevt_size-handlers) · [Freeze/Thaw](#freeze--thaw) ·
[Scrolled windows](#scrolled-windows) · [Static text wrapping](#wxstatictext-wrapping-and-ellipsizing) ·
[Layout on DPI change](#layout-on-dpi-change) · [Platform summary](#platform-summary) ·
[Orca idioms](#orcaslicer-layout-idioms-and-spacing-conventions)
## Rules
1. On a top-level window, attach the finished sizer with `SetSizerAndFit(sizer)`. If `SetSizer` must
come before the content exists, call `GetSizer()->SetSizeHints(this)` once the content is built, and
again after rebuilding content. Never rely on `Fit()` alone. → [Fitting](#fitting-functions-setsizer-setsizerandfit-setsizehints-fit-layout)
2. Child panels use plain `SetSizer`; never `SetSizerAndFit` or `sizer->SetSizeHints(panel)` on a
non-top-level window (it pins the panel's min size). → [Fitting](#fitting-functions-setsizer-setsizerandfit-setsizehints-fit-layout)
3. Keep a dialog's content within any `SetMaxSize` (cap a scrolled region), or the min > max hints are
silently dropped. → [Fitting](#fitting-functions-setsizer-setsizerandfit-setsizehints-fit-layout)
4. Proportion is the second argument: `Add(w, 0, wxEXPAND | wxALL, FromDIP(n))`, never `Add(w, wxEXPAND)`.
→ [Adding items](#adding-items-proportion-flags-wxsizerflags)
5. In a box sizer, alignment and `wxEXPAND` act only across the sizer's direction, and `wxEXPAND`
overrides alignment; contradictory flags are silently ignored in Orca. → [Adding items](#adding-items-proportion-flags-wxsizerflags)
6. Give `proportion > 0` only to items that must stretch; proportions inflate the sizer's min size.
→ [Adding items](#adding-items-proportion-flags-wxsizerflags)
7. A window managed by a sizer is a child of the sizer's containing window (or of the `wxStaticBox` of a
`wxStaticBoxSizer`), and sits in exactly one sizer; `Detach` before re-adding.
→ [Adding items](#adding-items-proportion-flags-wxsizerflags), [Ownership](#ownership-and-removal)
8. When rebuilding content, destroy the old windows; deleting, clearing or replacing a sizer leaves them
alive and visible. → [Ownership](#ownership-and-removal)
9. After changing content or visibility, `Layout()` the nearest ancestor whose allocation must change;
resize a top-level window with `GetSizer()->SetSizeHints(tlw)`. `Hide()` is always followed by a
`Layout()`. → [Re-layout](#re-layout-after-content-or-visibility-changes)
10. A `wxEVT_SIZE` handler calls `Skip()` and never `SetSize`s its own window. → [Size events](#wxevt_size-handlers)
11. `Freeze()`/`Thaw()` must balance on every path; use `wxWindowUpdateLocker`. → [Freeze/Thaw](#freeze--thaw)
12. A scrolled window needs a non-zero `SetScrollRate`, `FitInside()` after its content changes, and an
explicit min size or proportion + `wxEXPAND` in its parent. → [Scrolled windows](#scrolled-windows)
13. To re-wrap a `wxStaticText` after `SetLabel`, call `Wrap(-1); Wrap(w);`, or use `Label` with
`LB_AUTO_WRAP`; use `Label` for CJK text. → [Wrapping](#wxstatictext-wrapping-and-ellipsizing)
14. Never compute or commit a size from a width that has not been laid out yet. → [Wrapping](#wxstatictext-wrapping-and-ellipsizing)
15. Give wrapping labels a fixed width and `-1` height, never a fixed height. → [Wrapping](#wxstatictext-wrapping-and-ellipsizing)
16. In `on_dpi_changed`, re-apply what wx does not rescale, then resize with
`GetSizer()->SetSizeHints(this)`; never multiply existing min sizes or borders by a DPI ratio.
→ [Layout on DPI change](#layout-on-dpi-change)
17. A custom widget reports its size through its min size (Orca widgets) or `DoGetBestClientSize()`, and
invalidates it when content, label or font change. → [Size model](#the-size-model-best-min-effective-min-initial-virtual)
18. Pixel values are `FromDIP(n)` or `n * em_unit()`; borders are explicit `FromDIP(n)`; dialog button
rows are `DialogButtons`. → [Orca idioms](#orcaslicer-layout-idioms-and-spacing-conventions)
## Build facts that change how layout bugs present
- **Every wx layout assert is silent in Orca.** wx is built with `wxBUILD_DEBUG_LEVEL=0` and
`libslic3r_gui` with `wxDEBUG_LEVEL=0`, so `wxASSERT`/`wxFAIL` compile to nothing and `wxCHECK_*` return
early without a message (`include/wx/debug.h:314-324, 342-382`). The checks that would flag layout
mistakes in a debug wx therefore do nothing, and the review has to catch them by reading the code:
- flag consistency in box sizers (`wxBoxSizer::DoInsert`, `src/common/sizer.cpp:2295`);
- "window managed by the sizer must have the containing window as parent" (`wxSizer::DoInsert`,
`sizer.cpp:904, 947`);
- mixed parents in one `wxStaticBoxSizer` (`wxStaticBoxSizer::RepositionChildren`, `sizer.cpp:2888`);
- duplicate or out-of-range `AddGrowableCol/Row` (`sizer.cpp:2235, 2250`);
- `Thaw()` without `Freeze()` (`src/common/wincmn.cpp:1247`);
- a window added to a second sizer (`wxWindowBase::SetContainingSizer`, `wincmn.cpp:2421`): the
`wxCHECK_RET` refuses the bookkeeping, but the item is still inserted;
- top-level `SetSizeHints` with min > max (`wxWindowBase::DoSetSizeHints`, `wincmn.cpp:1041`): the
whole call is dropped.
- **Linux is GTK3** (`DEP_WX_GTK3` ON, `SLIC3R_GTK` "3"); GTK2 is an opt-out build. GTK-only facts below
are GTK3 unless marked.
- **DIP model.** `wxHAS_DPI_INDEPENDENT_PIXELS` is defined for wxGTK3 and wxOSX
(`include/wx/features.h:115`): logical pixels are DIPs and `FromDIP` is the identity. On MSW `FromDIP`
scales by the window's DPI (`wincmn.cpp` `wxWindowBase::FromDIP`); GTK2 takes the same conversion path,
but its display PPI is always 96, so `FromDIP` is the identity there too **[source]**. DIP conversion
itself: see `references/dpi-bitmaps-fonts.md`.
## The size model: best, min, effective min, initial, virtual
**Contract** (`docs/doxygen/overviews/windowsizing.h:23-100`):
- *Best size* is derived from content. *Min size* is "normally explicitly set by the programmer"; most
controls also take it from a non-default ctor size. *Initial size* is the ctor size; a partly specified
size such as `wxSize(150, -1)` is completed from the best size. *Virtual size* is the scrollable extent.
- `GetEffectiveMinSize()` merges the best size into the min size: "This is the value used by sizers to
determine the appropriate amount of space to allocate for the widget" (`interface/wx/window.h:1393-1402`). It is the min size with unspecified components filled
from the best size (`wincmn.cpp:868`). Once `SetMinSize(wxSize(w, h))` sets both components, the
content no longer affects the sizer allocation; pass `-1` for the component that must follow content.
- "The best size respects the minimal and maximal size explicitly set for the window" (`window.h:1342-1350`;
`wincmn.cpp:879`: raised to min, lowered to max). It is **cached only when the window has no sizer**
**[source]** (`wincmn.cpp:881`). Containers with sizers recompute every time; leaf controls and custom
widgets return the cache until `InvalidateBestSize()` (`window.h:1645-1651`; the cache is documented for
`DoGetBestClientSize`, `window.h:4432-4435`).
- `InvalidateBestSize()` also invalidates the parent chain, stopping at a top-level window **[source]**
(`wincmn.cpp:636`). It does not lay anything out (see [Re-layout](#re-layout-after-content-or-visibility-changes)).
- The default `DoGetBestSize()` uses the sizer's min size; without a sizer, the bounding box of visible
children; with no children, the min size or (1,1) (`wincmn.cpp:649`).
- `SetInitialSize(size)` sets the min size to `size`, merges it with the best size and resizes
(`window.h:1742-1757`, `wincmn.cpp:937`). A ctor size therefore becomes the min size: a fixed height
pins the minimum height.
- `SetMinSize` "doesn't prevent the program from making the window explicitly smaller … by calling
SetSize(), it just ensures that it won't become smaller than this size during the automatic layout"
(`window.h:1808-1815`). Top-level windows are the documented exception: their size hints also stop the
program's own `SetSize()` (`interface/wx/toplevel.h:576-603`). **[source]** On GTK, top-level windows and
`wxPopupWindow` clamp `SetSize` to min/max (`src/gtk/toplevel.cpp:1365` `ConstrainSize`,
`src/gtk/popupwin.cpp:171`).
- On a top-level window, `SetMinSize`/`SetMaxSize` go through `SetSizeHints(min, max)`
(`src/common/toplvcmn.cpp:197-205`), so a min larger than the current max (or the reverse) is dropped
silently.
- `wxWindow::SetSizeHints` on a non-top-level window "is discouraged. Please use SetMinSize() and
SetMaxSize() instead" (`window.h:1885-1891`).
- **Height-for-width (3.3.2).** `GetMinSizeFromKnownDirection(direction, size, availableOtherDir)`
(`window.h:1404-1444`) lets a control report its min size once the layout fixes one dimension; box and
flex-grid sizers feed the known width to their items during layout, and `wxSizer::CalcMinSizeFromKnownDirection`
is the sizer side (`interface/wx/sizer.h:339-377`). `InformFirstDirection` is the deprecated
compatibility path. `wxST_WRAP` and `wxWrapSizer` rely on this negotiation. `DoGetBestClientHeight()`/
`DoGetBestClientWidth()` are "not used by wxWidgets yet" (`window.h:4453-4455`): overriding them changes
no sizer layout.
**Writing a custom control (wx way).** Override `DoGetBestClientSize()` and let `DoGetBestSize()` add the
borders (`windowsizing.h:46-51`, `window.h:4421-4441`); the default returns `wxDefaultSize` and the best
size is then arbitrary. Call `SetInitialSize()` at the end of `Create()`, and `InvalidateBestSize()`
whenever content, label or font change.
**OrcaSlicer.** The `StaticBox`-based widgets (`Button`, `TextInput`, `SpinInput`, `ComboBox`) do not
override `DoGetBestClientSize`. Each has (or inherits) a `messureSize()` that measures its content and calls
`wxWindow::SetMinSize(...)`, and they override `SetMinSize` to merge a caller's request with the content
size: `Button::SetMinSize` stores the request (its height overrides, its width is a floor), and
`TextInput::SetMinSize` fills a `-1` height from the current size. Re-measuring happens inside their
`SetLabel`/`SetFont`/`Rescale` overrides, so callers only re-lay out the parent. A min size is honoured
identically by every sizer and has no cache to invalidate, so this design never needs `InvalidateBestSize()`.
Writing a new Orca widget: see `references/painting-custom-widgets.md`.
For owner-drawn text whose height depends on width, follow `WikiLabel` (`src/slic3r/GUI/Preferences.cpp`):
re-wrap on width change, then `SetMinSize(wxSize(-1, totalH))` + `InvalidateBestSize()`; override
`DoGetBestSize()`; guard `GetCharHeight()` against 0 before the window is realized on GTK (Orca comment).
## Fitting functions: SetSizer, SetSizerAndFit, SetSizeHints, Fit, Layout
**Contract.**
- `SetSizer(s, deleteOld = true)`: "The window will then own the object, and will take care of its
deletion"; `deleteOld` deletes a previous sizer (pass `false` only if you delete it yourself). It "will
also call SetAutoLayout() implicitly with true … so that the sizer will be effectively used to layout
the window children whenever it is resized" (`window.h:3693-3715`).
- `SetSizerAndFit(s)` "calls SetSizer() and then wxSizer::SetSizeHints() which sets the initial window
size to the size needed to accommodate all sizer elements and sets the minimal size to the same size,
this preventing the user from resizing this window to be less than this minimal size (if it's a
top-level window …)" (`window.h:3717-3728`; `wincmn.cpp:2414`).
- `wxSizer::SetSizeHints(win)` is documented as "first calls Fit() and then
wxTopLevelWindow::SetSizeHints() … It does nothing in normal windows or controls", with the idiom of
calling the panel's sizer's `SetSizeHints(frame)` to size the frame to fit the panel
(`interface/wx/sizer.h:937-970`). **[source]** The doc is outdated: it calls
`WXSetInitialFittingClientSize(wxSIZE_SET_CURRENT | wxSIZE_SET_MIN)` (`sizer.cpp:1284`), which calls
`SetMinClientSize()` then `SetClientSize()` on **any** window (`wincmn.cpp:974`). On a child panel it
freezes the panel's min size at its current content.
- `wxSizer::Fit(win)` resizes the window so its client area matches the sizer's min size
(`sizer.h:454-463`; `sizer.cpp:1243`: `wxSIZE_SET_CURRENT` only).
- `wxWindow::Fit()` "only changes the current window size and doesn't change its minimal size"
(`window.h:1066-1077`); it is `SetSize(GetBestSize())` (`wincmn.cpp:625`), except that a top-level window
without a sizer and with exactly one child sets its client size to that child's best size
(`toplvcmn.cpp:508`).
- `Layout()` "doesn't do anything" without a sizer unless the window is top-level; it "is called
automatically when the window size changes if it has the associated sizer" (`window.h:3753-3769`). It
positions children inside the current virtual size (`wincmn.cpp:2469`). A top-level window without a
sizer and with exactly one child resizes that child to fill the client area (`toplvcmn.cpp:475`).
| Call | Sets current size | Sets min size | Clamped to display (TLW) | GTK3: replayed at `Show()` if called while hidden |
|---|---|---|---|---|
| `win->SetSizer(s)` | no | no | – | – |
| `win->SetSizerAndFit(s)` | yes (client) | yes (client) | yes | yes |
| `s->SetSizeHints(win)` | yes | yes | yes | yes (with the window's own sizer) |
| `s->Fit(win)` | yes | **no** | yes | yes |
| `win->Fit()` | yes | **no** | **no** | **no** |
| `win->Layout()` | no (positions children) | no | – | – |
**[source]** `ComputeFittingClientSize` (`sizer.cpp:1194`) clamps a **top-level** window only to the
display client area; the doc's "maximum window size if previously set" applies to child windows only. If
a dialog's `SetMaxSize` is smaller than its content, `SetSizeHints` computes min > max,
`DoSetSizeHints` drops the hints, and on GTK the following `SetClientSize` is clamped to the max: the
dialog ends up with no enforced minimum.
**GTK3 hidden-window replay [source]** (`src/gtk/toplevel.cpp`). `wxTopLevelWindowGTK::WXSetInitialFittingClientSize`
(`:1726`) applies the fit at once and, if the window is still hidden, stores the flags because the "GTK
style cache hasn't been updated yet"; `Show()` (`:1262-1266`) and `GTKDoAfterShow()` (`:1687`) replay them
through `GTKUpdateClientSizeIfNecessary()` (`:1702`), which re-fits with the window's **own** sizer (a
`panel_sizer->SetSizeHints(frame)` on a sizer-less frame is not replayed). An explicit `SetMinSize()`
cancels the pending minimum (`:1715-1723`); an explicit size change cancels the pending current size but
keeps the pending minimum (`:1384-1396`). `wxWindow::Fit()` never takes this path, so a hint-less GTK3
dialog keeps whatever was measured with the stale style cache.
**GTK size hints [source].** WM min/max geometry hints are set only for `wxRESIZE_BORDER` windows; a
non-resizable dialog is sized through `gtk_widget_set_size_request` in `DoSetSize`
(`toplevel.cpp:1485-1503, 1399-1407`). Either way the min comes from `SetSizeHints`.
**Usage — the three canonical shapes:**
```cpp
// (a) content built before the sizer is attached
SetSizerAndFit(main_sizer); // size + min, display-clamped, GTK3-safe
CenterOnParent();
// (b) sizer attached early, content added later (MsgDialog::finalize)
SetSizer(main_sizer); /* ... add content ... */
GetSizer()->SetSizeHints(this); Layout(); CenterOnParent();
// (c) content changed after the dialog exists (PrinterPartsDialog::Show)
/* ... show/hide/add rows ... */
GetSizer()->SetSizeHints(this); // not Fit(): Fit() leaves the old minimum
```
`SetSizeHints` already sets the current size, so a `Fit()` before it is redundant, and a `Fit()` after it
only re-applies the best size without the display clamp.
**OrcaSlicer.** The project rule (`AGENTS.md`): "Always use `SetSizerAndFit(sizer)` instead of
`SetSizer(sizer)` on top level window. Unless `SetSizer` must be called before the full layout is built,
call `sizer->SetSizeHints(window)` afterwards in this case." Models: `CloneDialog` ctor (shape a);
`MsgDialog` (its ctor calls `SetSizer(main_sizer)`, subclasses add content, `MsgDialog::finalize` runs
`GetSizer()->SetSizeHints(this); Layout(); Fit(); CenterOnParent(); wxGetApp().UpdateDlgDarkUI(this);`);
`NetworkPluginDownloadDialog` ctor (`main_sizer->SetSizeHints(this)` after building the mode-specific UI);
`PrinterPartsDialog::Show` (shows/hides its panels and rows, then `GetSizer()->SetSizeHints(this)` before
`DPIDialog::Show`); the calibration dialogs in `calib_dlg.cpp` (`Layout(); Fit(); v_sizer->SetSizeHints(this);`).
**Pitfalls**
- **Rule:** Top-level dialogs get their minimum from `SetSizerAndFit` or `sizer->SetSizeHints(this)`,
never from `Fit()` alone.
**Why:** `Fit()` sets no minimum, is not display-clamped and is not replayed at GTK3 show; a dialog
whose minimum was never propagated from its children renders collapsed or mis-sized on GTK3 and can be
shrunk below its content anywhere.
```cpp
// Wrong
SetSizer(main_sizer); Layout(); main_sizer->Fit(this);
// Right (layout fully built)
SetSizerAndFit(main_sizer); Layout();
// Right (sizer set early, content built later, e.g. MsgDialog::finalize; its trailing Fit() is redundant)
GetSizer()->SetSizeHints(this); Layout(); Fit();
```
Cite: 5ede9711f5 (`MsgDialog::finalize`, `UnsavedChangesDialog::build`, `PrinterPartsDialog::Show`,
`CloneDialog` ctor and other dialog ctors; added the `AGENTS.md` rule).
- **Rule:** Lock in the minimum in the constructor, before the dialog can receive iconize, refresh or DPI
events.
**Why:** Per f760f4e462's message, on wxGTK iconizing the main window while a hint-less dialog is open
re-ran `Fit()` with transient zero-sized children and collapsed the dialog (OK button clipped); the
WM then honoured the small geometry, so the user could not resize it back. The trigger chain is not
visible in wx source; what is verified is that `Fit()` never updates the minimum and that a minimized
top-level window reports a (0,0) client size (`window.h:1375-1376`; GTK `toplevel.cpp:1463`).
```cpp
Layout(); Fit(); // Wrong: no enforced minimum
Layout(); Fit(); v_sizer->SetSizeHints(this); // Right (the Fit() is redundant)
```
Cite: f760f4e462 (`FlowRateCalibrationDialog` ctor, `calib_dlg.cpp`).
- **Rule:** Child panels use `SetSizer`, not `SetSizerAndFit`.
**Why:** `SetSizeHints` pins the panel's min client size at its current content **[source]**. A fully
specified min size replaces the best size in `GetEffectiveMinSize()`, so the parent sizer stops tracking
the panel's content: it no longer shrinks when content is removed or translations get shorter, nor grows
when content is added.
```cpp
panel->SetSizerAndFit(s); // Wrong
panel->SetSizer(s); // Right
```
Cite: `wincmn.cpp:974` `WXSetInitialFittingClientSize`.
- **Rule:** After content added to a shown dialog, call `GetSizer()->SetSizeHints(this)`, not `Fit()`.
**Why:** `Fit()` grows the window but keeps the old minimum, so the WM can shrink it below the new
content; after removing content `Fit()` cannot shrink below the stale minimum either.
```cpp
add_extra_row(); Fit(); // Wrong: stale minimum
add_extra_row(); GetSizer()->SetSizeHints(this); // Right
```
Cite: `PrinterPartsDialog::Show` (re-hints after showing/hiding its rows).
- **Rule:** Keep content within a dialog's `SetMaxSize` by capping a scrolled region.
**Why:** min > max hints are silently rejected (`wincmn.cpp:1041`) and the dialog has no minimum.
Cite: `MsgDialog` (`MSG_DLG_MAX_SIZE` caps height only, "ban setting the maximum width value") with
`add_msg_content` capping the scrolled text.
## Re-layout after content or visibility changes
**Contract.**
- `wxSizer::Layout()` recomputes min sizes and repositions items inside the sizer's **current**
rectangle (`sizer.h:705-710`, `sizer.cpp:1272`); `wxWindow::Layout()` does the same within the window's
size. Neither propagates upward. If a change alters a container's min size, `Layout()` the nearest
ancestor whose allocation must change; if the top-level window itself must grow or shrink, call
`GetSizer()->SetSizeHints(tlw)`. Manual `Layout()` is needed only after a content change that does not
come with a size change; a resize lays out automatically.
- "To make a sizer item disappear, use Hide() followed by Layout()" (`sizer.h:569-603`). `wxWindow::Show`
only flips the flag; the ports do not re-lay out the parent (`wincmn.cpp:1128`). A window item is shown
exactly when the window `IsShown()` (`sizer.cpp:863`) unless it carries `wxRESERVE_SPACE_EVEN_IF_HIDDEN`
(next item), so `win->Hide()` and `sizer->Hide(win)` are
equivalent for layout. `sizer->Show(win, …)` with `recursive = false` returns false and does nothing when
`win` sits in a nested sizer. Hiding is honoured only by `wxBoxSizer` and `wxFlexGridSizer`
(`docs/doxygen/overviews/sizer.h:134`).
- `wxRESERVE_SPACE_EVEN_IF_HIDDEN` (`wxSizerFlags::ReserveSpaceEvenIfHidden()`) makes `wxSizerItem::IsShown()`
return true whatever the window's state (`sizer.cpp:863-865`; doc `interface/wx/sizer.h:94-99, 1343-1345`).
The hidden item keeps its min size and position, since `wxBoxSizer::CalcMin` and `RepositionChildren` skip
only `!IsShown()` items (`sizer.cpp:2745, 2428`), and its `wxFlexGridSizer` row or column does not collapse
(`sizer.cpp:1957`): showing or hiding it moves no neighbour and resizes no parent. `sizer->IsShown(win)`
then reports true for a hidden window (`sizer.cpp:1562`); ask `win->IsShown()`. On a sizer item the flag
reserves only the nested sizer's min size, which still skips that sizer's hidden children
(`wxSizerItem::CalcMin`, `sizer.cpp:680-682`), so a nested sizer whose children are all hidden collapses to
the item's border (or the nested sizer's `SetMinSize`); put the flag on the window items.
- `wxStaticText::SetLabel` resizes the control itself (unless `wxST_NO_AUTORESIZE`) but never re-lays out
the parent (`src/common/stattextcmn.cpp` `AutoResizeIfNecessary`).
- `SendSizeEvent()`: "if the frame is using either sizers or constraints … it is enough to call
wxWindow::Layout() directly and this function should not be used in this case" (`window.h:1668-1686`).
`PostSizeEvent()` queues it instead (`window.h:1653-1658`), which defers a relayout past the current
handler and the GTK allocation.
**Usage.**
```cpp
row->Show(enabled); // or sizer->Show(row_sizer, enabled)
GetParent()->Layout(); // or the ancestor whose min size changed
// top-level window must change size too:
GetSizer()->SetSizeHints(this);
```
**OrcaSlicer.** Page switching by sizer visibility: the `wxEVT_TAB_SEL_CHANGED` handler in
`PreferencesDialog` runs `Freeze(); f_sizers[i]->Show(i == selection); Layout(); Thaw();`.
`PreferencesDialog::UpdateSidebarLayout` re-lays out the sidebar inside `Freeze()/Thaw()` and then calls
`plater->PostSizeEvent()` so the plater re-lays out after GTK has allocated. Show/hide driven by hover
must not run inside enter/leave handlers (it re-fires them); see `references/mouse-keyboard-focus.md`.
Reserved space: `KBShortcutsDialog::create_page` adds each editable row's reset button with
`wxALIGN_CENTRE_VERTICAL | wxRESERVE_SPACE_EVEN_IF_HIDDEN`, so `KBShortcutsDialog::apply_bindings` toggles
`reset->Show(is_customized)` without the buttons column changing width.
## Adding items: proportion, flags, wxSizerFlags
**Contract.**
- `Add(win, int proportion = 0, int flag = 0, int border = 0, userData = nullptr)`
(`interface/wx/sizer.h:186`). Proportion is the **second** argument; `Add(w, wxEXPAND)` passes
`0x2000` as a proportion.
- Proportion acts only along the sizer's direction; `wxEXPAND` and alignment act only across it. Default
alignment is left/top.
- **Proportion inflates the min size [source].** `wxBoxSizer::CalcMin` (`sizer.cpp:2728`) sizes the main
direction as `max_i(min_i / prop_i) × Σprop + Σ(fixed items)`. Two `proportion = 1` items with min
widths 100 and 300 make the sizer at least 600 wide, which also widens a `SetSizerAndFit` dialog. The
overview's "half the extra space each" (`overviews/sizer.h:114`) is outdated: 3.x distributes the
total space by proportion with min-size floors (`wxBoxSizer::RepositionChildren`, `sizer.cpp:2402`).
- Box-sizer flag rules, checked only by compiled-out asserts in `wxBoxSizer::DoInsert` (`sizer.cpp:2295`):
a vertical box ignores `wxALIGN_BOTTOM` and `wxALIGN_CENTRE_VERTICAL` (unless combined with
`wxALIGN_CENTRE_HORIZONTAL`, i.e. `wxALIGN_CENTRE`); a horizontal box ignores `wxALIGN_RIGHT` and
`wxALIGN_CENTRE_HORIZONTAL` (unless with `wxALIGN_CENTRE_VERTICAL`); `wxEXPAND` without `wxSHAPED`
overrides every alignment. In `wxGridSizer`, `wxEXPAND | wxALIGN_CENTRE_VERTICAL` means "expand
horizontally, centre vertically". `DisableConsistencyChecks()` (`sizer.h:1609`) is irrelevant in Orca.
- Parent rule: windows managed by a sizer must be children of the sizer's containing window, or of a
`wxStaticBox` inside it (`sizer.cpp` `CheckExpectedParentIs`); otherwise they are positioned in the
wrong coordinate space, silently.
- `wxSizerFlags` (`include/wx/sizer.h:40-240`): `Align()`/`Centre()` **replace** all alignment bits,
while `Left/Right/Top/Bottom/CentreHorizontal/CentreVertical` set one axis (`:60-75`).
`Border(dir, px)` takes raw pixels and the doc prefers the default border "to avoid too small borders
… with high DPI" (`interface/wx/sizer.h:1502-1519`). The default border is 6 on GTK and 5 on macOS (not
scaled); on MSW it is `5 × GetDPIScaleFactor()` of **`wxApp::GetMainTopWindow()`**, not of the window
being laid out (`include/wx/sizer.h:125-141`, `sizer.cpp:172` **[source]**; doc `sizer.h:1652-1661`).
`FixedMinSize()` (`wxFIXED_MINSIZE`) copies the window's current size into its min size when added
(`sizer.cpp:395-409`). `Shaped()` keeps the aspect ratio. `ReserveSpaceEvenIfHidden()` keeps a hidden
item's space (`wxRESERVE_SPACE_EVEN_IF_HIDDEN`, `sizer.h:1641-1650`; see
[Re-layout](#re-layout-after-content-or-visibility-changes)).
- `AddSpacer(n)`: in `wxSizer` it adds `n × n` (a whole cell in grid sizers); in `wxBoxSizer` only along
the main direction. `AddStretchSpacer(p)` is `Add(0, 0, p)` (`sizer.h:300-331`).
**OrcaSlicer.** Orca uses int flags with explicit DIP borders,
`Add(w, 0, wxEXPAND | wxALL, FromDIP(10))`, rather than `wxSizerFlags::Border()` defaults, so spacing is
identical on every port instead of 5/6 px (and main-window DPI on MSW).
**Pitfalls**
- **Rule:** Pass the proportion before the flags.
```cpp
sizer->Add(ctrl, wxEXPAND); // Wrong: proportion 0x2000, no flags
sizer->Add(ctrl, 0, wxEXPAND); // Right
sizer->Add(ctrl, wxSizerFlags(1).Expand().Border(wxALL, FromDIP(5))); // Right
```
- **Rule:** Do not combine `wxEXPAND` with an alignment in a box sizer, and align only across the sizer.
**Why:** EXPAND wins; a main-direction alignment is ignored. The assert that names the conflict is
compiled out, so the flag silently does nothing.
```cpp
vbox->Add(x, 0, wxEXPAND | wxALIGN_CENTER_VERTICAL); // Wrong: alignment ignored
vbox->Add(x, 0, wxALIGN_CENTER_VERTICAL); // Wrong: main-direction alignment
vbox->Add(x, 0, wxALIGN_CENTER_HORIZONTAL); // Right
```
- **Rule:** A fixed-size neighbour of a stretching item gets proportion 0.
**Why:** proportions inflate `CalcMin`, widening the fitted dialog.
## Ownership and removal
**Contract.**
- "Sizers, like child windows, are owned by the library and will be deleted by it which implies that
they must be allocated on the heap. However if you create a sizer and do not add it to another sizer or
window, the library wouldn't be able to delete such an orphan sizer and in this, and only this, case it
should be deleted explicitly" (`interface/wx/sizer.h:45-49`). Sizers own child sizers and spacers, not
child windows; a sizer has exactly one owner: a parent sizer or one `SetSizer`.
- `Detach(win|sizer|index)` never destroys and "does not cause any layout or resizing to take place, call
Layout() to update the layout 'on screen'" (`sizer.h:416-451`). `Remove(wxSizer*)`/`Remove(index)`
destroy sizers and spacers; `Remove(wxWindow*)` is deprecated and does **not** destroy the window
despite its name (`sizer.h:789-835`). `Clear(delete_windows = false)` always deletes child sizers and
destroys child windows (via `Destroy()`) only with `true` (`sizer.h:378-389`, `sizer.cpp:1169`).
`Replace(oldwin, newwin)` neither hides nor destroys `oldwin`, which stays on screen at its last
position; `Replace(oldsizer, newsizer)` deletes the old sizer (`sizer.h:836-882`).
- A destroyed window detaches itself from its containing sizer (`wincmn.cpp:510`). Deleting or replacing
a sizer, including `SetSizer(new)` with `deleteOld`, only clears its windows' containing-sizer pointers
(`sizer.cpp:519` `wxSizerItem::Free`): the old controls stay alive and visible as unmanaged ghosts.
- A window belongs to one sizer: "Adding a window already in a sizer, detach it first!"
(`wincmn.cpp:2421`). In Orca the check is silent and the item is still appended, so the second sizer
holds a dangling pointer once the window dies.
- `wxStaticBoxSizer` owns its box; its destructor destroys the box with `WXDestroyWithoutChildren`, which
reparents the box's children to the box's parent instead of destroying them (`sizer.cpp:2822`).
**Pitfalls**
- **Rule:** Destroy old windows when rebuilding a panel.
```cpp
panel->SetSizer(build_rows()); // Wrong: old rows stay as ghosts
panel->DestroyChildren(); panel->SetSizer(build_rows()); panel->Layout(); // Right
// or: old_sizer->Clear(true) before refilling it
```
- **Rule:** `Detach` a window before adding it to another sizer.
**Why:** the silent `wxCHECK` leaves both sizers pointing at it.
- **Rule:** After `Replace(old, neu)`, `old->Destroy()` (or `Hide()`) and `Layout()`.
**Why:** `Replace` leaves `old` drawn at its last position.
## Specific sizers
**`wxStaticBoxSizer`** — "strongly encouraged to create the windows which are added … as children of
wxStaticBox itself … creating them using the static box parent as parent still works too (but note that
items using different parents can't be used inside the same sizer" (`interface/wx/sizer.h:2036-2045`).
Use `sz->GetStaticBox()` as the parent. **[source]** Box-children are positioned box-relative: (0,0) on
GTK, a fixed 10 px inset on macOS, the static borders on MSW (`wxStaticBoxSizer::RepositionChildren`,
`sizer.cpp:2888`); mixing the two parents mispositions one group. Orca: `LabeledStaticBox`
(`Widgets/LabeledStaticBox.hpp`, a `wxStaticBox`) is the box to use (`new wxStaticBoxSizer(stb, wxVERTICAL)`
in `OptionsGroup` and `calib_dlg.cpp`); those layouts parent all items to the dialog, which is valid as long
as no item in that sizer is parented to the box.
**`wxGridSizer`** — every cell gets the size of the largest item; `cols` alone lets rows grow; with both
`rows` and `cols` given, at most `rows × cols` items are allowed (`sizer.h:1897-1944`). **[source]** Adding
more is an assert in a debug wx; in Orca `wxGridSizer::DoInsert` silently forgets the row count and lets rows
grow (`sizer.cpp:1642-1670`). Hidden items still occupy their cells (`wxGridSizer::RepositionChildren`,
`sizer.cpp:1711`, has no `IsShown` check).
**`wxFlexGridSizer`** — per-row heights and per-column widths; growables via `AddGrowableCol(idx, prop)`;
if all proportions are 0, all growables share equally; re-adding an index requires `RemoveGrowableCol`
first (`sizer.h:1770-1792`). Indices are checked only against fixed ctor counts (`sizer.cpp:2235-2262`).
`SetFlexibleDirection`/`SetNonFlexibleGrowMode` "do not trigger relayout" (`sizer.h:1859-1878`). **[source]**
Hidden items keep their cell; a row or column (gap included) collapses only when all its items are hidden
(`wxFlexGridSizer::FindWidthsAndHeights`, `SumArraySizes`). A growable
column grows the cell, not the control: the item also needs `wxEXPAND`.
```cpp
if (!flex->IsColGrowable(1)) flex->AddGrowableCol(1, 1); // rebuild-safe
flex->Add(value_ctrl, 0, wxEXPAND); // fill the grown cell
```
**`wxGridBagSizer`** — `Add(win, wxGBPosition, wxGBSpan, flag, border)` returns `nullptr` when the cell is
occupied (`interface/wx/gbsizer.h:82-95`) and the window stays unmanaged: check the result.
`SetEmptyCellSize` sets the size of empty rows and columns (`gbsizer.h:195`).
**`wxWrapSizer`** — lays items out along the primary direction and wraps to new lines;
`wxEXTEND_LAST_ON_EACH_LINE | wxREMOVE_LEADING_SPACES` is the default (`interface/wx/wrapsizer.h`).
**[source]** Before it has been given a width its min size is the largest single item
(`wxWrapSizer::CalcMin` → `CalcMaxSingleItemSize`, `src/common/wrapsizer.cpp:182, 243`), so a dialog fitted
before the first layout is sized for one item per line. Put it where it receives a definite width
(`wxEXPAND` in a vertical box under a container with a known width) and re-fit after the first `Layout()`.
**`wxStdDialogButtonSizer`** — `AddButton` accepts only the stock ids (`wxID_OK/YES/SAVE/APPLY/CLOSE/NO/
CANCEL/HELP/CONTEXT_HELP`); other ids go through `SetAffirmativeButton`/`SetNegativeButton`/`SetCancelButton`;
`Realize()` must be called to order and space the buttons; order follows the platform, and on macOS a
`wxID_SAVE` button is relabelled "Save" and `wxID_NO` "Don't Save" (`sizer.h:1030-1151`). Orca uses
`DialogButtons` instead
([Orca idioms](#orcaslicer-layout-idioms-and-spacing-conventions)).
**Books and splitters** (other API: `references/controls-dataview.md`). **[source]** A book control's best
size is the **max over all pages** unless the protected `SetFitToCurrentPage(true)` was called
(`wxBookCtrlBase::DoGetBestSize`, `src/common/bookctrl.cpp:125-148`), so a large hidden page enlarges a fitted
dialog. `wxSplitterWindow::SetMinimumPaneSize` takes pixels: pass `FromDIP(n)`.
## wxEVT_SIZE handlers
**Contract.** "Sizers rely on size events … in a sizer-based layout, do not forget to call Skip on all
size events you catch" (`interface/wx/event.h:5058-5060`). Automatic layout is the static-table handler
`wxWindowBase::InternalOnSize` (`wincmn.cpp:124, 2489`) and `wxTopLevelWindowBase::OnSize`
(`toplvcmn.cpp:40`). `Bind()` handlers run before static tables, so a bound handler that does not
`Skip()` suppresses auto-layout (handler order: `references/events.md`). **[source]** `wxScrolled<>` always
runs its own size handling after user handlers, skipped or not (`src/generic/scrlwing.cpp:203-214`).
**Pitfalls**
- **Rule:** Skip, don't `Layout()` (it runs anyway) and never `SetSize` the same window inside its size
handler; defer other relayouts with `CallAfter` or `PostSizeEvent`.
```cpp
Bind(wxEVT_SIZE, [this](wxSizeEvent&) { recompute(); }); // Wrong: no auto-layout
Bind(wxEVT_SIZE, [this](wxSizeEvent& e) { recompute(); e.Skip(); }); // Right
```
Cite: `Label::OnSize`, `CenteredMultiLinePanel::OnSize` (both skip).
New code binds with `Bind()` and lambdas; no new static event tables (`references/events.md`).
## Freeze / Thaw
**Contract** (`window.h:2203-2232`). `Freeze()` suppresses painting of the window and, recursively, its
non-top-level children; calls nest and must balance; it is "mostly just a hint". **[source]** Children
added while frozen are frozen too, removed ones are thawed (`wxWindowBase::AddChild`/`RemoveChild`).
Freezing does **not** stop layout or size events. RAII: `wxWindowUpdateLocker` (`include/wx/wupdlock.h:19`).
**Platforms [source].** MSW: `Freeze()` on a hidden window only counts; the native redraw lock is skipped
(`wxWindowMSW::DoFreeze`, `src/msw/window.cpp:1659`) and applied by a later `Show()` while still frozen
(`wxWindowMSW::Show`). macOS: `Refresh()` returns early while `IsFrozen()` or not shown
(`src/osx/window_osx.cpp:1340-1346`).
**Pitfalls**
- **Rule:** Balance on every path; prefer `wxWindowUpdateLocker`.
**Why:** an unmatched `Freeze()` leaves the window and its children unpainted. An unmatched `Thaw()`
is worse: `m_freezeCount` is `unsigned` (`include/wx/window.h:2063`) and the "Thaw() without matching
Freeze()" assert is compiled out, so the count wraps to `UINT_MAX`, `IsFrozen()` stays true from then on
(a later balanced pair only drops it to 0 between its `Freeze()` and `Thaw()`), and on macOS the window
stops repainting.
```cpp
Freeze(); if (!rebuild()) return; Thaw(); // Wrong: early return leaves it frozen
wxWindowUpdateLocker lock(this); if (!rebuild()) return; // Right
```
**OrcaSlicer.** `wxWindowUpdateLocker` brackets bulk rebuilds in `Tab.cpp`, `ParamsPanel.cpp`,
`PresetComboBoxes.cpp`, `GUI_ObjectSettings.cpp`; `DPIAware::rescale` wraps `on_dpi_changed` in
`Freeze()`/`Thaw()`.
## Scrolled windows
**Contract** (`interface/wx/scrolwin.h`).
- `wxScrolledWindow` (`wxScrolled<wxPanel>`) hosts child controls; `wxScrolledCanvas`
(`wxScrolled<wxWindow>`) is for drawn content; `wxScrolled<wxControl>` is not advised (`:24-40`).
- **Scrolling is off until a rate is set**: "scrolling is only enabled in orientations with a non-zero
increment" (`:57-66`); both rates start at 0. Vertical-only: `SetScrollRate(0, FromDIP(n))`.
- With a sizer, the virtual size follows the sizer; "if you add or remove any elements to the sizer, you
need to call wxSizer::FitInside() to adjust the virtual size" (`:66-68`). `wxWindow::FitInside()` =
`SetVirtualSize(GetBestVirtualSize())` (`wincmn.cpp:631`); `wxSizer::FitInside(win)` "will not alter the
on screen size" (`sizer.h:465-473`). **[source]** A size event re-derives the virtual size
(`wxScrollHelperBase::HandleOnSize`, `scrlwing.cpp:924`).
- **Best size ignores content in a scrolling direction [source]**: with a sizer, in each direction with a
non-zero rate the best size is `GetMinSize() + scrollbar thickness` (`wxScrolledT_Helper::FilterBestSize`,
`scrlwing.cpp:1594-1634`: "If the app needs some minimal size for its scrolled window, it should set it
and put the window into sizer as expandable"). Without `SetMinSize` and proportion/`wxEXPAND` it
collapses to about the scrollbar width.
- `EnableScrolling(x, y)` toggles **physical** (blit) scrolling; it does not enable or disable a
direction (`:338-356`). Direction is chosen by the scroll rate and the `wxHSCROLL`/`wxVSCROLL` style.
- `ShowScrollbars(horz, vert)` (`wxSHOW_SB_NEVER/DEFAULT/ALWAYS`) works only after creation; the
`wxALWAYS_SHOW_SB` style is the ctor-time equivalent for both directions (`:358-384`).
- Children report physical positions: a child at (10,10) reports (10,-90) after scrolling 100 px
(`:90-96`). `GetViewStart()` is in scroll units; `GetViewStartPixels()` is new in 3.3.2 (`:405-458`).
- `SetTargetWindow(w)` requires overriding `GetSizeAvailableForScrollTarget()` (`:602-617, 719-731`).
- A focused child is scrolled into view; override `ShouldScrollToChildOnFocus` to opt out (`:705-717`).
Mouse-drag autoscroll is configurable with `EnableAutoScrollInside`/`DisableAutoScrollOutside` (new in
3.3.2, `:628-661`).
- Drawing in a scrolled window (`OnDraw`, `DoPrepareDC`, `DoPrepareReadOnlyDC`): see
`references/painting-custom-widgets.md §DC coordinates and scale under DPI`.
**Usage — "shrink to content, scroll beyond a cap":**
```cpp
auto sw = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
sw->SetScrollRate(0, FromDIP(20));
sw->SetSizer(content);
// after every content change:
int h = std::min(content->GetMinSize().y, cap);
sw->SetMinSize(wxSize(-1, h));
sw->FitInside();
parent->Layout();
```
Orca: `Sidebar::update_filaments_area_height` (`SetMaxSize` from a preferred row count, then
`SetMinSize({-1, min(sizer min, max)})` on `m_panel_filament_content`, with `FitInside()` after rebuilds);
`add_msg_content` in `MsgDialog.cpp` (a `wxScrolledWindow(wxVSCROLL)` with `SetScrollRate(0, FromDIP(20))`, a
`Label(…, LB_AUTO_WRAP)` with min = max width, the window's min = max size set to
`(info_width, min(content, 48 * em))`, then `FitInside()`), which keeps the message within the dialog max.
**Orca `ScrolledWindow`** (`Widgets/ScrolledWindow.hpp`) — a `wxScrolled<wxWindow>` that hides the native
bars (`ShowScrollbars(NEVER, NEVER)`) and draws slim `MyScrollbar`s in a margin strip:
- content goes on `GetPanel()`, the `SetTargetWindow` target (`GetSizeAvailableForScrollTarget` is not
overridden);
- the ctor builds its inner windows from the passed `size`, so pass a real size;
- the virtual size is set manually in pixels: `SetScrollbars(1, 1, w, h)` or its own `SetVirtualSize`,
which only **hides** the non-virtual `wxWindow::SetVirtualSize` — `FitInside()` and calls through a
`wxWindow*` bypass the custom bars;
- its size handler resizes the inner panels and calls `Layout(); AdjustScrollbars();`;
- use it with `wxVSCROLL` only: `SetBackgroundColour` dereferences the vertical-only members.
```cpp
// SearchDialog::SearchDialog (Search.cpp)
auto sw = new ScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxSize(W, H), wxVSCROLL, 6, 6);
auto list = new wxWindow(sw->GetPanel(), wxID_ANY);
list->SetSizer(s); list->Fit();
sw->SetScrollbars(1, 1, 0, list->GetSize().GetHeight());
```
Ordinary dialogs and the sidebar use plain `wxScrolledWindow`.
**Pitfalls**
- **Rule:** A scrolled area that "doesn't scroll" lacks `SetScrollRate` or a `FitInside()` after the
content changed; one that collapses lacks a min size or proportion + `wxEXPAND`.
- **Rule:** Forbid horizontal scrolling with `SetScrollRate(0, y)` and/or a `wxVSCROLL`-only style.
```cpp
sw->EnableScrolling(false, true); // Wrong: only disables blit scrolling
sw->SetScrollRate(0, FromDIP(20)); // Right
```
## wxStaticText wrapping and ellipsizing
**Contract.**
- `Wrap(width)` breaks lines at word boundaries and **modifies the label** (`interface/wx/stattext.h:119-130`,
`include/wx/stattext.h:37-40`). `width < 0` means no wrapping; the width is not exact because of borders.
`Wrap` is not virtual.
- **3.3.2 wrap cache [source].** `wxStaticTextBase::Wrap` returns at once when `width == m_currentWrap`
(`src/common/stattextcmn.cpp:259-263`); `SetLabel` → `UpdateLabelOrig` clears the saved unwrapped
label but **not** `m_currentWrap` (`:354-365`). So `SetLabel(new); Wrap(sameWidth);` leaves the new
text unwrapped. `Wrap(-1); Wrap(w);` resets the cache.
- wx breaks only at whitespace and outputs an unbreakable run whole (`stattextcmn.cpp:184-215`): CJK text
without spaces never wraps with `wxStaticText::Wrap`.
- `wxST_WRAP` (new in 3.3.2): "Wrap label text on multiple lines if necessary, using the available
horizontal space. This style only works when the control is used inside a sizer"
(`interface/wx/stattext.h:46-49`; `docs/changes.txt:280`). It is opt-in; labels without it are
unaffected. It is implemented with `GetMinSizeFromKnownDirection` (`stattextcmn.cpp:285-312`).
**[source]** The initial `CalcMin` still uses the unwrapped best size, so a fitted dialog grows to the
full one-line width and nothing wraps; constrain the width another way (a fixed or min width on the
container, `SetMaxSize`).
- `SetLabel` resizes the control to its best size unless `wxST_NO_AUTORESIZE`, which right- or
centre-aligned labels whose width the sizer sets need (`stattext.h:30-36`); it never re-lays out the
parent; it does nothing when the text is unchanged (`stattext.h:107-117`).
- `wxST_ELLIPSIZE_*` ellipsize only when the control is narrower than its text (`stattext.h:37-45`); the
best size is always the full text, so the sizer allots the full width unless something limits it: a
width cap (`SetMaxSize(wxSize(w, -1))`), or an explicit min width plus `wxST_NO_AUTORESIZE`. **[source]**
The generic ellipsizer does nothing while the client width or height is < 2 px
(`wxStaticTextBase::Ellipsize`).
- Mnemonics, markup and `SetLabelText`: `references/controls-dataview.md`.
**Platforms [source].** GTK: `GtkLabel` always has line wrap on, so a `wxStaticText` given less width
than its text wraps natively, while MSW and macOS clip it; best width adds 1 px to avoid spurious wraps
(`src/gtk/stattext.cpp:126, 239-296`). `SetFont` on a hidden label makes wx measure the text itself
because the GTK style cache is stale (`gtk/stattext.cpp:178-190`). GTK2 only: a centre/right-aligned label
silently gets `ELLIPSIZE_MIDDLE`/`START` (`gtk/stattext.cpp:63-90`). MSW uses native end-ellipsis only for
single-line `wxST_ELLIPSIZE_END` labels; other modes are generic (`src/msw/stattext.cpp:233-256`).
**OrcaSlicer `Label`** (`Widgets/Label.hpp`, `: wxStaticText`):
- keeps the original text in `m_text`; `Label::Wrap(int)` **hides** the non-virtual `wxStaticText::Wrap`,
wraps `m_text` with Orca's `wxTextWrapper2` (breaks between ideographs above U+4E00 and hard-breaks
space-less runs) and sets the result through the base `SetLabel` with `m_skip_size_evt` set; it has no
width cache;
- `LB_AUTO_WRAP` binds `wxEVT_SIZE` (with `Skip()`) and re-wraps at the new width; `Label::SetLabel`
re-wraps at `GetSize().x` under `LB_AUTO_WRAP` and returns early when the text is unchanged;
- the label needs a real width: a ctor size `wxSize(FromDIP(w), -1)`, min = max width, or `wxEXPAND` in a
vertical sizer whose container width is fixed;
- `Label::split_lines(dc, width, text, out, max_count)` exposes the same wrapper for owner-drawn text.
Orca's wrapping is preferred over `wxST_WRAP` for CJK text.
**Pitfalls**
- **Rule:** Re-wrapping after `SetLabel` must defeat the wrap cache.
```cpp
st->SetLabel(msg); st->Wrap(w); // Wrong when w is unchanged: stays unwrapped
st->SetLabel(msg); st->Wrap(-1); st->Wrap(w); // Right
// or: Label with LB_AUTO_WRAP / label->Wrap(w) through a Label*
```
- **Rule:** Call `Wrap` through a `Label*`, never through a `wxStaticText*` that points at a `Label`.
**Why:** the base `Wrap` sets the wrapped text through the virtual `Label::SetLabel`, which overwrites
`m_text` with the wrapped copy (and re-wraps it under `LB_AUTO_WRAP`).
- **Rule:** Never compute or commit a size or wrap from a width that has not been laid out; guard with a
sanity check on the client width.
**Why:** a plain `wxWindow`/`wxPanel` child created with `wxDefaultSize` is wx's 20×20 placeholder on
every port until the first sizer layout **[source]** (`include/wx/window.h:1914-1915` `WidthDefault`/
`HeightDefault`; native controls instead take their best size through `SetInitialSize` in `Create()`),
and a minimized top-level window reports a (0,0) client size. Wrapping against that width commits a garbage min size, which `SetSizeHints` then locks in.
```cpp
void UpdateMinSize() {
int cWidth = GetClientSize().GetWidth();
if (cWidth < 50) return; // not laid out yet: don't commit a size
/* wrap at cWidth, SetMinSize(wxSize(-1, h)), GetParent()->Layout() */
}
```
Cite: 5ede9711f5 (`CenteredMultiLinePanel::UpdateMinSize` and `::OnPaint` in `TroubleshootDialog.hpp`).
- **Rule:** Give a wrapping label a fixed width and `-1` height, then wrap.
**Why:** the ctor size becomes the min size (`SetInitialSize`), so a fixed height clips the text when a
translation is longer.
```cpp
new wxStaticText(this, wxID_ANY, txt, wxDefaultPosition, wxSize(FromDIP(490), FromDIP(40))); // Wrong
m_action_line = new wxStaticText(this, wxID_ANY, wxEmptyString, wxDefaultPosition,
wxSize(FromDIP(490), -1)); // Right
m_action_line->Wrap(width);
```
Cite: 5ede9711f5 (`UnsavedChangesDialog`, `UNSAVE_CHANGE_DIALOG_ACTION_LINE_SIZE`, `m_action_line`).
- **Rule:** An ellipsized label needs a width limit.
**Why:** its best size is the full text, so the sizer gives it the full width and nothing ellipsizes.
## Layout on DPI change
**What wx does per platform.**
| Platform | When `DPIAware::on_dpi_changed` runs | What wx already rescaled |
|---|---|---|
| MSW (per-monitor v2 manifest) | `wxEVT_DPI_CHANGED`; also `wxEVT_MOVE_END` when the scale differs | **[source]** before the event, `MSWUpdateOnDPIChange` rescales every window's min/max size, the fonts of non-top-level windows (the TLW keeps its font), every sizer item's border, spacer and nested-sizer min sizes, and invalidates best sizes (`src/msw/window.cpp:5002-5085`; detail in `references/dpi-bitmaps-fonts.md` §wxEVT_DPI_CHANGED) |
| GTK3 | `wxEVT_DPI_CHANGED` when the integer GDK scale changes (GTK ≥ 3.10, `src/gtk/toplevel.cpp:336-350`) | nothing; logical pixels are DIPs, so DIP sizes stay valid |
| macOS | never: wx does send `wxEVT_DPI_CHANGED` on backing-scale changes **[source]** (`src/osx/cocoa/nonownedwnd.mm` `windowDidChangeBackingProperties`), but `DPIAware` binds it only `#ifndef __WXOSX__` | nothing needed; Cocoa scales |
A top-level window's default `wxEVT_DPI_CHANGED` handler resizes it, and a handler that does not `Skip()`
suppresses that (`interface/wx/event.h:3572-3584`). `DPIAware`'s handler does not `Skip()`, so on MSW wx
skips its own resize of the top-level window (`src/msw/nonownedwnd.cpp:284-318` `HandleDPIChange`): on MSW
the dialog must resize itself in `on_dpi_changed`. `DPIAware::rescale` (`src/slic3r/GUI/GUI_Utils.hpp`) runs
`Freeze(); update_em_unit(); on_dpi_changed(rect); Layout(); Thaw();`. The em machinery: see
`references/dpi-bitmaps-fonts.md`.
`em_unit` per platform (`DPIAware::update_em_unit`): MSW `max(10, 10 × dpi/96)`; macOS always 10 (Orca's
`get_dpi_for_window` returns 96 there); GTK `max(10, GetTextExtent("m").x − 1)`, i.e. from the font. So
on GTK an `n * em` size tracks the system font size while `FromDIP(n)` does not; pick one consistently
for related sizes.
**What `on_dpi_changed` re-applies.** Everything wx does not own:
- sizes set with `SetSize` or computed into members (cached metrics, wrap widths);
- `wxDataViewCtrl::SetRowHeight`, column widths and other setter-held values (`references/controls-dataview.md`);
- bitmaps and `ScalableBitmap`s (`references/dpi-bitmaps-fonts.md`);
- each Orca widget's `Rescale()` (and `msw_rescale()` where that is its name);
- min sizes expressed in em, recomputed from the new em (a ratio-free recompute is safe on MSW even
though wx already rescaled the stored value);
- then the top-level resize.
**Usage — the reconciled body:**
```cpp
void MyDialog::on_dpi_changed(const wxRect&) // MyDialog : DPIDialog; members illustrative
{
const int em = em_unit();
msw_buttons_rescale(this, em, {wxID_OK, wxID_CANCEL}); // raw wxButtons only: min height 2.5 em (omit with DialogButtons, below)
m_apply_btn->Rescale(); // Button / TextInput / ComboBox / SpinInput
m_logo.msw_rescale(); // ScalableBitmap
m_logo_ctrl->SetBitmap(m_logo.bmp()); // the control holds its own copy
m_list->SetMinSize(wxSize(40 * em, -1)); // recompute from em, never multiply
GetSizer()->SetSizeHints(this); // resize + new minimum
Refresh();
} // DPIAware::rescale then calls Layout() and Thaw()
```
`SetSizeHints` both resizes and re-derives the minimum, which a bare `Fit()` does not. A `Fit()` is
acceptable only when a correct minimum already exists (`PurgeModeDialog::on_dpi_changed` sets
`SetMinSize(wxSize(70 * em, 32 * em))` and then `Fit(); Refresh();`). **[source]** A `Refresh()`-only
body (`FlowRateCalibrationDialog::on_dpi_changed`) leaves the dialog at its old pixel size on MSW, so
content is clipped until the user resizes it; on GTK3 it is harmless. An empty override (`CloneDialog`)
is accepted only for trivially simple dialogs, with the same MSW caveat. Code placed only in
`on_dpi_changed` never runs on macOS.
Larger dialogs walk their widgets: `PreferencesDialog::on_dpi_changed` `dynamic_cast`s each child and
calls `Rescale()` on every `Button`, `TextInput`, `ComboBox`, `SpinInput` and `WikiLabel`, then re-runs its
tab switch. In such a walk, qualify `::CheckBox`: inside `Slic3r::GUI` an unqualified `CheckBox` names the
`Field` subclass from `Field.hpp`, which is not a `wxWindow`, so the cast never matches (namespaces:
`references/orca-widgets.md`). `DialogButtons` rescales itself: it binds its parent's `wxEVT_DPI_CHANGED`,
rebuilds its row and `Skip()`s. `msw_buttons_rescale` (`wxExtensions.cpp`) calls
`SetMinSize(wxSize(-1, 2.5 * em))` on whatever window has each id; `DialogButtons` gives its Orca
`Button`s stock ids, so on a dialog with a `DialogButtons` row it would override their style height
through `Button::SetMinSize`; leave it out there.
**Pitfalls**
- **Rule:** Every dialog has an enforced minimum before any DPI/refresh path can resize it, and
`on_dpi_changed` resizes with `GetSizer()->SetSizeHints(this)` rather than an unconditional bare `Fit()`.
**Why:** a `Fit()` on a dialog without a minimum is the f760f4e462 collapse; a `Fit()` with a stale
minimum keeps the old floor; and on MSW a body that does not resize leaves content clipped because
`DPIAware` suppresses wx's own resize.
```cpp
// Wrong
Layout(); Fit(); // ctor: no minimum
void on_dpi_changed(const wxRect&) override { Refresh(); Fit(); }
// Right
Layout(); v_sizer->SetSizeHints(this); // ctor
void on_dpi_changed(const wxRect&) override { /* rescale content */ GetSizer()->SetSizeHints(this); Refresh(); }
```
Cite: f760f4e462 (`calib_dlg.cpp`), 5ede9711f5; `src/msw/nonownedwnd.cpp` `HandleDPIChange`.
- **Rule:** Re-apply sizes that wx does not rescale with the same expression as the constructor;
never scale an existing value by a DPI ratio.
**Why:** setter-held values (row heights, column widths, `SetSize` sizes) never rescale on any port,
and stale pixels clip or overflow content (the object-list filament badge no longer fitting its row).
On MSW, wx already rescaled stored min/max sizes and sizer borders, so `SetMinSize(GetMinSize() * ratio)`
scales twice. On GTK3 and macOS, DIP values need no rescale at all.
```cpp
// ObjectList::create_objects_ctrl and again in ObjectList::msw_rescale
SetRowHeight(2 * em + FromDIP(2)); // same expression, new em
SetMinSize(GetMinSize() * new_scale / old); // Wrong: double scaling on MSW
```
Cite: d5638273c6 (`ObjectList::create_objects_ctrl`, `ObjectList::msw_rescale`).
## Platform summary
- **MSW.** DPI change rescales min/max sizes, non-top-level fonts, sizer borders, spacer and nested-sizer min sizes before
`wxEVT_DPI_CHANGED`; Orca must resize the top-level window itself. The default `wxSizerFlags` border
follows the main window's DPI. `Freeze()` on a hidden window skips the native redraw lock until the window
is shown. A minimized top-level window reports client size (0,0).
- **macOS.** `FromDIP` is the identity and `em_unit` is 10. Default sizer border 5. `wxStaticBoxSizer`
box-children sit at a 10 px inset. `Refresh()` is a no-op while frozen or not shown.
- **GTK3.** Sizer-fitting calls on a hidden top-level window are replayed at `Show()`; `wxWindow::Fit()` is
not. **[source]** `SetFont` before the top-level window is shown queues best-size revalidation at show
(`GTKSizeRevalidate`, `src/gtk/window.cpp:6651-6725`), so best sizes measured in a ctor can be wrong
until then; re-measure on show or DPI change, or let sizers do it. Top-level and popup `SetSize` are
clamped to min/max. WM geometry hints only for `wxRESIZE_BORDER`. `GtkLabel` wraps natively when given
less width. Default sizer border 6. A minimized top-level window reports client size (0,0)
(`toplevel.cpp:1463`). On X11 the first `Show()` of a top-level window may be deferred until
`_NET_FRAME_EXTENTS` arrives (`toplevel.cpp:1140-1240`), so geometry is not final right after `Show()`
(window showing: `references/windows-dialogs.md`). em follows the font.
- **GTK2** (opt-out build). `FromDIP` is the identity in effect (fixed 96 PPI); centre/right-aligned labels
are implicitly ellipsized.
- **Wayland.** `wxWindow::Update()` "doesn't do anything in wxGTK port when using Wayland"
(`window.h:2405-2407`): `Layout(); Update();` does not force a synchronous repaint. No deferred first show;
with client-side decorations the decoration size counts as 0 for hints (`toplevel.cpp:1519-1523`).
## OrcaSlicer layout idioms and spacing conventions
- **Units.** Every pixel value is `FromDIP(n)` or `n * em_unit()` (`em_unit()` on a `DPIDialog`/`DPIFrame`;
the free `em_unit(wxWindow*)` in `wxExtensions.cpp` returns the enclosing `DPIDialog`/`DPIFrame`'s em,
else `wxGetApp().em_unit()`). Never raw ints, never `wxSizerFlags::Border()` defaults.
- **Spacing conventions.** Outer dialog padding `FromDIP(10)`–`FromDIP(20)` with `wxEXPAND | wxALL`;
inter-widget gaps `FromDIP(4)`–`FromDIP(12)`; `FromDIP(1)` separators; `sizer->AddSpacer(FromDIP(n))`
for vertical rhythm; `AddStretchSpacer()` to push a group to the far side.
- **Separators.** A 1 px `wxPanel` with a light grey background (`FilamentPickerDialog::CreateSeparatorLine`)
or the `StaticLine` widget; avoid `wxStaticLine`.
- **Button rows.** `DialogButtons` (`Widgets/DialogButtons.hpp`, `Slic3r::GUI`) is a `wxPanel` with its own
horizontal `wxBoxSizer`, not a `wxStdDialogButtonSizer`; add it with `main_sizer->Add(dlg_btns, 0, wxEXPAND)`.
`UpdateButtons()` rebuilds the row with `m_sizer->Clear()` (the buttons are the panel's children and
survive), adds a leading gap when no button is left-aligned, a stretch spacer before the right-aligned
group, each button with
`wxRIGHT`/`wxLEFT | wxTOP | wxBOTTOM | wxALIGN_CENTER_VERTICAL` and a `FromDIP(ButtonProps::ChoiceButtonGap())`
border, then `Layout(); Fit();`. The order is the same on every platform, deliberately unlike wx's
per-platform ordering. Older dialogs hand-roll the row with `Button` + `AddStretchSpacer()`;
`DialogButtons` is the form for new code. Construction and labels: `references/orca-widgets.md §DialogButtons`;
its place in a dialog: `references/windows-dialogs.md §8`.
- **Long static text.** Fixed-width `wxStaticText` + `Wrap(FromDIP(w))` once, or `Label` with
`LB_AUTO_WRAP` when the text changes or must wrap CJK ([Wrapping](#wxstatictext-wrapping-and-ellipsizing)).
- **Bulk rebuilds.** `wxWindowUpdateLocker` (or a balanced `Freeze()`/`Thaw()`), rebuild, `Layout()` the
owning ancestor, `SetSizeHints` if the top-level size must change; `PostSizeEvent()` on the plater when
the sidebar's height changed.
- **Size clamps.** Dialogs may clamp with `SetMinSize/SetMaxSize(FromDIP(…))`; keep content within the
max ([Fitting](#fitting-functions-setsizer-setsizerandfit-setsizehints-fit-layout)).
- **Exemplars.** `CloneDialog.cpp` (minimal `SetSizerAndFit` dialog), `MsgDialog.cpp` (late content,
capped scrolled text), `PrintOptionsDialog.cpp` `PrinterPartsDialog::Show` (show/hide rows + re-hint),
`Preferences.cpp` (sizer-visibility pages, height-for-width `WikiLabel`), `Plater.cpp`
`Sidebar::update_filaments_area_height` (capped scrolled list).
@@ -0,0 +1,883 @@
# Strings, translation, files and app services
How text moves between UTF-8 `std::string` and `wxString` under Orca's build flags, how to format
and translate it, and the wx services that carry text or files in and out of the app: file and
directory dialogs, paths, browser/app launching, clipboard, drag and drop, logging, native message
boxes, settings storage, secrets and single-instance handling. Read it before touching any
user-visible string, any file path or any of those services, and when debugging mojibake, empty
strings, untranslated text or a lost `&`.
Contents: [Rules](#rules) · [Build facts](#build-facts-that-decide-string-behaviour) ·
[wxString conversions](#wxstring-encodings-and-conversions) · [Formatting](#formatting) ·
[Translation](#translation) · [Language switching](#language-switching-and-the-translation-lifecycle) ·
[Labels & mnemonics](#labels-mnemonics-and-markup) · [Paths](#paths-and-standard-locations) ·
[File & dir dialogs](#file-and-directory-dialogs) · [Launching](#launching-the-browser-files-and-programs) ·
[Clipboard](#clipboard) · [Drag & drop](#drag-and-drop) · [Logging](#logging-wxlog-vs-boost-log) ·
[Message boxes](#native-message-boxes) · [AppConfig](#settings-appconfig-not-wxconfig) ·
[Secrets](#secrets-wxsecretstore) · [Single instance](#single-instance)
## Rules
1. `std::string` is UTF-8 everywhere outside wx; `wxString` exists only at the wx boundary. Convert
with `from_u8`/`into_u8`, paths with `from_path`/`into_path`. → §wxString conversions
2. Never let a UTF-8 `std::string`/`const char*` reach a `wxString` parameter implicitly (default dir
of a file dialog, `SetLabel`, `wxString::Format("%s", …)`): it is decoded with the C-locale
encoding, the ANSI code page on Windows. → §wxString conversions
3. Never take narrow text out of a `wxString` with `ToStdString()`, `mb_str()` or `c_str()`; use
`into_u8(w)` or `w.utf8_string()`. → §wxString conversions
4. Never keep the pointer from `ToUTF8().data()`, `utf8_str()`, `mb_str()` or `c_str()` beyond the
full-expression; own a `std::string`. → §Pointer lifetime
5. Non-ASCII text in source only through `_L(...)`, `wxString::FromUTF8(u8"…")` or `L"…"`, never
`wxString("°C")`. → §Source literals
6. Build user-visible strings with `format_wxstr`/`GUI::format` and `%1%` placeholders; pass
`std::string` arguments as they are; write a literal percent as `%%`. → §Formatting
7. With `wxString::Format`/`Printf`, match every specifier to its argument type yourself (`%zu`,
casts); Orca compiles the check out. → §Formatting
8. Serialise numbers locale-independently; only display goes through the UI locale. → §Numbers
9. Mark every user-visible literal with an extracted keyword (`L`, `_L`, `_u8L`, `L_CONTEXT`,
`_L_CONTEXT`, `_u8L_CONTEXT`, `_L_PLURAL`); never `_()`, `_utf8()`, `_CHB()` for a new string.
→ §Extraction
10. A new source file with translatable strings is added to `localization/i18n/list.txt`.
→ §Extraction
11. Translate at construction/display time; never cache translated text in a static, namespace-scope
variable or long-lived singleton without a re-localise hook. → §Language switching
12. One complete sentence per msgid, placeholders instead of concatenated fragments, the number
placeholder in both plural forms, `// TRN` for ambiguous strings, a context for homonyms.
→ §Plurals and contexts
13. A wrapper around `wxGetTranslation` returns `wxString` by value. → §Translation
14. User-supplied text (preset, filament, file, printer names) in a mnemonic-interpreting label
goes through `SetLabelText` or `wxControl::EscapeMnemonics`. → §Labels
15. File dialogs: filters from `file_wildcards(FT_*)`, default dir `from_u8(app_config->get_last_dir())`,
results through `GetPaths`/`GetPath` + `into_path`, never `wxFD_CHANGE_DIR`/`wxDD_CHANGE_DIR`,
a real parent window. → §File and directory dialogs
16. Pass only the dialog's own style flags; `wxDD_NEW_DIR_BUTTON` is 0, use `wxDD_DEFAULT_STYLE`.
→ §File and directory dialogs
17. Folders open through `desktop_open_any_folder`/`desktop_open_datadir_folder`; a file that came
from a project is launched only after `is_safe_to_open_file_name`. → §Launching
18. Clipboard access goes through a checked `wxClipboardLocker`; use `SetText`/`wxTextDataObject(text)`,
not `wxTextDataObject::SetData`. → §Clipboard
19. Start `DoDragDrop` from the mouse handler; never replace a drop target from inside its own
callbacks. → §Drag and drop
20. wxLog never reaches the user in Orca; tell the user with `show_error` or a MsgDialog, log with
`BOOST_LOG_TRIVIAL` and stream UTF-8. → §Logging
21. `wxMessageBox` returns `wxYES/wxNO/wxCANCEL/wxOK/wxHELP`, `ShowModal` returns `wxID_*`; UI code
uses the MsgDialog family. → §Native message boxes
22. AppConfig values are set with a `std::string`, never a bare `const char*` third argument, and only
on the main thread; no wxConfig. → §AppConfig
23. Never touch `wxSecretStore` from a timer, poll or other per-tick UI path. → §Secrets
24. Cross-instance requests go through `instance_check` and `OtherInstanceMessageHandler`; do not add
another checker or a wxIPC server. → §Single instance
## Build facts that decide string behaviour
Generic wx advice is often wrong for Orca because of these settings. The installed `setup.h` is
`<wx install>/lib/wx/include/<port>-unicode-static-3.3/wx/setup.h`.
| Setting | Value in Orca | Where |
|---|---|---|
| `wxString` storage | `std::wstring` (`wxUSE_UNICODE_UTF8 0`, `wxUSE_UTF8_LOCALE_ONLY 0`), deep copy, never copy-on-write | installed setup.h; `include/wx/string.h:121-132` |
| implicit `wxString` → `const char*` / `const void*` | **off**: the app defines `wxNO_UNSAFE_WXSTRING_CONV` | top-level `CMakeLists.txt` (`add_definitions(-DwxNO_UNSAFE_WXSTRING_CONV)`, comment "This implicit conversion breaks the UTF-8 encoding quite often"); `include/wx/string.h:1631-1637` |
| implicit `wxString` → `std::string` | **off** (`wxUSE_STD_STRING_CONV_IN_WXSTRING 0`) | installed setup.h; `include/wx/string.h:1376-1385` |
| implicit `std::string` / `const char*` → `wxString` | **on**, decoded with the current locale (`wxNO_IMPLICIT_WXSTRING_ENCODING` is not defined) | `include/wx/string.h:1215-1217, 1323-1325` |
| wx's own `_()` macro | not defined (`-DWXINTL_NO_GETTEXT_MACRO`); Orca defines its own | `CMakeLists.txt`; `include/wx/translation.h:47-49`; `src/slic3r/GUI/I18N.hpp` |
| debug level | wx built with `wxBUILD_DEBUG_LEVEL=0`; `libslic3r_gui` gets `wxDEBUG_LEVEL=0` under `SLIC3R_STATIC` (default ON): `wxASSERT`, `Format` type checks, `wxLogDebug`, `wxLogTrace` compile out; `wxCHECK*` still return, silently | `deps/wxWidgets/wxWidgets.cmake`, `src/slic3r/CMakeLists.txt`; `include/wx/log.h:62-77` |
| printf positional parameters | on (`wxUSE_PRINTF_POS_PARAMS 1`) | installed setup.h |
| MSVC source charset | `/utf-8`, so narrow literals hold UTF-8 bytes | `CMakeLists.txt` (`add_compile_options(... /utf-8)`) |
| clipboard, DnD, secret store, config, single-instance checker, IPC | compiled in (all `wxUSE_* 1`); Orca uses its own config and messaging, and the wx checker only on Windows | installed setup.h |
Because asserts are compiled out, every misuse described below fails silently in Orca: wrong or
empty text, a dropped call, a leaked object. wx would assert in a debug build; Orca never shows it.
## wxString encodings and conversions
**Contract.** A narrow `char*`, `std::string` or `std::string_view` given to wxString "supposes that
the string contains data in the current locale encoding, use FromUTF8() if the string contains
UTF-8-encoded data instead"; the implicit constructors are "dangerous … the resulting string will be
empty if the conversion from the current locale encoding fails" (`interface/wx/string.h:58-89`).
In the other direction `c_str()`/`mb_str()` are "potentially destructive … an empty string is
returned if the conversion fails", and `ToStdString()` loses data unless given `wxConvUTF8`; use
`utf8_string()` (`interface/wx/string.h:108-120, 843-863`). `FromUTF8`: "If s is not a valid UTF-8
string, an empty string is returned" (`interface/wx/string.h:2037`). The Unicode overview calls
`FromUTF8()` followed by `c_str()` "a recipe for disaster … may work perfectly well during testing on
Unix systems using UTF-8 locale but completely fail under Windows" (`docs/doxygen/overviews/unicode.h:314-320`).
**What "current locale encoding" is** (`wxConvLibc`, `src/common/strconv.cpp:3403-3409`) [source]:
| Platform | Narrow ↔ wide conversion | Consequence for UTF-8 data |
|---|---|---|
| MSW | `wxMBConv_win32` with `CP_ACP`, the system ANSI code page (`src/common/strconv.cpp:2600-2607`), independent of the UI language Orca picks; Orca's manifest does not opt into a UTF-8 active code page | non-ASCII text becomes mojibake (single-byte code pages) or an empty string (DBCS code pages). A developer machine with Windows' system-wide "Use Unicode UTF-8" option hides the bug |
| macOS | `wxMBConvLibc` (`mbstowcs`, follows `LC_CTYPE`) | the C runtime stays in the `"C"` locale until `GUI_App::load_language` calls `wxLocale::Init`; in `"C"` macOS maps each byte to one character (mojibake) [tested]; region locales afterwards decode UTF-8 [tested] |
| GTK | `wxMBConvLibc` | wxGTK calls `gtk_disable_setlocale()` (`src/gtk/app.cpp:526-537`), so the locale is `"C"` until `load_language`; `wxUILocale` then prefers a UTF-8 codeset (`src/unix/uilocale.cpp` `TryCreateLocaleWithUTF8`, `wxSetlocaleTryUTF8`) |
The implicit conversions therefore work on macOS and Linux after startup and break on Windows, so a
bug of this class survives testing on a Mac.
**What compiles in Orca and what it does:**
| Expression | Compiles? | Encoding |
|---|---|---|
| `wxString w = s;` or `f(const wxString&)` called with `std::string`/`const char*` | yes | locale (CP_ACP on MSW): mojibake or empty for UTF-8 |
| `wxString w(sv)` from `std::string_view` | only explicitly: the ctor is `explicit` (`include/wx/string.h:1327-1328`) although `interface/wx/string.h:71` calls it implicit | locale |
| `std::string s = w;`, `const char* p = w;` | **no** | — |
| `w.ToStdString()`, `w.mb_str()`, `(const char*)w.c_str()` | yes | locale; `""` on failure (`wxCStrData::AsChar`, `include/wx/string.h:4292-4303`) |
| `w.utf8_string()`, `w.ToUTF8()`/`w.utf8_str()`, `into_u8(w)` | yes | UTF-8, never fails |
| `wxString::FromUTF8(s)`, `from_u8(s)` | yes | UTF-8; `""` on invalid input |
| `w.ToStdWstring()`, `w.wc_str()`, `wxString(L"…")` | yes | lossless |
**OrcaSlicer helpers** (`src/slic3r/GUI/GUI.hpp`/`GUI.cpp`):
| Helper | Does | Note |
|---|---|---|
| `from_u8(const std::string&)` | `wxString::FromUTF8(str.c_str())` | stops at an embedded NUL, `""` on invalid UTF-8; for binary-safe input use `wxString::FromUTF8(s)` (the `std::string` overload passes the length, `include/wx/string.h:1782-1783`) |
| `into_u8(const wxString&)` | `std::string(str.utf8_str().data())` | owning copy |
| `from_path(const boost::filesystem::path&)` | `wstring` on Windows, `from_u8(path.string())` elsewhere | |
| `into_path(const wxString&)` | `boost::filesystem::path(str.wx_str())` | wide on every platform |
| `file_url_from_path(path)` | `wxFileSystem::FileNameToURL(wxFileName(from_path(path)))` | |
| `I18N::translate*`, `L_str(std::string)` | decode their narrow input with `wxConvUTF8` | UTF-8 msgids are safe |
ImGui takes UTF-8 `const char*`: `ImGuiWrapper::text(const wxString&)` converts with `into_u8`; raw
`ImGui::` calls take `_u8L(...)`/`into_u8(...)` (see `references/webview-gl-aui-media.md` for the
ImGui layer).
### Source literals
MSVC compiles with `/utf-8`, so `"°C"` is a UTF-8 byte string on every compiler, and
`wxString("°C")` decodes it with the locale like any other narrow string ("never use 8-bit
characters directly in the program source", `docs/doxygen/overviews/unicode.h:145-160`). Non-ASCII
text goes through `_L("…")` (decodes UTF-8), `wxString::FromUTF8(u8"\u2103")` (the
`AMSDryControl.cpp` style for `℃`) or a wide literal `L"…"`. In C++20 `u8"…"` becomes `char8_t`,
which `FromUTF8` does not accept; Orca builds C++17. Mixing plain ASCII literals with a `wxString`
(`_L("Version") + " " + v`) is fine.
### Pointer lifetime
`utf8_str()`/`ToUTF8()` return a `wxScopedCharBuffer` and `mb_str()` a `wxCharBuffer` by value
(`interface/wx/string.h:745-755, 884-886`); each dies at the end of the full-expression. `c_str()`
returns a `wxCStrData` proxy whose narrow pointer points into a conversion buffer owned by the
`wxString` itself (`m_convertedToChar`, `src/common/string.cpp` `wxString::AsChar`) [source]: it
stays valid only until that string is modified, converted again or destroyed, so `_L("…").c_str()`
dangles at the end of the statement. Note that `wxCStrData` still converts implicitly to
`const char*` under `wxNO_UNSAFE_WXSTRING_CONV` (`include/wx/string.h:210-217`), so
`const char* p = w.c_str();` compiles.
- **Rule:** never keep a pointer into a temporary conversion buffer beyond the statement that
created it.
**Why:** the pointer dangles once the buffer is destroyed; the read returns garbage or crashes
later, intermittently, on every platform.
```cpp
// Wrong:
const char* p = name.ToUTF8().data();
use(p);
// Right:
std::string s = into_u8(name); // or name.utf8_string()
use(s.c_str());
```
Cite: `ImGuiWrapper::clipboard_get` keeps the converted text in the member
`m_clipboard_text` so the `const char*` it returns stays valid.
Passing a temporary such as `_L("…") + dots` straight into a `const wxString&` parameter is safe,
even if the callee yields or repaints: the temporary lives until the end of the full-expression and
`wxString` is a deep-copy `std::wstring`. (The Linux splash crash once blamed on such a temporary
was the splash screen's event filter, fixed in 4088a36095; see `references/threads-timers-app.md`.)
### Other traps
- `s[n]` returns a `wxUniCharRef` proxy: it cannot be `switch`ed on, and `auto c = s[0]; c = 'x';`
**modifies the string**. Use `s[n].GetValue()` or an explicit `int`/`wchar_t` type
(`interface/wx/string.h:171-226`).
- Since 3.3 `wxstr = {"Hello", 2}` is ambiguous (the `string_view` constructor); write
`wxString{"Hello", 2}` (`docs/changes.txt:191-194`).
- Never pass a `wxString`, `c_str()` or `mb_str()` to a real C vararg function (`printf`); use
`wxString::Format`/`wxPrintf` or convert explicitly (`interface/wx/string.h:262-300`).
- `wxUSE_STL` no longer exists; wx 3.3 re-enables implicit `wxString` → `std::string` only through
`wxUSE_STD_STRING_CONV_IN_WXSTRING=1` (`docs/changes.txt:163-166`). Orca keeps it 0 and adds
`wxNO_UNSAFE_WXSTRING_CONV`; do not enable either conversion, the compile error is the guard.
### Pitfalls
- **Rule:** wrap every UTF-8 `std::string` in `from_u8()` (or pass it to a helper that takes
`std::string`) before it reaches a wx API.
**Why:** the implicit constructor decodes with CP_ACP on MSW, so a non-ASCII preset name, path or
translated `_u8L` string turns into mojibake or `""`; macOS/Linux look correct.
```cpp
// Wrong:
label->SetLabel(preset.name);
wxFileDialog dlg(this, title, app_config->get_last_dir(), "", file_wildcards(FT_3MF), wxFD_OPEN);
// Right:
label->SetLabel(from_u8(preset.name));
wxFileDialog dlg(this, title, from_u8(app_config->get_last_dir()), "", file_wildcards(FT_3MF), wxFD_OPEN);
```
Cite: `GUI_App::import_model` (model opener with `from_u8` default dir); `CMakeLists.txt`
`wxNO_UNSAFE_WXSTRING_CONV` comment.
- **Rule:** get UTF-8 out of a `wxString` with `into_u8`/`utf8_string()`.
**Why:** `ToStdString()`, `mb_str()` and `c_str()` use `wxConvLibc` and return `""` when a character
is not representable in the ANSI code page (`include/wx/string.h:4292-4303`).
```cpp
// Wrong:
std::string path = dlg.GetPath().ToStdString();
// Right:
boost::filesystem::path path = into_path(dlg.GetPath()); // or into_u8(dlg.GetPath())
```
## Formatting
**`wxString::Format`/`Printf` contract.** Variadic templates that normalise each argument; positional
`%2$d %1$d` is supported because `wxUSE_PRINTF_POS_PARAMS` is 1 (`interface/wx/string.h:1533-1546`).
The specifier/argument type check is a `wxASSERT_MSG` (`include/wx/strvararg.h:336-347`), so at
debug level 0 nothing checks it: `%d` with `size_t`, `%s` with an `int` or a missing argument is
silent undefined behaviour. `const char*`, `std::string` and `std::string_view` arguments are
decoded with `wxConvLibc` (`wxArgNormalizerWchar<const char*>`, `include/wx/strvararg.h:612-624,
772-781`), so UTF-8 data garbles on MSW exactly as in the table above.
**OrcaSlicer: `format_wxstr` and `GUI::format`** (`src/slic3r/GUI/format.hpp`, on top of
`Slic3r::format` in `src/libslic3r/format.hpp`) wrap `boost::format`:
| Helper | Returns | Arguments |
|---|---|---|
| `format_wxstr(fmt, args...)` | `wxString`, built with `wxString::FromUTF8(result)` | `fmt` as `const char*`, `std::string` (UTF-8) or `wxString`; args of any streamable type |
| `GUI::format(fmt, args...)` | UTF-8 `std::string` | same |
| `Slic3r::format(fmt, args...)` (libslic3r) | UTF-8 `std::string` | narrow only |
- Placeholders: boost's `%1%` (preferred: position-independent, checked by `xgettext --boost`),
printf-style `%s`/`%d` and positional `%1$s`/`%1$d` all work.
- `std::string`/`const char*` arguments are inserted as raw bytes, i.e. as UTF-8; pass them directly
instead of converting to `wxString` first.
- `boost::format` throws `boost::io::too_many_args`/`too_few_args` on a placeholder/argument count
mismatch, and a stray `%` throws too (Orca's helpers keep boost's default "all errors throw"):
`bad_format_string` when it cannot start a directive (`"50%"`, `"5%/s"`), `too_few_args` when it
happens to parse as one (`"50% done"` reads `% d`, a space-flag `%d`) [tested]. A literal percent
is `%%`. A string that never reaches a formatter but contains `%` needs
`// xgettext:no-c-format, no-boost-format` above it, otherwise `msgfmt --check-format` fails
(AGENTS.md; e.g. `ConfigManipulation.cpp`).
- `wxString` arguments [source + tested]: the `cook(const wxString&)` overloads that convert to
UTF-8 live in `Slic3r::internal::format` in `slic3r/GUI/format.hpp`, declared after the
`format_recursive` template in `libslic3r/format.hpp`. Under two-phase lookup (clang, GCC) a
dependent call only finds later overloads through ADL, and `wxString`'s namespace is the global
one, so the generic `cook` is chosen and the `wxString` reaches boost through wx's
`operator<<(std::ostream&, const wxString&)` → `wxConvWhateverWorks` (C locale first, UTF-8
fallback; `src/common/string.cpp:158-170`, `src/common/strconv.cpp:3354-3363`). That is correct
while the C locale is UTF-8 (macOS and Linux after `load_language`); MSVC without `/permissive-`
finds the overloads. `into_u8(w)` as the argument is exact everywhere.
**Choosing.** New user-visible strings use `format_wxstr(_L("… %1% …"), args)` or
`GUI::format(_u8L(...), args)`: translators can reorder `%1%`/`%2%`, while plain `%s %d` order is
fixed (a reordered c-format translation fails `msgfmt --check-format`; never reorder positional
arguments in a c-format string). `wxString::Format` with printf specifiers is accepted in existing
code; plural strings often use `%1$d` with `GUI::format` (`NotificationManager.cpp`).
```cpp
wxString msg = format_wxstr(_L("The file %1% was loaded"), filename); // filename: UTF-8 std::string
wxString pl = wxString::Format(_L_PLURAL("%d object", "%d objects", n), n); // n: int/unsigned, not size_t
```
### Numbers
`wxLocale::Init` "changes the application locale … this will affect many of standard C library
functions such as printf()" (`interface/wx/intl.h:584-590`). Orca deliberately leaves `LC_NUMERIC`
localised (the `wxSetlocale(LC_NUMERIC, "C")` in `GUI_App::load_language` is commented out), so in
the GUI thread `wxString::Format("%.2f")`, `wxString::ToDouble` and `std::to_string` use the UI
language's decimal separator; `wxString::ToCDouble`/`FromCDouble` do not.
- Display: `double_to_string(value, precision)` (`Field.cpp`, `wxNumberFormatter` plus the locale
separator from `is_decimal_separator_point()`); parse user input by replacing the other separator
with the locale's and then calling the locale-aware `wxString::ToDouble` (`Field::get_value_by_opt_type`
style in `Field.cpp`), not `ToCDouble`.
- Data (config values, G-code, project files, URLs): `CNumericLocalesSetter` (RAII `LC_NUMERIC="C"`),
`float_to_string_decimal_point`, `string_to_double_decimal_point` (`libslic3r/LocalesUtils.hpp`).
TBB worker threads set `"C"` per thread (`libslic3r/Thread.cpp`); the GUI thread does not.
### Pitfalls
- **Rule:** pass UTF-8 `std::string` values to `format_wxstr`, or `from_u8` them for
`wxString::Format`.
**Why:** a `std::string` `%s` argument to `wxString::Format` is decoded with CP_ACP on MSW.
```cpp
// Wrong:
wxString::Format(_L("Preset %s not found"), preset_name); // std::string
// Right:
format_wxstr(_L("Preset %1% not found"), preset_name);
```
Cite: `include/wx/strvararg.h:772-781`.
- **Rule:** escape literal percent signs in strings that go through `format_wxstr`/`GUI::format`.
**Why:** a stray `%` throws at runtime (`boost::io::too_few_args` here, because `% d` parses as a
directive; `bad_format_string` for `"50%"` at the end), in whatever handler builds the message.
```cpp
// Wrong:
format_wxstr(_L("Progress: 50% done"));
// Right:
format_wxstr(_L("Progress: 50%% done"));
```
- **Rule:** use `%zu` or cast for `size_t` in `wxString::Format`.
**Why:** the type check is compiled out; `%d` with a 64-bit `size_t` is undefined behaviour that
truncates the value or misreads the following arguments, depending on the ABI.
```cpp
// Wrong:
wxString::Format("%d items", vec.size());
// Right:
wxString::Format("%d items", static_cast<int>(vec.size()));
```
## Translation
**wx contract** (`interface/wx/translation.h:577-640`): `wxGetTranslation(string, domain = "",
context = "")` returns the original string when no catalog has it; a non-empty context needs a
matching `msgctxt` in the catalog; the plural overload returns `string` for `n == 1` and `plural`
otherwise when no catalog is found; "This function is thread-safe". Since 3.3 it returns
**`wxString` by value**, not a const reference: "please change the return type of the function to
wxString" (`docs/changes.txt:139-142`). Orca's `I18N::translate` overloads already return by value;
keep any new wrapper that way, a `const wxString&` return now dangles.
**OrcaSlicer macros** (`src/slic3r/GUI/I18N.hpp`):
| Macro | Returns | Extracted by xgettext? |
|---|---|---|
| `_L(s)` | translated `wxString` | yes |
| `_u8L(s)` | translated UTF-8 `std::string` | yes |
| `_L_CONTEXT(s, ctx)` / `_u8L_CONTEXT(s, ctx)` | `msgctxt`-disambiguated `wxString` / `std::string` | yes (`1,2c`) |
| `_L_PLURAL(s, plural, n)` | plural-aware `wxString` (`n` is `unsigned int`) | yes (`1,2`) |
| `L(s)` / `L_CONTEXT(s, ctx)` | the literal unchanged: a **marker** for xgettext, translated later at display time | yes |
| `_(s)`, `_utf8(s)` | same as `_L`/`_u8L` | **no** |
| `_CHB(s)` | translated `wxScopedCharBuffer` (a temporary, see §Pointer lifetime) | **no** |
| `_devL(s)` | `wxString(s)`, untranslated and locale-decoded | **no** |
| `_omitL(s)` | `""` | **no** |
There is no `_CTX` macro and no plural-with-context macro. All overloads decode `const char*` and
`std::string` input with `wxConvUTF8`, and accept `wxString`/`std::wstring` too, so `_L(var)` works
for a runtime string, but only finds a translation if that exact string was marked with `L()`
somewhere. `L_str(std::string)` (`I18N.cpp`) is the function form of `_L` for a UTF-8 string.
libslic3r has its own `src/libslic3r/I18N.hpp`: `L`/`L_CONTEXT` markers (functions returning the
literal) and `_u8L`, which calls a callback that `GUI_App` installs
(`Slic3r::I18N::set_translate_callback(libslic3r_translate_callback)`); `#error` guards stop either
header being included in the other module.
### Extraction
`scripts/run_gettext.sh --full` (and `.bat`) run xgettext with exactly these keywords
`--keyword=L --keyword=_L --keyword=_u8L --keyword=L_CONTEXT:1,2c --keyword=_L_CONTEXT:1,2c
--keyword=_u8L_CONTEXT:1,2c --keyword=_L_PLURAL:1,2`, plus `--add-comments=TRN --from-code=UTF-8 --boost`
(among other flags), over the files listed in `localization/i18n/list.txt`, then `scripts/HintsToPot.py` appends the
strings of `resources/data/hints.ini`, and `msgfmt --check-format` compiles each catalog into
`resources/i18n/<lang>/OrcaSlicer.mo`. Consequences:
- A string inside `_()`/`_utf8()`/`_CHB()`/`_devL()` never reaches the `.pot`, so it stays English.
`_()` is fine only around a value that was already marked with `L()`.
- xgettext extracts only string literals (wx states the same for its own `_()`,
`interface/wx/translation.h:590-592`); a variable inside `_L()` is translated at runtime only if its
value was marked elsewhere.
- A file missing from `list.txt` is not scanned at all.
- `// TRN …` immediately above the line carries a translator comment into the catalog
(`//TRN To be shown in the main menu View->Top` in `MainFrame.cpp`, `// TRN %1% = file path` in
`DownloaderFileGet.cpp`).
- `--boost` marks `%1%` strings as boost-format, and `msgfmt --check-format` then rejects a
translation that drops or changes a placeholder.
- Only `OrcaSlicer.mo` ships; there is no `wxstd` catalog, so wx's own internal strings (standard
dialog buttons it creates, wx error messages) stay English.
### Deferred translation
Static tables, option definitions and enum labels hold `L("…")`-marked literals and are translated
where they are shown: `file_wildcards_by_type` stores `L("STL files")` titles and `file_wildcards()`
calls `I18N::translate(data.title_id)`; option labels marked `L_CONTEXT("Top", "Layers")` are shown
with `_L_CONTEXT(option.label, "Layers")` (`OG_CustomCtrl.cpp`) — only those hard-coded `"Top"`/`"Bottom"`
labels: every other def string is translated without context, so an `L_CONTEXT` in a def is ignored at
display. The settings side of this pattern is in `references/orca-settings-ui.md` §Localization of option
definitions.
### Plurals and contexts
```cpp
wxString s = wxString::Format(_L_PLURAL("%d object", "%d objects", n), n);
text += GUI::format(_L_PLURAL("%1$d Object has custom supports.", "%1$d Objects have custom supports.", cnt), cnt);
wxString top = _L_CONTEXT("Top", "Camera View"); // msgctxt "Camera View" (MainFrame.cpp)
```
- **Rule:** keep the number placeholder in both forms and give the full sentence to one msgid.
**Why:** the catalog's `Plural-Forms` decides which form applies (Russian uses the "singular" form
for 21, 31, …; ja/ko/zh have one form), so "One object" without `%d` is wrong for 21 objects;
glued fragments (`_L("Delete") + " " + _L("object")`) cannot be reordered or inflected.
- **Rule:** disambiguate homonyms with a context in the source (`_L_CONTEXT`/`_u8L_CONTEXT`), never
by tweaking a translation; the context string must match exactly at the marker and the call site.
## Language switching and the translation lifecycle
**OrcaSlicer design** (`GUI_App::load_language(wxString language, bool initial)`):
1. Initial call only: `wxFileTranslationsLoader::AddCatalogLookupPathPrefix(from_u8(localization_dir()))`
(a static wx list), then the language from AppConfig key `language`, else the system language
(MSW: `LCIDToLocaleName`), else `wxTranslations::GetBestTranslation(SLIC3R_APP_KEY, wxLANGUAGE_ENGLISH)`.
RTL languages are not supported and fall back.
2. The dictionary language and the C-runtime locale are chosen separately: Slovak uses the Czech
dictionary; when the locale is not available the code tries `linux_get_existing_locale_language`
(Linux), the base language (`en` from `en_IL`), then a fallback chain (current, system, best,
en_US, en_GB) while keeping the requested dictionary. If nothing is available it shows a
`wxMessageBox` and, on the initial call, exits.
3. `m_wxLocale.release()` (deliberate leak: "wxWidgets cause havoc if the current locale is
deleted"), `m_wxLocale = make_unique<wxLocale>()`, `m_wxLocale->Init(lang)`,
`wxTranslations::Get()->SetLanguage(language_dict)`, `m_wxLocale->AddCatalog(SLIC3R_APP_KEY)`,
`m_imgui->set_language(...)`, then rebuilds two caches: `Preset::update_suffix_modified(...)` and
`HintDatabase::get_instance().reinit()`.
Changing the language at runtime (`GUI_App::open_preferences` with a pending language) calls
`load_language(..., false)`, rebuilds the action-registry titles (`m_action_registry.relocalize_builtins()`)
and then `GUI_App::recreate_GUI`, which destroys and rebuilds `MainFrame` (and drops the cached
Speed Dial dialog). Everything constructed under the new `MainFrame` re-translates by construction;
anything that outlives it does not.
**wx caveats.** On macOS "it is impossible to change the application UI locale after launching it …
using this class doesn't affect the native controls and dialogs", and on macOS 11.0–12.2 changing the
C locale can break the menus (`interface/wx/intl.h:282-292`): native file dialogs, the app menu and
standard buttons follow the system language. `wxLocale::IsAvailable` builds a region tag and asks
`wxUILocale(...).IsSupported()` (`src/common/intl.cpp` `wxLocale::IsAvailable`), which on Unix no
longer falls back to another region of the same language (`docs/changes.txt:75-79`); keep the
fallbacks in `load_language`.
- **Rule:** translate at use; never store a translated string in a namespace-scope or function-local
`static`, or in a singleton/registry, unless it has a re-localise hook wired into the language
switch.
**Why:** a namespace-scope static is initialised before `load_language`, so it is English forever;
a function-local static captures the language of its first call and survives `recreate_GUI`.
```cpp
// Wrong:
static const wxString NA_STR = _L("N/A");
// Right:
m_label->SetLabel(_L("N/A")); // or keep L("N/A") in the table and translate when shown
```
Cite: `ActionRegistry::relocalize_builtins`, `HintDatabase::reinit`, `Preset::update_suffix_modified`
are the existing hooks.
## Labels, mnemonics and markup
**Contract** (`interface/wx/control.h:173-195, 380-398`): in `SetLabel` "All "&" characters … indicate
that the following character is a mnemonic … To insert a literal ampersand character, you need to
double it"; `SetLabelText` shows the text exactly (implemented as `SetLabel(EscapeMnemonics(text))`,
`include/wx/control.h:63-67`); `EscapeMnemonics()` is for a label combining program mnemonics with
user text. Constructor labels are interpreted like `SetLabel` (the ports' `wxStaticText::Create`
call `SetLabel(label)`) [source]. `SetLabelMarkup` also treats an
unescaped `&` as a mnemonic, needs `&amp;`/`&lt;` for literal characters, strips the markup where it
is unsupported, and leaves the label unchanged (returns false) when the string is not well-formed
(`interface/wx/control.h:200-360`); user text inside markup must be XML-escaped.
Since 3.3 wxListbook/wxChoicebook also interpret mnemonics in page titles (`docs/changes.txt:128-130`);
Orca uses neither, but the rule is the same for every book control.
**OrcaSlicer.** `Label` (`Widgets/Label.cpp`) derives from `wxStaticText`; `Label::SetLabel` stores
the text and goes through `wxStaticText::SetLabel` (or `Wrap` for `LB_AUTO_WRAP`, `SetLabelMarkup` for
`LB_HYPERLINK` on macOS), so `&` is a mnemonic in every `Label` too. The inherited `SetLabelText`
escapes and then calls `Label::SetLabel`, so it works for `Label`. Menu labels use `&File`-style
mnemonics plus `"\t" + accelerator`, and translations must keep the `&`. A plain-text MsgDialog
message is rendered by a `Label` (mnemonics interpreted); a message with a link, `is_marked_msg`,
code excerpts or a `<tr>` table goes to `wxHtmlWindow` through `xml_escape` instead (see
`references/windows-dialogs.md` §MsgDialog family).
- **Rule:** user-supplied text goes through `SetLabelText` or `wxControl::EscapeMnemonics`.
**Why:** the `&` disappears and the next character becomes a mnemonic: `PLA & PETG` shows as
`PLA PETG` on every port.
```cpp
// Wrong:
label->SetLabel(from_u8(preset.name));
menu->Append(id, from_u8(printer_name));
// Right:
label->SetLabelText(from_u8(preset.name));
menu->Append(id, wxControl::EscapeMnemonics(from_u8(printer_name)));
```
## Paths and standard locations
**OrcaSlicer path model.** Paths live as `boost::filesystem::path` or UTF-8 `std::string`.
`OrcaSlicer.cpp` calls `boost::nowide::nowide_filesystem()` at startup, which imbues boost paths with a
UTF-8 codecvt, so on Windows `boost::filesystem::path(utf8_string)` and `path.string()` are UTF-8.
`std::filesystem::path` is not imbued: on Windows a narrow `std::string` is read in the ANSI code page,
so build it from the wide form (`into_path(w).wstring()`) or stay with boost. At the wx boundary use
`from_path`/`into_path`.
**`wxFileName`** (`interface/wx/filename.h`): `Normalize()` without flags is deprecated
(`include/wx/filename.h:358-364`) because `wxPATH_NORM_ALL` includes `wxPATH_NORM_ENV_VARS` and
expands `$VAR`/`%VAR%` inside file names (`interface/wx/filename.h:90-103`); use `MakeAbsolute()` or
explicit `wxPATH_NORM_DOTS | wxPATH_NORM_ABSOLUTE`. 3.3 adds `IsMSWExtendedLengthPath()` for `\\?\`
paths "avoiding the 260 character path length restriction" (`interface/wx/filename.h:1040-1051`,
`docs/changes.txt:551`). `wxFileSystem::FileNameToURL`/`URLToFileName` convert to and from `file:`
URLs (`interface/wx/filesys.h:96-103, 175-180`).
**`wxStandardPaths`** (`interface/wx/stdpaths.h`): the directories "may or may not exist"
(`:39`); `GetUserDataDir()` is `~/.appinfo` on Unix, `%APPDATA%\appinfo` on Windows and
`~/Library/Application Support/appinfo` on macOS, and on Unix ignores `FileLayout_XDG` (`:406-420`);
`GetUserDir()` always follows XDG on Unix (`:422-433`).
**OrcaSlicer data dir** (`GUI_App::init_app_config`): a `data_dir` folder next to the executable if it
exists (portable mode), else `GetUserDataDir()` on MSW/macOS, or `$XDG_CONFIG_HOME/OrcaSlicer`
(default `~/.config/OrcaSlicer`) built by hand on Linux; then the process **chdirs to
`data_dir()/log`**. Relative paths therefore resolve into the log folder, and anything that changes the
working directory (`wxFD_CHANGE_DIR`, `wxDD_CHANGE_DIR`, `chdir`) breaks code that relies on it. Use
absolute paths built from `data_dir()`, `resources_dir()`, `localization_dir()`.
## File and directory dialogs
**Usage:**
```cpp
wxFileDialog dlg(this, _L("Choose one or more files (3MF/STEP/STL/SVG/OBJ/AMF):"),
from_u8(wxGetApp().app_config->get_last_dir()), wxEmptyString,
file_wildcards(FT_MODEL), wxFD_OPEN | wxFD_MULTIPLE | wxFD_FILE_MUST_EXIST);
if (dlg.ShowModal() != wxID_OK)
return;
wxArrayString paths;
dlg.GetPaths(paths); // GetPath() returns "" with wxFD_MULTIPLE
for (const wxString& p : paths)
load(into_path(p));
```
**Contract** (`interface/wx/filedlg.h`):
- `wxFD_OPEN` and `wxFD_SAVE` are exclusive; `wxFD_MULTIPLE`/`wxFD_FILE_MUST_EXIST` are open-only,
`wxFD_OVERWRITE_PROMPT` save-only (`:163-200`). Contradictory styles are only asserted
(`src/common/fldlgcmn.cpp:776-787`), i.e. ignored silently in Orca.
- Style bits are reused between classes: `wxPD_APP_MODAL` (0x0002) equals `wxFD_SAVE`
(`include/wx/progdlg.h:21`, `include/wx/filedlg.h:45-46`), so a foreign flag can change the dialog
type. Pass only `wxFD_*`.
- `GetPath()`/`GetFilename()` "can't be used with dialogs which have the `wxFD_MULTIPLE` style"
(`:343-347, 380-384`); they return `""` there (`docs/changes_32.txt:119-120`). Use
`GetPaths()`/`GetFilenames()`.
- Wildcard format `"Desc (*.a;*.b)|*.a;*.b|Desc2 (*.c)|*.c"` (`:106-115`); the default wildcard is
`"*.*"` on MSW and `"*"` elsewhere (`:31-36`).
- `SetFilename()` in wxGTK has "little effect unless a default directory has previously been set"
(`:452-456`).
- `SetExtraControlCreator()` forces old XP-style dialogs on MSW; `SetCustomizeHook()` is native
(`:131-155`). New-style MSW dialogs need a single-threaded COM apartment (`:157-161`).
- `wxDirDialog`: `wxDD_NEW_DIR_BUTTON` is `0` ("deprecated, on by default now",
`interface/wx/dirdlg.h:14`); the "Create new directory" button is shown exactly when
`wxDD_DIR_MUST_EXIST` is absent (`interface/wx/dirdlg.h:42-46`); on macOS 10.11+ there is no title
bar, the `message` argument is what the user sees (`interface/wx/dirdlg.h:59-62`).
**Platforms:**
| | Behaviour |
|---|---|
| MSW | New-style `IFileDialog`: wx calls `SetDefaultExtension` with the selected filter's first extension and no longer calls `AppendExtension` itself (`src/msw/filedlg.cpp:1644-1656, 1738-1740`; `docs/changes.txt:556`). `SetExtraControlCreator` → XP-style dialog. |
| macOS | Open dialogs show **no filter choice** and apply all wildcards at once unless `wxSystemOptions::SetOption(wxOSX_FILEDIALOG_ALWAYS_SHOW_TYPES, 1)`, and even then non-matching files are only greyed (`interface/wx/filedlg.h:117-128`). Matching compares the lower-cased last path extension (`wxOpenSavePanelDelegate panel:shouldEnableURL:`, `src/osx/cocoa/filedlg.mm:58-88`) [source]: case-insensitive, and a multi-dot pattern such as `*.gcode.3mf` or `*.zip.amf` never matches by itself. `wxFD_OVERWRITE_PROMPT` is always on (`:172-175`); `wxFD_OPEN` always behaves as `wxFD_FILE_MUST_EXIST` (`:184-189`). The save panel replaces the initial file name's extension with the first one in the wildcard (Orca's comment on `file_wildcards`). The native panel runs `runModal` and does not re-raise the parent dialog afterwards (`src/osx/cocoa/filedlg.mm` `ShowModal`); see `references/windows-dialogs.md` for the deferred re-raise. |
| GTK3 | `GtkFileChooserNative` (portal-capable, e.g. Flatpak) when GTK ≥ 3.20 at runtime and neither `wxFD_PREVIEW` nor an extra control/customize hook is used (`src/gtk/filedlg.cpp:265-274, 437-443`; `src/gtk/dirdlg.cpp:122-130`; `docs/changes.txt:393`). It runs through `gtk_native_dialog_run`: there is no wx window, so size/position calls do nothing (`wxFileDialog::DoSetSize` is empty). Patterns go to `gtk_file_filter_add_pattern` per token and are **case-sensitive** (`src/gtk/filectrl.cpp:169`). The chooser is transient for the parent left after `GetParentForModalDialog`, which replaces a null (or hidden, dying or `wxWS_EX_TRANSIENT`) parent with the active top-level window, else the app's main top window (`src/gtk/filedlg.cpp:206, 223-225`; `src/common/dlgcmn.cpp:180-203` `DoGetParentForDialog`) [source]. `wxFD_PREVIEW` is GTK-only (`interface/wx/filedlg.h:195-197`). |
| GTK2 (opt-out build) | never uses the native chooser; patterns case-sensitive. |
**OrcaSlicer:**
- `file_wildcards(FileType, custom_extension)` (`GUI_App.cpp`) builds every filter from
`file_wildcards_by_type`: translated title, and an upper-case twin of every extension
(`*.stl;*.STL`) because GTK patterns are case-sensitive. A `custom_extension` is put first because
the macOS save panel substitutes the first extension into the initial file name.
- Openers to copy: `GUI_App::import_model`, `GUI_App::import_zip` (default dir `from_u8(...)`).
Last directories come from `AppConfig::get_last_dir()`/`get_last_output_dir()`; the app stores
them itself (`update_config_dir`, `update_last_output_dir`) instead of using `wxFD_CHANGE_DIR`.
- `Plater::priv::get_export_file` appends the expected extension on `__WXMSW__` when the returned name
lacks it and then asks its own overwrite question, because the native overwrite prompt only checked
the name the user typed.
- `CheckboxFileDialog` (`GUI_Utils.hpp/.cpp`) uses `SetExtraControlCreator`, which costs the native
dialog on MSW and GTK3; prefer `SetCustomizeHook` for new extra controls.
**Pitfalls:**
- **Rule:** give file and directory dialogs the top-level window they belong to as parent.
**Why:** with `nullptr`, wxGTK (`wxFileDialog::Create`, `wxDirDialog::Create`) and wxMSW
(`wxFileDialog::ShowModal`) substitute whatever top-level window is active at that moment, else
the app's main top window (`wxDialogBase::DoGetParentForDialog`, `src/common/dlgcmn.cpp:180-203`)
[source], so the chooser becomes transient for and modal over an arbitrary window (a modeless
dialog or web window that happened to be active), or gets no parent when none qualifies. Gizmo
code has no `this` window to hand, which is where `nullptr` creeps in.
```cpp
// Wrong:
wxFileDialog dialog(nullptr, _L("Choose SVG file"), ...);
// Right:
wxFileDialog dialog(wxGetApp().mainframe, _L("Choose SVG file"), ...); // or GetTopWindow()
```
Cite: `src/gtk/filedlg.cpp` `wxFileDialog::Create` (`GetParentForModalDialog`, then `gtk_parent`).
- **Rule:** use `wxDD_DEFAULT_STYLE` for directory dialogs; add `wxDD_DIR_MUST_EXIST` only when the
user must not create a folder.
**Why:** `wxDD_NEW_DIR_BUTTON` is 0, so passing it alone means style 0: no
`wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER` (the generic dialog loses its frame). It does not control
the new-folder button either: that appears whenever `wxDD_DIR_MUST_EXIST` is absent, so adding
`wxDD_DIR_MUST_EXIST` removes it (and restricts the choice to existing folders).
```cpp
// Wrong:
wxDirDialog dlg(this, msg, path, wxDD_NEW_DIR_BUTTON);
// Right:
wxDirDialog dlg(this, msg, path, wxDD_DEFAULT_STYLE); // | wxDD_DIR_MUST_EXIST: existing folders only, no new-folder button
```
Cite: `include/wx/dirdlg.h:45-47`; `interface/wx/dirdlg.h:42-46`.
- **Rule:** never pass `wxFD_CHANGE_DIR`/`wxDD_CHANGE_DIR`.
**Why:** Orca's working directory is `data_dir()/log`; the native GTK path even `chdir`s directly
(`src/gtk/filedlg.cpp:449-458`).
## Launching the browser, files and programs
**Contract** (`interface/wx/utils.h`):
- `wxLaunchDefaultBrowser(url, flags)`: `wxBROWSER_NEW_WINDOW` is honoured only on Windows; a URL
without a scheme is tested as a local file/dir (then prefixed with `file:`), otherwise `http:` is
prepended; returns false on failure (`:468-490`). wxGTK tries `gtk_show_uri` and then `xdg-open`
(`src/unix/utilsx11.cpp:2674-2737`).
- `wxLaunchDefaultApplication(document, flags)` opens the file in its associated application; `flags`
is unused (`:454-462`).
- `wxExecute`: `wxEXEC_ASYNC` returns the pid; `wxEXEC_SYNC` "will call wxYield()" and disables all
windows unless `wxEXEC_NODISABLE` (`:1196-1212`), so a synchronous call re-enters your handlers;
main thread only (`:1250-1252`).
**OrcaSlicer:**
- Links: `wxGetApp().open_browser_with_warning_dialog(url, flags)` is the app-level entry point (it
forwards to `wxLaunchDefaultBrowser`, so a direct `wxLaunchDefaultBrowser` call behaves the same).
- Folders: `desktop_open_any_folder(path)` / `desktop_open_datadir_folder()` (`GUI.cpp`): `explorer`
on Windows (`explorer /select,<path>` for `desktop_open_any_folder`), `openFolderForFile`
(any folder) or `open` (data dir) on macOS, `xdg-open` on Linux (of the containing folder when
`path` is a file) with the AppImage variables (`APPIMAGE`, `APPDIR`, `LD_LIBRARY_PATH`,
`LD_PRELOAD`, `UNION_PRELOAD`) removed and `OWD` as the working directory. A bare `wxExecute("xdg-open …")` or
`wxLaunchDefaultApplication` from an AppImage passes Orca's bundled libraries to the file manager.
- Project attachments: `desktop_open_project_attachment` accepts only a regular file inside the
project's auxiliary temp folder (`is_absolute_path_within_root`) and asks before opening anything
`is_safe_to_open_file_name` (`libslic3r/utils.cpp`) does not allow-list, because the desktop would
run a script or executable without a download warning. Reuse it for any file that arrived inside a
project or from the network.
## Clipboard
**Contract** (`interface/wx/clipbrd.h`): `Open()` "should be tested"; keep the clipboard open "only
momentarily" (`:24-29, 145-155`). `SetData` replaces any previously set object, so several formats
need one composite object; "After this function has been called, the clipboard owns the data"
(`:157-170`). `Flush()` keeps the data after exit; implemented on MSW and GTK only, on GTK only for
the CLIPBOARD selection and with a clipboard manager running (`:56-61, 100-115`).
`UsePrimarySelection(true)` makes every operation fail on platforms without a PRIMARY selection
(`:172-187`). `wxClipboardLocker` (`include/wx/clipbrd.h:167-191`) opens in its ctor, closes in its
dtor, and `!lock` tests `IsOpened()`.
**Platforms** [source]:
| | Behaviour |
|---|---|
| MSW | `SetData`/`GetData` do not check `Open()` (`src/msw/clipbrd.cpp` `wxClipboard::SetData`), so code that forgets it works here only. `wxTextDataObject::SetData()` size must now include the 2-byte NUL (`docs/changes.txt:62-66`); use `SetText()` or the ctor. |
| macOS | without `Open()`, `SetData`/`AddData`/`GetData` return false through `wxCHECK_MSG(m_open, …)` without taking the object (`src/osx/carbon/clipbrd.cpp:84-105, 140-147`); a successful write is flushed to the pasteboard immediately (`:110-114`). |
| GTK | same `wxCHECK_MSG(m_open, …)` early return (`src/gtk/clipbrd.cpp:643-647, 775`). `GetData`/`IsSupported` are asynchronous underneath: `wxClipboardSync` spins `YieldFor(wxEVT_CATEGORY_CLIPBOARD)` until GTK answers and forbids re-entrancy (`src/gtk/clipbrd.cpp:68-92`). PRIMARY selection exists. Under Wayland, Wayland MIME types are advertised next to the X11 atoms (`src/gtk/clipbrd.cpp:655-700`). |
```cpp
wxClipboardLocker lock;
if (!lock)
return;
wxTheClipboard->SetData(new wxTextDataObject(text)); // clipboard owns it on success
```
**OrcaSlicer.** Models: the copy button in `TroubleshootDialog` (`wxClipboardLocker`) and
`ImGuiWrapper::clipboard_set`/`clipboard_get` (tested `Open()`, UTF-8 via `wxString::FromUTF8`/
`into_u8`). The 3D scene's copy/paste is an internal clipboard (`Selection::Clipboard`,
`Selection::copy_to_clipboard`/`paste_from_clipboard`), not the wx one.
- **Rule:** open the clipboard through a checked `wxClipboardLocker` before `SetData`/`GetData`, and
test the result.
**Why:** on GTK and macOS an unopened clipboard makes `SetData` return false without taking the
object (leak, nothing copied) and `GetData` return false (nothing pasted); MSW does not check, so
the bug passes Windows testing.
```cpp
// Wrong:
wxTheClipboard->Open();
wxTheClipboard->SetData(new wxTextDataObject(t));
wxTheClipboard->Close();
// Right:
wxClipboardLocker lock;
if (lock)
wxTheClipboard->SetData(new wxTextDataObject(t));
```
## Drag and drop
**Contract.** `SetDropTarget`: "If the window already has a drop target, it is deleted"
(`interface/wx/window.h:3651-3657`); the window also deletes its target in its destructor
(`src/common/wincmn.cpp:515`), so the window owns it. `DragAcceptFiles` "Cannot be used together
with SetDropTarget() on non-Windows platforms" (`interface/wx/window.h:3659-3672`). The
`wxDropTarget` destructor deletes its data object and `SetDataObject` deletes the previous one
(`interface/wx/dnd.h:59-62, 138-146`). Call sequence: `OnEnter` → `OnDragOver`* → `OnDrop` (return
false to refuse) → `OnData` (`interface/wx/dnd.h:44-45, 75-127`). `wxFileDropTarget::OnDropFiles(x, y,
filenames)` returns true to accept (`interface/wx/dnd.h:395-422`). `wxDropSource::SetData` "will not
delete any previously associated data", i.e. the source does not own it (`interface/wx/dnd.h:335-338`).
`DoDragDrop(flags)` "blocks the program until the user releases the mouse button", and the target
cannot change the result code the source gets (`docs/doxygen/overviews/dnd.h:44-50, 84-90`).
`wxDragMove` is reported on MSW only (`interface/wx/dnd.h:26`).
**Platforms** [source]:
| | Behaviour |
|---|---|
| MSW | `SetDropTarget` revokes and deletes the old target immediately (`src/msw/window.cpp:1753-1761`). `wxDropTarget::MSWUpdateDragImageOnLeave()` (undocumented, `include/wx/msw/ole/droptgt.h:73`) hides the shell drag image. |
| GTK | `SetDropTarget` unregisters and deletes immediately (`src/gtk/window.cpp:6605-6616`). `DoDragDrop` returns `wxDragNone` unless a mouse button is down after a mouse event (`src/gtk/dnd.cpp:835-850`), then runs a nested `gtk_main_iteration` loop with `g_blockEventsOnDrag` set (`:906-916`). Under Wayland it also hooks button/motion events because "drag-end" may never arrive (`:945-955`). |
| macOS | `SetDropTarget` deletes the old target immediately (`src/osx/window_osx.cpp:616-622`). |
**wxDataViewCtrl DnD** (`interface/wx/dataview.h`): `EnableDropTargets` is fully implemented in the
generic and native macOS versions, wxGTK uses only the first format (`:1376-1386`); `SetDragFlags` is
honoured only by the generic control (MSW), not native GTK/macOS (`:4010-4024`); `GetDropEffect`
returns `wxDragNone` on native GTK and macOS (`:4026-4041`); `GetProposedDropIndex` works from
`ITEM_DROP` everywhere and from `ITEM_DROP_POSSIBLE` except native GTK (`:4053-4064`). The control
itself is in `references/controls-dataview.md`.
**OrcaSlicer:**
- Model-file drops: `PlaterDropTarget : wxFileDropTarget` (`Plater.cpp`), installed with
`q->SetDropTarget(new PlaterDropTarget(...))` (the window takes ownership); `SetDefaultAction(wxDragCopy)`.
`OnDropFiles` calls `MSWUpdateDragImageOnLeave()` under `WIN32` before showing UI, raises the main
frame, switches to the Prepare tab, routes a single `.svg` to the SVG gizmo (`GLGizmoSVG::create_volume`
at the drop point) and otherwise calls `Plater::load_files`. `Preview::set_drop_target(target)` is
the helper for putting a target on the preview window.
- Custom payload: `DragDropPanel.cpp` (filament-group dragging). `ColorDataObject : wxCustomDataObject`
with `wxDataFormat("application/customize_format")` and fixed-size POD data; `ColorDropSource` keeps
the data object as a member and is constructed on the stack in `DragDropPanel::DoDragDrop`, which
`ColorPanel::OnLeftDown` calls (satisfying the GTK button-down rule); `ColorDropTarget` owns its
`ColorDataObject` through `SetDataObject`.
- ObjectList row reordering (`GUI_ObjectList.cpp`): `ObjectList::OnBeginDrag` keeps the real payload in
`m_dragged_data`, gives wx a dummy `wxTextDataObject` with non-empty text ("needed for GTK") and
`SetDragFlags(wxDrag_DefaultMove)`; `m_prevent_list_events` suppresses the selection events GTK fires
because it drops *between* rows where MSW/macOS drop *on* a row.
**Pitfalls:**
- **Rule:** call `wxDropSource::DoDragDrop` synchronously from the mouse-down/drag handler.
**Why:** wxGTK refuses (returns `wxDragNone`) without a current button press after a mouse event; a
`CallAfter` or timer loses it.
```cpp
// Wrong:
CallAfter([this] { wxDropSource src(this); src.SetData(m_obj); src.DoDragDrop(); });
// Right:
void ColorPanel::OnLeftDown(wxMouseEvent&) { m_parent->DoDragDrop(this, ...); }
```
- **Rule:** never call `SetDropTarget` on a window from inside that window's current target
(`OnDrop`/`OnData`/`OnDropFiles`); defer it with `CallAfter`.
**Why:** every port deletes the old target immediately, so the callback returns into a freed object.
## Logging: wxLog vs boost log
**Contract** (`docs/doxygen/overviews/log.h`): with the default `wxLogGui`, `wxLogError`/`wxLogWarning`/
`wxLogMessage` pop up a message box (`:31-38`); messages from other threads are buffered until the main
thread flushes (`:228-240`); `wxLogNull`/`EnableLogging(false)` affect only the current thread
(`:242-244`). `wxLog::SetActiveTarget` takes ownership of the new target and the caller must delete the
returned old one (`interface/wx/log.h:326-340`). `wxLogNull` suppresses **all** messages, not only the
one you expect (`interface/wx/log.h:1026-1035`).
**OrcaSlicer.** `GUI_App::on_init_inner` first calls `wxLog::SetActiveTarget(new wxBoostLog())`;
`wxBoostLog::DoLogText` writes every wx message to `BOOST_LOG_TRIVIAL(warning)` as UTF-8, and its
destructor flushes pending thread messages into itself so they never reach a `wxLogGui`. Release builds
(`BBL_RELEASE_TO_PUBLIC`) also `wxLog::SetLogLevel(wxLOG_Message)`. `wxLogDebug`/`wxLogTrace` compile to
nothing at debug level 0 (`include/wx/log.h:62-77`). So **wxLog never shows UI in Orca**, including
the errors wx itself logs (failed file operations, image loading), which end up only in the log file.
Use `wxLogNull` around a probing call whose failure wx would log (`OpenGLManager.cpp`), and still check
the return value.
Write diagnostics with `BOOST_LOG_TRIVIAL(level) << __FUNCTION__ << …` (or
`boost::format("%1%: …") % …`) and stream UTF-8: `into_u8(w)` or `w.ToUTF8().data()` inside the
statement. Streaming a `wxString` directly goes through `wxConvWhateverWorks` (locale first;
`src/common/string.cpp:158-170`) and writes ANSI bytes into the UTF-8 log on MSW.
User-facing errors go through `show_error(parent, msg)` (`GUI.hpp`), which is **asynchronous**: it
queues an `ErrorDialog` with `wxGetApp().CallAfter` and captures the raw `parent`, so pass a parent that
outlives the call. `show_info` and `warning_catcher` are synchronous MsgDialogs; the `const char*`/
`std::string` overloads decode UTF-8. The dialogs themselves are in `references/windows-dialogs.md`
§MsgDialog family.
- **Rule:** never use `wxLogError`/`wxLogWarning`/`wxLogMessage` to inform the user.
**Why:** `wxBoostLog` sends them to the log file only; the user sees nothing.
```cpp
// Wrong:
wxLogError(_L("Export failed"));
// Right:
show_error(this, _L("Export failed"));
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": export failed: " << into_u8(path);
```
## Native message boxes
**Contract** (`interface/wx/msgdlg.h`): `wxCANCEL` "Must be combined with either wxOK or wxYES_NO"
(`:30`); `wxYES_NO` without `wxCANCEL` has no close button on MSW (`:31-35`); `wxHELP` is unsupported
from a non-main thread on wxOSX (`:39-40`); `wxCANCEL_DEFAULT` is ignored on wxOSX (`:44-46`);
`wxSTAY_ON_TOP` works only on MSW and GTK (`:89-91`). `wxMessageDialog::ShowModal()` returns
`wxID_OK/wxID_CANCEL/wxID_YES/wxID_NO/wxID_HELP` (`:269-275`), but **`wxMessageBox()` returns
`wxYES/wxNO/wxCANCEL/wxOK/wxHELP`** (`:308-312`). `wxRichMessageDialog` is native only on MSW and
generic elsewhere (`interface/wx/richmsgdlg.h:18-23`). On MSW every `TaskDialog`-based dialog
(`wxMessageBox`, `wxMessageDialog`, `wxRichMessageDialog`, `wxProgressDialog`) ignores dark mode
(`interface/wx/app.h:1436-1440`).
**OrcaSlicer.** UI code uses the MsgDialog family (`MessageDialog`, `RichMessageDialog`,
`WarningDialog`, `ErrorDialog`, `InfoDialog`; `references/windows-dialogs.md` §MsgDialog family) for
dark mode, DPI and a consistent look; its `ShowModal()` returns `wxID_*`. Native boxes are for code that
runs before the GUI exists (`wxMessageBox` in `GUI_App::load_language`, `MessageBoxA` in `OrcaSlicer.cpp`).
- **Rule:** compare each API's result with its own constants.
**Why:** `wxYES` (0x2) is not `wxID_YES` (5103); the comparison is always false and the "Yes" branch
never runs.
```cpp
// Wrong:
if (wxMessageBox(msg, title, wxYES_NO) == wxID_YES) ...
// Right:
if (MessageDialog(this, msg, title, wxYES_NO).ShowModal() == wxID_YES) ... // or wxMessageBox(...) == wxYES
```
## Settings: AppConfig, not wxConfig
Orca does not use `wxConfig`/`wxFileConfig` (so the 3.3 change of the Unix default location to XDG,
`docs/changes.txt:25-29`, does not affect it). Settings live in `AppConfig`
(`src/libslic3r/AppConfig.hpp`), a JSON file `OrcaSlicer.conf` under `data_dir()`
(`AppConfig::config_path`), reached through `wxGetApp().app_config` (the object map is in
`references/orca-architecture.md`).
- String-typed API: `get(key)`/`get(section, key)` return `std::string` (UTF-8) and `""` when absent;
`set(key, value)`, `set(section, key, value)`, `set_str(section, key, value)`, `set_bool(key, bool)`;
`has`, `erase`. `get_bool(section, key)` is `get(section, key) == "true" || get(key) == "1"` (the
`"1"` test reads the `app` section). Some keys hold `"true"`/`"false"` (what `set(section, key, bool)`
writes), others `"1"`/`"0"`; check how a key is written before testing it.
- `set` marks the config dirty when the value changes; `GUI_App`'s idle handler saves a dirty config
after post-init. Call `app_config->save()` explicitly only when the value must be on disk before control
returns to the event loop (before a restart, exit or launching another instance); Preferences rows also
`save()` at once by convention (`references/orca-architecture.md` §Preferences).
- `AppConfig::save()` throws `CriticalException` off the main thread, and nothing in `AppConfig` is
locked: read and write it on the main thread only.
- Values are UTF-8: `from_u8(app_config->get(...))` at the wx boundary, `into_u8(w)` going back.
- **Rule:** pass a `std::string` (or use `set_str`) when setting a string value with a section.
**Why:** for `set("app", "key", "value")` the `const char*` → `bool` conversion is a standard
conversion and beats the user-defined `std::string` one, so `set(section, key, bool)` wins and stores
`"true"` [tested with clang].
```cpp
// Wrong:
app_config->set("app", "theme", "dark"); // stores "true"
// Right:
app_config->set_str("app", "theme", "dark"); // or std::string("dark"), or set("theme", "dark")
```
Cite: `AppConfig::set` overloads in `AppConfig.hpp`.
## Secrets: wxSecretStore
**Contract** (`interface/wx/secretstore.h`): on Unix it needs libsecret and a running secret service, so
always check `IsOk(&errmsg)` (`:193-210, 257`); libsecret is no longer required at run time
(`docs/changes.txt:533`), so a missing library or service shows up only as `IsOk()` false.
`GetDefault()` "may show a dialog to the user under some platforms, so it can take an arbitrarily long
time to return" (`:243-246`). One username per service (`:260-268`).
**OrcaSlicer** (`OrcaCloudServiceAgent`): AppConfig `SETTING_USE_ENCRYPTED_TOKEN_FILE` selects either an
AES-GCM encrypted token file in the data dir or the system store; `secret_stored` records whether this
process read or wrote a secret, and `clear_user_secret` touches the store only then (or on an explicit
all-backends logout).
- **Rule:** never call `wxSecretStore::GetDefault()`, `Load`, `Save` or `Delete` from a timer, poll or
repeated UI path; touch the store on explicit login/logout and cache the state.
**Why:** each call is a blocking keychain/D-Bus round trip on the UI thread; an unresponsive keychain
froze the UI for 25 s per poll, and the delete also removed a login another instance had just saved.
Cite: 1a5bc8982d (`OrcaCloudServiceAgent::clear_user_secret`).
## Single instance
**Contract** (`interface/wx/snglinst.h`): `wxSingleInstanceChecker::Create(name, path)`: `name` "is used
as the mutex name under Win32 and the lock file name under Unix", and `path` "is ignored under Win32"
(`:97-103`); the default name includes the user id, so different users may run concurrently (`:49-53`).
**OrcaSlicer** (`instance_check(argc, argv, app_config_single_instance)`, `InstanceCheck.cpp`), run at
startup from `GUI_Init.cpp` before the GUI: the executable path (the AppImage file on Linux) is hashed into a lock name.
Windows checks it with a `wxSingleInstanceChecker` (`GUI_App::init_single_instance_checker`, name
`<hash>.lock`); macOS and Linux use Orca's own lock file `data_dir()/cache/<hash>.lock`
(`instance_check_internal::get_lock`). When another instance holds the lock and single-instance mode
applies (command line, else AppConfig), the command line is forwarded and this process exits: Windows
`WM_COPYDATA` to the other instance's window, macOS `NSDistributedNotificationCenter`
(`InstanceCheckMac.mm`, `send_message_mac`), Linux D-Bus. No wxIPC is involved. The receiving
`OtherInstanceMessageHandler` turns messages into `EVT_LOAD_MODEL_OTHER_INSTANCE`,
`EVT_START_DOWNLOAD_OTHER_INSTANCE` and `EVT_INSTANCE_GO_TO_FRONT` for the main frame. A new kind of
cross-instance request extends this message path on all three transports rather than adding a second
checker or a wxIPC server.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,991 @@
# Windows, dialogs and window lifetime
How windows are created, parented, closed and destroyed; how to tell whether a window is still
alive; how top-level windows show, raise and persist; how modal and modeless dialogs work on each
port; what Orca's `DPIDialog`/`DPIFrame` add; the Orca dialog recipe; and the `MsgDialog` family.
Read it before writing or reviewing any dialog, frame, close handler, `Destroy()`/`delete`, or code
that keeps a pointer to a window across an event, a `CallAfter` or a modal loop.
wx cites are relative to the pinned wx 3.3.2 tree (located as in `SKILL.md` §Ground truth). wx is
built with `wxBUILD_DEBUG_LEVEL=0` and `libslic3r_gui` with `wxDEBUG_LEVEL=0`: every wx assert
quoted below is compiled out, so misuse fails silently (dropped call, stuck loop, freed memory), never with an assert dialog. "GTK" below
means wxGTK as Orca builds it on Linux: GTK3 by default (X11 or Wayland); GTK2 is only an opt-out.
Contents: [Rules](#rules) · [1 Creating and parenting](#1-creating-and-parenting-windows) ·
[2 Destroy, delete, Close](#2-destroying-windows-destroy-delete-close-and-the-close-event) ·
[3 Liveness](#3-liveness-is-this-window-still-alive) ·
[4 Top-level windows](#4-top-level-windows-show-raise-enable-state-geometry) ·
[5 Window styles](#5-window-styles) · [6 Dialogs and modality](#6-dialogs-and-modality) ·
[7 DPIDialog and DPIFrame](#7-dpidialog-and-dpiframe) ·
[8 The Orca dialog recipe](#8-the-orca-dialog-recipe) ·
[9 MsgDialog family](#9-message-boxes-the-msgdialog-family) ·
[10 Overlay frames](#10-overlay-frames-basetransparentdpiframe)
## Rules
1. Free heap-allocated windows with `Destroy()`, never `delete`; a modal dialog on the stack is freed by its scope and must never be `Destroy()`ed. §2
2. Never `Destroy()` a child window (or an ancestor of it) from inside that child's own event handler: child `Destroy()` is a synchronous `delete this`. Defer with `wxTheApp->ScheduleForDestruction(w)` or a guarded `CallAfter`. §2
3. A close handler either vetoes or destroys/ends the window; prompts and vetoes only when `CanVeto()`; when `!CanVeto()` it must not veto. §2
4. Do irreversible teardown in a close handler only once every veto is decided — the default top-level handler still vetoes a vetoable close while any modal dialog is open. §2
5. Give every dialog and frame a live top-level parent (`parent ? parent : wxGetApp().mainframe`); a parentless window that only hides on close keeps the process alive. §1, §2
6. Never construct a window to reach computation; make the logic `static` or free. §1
7. To ask "is this top-level window still alive", check `wxTheApp->IsScheduledForDestruction(w)` as well as `IsBeingDeleted()`; `wxWeakRef` nulls only at the very end of destruction; cross-thread code uses a `std::shared_ptr<std::atomic<bool>>` alive flag. Re-check after every nested event loop. §3
8. A `wxEVT_DESTROY` handler on a parent fires for every descendant: compare `GetEventObject()` and `Skip()`. For a top-level window it runs after the derived destructor — do derived cleanup in the destructor. §3
9. Accessors reachable from teardown-time events return `nullptr` instead of dereferencing a pimpl or child; the real fix is still to stop the events first. §3
10. Bring a top-level window forward with `if (!w->IsShown()) w->Show(); w->Raise();` — `Raise()` never shows, and a redundant `Show()` is not inert on GTK3. §4
11. On macOS, after a native modal (file/dir dialog, native message box) or a generic progress dialog opened from a secondary top-level window, re-raise that window with a deferred, liveness-guarded `Raise()`. §4
12. Remove style bits with `& ~flag`, never `!flag`. §5
13. Always pass the style to a `DPIDialog`: the `DPIAware` default is `wxDEFAULT_FRAME_STYLE` (resizable, min/max boxes). §5, §7
14. A `wxFRAME_FLOAT_ON_PARENT` frame takes a non-null top-level parent (parent to `mainframe`, not to a panel). §5
15. End a modal dialog with `EndModal(wxID_*)` (or `EndDialog` from inside the class); never with `Hide()`, `Show(false)` or `Destroy()` while its loop runs. A heap modal is `Destroy()`ed after `EndModal()` — normally by the caller once `ShowModal()` returns. §6
16. Return codes are `wxID_*` ids, never the `wxOK/wxCANCEL/wxYES/wxNO/wxCLOSE` style bits (the `MsgDialog` "Go to" button's `wxFORWARD` is the one deliberate exception). §6, §9
17. Nested modal dialogs end innermost first; `DPIDialog::EndModal` refuses (logs, leaves the dialog open) on a dialog that is not the innermost `DPIDialog`. §6, §7
18. Never call `EndModal()` on a modeless dialog; use `Hide()`, `Close()`, `Destroy()`, or `EndDialog(rc)` inside the class. §6
19. Never show a modal dialog directly from a mouse-button or motion handler; defer with `CallAfter`. §6
20. Run cancel cleanup on every close path: ESC and the title-bar close box go through `Close()` and `wxID_CANCEL`, never through an Orca `Button`'s handler. §6, §8
21. Dialogs derive `DPIDialog`, implement `on_dpi_changed`, call `CenterOnParent()` after fitting, and call `wxGetApp().UpdateDlgDarkUI(this)` last; an override of `ShowModal()` calls `DPIDialog::ShowModal()`. §7, §8
22. The bottom row is `DialogButtons` with untranslated labels; bind every button whose default wx handling is not what you want — Yes/No/Confirm/custom buttons never close the dialog by themselves. §8
23. Messages to the user go through the `MsgDialog` family (`MessageDialog`, `RichMessageDialog`, `WarningDialog`, `ErrorDialog`, `InfoDialog`, `show_error`/`show_info`), never `wxMessageBox`/`wxMessageDialog` once the GUI exists; treat `wxID_CANCEL` (ESC/close box) as "no". §9
24. `show_error` is asynchronous and captures its parent pointer; pass a long-lived parent or `nullptr`. §9
---
## 1 Creating and parenting windows
**Contract.**
- Child windows are created shown; top-level windows (frames, dialogs) are created hidden "to allow
you to create their contents without flicker" (`interface/wx/window.h:3140-3158`).
- "Child windows are deleted from within the parent destructor. This includes any children that are
themselves frames or dialogs, so you may wish to close these child frame or dialog windows explicitly
from within the parent close handler" (`docs/doxygen/overviews/windowdeletion.h:96-100`).
- `wxDialog` with a NULL parent "will be owned by the application's top window, if any. Use
`wxDIALOG_NO_PARENT` style to really make dialog not owned by any window" (`interface/wx/dialog.h:166-172`).
The style doc adds: orphan dialogs are "not recommended for modal dialogs" (`interface/wx/dialog.h:122-126`).
- **[source]** Only the *native* owner is substituted; `GetParent()` stays NULL. Owner resolution
(`wxDialogBase::DoGetParentForDialog`, `src/common/dlgcmn.cpp:180-204`): the given parent's
top-level parent → the active window's top-level parent → `wxApp::GetMainTopWindow()`, each
rejected if it is pending delete, being deleted, has `wxWS_EX_TRANSIENT`, or — for modal use — is
not `IsShownOnScreen()` (`CheckIfCanBeUsedAsParent`, `src/common/dlgcmn.cpp:130-178`).
**Two-step creation.** "the underlying window must be created exactly once… if you use the default
constructor… you *must* call Create() before using the window and if you use the non-default
constructor, you can *not* call Create()" (`interface/wx/window.h:411-417`). Methods may be called
between the C++ constructor and `Create()` — `Hide()` to build invisibly, `Enable(false)`,
`SetExtraStyle()` for create-time extra styles (`interface/wx/window.h:419-428, 3120-3126`; `interface/wx/dialog.h:127-133`).
```cpp
auto* panel = new wxPanel(); // C++ object only
panel->Hide(); // legal before Create
panel->Create(parent, wxID_ANY); // native window, still hidden
/* build children */ panel->Show();
```
**[source]** `Destroy()` on a never-created window skips `wxEVT_DESTROY` (`src/common/wincmn.cpp:559-573`),
and a never-created top-level window is deleted immediately (`src/common/toplvcmn.cpp:102-112`; not on
macOS, whose `Destroy()` always defers, §2 table).
Orca's widgets (`StaticBox`, `Button`, `TextInput`, `SpinInput`) follow the layering:
default constructor + `Create(...)`, the convenience constructor delegates to both, and each
`Create` calls its base `Create` first, then attaches `state_handler`. `ComboBox` has only the
convenience constructor, which default-constructs its `TextInput` base and calls `TextInput::Create`.
Subclasses keep that layering.
**Reparent.** `Reparent(newParent)` moves the window between children lists (and between
`wxTopLevelWindows` and a parent); "you need to explicitly call wxNotebook::RemovePage() before
reparenting a notebook page" (`interface/wx/window.h:730-742`). **[source]** It does not touch
sizers (`src/common/wincmn.cpp:1339-1377`); GTK hides the widget and re-shows it on idle if the new parent is
visible (`src/gtk/window.cpp:5195-5230`); MSW just calls `::SetParent`.
**Platforms.**
- MSW fixes a dialog's native owner at `Create` time (`wxTopLevelWindowMSW::CreateDialog` →
`GetParentForModalDialog`, `src/msw/toplevel.cpp:337-352`). A dialog created while its parent is
not on screen (e.g. inside the parent frame's constructor) is owned by the active window, the main
top window, or nothing — wrong z-order and taskbar grouping.
- GTK sets `transient_for` for modal dialogs in `ShowModal()` (`src/gtk/dialog.cpp:139-144`), and for
dialogs/float-on-parent/stay-on-top frames with a top-level parent in `Create`
(`src/gtk/toplevel.cpp:774-782`).
- Wayland: the compositor places top-level windows (see §4); dialog placement comes from
`transient_for`, so a real parent is the only positioning control there is.
**OrcaSlicer.** Dialogs take `parent ? parent : wxGetApp().mainframe` — never orphan a dialog (the
`MsgDialog` constructor does this substitution itself). Passing a child panel as parent is legal:
the dialog dies with that panel (§2) and `CenterOnParent()` centres on the panel's screen rect
(`wxTopLevelWindowBase::DoCentre`, `src/common/toplvcmn.cpp:239-279`); with no wx parent it centres on the display.
**Pitfalls.**
- **Rule:** Never construct a window — especially one hosting a `wxWebView` — just to call one of
its computation methods; make the computation `static` or a free function.
**Why:** window construction has heavy native side effects (native handles, webview processes,
event bindings). `is_flush_config_modified()` built a full `WipingDialog` (with a WebView) only to
call `CalcFlushingVolumes()`; it runs on the UI-rebuild path (`Sidebar::msw_rescale`,
`Sidebar::sys_color_changed`), and on macOS the language switch froze the app.
```cpp
// Wrong
WipingDialog dlg(wxGetApp().mainframe, extra_flush_volumes);
auto m = dlg.CalcFlushingVolumes(i);
// Right
static VolumeMatrix CalcFlushingVolumes(int extruder_id); // no window needed
auto m = WipingDialog::CalcFlushingVolumes(i);
```
Cite: 80f4a7a40f (`WipingDialog::CalcFlushingVolumes`/`CalcFlushingVolume` in `src/slic3r/GUI/WipeTowerDialog.hpp`).
- **Rule:** Create a dialog lazily, after its parent is on screen, when MSW ownership matters.
**Why:** the native owner is chosen at `Create` and a hidden parent is rejected (above).
- **Rule:** `Reparent()` does not move sizer membership.
```cpp
// Wrong: w->Reparent(p);
// Right:
old_sizer->Detach(w); w->Reparent(p); new_sizer->Add(w, 0, wxEXPAND); p->Layout();
```
- **Rule:** Call `Create()` exactly once; never on an object built with the non-default constructor.
---
## 2 Destroying windows: Destroy, delete, Close and the close event
**Contract.**
- `Destroy()`: "Use this function instead of the delete operator, since different window classes can
be destroyed differently. Frames and dialogs are not destroyed immediately when this function is
called -- they are added to a list of windows to be deleted on idle time, when all the window's
events have been processed. This prevents problems with events being sent to non-existent windows."
Returns true "if the window has either been successfully deleted, or it has been added to the list of
windows pending real deletion" (`interface/wx/window.h:3607-3618`).
- Destructor: "Deletes all sub-windows, then deletes itself. Instead of using the delete operator
explicitly, you should normally use Destroy() so that wxWidgets can delete a window only when it is
safe to do so, in idle time" (`interface/wx/window.h:389-394`). `DestroyChildren()` is called automatically by the
destructor (`interface/wx/window.h:626`).
- "Windows with parents, such as controls, don't have delayed destruction… For consistency, continue
to use the wxWindow::Destroy function instead of the delete operator when deleting these kinds of
windows explicitly" (`docs/doxygen/overviews/windowdeletion.h:103-109`).
- `Close(force)` "simply generates a wxCloseEvent whose handler usually tries to close the window. It
doesn't close the window itself"; `force=true` means the handler cannot veto; returns "true if the
event was handled and not vetoed" — a handler that hides instead of destroying still makes it return
true. "Calling Close does not guarantee that the window will be destroyed… To guarantee that the
window will be destroyed, call wxWindow::Destroy instead" (`interface/wx/window.h:3574-3605`).
- Close handler: "If [CanVeto()] is false, you *must* destroy the window using wxWindow::Destroy… If you
don't destroy the window, you should call wxCloseEvent::Veto" (`interface/wx/event.h:4708-4717`).
"The wxCloseEvent handler should only call wxWindow::Destroy to delete the window, and not use the
delete operator" (`docs/doxygen/overviews/windowdeletion.h:38-42`).
- Defaults (`docs/doxygen/overviews/windowdeletion.h:63-73`): wxDialog's close handler simulates `wxID_CANCEL`; the cancel
handler hides a modeless dialog or `EndModal(wxID_CANCEL)`s a modal one — "the dialog *is not*
destroyed (it might have been created on the stack)". wxFrame's default close handler calls `Destroy()`.
**[source]** The top-level default (`wxTopLevelWindowBase::OnCloseWindow`, `src/common/toplvcmn.cpp:535-546`)
**vetoes** a vetoable close while `wxModalDialogHook::GetOpenCount() > 0` (any app-modal wx or native
dialog open), otherwise calls `Destroy()`.
- Deleting from inside a handler: "it may be unsafe for an event handler to delete the object which
generated the event because more events may be still pending for the same object. In this case the
handler may call ScheduleForDestruction() instead" (`interface/wx/app.h:191-212`); without an event
loop it deletes immediately.
**When does `Destroy()` actually free the window?** **[source]**
| Window | `Destroy()` does | Cite |
|---|---|---|
| Child (control, panel) | sends `wxEVT_DESTROY`, then `delete this` **synchronously**; the window detaches from its containing sizer in `~wxWindowBase` | `src/common/wincmn.cpp:559-573` |
| Top-level, normal case | appends to `wxPendingDelete`, hides unless it is the last visible top-level window; deleted by `DeletePendingObjects()` at the next idle — which also runs inside full yields and modal loops (`wxYield`, `ShowModal`), not inside a masked `YieldFor` (progress-dialog `Update`) | `src/common/toplvcmn.cpp:102-142`; `src/common/appbase.cpp:643-662` |
| Top-level, parent already being deleted, or never created (not macOS) | deleted immediately | `src/common/toplvcmn.cpp:102-112` |
| Top-level, macOS | always deferred and always `Hide()`n | `src/osx/toplevel_osx.cpp:96-105` |
| Top-level, MSW | base behaviour plus `wxWakeUpIdle()` so iconized windows still get deleted | `src/msw/toplevel.cpp:771-784` |
| Any child of a dying parent, top-level children included | `DestroyChildren` calls the non-virtual `wxWindowBase::Destroy()`: deleted **immediately**, not queued | `src/common/wincmn.cpp:586-607` |
| `wxPopupTransientWindow` | deferred to idle (not hidden); a second `Destroy()` fails a `wxCHECK` — see `references/popups-menus.md` | `src/common/popupcmn.cpp` `wxPopupTransientWindowBase::Destroy` |
**Close-handler shapes** (pick one per window, never mix; `confirm_discard`/`cleanup` stand for your code):
```cpp
// destroy-on-close (a frame, or a modeless dialog the user owns)
Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& e) {
if (e.CanVeto() && !confirm_discard()) { e.Veto(); return; }
cleanup(); // members still alive here
e.Skip(); // frame: default Destroy() (vetoed while a modal is open, Rule 4)
// modeless dialog: call Destroy() instead -- its default only EndDialog(wxID_CANCEL)s = hides it
});
// hide-on-close (a reusable window someone else owns) — TextureProjectorFrame's shape
Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& e) {
if (e.CanVeto()) { e.Veto(); Hide(); } else e.Skip(); // a forced close must still destroy
});
// usable both modal and modeless — WebDialog::on_close_window's shape
void on_close(wxCloseEvent&) { if (IsModal()) EndModal(wxID_CANCEL); else Destroy(); }
```
A dialog's `e.Skip()` reaches `wxDialogBase::OnCloseWindow`, which **ends** the dialog
(`EndDialog(wxID_CANCEL)` = `EndModal` or `Hide`) but never destroys it: a modeless dialog closed
with the close box is only hidden (`src/common/dlgcmn.cpp:525-559`).
**Application exit.** The app exits when the last top-level window is destroyed
(`docs/doxygen/overviews/windowdeletion.h:89-93`; `SetExitOnFrameDelete(false)` disables it, `interface/wx/app.h:1195-1207`). "By default,
the application stays alive as long as there are any open top level windows" — hidden ones count;
override `ShouldPreventAppExit()` to return false for unimportant windows (`interface/wx/toplevel.h:650-657`).
**[source]** `IsLastBeforeExit` runs from `~wxTopLevelWindowBase` (`src/common/toplvcmn.cpp:93-97, 144-189`);
`GetTopWindow()` skips windows pending delete (`src/common/appcmn.cpp:185-208`).
**OrcaSlicer — shapes to follow.**
- *Modeless singleton owned by `GUI_App`* (`GUI_App::open_terminal_dialog`, `GUI_App::open_speed_dial`):
```cpp
if (!m_dlg) {
m_dlg = new TerminalDialog(mainframe, wxID_ANY, _L("Plugin Terminal"));
m_dlg->Bind(wxEVT_DESTROY, [this](wxWindowDestroyEvent& e) {
if (e.GetEventObject() == m_dlg) m_dlg = nullptr; // the event also arrives from children
e.Skip();
});
}
if (!m_dlg->IsShown()) m_dlg->Show();
m_dlg->Raise();
```
These dialogs bind no close handler, so the close box only hides them (the wx default for a modeless
dialog, below) and the reopen path re-shows the same window; they die with their parent `mainframe`
(or an explicit `Destroy()`, as `GUI_App::recreate_GUI` does for the speed dial), and the
`wxEVT_DESTROY` reset clears the pointer then. A reset that does not compare `GetEventObject()` clears
the pointer when any child of the dialog is destroyed. `open_terminal_dialog` also wraps all of this in
`CallAfter` because it is reached from a webview script message (§6).
- *Dual-mode dialog* (`WebDialog`): close handler `IsModal() ? EndModal(wxID_CANCEL) : Destroy()`;
forced teardown (`WebDialog::destroy_silently`) calls `EndModal` first, then `Destroy()`, because
destroying a modal "can leave ShowModal() running". Registry cleanup runs in `~WebDialog`, explicitly
not in a `wxEVT_DESTROY` handler (comment there: the event comes from the base `~wxDialog`, after the
members died).
- *Hide-on-close reusable frame* (`TextureProjectorFrame`): veto + `Hide()` when `CanVeto()`, else
`Skip()`. The owner (the texture gizmo) holds the pointer; it is parented to `mainframe` because
`wxFRAME_FLOAT_ON_PARENT` needs a real top-level parent.
- *Pseudo-modal modeless dialog* (`ParamsDialog`): see §6 "Modality helpers".
- *By-value top-level member*: `MainFrame::m_settings_dialog` is a `SettingsDialog` (a `DPIDialog`)
stored by value with a NULL parent; its close handler only `Hide()`s. Such a window must never be
`Destroy()`ed or `delete`d — it dies in member destruction, before the frame's base destructors.
Prefer a heap child for new code.
- *Main-frame replacement* (`GUI_App::recreate_GUI`): `mainframe->shutdown()`, create the new
`MainFrame`, `SetTopWindow(new_frame)`, then `old->Destroy()` — new frame first, so wx never sees
"last top-level window gone" and exits.
- *Main-frame close* (`MainFrame` constructor's `wxEVT_CLOSE_WINDOW` lambda): every prompt and veto is
gated by `event.CanVeto()` (gizmo editing, `Plater::close_with_confirm`, print-host queue); then
`set_closing(true)`, `m_plater->reset()`, `shutdown()`, `event.Skip()` → default `Destroy()`. Cmd+Q on
macOS posts a vetoable close (`wxPostEvent(this, wxCloseEvent(wxEVT_CLOSE_WINDOW))`); updater paths use
`Close(true)` to force. Its teardown runs before `Skip()`, so a vetoable close that arrives while a
modal is open (the `wxEVT_QUERY_END_SESSION` handler in `GUI_App::on_init_inner` sends exactly that) is
vetoed by the default handler after teardown — the hazard of Rule 4; don't copy that ordering into new
handlers. Sequence detail: `references/orca-architecture.md`.
- *Lifetime-tied cleanup*: `GUI_App::recreate_GUI` attaches a `wxClientData` subclass with
`SetClientObject`; its destructor runs in `~wxEvtHandler`, after all children are gone.
- *Lazily built window* (`Lazy<T>` / `LazyInstance<T>` in `Lazy.hpp`, e.g. `MainFrame::m_diff_dialog`, a
`Lazy<DiffPresetDialog>`): the holder builds the window on demand or at idle, but the window's parent
owns it; code that only needs an already-built instance asks `T::if_built()` instead of forcing a
build. Design: `docs/HLSD/deferred-page-construction.md`, `references/orca-architecture.md`.
**Pitfalls.**
- **Rule:** Destroy heap-allocated top-level windows with `Destroy()`, never `delete`; stack-allocated
modal dialogs are destroyed by scope.
**Why:** `delete` skips the pending-delete queue, and for a top-level window `wxEVT_DESTROY` is then
sent only from the base destructor. `Destroy()` *delays* deletion; it does not stop events: queued
events and `CallAfter`s of a window sitting in `wxPendingDelete` still run (§3), so deferred code
still needs liveness checks.
```cpp
// Wrong: auto* dlg = new MyDialog(this); dlg->ShowModal(); delete dlg;
// Right: auto* dlg = new MyDialog(this); dlg->ShowModal(); dlg->Destroy();
// Right: MyDialog dlg(this); dlg.ShowModal();
```
Cite: 0a0d59b76b (`detail::run_off_thread_with_progress` in `src/slic3r/GUI/PluginsDialog.hpp`: the
worker body in `try/catch`, then one main-thread `CallAfter` that does `timer->Stop(); delete timer;`
and `progress->Destroy()` only while the host's alive flag is set, or when the caller passed no flag —
the progress dialog is a child of the host and has already died with it otherwise, §2 table). Older code that `delete`s a dialog
after `ShowModal()` is legacy; don't extend it.
- **Rule:** Never `Destroy()` a control (or its ancestor panel) from inside that control's own handler.
```cpp
// Wrong: m_btn->Bind(wxEVT_BUTTON, [this](auto&) { m_panel->Destroy(); }); // m_btn is inside m_panel
// Right:
m_btn->Bind(wxEVT_BUTTON, [this](auto&) { wxTheApp->ScheduleForDestruction(m_panel); m_panel = nullptr; });
```
**Why:** child `Destroy()` deletes synchronously; the dispatcher then returns into freed memory.
- **Rule:** A close handler that neither destroys nor vetoes, or that vetoes without checking
`CanVeto()`, is wrong.
**Why:** `Close(true)`, session end and the default top-level handler rely on a non-vetoable close
destroying the window; a hide-only handler leaks the window, and a parentless leaked window keeps the
process alive after the main frame is gone. Parent it to `mainframe`, `Destroy()` it, or override
`ShouldPreventAppExit()`.
- **Rule:** Do irreversible teardown only when the close is certain.
**Why:** a handler that tears down and then `Skip()`s can still be vetoed by
`wxTopLevelWindowBase::OnCloseWindow` when the close is vetoable and any modal dialog is open (for
example a vetoable close sent while a dialog is up, as the `wxEVT_QUERY_END_SESSION` path does): the
window survives, half torn down.
```cpp
// Wrong
Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& e) { shutdown(); e.Skip(); });
// Right
Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& e) {
if (e.CanVeto() && wxModalDialogHook::GetOpenCount() > 0) { e.Veto(); return; }
shutdown(); e.Skip();
});
```
- **Rule:** A `wxTimer` must not outlive the handler that receives its events — make it a member
(`wxTimer m_timer{this}`) or delete it before the owner dies. Detail in `references/threads-timers-app.md` §wxTimer.
---
## 3 Liveness: is this window still alive?
**`IsBeingDeleted()`.** Doc: true "if this window, or one of its parent windows, is scheduled for
destruction and can be useful to avoid manipulating it as it's usually useless to do something with a
window which is at the point of disappearing anyhow" (`interface/wx/window.h:3620-3633`). **[source]** Wrong for top-level windows: the flag is
set only by `SendDestroyEvent()`, i.e. when deletion actually starts (`src/common/wincmn.cpp:541-557`);
`wxTopLevelWindowBase::Destroy()` only queues and hides, so **after `tlw->Destroy()` returns,
`tlw->IsBeingDeleted()` is false until idle**. The parent walk also stops at a top-level window
(`m_isBeingDeleted || (!IsTopLevel() && m_parent->IsBeingDeleted())`, `src/common/wincmn.cpp:535-539`): a dialog
does not report its parent frame's deletion.
**`wxApp::IsScheduledForDestruction(obj)` / `ScheduleForDestruction(obj)`** (`interface/wx/app.h:191-221`):
the first answers "has `Destroy()` (or `ScheduleForDestruction`) already been called"; both share
`wxPendingDelete` (`src/common/appbase.cpp:624-641`). `ScheduleForDestruction` defers deletion of *any*
`wxObject`, children included; **[source]** it deletes with plain `delete` at idle (`src/common/appbase.cpp:643-662`).
| After… | `IsBeingDeleted()` | `IsScheduledForDestruction()` | `wxWeakRef` |
|---|---|---|---|
| `child->Destroy()` returned | object gone | object gone | null |
| `tlw->Destroy()` returned, before idle | **false** | true | **non-null** |
| inside `wxEVT_DESTROY` handlers and the wx base destructors | true | false (already removed) | **non-null** until `~wxTrackable` |
| inside a top-level window's own derived destructor (deleted at idle or by `delete`) | **false** — `SendDestroyEvent()` runs later, in `~wxFrameBase` or the port's top-level/dialog destructor (`src/common/framecmn.cpp:200`, `src/msw/toplevel.cpp:522`, `src/gtk/toplevel.cpp:975`, `src/osx/dialog_osx.cpp:90`) | false | non-null |
**`wxWeakRef<T>`** auto-resets "when the object pointed is destroyed"; works for `wxEvtHandler`/`wxWindow`
(`interface/wx/weakref.h:40-99`). **[source]** The reset happens in `~wxTrackable`, the last base
destructor (`include/wx/tracker.h`) — non-null throughout the destroy sequence and while a top-level window
sits in the pending list. The tracker list is unlocked: main thread only. Orca uses it for the splash
(`wxWeakRef<SplashScreen> scrn` in `GUI_App::on_init_inner`), `g_delay_webviews` (`Widgets/WebView.cpp`),
`DockPanel`, `GuideFrame`.
**`wxWindowPtr<T>`** is a shared pointer that calls `Destroy()` at refcount 0
(`interface/wx/windowptr.h:10-26`). It does not track: if the window dies another way (parent deletion,
default frame close) the last release calls `Destroy()` on freed memory. Use it only for parentless,
self-owned top-level windows — the documented `ShowWindowModalThenDo` idiom (§6).
**`wxEVT_DESTROY`** (`wxWindowDestroyEvent`, `interface/wx/event.h:4525-4553`): for top-level windows it
is sent "by wxFrame or wxDialog destructor, i.e. after the destructor of the derived class was executed";
for children "just before deleting the window from wxWindow::Destroy()… or from the window destructor if
operator delete was used directly". **[source]** It derives from `wxCommandEvent` and **propagates to the
parent** unless the parent is being deleted (`wxWindowBase::TryAfter`, `src/common/wincmn.cpp:3499-3522`), so a
parent's handler fires for every descendant destroyed before it. Derived-class code that must run at
destruction goes in the derived destructor (or call `SendDestroyEvent()` there).
**Pending events and nested loops.** **[source]** `ProcessPendingEvents` runs queued events and
`CallAfter`s of a top-level window that is already in `wxPendingDelete` (`src/common/appbase.cpp:561-601`); only the
actual deletion discards them (`~wxEvtHandler` → `DeletePendingEvents`, `src/common/event.cpp`). Idle
events are skipped for pending-delete windows (`src/common/appcmn.cpp:405-428`). "Deferred" does not mean "after the
current handler returns": `ShowModal()`, `wxYield()` and `wxSafeYield()` run pending events *and*
`DeletePendingObjects()` (`src/common/evtloopcmn.cpp:172-192`), so a raw pointer to a `Destroy()`ed
window dies across any of them, and a lambda queued before a modal opens can run while the caller is
still inside `ShowModal()`. A masked `YieldFor` — `wxProgressDialog::Update`/`Pulse` — runs no idle pass
and only the queued events its categories allow (none on GTK): `references/threads-timers-app.md`
§Event categories and yields. `CallAfter` liveness mechanics (self-queued calls are dropped
with their handler; `wxGetApp().CallAfter` calls are not): `references/events.md` §CallAfter. Yields:
`references/threads-timers-app.md`.
**OrcaSlicer.**
- Alive flag for anything that may run after the window died, including worker threads:
`std::shared_ptr<std::atomic<bool>> m_alive = std::make_shared<std::atomic<bool>>(true);`, set false in the
destructor (`PluginsDialog::~PluginsDialog`), captured by value, checked inside the lambda. Unlike
`wxWeakRef` it flips at the *start* of the derived destructor and is thread-safe.
- App shutdown gate: `wxGetApp().is_closing()`; background-to-UI callbacks check it before posting and
again inside the `CallAfter` lambda (`references/threads-timers-app.md`).
**Pitfalls.**
- **Rule:** For a top-level window that something may have `Destroy()`ed, test both flags.
```cpp
// Wrong: if (!tlw->IsBeingDeleted()) use(tlw);
// Right: if (!wxTheApp->IsScheduledForDestruction(tlw) && !tlw->IsBeingDeleted()) use(tlw);
```
- **Rule:** Don't trust `wxWeakRef` inside teardown code; combine it with `IsBeingDeleted()` /
`IsScheduledForDestruction()` or an alive flag.
- **Rule:** In a `wxEVT_DESTROY` handler bound on a parent, compare the event object and `Skip()`.
```cpp
// Wrong: parent->Bind(wxEVT_DESTROY, [this](auto&) { m_child = nullptr; });
// Right:
parent->Bind(wxEVT_DESTROY, [this](wxWindowDestroyEvent& e) {
if (e.GetEventObject() == m_child) m_child = nullptr;
e.Skip();
});
```
- **Rule:** After `ShowModal()` returns, re-validate anything the modal loop could have destroyed (a
popup, a panel rebuilt on a language or preset change) — hold a `wxWeakRef` or alive flag and re-check.
- **Rule:** Make accessors that are reachable from teardown-time events null-safe: guard the pimpl or
child pointer and return `nullptr` instead of dereferencing.
**Why:** on macOS, close and shutdown still deliver events (render, idle, focus) that call back into
widget accessors after internals are gone; `Plater::get_view3D_canvas3D()` crashed on app close until it
checked its pimpl. `Plater::~Plater() = default` destroys `std::unique_ptr<priv> p` *before* the base
`wxWindow` destructor runs `DestroyChildren()` (`src/osx/window_osx.cpp:248`, `src/gtk/window.cpp:3105`,
`src/msw/window.cpp:421`; `src/common/wincmn.cpp:586-609`), so events raised while children die see a dead pimpl.
The null check works only because libc++'s `~unique_ptr` resets before deleting; the standard does not
require that, and touching a member after its destructor ran is UB. Treat the guard as a last line of
defence; the real fix is to stop the events (unbind, `is_closing()`) before teardown.
```cpp
return p->view3D->get_canvas3d(); // Wrong: crash on close
return p ? p->view3D->get_canvas3d() : nullptr; // Right
```
Cite: a162e3f031 (`Plater::get_view3D_canvas3D` in `src/slic3r/GUI/Plater.cpp`).
---
## 4 Top-level windows: show, raise, enable, state, geometry
**Show / Hide.** `Show()` returns false when nothing changed (`interface/wx/window.h:3140-3158`).
`IsShownOnScreen()` = shown and every parent up to the top-level window shown (`interface/wx/window.h:3100-3106`).
**[source]** GTK3: `wxTopLevelWindowGTK::Show` calls `GTKSendSizeEventIfNeeded()` even when nothing
changes, so a redundant `Show(true)` can flush a pending size event into layout handlers
(`src/gtk/toplevel.cpp:1258-1268`). X11 (GTK2/GTK3 without client-side decorations): the first `Show()`
may be deferred until `_NET_FRAME_EXTENTS` arrives — `IsShown()` is already true but the window is not
mapped (`src/gtk/toplevel.cpp:1141-1243`).
**Raise.** "only requests the window manager to raise this window… If the window is currently hidden,
this function does *not* show it", top-level windows only (`interface/wx/window.h:3013-3033`); true on all ports
since 3.3 (`docs/changes.txt:144-146`). **[source]** MSW = `::SetForegroundWindow`, subject to the
foreground lock — Windows may only flash the taskbar button (`src/msw/toplevel.cpp:650-655`); GTK =
`gtk_window_present` only if shown (`src/gtk/toplevel.cpp:1301-1310`; during a deferred X11 first show it
already counts as shown); macOS = `makeKeyAndOrderFront` only if shown (`src/osx/nonownedwnd_osx.cpp:289-295`, `src/osx/cocoa/nonownedwnd.mm:897-899`),
which also makes a `wxPopupWindow` the key window — never `Raise()` a popup (`references/popups-menus.md` §5, §10).
**Enable.** `Enable(false)` on a parent disables children logically: `IsEnabled()` reflects ancestors,
`IsThisEnabled()` the window's own flag (`interface/wx/window.h:3060-3070, 3116-3138`). **[source]** On MSW/macOS wx
propagates through `NotifyWindowOnEnableChange` → `DoEnable` on non-top-level children — not through the
virtual `Enable()` — and skips children entirely when a top-level window is disabled, so a modal dialog
does not grey the frame (`src/common/wincmn.cpp:1150-1191`); GTK relies on native sensitivity. Orca widgets update
their painted state only from their own `Enable()` override, so disabling an ancestor leaves them
looking enabled: `references/orca-widgets.md`.
**State and geometry** (`interface/wx/toplevel.h`):
| API | Contract / platform note |
|---|---|
| `Iconize()`, `Maximize()` | on wxGTK "the change… is not immediate" (`:260-274, 335-346`); **[source]** MSW on a hidden window only records the state for the next show (`src/msw/toplevel.cpp:661-735`) |
| `Restore()` | on wxGTK does not unmaximize — call `Maximize(false)` (`:394-404`) |
| `ShowFullScreen(show, style)` | also shows a hidden window (`:730-750`) |
| `EnableFullScreenView()` | wxOSX only; then `ShowFullScreen` uses the native Spaces API and only `wxFULLSCREEN_NOTOOLBAR|NOMENUBAR` apply (`:697-728`); `wxEVT_FULLSCREEN` is macOS-only, only with it, and not generated by `ShowFullScreen()` (`interface/wx/event.h:2391-2412`) |
| `RequestUserAttention()` | documented for Win32 (taskbar flash) and wxGTK (`:375-392`); **[source]** macOS bounces the dock icon (`src/osx/cocoa/nonownedwnd.mm:1306`) |
| `SetIcon/SetIcons` | MSW needs a 16×16 or 32×32 icon; no effect on Wayland — ship a `.desktop` file (`:506-546`); **[source]** no macOS override: stored, never shown |
| `SetSizeHints/SetMinSize/SetMaxSize` | on a top-level window they also constrain programmatic `SetSize()` (`:576-603`) |
| `SetTransparent()` | on wxGTK call it before the first show (`:632-648`) |
| `EnableMaximizeButton/EnableMinimizeButton` | MSW and macOS only (`:172-203`) |
| `EnableCloseButton` | all ports, but its result is unreliable on X11, GTK included (`:160-170`) |
| `wxEVT_MOVE_START/END` | wxMSW only (`:80-87`) |
| `wxEVT_SHOW` | not sent for iconize/restore on MSW (`interface/wx/event.h:4905-4914`) |
| `SaveGeometry/RestoreToGeometry`, `wxPersistentTLW` | serializer-based persistence (`:406-492`; `interface/wx/persist/toplevel.h`) — not used by Orca |
**Wayland.** `SetIcon(s)` do nothing (`interface/wx/toplevel.h:518-521, 539-542`); the app id comes from
`SetClassName` with GTK ≥ 3.24.22 (`interface/wx/app.h:765-771`). `SetPosition()`/`Move()` on a
top-level window is a no-op (the compositor places windows), so `CentreOnParent` and saved positions are
ignored (observed; recorded in `GUI_App::window_pos_restore`, not documented by wx). Detection: `Slic3r::GUI::is_running_on_wayland()`; see `references/platforms.md`.
**OrcaSlicer geometry persistence.** Orca uses `AppConfig`, not `wxPersistentTLW`:
`GUI_App::window_pos_save` / `window_pos_restore` / `window_pos_sanitize` / `window_pos_center` (key
`window_<name>`, a `WindowMetrics` = screen rect + maximized; restore skips `SetPosition` on Wayland), and
`on_window_geometry(tlw, callback)` (`GUI_Utils.cpp`) to run the callback when geometry is real — MSW
immediately (no `wxEVT_SHOW` for windows created maximized), Linux on `wxEVT_SHOW` + `CallAfter`, macOS on
`wxEVT_SHOW`. `GUI_App::persist_window_geometry(window, default_maximized)` saves on
`wxEVT_CLOSE_WINDOW` (then `Skip()`) but always uses the key `window_mainframe`, whatever the window's
name — for any other window call `window_pos_save/restore` with its own name.
**Pitfalls.**
- **Rule:** When restoring a top-level window (the main frame after hiding a popup or overlay frame, or a
singleton dialog being re-opened), call `Show()` only if `!IsShown()`, and call `Raise()` unconditionally.
**Why:** `Raise()` never shows a hidden window (3.3 on all ports); on GTK3 a redundant `Show(true)` runs
`GTKSendSizeEventIfNeeded()`, flushing a pending size event into layout handlers — in Orca this froze the
app permanently after hiding the filament-sync popup.
```cpp
// Wrong
mainframe->Show(); mainframe->Raise();
// Right
if (!mainframe->IsShown()) mainframe->Show();
mainframe->Raise();
```
Cite: dd8cb89f6d (`BaseTransparentDPIFrame::on_hide`); the same guard in `GUI_App::open_terminal_dialog`.
- **Rule:** On macOS, after any native modal (`wxFileDialog`/`wxDirDialog`, `wxMessageBox`/`wxMessageDialog`)
or a generic `wxProgressDialog` opened from a secondary top-level dialog, re-raise that dialog with a
deferred, liveness-guarded `Raise()` (`CallAfter` + alive flag + `IsShown()`).
**Why:** when the native panel closes, macOS re-activates the app's main window instead of the dialog
that opened it, burying the dialog behind the main frame (observed; not documented by wx). wx compensates
only for its own `wxDialog` modals — `EndModal` raises the parent (`src/osx/dialog_osx.cpp:191-202`) —
while `wxFileDialog::ShowModal` runs `[panel runModal]` with no such step (`src/osx/cocoa/filedlg.mm:597-620`),
and on macOS `wxProgressDialog` is the generic dialog (only MSW has a native one,
`include/wx/progdlg.h:30-37`), normally shown modeless behind a disabler and destroyed, never ended
through `EndModal`. The `Raise()` is deferred to run after the modal has fully torn down, and guarded
because the dialog may be destroyed while queued.
```cpp
void PluginsDialog::restore_z_order()
{
wxGetApp().CallAfter([this, alive = m_alive]() {
if (alive->load(std::memory_order_acquire) && IsShown())
Raise();
});
}
```
Cite: 0a0d59b76b (`PluginsDialog::restore_z_order`; also passed as the `restore` callback of
`detail::run_off_thread_with_progress`).
- **Rule:** Never position windows by absolute coordinates on Wayland, and never expect `SetIcon` to show on
macOS or Wayland.
---
## 5 Window styles
**Contract.**
- `wxDEFAULT_FRAME_STYLE` = `wxSYSTEM_MENU | wxRESIZE_BORDER | wxMINIMIZE_BOX | wxMAXIMIZE_BOX |
wxCLOSE_BOX | wxCAPTION | wxCLIP_CHILDREN` (`interface/wx/toplevel.h:55-61`); `wxDEFAULT_DIALOG_STYLE`
= `wxCAPTION | wxSYSTEM_MENU | wxCLOSE_BOX`, `wxSYSTEM_MENU` unused under Unix (`interface/wx/dialog.h:20, 99-101`).
Non-resizable frame: `wxDEFAULT_FRAME_STYLE & ~(wxRESIZE_BORDER | wxMAXIMIZE_BOX)`.
- `wxMINIMIZE_BOX`, `wxMAXIMIZE_BOX`, `wxCLOSE_BOX` implicitly enable `wxCAPTION` "on most systems"
(`interface/wx/dialog.h:94-114`, `interface/wx/frame.h:61-80`). `wxMAXIMIZE_BOX` is ignored on wxGTK without
`wxRESIZE_BORDER` (`interface/wx/frame.h:75-78`). The `wxMAXIMIZE` style works on Windows and GTK only; `wxICONIZE`/
`wxMINIMIZE` on Windows only (`interface/wx/frame.h:59-74`).
- `wxSTAY_ON_TOP`: above all other windows. `wxFRAME_FLOAT_ON_PARENT`: above its parent only, "must have a
non-null parent". `wxFRAME_NO_TASKBAR`: no taskbar button on Windows/GTK (GTK only with
`_NET_WM_STATE_SKIP_TASKBAR` support). `wxFRAME_TOOL_WINDOW`: small title bar, no taskbar button
(`interface/wx/frame.h:82-106`).
- Borders: `wxBORDER_NONE/SIMPLE/SUNKEN/RAISED/STATIC(MSW)/THEME`; `wxTRANSPARENT_WINDOW` is obsolete and
does nothing (`interface/wx/window.h:191-220`).
- Extra styles (`SetExtraStyle`, some must precede two-step `Create`): `wxWS_EX_BLOCK_EVENTS`,
`wxWS_EX_TRANSIENT` ("Don't use this window as an implicit parent… risk of creating a dialog/frame with
this window as a parent, which would lead to a crash"), `wxWS_EX_PROCESS_IDLE`,
`wxWS_EX_PROCESS_UI_UPDATES` (`interface/wx/window.h:262-290`). Dialogs set `wxWS_EX_BLOCK_EVENTS` by default
(`src/common/dlgcmn.cpp:124-127`): command events from inside a dialog never reach its parent frame;
frames do not block.
**Platforms. [source]**
| Style | MSW | macOS | GTK |
|---|---|---|---|
| MIN/MAX/CLOSE_BOX | force `WS_CAPTION` (`src/msw/toplevel.cpp:132-137`) — a custom title bar must strip it | — | — |
| frame with a parent, no `FLOAT_ON_PARENT` | unowned; gets its own taskbar button (`WS_EX_APPWINDOW`) unless `wxFRAME_NO_TASKBAR` (`src/msw/toplevel.cpp:193-244`) | — | — |
| `wxFRAME_FLOAT_ON_PARENT` | native owner = `GetHwndOf(parent)` as given (`MSWGetParent`, `src/msw/toplevel.cpp:212-240`) — pass a top-level window | `NSFloatingWindowLevel`, a *global* level: floats above other apps' windows too (`src/osx/cocoa/nonownedwnd.mm:835-876`) | `transient_for` the parent's top-level window, set only in `Create` (`src/gtk/toplevel.cpp:774-782`) |
| `wxFRAME_TOOL_WINDOW` | small caption, no taskbar | `NSFloatingWindowLevel` | no taskbar |
| `wxSTAY_ON_TOP` | `WS_EX_TOPMOST` | `NSModalPanelWindowLevel` | keep-above + `transient_for` in `Create` (`src/gtk/toplevel.cpp:774-791`); runtime change honoured |
| runtime `SetWindowStyleFlag` | — | re-levels the window (`src/osx/cocoa/nonownedwnd.mm:1038-1056`) | updates only `STAY_ON_TOP` and `NO_TASKBAR` (`src/gtk/toplevel.cpp:1943-1968`) |
macOS dialogs are not attached as Cocoa child windows (no `addChildWindow`, `src/osx/cocoa/nonownedwnd.mm:947-991`),
so they do not move with their parent; non-tool windows get `setHidesOnDeactivate:NO`.
**OrcaSlicer.**
- `DPIAware`'s constructor defaults `style = wxDEFAULT_FRAME_STYLE` and `name = wxFrameNameStr` for
dialogs too: `DPIDialog(parent, id, title)` without a style is resizable with min/max boxes. Pass
`wxCAPTION | wxCLOSE_BOX` or `wxDEFAULT_DIALOG_STYLE` (§7).
- `MainFrame` uses `BORDERLESS_FRAME_STYLE` (`MainFrame.cpp`: min/max/close boxes, plus `wxRESIZE_BORDER`
off Apple, no `wxCAPTION`) and paints its own title bar: MSW strips the `WS_CAPTION` that wx adds,
macOS calls `set_miniaturizable`, GTK adds `ResizeEdgePanel`s. Custom titlebar rules:
`references/platforms.md`.
- `TextureProjectorFrame` is the model for a tool frame floating above the main window:
`wxCAPTION | wxRESIZE_BORDER | wxCLOSE_BOX | wxFRAME_NO_TASKBAR | wxFRAME_FLOAT_ON_PARENT`, parented to
`mainframe`, `SetTransparent` before the first show.
**Pitfalls.**
- **Rule:** Remove style bits with `& ~flag`; `!flag` is 0.
```cpp
// Wrong: !wxCAPTION | !wxCLOSE_BOX | wxBORDER_NONE // == wxBORDER_NONE by accident
// Right: wxBORDER_NONE // or: wxDEFAULT_FRAME_STYLE & ~wxCAPTION
```
- **Rule:** Parent a `wxFRAME_FLOAT_ON_PARENT` frame to a top-level window, never to a panel or NULL.
**Why:** with NULL wx asserts (silently in Orca) and ignores the flag (`wxTopLevelWindowMSW::MSWGetParent`).
With a panel, the ports disagree on the owner — MSW passes the panel's own HWND (`GetHwndOf(parent)`),
GTK resolves `wxGetTopLevelParent(parent)` (`src/gtk/toplevel.cpp:774-782`) — and the frame is deleted
with the panel (§2). A top-level parent gives every port the same owner and lifetime.
- **Rule:** Don't use `wxSTAY_ON_TOP` or `wxFRAME_FLOAT_ON_PARENT` to keep a tool window "above the app" on
macOS without accepting that it also floats above other applications.
---
## 6 Dialogs and modality
**Contract** (`interface/wx/dialog.h`).
- Stack allocation is the sanctioned form for a modal dialog: "the modal dialog is one of the very few
examples of wxWindow-derived objects which may be created on the stack… no need to call Destroy()";
heap form `ShowModal()` then `dlg->Destroy()` (`interface/wx/dialog.h:61-88`).
- `ShowModal()`: "Program flow does not return until the dialog has been dismissed with EndModal()…
ShowModal() can't be called twice without intervening EndModal() calls… creates a temporary event loop…
also results in a call to wxApp::ProcessPendingEvents()" (`interface/wx/dialog.h:597-618`). Timers, `CallAfter`s,
idle-time deletion, socket and worker events all run inside it.
- `EndModal(retCode)` sets the value `ShowModal()` returns (`interface/wx/dialog.h:340-349`). `Show(false)`: "The preferred
way of dismissing a modal dialog is to use EndModal()" (`interface/wx/dialog.h:586-595`).
- `ShowWindowModal()` is "only fully implemented in wxOSX… under the other platforms it behaves like
ShowModal()" (`interface/wx/dialog.h:620-638`). `ShowWindowModalThenDo(functor)`: the dialog must outlive the functor —
hold it in a `wxWindowPtr` captured by value (`interface/wx/dialog.h:640-678`).
- "you shouldn't show a modal dialog from a mouse click event handler as this would break the mouse capture
state" — defer with `CallAfter` (`interface/wx/event.h:490-497`). **[source]** GTK's `ShowModal` releases
any mouse capture first (`GTKReleaseMouseAndNotify`, `src/gtk/dialog.cpp:137`). Capture rules:
`references/mouse-keyboard-focus.md`.
**[source] facts.**
- `wxDialogBase::EndDialog(rc)` (protected, `src/common/dlgcmn.cpp:361-367`) = `IsModal() ? EndModal(rc) : Hide()`:
ends either kind from inside the class.
- Modal loops nest and unwind LIFO on every port: MSW `wxEventLoopManual::DoStop` only wakes the loop
(`src/common/evtloopcmn.cpp:388-401`), GTK re-enters `gtk_main()` until its own `m_shouldExit`
(`src/gtk/evtloop.cpp:58-90`), macOS keeps a LIFO `s_modalStack` (`src/osx/dialog_osx.cpp:47-60`) and
stops via `[NSApp abortModal]`, which hits the innermost session (`src/osx/cocoa/evtloop.mm:453-456`).
Every port's `EndModal` stops its loop through `wxEventLoopBase::Exit()`, whose
`wxCHECK_RET(IsRunning())` returns silently unless that loop is the active (innermost) one
(`src/common/evtloopcmn.cpp:91-96`; MSW via `Hide()` → `wxDialogModalData::ExitLoop`,
`src/msw/dialog.cpp:64-67, 199-207, 261-268`; macOS `src/osx/dialog_osx.cpp:191-194`; GTK's `EndModal` tests
`IsRunning()` itself, `src/gtk/dialog.cpp:199-202`). Ending a lower dialog first therefore hides it
but never tells its loop to exit: its `ShowModal()` does not return even after every dialog above it
has ended. On MSW its `wxWindowDisabler` (owned by the generic `wxModalEventLoop` that MSW's
`ShowModal` runs, deleted only in its `OnExit()`, `include/wx/evtloop.h:377-396`) stays alive too,
so the other top-level windows remain disabled. GTK uses no disabler: modality is each dialog's own
`gtk_window_set_modal` grab (`src/gtk/dialog.cpp:158`). The same silent `Exit()` no-op applies to
nested `wxEventLoop`s: `references/threads-timers-app.md` §Nested event loops.
- Return codes are ids: `wxID_OK = 5100`, `wxID_CANCEL`, `wxID_APPLY`, `wxID_YES`, `wxID_NO`, …
(`include/wx/defs.h:1847`). `wxYES 0x2`, `wxOK 0x4`, `wxNO 0x8`, `wxCANCEL 0x10`, `wxAPPLY 0x20`,
`wxCLOSE 0x40` are style bits (`include/wx/defs.h:1665-1672`) — `EndModal(wxCANCEL)` makes `ShowModal() == wxID_CANCEL` false.
- Whichever `EndModal` runs last before `ShowModal()` returns sets the result: every port's `EndModal`
calls `SetReturnCode` unconditionally and `ShowModal()` returns `GetReturnCode()`.
**Per-port modal behaviour. [source]**
| | MSW (`src/msw/dialog.cpp:195-268`) | macOS (`src/osx/dialog_osx.cpp:106-202`) | GTK (`src/gtk/dialog.cpp:60-205`) |
|---|---|---|---|
| `Hide()`/`Show(false)` on a modal dialog | exits the loop; `ShowModal()` returns the current code (0 if none set) | **does not exit**: clears the modality, orders the window out; `ShowModal()` stays blocked behind an invisible dialog, and wx's `EndDialog` path then takes the `Hide()` branch | calls the virtual `EndModal(wxID_CANCEL)`, overwriting any code |
| `EndModal()` on a modeless dialog | sets the code and hides (assert compiled out) | sets the code, hides, raises the parent | sets the code, then `wxFAIL` + return: the dialog stays visible |
| `IsModal()` between `EndModal()` and the return of `ShowModal()` | true (`m_modalData`, `include/wx/msw/dialog.h:48`) | false | false |
| `EndModal()` raises the parent | no | yes ("Prevent app frame from taking z-order precedence") | no |
| native owner / transient | fixed at `Create` (§1) | none (not a child window) | `transient_for` set in `ShowModal()` |
| how other windows are blocked | `wxWindowDisabler` in a generic `wxModalEventLoop` (`src/msw/dialog.cpp:70`) | Cocoa modal session (`src/osx/cocoa/evtloop.mm:424-456`) | `gtk_window_set_modal` grab; mouse capture released first |
**Buttons, ESC and the close box. [source]**
- `SetAffirmativeId(id)` (default `wxID_OK`): that button runs `Validate()` + `TransferDataFromWindow()` and
closes with the id (`interface/wx/dialog.h:480-494`). `SetEscapeId(id)`: default `wxID_ANY` = the `wxID_CANCEL`
button if present, else the affirmative one; `wxID_NONE` = ignore ESC; native dialogs cannot be
customized (`interface/wx/dialog.h:496-515`). `CreateStdDialogButtonSizer` makes `wxButton`s and sets the affirmative
id (`interface/wx/dialog.h:291-304`) — Orca uses `DialogButtons` instead (§8).
- Routing is by **id**, not type: `wxDialogBase::OnButton` (static table, `src/common/dlgcmn.cpp:455-480`) handles any
`wxEVT_BUTTON` that propagates to the dialog — affirmative id → `AcceptAndClose()` (`Validate()` +
`TransferDataFromWindow()` then `EndDialog(id)`), `wxID_APPLY` → validate + transfer, no close, escape id
or `wxID_CANCEL` → `EndDialog(wxID_CANCEL)`, anything else skipped. An Orca `Button` with `wxID_OK` or
`wxID_CANCEL` and no handler (or a handler that `Skip()`s) closes the dialog by itself; a handler bound
on the button that does not `Skip()` suppresses it.
- ESC → `wxDialogBase::OnCharHook` → `SendCloseButtonClickEvent()`: tries the escape id (`wxID_CANCEL`), then
the affirmative id, through `EmulateButtonClickIfPresent`, which does
`wxDynamicCast(FindWindow(id), wxButton)` and requires the button enabled and shown
(`src/common/dlgcmn.cpp:387-453`). Orca's `Button` derives from `StaticBox : wxWindow`, so emulation finds nothing:
in a plain `wxDialog` with Orca buttons ESC does nothing; in a `DPIDialog`, `DPIAware`'s own char hook
turns ESC into `Close()` first (§7).
- Close box (and `Close()`) → `wxDialogBase::OnCloseWindow` (`src/common/dlgcmn.cpp:525-559`): if shown, try
`SendCloseButtonClickEvent()`; when that finds no `wxButton`, `EndDialog(wxID_CANCEL)`. The Cancel
button's own handler never runs on this path.
**Modeless dialogs.** `Show()`; the close box only hides them (above). Long-lived modeless windows (monitor
pages, progress dialogs, plugin dialogs) either destroy themselves in a close handler or are deliberately
hide-on-close and reused (§2 shapes).
**Modality helpers.**
- `wxWindowDisabler(winToSkip, winToSkip2)` disables all *shown and enabled* top-level windows except the
skipped ones and re-enables them in its destructor (`interface/wx/utils.h:58-111`). MSW: a skipped window
that appears in the taskbar lets the user close the whole app from the taskbar (`interface/wx/utils.h:93-99`).
**[source]** The destructor re-enables every top-level window not recorded as skipped — including ones
created after the disabler (`src/common/utilscmn.cpp:1527-1546`); on macOS the constructor begins a Cocoa
modal session and **shows** `winToSkip` if it is not on screen (`src/osx/cocoa/evtloop.mm:458-514, 573-584`).
Keep disablers scoped and strictly nested.
- `wxModalDialogHook::GetOpenCount()` (since 3.3.0, `interface/wx/modalhook.h:118-126`) counts every open
modal — generic and native message/file/colour/font/print dialogs all use `WX_HOOK_MODAL_DIALOG`. Use it
for "is any modal open" in new code.
- `wxFrame::SetWindowModality(wxWindowMode)` (new in 3.3.2): call before showing; `AppModal`/`WindowModal`
(`interface/wx/frame.h:318-329`). **[source]** Applies immediately in the call, ends at the first hide, adds
`wxFRAME_NO_TASKBAR` and removes `wxMINIMIZE_BOX` (`src/common/framecmn.cpp:159-195, 304-339`); it does not
block — the caller keeps running.
- Orca's pseudo-modal `ParamsDialog` (filament/printer settings): shown modeless with `Popup()` →
`Show()`, creates a heap `wxWindowDisabler(this)` in its `wxEVT_SHOW` handler and deletes it on hide; its
close handler validates (vetoes when validation fails and `CanVeto()`), hides, and never destroys, so the
hosted tabs stay reusable; `ParamsDialog::Popup()` calls `Reparent(mainframe)` on Windows before showing.
**Pitfalls.**
- **Rule:** Use `wxID_*` codes with `EndModal`.
```cpp
// Wrong: EndModal(wxCANCEL); EndModal(wxCLOSE); EndModal(wxOK);
// Right: EndModal(wxID_CANCEL); EndModal(wxID_CLOSE); EndModal(wxID_OK);
```
- **Rule:** Dismiss a modal dialog with `EndModal(rc)`, not `Hide()`/`Show(false)`.
**Why:** macOS keeps `ShowModal()` blocked behind an invisible dialog; GTK forces `wxID_CANCEL`; MSW
returns whatever code was last set.
- **Rule:** `EndModal()` only on a modal dialog; a modeless one is hidden/closed/destroyed (or `EndDialog`).
**Why:** GTK ignores `EndModal` on a modeless dialog — it stays on screen there and only there.
- **Rule:** Never `Destroy()` a modal dialog whose loop is still running; `EndModal(rc)` first, then
`Destroy()` (heap) — from the caller once `ShowModal()` has returned, or straight after `EndModal()`
as forced teardown does, relying on a top-level `Destroy()` only queuing the deletion. Never `Destroy()`
a stack dialog.
Cite: `WebDialog::destroy_silently` (`EndModal(wxID_CANCEL)` then `Destroy()`).
- **Rule:** Close nested modal dialogs innermost-first.
**Why:** ending a lower one hides it but leaves its `ShowModal()` stranded (and, on MSW, its disabler
alive) on every port — see the LIFO bullet above.
- **Rule:** Show a modal from a mouse handler only through `CallAfter`.
```cpp
// Wrong: m_btn->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent&) { MyDialog dlg(this); dlg.ShowModal(); });
// Right:
m_btn->Bind(wxEVT_LEFT_DOWN, [this](wxMouseEvent& e) {
e.Skip();
CallAfter([this] { MyDialog dlg(this); dlg.ShowModal(); });
});
```
The same applies to `EndModal` bound to `wxEVT_LEFT_DOWN`; bind `wxEVT_BUTTON` on an Orca `Button` instead.
- **Rule:** No window work (create, show, raise, destroy, modal dialogs) on the stack of a
`wxEVT_WEBVIEW_SCRIPT_MESSAGE_RECEIVED` handler — WebKitGTK and WKWebView deliver it synchronously. Detail
and the `WebViewHostDialog` contract: `references/webview-gl-aui-media.md`.
- **Rule:** Scope a `wxWindowDisabler` on the stack or tie it strictly to show/hide; never let heap
disablers outlive their window or end out of order (on macOS each one is a modal session).
---
## 7 DPIDialog and DPIFrame
`template<class P> class DPIAware : public P, public wxInspector::wxInspectable` (`GUI_Utils.hpp`);
`typedef DPIAware<wxFrame> DPIFrame;` `class DPIDialog : public DPIAware<wxDialog>`. New Orca dialogs and
frames derive one of them. Public API: `scale_factor()`, `prev_scale_factor()`, `em_unit()`,
`normal_font()`, `enable_force_rescale()`; on Windows `force_color_changed()`. Subclasses implement the pure
virtual `on_dpi_changed(const wxRect&)` and may override `on_sys_color_changed()`.
**What the constructor does** (one-step construction only; signature
`(parent, id, title, pos = wxDefaultPosition, size = wxDefaultSize, style = wxDEFAULT_FRAME_STYLE,
name = wxFrameNameStr)`):
| Step | Detail | Owner of the topic |
|---|---|---|
| scale factor, normal font | from `get_dpi_for_window(this)`; `SetFont(m_normal_font)` **except on macOS** (`#ifndef __WXOSX__`, avoids name cutting in `ObjectList`) | `references/dpi-bitmaps-fonts.md` |
| `CenterOnParent()` | runs **before any content exists** — re-centre after fitting | §8 |
| `SetupInspectorAccelerator(this)` | wxInspector toggle (Ctrl+Shift+I) in builds without `WXINSPECTOR_DISABLE`; a later `SetAcceleratorTable()` on the window replaces it | `references/orca-widgets.md` |
| MSW `update_dark_ui(this)` | no-op (its body only reads the dark flag) | `references/colours-dark-mode.md` |
| `update_em_unit()` | non-GTK `max(10, 10·scale)`; GTK `max(10, GetTextExtent("m").x - 1)` | `references/dpi-bitmaps-fonts.md` |
| bind `wxEVT_DPI_CHANGED` (not macOS) | rescales (`Freeze` → font/em → `on_dpi_changed` → `Layout` → `Thaw`) when the scale changed and no monitor drag is in progress, and does **not** `Skip()` — so wx's default top-level handler (scale the window size by the DPI ratio) never runs (`interface/wx/event.h:3572-3583`) | `references/dpi-bitmaps-fonts.md` |
| bind `wxEVT_MOVE_START/END` (MSW-only events) | defer rescale while the window is dragged between monitors | `references/dpi-bitmaps-fonts.md` |
| bind `wxEVT_SYS_COLOUR_CHANGED` | non-Windows: `update_dark_config()` + `on_sys_color_changed()` + `Skip()`; Windows: swallowed | `references/colours-dark-mode.md` |
| dialogs only: bind `wxEVT_CHAR_HOOK` | `WXK_ESCAPE` → `this->Close()`, never skipped; other keys skipped | below |
**ESC in a `DPIDialog`.** ESC = the close box: your `wxEVT_CLOSE_WINDOW` handler if any, else
`wxDialogBase::OnCloseWindow` → `EndDialog(wxID_CANCEL)` (modal: `ShowModal()` returns `wxID_CANCEL`;
modeless: hidden). The dynamic `DPIAware` hook runs before wx's static `wxDialogBase::OnCharHook`, so
`SetEscapeId()` has no effect on ESC. A `MessageDialog` with only Yes/No buttons therefore returns
`wxID_CANCEL` on ESC or the close box — callers must treat anything but `wxID_YES` as "no".
**`dialogStack` and the `EndModal` guard.** `DPIAware::ShowModal()` (same signature as the virtual
`wxDialog::ShowModal`, so it overrides it) pushes `this` on the global `std::deque<wxDialog*> dialogStack`
(`GUI_Utils.cpp`) and pops it after the loop. `DPIDialog::EndModal(retCode)` **refuses** — logs
"DPIAware::EndModal Error…", returns, the dialog stays open and modal — when the stack is non-empty and
`this` is not `dialogStack.front()`. It is a guard for the LIFO rule of §6, not a fix for a wx bug: wx
cannot end a lower loop first on any port (the dialog would be hidden with its `ShowModal()` stranded),
and the guard turns that into a no-op. It only knows
`DPIDialog`s that went through `DPIAware::ShowModal()`; native and plain-wx modals are not on the stack.
Consumers: `GUI_App::ShowDownNetPluginDlg` (searches the stack to avoid a second instance) and the
`wxEVT_QUERY_END_SESSION` handler in `GUI_App::on_init_inner` (sends a vetoable close to `mainframe`, then
`EndModal(wxID_ABORT)` on every stacked dialog — with the guard only the innermost actually ends). For "is
any modal open" in new code prefer `wxModalDialogHook::GetOpenCount()`.
There is no `Destroy()` override: heap `DPIDialog`s follow the stock `ShowModal(); Destroy();` or the
stack form.
**Pitfalls.**
- **Rule:** Pass the style explicitly.
```cpp
// Wrong: MyDialog(wxWindow* p) : DPIDialog(p, wxID_ANY, _L("Title")) {} // resizable, min/max boxes
// Right: MyDialog(wxWindow* p) : DPIDialog(p, wxID_ANY, _L("Title"), wxDefaultPosition,
// wxDefaultSize, wxCAPTION | wxCLOSE_BOX) {}
```
- **Rule:** Call `CenterOnParent()` again after `SetSizerAndFit()` (`MsgDialog::finalize` does).
**Why:** the `DPIAware` constructor centred the empty, default-sized window.
- **Rule:** An override of `ShowModal()` must call `DPIDialog::ShowModal()` (as
`RichMessageDialog::ShowModal` → `MsgDialog::ShowModal()`, `UnsavedChangesDialog`, `TextureImportDialog`
do), never `wxDialog::ShowModal()`.
**Why:** a dialog that bypasses the push is not on `dialogStack`; when it is opened above another
`DPIDialog`, its own `EndModal` is refused and it cannot close.
- **Rule:** To keep ESC from closing a `DPIDialog`, veto in the close handler or bind your own
`wxEVT_CHAR_HOOK` that swallows `WXK_ESCAPE`; `SetEscapeId(wxID_NONE)` does nothing here.
```cpp
// Right: bound in the subclass constructor, i.e. after DPIAware's hook, so it runs first
Bind(wxEVT_CHAR_HOOK, [](wxKeyEvent& e) { if (e.GetKeyCode() != WXK_ESCAPE) e.Skip(); });
```
- **Rule:** A child that binds the dialog's `wxEVT_DPI_CHANGED` must be bound after `DPIAware` (any child
created in the subclass constructor is) and must `Skip()`.
**Why:** dynamic handlers run most-recently-bound first (`interface/wx/event.h:592-593`) and `DPIAware`'s
handler does not skip; `DialogButtons` binds its parent's event in its constructor and calls `Skip()`,
so it runs and then `DPIAware` rescales. It unbinds in its destructor — the model for any widget that
binds an event on another window.
---
## 8 The Orca dialog recipe
Exemplars: `src/slic3r/GUI/CloneDialog.cpp` (minimal; `DialogButtons` with a left-aligned extra button,
an Enter-key `wxEVT_CHAR_HOOK` that synthesizes the OK `wxEVT_BUTTON`; its OK handler's `wxYield()` loop
inside a frozen plater is not a pattern to copy), `PurgeModeDialog.cpp` (custom-painted clickable card
panels; OK/Cancel `Button`s that close purely by id through `wxDialogBase::OnButton`;
`on_dpi_changed` with min size + `Fit()`/`Refresh()`; its `msw_buttons_rescale` call also resizes those
Orca `Button`s, so leave it out when copying), `FilamentPickerDialog.cpp`
(larger; helper `Create*()` methods returning sizers; positions itself next to the sidebar).
```cpp
class MyDialog : public DPIDialog
{
public:
explicit MyDialog(wxWindow* parent)
: DPIDialog(parent ? parent : static_cast<wxWindow*>(wxGetApp().mainframe), wxID_ANY,
_L("My Dialog"), wxDefaultPosition, wxDefaultSize,
wxCAPTION | wxCLOSE_BOX) // or wxDEFAULT_DIALOG_STYLE; never omit
{
SetBackgroundColour(*wxWHITE); // light design colour, dark-mapped at the end
SetFont(Label::Body_14);
auto* sizer = new wxBoxSizer(wxVERTICAL);
// ... children: Orca widgets, sizes via FromDIP(n), labels via _L() ...
sizer->Add(content_sizer, 1, wxEXPAND | wxALL, FromDIP(10));
auto* dlg_btns = new DialogButtons(this, {"OK", "Cancel"}); // NOT pre-translated
dlg_btns->GetOK()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { /* apply */ EndModal(wxID_OK); });
dlg_btns->GetCANCEL()->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { /* cancel work */ EndModal(wxID_CANCEL); });
Bind(wxEVT_CLOSE_WINDOW, [this](wxCloseEvent& e) { /* same cancel work */ e.Skip(); }); // ESC, close box
sizer->Add(dlg_btns, 0, wxEXPAND);
SetSizerAndFit(sizer);
CenterOnParent(); // DPIAware centred the empty window
wxGetApp().UpdateDlgDarkUI(this); // always last, after all children exist
}
protected:
void on_dpi_changed(const wxRect&) override; // rescale bitmaps/widgets, min sizes, GetSizer()->SetSizeHints(this), Refresh()
};
// caller
MyDialog dlg(this);
if (dlg.ShowModal() == wxID_OK) { /* read results */ }
```
**Conventions and why.**
- `DPIDialog` supplies `scale_factor()`, `em_unit()`, DPI-change handling and ESC handling, and requires
`on_dpi_changed(const wxRect&)`. Typical body: `Rescale()` on Orca widgets, `msw_rescale()` (+
`SetBitmap()` on the displaying control) on `ScalableBitmap`s, min sizes reset in em units or `FromDIP`,
then `GetSizer()->SetSizeHints(this)` (or a dialog `SetMinSize` + `Fit()`) and `Refresh()`.
`msw_buttons_rescale(this, em_unit(), ids)` (min height 2.5 em on the windows with those ids) is for raw
`wxButton`s only: it also overrides the style height of Orca `Button`s carrying those ids, including the
`DialogButtons` OK/Cancel. An empty override is acceptable only for trivially simple dialogs
(`CloneDialog`; on MSW it then keeps its old pixel size). Detail: `references/dpi-bitmaps-fonts.md`
§DPIAware rescale path, `references/sizers-layout.md` §Layout on DPI change.
- Parent fallback `parent ? parent : wxGetApp().mainframe` — never orphan a dialog (§1).
- Title and labels through `_L()` (`references/strings-i18n-files.md`).
- `SetBackgroundColour(*wxWHITE)` and `SetFont(Label::Body_14)` at the top. Setting a font on a dialog is
fine on every platform; the macOS "no `SetFont`" concern is `DPIAware`'s per-DPI default font
(ObjectList name cutting), which `DPIAware` already skips there.
- Light design colours everywhere; `wxGetApp().UpdateDlgDarkUI(this)` as the last line maps them for dark
mode and themes the native parts on Windows; child panels built later use `UpdateDarkUIWin`. Hand-picked
colours go through `StateColor::darkModeColorFor(wxColour("#..."))`. Detail: `references/colours-dark-mode.md`.
- `SetSizerAndFit(sizer)` on the dialog (AGENTS.md rule); where `SetSizer` must come first, follow with
`sizer->SetSizeHints(this)` (`MsgDialog::finalize` does `GetSizer()->SetSizeHints(this)`). Child panels use
plain `SetSizer`. Detail: `references/sizers-layout.md`.
- `CenterOnParent()` after sizing. Dialogs may set the app icon
(`SetIcon(wxIcon(encode_path(icon_path.c_str()), wxBITMAP_TYPE_ICO))` with
`resources_dir()/images/OrcaSlicerTitle.ico`, as `PurgeModeDialog` does — no effect on macOS or
Wayland, §4) and
clamp their size with `SetMinSize/SetMaxSize(FromDIP(...))`.
- Modality: construct on the caller's stack, `ShowModal()`, read the `wxID_*` result. Heap form:
`auto* dlg = new MyDialog(this); dlg->ShowModal(); dlg->Destroy();`. `Show()` is for modeless,
long-lived windows (monitor pages, progress dialogs, plugin dialogs), with a close shape from §2.
**`DialogButtons` and how its buttons close the dialog.** `Slic3r::GUI::DialogButtons(parent,
non_translated_labels, primary_btn_translated_label = "", left_aligned_buttons_count = 0)` is a `wxPanel`
of Orca `Button`s. It calls `_L()` on each label itself and assigns a stock id by matching the lower-cased
label (catalogue: `references/orca-widgets.md`).
| Label | Id | Closes the dialog with no handler bound? |
|---|---|---|
| OK | `wxID_OK` | yes — `AcceptAndClose()` → `EndDialog(wxID_OK)` (affirmative id) |
| Cancel | `wxID_CANCEL` | yes — `EndDialog(wxID_CANCEL)` |
| Apply, Confirm | `wxID_APPLY` (both) | no — `Validate()` + `TransferDataFromWindow()` only |
| Yes, No, Save, Delete, … | `wxID_YES`, `wxID_NO`, `wxID_SAVE`, `wxID_DELETE`, … | no |
| anything unknown | auto id | no — fetch with `GetButtonFromLabel(_L("…"))` or `GetButtonFromIndex(i)` |
Getters (`GetOK`, `GetCANCEL`, …), the full label → id map, and how the primary (Confirm-styled) and alert
buttons are chosen — in numeric id order, so `{"Save", "OK"}` makes Save primary — are in
`references/orca-widgets.md §DialogButtons`.
**Pitfalls.**
- **Rule:** Run cancel cleanup on every close path — the Cancel handler *and* `wxEVT_CLOSE_WINDOW` (or after
`ShowModal()` returns, where every path ends).
**Why:** ESC (via `DPIAware` → `Close()`) and the close box go through `wxDialogBase::OnCloseWindow` →
`EndDialog(wxID_CANCEL)`; wx's button emulation needs a real `wxButton`, so an Orca Cancel button's
handler never runs on those paths (§6).
- **Rule:** Bind every button whose default handling is not what you want; Yes/No/Apply/Confirm/custom
buttons never close by themselves, and an OK handler that does real work ends the dialog itself
(`EndModal(wxID_OK)`) or `Skip()`s to the default `AcceptAndClose()`.
- **Rule:** Pass untranslated labels to `DialogButtons`.
```cpp
// Wrong: new DialogButtons(this, {_L("OK"), _L("Cancel")}); // double translation; no stock ids in non-English UIs
// Right: new DialogButtons(this, {"OK", "Cancel"});
```
- **Rule:** `UpdateDlgDarkUI(this)` runs once, after every child exists; children added later are themed with
`UpdateDarkUIWin(child)`.
- **Rule:** Don't create dialogs in a constructor of their parent or before the main frame is shown when MSW
ownership matters (§1); don't keep `CloneDialog`'s `wxYield()` loop pattern — long work goes to a job
(`references/threads-timers-app.md`).
---
## 9 Message boxes: the MsgDialog family
**Rule.** Never `wxMessageBox`/`wxMessageDialog`/`wxRichMessageDialog` once the GUI exists; use the themed
replacements in `src/slic3r/GUI/MsgDialog.hpp` (`Slic3r::GUI`), rooted in `MsgDialog : DPIDialog` (logo on the
left, content on the right, `Button` row underneath, dark-mode and DPI aware). Why: on MSW the TaskDialog-based
native boxes (`wxMessageBox`, `wxMessageDialog`, `wxRichMessageDialog`, `wxProgressDialog`) ignore dark mode
(`interface/wx/app.h:1436-1446`), and native boxes cannot match Orca's look. `wxMessageBox` remains only for
failures before the GUI exists (e.g. `GUI_App::load_language`). Native message-box style limits:
`references/strings-i18n-files.md`. Return-value trap when reading old code: `wxMessageBox()` returns
`wxYES/wxNO/wxCANCEL/wxOK/wxHELP`, while `ShowModal()` returns `wxID_YES/…` (`interface/wx/msgdlg.h:269-276` vs `309-311`).
| Class | Constructor | Notes |
|---|---|---|
| `MessageDialog` | `(parent, message, caption = "", style = wxOK, forward_str = "", link_text = "", link_callback = nullptr)` | default choice; first four parameters match `wxMessageDialog` (style `wxOK`, `wxCANCEL`, `wxYES_NO`, `wxICON_*`); empty caption → "<app> info" |
| `RichMessageDialog` | `(parent, message, caption = "", style = wxOK)` | adds `ShowCheckBox(text, checked)` / `IsCheckBoxChecked()` (a "Don't show again" check box added in its `ShowModal()`); its `SetYesNoLabels`/`SetYesNoCancelLabels`/`SetOKLabel`/`SetOKCancelLabels`/`SetHelpLabel` only store strings and never relabel a button |
| `WarningDialog` | `(parent, message, caption = "", style = wxOK)` | empty caption → "<app> warning" |
| `ErrorDialog` | `(parent, msg, has_code_excerpts)` | caption "<app> error"; `has_code_excerpts` renders source/caret line pairs monospaced (placeholder-parser errors) |
| `InfoDialog` | `(parent, title, msg, is_marked = false, style = wxOK | wxICON_INFORMATION)` | caption is always "<app> information"; `title` is passed as the base's headline, which is not displayed |
| `DeleteConfirmDialog` | `(parent, title, msg)` | a plain `DPIDialog`, not a `MsgDialog`: Delete → `wxID_OK`, Cancel → `wxID_CANCEL` |
`DownloadDialog` and `FilamentWarningDialog` are single-purpose `MsgDialog` subclasses; read their
constructors before reusing them.
**Behaviour** (`MsgDialog.cpp`):
- Window style is always `wxDEFAULT_DIALOG_STYLE`; the `style` argument only selects buttons and icon. A NULL
parent becomes `wxGetApp().mainframe`.
- `MsgDialog::apply_style`: `wxOK` → OK, `wxYES` → Yes, `wxNO` → No, `wxCANCEL` → Cancel; Orca's use of
`wxFORWARD` adds a "Go to <forward_str>" button and turns OK into "Later" (`wxID_CANCEL`). Every button's
handler is `EndModal(btn_id)`, so compare with `wxID_OK/wxID_YES/wxID_NO/wxID_CANCEL` — and with
`wxFORWARD` (the style bit `0x2000`, used as the button id) for "Go to". OK/Yes/Go-to are Confirm-styled and
focused; `wxNO_DEFAULT`, `wxCANCEL_DEFAULT`, `wxHELP` and `wxSTAY_ON_TOP` are ignored.
- Icon from the style: `wxAPPLY` → "completed", `wxICON_WARNING` → "exclamation", `wxICON_INFORMATION` →
"info", `wxICON_QUESTION` → "question", otherwise the app logo; `wxICON_ERROR` greys it.
- ESC and the close box return `wxID_CANCEL` whatever the buttons (§7).
- Content (`add_msg_content`): plain text → a wrapped `Label` in a scrolled window, so `&` is a mnemonic —
escape user text (`references/strings-i18n-files.md`); with `link_text`/`link_callback`, `is_marked`,
code excerpts, or a message containing `<tr>` → a `wxHtmlWindow` with the text `xml_escape`d (`is_marked`
keeps `<`/`>` so markup works) and `\n` → `<br>`.
- Base helpers: `SetButtonLabel(wxID_*, label, set_focus = false)` relabels a created button;
`AddButton(id, label, set_focus = false)` appends a choice button that also ends with `EndModal(id)`;
`show_dsa_button(title = {})` adds a "Don't show again" `CheckBox` that posts `EVT_CHECKBOX_CHANGE`
(int = checked) to the dialog; `get_checkbox_state()`.
- `finalize()` (called by each subclass constructor): `SetSizeHints`, `Layout`, `Fit`, `CenterOnParent`,
`UpdateDlgDarkUI` — the recipe of §8 in one call; a custom `MsgDialog` subclass ends its constructor with it.
**Free helpers** (`GUI.hpp`): `show_error(parent, msg, has_code_excerpts = false)` is **asynchronous** — it
shows an `ErrorDialog` from `wxGetApp().CallAfter`, capturing the raw `parent`; `show_info(parent, msg,
title)` and `warning_catcher(parent, msg)` are synchronous `MessageDialog`s. The `const char*`/`std::string`
overloads decode UTF-8.
**Pitfalls.**
- **Rule:** Relabel buttons with `SetButtonLabel`, not the `RichMessageDialog::Set*Labels` methods.
```cpp
// Wrong: dlg.SetYesNoLabels(_L("Discard"), _L("Keep")); // stored, never shown
// Right: dlg.SetButtonLabel(wxID_YES, _L("Discard")); dlg.SetButtonLabel(wxID_NO, _L("Keep"));
```
- **Rule:** Test for the positive answer; everything else (No, Cancel, ESC, close box) is "no".
```cpp
// Wrong: if (dlg.ShowModal() != wxID_NO) discard();
// Right: if (dlg.ShowModal() == wxID_YES) discard();
```
- **Rule:** Never compare a `MsgDialog` result with `wxYES`/`wxOK` (style bits), except `wxFORWARD` for "Go to".
- **Rule:** Give `show_error` a parent that outlives the deferred call (the main frame, or `nullptr`, which
`MsgDialog` maps to it), not a dialog that may close first; don't rely on the error being visible when
`show_error` returns.
---
## 10 Overlay frames: BaseTransparentDPIFrame
`BaseTransparentDPIFrame : DPIFrame` (`BaseTransparentDPIFrame.hpp/.cpp`) is the base for small
semi-transparent prompt frames (text, OK/Cancel `Button`s, optional timed fade-out via
`DisappearanceMode::TimedDisappearance`). Its lifetime design:
- Parent is always `wxGetApp().mainframe`; borderless.
- `wxEVT_CLOSE_WINDOW` → `on_hide()` on every close, forced or not (it neither vetoes nor destroys): stop
the refresh timer, `Hide()`, then restore the main frame with
`if (!mainframe->IsShown()) mainframe->Show(); mainframe->Raise();` (§4, dd8cb89f6d). The frame is reused;
it is destroyed only through `on_close()` → `Destroy()`, or with the main frame.
- `Show(bool)` override starts/stops the refresh timer; `on_full_screen` adds `wxSTAY_ON_TOP` on macOS so the
overlay stays above a full-screen main window.
When writing a similar overlay: keep the restore rule; give a close handler that respects `CanVeto()` if the
frame can outlive its owner; own the timer as a member (`wxTimer m_timer{this}`) rather than a heap
`new wxTimer()` that is never deleted; write the style as `wxBORDER_NONE` (not `!wxCAPTION | …`, §5).
@@ -0,0 +1,675 @@
# wx 3.1.5 → 3.3.2: changes Orca code can trip on
OrcaSlicer moved from wxWidgets **3.1.5** to **3.3.2** (8248b06337, #12941). This file digests the
changes between those versions that matter to GUI code — the 3.1.6–3.2.0 incompatible changes, the
3.3 incompatible changes, and the notable 3.3.0/3.3.1/3.3.2 changes — each with what it means for
Orca, plus the migration already done in Orca as rules for new code. Read it when code written
from older wx knowledge behaves oddly, when a wx doc says "since 3.3", or when reviewing code near
the workarounds in §8. Where this file and prior wx knowledge disagree, this file wins.
Contents: [Rules](#rules) · [1 Reading the change logs](#1-reading-the-change-logs) ·
[2 3.1.6 → 3.2.0](#2-changes-from-316-to-320) · [3 3.3 behaviour changes](#3-33-behaviour-changes-that-compile) ·
[4 3.3 build changes](#4-33-changes-that-break-the-build) · [5 3.3.0](#5-330-notable-changes) ·
[6 3.3.1](#6-331-notable-fixes) · [7 3.3.2](#7-332-notable-changes) ·
[8 Migration done in Orca](#8-migration-already-done-in-orca)
All `docs/`, `interface/`, `include/`, `src/`, `build/` cites are relative to the pinned wx tree
(located as in `SKILL.md` §Ground truth), except paths
explicitly called Orca's (`deps/…`, Orca's `src/CMakeLists.txt`) and bare Orca file + symbol cites.
## Rules
1. Read every "now asserts" in the change logs as "now silently returns or does nothing" in Orca:
wx asserts are compiled out. Validate arguments yourself. → [§1](#1-reading-the-change-logs)
2. Target 3.3.2 only. Do not add `wxCHECK_VERSION` / `wxVERSION_NUMBER` /
`wxVERSION_EQUAL_OR_GREATER_THAN` branches for older wx. → [§8](#8-migration-already-done-in-orca)
3. Mark every override of a wx virtual `override`. 3.2/3.3 changed parameter types (`wxBitmapBundle`,
`wxReadOnlyDC`, `wxWindowBase*`, `wxVersionContext`); a stale signature must fail to compile, not
become a silent overload. → [§4](#4-33-changes-that-break-the-build)
4. Call `Show()` before `Raise()` on a window that may be hidden, guarded by `!IsShown()` (a redundant
`Show()` is not inert on GTK3). → [§3](#3-33-behaviour-changes-that-compile)
5. Never call `SetLabel`/`SetLabelText` on a `wxTextCtrl`; use `ChangeValue` (quiet) or `SetValue`.
→ [§3](#3-33-behaviour-changes-that-compile)
6. Do not use `wxTRANSPARENT_WINDOW` (it is `0`). Give the panel an explicit background colour, or set
`wxBG_STYLE_TRANSPARENT` before `Create()`. → [§3](#3-33-behaviour-changes-that-compile)
7. Decide Orca's theme with `GUI_App::dark_mode()`, never `wxSystemAppearance::IsDark()` or
`SelectLightDark()` (on MSW they report the OS app mode); ask about the OS with `AreAppsDark()` /
`IsSystemDark()`. → [§3](#3-33-behaviour-changes-that-compile)
8. Request multisampling explicitly (`.SampleBuffers(1).Samplers(4)` or `WX_GL_SAMPLE_BUFFERS` +
`WX_GL_SAMPLES`); `wxGLAttributes::Defaults()` no longer includes it. → [§3](#3-33-behaviour-changes-that-compile)
9. Size a `wxImageList` in physical pixels (the bitmaps' `GetSize()`), or use the `wxBitmapBundle`
image APIs. → [§3](#3-33-behaviour-changes-that-compile)
10. Return translated strings as `wxString` by value. → [§3](#3-33-behaviour-changes-that-compile)
11. Escape `&` in user text used as a control label or book page title. → [§3](#3-33-behaviour-changes-that-compile)
12. `wxDynamicCast` only on a pointer whose static type derives from `wxObject`; use `dynamic_cast`
for mixin interfaces (`wxComboPopup`, `wxItemContainer`, `wxTextEntry`). → [§4](#4-33-changes-that-break-the-build)
13. Build a `wxArrayString` with `Add()` or an initializer list, walk wx lists with
`compatibility_iterator` or range-for, and make a `wxString` the first operand of a mixed
`+` chain. → [§4](#4-33-changes-that-break-the-build)
14. MSW windows are not double-buffered by default in 3.3.2; a custom-painted control buffers itself.
→ [§5](#5-330-notable-changes)
15. `wxEVT_DPI_CHANGED` now fires on GTK3 too; such handlers must be correct on GTK, and every handler
you bind calls `Skip()` (`DPIAware`'s own is the one exception). → [§5](#5-330-notable-changes)
16. wx MSW dark mode cannot be switched at runtime; do not replace Orca's NppDarkMode with it without
keeping live theme switching. → [§5](#5-330-notable-changes)
17. After `wxStaticText::SetLabel`, re-wrap with `Wrap(-1); Wrap(w);` (or use `Label`); `wxST_WRAP`
is opt-in and works only inside a sizer. → [§7](#7-332-notable-changes)
18. On Linux/X11 call `wxGLCanvas::PreferGLX()` before any GL use; on Unix set a swap interval explicitly
if frame pacing matters (wx forces 0). → [§7](#7-332-notable-changes)
19. Compare `wxGrid::GetSelectedBlocks()` `begin()` with `end()` before dereferencing.
→ [§8](#8-migration-already-done-in-orca)
20. Keep the post-upgrade workarounds (MainFrame `WS_CAPTION` masking, macOS deep-link handler,
`MSWEnableDarkMode` ordering, `SidePopup` anchoring, MSW `GLCanvas3D::on_paint` render) when
touching that code. → [§8](#8-migration-already-done-in-orca)
## 1 Reading the change logs
**Sources.** `docs/changes.txt` lists changes since 3.2: incompatible behaviour changes :11-146,
build-breaking changes :149-252, 3.3.2 :255-330, 3.3.1 :333-377, 3.3.0 :380-622.
`docs/changes_32.txt` covers 3.x → 3.2.0: the cumulative "INCOMPATIBLE CHANGES SINCE 3.0.x" list
:9-233, 3.2.0 :235-276, 3.1.7 :279-340, 3.1.6 :343-444. wxQt, wxiOS, wxUniv, wxMotif and wxGTK1
items are left out here.
**Coverage gaps.**
- The "since 3.0.x" list in `changes_32.txt` is cumulative over all of 3.1.x. Most of it was already
in force in 3.1.5; §2.1 lists only what was added after 3.1.5, §2.3 the older items worth knowing.
- `changes_32.txt` in this tree stops at 3.2.0. The 3.2.1–3.2.10 maintenance fixes are not listed
anywhere in the tree.
- 3.3.0's list is relative to 3.2.8 (`changes.txt:383`). 3.3.2's list is relative to 3.2.10, and the
full set of changes since 3.3.1 *also includes* the 3.2.9 and 3.2.10 fixes, which are **not**
listed in this file (`changes.txt:258-259`).
**"Asserts" mean silence in Orca.** wx is built with `-DwxBUILD_DEBUG_LEVEL=0`
(`deps/wxWidgets/wxWidgets.cmake` → `build/cmake/init.cmake:245-246`), and `libslic3r_gui` gets
`-DwxDEBUG_LEVEL=0` when `SLIC3R_STATIC` (Orca's `src/slic3r/CMakeLists.txt`). At level 0 `wxASSERT`,
`wxASSERT_MSG`, `wxFAIL` and `wxFAIL_MSG` expand to nothing (`include/wx/debug.h:314-324`), while
`wxCHECK*` still test the condition and return early, but silently (`include/wx/debug.h:342-368`).
Every "now asserts" item below is, in Orca, either a silent early return (`wxCHECK`) or carrying on
with bad state (`wxASSERT`). Nothing ever shows a dialog.
**Build configuration that decides which changes bite** (generated `wx/setup.h` in the wx build
directory, `dep_wxWidgets-build/lib/wx/include/<toolkit>-unicode-static-3.3/wx/setup.h`, plus
`deps/wxWidgets/wxWidgets.cmake`):
| Setting | Orca value | Consequence |
|---|---|---|
| Linux toolkit | GTK3 (`option(DEP_WX_GTK3 … ON)` in Orca's `deps/CMakeLists.txt`, `SLIC3R_GTK` "3"; Flatpak gtk3) | GTK3 items apply; GTK2 is only the `-DDEP_WX_GTK3=OFF` opt-out |
| `WXWIN_COMPATIBILITY_3_0` / `_3_2` | `0` / `1` | 3.0-deprecated API is gone, 3.2-deprecated still compiles |
| `wxUSE_STD_CONTAINERS` | `1` | wx containers are std-like; `wxList::Node` and `wxArrayString(n, s)` are gone (§4) |
| `wxUSE_STD_STRING_CONV_IN_WXSTRING` / `wxUSE_UNSAFE_WXSTRING_CONV` | `0` / `1` | no implicit `wxString` → `std::string`; use `into_u8` / `ToStdString` |
| `wxUSE_NANOSVG` / `wxUSE_LUNASVG` | `0` / `0` | `wxHAS_SVG` undefined: no `wxBitmapBundle::FromSVG*`; Orca rasterises SVG itself |
| `wxUSE_STC` | `OFF` | Scintilla items do not apply |
| `wxUSE_LIBWEBP` | `builtin` (Flatpak: `sys`) | WebP decodes through `wxImage` (§5) |
| `wxUSE_WEBVIEW_EDGE` | MSVC only, IE off | Edge items apply on Windows only; Chromium backend not built |
| `wxUSE_GLCANVAS_EGL` | `ON` | effective only on GTK3 with EGL found |
## 2 Changes from 3.1.6 to 3.2.0
### 2.1 Incompatible changes new since 3.1.5
These entries of `changes_32.txt` "INCOMPATIBLE CHANGES SINCE 3.0.x" were not in 3.1.5's list, plus
the 3.1.7-specific section.
| Change | Cite (`changes_32.txt`) | Orca relevance |
|---|---|---|
| `wxRegEx` uses PCRE; `wxRE_ADVANCED` syntax is now PCRE syntax, `wxRE_BASIC` is deprecated, POSIX classes `[:XXXX:]` fail to compile | :15-17; `interface/wx/regex.h:95, 148-201` | Orca uses `wxRegEx` for simple patterns (e.g. `TroubleshootDialog`); write new patterns in PCRE syntax |
| `wxSpinCtrlDouble::SetValue(wxString)` with invalid text resets to `GetMin()` | :126-127 | raw spin controls only; Orca's `SpinInput` is custom |
| `wxSpinCtrl::SetValue(wxString)` sends no events on MSW (as documented) | :129-130 | do not rely on setter events (`controls-dataview.md`) |
| `wxButton::GetBitmap{Current,Disabled,Focus,Pressed}()` return a valid bitmap on MSW only if set | :132-134 | check `IsOk()` |
| `wxFileName::GetVolume()` returns `\\share` for UNC paths and `\\?\Volume{GUID}` for GUID paths | :136-140 | path code comparing volumes |
| `wxBitmapComboBoxBase::SetItemBitmap()` takes `wxBitmapBundle` | :152-153 | the bundle change also retyped `OnAddBitmap` (5f365b5c6b, §8) |
| MSW also links `oleacc` (3.1.5 already needed `shlwapi`, `uxtheme`, `version`) | :158-163 | automatic with MSVC |
| Xcode projects drop i386, add arm64 | :205-207 | none (CMake build) |
| `wxImage` ctor from XPM data is `explicit` | :225-226 | write `wxImage(xpm)` |
| `wxWindow::DoGetBorderSize()` removed | :228-229 | use `GetWindowBorderSize()` |
| MSVC 7 unsupported | :231-232 | none |
| 3.1.7: `wxImageFileProperty` members and internal `wxPropertyGridPageState` functions removed | :282-290 | no propgrid in Orca's own code (only the `wxInspector` dev dependency links it, on MSW/macOS) |
### 2.2 New APIs and behaviour in 3.1.6–3.2.0
| Change | Cite | Orca relevance |
|---|---|---|
| `wxBitmapBundle` added and used "throughout the entire API" (3.1.6) | `changes_32.txt:366` | setters take bundles (`wxBitmap` converts implicitly); **overrides** of virtuals that took `wxBitmap` break (5f365b5c6b). Bundles and sizing: `dpi-bitmaps-fonts.md` |
| Bitmap logical/DIP API: `CreateWithDIPSize` (3.1.6), `CreateWithLogicalSize` (3.3.0), `GetLogicalSize`; `CreateScaled` and `GetScaledSize/Width/Height` are "older synonyms … use the new function in the new code" | `interface/wx/bitmap.h:514, 551, 563-579, 706, 770-787` | new code uses the new names; Orca's older call sites (`BitmapCache`, `ScalableBitmap`) still compile |
| `wxDC::GetContentScaleFactor()` returns the effective DPI factor for window DCs (e.g. 1.5 on MSW), unlike `wxWindow::GetContentScaleFactor()` (always 1 on MSW), "since wxWidgets 3.1.6" | `interface/wx/dc.h:153-168` | do not divide bitmap sizes by the DC scale (`painting-custom-widgets.md`) |
| `wxDPIChangedEvent::Scale()` (3.1.6) | `changes_32.txt:372` | convenience for rescaling stored sizes |
| MSW: TLW resizing on DPI change improved and overridable — a handler that sizes the TLW itself does not `Skip()` (3.2.0) | `changes_32.txt:269`; `interface/wx/event.h` (`wxDPIChangedEvent`) | Orca's `DPIAware` handler does not `Skip()` (`dpi-bitmaps-fonts.md`) |
| `wxUILocale` (3.1.6); `wxLocale::IsAvailable` is now implemented through it | `changes_32.txt:348`; `src/common/intl.cpp:740-781` | see `wxUILocale::IsSupported` in §3 |
| `wxWebView::RunScriptAsync()` (3.1.6) | `changes_32.txt:382` | `webview-gl-aui-media.md` |
| wxOSX: "Allow user input in `wxPopupTransientWindow`" (3.1.7) — the popup's child holds mouse capture while the cursor is outside it and releases it inside, toggled from `OnIdle` [source] | `changes_32.txt:334`; `src/common/popupcmn.cpp:438-471` | how wxOSX transient popups track the mouse; the macOS gap-dismissal worked around in `SidePopup::Popup` is not traced to a specific wx change (§8; `popups-menus.md`) |
| wxGTK: Wayland fixes (3.1.6), no GDK errors from `PopupMenu()` on Wayland (3.1.7), `wxCURSOR_SIZING` on Wayland (3.2.0) | `changes_32.txt:397, 317, 261` | `platforms.md` |
| MSW: all native modal dialogs are app-modal (3.1.6) | `changes_32.txt:409` | `windows-dialogs.md` |
| Also new: `wxKeyEvent::IsAutoRepeat()`, `wxSpinCtrl::GetTextValue()/SetIncrement()`, `wxTopLevelWindow::SetContentProtection()`, `wxEVT_SPLITTER_SASH_POS_RESIZE`, OSX full-screen view options, OSX `wxEVT_CHAR` from `wxDataViewCtrl` | `changes_32.txt:364-436` | available API |
### 2.3 Older 3.x changes that still trip prior knowledge
These were already in force in 3.1.5, so the upgrade did not change them, but code written from
3.0-era knowledge gets them wrong (`changes_32.txt` line):
| Contract | Line |
|---|---|
| MSW `wxYield()` generates `wxEVT_IDLE` (idle handlers can run inside a yield) | :25-27 |
| A 0-width or 0-height `wxBitmap` fails on every port | :29-30 |
| Invalid sizer flags assert (silent in Orca; `sizers-layout.md`) | :32-37 |
| `Validate()`/`TransferData*Window()` recurse into children by default (`wxWS_EX_VALIDATE_RECURSIVELY`) | :39-41 |
| MSW: call `Skip()` in `wxEVT_KEY_DOWN`/`wxEVT_CHAR` handlers, or system keys such as Alt+Space and Alt+F4 stop working | :54-57 |
| MSW dotted/dashed pens are high quality and slow; `wxPenInfo::LowQuality()` | :59-62 |
| `wxEVT_AUINOTEBOOK_PAGE_CHANGED` comes after the change | :68-69 |
| Generic `wxDataViewCtrl` stretches its last column | :76-77 |
| GTK `wxNotebook::AddPage()` sends no event for the first page; GTK `wxTextCtrl` sends no `wxEVT_TEXT` at creation | :79-83 |
| `wxDC::GetTextExtent("")` height is 0 on every port | :85-86 |
| `wxTE_PROCESS_ENTER` is required for `wxEVT_TEXT_ENTER`, even multi-line | :96-99 |
| `wxGLCanvas` uses physical pixels on GTK3/macOS: multiply `GetSize()` by `GetContentScaleFactor()` (Orca: `RetinaHelper::get_scale_factor`, GTK3 variant in `GLCanvas3D.cpp`) | :101-104 |
| `wxSizer::RecalcSizes()` is not to be called; call `Layout()` | :116-117 |
| `wxFileDialog::GetPath()/GetFilename()` assert and return empty with `wxFD_MULTIPLE` | :119-120 |
| `wxChoice::GetString()` asserts on an invalid index | :124 |
| Application code cannot construct `wxPaintEvent` | :165-167 |
## 3 3.3 behaviour changes that compile
`changes.txt` "Changes in behaviour not resulting in compilation errors" (:11-146). Each row: the
change, its line, and what it means for Orca.
| Change | Line | Orca relevance |
|---|---|---|
| wxMSW needs Windows 7+ | :14-15 | drop XP/Vista assumptions (they need wx 3.2) |
| Fatal-error exit code is 255 everywhere (was 127 with MSVC); `wxApp::SetErrorExitCode()` (virtual, `interface/wx/app.h:886`), static `SetFatalErrorExitCode()` (:922) | :17-19 | update anything (CI, crash reporting, wrapper scripts) that matches the old code |
| `wxGLCanvas` no longer multisamples by default | :21-23 | pitfall below |
| `wxFileConfig` on Unix defaults to XDG `~/.config/appname.conf` (old file still used; `wxCONFIG_USE_XDG`/`wxCONFIG_USE_HOME`, `MigrateLocalFile()`) | :25-29 | none: Orca uses `AppConfig`, no `wxConfig` (`strings-i18n-files.md`) |
| `wxColourDatabase` uses CSS values; `UseScheme()` restores the old ones (`interface/wx/gdicmn.h:845, 973-996`) | :31-33 | none: stock colours (`*wxGREEN`, …) are fixed RGB (`src/common/gdicmn.cpp:789-830`) and Orca builds colours from hex/RGB. Do not build colours from names |
| `wxAuiNotebook` default art is the new flat art; `wxAuiNativeTabArt` (or `"native"` in XRC) keeps the old look | :35-37 | none: no `wxAuiNotebook` in Orca |
| `wxTHREAD_WAIT_DEFAULT` is `wxTHREAD_WAIT_BLOCK`: `wxThread::Delete()/Wait()` no longer pump events | :39-41 | Orca uses std/boost threads (`wxThread` only for `IsMain()`); a new `wxThread` whose exit needs the main loop would deadlock |
| `wxDocument::OnCloseDocument()` runs once, after the views are destroyed (it used to run twice when the document was closed from the menu); an override must not rely on any view existing | :43-46 | none: no doc/view |
| `wxGrid::FreezeTo()` asserts on out-of-range counts, and freezes even when the grid is too small | :48-51 | silent `false` in Orca; it also refuses reordered or drag-movable rows/columns [source] (`src/generic/grid.cpp:5742-5750`). Clamp arguments (`controls-dataview.md`) |
| Invalid `wxImageList` calls assert | :53-56 | silent in Orca; `wxCHECK` returns (generic `Add` → -1). Create the list with a valid size before use |
| `wxTRANSPARENT_WINDOW` does nothing (`#define wxTRANSPARENT_WINDOW 0`, `include/wx/defs.h:1449`); MSW code that needs it can set `WS_EX_TRANSPARENT` | :58-60 | pitfall below; 026b105dcb |
| MSW `wxTextDataObject::SetData()` size includes the 2-byte NUL (consistent with `GetDataSize()`); an old-style size chops the last character | :62-66 | none: Orca uses the `wxTextDataObject(text)` ctor; use `SetText()`, never `SetData()` |
| `wxListCtrl::EditLabel()` asserts without `wxLC_EDIT_LABELS` | :68-69 | silent no-op in Orca; add the style where editing is intended |
| MSW `wxSystemAppearance::IsDark()` reports the app's own mode; `AreAppsDark()`/`IsSystemDark()` report the OS | :71-73 | pitfall below |
| Unix `wxUILocale::IsSupported()` no longer falls back to another region of the same language; pass just `"fr"` to accept any `fr_XX` | :75-79 | `wxLocale::IsAvailable` builds the region-qualified tag (`GetCanonicalWithRegion()`) and calls `IsSupported()` [source] (`src/common/intl.cpp:740-781`); `GUI_App::load_language` relies on it, so on Linux a language whose canonical locale (e.g. `fr_FR`) is not installed reports unavailable. Keep the fallbacks in `load_language` |
| Deprecated `wxPGCellRenderer::DrawCaptionSelectionRect()` overload not called; override the overload taking `wxWindow*`, or enable 3.0 compatibility | :81-83 | none: no propgrid in Orca's own code |
| `wxImageList` size is in physical pixels | :85-88 | pitfall below |
| Mac `wxWebRequest` no longer uses persistent storage. The entry names `wxWebRequest::EnablePersistentStorage()`; the real API is `wxWebSession::EnablePersistentStorage(bool)` (`interface/wx/webrequest.h:1612-1629`; also `wxWebSessionSync`, :1835), macOS-only, before the first request | :90-92 | none: Orca uses `wxWebRequest` only for image downloads (`wxWebSession::GetDefault().CreateRequest` in `StatusPanel`, `SliceInfoPanel`, `ReleaseNote`, `DeviceErrorDialog`); network and login agents use libcurl (`Slic3r::Http`) |
| MSW `wxBitmap::Create(size, dc)` no longer multiplies by the DC's content scale; the size is physical | :94-96 | remove compensating scaling; use `CreateWithLogicalSize` for logical sizes |
| `wxIMAGE_QUALITY_NEAREST` has a new value and is no longer `wxIMAGE_QUALITY_NORMAL`; `NORMAL` (the `Scale`/`Rescale` default) is bilinear + box average (`interface/wx/image.h:31-67`) | :98-99 | never store or compare the enum numerically; default-quality thumbnails and icons look smoother than under 3.1.5; pass `wxIMAGE_QUALITY_NEAREST` for pixel-exact scaling |
| `wxTextCtrl::{Save,Load}File()` treat `.rtf` as RTF | :101-104 | pass `wxTEXT_TYPE_PLAIN` to keep plain text |
| `wxClientDC`/`wxPaintDC` offset their origin by a `wxFrame` toolbar on every port | :106-109 | none: Orca frames have no native toolbar (`BBLTopbar` is a child `wxAuiToolBar`) |
| `wxTextCtrl::SetLabel()` does nothing and asserts on every port (MSW used to act as `SetValue`) | :111-113 | pitfall below |
| `wxAuiGenericTabArt` subclasses (also via `wxAuiMSWTabArt`) override `DrawPageTab()`/`GetPageTabSize()` instead of `DrawTab()`/`GetTabSize()`; direct `wxAuiTabArt` subclasses still work | :115-120 | none: no Orca tab art |
| `wxAuiNotebook` page index is logical (reorder-independent); `GetPagePosition()` gives the screen position | :122-126 | none; index math under `wxAUI_NB_TAB_MOVE` is what breaks |
| `wxListbook`/`wxChoicebook` interpret mnemonics in page titles "just as the other wx*book classes already did" | :128-130 | Orca uses neither (`BedShapeDialog` uses `wxSimplebook` + a combo); every book interprets `&` (`interface/wx/bookctrl.h:141-147`) → rule 11 |
| `wxAUI_MGR_HINT_FADE` is not in the default `wxAuiManager` style | :132-133 | Orca's `AuiMgr` keeps the default flags (minus `wxAUI_MGR_ALLOW_FLOATING` on Wayland, `AuiMgr::init`), so the docking hint no longer fades; add the flag if wanted |
| `wxPrintDialogData::SetAllPages(false)`/`SetSelection(false)` changed meaning | :135-137 | none |
| `wxGetTranslation()` returns `wxString` by value | :139-142 | pitfall below |
| `wxWindow::Raise()` no longer shows a hidden window on any port | :144-146 | pitfall below; ba867cc534 |
**Pitfalls**
- **Rule:** Request multisampling explicitly; never rely on `wxGLAttributes::Defaults()` or a null
attribute list for MSAA.
**Why:** 3.1.5's `Defaults()` was RGBA, double buffer, depth 16 plus `SampleBuffers(1).Samplers(4)`
on every port; 3.3's is `RGBA().Depth(16).DoubleBuffer()` (`include/wx/glcanvas.h:175-178`), also used for a null
attribute list (`src/common/glcmn.cpp:159-165`). The change-log line has the arguments swapped:
`SampleBuffers(n)` is "number of sample buffers, usually 1" and `Samplers(n)` the samples per pixel
(`interface/wx/glcanvas.h:233-246`). The plater canvas is unaffected:
`OpenGLManager::create_wxglcanvas` passes `WX_GL_SAMPLE_BUFFERS`/`WX_GL_SAMPLES` explicitly (from
the anti-aliasing setting). A canvas built with `.Defaults()` — the `SkipPartCanvas` that
`PartSkipDialog` creates — renders without MSAA.
```cpp
// Wrong: attrs.PlatformDefaults().Defaults().Stencil(8).EndList(); // no MSAA in 3.3
// Right: attrs.PlatformDefaults().Defaults().SampleBuffers(1).Samplers(4).Stencil(8).EndList();
```
Cite: `docs/changes.txt:21-23`; `OpenGLManager::create_wxglcanvas`.
- **Rule:** Use `GUI_App::dark_mode()` for "is Orca dark", and `AreAppsDark()`/`IsSystemDark()` for
"is the OS dark"; never `GetAppearance().IsDark()` in GUI code (it breaks on MSW, see below;
`colours-dark-mode.md` rule 1 forbids it on every port).
**Why:** `IsDark()` returns true whenever wx's own dark mode is active, else it falls back to
`IsUsingDarkBackground()` (`src/msw/settings.cpp:431-441`). Orca calls
`MSWEnableDarkMode(DarkMode_Auto)` (`DarkMode_Auto = 0`, `include/wx/msw/app.h:48`), which puts wx
in `AppMode_AllowDark` (`src/msw/darkmode.cpp:235-252`); from then on `IsDark()` follows the OS
apps setting (`ShouldUseDarkMode()`, :205-227), whatever theme the user picked in Orca.
`AreAppsDark()` reads `AppsUseLightTheme`, `IsSystemDark()` reads `SystemUsesLightTheme`
(`src/msw/settings.cpp:444-452`; `interface/wx/settings.h:307-358`). On other ports the three agree.
`GUI_App::dark_mode()` honours the `dark_color_mode` app-config value first and only then calls
`check_dark_mode()`, which still uses `IsDark()`.
```cpp
// Wrong: bool dark = wxSystemSettings::GetAppearance().IsDark(); // OS apps setting on MSW
// Right: bool dark = wxGetApp().dark_mode();
```
Cite: 8248b06337; `GUI_App::dark_mode`, `check_dark_mode` (`GUI_Utils.cpp`); `colours-dark-mode.md`.
- **Rule:** `Show()` before `Raise()`, and `Show()` only if the window is hidden.
**Why:** "If the window is currently hidden, this function does *not* show it automatically"
(`interface/wx/window.h:3015-3033`). MSW already behaved so; GTK and macOS used to show it, so a
bring-to-front path that only raises leaves a hidden frame hidden on those ports. On GTK3 a
redundant `Show(true)` still runs `GTKSendSizeEventIfNeeded()` [source]
(`src/gtk/toplevel.cpp:1259-1269`), which froze Orca once (dd8cb89f6d), so guard it.
```cpp
// Wrong: wxGetApp().mainframe->Raise();
// Right: auto* mf = wxGetApp().mainframe; if (!mf->IsShown()) mf->Show(); mf->Raise();
```
Cite: ba867cc534 (the other-instance handlers in `Plater::priv::priv`,
`Plater::priv::bring_instance_forward`); `windows-dialogs.md` §4.
- **Rule:** Do not use `wxTRANSPARENT_WINDOW`; give the panel the background it must blend with, or
set `wxBG_STYLE_TRANSPARENT` **before** `Create()`.
**Why:** the flag is `0`. Under 3.1.5 it set `WS_EX_TRANSPARENT` on MSW; now the panel paints its
own background. A `SetBackgroundStyle(wxBG_STYLE_TRANSPARENT)` call after the window exists is a
`wxCHECK_MSG` that returns `false` silently (`src/common/wincmn.cpp:1616-1625`), so it is no
replacement for the flag — 026b105dcb assumed it was.
```cpp
// Wrong: new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTRANSPARENT_WINDOW);
// Right: auto p = new wxPanel(this, wxID_ANY);
// p->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#3B4446")));
```
Cite: 026b105dcb, 8248b06337 (`MainFrame::create_side_tools`, `MainFrame::update_side_button_style`
re-apply the colour on theme change); `painting-custom-widgets.md`.
- **Rule:** Never `SetLabel`/`SetLabelText`/`GetLabel` on a `wxTextCtrl` to show or read its value.
**Why:** `wxTextCtrlBase::SetLabel` is only `wxFAIL_MSG("Use SetValue() or ChangeValue()
instead.")` (`src/common/textcmn.cpp:933-936`); `SetLabelText` calls the virtual `SetLabel`
(`include/wx/control.h:64-67`), and `GetLabel()` returns `m_labelOrig`, not the text
(`include/wx/control.h:61`). Orca compiles the assert out, so the call is a complete no-op: the
field never updates. Under 3.1.5 MSW it acted as `SetValue`, so code written then looked fine on
Windows.
```cpp
// Wrong: m_input_ip->GetTextCtrl()->SetLabelText(m_obj->get_dev_ip());
// Right: m_input_ip->GetTextCtrl()->ChangeValue(m_obj->get_dev_ip()); // no wxEVT_TEXT
```
Cite: `docs/changes.txt:111-113`; `controls-dataview.md`.
- **Rule:** Size a `wxImageList` from the bitmaps' physical size, or use
`SetImages(std::vector<wxBitmapBundle>)`.
**Why:** "the size is specified in physical pixels and must correspond to the size of bitmaps, in
pixels" (`interface/wx/imaglist.h:62-63`). An Orca icon from `create_scaled_bitmap(name, win, 16)`
is larger than 16 px on HiDPI MSW, where `Add()` hands it to the native `ImageList_Add`
(`src/msw/imaglist.cpp:299-313`), which splits a wider bitmap into list-width images. The generic
list (GTK, macOS) keeps a bitmap whose scale factor is not 1 intact, but returns -1 for a narrower
1×-scale bitmap and chops a wider one into several list-width images [source]
(`src/generic/imaglist.cpp:126-160`).
```cpp
// Wrong: m_images = new wxImageList(16, 16); m_images->Add(create_scaled_bitmap("icon", this, 16));
// Right: wxBitmap bmp = create_scaled_bitmap("icon", this, 16);
// m_images = new wxImageList(bmp.GetWidth(), bmp.GetHeight(), false);
```
Cite: `docs/changes.txt:85-88`; `Tab` builds its list from `bmp().GetWidth()/GetHeight()`;
`dpi-bitmaps-fonts.md`.
- **Rule:** A function that returns a translation returns `wxString` by value.
**Why:** `wxGetTranslation()` returns by value now (`include/wx/translation.h:278-321`); returning
it as `const wxString&` dangles. Orca's `I18N::translate` overloads and `_L` already return by
value.
```cpp
// Wrong: const wxString& title() { return _L("Printer"); }
// Right: wxString title() { return _L("Printer"); }
```
Cite: `docs/changes.txt:139-142`; `I18N.hpp`.
- **Rule:** Escape `&` in user data (preset, filament, printer, file names) shown as a control label or
book page title.
**Why:** labels and page titles interpret `&` as a mnemonic; an unescaped `&` disappears or
underlines the next character. 3.3 extended this to `wxListbook`/`wxChoicebook`.
```cpp
// Wrong: book->AddPage(page, preset_name);
// Right: book->AddPage(page, wxControl::EscapeMnemonics(preset_name)); // or label->SetLabelText(name)
```
Cite: `docs/changes.txt:128-130`; `interface/wx/bookctrl.h:141-147`; `controls-dataview.md`.
## 4 3.3 changes that break the build
`changes.txt` "Changes in behaviour which may result in build errors" (:149-252).
| Change | Line | Orca relevance |
|---|---|---|
| 3.0-deprecated symbols disabled by default (`WXWIN_COMPATIBILITY_3_0=1` at wx build time re-enables them), 2.8-deprecated removed | :152-154 | Orca builds `WXWIN_COMPATIBILITY_3_0 0`, `_3_2 1`: port off 3.0-deprecated API |
| `wxUSE_UNICODE=0` unsupported | :156 | none |
| `wxUSE_STD_CONTAINERS=1` by default ("Container Classes" overview); building wx with 0 keeps the old containers | :158-161 | Orca builds with 1 → pitfall below |
| `wxUSE_STL` gone; implicit `wxString` → `std::[w]string` only with `wxUSE_STD_STRING_CONV_IN_WXSTRING=1` at wx build time | :163-166 | Orca builds 0: convert explicitly (`into_u8`, `ToStdString`, `ToUTF8()`) |
| MSW links `gdiplus.lib`, `msimg32.lib` | :168-172 | automatic with MSVC/wx-config; only static non-MSVC builds add them |
| wxMotif, wxGTK1 removed | :174-175 | none |
| Private containers (e.g. `wxSimpleDataObjectList`) removed; object arrays (`wxImageArray`) compare values in `Index()` | :177-183 | use `std::vector`/`std::list` |
| Operators on wx types are hidden (not global) | :185-189 | pitfall below |
| `wxString` from `std::string_view` makes `wxstr = {"Hello", 2}` ambiguous | :191-194 | write `wxString{"Hello", 2}` |
| Generic `wxSearchCtrl` lost multi-line-only methods | :196-197 | none |
| Wide-filename `wxOnAssert()` overload removed | :199-200 | none |
| 64-bit DLLs carry an `x64` suffix in all build systems | :202-204 | packaging scripts matching DLL names; Orca links wx statically (Flatpak builds it shared in its own manifest) |
| CMake config installs to `lib/cmake/wxWidgets-3.3`; plain `find_package(wxWidgets)` is unaffected, hard-coded paths break | :206-211 | Orca: `find_package(wxWidgets 3.3 CONFIG …)` on Windows/macOS, `wx-config --toolkit=gtk${SLIC3R_GTK}` on Linux (Orca's `src/CMakeLists.txt`) |
| Memory-tracing options removed | :213-216 | use ASan |
| `wxTEST_DIALOG()` needs a trailing `;` | :218-219 | none |
| `wxWindow::GetDefaultBorderForControl()` not virtual | :221-223 | do not override; use `wxBORDER_THEME` |
| GTK `wxDirButton::Create()` lost `wildcard` | :225-226 | none |
| Several virtuals take `wxReadOnlyDC` | :228-231 | pitfall below |
| `wxSizer::Detach()` takes `wxWindowBase*` | :233-236 | only custom sizer subclasses |
| `wx/cursor.h` no longer includes `wx/utils.h` | :238-240 | include `<wx/utils.h>` explicitly (8248b06337 added it to `GLCanvas3D.cpp`) |
| `wxStyledTextCtrl::AddSelection()` returns void | :242-244 | none: `wxUSE_STC=OFF` |
| `wxColour` from `bool` no longer compiles | :246-248 | use the RGB or string ctor explicitly |
| `wxGLCanvas::CreateSurface()` removed from EGL builds | :250-252 | none: wx chooses EGL or GLX itself (§7) |
**Pitfalls**
- **Rule:** Override wx measuring virtuals with `wxReadOnlyDC&` and mark them `override`.
**Why:** these virtuals now take `wxReadOnlyDC&` (the non-drawing base of `wxDC`, since 3.3.0,
`interface/wx/dc.h:115-127`): `wxRendererNative::GetCollapseButtonSize`
(`interface/wx/renderer.h:477`), the AUI tab and toolbar art size getters (`GetTabSize`,
`GetLabelSize`, `GetToolSize`, …; `interface/wx/aui/auibook.h:1186+`, `interface/wx/aui/auibar.h:648-664, 887-903`),
the `wxGridCellRenderer::GetPreferred{Size,Height,Width}`/`GetMaxSize` virtuals that 3.3 added
beside the old `wxDC&` `GetBestSize` family (`include/wx/generic/grid.h:203-251`), the new `wxScrolled::PrepareReadOnlyDC`
(`interface/wx/scrolwin.h:519`), and richtext/ribbon art. Nothing near `DoGetBestSize`, which takes
no DC. Without `override` an old `wxDC&` signature silently becomes an overload that is never
called; with `override` it fails to compile, which is what you want. Orca's `Widgets/` override
none of them; `BBLTopbarArt` overrides only `DrawBackground`/`DrawButton` (still `wxDC&`); the
`ObjectTable` grid renderers override the compatibility `GetBestSize(…, wxDC&, …)`, which "are the
ones actually called by wxGrid" (`include/wx/generic/grid.h:253-271`). Callers are unaffected; a
helper taking `wxDC&` cannot accept a `wxInfoDC`.
```cpp
// Wrong: wxSize GetToolSize(wxDC& dc, wxWindow* w, const wxAuiToolBarItem& it); // never called
// Right: wxSize GetToolSize(wxReadOnlyDC& dc, wxWindow* w, const wxAuiToolBarItem& it) override;
```
Cite: `docs/changes.txt:228-231`; `painting-custom-widgets.md`.
- **Rule:** With `wxUSE_STD_CONTAINERS=1`, build a `wxArrayString` with `Add()` or an initializer
list, and walk `WX_DECLARE_LIST` lists with `compatibility_iterator` or range-for.
**Why:** the std-container `wxArrayString` has no `(count, value)` ctor
(`include/wx/arrstr.h:64-84`), and `List::Node` no longer exists. Convert Orca's UTF-8
`std::string` explicitly: the implicit `wxString(const std::string&)` uses the current locale
(`include/wx/string.h:1323-1326`).
```cpp
// Wrong: load_files(wxArrayString(1, wxString::FromUTF8(target_path.string())));
// AmsRadioSelectorList::Node* node = m_radio_group.GetFirst();
// Right: wxArrayString arr; arr.Add(wxString::FromUTF8(target_path.string())); load_files(arr);
// for (AmsRadioSelector* rs : m_radio_group) { ... } // or ::compatibility_iterator
```
Cite: 1765d296a8 (`Plater::import_model_id`, `SendMultiMachinePage::request_params`); `strings-i18n-files.md`.
- **Rule:** In a concatenation, make a `wxString` the first operand when the others are `char`,
`wchar_t`, `std::string` or `std::wstring`.
**Why:** wx operators are hidden friends now, found only by argument-dependent lookup on a wx type;
"preventing them from implicitly being used with types convertible to wx types"
(`changes.txt:185-189`). `char + std::wstring + …` compiled under 3.1.5 through the global
`operator+(char, const wxString&)`, converting the `std::wstring` implicitly; under 3.3 that
operator is a hidden friend (`include/wx/string.h:2150`) and no operand is a `wxString`, so it
does not.
```cpp
// Wrong: return marker_by_type(opt.type, printer_technology) + opt.category_local + sep + opt.label_local;
// Right: return wxString(marker_by_type(opt.type, printer_technology)) + opt.category_local + sep + opt.label_local;
```
(`marker_by_type` returns `char`, the `*_local` labels are `std::wstring`.) Making only `sep` a
`wxString` (1765d296a8) did not help, because the leading `char + std::wstring` is evaluated first.
Cite: 1765d296a8, 2b3328c2b2 (`OptionsSearcher::search` `get_tooltip`, `Search.cpp`).
- **Rule:** Use `wxDynamicCast` only on a pointer whose static type derives from `wxObject`; for
mixin interfaces use `dynamic_cast`.
**Why:** 3.3's macro casts its argument straight to `const wxObject*` [source]
(`include/wx/object.h:118-121`); 3.1.5 first `static_cast` it to the target class, so a
`wxComboPopup*` → `wxCheckListBoxComboPopup` cast used to compile. `wxComboPopup` has no `wxObject`
base (`include/wx/combo.h:748`), so the 3.3 cast does not compile. The same holds for
`wxItemContainer`, `wxTextEntry` and other non-`wxObject` mixins.
```cpp
// Wrong: auto* p = wxDynamicCast(combo->GetPopupControl(), wxCheckListBoxComboPopup);
// Right: auto* p = dynamic_cast<wxCheckListBoxComboPopup*>(combo->GetPopupControl());
```
Cite: 7ea69199fd (`combochecklist_get_flags`, `combochecklist_set_flags`, `GUI.cpp`).
## 5 3.3.0 notable changes
`changes.txt:380-592` (relative to 3.2.8).
**Major changes** (:385-394)
| Change | Orca relevance |
|---|---|
| Experimental MSW dark mode (#23028), with `wxApp::SetAppearance()` (#24461) | pitfall below; Orca kept NppDarkMode and only calls `MSWEnableDarkMode(DarkMode_Auto)` |
| Chromium `wxWebView` backend (#706) and `wxEVT_WEBVIEW_CREATED` | Chromium is not built; `wxEVT_WEBVIEW_CREATED` is the documented ready signal for Edge (`webview-gl-aui-media.md`) |
| WebP images (#25205) | Orca builds `wxUSE_LIBWEBP=builtin` and calls `wxInitAllImageHandlers()` (`GUI_App::on_init_inner`), so `wxImage` decodes WebP (e.g. downloaded thumbnails) |
| Pinned and multi-row AUI tabs (#25187, #25076) | none: no `wxAuiNotebook` |
| Synchronous `wxWebRequest` (#24760) | available (`wxWebSessionSync`); "must not be used from the main thread of GUI applications" (`interface/wx/webrequest.h:614-615`) |
| Raw touch events (#17077); wxGrid accessibility (#24368) | available |
| Unix power events and blockers (#22396, #23717) | could keep a Linux system awake during long work; Linux covers only `wxPOWER_RESOURCE_SYSTEM` and needs systemd ≥ 183 (`interface/wx/power.h:163-171`); not used by Orca |
| Native GTK file dialogs when possible (#24486, #25104) | the portal dialog is used only with GTK ≥ 3.20 at runtime, without `wxFD_PREVIEW` and without an extra control [source] (`src/gtk/filedlg.cpp:265-273, 438-443`); Orca's `CheckboxFileDialog` (`SetExtraControlCreator`, `GUI_Utils.hpp`) therefore gets the non-native GTK dialog |
| Native `wxTextCtrl` contents / RTF (#24626, #24912) | available (`GetRTFValue`, `SearchText`) |
**All** (:398-442)
| Change | Orca relevance |
|---|---|
| `wxWebRequest`: base URL (#24769), proxy (#24762), repeated headers (#24878); `wxWebSession::EnablePersistentStorage()` (#23743) | see §3 for storage |
| `wxString`: move operations (#23215), `std::string_view` ctor (#23711), faster and more robust `To/FromCDouble()` (#23287), `errno` preserved (#23113), `wc_string()` (#23463), `wxWARN_UNUSED` on the class (#24833, unused-variable warnings for `wxString` locals) | settings fields do **not** parse through `ToCDouble`: `Field` normalises the decimal separator and calls `wxString::ToDouble`, and `double_to_string` uses `wxNumberFormatter::ToString`; `ToCDouble`/`FromCDouble` appear only in a few helpers (e.g. `PreferencesDialog::create_camera_orbit_mult_input`). The `wxNumberFormatter` changes (3.3.1, 3.3.2) matter more for `Field` |
| Lambdas with `Bind()` without RTTI (#14850); move-only `wxMessageQueue` (#25026); `wxLogXXX(string)` safe with a single string (#25414) | available |
| Environment variables use UTF-8 (#25101) | check round-tripping of non-ASCII paths through `wxGetEnv`/`wxSetEnv` |
| Improved locale matching (#24855); thread-safe `wxPlatformInfo::Get()` (#25459); `wxXmlParseError` from `wxXmlDocument::Load()` (#24215); customisable error exit code (#24770) | available |
**All (GUI)** (:444-500)
| Change | Orca relevance |
|---|---|
| High-DPI wave: `wxCursorBundle` (#25374), animations (#23817), generic `wxListCtrl` (#22916), print preview (#24666), AUI dock art after DPI change (#23420), `wxBusyInfo` bitmaps (#23813) | AUI dock-art size metrics are DIP-like now: `wxAuiManager` reads them through `GetMetricForWindow()` (since 3.3.0), which scales them by the window DPI. The docs exempt `wxAUI_DOCKART_SASH_SIZE` and `wxAUI_DOCKART_PANE_BORDER_SIZE` (`interface/wx/aui/dockart.h:274-294`), but the default implementation exempts only `wxAUI_DOCKART_PANE_BORDER_SIZE` (and the non-pixel gradient type) and does scale the sash size [source] (`src/aui/dockart.cpp:199-226`). Pass DIP values to `SetMetric` (Plater's `SetMetric(wxAUI_DOCKART_CAPTION_SIZE, 18)`), never `FromDIP(…)` |
| Dark-mode colours in XRC (#23571); CSS colour names (#23518) | none: no XRC, no colour names |
| `wxTextCtrl::SearchText()` (#24756) and RTF (#24626); locale-aware date/time pickers (#23965, display format changes); `wxSearchCtrl` derives from `wxTextEntry` on all ports (#23686) | available |
| Scintilla 5.0 / Lexilla 5.3 (#23117, #24369); nanosvg crash fixes (#24213) | none: `wxUSE_STC=OFF`, `wxUSE_NANOSVG=OFF` |
| Better `wxImage` resizing (#25252) | default-quality scaling output differs from 3.1.5 (§3) |
| `wxAuiManager::{Save,Load}Layout()` (#24235); `wxAuiNotebook` layout save/restore (#24950) | Orca persists docking with `SavePerspective`/`LoadPerspective` (Plater, `AuiPaneLayout`) |
| Non-live resize restored in wxAUI and `wxSplitterWindow` (#24193) | `wxAUI_MGR_LIVE_RESIZE` is in `wxAUI_MGR_DEFAULT` since 3.3.0 (`interface/wx/aui/framemanager.h:59-66`). The style table's "always enabled in wxGTK3 and wxOSX ports as non-live resizing is not implemented in them" (:199-206) is as stale as the `AlwaysUsesLiveResize()` note: that function "always returns false" as of 3.3.0 (:345; `src/aui/framemanager.cpp:711-714`), and the flag decides on every port [source] (`HasLiveResize`, :716-719). See `webview-gl-aui-media.md` §AUI docking |
| `wxInfoBar::ShowCheckBox()` (#25394); printing multiple page ranges (#25030); `wxGrid::CopySelection()` (#24124); new default flat AUI tab art (#25316); `wxApp::SetAppearance()` (#24461) | available |
**All (WebView)** (:510-525) — `wxWebViewConfiguration` + `GetNativeConfiguration()`, `SetProxy()`,
`ShowDevTools()`, `EnablePersistentStorage()`, `EnableBrowserAcceleratorKeys()`, clearing browsing
data, advanced requests, child-window handling, `IsTargetMainFrame()`, Edge user agent settable after
creation, and **Edge events queued** (#22744, #19075): Edge handlers run later than on the other
backends. Edge posts its events with `AddPendingEvent` (script messages: `src/msw/webview_edge.cpp:811`)
except the vetoable ones, `wxEVT_WEBVIEW_NAVIGATING` (:599) and `wxEVT_WEBVIEW_NEWWINDOW` with its
`NEWWINDOW_FEATURES` follow-up (:723, :737), which stay synchronous; WebKit and
WebKit2GTK deliver script messages synchronously (`src/osx/webview_webkit.mm:1360`,
`src/gtk/webview_webkit2.cpp:408`) [source]. `webview-gl-aui-media.md` owns the details.
**wxGTK** (:527-547)
| Change | Orca relevance |
|---|---|
| `wxDPIChangedEvent` generated (#19290, #24040), GTK ≥ 3.10 (`interface/wx/event.h:3592-3593`) | pitfall below |
| `wxGLCanvas` scale fixed with EGL/Wayland in high DPI (#23733) | Orca has no compensating hack: GTK3 `RetinaHelper::get_scale_factor` returns `GetContentScaleFactor()` |
| `wxKeyEvent::GetKeyCode()` fixed for non-US layouts (#23379) | shortcuts match through `KeyChord::from_event` (`mouse-keyboard-focus.md`) |
| Missing enter/leave events fixed (#24339); mouse event generation fixes (#24931-#24933) | hover logic (`StateHandler`) gets enter/leave reliably |
| Total window size with GNOME on X11 (#25348) | TLW geometry |
| `libwebkit2gtk-4.1` support (#23633) | wx's CMake build prefers 4.1 and falls back to 4.0 (`build/cmake/init.cmake:571-577`) |
| `libsecret` not required at runtime (#25355) | `wxSecretStore` loads it on demand: always check `IsOk()` (`interface/wx/secretstore.h:197-201`), as `OrcaCloudServiceAgent` does |
| Multi-line `wxTextCtrl` max length (#24751); `wxRB_SINGLE` (#23652); `GTKSetPangoMarkup()` (#24912) | `controls-dataview.md` |
| `wxDC::DrawRoundedRectangle()` radius limited to half the smaller side (#24327) | the clamp is in the GTK2 GDK DC (`src/gtk/dcclient.cpp:874-875`, built only in the GTK2 opt-out, `GTK2_LOWLEVEL_SRC` in `build/files`) and also in the common `wxGraphicsPathData::AddRoundedRectangle` (`src/common/graphcmn.cpp:439-440`, absent in 3.1.5) that `wxGraphicsContext::DrawRoundedRectangle` uses, so every `wxGCDC` clamps: GTK3 (Cairo) and macOS window DCs, and Orca's memory-DC + `wxGCDC` paint paths on all ports [source]. A radius larger than half the smaller side is now clamped instead of drawing overlapping arcs |
| Read-only `wxBitmapComboBox` height fixed (#25468) | `Slic3r::GUI::BitmapComboBox` on GTK |
**wxMSW** (:549-583)
| Change | Orca relevance |
|---|---|
| "Enable double buffering for all windows" (#22851) | **reverted in 3.3.2** (#25808) → pitfall below |
| `wxOverlay` reimplemented with layered windows (#23261) | none: no `wxOverlay` |
| `wxBG_STYLE_TRANSPARENT` implemented (#23412) | as `WS_EX_TRANSPARENT` on non-TLW children [source] (`src/msw/window.cpp:1581-1582`); set before `Create()` (`painting-custom-widgets.md`) |
| `wxCAPTION` turned on when min/max/close boxes are set (#23575) | `WS_CAPTION` is added to the style (`src/msw/toplevel.cpp:132-135`); origin of the MainFrame workaround (§8) |
| `wxButton` default size larger in high DPI (#25297) | raw `wxButton` layouts may grow; Orca dialogs use `Button`/`DialogButtons` |
| Markup in `wxStaticText` (#25000); `wxHyperlinkCtrl` colour changeable (#23549) | `SetLabelMarkup` now works on MSW (single line) |
| Extended-length paths (#25033); `wxFileDialog` no unwanted extension (#24949); UTF-8 build fixes (#23313); non-BMP strings (#25128) | available |
| `wxDisplay` invalidated on display change (#25396); TLW with one child resized on DPI change (#22983); Aero-snapped geometry saved | multi-monitor geometry |
| Modern default `wxTreeCtrl` look (#23844); RTL fixes for `wxOverlay`/`wxScrolled` (#25413) and GDI+ (#25431); `wxBitmap::UseAlpha()` returns `bool` (#23919) | available |
**wxOSX** (:585-592) — `wxEventLoop::OnExit()` is always called (#25409); TLW cursor setting fixed
(#25131); Cmd-C no longer activates a "Close" button (#25346); `wxCursor` loadable from resources
(#24374); `wxUIActionSimulator` works (#23692), usable for GUI tests.
**Pitfalls**
- **Rule:** Do not replace Orca's MSW dark mode with wx's (`SetAppearance`/`MSWEnableDarkMode`)
unless live theme switching is kept.
**Why:** wx's MSW dark mode cannot change once any top-level window exists or a mode was chosen:
`SetAppearance` returns `CannotChange` (`src/msw/darkmode.cpp:263-270`;
`interface/wx/app.h:1166-1173`). TaskDialog-based dialogs (`wxMessageDialog`, `wxProgressDialog`,
simple `wxAboutBox`), the common dialogs (colour, find/replace, font, page setup, print) and the
date/time/calendar controls stay light (`interface/wx/app.h:1434-1448`). Orca switches themes at
runtime, which is why 8248b06337 kept NppDarkMode and only informs wx with
`MSWEnableDarkMode(DarkMode_Auto)` (§8).
Cite: 8248b06337 (PR text); `colours-dark-mode.md`.
- **Rule:** On MSW, a custom-painted control buffers its own drawing (`wxAutoBufferedPaintDC`/
`wxBufferedPaintDC` with `wxBG_STYLE_PAINT`, or the Orca memory-DC + `wxGCDC` path), or calls
`SetDoubleBuffered(true)` after creation.
**Why:** 3.3.0's global `WS_EX_COMPOSITED` was reverted in 3.3.2 (`changes.txt:308`). In 3.3.2
nothing sets it except an explicit `SetDoubleBuffered(true)` [source] (`src/msw/window.cpp:4704-4721`;
the only other use is `MSWDisableComposited`, :1643-1652), so MSW windows are not double-buffered by
default, exactly as in 3.1.5. `wxAutoBufferedPaintDC` is a `wxBufferedPaintDC` on MSW at compile
time (`include/wx/dcbuffer.h:18-23, 215-221`). Nothing tuned against 3.3.0/3.3.1 applies.
Cite: `docs/changes.txt:308, 557`; `painting-custom-widgets.md`.
- **Rule:** A `wxEVT_DPI_CHANGED` handler must be correct on GTK3, and every handler you bind — on a
child, a control or a `DPIDialog`/`DPIFrame` — calls `Skip()`.
**Why:** wxGTK3 now emits the event from `wxTopLevelWindowGTK::GTKConfigureEvent` when the integer
content scale changes, with DPI = 96 × scale [source] (`src/gtk/toplevel.cpp:336-351`;
`src/common/wincmn.cpp:2828-2830`). The event reaches each top-level window and its children
recursively, and the docs say handlers "should almost always call `event.Skip()`"
(`interface/wx/event.h:3564-3582`). Orca's `DPIAware` binds it on every non-macOS port and does
not `Skip()`; it sets `m_scale_factor` from that DPI although GTK3 pixels are already DIPs (the
ctor starts at 1 because `get_dpi_for_window` returns 96 on Linux). Double scaling when a window moves between monitors of
different scale is a risk that has not been verified at runtime; test DPI changes on GTK when
touching `DPIAware::rescale` paths.
Cite: `docs/changes.txt:541`; `dpi-bitmaps-fonts.md` §wxEVT_DPI_CHANGED.
## 6 3.3.1 notable fixes
`changes.txt:333-377`.
| Port | Change | Orca relevance |
|---|---|---|
| All | Persistence for `wxCheckBox` (#25515) and `wxRadioButton` groups (#25530); `wxAuiPaneInfo::FloatingClientSize()` (#25483); PNG "Description" chunk (#25556) | available |
| All | Settable app id (#25548): `wxAppConsole::SetClassName()` — the Windows AppUserModelID and the Wayland `app_id` (wxGTK ≥ 3.24.22), unused elsewhere; call it before any TLW, typically in the app ctor; on Windows it also changes shell behaviour (shift-middle-click new instance, shell MRU) (`interface/wx/app.h:760-812`) | Orca calls only `SetAppName`; setting a class name would change Windows taskbar grouping and jump lists |
| All | `wxDataViewCtrl::Collapse()` safe from event handlers (#25631); no `wxEVT_GRID_SELECT_CELL` at `wxGrid` creation (#25498); empty `wxGridSizer` no longer asserts (#25641); `wxPropertyGrid` compatibility (#25627); `wxNumberFormatter` (#25614, #25635); reproducible static Unix builds (#25502) | `ObjectList`/`ObjectGrid` event handlers; `Field` number formatting |
| wxGTK | Crash sorting a `wxDataViewCtrl` with a single leaf (#25625); `wxListCtrl` contents lost after `AppendColumn()` (#25519) | native GTK `wxDataViewCtrl` |
| wxMSW | Dark-mode fixes: disabled `wxButton` bitmaps (#25575), disabled `wxStaticText` (#25574), `wxComboCtrl` (#23766), `wxTE_RICH` `wxTextCtrl` (#25602), selected toolbar buttons (#25616), `wxStaticBitmap` in `wxNotebook` crash (#25499), notebook background in high-contrast (#25542) | apply only where wx's own dark mode is active (`wxMSWDarkMode::IsActive()`; with Orca's `DarkMode_Auto`, whenever `ShouldAppsUseDarkMode()` reports dark, `src/msw/darkmode.cpp:205-227, 414-417`) |
| wxMSW | `wxDataViewCtrl` border in light mode (#25532); `wxTreeCtrl::EnsureVisible()` while frozen (#18435); preferred-languages buffer overrun (#25612); `wxAcceleratorTable` with 0 entries (#25517); date/time pickers on non-English Windows (#25511); per-window menu MDI crash (#25522) | available |
| wxOSX | Border look of `wxDataViewCtrl`, `wxListBox`, `wxTextCtrl` (#25570); startup crash with Farsi system language (#25561) | native macOS controls |
## 7 3.3.2 notable changes
`changes.txt:255-318`, relative to 3.2.10 (see §1 for the unlisted 3.2.9/3.2.10 fixes).
| Port | Change | Orca relevance |
|---|---|---|
| All | `wxWebRequestDebugLogger` (#26086); configurable `wxWebRequest` timeouts (#25673); number/currency formatting (#25765); 3rd-party libraries updated (#26010); `wxSOCKET_NOWAIT_READ\|wxSOCKET_WAITALL_WRITE` (#17114) | image downloads; `Field` formatting |
| All | `wxDC::DrawLabel()` bitmap position fixed in high DPI (#25888) | available |
| GUI | `wxGLContext::ClearCurrent()` (#25958) and `wxGLContext::GetProcAddress()` (#9215) — static members of `wxGLContext`, not `wxGLCanvas` as the change log says; `GetProcAddress` "is currently not implemented under macOS and always returns NULL" (`interface/wx/glcanvas.h:550-599`) | Orca loads GL with GLAD (through `eglGetProcAddress` on Wayland, `OpenGLManager::init_gl`) |
| GUI | `wxGLCanvas::SetSwapInterval()` (#25449) returning `SwapInterval::{NotSet, Set, NonAdaptive}`, `GetSwapInterval()`, `DefaultSwapInterval` (`interface/wx/glcanvas.h:866-893, 1036-1154`) | pitfall below |
| GUI | Automatic `wxStaticText` wrapping (#25753) | pitfall below |
| GUI | `wxDisplay::GetRawPPI()` (#26082); configurable `wxScrolled<>` autoscroll (#25978, `EnableAutoScrollInside`/`DisableAutoScrollOutside`); `wxScrolled::GetViewStartPixels()`; `wxWindow::GetMinSizeFromKnownDirection()` | `sizers-layout.md` |
| GUI | `wxPersistentDVC` restores column positions (#26222); safer `wxTipWindow` close detection (#26070); wxDC-derived objects movable (#25726) | available |
| GUI | AUI: pane minimising (#23986), crash on hover after closing a notebook tab (#25959), notebook splitting (#26081) | Plater docking |
| GUI | LunaSVG option (#25902); `wxSVGFileDC` improvements (#25723); generic `wxCalendarCtrl` DPI-aware (#25713); `wxTextEntryDialog::SetHint()` (#26176); `wxStyledTextCtrlMiniMap` (#25887) | LunaSVG and STC are off in Orca's build |
| GUI | Many RTL layout fixes in wxMSW and wxGTK (#25426) | RTL languages |
| wxGTK | GLX and EGL in the same program (#26023): wx defaults to EGL even on X11 unless `PreferGLX()` or `wx_opengl_egl=0`; Wayland is always EGL [source] (`src/unix/glcanvas.cpp:218-245`; `interface/wx/glcanvas.h:1082-1102`) | pitfall below |
| wxGTK | `WarpPointer()` on Wayland compositors with the pointer-warp protocol (#23778); mutter moves the pointer only while a button is pressed (`interface/wx/window.h:3895-3914`) | Orca does not call `WarpPointer` |
| wxGTK | Gesture handling fixes (#26241); `wxDIRP_DIR_MUST_EXIST` | `GLCanvas3D::bind_event_handlers` binds `wxEVT_GESTURE_PAN/ZOOM/ROTATE` |
| wxMSW | `WS_EX_COMPOSITED` use from earlier 3.3 reverted (#25808) | §5 pitfall |
| wxMSW | Dark-mode rendering of several controls (#25835), toolbar (#25892), menus (#26182); accessibility: `wxCheckBox` in dark mode (#26184), full `wxCheckListBox` (#25948) and `wxStyledTextCtrl` (#25956), basic `wxRichTextCtrl` (#26202) | wx dark mode only |
| wxMSW | `wxNO_WIN32_W` (#25965); MSVS 2026 project files (#26131); `wxString` debug visualiser (#25684) | none |
| wxOSX | Visual fixes for macOS 26 Tahoe (#25766, #25743, #25767); dark-mode grid lines (#25783); nested markup attributes (#25864); suspend/resume events (#25778); threaded `wxGA_SMOOTH` animation (#25906); more joystick axes (#26216) | native look on Tahoe |
| wxOSX | Click events consistent with wxMSW (#25886) | pitfall below |
**Pitfalls**
- **Rule:** After `SetLabel` on a `wxStaticText`, re-wrap with `Wrap(-1); Wrap(w);`, or use Orca's
`Label` (`LB_AUTO_WRAP`, `Label::Wrap`); use `wxST_WRAP` only inside a sizer that constrains the
width.
**Why:** "Automatic wrapping" is the opt-in `wxST_WRAP` style, which "only works when the control is
used inside a sizer" (`interface/wx/stattext.h:46-49`); labels without it lay out as before, and
Orca uses no `wxST_WRAP`. What does change existing code is the rewritten `Wrap()`: it returns
immediately when `width == m_currentWrap` (`src/common/stattextcmn.cpp:259-263`), and `SetLabel`
clears the saved unwrapped text but not `m_currentWrap` (`UpdateLabelOrig`, :354-365) [source].
So `SetLabel(new); Wrap(sameWidth);` — correct under 3.1.5, whose `Wrap()` always re-wrapped —
leaves the new label unwrapped. `wxST_WRAP` wraps at the width the sizer offers via
`GetMinSizeFromKnownDirection` (:285-316), but the first `CalcMin` still uses the unwrapped best
size, so a fitted dialog grows to the full line [source]; constrain the width another way (a fixed
or max width on the container). `Label::Wrap` re-wraps from its stored text and has no cache.
```cpp
// Wrong: m_static_valid->SetLabel(info_line); m_static_valid->Wrap(FromDIP(300));
// Right: m_static_valid->SetLabel(info_line); m_static_valid->Wrap(-1); m_static_valid->Wrap(FromDIP(300));
```
Cite: `docs/changes.txt:280`; `sizers-layout.md` §wxStaticText wrapping.
- **Rule:** On Linux/X11, call `wxGLCanvas::PreferGLX()` before any GL use, attribute objects
included; set a swap interval explicitly if the canvas needs VSync.
**Why:** `PreferGLX()` called late "will trigger an assert failure and have no other effect" (silent
in Orca) and has no effect on Wayland (`interface/wx/glcanvas.h:1082-1102`). Orca calls it when
`is_running_on_x11()` early in `GUI_App::on_init_inner`. On Unix wx sets the swap interval to **0**
(VSync off) at the first `SwapBuffers` unless `SetSwapInterval` was called, so that
`eglSwapBuffers`/`glXSwapBuffers` never block on an occluded window [source]
(`include/wx/unix/private/glcanvas.h:96`; `src/unix/glegl.cpp:897-915`; `src/unix/glx11.cpp:940-950`).
`SetSwapInterval(DefaultSwapInterval)` keeps the driver's default. macOS and MSW leave the default
unless asked. `OpenGLManager` sets no swap interval, so on Linux the 3D view's buffer swaps are
not synchronised to VSync.
```cpp
// Right (if frame pacing is wanted): canvas->SetSwapInterval(1); // before the first SwapBuffers
```
Cite: `docs/changes.txt:275-276, 292`; `webview-gl-aui-media.md` §EGL vs GLX.
- **Rule:** Do not count clicks from `wxEVT_LEFT_DCLICK` alone on macOS; handle `DOWN` and `DCLICK`
as on MSW.
**Why:** `wxWidgetCocoaImpl::DoHandleMouseEvent` turns every second `DCLICK` back into a `DOWN`
(left and right buttons), so a triple click gives `DOWN, DCLICK, DOWN` as on MSW; previously every
click with `clickCount > 1` was a `DCLICK` [source] (`src/osx/cocoa/window.mm:4103-4150`). Per-platform click-count ifdefs
written for 3.1.5 need rechecking.
Cite: `docs/changes.txt:316`; `mouse-keyboard-focus.md`.
## 8 Migration already done in Orca
The upgrade landed as 8248b06337 ("Updated wxWidgets to 3.3.2", #12941; build system 2d7e26292b).
Its PR kept Orca's own MSW dark mode to avoid broader changes and because wx's needs an app restart to
switch. Each row is a rule for new code; the owning file has the detail.
| Commit | Rule for new code | Where | Owner |
|---|---|---|---|
| 8248b06337 | No version-conditional code for wx < 3.3: the upgrade deleted the pre-3.1.3 DPI-event shim (`DpiChangedEvent`, `EVT_DPI_CHANGED_SLICER`), the luma fallback in `check_dark_mode`, the `wxCHECK_VERSION` guards in `I18N.hpp`/`GUI_App.hpp`, and the macOS 10.9.5 `wxGLContext` hack | `GUI_Utils.hpp` `DPIAware`, `OpenGLManager` | this file |
| 8248b06337 | `MSWEnableDarkMode(DarkMode_Auto)` runs before `NppDarkMode::InitDarkMode()`, so NppDarkMode's `SetPreferredAppMode(ForceDark` or `ForceLight`, per Orca's setting) overrides wx's `AllowDark` at OS level in both directions; `GUI_App::dark_mode()` honours `dark_color_mode` before `check_dark_mode()` | `GUI_App::on_init_inner`, `GUI_App::dark_mode` | `colours-dark-mode.md` |
| 8248b06337 | `wxToolTip::GetToolTipCtrl()` is private in 3.3 (`include/wx/msw/tooltip.h:92`); wx applies dark mode to the tooltip window itself through `wxMSWDarkMode::AllowForWindow`, which follows wx's mode [source] (`src/msw/tooltip.cpp:321`; `src/msw/darkmode.cpp:454-457`) | `GUI_App::force_colors_update` (the `#if wxVERSION_NUMBER < 3300` block is dead under 3.3) | `colours-dark-mode.md` |
| 8248b06337 | Backend webview factories override `GetVersionInfo(wxVersionContext)` without a default argument (`include/wx/msw/webview_edge.h:150`); pass `wxVersionContext::RunTime` when calling through the concrete factory | `WebView::CheckWebViewRuntime` | `webview-gl-aui-media.md` |
| 8248b06337 | The GL canvas gets `wxBG_STYLE_PAINT`; on MSW `GLCanvas3D::on_paint` renders immediately because idle events are not dispatched inside the modal resize loop (c06a0223a7) | `OpenGLManager::create_wxglcanvas`, `GLCanvas3D::on_paint` | `webview-gl-aui-media.md` |
| 8248b06337 | On MSW, do not `SetFocus()` the GL canvas while `wxCurrentPopupWindow` is set: the focus change makes wx call `MSWDismissUnfocusedPopup` and closes the search dropdown. `wxCurrentPopupWindow` is a wx-internal global (`src/msw/popupwin.cpp`) that Orca declares `extern` itself, usable only because wx is linked statically | `GLCanvas3D::on_mouse` (`evt.Entering()` branch) | `popups-menus.md` |
| 6148ba16b3 (in 8248b06337), 988b500f33 | On GTK a `wxBitmapToggleButton`/`wxButton`-based widget sized to exactly its bitmap leaves no room for the theme's CSS padding (GTK "negative content width" criticals). Either strip the native button CSS with `Slic3r::GUI::RemoveButtonBorder` and size to the bitmap (`CheckBox`), or size to `GetBestSize()` grown to the bitmap (`IncTo`; `RadioBox` and `SwitchButton` do both) | `RemoveButtonBorder` (`GUI_Utils.cpp`), `CheckBox::Rescale`, `RadioBox::Rescale`, `SwitchButton::Rescale` | `platforms.md` §GTK native chrome |
| 8248b06337 | The macOS-only vertical text nudges in `Button::render` and `SideButton::dorender` were removed; do not re-add per-OS baseline offsets | `Button::render`, `SideButton::dorender` | `painting-custom-widgets.md` |
| 8248b06337 | `wxEXPAND` combined with `wxALIGN_*` in a box sizer was cleaned up; the combination is ignored | `Sidebar::priv::layout_printer`, `AMSControl::createAmsPanel` | `sizers-layout.md` |
| 8248b06337 | `GUI_App::on_init_inner` filters known-harmless GTK criticals (allocation on hidden widgets, events on unrealised widgets, style-context calls from `SetBackgroundColour` before realisation); check that filter before chasing such a message | `GUI_App::on_init_inner` | `platforms.md` |
| 5f365b5c6b | `wxBitmapComboBox` overrides take `wxBitmapBundle` (`OnAddBitmap(const wxBitmapBundle&)`, `include/wx/bmpcbox.h:89`); `m_bitmaps` became `m_bitmapbundles`; get a bitmap with `GetBitmap(GetDefaultSize())` | `BitmapComboBox::OnAddBitmap`, `BitmapComboBox::OnDrawItem` (both macOS-only overrides) | `dpi-bitmaps-fonts.md` |
| ed88cbe3f5 → d62aa42e61 | 3.3's `wxWebViewWebKit` has neither the default ctor nor the creating `(parent, id, url, …)` ctor of 3.1.5; its only ctor is `explicit wxWebViewWebKit(const wxWebViewConfiguration&, WX_NSObject request = nullptr)` (`include/wx/osx/webview_webkit.h:36`). ed88cbe3f5 switched macOS to `wxWebView::New()`, which bypassed the subclass destructor that calls `RemoveScriptMessageHandler("wx")`; d62aa42e61 restored `WebViewWebKit` via `wxWebView::NewConfiguration(wxWebViewBackendWebKit)`. Only Linux uses `wxWebView::New()` | `WebView::CreateWebView`, `WebViewWebKit` | `webview-gl-aui-media.md` |
| 1765d296a8, 2b3328c2b2 | `wxArrayString` via `Add()`; `wxString` first in mixed concatenation; `compatibility_iterator` for wx lists (§4) | `Plater::import_model_id`, `OptionsSearcher::search`, `SendMultiMachinePage` | this file |
| 7ea69199fd | `dynamic_cast`, not `wxDynamicCast`, on `wxComboPopup` (§4) | `combochecklist_get_flags`/`_set_flags` (`GUI.cpp`) | this file |
| ba867cc534 | `Show()` (only if `!IsShown()`) before `Raise()` (§3) | `Plater::priv::priv` handlers, `Plater::priv::bring_instance_forward` | `windows-dialogs.md` |
| 026b105dcb | No `wxTRANSPARENT_WINDOW` (§3); it is a no-op `0`, so this was cleanup, not a build fix | `MainFrame` (`ResizeEdgePanel`, side-tool panels) | this file |
| eefdabcd98, f70d30bf79 (#13074) | The auto-added `WS_CAPTION` (3.3.0, #23575; eefdabcd98's message says 3.3.2) made `DefWindowProc` subtract a caption from the maximised client area, and on Windows 10 left the native frame visible behind the custom title bar ("double window"). The `MainFrame` ctor strips `WS_CAPTION` from `GWL_STYLE` right after creation (`SetWindowPos(… SWP_FRAMECHANGED)`); `WM_NCCALCSIZE` computes border thickness with `GetWindowLongPtr(hWnd, GWL_STYLE) & ~WS_CAPTION` and strips the border overshoot itself when maximised; f70d30bf79 restored the `wxEVT_MAXIMIZE` handler that clamps the maximised frame to the display client area | `MainFrame::MainFrame`, `MainFrame::MSWWindowProc` (`WM_NCCALCSIZE`), `AdjustWorkingAreaForAutoHide` | `platforms.md` §MSW title bar |
| 46e47cec0a | `wxGrid::GetSelectedBlocks()` (unordered, possibly overlapping, `interface/wx/grid.h:5091-5109`) is an empty range after the user deselects; compare `begin()` with `end()` and fall back to the activating cell | `GridCellSupportEditor::DoActivate` (`GUI_ObjectTable.cpp`) | `controls-dataview.md` |
| 9a053f15eb (#12936) | Since the upgrade, on macOS the Slice/Print split-button's transient popup was dismissed the moment the cursor entered the gap between button and menu; on macOS anchor transient popups flush with (slightly overlapping, 2 px) their button | `SidePopup::Popup` (`__APPLE__` branch) | `popups-menus.md` |
| 1f2ed70288 (#13119) | Orca registers its own `kAEGetURL` handler so `orcaslicer://` links reach `MacOpenURL` | `register_mac_deep_link_handler` (`DeepLinkHandlerMac.mm`), called from `GUI_App::on_init_inner` | this file |
These post-upgrade regressions were fixed: 46e47cec0a (ObjectTable crash on cell deselect),
1f2ed70288 (macOS deep links), d62aa42e61 (WebView script-handler cleanup), eefdabcd98 (maximised
window not filling the desktop), f70d30bf79 (Windows 10 double window frame), c06a0223a7 (blank 3D
canvas during MSW resize). Watch for them to
recur when touching the same code.
**Pitfalls**
- **Rule:** Keep Orca's own `kAEGetURL` registration in `GUI_App::on_init_inner`; do not rely on
wx's handler for `orcaslicer://` links.
**Why:** wx installs its handler in `applicationWillFinishLaunching:`
(`src/osx/cocoa/utils.mm:46-57`); it forwards to `MacOpenURL` only once `OSXInitWasCalled()` is
true and otherwise stores the URL with `OSXStoreOpenURL` (:191-200) [source]. After the upgrade
the wx handler stopped delivering deep links on macOS (#13119, reported on macOS 26): links from
Printables/Thingiverse opened a blank project. `NSAppleEventManager` keeps the last registration, so Orca's later registration wins
and routes straight to `MacOpenURL` → `start_download`. Removing it, or registering before wx does,
brings the regression back.
```cpp
// Right (GUI_App::on_init_inner, __APPLE__): register_mac_deep_link_handler();
```
Cite: 1f2ed70288; `DeepLinkHandlerMac.mm`.
- **Rule:** Do not reintroduce `wxTRANSPARENT_WINDOW` or a post-creation
`SetBackgroundStyle(wxBG_STYLE_TRANSPARENT)` to make a panel blend in; set its background colour,
dark-mode mapped, and re-apply it on theme change.
**Why:** see §3. The side-tool panels lost MSW transparency with the upgrade, and 8248b06337
(b9952b39ad) fixed them by giving them `StateColor::darkModeColorFor(wxColour("#3B4446"))`,
re-applied in `MainFrame::update_side_button_style`.
Cite: 026b105dcb, 8248b06337 (b9952b39ad).
+3
View File
@@ -10,3 +10,6 @@
# resume after `call :label`. With LF endings that offset can land wrong and the
# label lookup fails, so keep these CRLF whatever the platform.
*.bat text eol=crlf
# OCCT BRep fixtures, kept byte for byte as OCCT wrote them.
*.brep -text
+9 -11
View File
@@ -5,7 +5,6 @@ on:
branches:
- main
- release/*
- belt-printer
paths:
- 'deps/**'
- 'src/**'
@@ -45,7 +44,7 @@ on:
schedule:
- cron: '0 17 * * *' # run once a day at 1 AM Singapore time (UTC+8)
- cron: '15 2 * * *' # 10:15 AM Singapore time (UTC+8), when macOS runners are least busy
workflow_dispatch: # allows for manual dispatch
inputs:
@@ -56,7 +55,9 @@ on:
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
# Pushes don't cancel a running build, because only a finished branch build
# saves caches every pull request can restore.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
@@ -125,7 +126,7 @@ jobs:
if: ${{ !cancelled() && (github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer') }}
uses: ./.github/workflows/build_check_cache.yml
with:
os: ${{ vars.SELF_HOSTED && 'orca-macos-arm64' || 'macos-14' }}
os: ${{ vars.SELF_HOSTED && 'orca-macos-arm64' || 'macos-15' }}
arch: ${{ matrix.arch }}
build-deps-only: ${{ inputs.build-deps-only || false }}
force-build: ${{ github.event_name == 'schedule' }}
@@ -136,7 +137,7 @@ jobs:
if: ${{ !cancelled() && needs.build_macos_arch.result == 'success' && !inputs.build-deps-only && (github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer') }}
uses: ./.github/workflows/build_orca.yml
with:
os: ${{ vars.SELF_HOSTED && 'orca-macos-arm64' || 'macos-14' }}
os: ${{ vars.SELF_HOSTED && 'orca-macos-arm64' || 'macos-15' }}
arch: universal
macos-combine-only: true
secrets: inherit
@@ -180,7 +181,7 @@ jobs:
if: ${{ !cancelled() && success() }}
uses: ./.github/workflows/unit_tests.yml
with:
os: ${{ vars.SELF_HOSTED && 'orca-macos-arm64' || 'macos-14' }}
os: ${{ vars.SELF_HOSTED && 'orca-macos-arm64' || 'macos-15' }}
artifact: ${{ github.sha }}-tests-macos-arm64
test-dir: build/arm64/tests
# Slice a two-colour cube through every shipped printer, and through every
@@ -260,9 +261,6 @@ jobs:
date:
ver:
ver_pure:
# Belt-printer nightlies share the main nightly release but carry a `_belt`
# suffix so they never overwrite the main assets.
nightly_suffix: ${{ github.ref == 'refs/heads/belt-printer' && '_belt' || '' }}
steps:
- name: "Remove unneeded stuff to free disk space"
run:
@@ -437,13 +435,13 @@ jobs:
name: OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak
path: '/__w/OrcaSlicer/OrcaSlicer/OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak'
- name: Deploy Flatpak to nightly release
if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/belt-printer')
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main'
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: /__w/OrcaSlicer/OrcaSlicer/OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak
asset_name: OrcaSlicer-Linux-flatpak_nightly${{ env.nightly_suffix }}_${{ matrix.variant.arch }}.flatpak
asset_name: OrcaSlicer-Linux-flatpak_nightly_${{ matrix.variant.arch }}.flatpak
asset_content_type: application/octet-stream
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
# The asset is /app (the exes link it at runtime) plus the build tree
+1 -1
View File
@@ -40,7 +40,7 @@ jobs:
# Anything that changes how the tree is built belongs in the key, or a job
# restores one it cannot use. Linux amd64 passes no arch deliberately, so
# 'linux-clang' keeps the cache it already has.
cache-os: ${{ runner.os == 'macOS' && format('macos-{0}', inputs.arch) || (runner.os == 'Windows' && format('windows-{0}-{1}', inputs.arch, inputs.compiler) || format('linux-clang{0}', inputs.arch && format('-{0}', inputs.arch) || '')) }}
cache-os: ${{ runner.os == 'macOS' && format('{0}-{1}', inputs.os, inputs.arch) || (runner.os == 'Windows' && format('windows-{0}-{1}', inputs.arch, inputs.compiler) || format('linux-clang{0}', inputs.arch && format('-{0}', inputs.arch) || '')) }}
# The Windows ARM64 deps build in build-arm64, all others under build;
# build_deps.yml and build_orca.yml pass the Windows directory to build_win.bat.
dep-folder-name: ${{ runner.os == 'macOS' && format('/{0}', inputs.arch) || (runner.os == 'Windows' && inputs.arch == 'arm64') && '-arm64/OrcaSlicer_dep' || '/OrcaSlicer_dep' }}
-17
View File
@@ -49,28 +49,11 @@ jobs:
key: ${{ inputs.cache-key }}
- uses: lukka/get-cmake@latest
# The windows-11-arm runner needs CMake <= 3.31 (handled in the next step).
if: ${{ !(runner.os == 'Windows' && inputs.arch == 'arm64') }}
with:
cmakeVersion: "~4.3.0" # use most recent 4.3.x version
useLocalCache: true # <--= Use the local cache (default is 'false').
useCloudCache: true
- name: Install CMake 3.31.x (Windows ARM64)
# windows-11-arm ships CMake 4.x, which removed pre-3.5 policy
# compatibility AND has incomplete ASM_ARMASM linker modules
# (breaks Boost.Context on ARM64). Pin to the last 3.x release.
if: runner.os == 'Windows' && inputs.arch == 'arm64'
shell: pwsh
run: |
$ver = "3.31.6"
$url = "https://github.com/Kitware/CMake/releases/download/v$ver/cmake-$ver-windows-arm64.zip"
Invoke-WebRequest -Uri $url -OutFile "$env:RUNNER_TEMP\cmake.zip"
Expand-Archive -Path "$env:RUNNER_TEMP\cmake.zip" -DestinationPath "$env:RUNNER_TEMP\cmake" -Force
$cmakeBin = "$env:RUNNER_TEMP\cmake\cmake-$ver-windows-arm64\bin"
if (-not (Test-Path "$cmakeBin\cmake.exe")) { throw "cmake.exe not found at $cmakeBin" }
Add-Content -Path $env:GITHUB_PATH -Value $cmakeBin
- name: setup dev on Windows
if: runner.os == 'Windows'
uses: microsoft/setup-msbuild@v3
+19 -41
View File
@@ -33,11 +33,6 @@ jobs:
ubuntu-ver: '2404'
ubuntu-ver-str: '_Ubuntu2404'
ORCA_UPDATER_SIG_KEY: ${{ secrets.ORCA_UPDATER_SIG_KEY }}
# Branches whose builds are published to the nightly release. The
# belt-printer branch ships alongside main but its assets carry a `_belt`
# suffix (nightly_suffix) so they never overwrite the main nightly assets.
deploy_nightly: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/belt-printer' }}
nightly_suffix: ${{ github.ref == 'refs/heads/belt-printer' && '_belt' || '' }}
steps:
- name: Checkout
@@ -54,28 +49,11 @@ jobs:
fail-on-cache-miss: true
- uses: lukka/get-cmake@latest
# The windows-11-arm runner needs CMake <= 3.31 (handled in the next step).
if: ${{ !(runner.os == 'Windows' && inputs.arch == 'arm64') }}
with:
cmakeVersion: "~4.3.0" # use most recent 4.3.x version
useLocalCache: true # <--= Use the local cache (default is 'false').
useCloudCache: true
- name: Install CMake 3.31.x (Windows ARM64)
# windows-11-arm ships CMake 4.x, which removed pre-3.5 policy
# compatibility AND has incomplete ASM_ARMASM linker modules
# (breaks Boost.Context on ARM64). Pin to the last 3.x release.
if: runner.os == 'Windows' && inputs.arch == 'arm64'
shell: pwsh
run: |
$ver = "3.31.6"
$url = "https://github.com/Kitware/CMake/releases/download/v$ver/cmake-$ver-windows-arm64.zip"
Invoke-WebRequest -Uri $url -OutFile "$env:RUNNER_TEMP\cmake.zip"
Expand-Archive -Path "$env:RUNNER_TEMP\cmake.zip" -DestinationPath "$env:RUNNER_TEMP\cmake" -Force
$cmakeBin = "$env:RUNNER_TEMP\cmake\cmake-$ver-windows-arm64\bin"
if (-not (Test-Path "$cmakeBin\cmake.exe")) { throw "cmake.exe not found at $cmakeBin" }
Add-Content -Path $env:GITHUB_PATH -Value $cmakeBin
# Compiler cache. Pushes save it, so main keeps it warm; pull requests
# restore it and discard what they compiled. Objects are keyed on the
# preprocessed source, the compiler and the flags, so a leg only ever
@@ -200,10 +178,12 @@ jobs:
- name: Free disk space
if: runner.os == 'macOS' && !inputs.macos-combine-only && !vars.SELF_HOSTED
run: |
df -hI /dev/disk3s1s1
sudo find /Applications -maxdepth 1 -type d -name "Xcode_*.app" ! -name "Xcode_15.4.app" -exec rm -rf {} +
df -hI /
# Keep only the selected Xcode
xcode=$(basename "$(cd "$(xcode-select -p)/../.." && pwd -P)")
sudo find /Applications -maxdepth 1 -type d -name "Xcode_*.app" ! -name "$xcode" -exec rm -rf {} +
sudo rm -rf ~/Library/Developer/CoreSimulator/Caches/*
df -hI /dev/disk3s1s1
df -hI /
- name: Build slicer mac
if: runner.os == 'macOS' && !inputs.macos-combine-only
@@ -275,7 +255,8 @@ jobs:
# Thanks to RaySajuuk, it's working now
- name: Sign app and notary
if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/belt-printer' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'macOS' && inputs.macos-combine-only
if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'macOS' && inputs.macos-combine-only
timeout-minutes: 30
working-directory: ${{ github.workspace }}
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
@@ -362,7 +343,7 @@ jobs:
fi
- name: Create DMG without notary
if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/belt-printer' && runner.os == 'macOS' && inputs.macos-combine-only
if: github.ref != 'refs/heads/main' && runner.os == 'macOS' && inputs.macos-combine-only
working-directory: ${{ github.workspace }}
run: |
# Load the `retry` helper (retries flaky commands such as `hdiutil create`).
@@ -408,13 +389,13 @@ jobs:
if-no-files-found: ignore
- name: Deploy Mac release
if: github.repository == 'OrcaSlicer/OrcaSlicer' && env.deploy_nightly == 'true' && runner.os == 'macOS' && inputs.macos-combine-only && !vars.SELF_HOSTED
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'macOS' && inputs.macos-combine-only && !vars.SELF_HOSTED
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ${{ github.workspace }}/OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
asset_name: OrcaSlicer_Mac_universal_nightly${{ env.nightly_suffix }}.dmg
asset_name: OrcaSlicer_Mac_universal_nightly.dmg
asset_content_type: application/octet-stream
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
@@ -557,24 +538,24 @@ jobs:
path: ${{ github.workspace }}/build/src/Release/OrcaSlicer_profile_validator.exe
- name: Deploy Windows release portable
if: github.repository == 'OrcaSlicer/OrcaSlicer' && env.deploy_nightly == 'true' && runner.os == 'Windows' && !vars.SELF_HOSTED
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable.zip
asset_name: OrcaSlicer_Windows${{ env.ARCH_SUFFIX }}_nightly${{ env.nightly_suffix }}_portable.zip
asset_name: OrcaSlicer_Windows${{ env.ARCH_SUFFIX }}_nightly_portable.zip
asset_content_type: application/x-zip-compressed
max_releases: 1
- name: Deploy Windows release installer
if: github.repository == 'OrcaSlicer/OrcaSlicer' && env.deploy_nightly == 'true' && runner.os == 'Windows' && !vars.SELF_HOSTED
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe
asset_name: OrcaSlicer_Windows_Installer${{ env.ARCH_SUFFIX }}_nightly${{ env.nightly_suffix }}.exe
asset_name: OrcaSlicer_Windows_Installer${{ env.ARCH_SUFFIX }}_nightly.exe
asset_content_type: application/x-msdownload
max_releases: 1
@@ -715,23 +696,20 @@ jobs:
path: './build/src/dev-utils/Release/generate_system_cache'
- name: Deploy Ubuntu release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && env.deploy_nightly == 'true' && runner.os == 'Linux' && !vars.SELF_HOSTED }}
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED }}
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ./build/OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_${{ env.ver }}.AppImage
asset_name: OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_nightly${{ env.nightly_suffix }}.AppImage
asset_name: OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_nightly.AppImage
asset_content_type: application/octet-stream
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
- name: Deploy Ubuntu release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
uses: rickstaa/action-create-tag@v1
with:
tag: "nightly-builds"
tag_exists_error: false
force_push_tag: true
message: "nightly-builds"
run: |
git -c user.name="${GITHUB_ACTOR}" -c user.email="${GITHUB_ACTOR}@users.noreply.github.com" tag -f -a nightly-builds "${GITHUB_SHA}" -m nightly-builds
git push -f origin refs/tags/nightly-builds
- name: Deploy Ubuntu OrcaSlicer_profile_validator release
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED && inputs.arch != 'aarch64' }}
+14
View File
@@ -74,12 +74,26 @@ jobs:
set +e
./OrcaSlicer_profile_validator -p ${{ github.workspace }}/resources/profiles -l 2 2>&1 | tee ${{ runner.temp }}/validate_system.log
exit ${PIPESTATUS[0]}
# The validator above is the nightly build of main, so it cannot slice profiles that use
# settings a PR adds to the engine: it reports their placeholders as undefined. A PR that
# changes src/ also runs Build all, whose Slice check runs this same sweep with the
# validator built from the PR, so the sweep below only runs for the other PRs.
- name: Detect engine changes
id: engine_changes
if: ${{ github.event_name == 'pull_request' }}
run: |
base=${{ github.event.pull_request.base.sha }}
if git fetch --no-tags --depth=1 origin "$base" && ! git diff --quiet "$base" HEAD -- src/; then
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "::notice::This PR changes src/, so Build all's Slice check slices the profiles with the PR-built validator."
fi
# Slice a two-colour cube through every printer, and through every system process/filament whose
# templates no printer's own slice reaches, so every custom g-code and filename_format shipped is
# expanded (names in {if} branches not taken included) - catches undefined-placeholder /
# invalid-flow bugs the static checks above cannot see.
- name: validate slice (expand custom g-code)
id: validate_slice
if: ${{ steps.engine_changes.outputs.changed != 'true' }}
continue-on-error: true
run: |
set +e
+89
View File
@@ -0,0 +1,89 @@
name: clang-tidy
# Runs clang-tidy, with the checks in .clang-tidy, over the C++ lines a pull
# request changes (scripts/clang_tidy_diff.py). The compile database is
# configured without the precompiled header, so code that only builds because
# the PCH supplied an include fails here.
#
# No paths filter: the job is a required check, and a workflow skipped by a
# paths filter leaves a required check pending forever. A PR with no C++
# changes finishes after the first step.
on:
pull_request:
branches:
- main
- release/*
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
clang_tidy:
# Branch protection requires this check by name. Renaming the job disables
# that gate.
name: clang-tidy
runs-on: ${{ vars.SELF_HOSTED && 'orca-lnx-server' || 'ubuntu-24.04' }}
steps:
- name: Checkout
uses: actions/checkout@v7
with:
lfs: 'false'
# The PR merge commit plus its first parent, the base it is diffed against.
fetch-depth: 2
- name: Look for changed C++ files
id: changes
run: |
if git -c core.quotePath=false diff --name-only HEAD^1 -- src tests | grep -qE '\.(cpp|cc|cxx|hpp|h|hxx)$'; then
echo "cpp=true" >> "$GITHUB_OUTPUT"
else
echo "No C++ changes under src/ or tests/."
fi
# Parsing needs the dependency headers, not a build of this PR's deps/, so
# a PR that changes deps/ falls back to the newest cache main has.
- name: Restore cached deps
if: steps.changes.outputs.cpp == 'true'
uses: actions/cache/restore@v6
with:
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
key: linux-clang-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }}
restore-keys: linux-clang-cache-orcaslicer_deps-build-
fail-on-cache-miss: true
- name: Apt-Install Dependencies
if: steps.changes.outputs.cpp == 'true' && !vars.SELF_HOSTED
uses: ./.github/actions/apt-install-deps
- name: Install clang-tidy
if: steps.changes.outputs.cpp == 'true'
run: |
python3 -m venv "$RUNNER_TEMP/clang-tidy"
"$RUNNER_TEMP/clang-tidy/bin/pip" install --quiet -r scripts/clang_tidy_requirements.txt
# DEP_BUILD_DIR is named outright: CMake would otherwise derive it from the build
# directory's name and look for the dependencies in deps/build-tidy.
- name: Configure without the precompiled header
if: steps.changes.outputs.cpp == 'true'
run: >
cmake -S . -B build-tidy -G Ninja
-DCMAKE_BUILD_TYPE=Release
-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
-DSLIC3R_PCH=OFF -DORCA_TOOLS=ON -DBUILD_TESTS=ON
-DDEP_BUILD_DIR=${{ github.workspace }}/deps/build
# The one header the build generates rather than the configure.
- name: Generate git_commit_hash.h
if: steps.changes.outputs.cpp == 'true'
run: cmake --build build-tidy --target git_commit_hash_header
- name: Run clang-tidy on the changed lines
if: steps.changes.outputs.cpp == 'true'
run: >
python3 scripts/clang_tidy_diff.py -p build-tidy --base HEAD^1
--clang-tidy "$RUNNER_TEMP/clang-tidy/bin/clang-tidy" -j "$(nproc)"
+28
View File
@@ -0,0 +1,28 @@
# Reports the newest push build of main that was not cancelled, for the README badge.
name: Main build status
on:
workflow_run:
workflows: ["Build all"]
types: [completed]
branches: [main]
workflow_dispatch:
permissions:
actions: read
jobs:
status:
if: github.repository == 'OrcaSlicer/OrcaSlicer'
runs-on: ubuntu-latest
steps:
- env:
GH_TOKEN: ${{ github.token }}
run: |
for attempt in 1 2 3; do
conclusion=$(gh api "repos/${{ github.repository }}/actions/workflows/build_all.yml/runs?branch=main&event=push&status=completed&per_page=100" \
--jq '[.workflow_runs[] | select(.conclusion != "cancelled")][0].conclusion') && break
sleep 10
done
echo "Latest finished push build of main: $conclusion"
[ "$conclusion" = success ]
+2 -2
View File
@@ -156,7 +156,7 @@ jobs:
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libopengl0 libglu1-mesa libgl1 libegl1 libwebkit2gtk-4.1-0
libopengl0 libgl1 libegl1 libwebkit2gtk-4.1-0
- uses: actions/setup-python@v6
with:
@@ -224,7 +224,7 @@ jobs:
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
xvfb xdotool imagemagick openbox mesa-utils \
libopengl0 libglu1-mesa libgl1 libegl1 libwebkit2gtk-4.1-0
libopengl0 libgl1 libegl1 libwebkit2gtk-4.1-0
- name: Run the parity harness
run: |
+1 -1
View File
@@ -303,7 +303,7 @@ jobs:
- name: Mint profiles-repo token
id: token
if: steps.vendors.outputs.vendors != ''
uses: actions/create-github-app-token@v1
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.PROFILES_APP_ID }}
private-key: ${{ secrets.PROFILES_APP_PRIVATE_KEY }}
+22 -22
View File
@@ -96,7 +96,7 @@ jobs:
issues: write
runs-on: ubuntu-latest
steps:
- name: Auto-label and remind about the localization glossary
- name: Auto-label localization changes and remind contributors about the glossary
uses: actions/github-script@v9
with:
script: |
@@ -106,37 +106,37 @@ jobs:
const pr = context.payload.pull_request;
// List changed files once (mirrors the `localization/**` paths filter in check_locale.yml)
// Mirrors the `localization/**` paths filter in check_locale.yml
const files = await github.paginate(github.rest.pulls.listFiles, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
per_page: 100
});
const touchesLocalization = files.some((file) => file.filename.startsWith('localization/'));
const onlyPoFiles = files.length > 0 && files.every((file) => file.filename.endsWith('.po'));
if (!files.some((file) => file.filename.startsWith('localization/'))) {
core.info('No localization changes detected; skipping Localization label.');
return;
}
// If the PR changes only .po files, automatically apply the Localization label
if (onlyPoFiles) {
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
labels: ['Localization']
});
core.info('Applied Localization label (PR changes only .po files).');
} catch (error) {
if (isPermissionDenied(error)) {
core.warning('Cannot add Localization label because token cannot write.');
} else {
throw error;
}
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
labels: ['Localization']
});
core.info('Applied Localization label.');
} catch (error) {
if (isPermissionDenied(error)) {
core.warning('Cannot add Localization label because token cannot write.');
} else {
throw error;
}
}
if (!touchesLocalization) {
core.info('No localization changes detected; skipping glossary reminder.');
// Collaborators know the glossary; only remind outside contributors
if (['COLLABORATOR', 'OWNER', 'MEMBER'].includes(pr.author_association)) {
core.info('Author is a collaborator; skipping glossary reminder.');
return;
}
-8
View File
@@ -44,14 +44,6 @@ jobs:
uses: actions/download-artifact@v8
with:
name: ${{ inputs.artifact }}
# run_unit_tests.sh installs the plugin tests' numpy with the uv the build stages
# beside them; the Windows arm64 build bundles none, so put one on PATH there.
- name: Install uv
if: runner.os == 'Windows' && runner.arch == 'ARM64'
uses: astral-sh/setup-uv@v10.2.0
with:
version: "0.11.21" # ORCA_UV_VERSION in CMakeLists.txt
enable-cache: false
- uses: lukka/get-cmake@latest
with:
cmakeVersion: "~4.3.0" # use most recent 4.3.x version
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
uses: actions/checkout@v7
- name: Setup Python
uses: actions/setup-python@v6
uses: actions/setup-python@v7
with:
python-version: '3.12'
+8 -4
View File
@@ -4,15 +4,17 @@ OrcaSlicer — open-source C++17 3D slicer. wxWidgets GUI, CMake build system.
## Build Commands
Build the Release configuration unless asked otherwise.
```bash
# macOS
cmake --build build/arm64 --config RelWithDebInfo --target all --
cmake --build build/arm64 --config Release --target all --
# Linux
cmake --build build --config RelWithDebInfo --target all --
cmake --build build --config Release --target all --
# Windows (replace %build_type% with Debug/Release/RelWithDebInfo)
cmake --build . --config %build_type% --target ALL_BUILD -- -m
# Windows
cmake --build . --config Release --target ALL_BUILD -- -m
```
## Testing
@@ -36,6 +38,7 @@ ctest --test-dir ./tests/fff_print -C Release
- C++17, selective C++20. PascalCase classes, snake_case functions/variables
- `#pragma once` for headers. Smart pointers and RAII preferred
- Include what you use: include the header for every symbol a file uses, and keep headers compilable on their own. Never rely on the precompiled header or a transitive include. The `clang-tidy` CI job enforces this on changed lines; run the same check locally with `scripts/run_clang_tidy.sh` (`scripts\run_clang_tidy.ps1` on Windows), which sets up everything it needs
- Parallelization via TBB — be mindful of shared state
- Always use `SetSizerAndFit(sizer)` instead of `SetSizer(sizer)` on top level window. Unless `SetSizer` must be called before the full layout is built, call `sizer->SetSizeHints(window)` afterwards in this case.
@@ -89,6 +92,7 @@ See the [Localization guide](https://github.com/OrcaSlicer/OrcaSlicer_WIKI/blob/
- Plural entries: read `nplurals` from the catalog's `Plural-Forms` header (it is **not** always 2 — ja/ko/zh/th/vi use 1, ru/cs/pl/lt use 3, uk uses 4). Each form must be genuinely inflected for its quantity; repeating one sentence across all forms is a bug in Slavic/Baltic languages, though it is correct for Turkish and Hungarian.
- An entry whose `msgstr` equals its `msgid` is untranslated even though it is not empty; a plural entry with any empty form is likewise incomplete.
- Mark machine-produced translations with an `# AI Translated` translator comment. Don't add it to a human translation you didn't actually rewrite.
- When you can't be sure of a machine translation's meaning or UI wording, also add `# Needs human review: <what to check>`. Never use `fuzzy` for this — fuzzy entries are hidden from users.
- Don't reflow or re-wrap unrelated entries — keep the diff limited to the strings you changed.
### Verifying
+20 -5
View File
@@ -135,6 +135,7 @@ set(ORCA_UV_SHA256_aarch64-apple-darwin "1f921d491ba5ffeea774eb04d6681ecee3
set(ORCA_UV_SHA256_x86_64-apple-darwin "f3c8e5708a84b920c18b691214d54d2b0da6b984789caae95d47c95120cb7765")
set(ORCA_UV_SHA256_aarch64-unknown-linux-gnu "88e800834007cc5efd4675f166eb2a51e7e3ad19876d85fa8805a6fb5c922397")
set(ORCA_UV_SHA256_x86_64-unknown-linux-gnu "8c88519b0ef0af9801fcdee419bbb12116bd9e6b18e162ae093c932d8b264050")
set(ORCA_UV_SHA256_aarch64-pc-windows-msvc "74e443f8004022dde57a1bd0d10c097830f9ea8feb4ec927db52cd5d805c2f48")
set(ORCA_UV_SHA256_x86_64-pc-windows-msvc "ace861f360c6de2babedc1607d0f454b6b09a820dbc8182dc15af927e4df9589")
# Version-scoped cache dir so a version bump invalidates the cached binary.
@@ -173,7 +174,10 @@ if(NOT ORCA_BUNDLED_UV_EXECUTABLE)
set(ORCA_UV_ARCH "x86_64-unknown-linux-gnu")
endif()
elseif(_orca_uv_proc MATCHES "aarch64|arm64|ARM64")
if(APPLE)
if(WIN32)
set(ORCA_UV_ARCH "aarch64-pc-windows-msvc")
set(ORCA_UV_EXT "zip")
elseif(APPLE)
set(ORCA_UV_ARCH "aarch64-apple-darwin")
else()
set(ORCA_UV_ARCH "aarch64-unknown-linux-gnu")
@@ -819,7 +823,9 @@ if(SLIC3R_STATIC)
set(TBB_STATIC 1)
endif()
set(TBB_DEBUG 1)
set(CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO RelWithDebInfo Release "")
if ("${CMAKE_BUILD_TYPE}" STREQUAL "RelWithDebInfo" OR MSVC)
set(CMAKE_MAP_IMPORTED_CONFIG_RELWITHDEBINFO RelWithDebInfo Release "")
endif()
find_package(TBB REQUIRED)
# include_directories(SYSTEM ${TBB_INCLUDE_DIRS})
# add_definitions(${TBB_DEFINITIONS})
@@ -1078,10 +1084,10 @@ endif ()
find_path(SPNAV_INCLUDE_DIR spnav.h)
if (SPNAV_INCLUDE_DIR)
find_library(SPNAV_LIB NAMES libspnav.a) # Force linking libspnav statically
find_library(SPNAV_LIB NAMES libspnav.a spnav)
if (SPNAV_LIB)
add_definitions(-DHAVE_SPNAV)
message(STATUS "SPNAV library found")
message(STATUS "SPNAV library found: ${SPNAV_LIB}")
else()
message(STATUS "SPNAV library NOT found, Spacenavd not supported")
endif()
@@ -1125,7 +1131,8 @@ function(orcaslicer_copy_dlls target config postfix output_dlls)
if (NOT OCCT_LIBS)
message(FATAL_ERROR "OCCT_LIBS is not set; libslic3r must be configured first.")
endif ()
set(_occt_bin "${CMAKE_PREFIX_PATH}/bin/occt")
string(TOUPPER "${config}" _config_upper)
set(_occt_bin "${OCCT_BIN_DIR_${_config_upper}}")
set(_occt_dlls "")
set(_occt_staged "")
set(_missing_occt "")
@@ -1301,6 +1308,14 @@ if (WIN32)
endif()
set(CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS_SKIP TRUE)
include(InstallRequiredSystemLibraries)
# A missing MSVC runtime is an error because an installer without it cannot start on a clean machine.
set(_orca_runtime_names ${CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS})
list(TRANSFORM _orca_runtime_names REPLACE "^.*/" "")
if (MSVC AND (NOT "msvcp140.dll" IN_LIST _orca_runtime_names OR NOT "vcruntime140.dll" IN_LIST _orca_runtime_names))
set(_orca_runtime_error "CMake ${CMAKE_VERSION} did not find msvcp140.dll and vcruntime140.dll for MSVC ${MSVC_VERSION}. Update CMake to a release that supports this Visual Studio.")
message(WARNING "${_orca_runtime_error}")
install(CODE "message(FATAL_ERROR \"${_orca_runtime_error}\")")
endif ()
install (PROGRAMS ${CMAKE_INSTALL_SYSTEM_RUNTIME_LIBS} DESTINATION ".")
elseif (SLIC3R_FHS)
# CMAKE_INSTALL_FULL_DATAROOTDIR: read-only architecture-independent data root (share)
+5 -12
View File
@@ -6,7 +6,7 @@
<a href="https://trendshift.io/repositories/15552" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15552" alt="OrcaSlicer%2FOrcaSlicer | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![GitHub Repo stars](https://img.shields.io/github/stars/OrcaSlicer/OrcaSlicer)](https://github.com/OrcaSlicer/OrcaSlicer/stargazers) [![Build all](https://github.com/OrcaSlicer/OrcaSlicer/actions/workflows/build_all.yml/badge.svg?branch=main)](https://github.com/OrcaSlicer/OrcaSlicer/actions/workflows/build_all.yml)
[![GitHub Repo stars](https://img.shields.io/github/stars/OrcaSlicer/OrcaSlicer)](https://github.com/OrcaSlicer/OrcaSlicer/stargazers) [![Build all](https://img.shields.io/github/actions/workflow/status/OrcaSlicer/OrcaSlicer/main_build_status.yml?label=Build%20all)](https://github.com/OrcaSlicer/OrcaSlicer/actions/workflows/build_all.yml)
OrcaSlicer: an open source Next-Gen Slicing Software for Precision 3D Prints.
Optimize your prints with ultra-fast slicing, intelligent support generation, and seamless printer compatibility—engineered for perfection.
@@ -68,6 +68,10 @@ If you come across any of these in search results, please <b>report them</b> as
Regular updates fueled by continuous community contributions.
- **Wide Printer Compatibility**
Supports a broad range of printers: Bambu Lab, Prusa, Creality, Voron, and more.
- **[Belt Printer Support](https://www.orcaslicer.com/wiki/belt_printing)**
Slice for belt / conveyor (infinite-Z) printers, with belt-aware supports and a tilted-bed preview. Contributed by [Joseph Robertson (@HarrierPigeon)](https://github.com/HarrierPigeon).
- **[IDEX/IQEX Parallel Printing Support](https://www.orcaslicer.com/wiki/idex_iqex_parallel_printing)**
Print copies or mirror images of a part on every carriage of an IDEX or IQEX printer at once, with the mode chosen per plate and nozzle clearance zones shown on the bed. Contributed by [Clifford (@cgarwood82)](https://github.com/cgarwood82).
- Additional features can be found in the [change notes](https://github.com/OrcaSlicer/OrcaSlicer/releases/).
# Wiki
@@ -89,17 +93,6 @@ Visit our GitHub Releases page for the latest stable version of OrcaSlicer, reco
🌙 **[Download the Latest Nightly Build](https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds)**
Explore the latest developments in OrcaSlicer with our nightly builds. Feedback on these versions is highly appreciated.
### Belt Printer Builds
The [nightly release](https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds) ships **two parallel builds**: the standard build and a belt-printer build. Both are attached to the same release — tell them apart by the filename suffix:
- **Standard** — no suffix (e.g. `OrcaSlicer_Windows_Installer_x64_nightly.exe`)
- **Belt** — `_belt` suffix (e.g. `OrcaSlicer_Windows_Installer_x64_nightly_belt.exe`)
The `_belt` builds add **experimental support for belt / conveyor (infinite-Z) printers**, where the model is sliced against a tilted belt surface instead of a flat horizontal bed. They include ready-to-use belt printer profiles, the full belt slicing pipeline (mesh rotation and G-code transforms), belt-aware support generation, and a tilted-bed preview.
> ⚠️ Belt printer support is under active development and is **not yet merged into `main`** — it currently ships only in these parallel `_belt` builds, produced from the [`belt-printer`](https://github.com/OrcaSlicer/OrcaSlicer/tree/belt-printer) branch. See tracking PR [#14394](https://github.com/OrcaSlicer/OrcaSlicer/pull/14394) and the original documentation in [#12998](https://github.com/OrcaSlicer/OrcaSlicer/pull/12998).
# How to install
## Windows
-581
View File
@@ -1,581 +0,0 @@
# Texture Displacement - Technical Notes
Branch: `feature/texture_displacement`. Reference for the feature as it stands: what it does, how the
algorithms work, and where the code lives.
## What it does
A paint-style gizmo (`GLGizmoTextureDisplacement`) that lets you:
- Paint one or more "layers" onto a model's surface, each a height-map texture with its own
depth/tiling/rotation/offset/invert/tile-mode/projection-mode/blend-mode.
- Pick a texture from a shipped library (`resources/textures/displacement/`) or import your own
(saved into `<data_dir>/textures/displacement/`, kept separate so app updates can't clobber it).
- Combine overlapping layers with image-editor-style blend modes (Add/Subtract/Multiply/Divide).
- Preview the true displaced result live, before baking (background job, not on the UI thread).
- Preview via a fast GPU shader instead (no real geometry movement) for a lighter-weight alternative.
- Bake into real mesh geometry on demand, restricted to the painted area only.
- Remesh and subdivide so a low-poly model has enough vertices to show fine detail.
- Unwrap a painted patch with a real CGAL LSCM parameterization and view it in a dedicated,
dockable 2D "UV Editor" pane.
## Standard vs Pro mode
A two-position slider in the panel header, right of the Dock/Undock button.
**Pro** shows every mesh-preparation control; Remesh, Subdivide and Bake are run separately by the user,
in whatever order they like.
**Standard** hides all of it and folds one fixed recipe into the Bake button, because a height map only
ever *moves vertices that already exist* - painting onto an imported 12-triangle box and pressing Bake
would otherwise do nothing visible. Standard's Bake is:
1. `plan_remesh()` + `replace_mesh_keep_all_paint()` - isotropic remesh to 1 mm, sharp edges above 40
degrees protected. Gives the subdivider an even starting density whatever the input looked like.
2. `plan_adaptive_subdivision()` + `apply_adaptive_subdivision()` - feature-adaptive refinement, max
edge 20 mm, detail 0.02 mm, min edge 0.02 mm.
3. `bake()` - the ordinary background displacement job.
Both preparation stages are *planned* before the undo snapshot and *applied* after it, so a stage with
nothing to do is skipped without leaving an empty undo step. The standalone Pro buttons share the same
plan/apply split.
**All three stages sit under one undo step.** `Plater::take_snapshot()` records the state *before* the
change, so a single snapshot taken at the top of `bake_standard()` means one Undo returns the mesh to
exactly what was imported. `TextureDisplacementBakeInput::take_snapshot` lets the caller say who owns
the undo step - true for the Pro-mode button, false for the pipeline, whose background job commits long
after that snapshot's scope has closed.
The presets live in one place (`STD_*` constants) and `apply_standard_mode_presets()` pins the hidden
controls to them every frame while Standard is active, so the live preview cannot disagree with what
Bake will do. Switching to Standard also closes the subdivision preview, whose controls have just gone.
One control survives into Standard: **"Added triangles (k)"**, the subdivision budget. It is deliberately
*not* pinned - pinning would fight the user's own slider every frame - because unlike the rest of the
recipe its right value depends on the part rather than on the method (a big model, or a fine texture,
simply needs more triangles). Default 1500. The widget is one lambda shared by both layouts.
Standard remeshes *after* painting, so the remesh has to preserve paint: `ModelVolume::restore_painting()`
only remaps the four standard channels, so `replace_mesh_keep_all_paint()` additionally runs
`TriangleSelector::remap_painting()` over the eight texture-displacement masks. The Pro Remesh button
goes through the same helper. If the remap comes back empty the pipeline stops with a message rather
than baking a flat mesh.
## Architecture
### Data model (per `ModelVolume`)
Each of up to `TEXTURE_DISPLACEMENT_MAX_LAYERS` (8) layers gets its **own independent
`FacetsAnnotation`** paint mask - the same `TriangleSelector`/`FacetsAnnotation` machinery every other
paint gizmo (FdmSupports, Seam, MMU, FuzzySkin) already uses, just one full instance per layer slot
instead of one per volume. This is what makes layered/blended painting work for free: the same triangle
can be `ENFORCER` in layer 2's mask and layer 5's mask simultaneously, and at bake/preview time each
layer displaces the surface left by the previous one (image-editor-layer semantics).
Whole-stack settings (border handling, post-process smoothing) live beside the layers in
`texture_displacement_options` (`TextureDisplacementOptions`), since they belong to no single layer.
### Bake algorithm (`libslic3r/TextureDisplacement.cpp`)
`build_texture_displacement(base_mesh, layers, facets_data, options)` is **accumulate-then-displace,
and topology-preserving**: the returned mesh has exactly the input's vertices and triangles, in the
same order - only the positions of displaced vertices differ.
1. `its_compactify_vertices()` on a copy of the input. In practice a no-op (it only drops
*unreferenced* vertices, and preserves the order and indices of the rest). It is there to
guarantee the index alignment step 3 depends on.
2. Area-weighted vertex normals of the **undisplaced** mesh, computed once. Every layer both projects
and displaces along these, so a vertex covered by several layers moves along one single well-defined
direction. Where the paint does *not* cover every triangle around a vertex, the normal is recomputed
from the painted triangles alone (the union over all layers, so it stays one direction per vertex):
on the rim of a fully painted top face the whole-mesh normal is the 45-degree bisector it shares with
the side wall, and displacing along that flares the rim outwards instead of raising it. Interior
vertices are unaffected - all their triangles are painted, so the two normals coincide. Paint
coverage per original triangle comes straight off `TriangleSplittingData::triangles_to_split`.
3. For each layer in slot order: deserialize its stored paint mask into a `TriangleSelector` against
the **base mesh** (never against a previous layer's output), then
`selector.get_facets_strict(ENFORCER)` -> the painted patch. Two facts are exploited:
- `get_facets_strict()` returns the mesh's **entire** referenced vertex array regardless of which
state was asked for - only `.indices` is filtered by state. So `get_facets_strict(ENFORCER)`
and `get_facets_strict(NONE)` share identical vertex indexing, which is what lets boundary
detection be a plain index check instead of a position-hash lookup.
- The selector's vertex array *starts with* the mesh's own vertices (extra ones created where a
brush stroke split a triangle are appended after them), and `get_facets_strict()` emits the
referenced ones in order. Combined with step 1, **selector vertex index `i` is our vertex `i`**.
Split vertices live past the end of our array and are simply skipped - they sit on the paint
boundary anyway (splitting only happens at partial coverage).
4. A vertex used by at least one **unpainted** triangle is a border vertex. Whether it moves is
`TextureDisplacementOptions::displace_border`, and it does by default. Nothing can tear: the bake is
topology-preserving, so a border vertex is *one* vertex shared by both regions and moving it simply
tilts the unpainted triangles that use it. Pinning it instead clamps the outermost ring of relief to
zero, which on a fully painted face collapses the pattern into a ring of steep ramps at the edge; it
is kept as an option for when the relief must not spill past the paint at all. Either way the border
drives the `edge_smoothing` falloff.
5. Per interior vertex: sample the height texture (`sample_layer_height()`, see Projection methods)
and fold `height * depth_mm * (invert ? -1 : 1)` into that vertex's running total via the layer's
`TextureBlendMode` (see Blend modes). A `visited` set makes each layer fold in exactly **once**
per vertex, no matter how many of the patch's triangles share it - otherwise a Multiply/Subtract
layer would apply two or three times over depending on local triangle fan-out.
6. Move each touched vertex along its (step 2) normal by its accumulated total.
7. Optionally (`TextureDisplacementOptions::smooth_*`) relax the result - see Post-process smoothing.
### Post-process smoothing
`smooth_mesh_vertices(mesh, movable, strength, iterations)` - Laplacian relaxation, run after all layers
have been folded in, restricted to the vertices flagged in `movable`. Each pass moves a movable vertex a
`strength` fraction of the way to the average of its one-ring, read from a **snapshot** of the previous
pass so the result does not depend on vertex order (a Gauss-Seidel sweep would smooth several times as
hard at the end of the array as at the start). Neighbours come from a CSR-style adjacency built once per
call. Topology-preserving, like the bake.
Its job is to round off the hard steps a bitmap height map leaves behind - a different knob from
`TextureDisplacementLayer::smoothing`, which blurs the *height map* before it is ever sampled.
Two ways in, sharing one set of settings on the volume:
- The **"Smooth result"** checkbox + "Smoothing (%)" / "Passes" ride along with Preview and Bake.
`movable` is exactly the set of vertices the displacement moved, so the untouched part of the model
keeps its exact geometry and the ring just outside the displaced set anchors the relaxation (the
relief cannot creep outward).
- **"Smooth baked mesh now"** (`GLGizmoTextureDisplacement::smooth_model()`) applies the same settings to
the volume's *committed* geometry, for relief that is already baked in. `movable` there is the painted
triangles' vertices. Because smoothing never touches the triangle list, this is the one geometry
operation in the gizmo that keeps **every** paint channel verbatim - it saves and restores the eight
texture-displacement masks around `set_mesh()` rather than remapping or dropping them.
**"Ignore outer ring"** (`smooth_skip_border`, on by default) drops the patch's own outermost ring of
vertices from `movable`. That ring's neighbours *outside* the paint never move, so relaxing it drags the
rim of the relief down toward the flat surface and the pattern comes out half-melted where it meets the
edge. Held out, the border keeps the full depth the texture asked for and only the interior relaxes.
Turning it off softens the outer edge deliberately (a blunter version of the per-layer edge-smoothing
falloff). This is the *smoothing* rim, independent of whether that rim is displaced at all
(`displace_border`, step 4 above); both default to keeping the border sharp.
### Blend modes
`TextureBlendMode` {Add, Subtract, Multiply, Divide}, per layer, applied per vertex against the
total accumulated by the layers **below** it (lower slots). The quantity blended is a signed
displacement in **mm**, not a pixel value.
Add/Subtract are self-explanatory. Multiply/Divide are *scaling* operations and so need a unit
convention: they treat the layer's own value as a **factor relative to 1 mm**. That makes `depth_mm`
a gain, and - the property that makes a Multiply layer usable as a mask - a layer with depth 1 mm
sampling a white (1.0) texel multiplies by exactly 1, i.e. leaves the layers below unchanged.
Divide floors its divisor's magnitude at 0.05: a black texel samples to *exactly* zero, so the divisor
really does hit zero in ordinary use, and an unbounded `1/0` would fling vertices thousands of mm away
and poison the mesh's bounding box (and every plate/print-volume check downstream). The floor doubles as
a cap on how far Divide can amplify the relief beneath it: at most 20x.
The **lowest painted layer ignores its blend mode**: it has nothing beneath it, and Multiply/Divide
against an implicit zero base would annihilate (or blow up) it. Enforced in
`build_texture_displacement()` (the first layer to reach a given vertex always folds in additively) and
surfaced in the UI, which labels that layer "Base layer" instead of offering a control that does nothing.
### Projection methods
Five choices per layer (`TextureProjectionMethod`), all funneling through `apply_uv_transform()`
(scale by `1/tiling_scale`, rotate by `rotation_deg`, add `offset`). They are dispatched by
`sample_layer_height()`, which returns a **height**, not a UV - because Triplanar takes three
texture samples per vertex and so has no single UV that represents it.
- **Triplanar** (default) - samples the texture on all three world planes (`(y,z)`, `(x,z)`, `(x,y)`)
and blends the three by the vertex's own normal raised to `TRIPLANAR_BLEND_SHARPNESS` (4). Hard-picking
the single axis most aligned with the normal instead is discontinuous wherever that dominant axis
flips: on a +X face the planar coordinate is `(y, z)`, on a -Y face it is `(x, z)`, so at the shared
edge `u` jumps. A weighted blend is continuous across the transition by construction, since the weight
of the axis being left behind falls smoothly to zero. This removes the hard *seam*; some cross-fade
blurring in the band right at a 90° edge is inherent to triplanar mapping. A genuinely seam-free wrap
around a box needs a real unwrap - that is what the LSCM mode is for.
- **Cylindrical** - wraps around an axis through the patch centroid, axis auto-picked as the world
axis *least* aligned with the average normal (perpendicular to the outward radial normal, as a
cylinder's own axis would be). `u = angle * local_radius` (arc length in mm), `v = distance along
axis`. An approximation, not an exact fit for arbitrary geometry, and the axis/centre are not
user-overridable.
- **Spherical** - longitude/latitude around the centroid, scaled by local radius. Same caveat.
- **LSCM** - real UV unwrap via `MeshBoolean::cgal::parameterize_lscm()` (CGAL's
`Surface_mesh_parameterization` package, LSCM algorithm). Computed **once per patch** (not
per-vertex like the others - it's a single global least-squares solve), then each vertex looks up
its precomputed UV. Requires the patch to be a single topological disk (one connected component,
one boundary loop) - `compute_lscm_uvs()` returns empty and the layer falls back to Triplanar if not
(e.g. multiple disconnected painted islands, or a fully closed patch). CGAL's parameterizer needs a
mesh with no isolated/unreferenced vertices, but `get_facets_strict()` returns the *whole* mesh's
vertex array - so `compact_patch_with_map()` builds a clean sub-mesh plus an index map back to the
original vertex numbering, purely local to this file.
- **ViewProjected** ("From view") - a flat projection along a fixed direction captured from the 3D
camera, like a slide projector. `capture_view_projection()` takes the camera's right/up axes,
transforms them into the volume's *local* frame (so the projection rides along if the part is later
moved), and stores them as `TextureDisplacementLayer::view_project_right/up` (unit vectors, so the
projected coordinate stays in mm and `tiling_scale` keeps meaning mm). `sample_layer_height()`
projects `Vec2f(dot(pos, right), dot(pos, up))`. Single-valued per point, so - like LSCM but unlike
blended Triplanar - the fast preview and UV-check overlay precompute it per vertex
(`compute_layer_vertex_uvs()`) and drive the shader's `use_vertex_uv` path. Faces angled away from
the projector smear; that is inherent to view projection.
Two companions to this mode:
- **Projection frame overlay** (`TextureProjectorFrame`, see below) - a semi-transparent window
dragged over the 3D view whose border becomes the projection's edge. Applying it stores an exact
**projective** map in `view_project_matrix`, which supersedes the affine `right`/`up` axes above
for that layer (`view_project_projective`).
- **"Project only on visible"** (`select_visible_faces()`) - repaints the layer with exactly the
facets the camera can see, so the projected area matches the viewpoint the projector was captured
from. Two tests: a facing test (normal vs. view direction, per triangle - under perspective the
view direction varies across the model, so it is taken from the eye to each centroid), then
`MeshRaycaster::get_unobscured_idxs()` on the survivors to drop facets hidden behind other
geometry, so a concave part's far inner wall is correctly excluded. One ray query per front-facing
facet, hence click-driven (on the checkbox and on each "Capture current view"), never per frame.
It **replaces** the layer's paint rather than adding to it - "project onto what I can see" would
otherwise accumulate every angle the user had ever looked from.
### Manual seams and island cutting
`TextureDisplacementLayer::lscm_seam_edges` - undirected mesh-vertex-index edge pairs the unwrap is
forced to cut along, on top of the dihedral-angle seams. `segment_into_charts()` takes a set of these
(translated from mesh -> compacted-patch numbering inside `compute_patch_unwrap()`) and refuses to
union two triangles across a marked edge whatever their angle. Both the unwrap cache key and the
gizmo's `UVEditorState` include the seam list, so marking a seam (which leaves the paint mask
untouched) still forces a re-solve. Like the paint masks, seams are mesh-index-space and so dropped on
any topology change.
Two ways to write to it:
- **Mark seam (manual)** - a "Mark seams" click mode (`m_seam_edit_mode`) that suppresses painting. A
click raycasts the volume (`m_c->raycaster()->raycasters()[idx]->unproject_on_mesh()`, `idx` = the
volume's slot among model-part volumes), finds the facet's edge nearest the hit point, and toggles it.
Marked edges render as a red overlay (`render_seam_overlay()`), pulled toward the camera so they read
on top. This is the Blender mark-seam workflow.
- **Cut island (auto)** - `cut_island()` takes the selected chart's triangles (back-mapped from the
unwrap via `source_vertex`), finds their 3D bounding box, and marks every edge that straddles the
mid-plane perpendicular to the longest axis. The re-unwrap then splits the chart across its narrow
waist. Exposed as the UV pane's **Cut** button.
### UV-check overlays (checker / distortion)
`resources/shaders/{110,140}/texture_displacement_uvcheck.{vs,fs}`, one shader with a `mode` uniform,
drawn over the painted patch (`rebuild_uvcheck_mesh()`/`render_uvcheck_mesh()`, P3N3T2: `normal.x` =
distortion, `tex_coord` = uv), pulled forward with a polygon offset. **Checker** samples a procedural
checkerboard at the layer's uv (per-vertex for LSCM/ViewProjected, in-shader triplanar otherwise) -
squares that stay square mean low distortion. **Distortion** colours each triangle blue->green->red by
`log2(uv_area / surface_area)` centred on the patch's *median* stretch (so a globally-scaled unwrap
reads as uniformly ideal and only relative stretch shows), averaged to vertices. A separate **Show mesh
wireframe** toggle draws the whole volume's triangle edges, rebuilt only when the vertex count changes
(not per stroke).
### Tiling
`DecodedHeightTexture::sample(uv, tile_enabled, tile_method)`. Two tile methods when enabled
(Repeat, MirroredRepeat). **When `tile_enabled` is false, sampling outside `[0,1)` returns `0`
directly** rather than clamping the *coordinate* into range, which would smear the border row/column of
pixels outward to infinity in every direction (streaky lines radiating out from the painted patch).
### Subdivision - two modes
**Uniform (`subdivide_mesh_uniform()`)** - whole-mesh, 1-to-4 split. Recursive edge-midpoint split with
a shared per-pass midpoint cache (keyed by sorted vertex-index pair) so triangles sharing an edge get
the *same* new vertex - capped at `max_iterations` (default 6). Whole-mesh so it never leaves a
T-junction, at the cost of densifying everywhere. Wired as a "Subdivide steps" slider (**0-5**, 0 =
no subdivision), Apply snaps back to 0. Drops texture-displacement paint (no remap) via the standard
`save_painting()`/`set_mesh()`/`restore_painting()` dance; the other four channels are remapped.
**Adaptive (`subdivide_mesh_adaptive()`)** - refine **only the painted area**, by **Rivara longest-edge
bisection**, which is *conformal by construction*. Only **terminal** edges are ever bisected - an edge
that is the longest edge of *every* triangle sharing it - which splits both those triangles along one
shared midpoint at once, so a hanging node is never created. The edge to split for a triangle that wants
refining is found by **longest-edge propagation (LEPP)**: walk to the longest edge of ever-longer-edged
neighbours until a terminal one is reached, and bisect that. Edge length strictly increases along the
path (ties broken by mesh-vertex key, which both sides of an edge compute identically), so the walk
cannot cycle, and Rivara's result is that repeating it refines the original triangle in a bounded number
of bisections. The transition triangles it pulls in just outside the painted patch are the graded band
that makes the size change conformal.
The win: a small decal on a big model no longer quadruples the *whole* model's triangle count.
**Run to completion, worst-first, against a triangle budget.** The refinement loop is not a fixed number
of sweeps: it holds every triangle that is over its criteria in a max-heap keyed by *how many times over*
it is, pops the worst, walks its LEPP, bisects, and re-scores. Edge adjacency (`nb[e]`, the triangle
across each edge) is built **once** and maintained incrementally through each bisection, so the cost
scales with the refined region rather than with the whole model. `max_triangles` is the only bound;
stopping on it leaves a perfectly valid, still-conformal mesh that spent its budget on the largest
errors. A fixed sweep count instead spends itself grading the *coarse surroundings* - whose edges are
the longest, so they win every terminal-edge contest - and never reaches the painted patch.
**It carries the paint forward**, which is what makes it usable (uniform subdivide drops paint). Because
the refinement is *driven by* the paint, the remap is trivial: `subdivide_mesh_adaptive()` fills an
`out_source[new_tri] = input_tri` map (children inherit their parent), and the gizmo rebuilds each
layer's mask on the new mesh - a new triangle is painted iff its source was fully painted in that
layer. `collect_paint_region()` derives both:
- the union refine-region: **exactly** the original triangles the brush touched, read straight off
`TriangleSplittingData::triangles_to_split` (`serialize()` records an entry per original triangle that
is either split - i.e. partially painted, the patch boundary - or carries a non-default state). No
dilation: marking every triangle that shares a *vertex* with the patch drags in a whole fan of huge
unpainted neighbours and refines *those* down to the resolution floor, since the height field the
detail test samples is not restricted to the painted area. The conformal closure already grades the
size change outward on its own.
- the per-layer fully-painted-triangle sets (a `get_facets_strict(ENFORCER)` sub-triangle with all three
*original* vertex indices == a whole, fully-painted original triangle; a partial stroke's sub-triangles
always carry a split vertex).
The other four channels ride the normal `restore_painting()` remap.
Both modes share the gizmo's Preview/Apply/Done flow; the **"Only painted area (adaptive)"** checkbox
picks the mode, and the adaptive preview follows the paint live (`rebuild_preview()` refreshes the
wireframe while the subdivide preview is open in adaptive mode). The panel shows the previewed triangle
count.
**Feature-adaptive (follow texture detail).** A sub-mode of adaptive (the **"Follow texture detail"**
checkbox) that puts triangles where the *displaced surface actually bends*, not evenly. A flat region or
a linear **ramp** needs no extra vertices (linear interpolation is exact for a ramp); what needs them is
**curvature** - the *second* derivative, not the gradient. So the extra predicate is a **chord-error**
test: sample the combined displacement at the triangle's three edge midpoints *and its centroid*
(sampling the interior is what catches a hill sitting inside a triangle, the blind spot of an edge-only
test) and take the largest departure from the flat triangle's barycentric interpolation. Refine while
that exceeds `chord_tolerance_mm` ("Detail (mm)"). Zero chord error on a ramp => untouched; high on a
hill/ridge/noise => refined until captured. Same conformal machinery, so still crack-free. The
per-triangle error is cached and recomputed only for the children of a split.
Four knobs bracket it, and all four matter:
- **"Max edge (mm)"** (`target_edge_length_mm`) is a **baseline that applies in feature mode too**.
Without it the chord test aliases: a big triangle over a fine pattern can sample four points that all
land at similar heights, report no error, and stall before refinement ever starts. The baseline
guarantees a sampling density fine enough for the curvature test to see the texture at all.
- **"Detail (mm)"** is the chord tolerance above.
- **"Min edge (mm)"** is a hard floor under both, and is what guarantees termination across a sharp
texture *step*, where the error never falls however fine the mesh gets.
- **"Added triangles (k)"** is the budget, passed as `max_triangles` (the model's own triangle count plus
the slider, so the control still means something on an already-dense model).
The height field is `make_combined_displacement_sampler()` - it mirrors `build_texture_displacement()`'s
per-layer setup (decode, patch centroid, cylinder axis, blend order, "lowest layer folds additively")
but evaluated per point. Two deliberate simplifications, both erring toward *more* detail (safe -
over-refinement is never a crack): every sampleable layer is sampled at every point (no per-point paint
test), and edge-smoothing falloff is ignored. The first is *why* the refine region must not be dilated -
outside the paint the sampler still reports full relief. **LSCM layers are skipped** (no per-point UV); a
purely LSCM stack yields a null sampler and the code falls back to the length baseline alone. Per-vertex
heights are sampled lazily, so a small patch on a huge model never pays for the rest of it.
### Fast preview (GPU-only, no CPU meshing)
`resources/shaders/{110,140}/texture_displacement_shaded.{vs,fs}`, registered as
`"texture_displacement_shaded"`. Shades the *displaced* surface without moving geometry - active-layer
only, selected from the View row, and the default when the gizmo opens (`m_use_shaded_preview = true`).
Vertex format is `GLModel::Geometry::EVertexLayout::P3N3T2`: `normal.x` carries the per-vertex paint
weight (0/1), `normal.y` flags the UV island currently being dragged, and `tex_coord` carries a
precomputed texture UV, so it can use `GLModel` normally instead of a hand-rolled VBO/VAO manager.
The mesh is **flat** (vertices not shared between triangles): every corner of a painted triangle gets
weight 1, every corner of an unpainted one weight 0. A coarse mesh needs that - one painted face of a raw
cube has no strictly-interior vertex, so per-vertex weighting would either bleed onto the neighbours or
vanish outright. Duplicating vertices costs no shading quality here because the shader takes its surface
normal from screen-space derivatives of position, not from a per-vertex normal.
**Both preview meshes work in the patch's vertex space, not the mesh's.** Those agree only until a
*brush* stroke splits a triangle: `get_facets_strict()` then appends the split vertices, so the patch
array is longer. `rebuild_shaded_preview_mesh()` and `rebuild_uvcheck_mesh()` therefore index
`patch.vertices` throughout. The weight buffer is rebuilt at the same cadence as the true-displacement
preview (stroke-end/slider-release) but from the **live** `TriangleSelector` state, not the flushed model
facets, so it does not lag by a full model round-trip.
The perturbed normal is the analytic one for a height field `H = +/-depth_mm - h(uv)` displaced along
`N` over any orthonormal surface tangent pair `T`/`B`:
N' = normalize(N - (dH/da)-T - (dH/db)-B), a = dot(p,T), b = dot(p,B)
The two slopes have to be genuine **mm-per-mm** derivatives for the preview's apparent depth to match
the bake's.
**Two projection paths (`use_vertex_uv` uniform):**
- **Triplanar (`use_vertex_uv = 0`)** - `uv` and the `T`/`B` axes are both derived in-shader from
the dominant normal component, mirroring `project_planar()`/`apply_uv_transform()`, and the slope is
formed analytically. `T`/`B` are the projection's axis-aligned pair, exact only when the face is
axis-aligned; the shader drops the along-normal component to keep the gradient in the surface. Here
one `uv` unit is exactly `tiling_scale` mm, so the `1/tiling_scale` gradient factor is right.
- **Precomputed UV (`use_vertex_uv = 1`, used for LSCM and ViewProjected)** - `uv` comes per-vertex from
the CPU (`compute_layer_vertex_uvs()`, so island placement + tiling/rotation/offset are already folded
in), and the perturbed normal is built with **Mikkelsen's method** ("Bump Mapping Unparametrized
Surfaces on the GPU"): the surface gradient taken directly from the screen-space derivatives of the
*sampled height* and position. **This makes no uv->mm scale assumption**, which is essential, because an
LSCM map is **conformal, not isometric**: it is globally area-scaled but the *local* mm-per-uv varies
across the chart, so a single global `1/tiling_scale` factor gets the apparent depth wrong. `dFdx(h)`
captures the true on-screen rate of change however the chart is stretched. This path is also what makes
the fast preview follow the UV editor: move an island and its uv - hence its shading - moves with it
(the mesh rebuilds on drag-end, `on_island_edited(finished)` -> `rebuild_preview()` ->
`rebuild_shaded_preview_mesh()`). The branch is uniform and the paint weight gates by multiply, so the
texture derivatives stay well defined. A triangle straddling a seam has a discontinuous uv -> the
`det~0` guard skips it (a localised preview-only artifact, never in the bake).
**Parallax (triplanar path).** Perturbing the shading normal alone welds the pattern to the base surface:
it does not slide as the camera orbits, and does not get deeper as `depth_mm` grows. The triplanar path
therefore shades at the point the *displaced* surface would show at this pixel, found by **ray marching**
(parallax occlusion mapping). A point at ray parameter `s`, i.e. `P + V-s` (`P` the base point, `V` the
unit direction to the eye), sits at height `s-dot(V,n)` above the undisplaced surface. The displaced
surface lives in a shell between the extreme values of `amp-(h - midlevel)` - taken from both ends of
`h in [0,1]`, so it holds for an inverted layer and a raised midlevel too, where the surface sits *below*
the undisplaced one. The march starts at the top of that shell, where the ray is outside the surface by
construction, and steps inward until the ray height drops below the sampled height. That crossing *is*
the visible point.
Solving `Q = P + V-(H(Q)/dot(V,n))` by fixed-point iteration instead is geometrically exact but the
divisor goes to zero edge-on; the sample then lands a large fraction of a tile away and the iteration
oscillates, which reads as a second, flat copy of the pattern ghosted over the real one. Clamping the
step to one tile does not help - a tile-sized shift lands on the neighbouring tile, the same pattern
again. Offset limiting (stepping along the tangential part of `V`) is stable but understates parallax
enough that the relief still flattens as soon as the camera tilts. Marching has neither problem.
The hit is interpolated between the last two samples, which keeps `PARALLAX_STEPS` (24) affordable, and
the whole march is skipped when sweeping the shell would move the sample point less than half a texel -
the head-on case, so the common view pays almost nothing. The 140 variant samples with
`textureLod(..., 0.0)` inside the loop, since implicit derivatives are undefined in non-uniform control
flow. Two uniforms exist for this: `midlevel` (parallax needs the real height, not just its derivative)
and `eye_model_pos` (the camera in the volume's local frame).
Parallax cannot change the model's silhouette or cast shadows; the View row's Normal mode is one click
away for that. The LSCM path stays plain Mikkelsen normal perturbation - it has no closed-form uv, so there is no cheap
way to re-project a marched position. One further approximation: the GPU sampler's wrap mode stands in
for `tile_enabled`/`tile_method`, so with tiling *off* the GPU repeats where the CPU returns 0 outside
`[0,1)`.
### On-canvas "Adjust Texture" gizmo
A per-active-layer toggle ("Adjust placement") that disables painting and shows a flat pan panel (free
2D drag on both axes) plus two arrows along the patch's own U/V axes (constrained single-axis drag).
Anchored to the painted patch's centroid/average-normal (`compute_layer_paint_anchor()`). Hit-testing is
screen-space distance/point-to-segment, not real 3D ray intersection against the handle geometry - simple
and good enough at this handle size.
### Projection frame overlay (ViewProjected)
`src/slic3r/GUI/TextureProjectorFrame.hpp/.cpp` - a semi-transparent, resizable `wxFrame` the user
drags **over the 3D view**, like a slide projector's gate. Whatever the model shows through it is what
the texture is projected onto, and the window's border becomes the hard edge of the displacement.
Press **Apply projection frame** and the gizmo reads the window's rectangle and commits it.
The window is deliberately **dumb**: it owns no placement state and reports nothing continuously. Its
position and size *are* the placement, read on demand at Apply - which is also when the expensive
visible-facet raycast runs. So dragging it is free and nothing recomputes until asked.
Plain 2D (`wxPaintDC`), not a `wxGLCanvas`: a second GL canvas would have to share the app's one real
`wxGLContext`. It only ever draws a bitmap and a border.
**The projective mapping (`apply_projection_frame()`)**. The frame defines a **screen-space** rectangle,
but the bake samples from a **local-space** position, so the two have to be reconciled.
`view_project_right/up` can only express an *affine* projection - exact under an orthographic camera, but
wrong under perspective, where the near end of a part projects larger than the far end and no pair of
axes reproduces that. So the layer instead stores a full projective map (`view_project_matrix`, row-major
3x4, `uv = (row0-p/row2-p, row1-p/row2-p)`), built like this:
- `K = projection - view - (instance - volume)`, i.e. local -> clip, the same product the renderer uses.
Note `Camera::get_projection_matrix()` is typed `Transform3d` (nominally affine) but its perspective
form explicitly writes a `(0, 0, -1, 0)` bottom row into the underlying 4x4, so `clip.w = -z_eye` is
genuinely carried. The build therefore multiplies **`.matrix()` products** (plain `Matrix4d`), never
`Transform3d` products, which would not compose that row correctly.
- Window coordinates follow `igl::project`'s convention (as `CameraUtils::project` does), with y
measured downward. Writing `uv = (win - rect_origin) / rect_size` makes u and v affine in
`ndc = clip.xyz / clip.w`; multiplying through by `clip.w` leaves a plain linear combination of `K`'s
rows, which is exactly the 3x4 matrix - the perspective divide survives intact.
- `w > 0` is checked rather than divided blindly. A point behind the projector has `w < 0` and divides
to a plausible-looking but **mirrored** uv - the classic way a projected decal reappears on the back
of a model. `project_uv_projective()` returns false there and the caller treats it as no height.
The map already includes placement, so `apply_uv_transform()` is **not** applied on top of it - the
window's own position and size are the placement, and the tiling/rotation/offset sliders would shove
the result off the frame the user just aligned. A "Clear" button drops back to the affine path where
those controls mean something again.
Apply also sets `tile_enabled = false`, so `DecodedHeightTexture::sample()` returns 0 outside `[0,1)`
and the border is a hard edge rather than the first seam of an endless repeat, and repaints the layer
via `select_visible_faces(&matrix)` - the frame's uv square clips the selection, which both matches the
paint to the border and keeps the ray queries proportional to the framed area instead of the model.
Owned by the gizmo and **destroyed** (not just hidden) in `on_shutdown()`. Closing it only hides it, so
reopening keeps it where it was left.
### UV Editor pane
`UVEditorCanvas` (`src/slic3r/GUI/UVEditorCanvas.hpp/.cpp`) - a standalone `wxGLCanvas` rendering the
flattened LSCM islands (per-island wireframe + outline + fill) over the height texture (background
quad tiled across the whole unwrap), with mouse pan/zoom. It is wrapped in a **`UVEditorPanel`**
(same file) that adds a button row (Frame / Snap / Avg scale / Cut / Join / Unjoin) and a status line
along the bottom naming the current gesture and the shortcuts in play. The *panel* is what is
registered as a `wxAuiPaneInfo` pane on `Plater`'s `m_aui_mgr`; `Plater::show_uv_editor(bool)`
shows/hides it (deferred via `CallAfter`, since the gizmo calls it mid-3D-frame), and
`get_uv_editor_canvas()` returns the inner canvas the gizmo talks to.
Deliberately **shares the app's one real `wxGLContext`** (`wxGetApp().init_glcontext(*this)`, the
same call `View3D`/`Preview`/`AssembleView` make) rather than creating an independent context like
`SkipPartCanvas` does elsewhere in this codebase - this is what lets it reuse the already-registered
`"flat"`/`"flat_texture"` shaders and `GLModel` as-is, instead of needing its own shader
compilation/VBO management.
**Geometry is uploaded once, in the unwrap's own (raw, mm) coordinates**, one `GLModel` set per island;
each island is then drawn through its own 2x3 affine (`island_transform_matrix()` composed with the
layer's tiling/rotation/offset) passed as the `flat` shader's `view_model_matrix`. A drag updates one
matrix per island and touches no vertex buffer - `on_island_edited(!finished)` calls only
`set_island_transforms()`, and the full `set_islands()` rebuild happens solely when the unwrap itself
changes (`unwrap_changed` in `update_uv_editor()`).
**Gestures** (canvas-owned, reported to the gizmo as incremental deltas via `IslandEditFn`): left-drag
= move, right-drag or **R** = rotate (hold **Shift** to snap to 15° steps - quantised on the
*cumulative* rotation, not each delta, so it doesn't judder, and accumulated incrementally so it
survives crossing +/-180°), **S** = scale (R/S modal, click/Enter to confirm, Esc to cancel), wheel =
zoom about the cursor, middle-drag = pan, **Home**/**F** = frame all. Scale writes
`TextureIsland::scale`; "Avg scale" (`average_island_scales()`) sets every island to the mean, so
one island scaled by hand can be matched back to its neighbours' texel density. **Snap** (canvas-owned
`m_snap_enabled`, toggled from the toolbar) sticks a dragged island's nearest boundary vertex onto a
neighbouring island's at drag-*end* only - a magnet that re-applies mid-drag is very hard to pull out
of. Toolbar commands the canvas can't service itself (Avg scale, Cut, Join, Unjoin) are forwarded to the
gizmo via `CommandFn`; view-only ones (Frame, Snap) it handles directly.
## File map
**libslic3r (core, no GUI dependency):**
- `src/libslic3r/TextureDisplacement.hpp/.cpp` - data model, bake algorithm, projection methods,
tiling, subdivision (uniform + adaptive longest-edge bisection), post-process smoothing
(`smooth_mesh_vertices()`), and `TextureDisplacementOptions` (the whole-stack settings). See doc
comments throughout, they're kept accurate and up to date.
- `src/libslic3r/MeshBoolean.hpp/.cpp` - `parameterize_lscm()` and `remesh_isotropic()` in the `cgal`
sub-namespace, reusing the existing `CGALMesh`/`_EpicMesh`/conversion-helper infrastructure already
there for mesh boolean ops. CGAL includes: `Polygon_mesh_processing/border.h`,
`Polygon_mesh_processing/connected_components.h`, `Surface_mesh_parameterization/{Error_code,
LSCM_parameterizer_3, parameterize}.h`. No new dependency - CGAL 5.6.3 is already vendored and the
`Surface_mesh_parameterization` package headers were already present.
- `src/libslic3r/Model.hpp/.cpp` - the 8 named `FacetsAnnotation` fields + accessor,
`texture_displacement_layers`, `texture_displacement_options`, and all the mirrored touch points
(see Data model above).
**GUI:**
- `src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp/.cpp` - the gizmo and its whole panel.
- `src/slic3r/GUI/TextureLibrary.hpp/.cpp` - scans the shipped + user texture folders, imports an
arbitrary image into the user folder (converting it to the 8-bit grayscale PNG libslic3r decodes),
and loads a library file's bytes for a layer. The image->grayscale-PNG conversion lives here, on the
GUI side, because libslic3r has no image toolkit; both the import path and the "pick a shipped
texture" path go through the same one function.
- `resources/textures/displacement/*.png` - the 10 shipped height maps (Bricks, Grid, Hexagons,
Knurl, Noise, Quilt, Studs, Waves, Weave, Wood Grain). All 512x512 8-bit grayscale and **seamless**
(each is periodic over the full image in both axes, so tiling shows no seam). Generated
procedurally; the whole `resources/` tree is installed recursively by CMake, so a new folder under
it ships with no build-system change.
- `src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.hpp/.cpp` - background bake commit.
- `src/slic3r/GUI/Jobs/TextureDisplacementPreviewJob.hpp/.cpp` - background preview compute
(mirrors the bake job's shape but commits nothing to the Model).
- `src/slic3r/GUI/TextureProjectorFrame.hpp/.cpp` - the semi-transparent projection-frame overlay for
ViewProjected layers (plain 2D `wxPaintDC`, no GL context - see its section above).
- `src/slic3r/GUI/UVEditorCanvas.hpp/.cpp` - the 2D UV unwrap viewer widget.
- `src/slic3r/GUI/Plater.hpp/.cpp` - `uv_editor_canvas` member, AUI pane registration,
`get_uv_editor_canvas()`/`show_uv_editor()`.
- `src/slic3r/GUI/GLShadersManager.cpp` - registers `"texture_displacement_shaded"`.
- `resources/shaders/{110,140}/texture_displacement_shaded.{vs,fs}` - the fast-preview shader.
- `src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp` - `PainterGizmoType::TEXTURE_DISPLACEMENT`.
- `src/slic3r/GUI/Gizmos/GLGizmosManager.hpp/.cpp` - `EType::TextureDisplacement` registration.
## Tests
`tests/libslic3r/test_texture_displacement.cpp`. Covers `decode_height_texture` round-trip, empty-layer
no-op, full-cube uniform displacement, a second layer over the same area contributing, all four blend
modes (table-driven), the lowest layer ignoring its blend mode, border displace/pin, post-process
smoothing and its mask guarantees, and adaptive subdivision: conformality (`every_edge_used_twice` on a
partially-refined cube - an exact crack detector for a closed mesh), the target edge length actually
being reached, the triangle budget capping the result without opening a crack, curvature-driven
refinement (a Gaussian hill refines at its centre, a linear ramp adds nothing), and the max-edge
baseline.
`BUILD_TESTS` is `OFF` in the checked-in build cache; flip it on to run them:
cmake -S . -B build -DBUILD_TESTS=ON
cmake --build build --config Release --target libslic3r_tests -- -m
./build/tests/libslic3r/Release/libslic3r_tests.exe "[TextureDisplacement]" --order rand
-326
View File
@@ -1,326 +0,0 @@
# Texture Displacement - Feature & Controls Guide
Texture Displacement is a paint-style gizmo that stamps height-map textures onto a model's surface
and turns them into real relief - engraved or embossed detail - either as a live preview or baked
into actual mesh geometry. You paint where the texture applies, stack multiple textures as blended
layers, choose how each is projected onto the surface, and (for the unwrap projection) lay the result
out by hand in a dedicated 2D **UV Editor** pane.
This document describes every feature and control. For the internal architecture and algorithms, see
`TEXTURE_DISPLACEMENT.md`.
---
## Table of contents
1. [Quick start](#quick-start)
2. [Entering the tool](#entering-the-tool)
3. [Selection modes](#selection-modes)
4. [View modes](#view-modes)
5. [Auto update](#auto-update)
6. [Texture layers](#texture-layers)
7. [Per-layer settings](#per-layer-settings)
8. [Projection methods](#projection-methods)
9. [The UV Editor](#the-uv-editor)
10. [Seams](#seams)
11. [Adjust placement (on-model)](#adjust-placement-on-model)
12. [Preparing the mesh: Subdivide & Remesh](#preparing-the-mesh-subdivide--remesh)
13. [Baking & resetting](#baking--resetting)
14. [Controls reference](#controls-reference)
15. [Tips & limitations](#tips--limitations)
---
## Quick start
1. Select an object and open the **Texture displacement** gizmo from the left toolbar.
2. A texture layer is added automatically. Pick a texture from the layer's picker, or import your own.
3. **Paint** the area you want the texture to affect (or press **Select whole model**).
4. The relief appears live on the model. Tune **Depth**, **Tile size**, **Rotation**, etc.
5. If the model is low-poly, use **Subdivide** or **Remesh** so there are enough vertices for detail.
6. Press **Bake** to convert the preview into real geometry, or leave it as a live preview.
> The tool only ever affects the **painted** area. Everything you don't paint keeps its original
> surface, and bake blends the relief seamlessly into it.
---
## Entering the tool
The gizmo lives on the left gizmo toolbar (icon: `toolbar_texture_displacement.svg`). Its settings
panel opens beside the toolbar. You can **Dock panel / Undock panel** (top of the panel) to pin it or
float it freely over the 3D view, and **Close** at the bottom exits the gizmo.
When you first open the tool on a never-textured object it starts with **one texture layer already
added**, so you can paint straight away.
---
## Selection modes
Choose *how* you paint. All three write into the **active layer's** mask.
| Mode | What it does |
|------|--------------|
| **Brush** | Free-hand painting with a round brush. Shows a **Brush size** slider and a **Circle / Sphere** choice (circle = surface disc, sphere = 3D ball that also paints around curves). |
| **Face** | Click a single triangle to paint it. |
| **Connected area** | Click to flood-fill a region; the **Angle threshold** slider limits how far the fill spreads across changes in surface angle. |
- **Select whole model** - marks the entire model as painted for the active layer, instead of
brushing it by hand.
---
## View modes
A row of icon buttons labelled **View** controls how the painted area is shown. The first four are a
radio group; **Wireframe** is an independent toggle. Hover any icon for its tooltip.
| View | Meaning |
|------|---------|
| **Normal** | The true displaced geometry - exactly what **Bake** produces. Rebuilt in the background. |
| **Fast** | A GPU shaded approximation of the *active layer only*. No real geometry movement - quick to update, not exact. Best while tuning or dragging islands. |
| **Checker** | A test grid painted over the unwrap so you can see stretching (squares stay square where the map isn't distorted). |
| **Distortion** | A blue->green->red heatmap of how much each area is compressed or stretched in UV space. Needs the **Unwrap (LSCM)** projection. |
| **Wireframe** | Overlays the mesh edges (white). Independent of the view above; in **Normal** view it sits on the displaced surface. |
---
## Auto update
**Auto update** (on by default) rebuilds the true displaced geometry as soon as *anything* changes -
painting, swapping textures, moving sliders. Turn it off on very heavy models to only rebuild when you
release a slider (painting still updates on stroke end).
---
## Texture layers
You can stack up to **8** texture layers. Each has its own independent paint mask, its own texture,
and its own parameters, and they combine in slot order like layers in an image editor.
- **Add a layer** - the **+ icon** to the right of the *Texture layers* heading (reuses the tool icon
for now).
- **Remove** - the button on each layer's header row.
- **Active layer** - click a layer's header (or anywhere in its block) to make it active. The active
layer is the one you paint into and the one whose block is tinted. Only one layer is active at a time.
- **Erase all** - clears the active layer's paint.
Each layer shows a texture **picker** (large preview + name). Open it to choose from the shipped
library or import your own image (any png/jpg/bmp; it's converted to an 8-bit grayscale height map and
copied into your user texture folder so app updates can't overwrite it).
---
## Per-layer settings
| Control | Range / options | What it does |
|---------|-----------------|--------------|
| **Depth (mm)** | 0.01-10 (log) | Maximum displacement along the surface normal. |
| **Tile size (mm)** | 0.2-200 (log) | Physical size of one texture tile on the surface. |
| **Rotation** | 0-360° | Rotates the texture on the surface. |
| **Midlevel** | 0-10 | The grey level that means "don't move". At 0 the texture only pushes outward; raise it and darker texels cut *inward* (one map both embosses and engraves). 0.5 makes mid-grey neutral. |
| **Smoothing** | 0-1 | Blurs the height texture before it displaces - rounds hard edges and removes speckle without needing a softer source image. |
| **Edge smoothing** | checkbox + **Edge amount** 0-1 | Fades the relief to flat toward the *edge of the painted area*, so it blends into the surrounding surface. A small amount softens only a thin band at the very edge; the maximum flattens the whole painted face. |
| **Invert** | checkbox | Flips the height map (peaks become valleys). |
| **Blend** | Add / Subtract / Multiply / Divide | How this layer combines with the layers **below** it where they overlap. Add/Subtract pile relief on or carve it away; Multiply/Divide scale the relief underneath (a mask). The lowest painted layer is the **Base** and always behaves additively. |
| **Tile** | checkbox + **Repeat / Mirrored repeat** | When off, the texture is placed once (a decal) instead of repeating. Mirrored repeat flips every other tile to hide seams. |
| **Projection** | see below | How the texture is mapped onto the painted surface. |
> **Midlevel warning:** cutting inward can fold the surface through itself in sharp concave corners or
> thin walls. Keep Depth small relative to the feature you're cutting into; the panel warns when a deep
> inward setting is risky.
---
## Projection methods
How the 2D texture is wrapped onto the 3D painted area.
| Method | Best for | Notes |
|--------|----------|-------|
| **Triplanar (blended)** | Patches wrapping around edges | Projects from all three axes at once and blends, so there's no seam across a sharp edge. |
| **Cylindrical** | Round, tube-like selections | Wraps the texture around the patch's own centre/axis. |
| **Spherical** | Ball-like selections | Longitude/latitude wrap around the patch centre. |
| **Unwrap (LSCM)** | Flat, controlled layout | A real conformal unwrap. Cuts the area into pieces at sharp edges (see **Seam angle**), flattens each, and lets you lay them out by hand in the **UV Editor**. Unlocks Checker/Distortion, seams, and island editing. |
| **From view** | Decals / slide-projector look | Projects straight onto the surface from the current camera direction. Use **Capture current view** to re-lay it from wherever you're looking. |
### LSCM-only controls
These appear when a layer uses **Unwrap (LSCM)**:
- **Seam angle** (5-90°) - edges sharper than this are cut so each piece lies flat. Lower cuts more
(less stretching, more seams); raise to keep more in one piece. A box's 90° corners are cut by
default. *Ignored once you've marked any seam by hand* (your seams then define the pieces).
- **Connect islands** (on by default) - lays the unwrap out as a **connected net**: pieces that share
an edge are unfolded next to each other (a cube becomes a joined net instead of six loose squares).
They stay separate islands, so you can still move any of them by hand. Turn off for the classic
packed-grid layout.
- **Open UV editor** - shows the flattened unwrap in a side pane (see below). Opens *only* when you
turn this on - it never pops up on its own.
- **Mark seams** / **Path** / **Clear seams** - see [Seams](#seams).
- An **Unwrap: N islands, F faces, V verts** read-out tells you what the unwrap actually produced.
---
## The UV Editor
A dockable 2D pane (enable **Open UV editor** on an LSCM layer) showing the flattened unwrap over the
height texture. Islands are the flattened pieces; you can rearrange them freely - nothing re-packs them
behind your back. Moving an island updates the model **live** (in Fast view it tracks the cursor
smoothly, via a shader uniform - no rebuild until you release).
### Navigation
| Action | Control |
|--------|---------|
| Pan | Middle-drag |
| Zoom | Mouse wheel (zooms about the cursor) |
| Frame everything | **Home** or **F**, or the **Frame** toolbar button |
### Editing an island
| Action | Control |
|--------|---------|
| Select | Left-click an island |
| Move | Left-drag |
| Rotate | Right-drag, or press **R** then move the mouse (click/Enter to confirm, Esc to cancel) |
| Rotate snapped | Hold **Shift** while rotating - snaps to **global** 15° marks (0/15/30...). A protractor dial with tick marks and the current angle is shown. |
| Scale | Press **S** then move the mouse (click/Enter to confirm, Esc to cancel) |
| Undo / Redo | **Ctrl+Z** / **Ctrl+Shift+Z** or **Ctrl+Y** |
The **selected** island gets a bold light-green outline and a brighter wireframe; unselected islands
are a translucent light-green wash. The texture underneath repeats exactly as it will when baked.
A **status line** along the bottom always names the current gesture and the shortcuts in play.
### Toolbar
| Button | Action |
|--------|--------|
| **Frame** | Frame all islands (same as Home). |
| **Snap** | Toggle magnetic snapping - a dragged island sticks its boundary to a neighbour's when they come close. |
| **Avg scale** | Give every island the same texel density (Blender's "Average Islands Scale"). |
| **Cut** | Split the selected island across its long axis (useful for very long islands). |
| **Join** | Unfold the selected island onto its nearest neighbour along their shared edge - keeps both as separate islands with their own borders. |
| **Unjoin** | Send the selected island back to its own packed position. |
> **Checker / Distortion in the UV editor:** selecting those View modes also colours the UV pane - a
> checker background, or a per-island distortion heatmap - so you can judge stretch in 2D as well as
> on the model.
---
## Seams
Seams are edges the unwrap is forced to cut along, on top of whatever the Seam angle cuts - the
Blender "mark seam" workflow. They let you control exactly where the unwrap splits.
Enable **Mark seams** on an active LSCM layer, then:
- **Click an edge** on the model to mark it (it turns **red**); click a red edge again to unmark it.
The edge under the cursor is highlighted **yellow** so you can see what a click will toggle.
- **Path mode** (the **Path** checkbox) - for dense meshes where clicking each edge is tedious: click a
start point, then an end point, and the whole **shortest path** between them is seamed at once. It
chains (each click extends from the last point); the start vertex is shown in **green**.
- **Ctrl+drag** rotates/pans the camera while in seam mode.
- **Clear seams** removes them all.
Once any seam is marked, the automatic Seam-angle cutting is disabled so *your* seams define the
islands - pieces you leave un-seamed merge together.
---
## Adjust placement (on-model)
**Adjust placement** (on an active layer) lets you position the texture by dragging a handle on the
model instead of nudging the Rotation/offset numbers. The handle is a flat panel in the patch's
tangent plane (drag anywhere on it to move freely) plus U/V arrows for single-axis nudges. It's
anchored to the painted patch, so paint something first.
---
## Preparing the mesh: Subdivide & Remesh
Displacement can only move vertices that exist, so a coarse model needs more of them first.
### Subdivide
Splits every triangle into four, **1-5 times** (each step roughly quadruples the triangle count).
- **Subdivide steps** (1-5) - how many times to split.
- **Preview subdivision** - shows the result as a **cyan wireframe** without changing the model.
- **Apply** - commits the subdivision to the geometry.
- **Done** - ends the preview and leaves the model as it is.
### Remesh
Rebuilds the whole model with triangles close to a target edge length - evens out a mesh with wildly
varying triangle sizes (CGAL isotropic remeshing).
- **Target edge (mm)** - desired triangle edge length (seeded to the model's current average).
- **Remesh** - splits the big triangles and merges the small ones to that size.
> Both Subdivide-Apply and Remesh **replace the geometry** and clear any *not-yet-baked* paint on it
> (already-baked relief is kept). If you had the mesh **Wireframe** on before, it stays on afterward.
---
## Baking & resetting
- **Bake** - converts the current preview into real, permanent mesh geometry, restricted to the
painted area. Runs in the background; the button shows *Baking...* while it works.
- **Erase all** - clears the active layer's paint.
Baking is the exact same algorithm as the **Normal** preview, so what you see is what you get.
---
## Controls reference
### Mouse - 3D view (while painting)
| Input | Action |
|-------|--------|
| Left-drag | Paint the active layer |
| Ctrl + drag | Rotate / pan the camera (works in seam mode too) |
| Wheel | Zoom |
### Mouse & keys - UV Editor
| Input | Action |
|-------|--------|
| Left-click | Select island |
| Left-drag | Move island |
| Right-drag | Rotate island |
| **R** / **S** | Modal rotate / scale (mouse drives it, click or Enter confirms, Esc cancels) |
| **Shift** (while rotating) | Snap to global 15° marks |
| Middle-drag | Pan |
| Wheel | Zoom about cursor |
| **Home** / **F** | Frame all islands |
| **Ctrl+Z** / **Ctrl+Shift+Z** / **Ctrl+Y** | Undo / redo |
### Seam mode
| Input | Action |
|-------|--------|
| Click edge | Mark / unmark a seam (yellow = hover, red = marked) |
| Click (Path mode) | Set start, then seam the shortest path to the next click |
| Ctrl + drag | Rotate / pan camera |
---
## Tips & limitations
- **Paint first, then bake.** The preview is free to explore; only Bake changes the real mesh.
- **Not enough detail?** Subdivide or Remesh before painting fine textures.
- **Inward cuts** (high Midlevel + big Depth) can self-intersect on thin walls or sharp concave
corners - keep Depth modest there.
- **Fast vs Normal:** Fast preview only shades the relief and shows only the active layer; use it for quick
tuning and smooth UV dragging, but trust **Normal**/**Bake** for the exact result.
- **Topology changes drop unbaked paint.** Subdivide-Apply, Remesh, and Simplify replace the mesh, and
texture-displacement paint isn't remapped across that change (already-baked relief is unaffected).
- **Island placements** are tied to the current unwrap. Re-painting or changing the Seam angle can
re-segment the charts and renumber them, so a re-unwrap re-lays the connected net and discards
hand placements made before it.
- **Connect islands** is on by default; turn it off (per layer) for the classic packed-grid layout, or
if an unfold looks wrong on an unusual mesh.
+36 -3
View File
@@ -8,9 +8,10 @@ SCRIPT_PATH=$(dirname "$(readlink -f "${0}")")
pushd "${SCRIPT_PATH}" > /dev/null
function usage() {
echo "Usage: ./${SCRIPT_NAME} [-1][-b][-c][-d][-D][-e][-F][-g][-h][-i][-j N][-p][-r][-s][-t][-u][-l][-L]"
echo "Usage: ./${SCRIPT_NAME} [-1][-b][-c][-d][-D][-e][-F][-g][-h][-i][-j N][-J N][-p][-r][-s][-t][-u][-l][-L]"
echo " -1: limit builds to one core (where possible)"
echo " -j N: limit builds to N cores (where possible)"
echo " -J N: build up to N dependencies at a time, each still using -j jobs (default: 1)"
echo " -b: build in Debug mode"
echo " -c: force a clean build"
echo " -C: enable ANSI-colored compile output (GNU/Clang only)"
@@ -36,12 +37,13 @@ function usage() {
}
SLIC3R_PRECOMPILED_HEADERS="ON"
DEPS_PARALLEL=""
unset name
BUILD_DIR=build
BUILD_CONFIG=Release
FORWARDED_ARGS=()
while getopts ":1j:bcCdDeFghiprstulL" opt ; do
while getopts ":1j:J:bcCdDeFghiprstulL" opt ; do
case ${opt} in
1 )
export CMAKE_BUILD_PARALLEL_LEVEL=1
@@ -51,6 +53,10 @@ while getopts ":1j:bcCdDeFghiprstulL" opt ; do
export CMAKE_BUILD_PARALLEL_LEVEL=$OPTARG
FORWARDED_ARGS+=("-j" "$OPTARG")
;;
J )
DEPS_PARALLEL=$OPTARG
FORWARDED_ARGS+=("-J" "$OPTARG")
;;
b )
BUILD_DIR=build-dbg
BUILD_CONFIG=Debug
@@ -135,6 +141,11 @@ if [[ -n "${CLEAN_DOCKER_IMAGE}" ]] && [[ -z "${USE_DOCKER}" ]] ; then
exit 1
fi
if [[ -n "${DEPS_PARALLEL}" ]] && ! [[ "${DEPS_PARALLEL}" =~ ^[1-9][0-9]*$ ]] ; then
echo "Error: -J expects a positive integer."
exit 1
fi
function check_available_memory_and_disk() {
FREE_MEM_GB=$(free --gibi --total | grep 'Mem' | rev | cut --delimiter=" " --fields=1 | rev)
MIN_MEM_GB=10
@@ -537,7 +548,29 @@ if [[ -n "${BUILD_DEPS}" ]] ; then
fi
print_and_run cmake -S deps -B deps/$BUILD_DIR "${CMAKE_C_CXX_COMPILER_CLANG[@]}" "${CMAKE_LLD_LINKER_ARGS[@]}" "${CMAKE_CCACHE_ARGS[@]}" -G Ninja "${COLORED_OUTPUT}" "${BUILD_ARGS[@]}"
print_and_run cmake --build deps/$BUILD_DIR -j1
# The top-level build runs one dependency at a time by default, which keeps the console
# output readable and lets that dependency's own build use all of CMAKE_BUILD_PARALLEL_LEVEL.
# -J raises the top level instead, and -j still applies in full to every dependency, so the
# worst case is -J times -j compile jobs at once. Ninja has no job server to share a pool
# across the nested builds, so that ceiling is not enforced anywhere: pick -J to suit the RAM.
DEPS_JOBS=1
if [[ -n "${DEPS_PARALLEL}" ]] ; then
DEPS_JOBS=${DEPS_PARALLEL}
SAVED_PARALLEL_LEVEL=${CMAKE_BUILD_PARALLEL_LEVEL-}
export CMAKE_BUILD_PARALLEL_LEVEL=${CMAKE_BUILD_PARALLEL_LEVEL:-$(nproc)}
echo "Building up to ${DEPS_JOBS} dependencies at a time, ${CMAKE_BUILD_PARALLEL_LEVEL} jobs each: up to $(( DEPS_JOBS * CMAKE_BUILD_PARALLEL_LEVEL )) compile jobs at once."
fi
print_and_run cmake --build deps/$BUILD_DIR -j"${DEPS_JOBS}"
if [[ -n "${DEPS_PARALLEL}" ]] ; then
# Give the whole -j back to the OrcaSlicer build below.
if [[ -n "${SAVED_PARALLEL_LEVEL}" ]] ; then
export CMAKE_BUILD_PARALLEL_LEVEL=${SAVED_PARALLEL_LEVEL}
else
unset CMAKE_BUILD_PARALLEL_LEVEL
fi
fi
fi
if [[ -n "${BUILD_ORCA}" ]] || [[ -n "${BUILD_TESTS}" ]] ; then
+2 -2
View File
@@ -104,8 +104,8 @@ fi
CMAKE_VERSION=$(cmake --version | head -1 | sed 's/[^0-9]*\([0-9]*\).*/\1/')
if [ "$CMAKE_VERSION" -ge 4 ] 2>/dev/null; then
export CMAKE_POLICY_VERSION_MINIMUM=3.5
export CMAKE_POLICY_COMPAT="-DCMAKE_POLICY_VERSION_MINIMUM=3.5"
export CMAKE_POLICY_VERSION_MINIMUM=3.10
export CMAKE_POLICY_COMPAT="-DCMAKE_POLICY_VERSION_MINIMUM=3.10"
echo "Detected CMake 4.x, adding compatibility flag (env + cmake arg)"
else
export CMAKE_POLICY_COMPAT=""
+1 -5
View File
@@ -282,11 +282,7 @@ if "%install_deps%" == "ON" (
call :note_failed "Visual Studio" !errorlevel!
)
REM CMake 4 dropped pre-3.5 policy support and ships incomplete ASM_ARMASM
REM linker modules, which breaks Boost.Context on ARM64. CI pins the same way.
set "cmake_version_flag="
if /I "%arch%" == "ARM64" set "cmake_version_flag=--version 3.31.8"
call :print_and_run winget install !winget_args! --id=Kitware.CMake !cmake_version_flag!
call :print_and_run winget install !winget_args! --id=Kitware.CMake
call :note_failed CMake !errorlevel!
call :print_and_run winget install !winget_args! --id=StrawberryPerl.StrawberryPerl
call :note_failed Perl !errorlevel!
-359
View File
@@ -1,359 +0,0 @@
# Distributed under the OSI-approved BSD 3-Clause License. See accompanying
# file Copyright.txt or https://cmake.org/licensing for details.
# PrusaSlicer specifics:
# This file is backported from CMake 3.15 distribution to behave uniformly
# across all versions of CMake. It explicitly adds GLEW_STATIC compile
# definition to static targets which is needed to prevent link errors.
#[=======================================================================[.rst:
FindGLEW
--------
Find the OpenGL Extension Wrangler Library (GLEW)
Input Variables
^^^^^^^^^^^^^^^
The following variables may be set to influence this module’s behavior:
``GLEW_USE_STATIC_LIBS``
to find and create :prop_tgt:`IMPORTED` target for static linkage.
``GLEW_VERBOSE``
to output a detailed log of this module.
Imported Targets
^^^^^^^^^^^^^^^^
This module defines the following :ref:`Imported Targets <Imported Targets>`:
``GLEW::glew``
The GLEW shared library.
``GLEW::glew_s``
The GLEW static library, if ``GLEW_USE_STATIC_LIBS`` is set to ``TRUE``.
``GLEW::GLEW``
Duplicates either ``GLEW::glew`` or ``GLEW::glew_s`` based on availability.
Result Variables
^^^^^^^^^^^^^^^^
This module defines the following variables:
``GLEW_INCLUDE_DIRS``
include directories for GLEW
``GLEW_LIBRARIES``
libraries to link against GLEW
``GLEW_SHARED_LIBRARIES``
libraries to link against shared GLEW
``GLEW_STATIC_LIBRARIES``
libraries to link against static GLEW
``GLEW_FOUND``
true if GLEW has been found and can be used
``GLEW_VERSION``
GLEW version
``GLEW_VERSION_MAJOR``
GLEW major version
``GLEW_VERSION_MINOR``
GLEW minor version
``GLEW_VERSION_MICRO``
GLEW micro version
#]=======================================================================]
include(FindPackageHandleStandardArgs)
if(APPLE)
find_package(OpenGL QUIET)
if(OpenGL_FOUND)
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: Found OpenGL Framework.")
message(STATUS "FindGLEW: OPENGL_LIBRARIES: ${OPENGL_LIBRARIES}")
endif()
else()
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: could not find GLEW library.")
endif()
return()
endif()
endif()
function(__glew_set_find_library_suffix shared_or_static)
if((UNIX AND NOT APPLE) AND "${shared_or_static}" MATCHES "SHARED")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".so")
elseif((UNIX AND NOT APPLE) AND "${shared_or_static}" MATCHES "STATIC")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")
elseif(APPLE AND "${shared_or_static}" MATCHES "SHARED")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".dylib;.so")
elseif(APPLE AND "${shared_or_static}" MATCHES "STATIC")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".a")
elseif(WIN32 AND "${shared_or_static}" MATCHES "SHARED")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".lib")
elseif(WIN32 AND "${shared_or_static}" MATCHES "STATIC")
set(CMAKE_FIND_LIBRARY_SUFFIXES ".lib;.a;.dll.a")
endif()
set(CMAKE_FIND_LIBRARY_SUFFIXES "${CMAKE_FIND_LIBRARY_SUFFIXES}" PARENT_SCOPE)
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: CMAKE_FIND_LIBRARY_SUFFIXES for ${shared_or_static}: ${CMAKE_FIND_LIBRARY_SUFFIXES}")
endif()
endfunction()
if(GLEW_VERBOSE)
if(DEFINED GLEW_USE_STATIC_LIBS)
message(STATUS "FindGLEW: GLEW_USE_STATIC_LIBS: ${GLEW_USE_STATIC_LIBS}.")
else()
message(STATUS "FindGLEW: GLEW_USE_STATIC_LIBS is undefined. Treated as FALSE.")
endif()
endif()
find_path(GLEW_INCLUDE_DIR GL/glew.h)
mark_as_advanced(GLEW_INCLUDE_DIR)
set(GLEW_INCLUDE_DIRS ${GLEW_INCLUDE_DIR})
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: GLEW_INCLUDE_DIR: ${GLEW_INCLUDE_DIR}")
message(STATUS "FindGLEW: GLEW_INCLUDE_DIRS: ${GLEW_INCLUDE_DIRS}")
endif()
if("${CMAKE_GENERATOR_PLATFORM}" MATCHES "x64" OR "${CMAKE_GENERATOR}" MATCHES "Win64")
set(_arch "x64")
elseif("${CMAKE_GENERATOR_PLATFORM}" MATCHES "ARM64")
set(_arch "x64") # GLEW ships one header set; ARM64 uses the x64 import path
else()
set(_arch "Win32")
endif()
set(__GLEW_CURRENT_FIND_LIBRARY_SUFFIXES ${CMAKE_FIND_LIBRARY_SUFFIXES})
__glew_set_find_library_suffix(SHARED)
find_library(GLEW_SHARED_LIBRARY_RELEASE
NAMES GLEW glew glew32
PATH_SUFFIXES lib lib64 libx32 lib/Release/${_arch}
PATHS ENV GLEW_ROOT)
find_library(GLEW_SHARED_LIBRARY_DEBUG
NAMES GLEWd glewd glew32d
PATH_SUFFIXES lib lib64
PATHS ENV GLEW_ROOT)
__glew_set_find_library_suffix(STATIC)
find_library(GLEW_STATIC_LIBRARY_RELEASE
NAMES GLEW glew glew32s
PATH_SUFFIXES lib lib64 libx32 lib/Release/${_arch}
PATHS ENV GLEW_ROOT)
find_library(GLEW_STATIC_LIBRARY_DEBUG
NAMES GLEWds GLEWd glewd glewds glew32ds
PATH_SUFFIXES lib lib64
PATHS ENV GLEW_ROOT)
set(CMAKE_FIND_LIBRARY_SUFFIXES ${__GLEW_CURRENT_FIND_LIBRARY_SUFFIXES})
unset(__GLEW_CURRENT_FIND_LIBRARY_SUFFIXES)
include(SelectLibraryConfigurations)
select_library_configurations(GLEW_SHARED)
select_library_configurations(GLEW_STATIC)
if(NOT GLEW_USE_STATIC_LIBS)
set(GLEW_LIBRARIES ${GLEW_SHARED_LIBRARY})
else()
set(GLEW_LIBRARIES ${GLEW_STATIC_LIBRARY})
endif()
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: GLEW_SHARED_LIBRARY_RELEASE: ${GLEW_SHARED_LIBRARY_RELEASE}")
message(STATUS "FindGLEW: GLEW_STATIC_LIBRARY_RELEASE: ${GLEW_STATIC_LIBRARY_RELEASE}")
message(STATUS "FindGLEW: GLEW_SHARED_LIBRARY_DEBUG: ${GLEW_SHARED_LIBRARY_DEBUG}")
message(STATUS "FindGLEW: GLEW_STATIC_LIBRARY_DEBUG: ${GLEW_STATIC_LIBRARY_DEBUG}")
message(STATUS "FindGLEW: GLEW_SHARED_LIBRARY: ${GLEW_SHARED_LIBRARY}")
message(STATUS "FindGLEW: GLEW_STATIC_LIBRARY: ${GLEW_STATIC_LIBRARY}")
message(STATUS "FindGLEW: GLEW_LIBRARIES: ${GLEW_LIBRARIES}")
endif()
# Read version from GL/glew.h file
if(EXISTS "${GLEW_INCLUDE_DIR}/GL/glew.h")
file(STRINGS "${GLEW_INCLUDE_DIR}/GL/glew.h" _contents REGEX "^VERSION_.+ [0-9]+")
if(_contents)
string(REGEX REPLACE ".*VERSION_MAJOR[ \t]+([0-9]+).*" "\\1" GLEW_VERSION_MAJOR "${_contents}")
string(REGEX REPLACE ".*VERSION_MINOR[ \t]+([0-9]+).*" "\\1" GLEW_VERSION_MINOR "${_contents}")
string(REGEX REPLACE ".*VERSION_MICRO[ \t]+([0-9]+).*" "\\1" GLEW_VERSION_MICRO "${_contents}")
set(GLEW_VERSION "${GLEW_VERSION_MAJOR}.${GLEW_VERSION_MINOR}.${GLEW_VERSION_MICRO}")
endif()
endif()
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: GLEW_VERSION_MAJOR: ${GLEW_VERSION_MAJOR}")
message(STATUS "FindGLEW: GLEW_VERSION_MINOR: ${GLEW_VERSION_MINOR}")
message(STATUS "FindGLEW: GLEW_VERSION_MICRO: ${GLEW_VERSION_MICRO}")
message(STATUS "FindGLEW: GLEW_VERSION: ${GLEW_VERSION}")
endif()
find_package_handle_standard_args(GLEW
REQUIRED_VARS GLEW_INCLUDE_DIRS GLEW_LIBRARIES
VERSION_VAR GLEW_VERSION)
if(NOT GLEW_FOUND)
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: could not find GLEW library.")
endif()
return()
endif()
if(NOT TARGET GLEW::glew AND NOT GLEW_USE_STATIC_LIBS)
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: Creating GLEW::glew imported target.")
endif()
add_library(GLEW::glew UNKNOWN IMPORTED)
set_target_properties(GLEW::glew
PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${GLEW_INCLUDE_DIRS}")
if(APPLE)
if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.0")
set_target_properties(GLEW::glew
PROPERTIES INTERFACE_LINK_LIBRARIES "-framework OpenGL")
else()
set_target_properties(GLEW::glew
PROPERTIES INTERFACE_LINK_LIBRARIES OpenGL::GL)
endif()
endif()
if(GLEW_SHARED_LIBRARY_RELEASE)
set_property(TARGET GLEW::glew
APPEND
PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
set_target_properties(GLEW::glew
PROPERTIES IMPORTED_LOCATION_RELEASE "${GLEW_SHARED_LIBRARY_RELEASE}")
endif()
if(GLEW_SHARED_LIBRARY_DEBUG)
set_property(TARGET GLEW::glew
APPEND
PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_target_properties(GLEW::glew
PROPERTIES IMPORTED_LOCATION_DEBUG "${GLEW_SHARED_LIBRARY_DEBUG}")
endif()
elseif(NOT TARGET GLEW::glew_s AND GLEW_USE_STATIC_LIBS)
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: Creating GLEW::glew_s imported target.")
endif()
add_library(GLEW::glew_s UNKNOWN IMPORTED)
set_target_properties(GLEW::glew_s
PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${GLEW_INCLUDE_DIRS}")
set_target_properties(GLEW::glew_s PROPERTIES INTERFACE_COMPILE_DEFINITIONS GLEW_STATIC)
if(APPLE)
if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.0")
set_target_properties(GLEW::glew_s
PROPERTIES INTERFACE_LINK_LIBRARIES "-framework OpenGL")
else()
set_target_properties(GLEW::glew_s
PROPERTIES INTERFACE_LINK_LIBRARIES OpenGL::GL)
endif()
endif()
if(GLEW_STATIC_LIBRARY_RELEASE)
set_property(TARGET GLEW::glew_s
APPEND
PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
set_target_properties(GLEW::glew_s
PROPERTIES IMPORTED_LOCATION_RELEASE "${GLEW_STATIC_LIBRARY_RELEASE}")
endif()
if(GLEW_STATIC_LIBRARY_DEBUG)
set_property(TARGET GLEW::glew_s
APPEND
PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_target_properties(GLEW::glew_s
PROPERTIES IMPORTED_LOCATION_DEBUG "${GLEW_STATIC_LIBRARY_DEBUG}")
endif()
endif()
if(NOT TARGET GLEW::GLEW)
if(GLEW_VERBOSE)
message(STATUS "FindGLEW: Creating GLEW::GLEW imported target.")
endif()
add_library(GLEW::GLEW UNKNOWN IMPORTED)
set_target_properties(GLEW::GLEW
PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${GLEW_INCLUDE_DIRS}")
if(APPLE)
if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.0")
set_target_properties(GLEW::GLEW
PROPERTIES INTERFACE_LINK_LIBRARIES "-framework OpenGL")
else()
set_target_properties(GLEW::GLEW
PROPERTIES INTERFACE_LINK_LIBRARIES OpenGL::GL)
endif()
endif()
if(TARGET GLEW::glew)
if(GLEW_SHARED_LIBRARY_RELEASE)
set_property(TARGET GLEW::GLEW
APPEND
PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
set_target_properties(GLEW::GLEW
PROPERTIES IMPORTED_LOCATION_RELEASE "${GLEW_SHARED_LIBRARY_RELEASE}")
endif()
if(GLEW_SHARED_LIBRARY_DEBUG)
set_property(TARGET GLEW::GLEW
APPEND
PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_target_properties(GLEW::GLEW
PROPERTIES IMPORTED_LOCATION_DEBUG "${GLEW_SHARED_LIBRARY_DEBUG}")
endif()
elseif(TARGET GLEW::glew_s)
if(GLEW_STATIC_LIBRARY_RELEASE)
set_property(TARGET GLEW::GLEW
APPEND
PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
set_target_properties(GLEW::GLEW
PROPERTIES IMPORTED_LOCATION_RELEASE "${GLEW_STATIC_LIBRARY_RELEASE}"
INTERFACE_COMPILE_DEFINITIONS GLEW_STATIC)
endif()
if(GLEW_STATIC_LIBRARY_DEBUG AND GLEW_USE_STATIC_LIBS)
set_property(TARGET GLEW::GLEW
APPEND
PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
set_target_properties(GLEW::GLEW
PROPERTIES IMPORTED_LOCATION_DEBUG "${GLEW_STATIC_LIBRARY_DEBUG}"
INTERFACE_COMPILE_DEFINITIONS GLEW_STATIC)
endif()
elseif(GLEW_VERBOSE)
message(WARNING "FindGLEW: no `GLEW::glew` or `GLEW::glew_s` target was created. Something went wrong in FindGLEW target creation.")
endif()
endif()
+3 -3
View File
@@ -5,10 +5,10 @@ endif ()
orcaslicer_add_cmake_project(
CGAL
# GIT_REPOSITORY https://github.com/CGAL/cgal.git
# GIT_TAG 3654f780ae0c64675cabaef0e5ddaf904c48b4b7 # releases/CGAL-5.6.3
# GIT_TAG 28811b671a12b5caa9e3688569dadbc6b3728fe6 # v6.2.1
# For whatever reason, this keeps downloading forever (repeats downloads if finished)
URL https://github.com/CGAL/cgal/releases/download/v5.6.3/CGAL-5.6.3.zip
URL_HASH SHA256=5d577acb4a9918ccb960491482da7a3838f8d363aff47e14d703f19fd84733d4
URL https://github.com/CGAL/cgal/releases/download/v6.2.1/CGAL-6.2.1.zip
URL_HASH SHA256=eebd737d9b7f0199647ba5c1f9f6c7a3d0651aacef9c2fd4dedc52da6c25edbe
DEPENDS dep_Boost dep_Eigen dep_GMP dep_MPFR
)
+4 -7
View File
@@ -1,5 +1,5 @@
if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "4.0")
set(CMAKE_POLICY_VERSION_MINIMUM 3.5 CACHE STRING "" FORCE)
set(CMAKE_POLICY_VERSION_MINIMUM 3.10 CACHE STRING "" FORCE)
endif()
#
@@ -24,7 +24,7 @@ endif()
# therefore, unfortunately, the installation cannot be copied/moved elsewhere without re-installing wxWidgets.
#
cmake_minimum_required(VERSION 3.2)
cmake_minimum_required(VERSION 3.10)
if (APPLE)
# if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 12.0 (the lowest Xcode 27 accepts)
if (NOT CMAKE_OSX_DEPLOYMENT_TARGET)
@@ -224,7 +224,7 @@ if (NOT IS_CROSS_COMPILE OR NOT APPLE)
${_source_dir_arg}
${_gen}
CMAKE_ARGS
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
-DCMAKE_POLICY_VERSION_MINIMUM=3.10
-DCMAKE_INSTALL_PREFIX:STRING=${DESTDIR}
-DCMAKE_MODULE_PATH:STRING=${PROJECT_SOURCE_DIR}/../cmake/modules
-DCMAKE_PREFIX_PATH:STRING=${DESTDIR}
@@ -279,7 +279,7 @@ else()
${_source_dir_arg}
${_gen}
CMAKE_ARGS
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
-DCMAKE_POLICY_VERSION_MINIMUM=3.10
-DCMAKE_INSTALL_PREFIX:STRING=${DESTDIR}
-DCMAKE_PREFIX_PATH:STRING=${DESTDIR}
-DCMAKE_IGNORE_PREFIX_PATH:STRING=${CMAKE_IGNORE_PREFIX_PATH}
@@ -379,10 +379,8 @@ include(Boost/Boost.cmake)
include(Cereal/Cereal.cmake)
include(Qhull/Qhull.cmake)
include(GLEW/GLEW.cmake)
include(GLFW/GLFW.cmake)
include(OpenCSG/OpenCSG.cmake)
set(SLVS_PKG "")
if (SLIC3R_CAD)
include(SLVS/SLVS.cmake)
@@ -478,7 +476,6 @@ set(_dep_list
dep_Draco
dep_NLopt
dep_OpenVDB
dep_OpenCSG
${SLVS_PKG}
dep_OpenCV
dep_Eigen
-2
View File
@@ -1,6 +1,4 @@
orcaslicer_add_cmake_project(EXPAT
# GIT_REPOSITORY https://github.com/nigels-com/glew.git
# GIT_TAG 3a8eff7 # 2.1.0
SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/expat
)
-14
View File
@@ -1,14 +0,0 @@
# We have to check for OpenGL to compile GLEW
set(OpenGL_GL_PREFERENCE "LEGACY") # to prevent a nasty warning by cmake
find_package(OpenGL QUIET REQUIRED)
orcaslicer_add_cmake_project(
GLEW
SOURCE_DIR ${CMAKE_CURRENT_LIST_DIR}/glew
CMAKE_ARGS
-DGLEW_USE_EGL=OFF
)
if (MSVC)
add_debug_dep(dep_GLEW)
endif ()
-44
View File
@@ -1,44 +0,0 @@
cmake_minimum_required(VERSION 3.0)
project(GLEW)
find_package(OpenGL REQUIRED)
# Allow parent project to control EGL usage.
# Default to OFF since OrcaSlicer forces GDK_BACKEND=x11 (using GLX contexts).
# GLEW must use glXGetProcAddressARB (GLX) to match wxWidgets GL canvas.
# Using EGL function loading with GLX contexts causes rendering failures.
option(GLEW_USE_EGL "Use EGL instead of GLX for OpenGL function loading" OFF)
if(GLEW_USE_EGL)
message(STATUS "Building GLEW with EGL support")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DGLEW_EGL")
else()
message(STATUS "Building GLEW with GLX support")
endif()
add_library(GLEW src/glew.c)
target_include_directories(GLEW PRIVATE include/)
target_link_libraries(GLEW PUBLIC OpenGL::GL)
if (NOT BUILD_SHARED_LIBS)
target_compile_definitions(GLEW PUBLIC GLEW_STATIC)
endif ()
include(GNUInstallDirs)
install(
FILES
${PROJECT_SOURCE_DIR}/include/GL/glew.h
${PROJECT_SOURCE_DIR}/include/GL/wglew.h
${PROJECT_SOURCE_DIR}/include/GL/glxew.h
${PROJECT_SOURCE_DIR}/include/GL/eglew.h
DESTINATION
${CMAKE_INSTALL_INCLUDEDIR}/GL
)
install(TARGETS GLEW GLEW
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
-73
View File
@@ -1,73 +0,0 @@
The OpenGL Extension Wrangler Library
Copyright (C) 2002-2007, Milan Ikits <milan ikits[]ieee org>
Copyright (C) 2002-2007, Marcelo E. Magallon <mmagallo[]debian org>
Copyright (C) 2002, Lev Povalahev
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* The name of the author may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.
Mesa 3-D graphics library
Version: 7.0
Copyright (C) 1999-2007 Brian Paul All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
BRIAN PAUL BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Copyright (c) 2007 The Khronos Group Inc.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and/or associated documentation files (the
"Materials"), to deal in the Materials without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Materials, and to
permit persons to whom the Materials are furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Materials.
THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
-251
View File
@@ -1,251 +0,0 @@
# GLEW - The OpenGL Extension Wrangler Library
The OpenGL Extension Wrangler Library (GLEW) is a cross-platform open-source C/C++ extension loading library. GLEW provides efficient run-time mechanisms for determining which OpenGL extensions are supported on the target platform. OpenGL core and extension functionality is exposed in a single header file. GLEW has been tested on a variety of operating systems, including Windows, Linux, Mac OS X, FreeBSD, Irix, and Solaris.
![](http://glew.sourceforge.net/glew.png)
http://glew.sourceforge.net/
https://github.com/nigels-com/glew
[![Build Status](https://travis-ci.org/nigels-com/glew.svg?branch=master)](https://travis-ci.org/nigels-com/glew)
[![Gitter](https://badges.gitter.im/nigels-com/glew.svg)](https://gitter.im/nigels-com/glew?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![Download](https://img.shields.io/sourceforge/dm/glew.svg)](https://sourceforge.net/projects/glew/files/latest/download)
## Table of Contents
* [Downloads](#downloads)
* [Recent snapshots](#recent-snapshots)
* [Build](#build)
* [Linux and Mac](#linux-and-mac)
* [Using GNU Make](#using-gnu-make)
* [Install build tools](#install-build-tools)
* [Build](#build-1)
* [Linux EGL](#linux-egl)
* [Linux mingw-w64](#linux-mingw-w64)
* [Using cmake](#using-cmake)
* [Install build tools](#install-build-tools-1)
* [Build](#build-2)
* [Windows](#windows)
* [Visual Studio](#visual-studio)
* [MSYS/Mingw](#msysmingw)
* [MSYS2/Mingw-w64](#msys2mingw-w64)
* [glewinfo](#glewinfo)
* [Code Generation](#code-generation)
* [Authors](#authors)
* [Contributions](#contributions)
* [Copyright and Licensing](#copyright-and-licensing)
## Downloads
Current release is [2.1.0](https://sourceforge.net/projects/glew/files/glew/2.1.0/).
[(Change Log)](http://glew.sourceforge.net/log.html)
Sources available as
[ZIP](https://sourceforge.net/projects/glew/files/glew/2.1.0/glew-2.1.0.zip/download) or
[TGZ](https://sourceforge.net/projects/glew/files/glew/2.1.0/glew-2.1.0.tgz/download).
Windows binaries for [32-bit and 64-bit](https://sourceforge.net/projects/glew/files/glew/2.1.0/glew-2.1.0-win32.zip/download).
### Recent snapshots
Snapshots may contain new features, bug-fixes or new OpenGL extensions ahead of tested, official releases.
[glew-20200115.tgz](https://sourceforge.net/projects/glew/files/glew/snapshots/glew-20200115.tgz/download) *GLEW 2.2.0 RC3: fixes*
[glew-20190928.tgz](https://sourceforge.net/projects/glew/files/glew/snapshots/glew-20190928.tgz/download) *GLEW 2.2.0 RC2: New extensions, bug fixes*
## Build
It is highly recommended to build from a tgz or zip release snapshot.
The code generation workflow is a complex brew of gnu make, perl and python, that works best on Linux or Mac.
The code generation is known to work on Windows using [MSYS2](https://www.msys2.org/).
For most end-users of GLEW the official releases are the best choice, with first class support.
### Linux and Mac
#### Using GNU Make
GNU make is the primary build system for GLEW, historically.
It includes targets for building the sources and headers, for maintenance purposes.
##### Install build tools
Debian/Ubuntu/Mint: `$ sudo apt-get install build-essential libxmu-dev libxi-dev libgl-dev`
RedHat/CentOS/Fedora: `$ sudo yum install libXmu-devel libXi-devel libGL-devel`
FreeBSD: `# pkg install xorg lang/gcc git cmake gmake bash python perl5`
##### Build
$ make
$ sudo make install
$ make clean
Targets: `all, glew.lib (sub-targets: glew.lib.shared, glew.lib.static), glew.bin, clean, install, uninstall`
Variables: `SYSTEM=linux-clang, GLEW_DEST=/usr/local, STRIP=`
_Note: you may need to call `make` in the **auto** folder first_
##### Linux EGL
$ sudo apt install libegl1-mesa-dev
$ make SYSTEM=linux-egl
##### Linux mingw-w64
$ sudo apt install mingw-w64
$ make SYSTEM=linux-mingw32
$ make SYSTEM=linux-mingw64
#### Using cmake
The cmake build is mostly contributer maintained.
Due to the multitude of use cases this is maintained on a _best effort_ basis.
Pull requests are welcome.
*CMake 2.8.12 or higher is required.*
##### Install build tools
Debian/Ubuntu/Mint: `$ sudo apt-get install build-essential libxmu-dev libxi-dev libgl-dev cmake git`
RedHat/CentOS/Fedora: `$ sudo yum install libXmu-devel libXi-devel libGL-devel cmake git`
##### Build
$ cd build
$ cmake ./cmake
$ make -j4
| Target | Description |
| ---------- | ----------- |
| glew | Build the glew shared library. |
| glew_s | Build the glew static library. |
| glewinfo | Build the `glewinfo` executable (requires `BUILD_UTILS` to be `ON`). |
| visualinfo | Build the `visualinfo` executable (requires `BUILD_UTILS` to be `ON`). |
| install | Install all enabled targets into `CMAKE_INSTALL_PREFIX`. |
| clean | Clean up build artifacts. |
| all | Build all enabled targets (default target). |
| Variables | Description |
| --------------- | ----------- |
| BUILD_UTILS | Build the `glewinfo` and `visualinfo` executables. |
| GLEW_REGAL | Build in Regal mode. |
| BUILD_FRAMEWORK | Build as MacOSX Framework. Setting `CMAKE_INSTALL_PREFIX` to `/Library/Frameworks` is recommended. |
### Windows
#### Visual Studio
Use the provided Visual Studio project file in build/vc15/
Projects for vc6, vc10, vc12 and vc14 are also provided
#### MSYS/Mingw
Available from [Mingw](http://www.mingw.org/)
Requirements: bash, make, gcc
$ mingw32-make
$ mingw32-make install
$ mingw32-make install.all
Alternative toolchain: `SYSTEM=mingw-win32`
#### MSYS2/Mingw-w64
Available from [Msys2](http://msys2.github.io/) and/or [Mingw-w64](http://mingw-w64.org/)
Requirements: bash, make, gcc
$ pacman -S gcc make mingw-w64-i686-gcc mingw-w64-x86_64-gcc
$ make
$ make install
$ make install.all
Alternative toolchain: `SYSTEM=msys, SYSTEM=msys-win32, SYSTEM=msys-win64`
## glewinfo
`glewinfo` is a command-line tool useful for inspecting the capabilities of an
OpenGL implementation and GLEW support for that. Please include `glewinfo.txt`
with bug reports, as appropriate.
---------------------------
GLEW Extension Info
---------------------------
GLEW version 2.0.0
Reporting capabilities of pixelformat 3
Running on a Intel(R) HD Graphics 3000 from Intel
OpenGL version 3.1.0 - Build 9.17.10.4229 is supported
GL_VERSION_1_1: OK
---------------
GL_VERSION_1_2: OK
---------------
glCopyTexSubImage3D: OK
glDrawRangeElements: OK
glTexImage3D: OK
glTexSubImage3D: OK
...
## Code Generation
A Unix or Mac environment is needed for building GLEW from scratch to
include new extensions, or customize the code generation. The extension
data is regenerated from the top level source directory with:
make extensions
An alternative to generating the GLEW sources from scratch is to
download a pre-generated (unsupported) snapshot:
https://sourceforge.net/projects/glew/files/glew/snapshots/
## Authors
GLEW is currently maintained by [Nigel Stewart](https://github.com/nigels-com)
with bug fixes, new OpenGL extension support and new releases.
GLEW was developed by [Milan Ikits](http://www.cs.utah.edu/~ikits/)
and [Marcelo Magallon](http://wwwvis.informatik.uni-stuttgart.de/~magallon/).
Aaron Lefohn, Joe Kniss, and Chris Wyman were the first users and also
assisted with the design and debugging process.
The acronym GLEW originates from Aaron Lefohn.
Pasi K&auml;rkk&auml;inen identified and fixed several problems with
GLX and SDL. Nate Robins created the `wglinfo` utility, to
which modifications were made by Michael Wimmer.
## Contributions
GLEW welcomes community contributions. Typically these are co-ordinated
via [Issues](https://github.com/nigels-com/glew/issues) or
[Pull Requests](https://github.com/nigels-com/glew/pulls) in the
GitHub web interface.
Be sure to mention platform and compiler toolchain details when filing
a bug report. The output of `glewinfo` can be quite useful for discussion
also.
Generally GLEW is usually released once a year, around the time of the Siggraph
computer graphics conference. If you're not using the current release
version of GLEW, be sure to check if the issue or bug is fixed there.
## Copyright and Licensing
GLEW is originally derived from the EXTGL project by Lev Povalahev.
The source code is licensed under the
[Modified BSD License](http://glew.sourceforge.net/glew.txt), the
[Mesa 3-D License](http://glew.sourceforge.net/mesa.txt) (MIT) and the
[Khronos License](http://glew.sourceforge.net/khronos.txt) (MIT).
The automatic code generation scripts are released under the
[GNU GPL](http://glew.sourceforge.net/gpl.txt).
-1
View File
@@ -1 +0,0 @@
2.2.0
-3051
View File
File diff suppressed because it is too large Load Diff
-26427
View File
File diff suppressed because it is too large Load Diff
-1831
View File
File diff suppressed because it is too large Load Diff
-1468
View File
File diff suppressed because it is too large Load Diff
-31949
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,28 @@
diff --git a/src/ModelingAlgorithms/TKMesh/BRepMesh/BRepMesh_ModelPreProcessor.cxx b/src/ModelingAlgorithms/TKMesh/BRepMesh/BRepMesh_ModelPreProcessor.cxx
index 6f63781..9b1c08e 100644
--- a/src/ModelingAlgorithms/TKMesh/BRepMesh/BRepMesh_ModelPreProcessor.cxx
+++ b/src/ModelingAlgorithms/TKMesh/BRepMesh/BRepMesh_ModelPreProcessor.cxx
@@ -210,10 +210,10 @@ private:
// Define two pcurves of the seam-edge.
occ::handle<Geom2d_Curve> aPC1, aPC2;
- double af, al;
+ double af, al, af1, al1;
aE.Orientation(TopAbs_FORWARD);
- aPC1 = BRep_Tool::CurveOnSurface(aE, aF, af, al);
+ aPC1 = BRep_Tool::CurveOnSurface(aE, aF, af1, al1);
aE.Orientation(TopAbs_REVERSED);
aPC2 = BRep_Tool::CurveOnSurface(aE, aF, af, al);
@@ -224,7 +224,9 @@ private:
}
// Select the correct pcurve of the seam-edge.
- const gp_Pnt2d& aFPntOfPC1 = aPC1->Value(aPC1->FirstParameter());
+ // Use the edge's first parameter. A Geom2d_Line's FirstParameter() is -Precision::Infinite(),
+ // where a direction of (2e-16, -1) from rounding error gives an X far outside the U range.
+ const gp_Pnt2d aFPntOfPC1 = aPC1->Value(af1);
if (std::abs(aLPntOfIPC1.X() - aFPntOfPC1.X()) > Precision::Confusion())
{
-264
View File
@@ -1,264 +0,0 @@
diff --git a/adm/cmake/occt_defs_flags.cmake b/adm/cmake/occt_defs_flags.cmake
index 00000000..00000001 100644
--- a/adm/cmake/occt_defs_flags.cmake
+++ b/adm/cmake/occt_defs_flags.cmake
@@ -134,7 +134,11 @@
set (CMAKE_CXX_FLAGS "-std=c++0x ${CMAKE_CXX_FLAGS}")
endif()
# Optimize size of binaries
- set (CMAKE_SHARED_LINKER_FLAGS "-Wl,-s ${CMAKE_SHARED_LINKER_FLAGS}")
+ # clang-cl reports the Clang compiler ID, and OCCT builds shared on Windows,
+ # where the MSVC-style linker gets this flag as an argument it does not know.
+ if (NOT WIN32)
+ set (CMAKE_SHARED_LINKER_FLAGS "-Wl,-s ${CMAKE_SHARED_LINKER_FLAGS}")
+ endif()
elseif(MINGW)
add_definitions(-D_WIN32_WINNT=0x0601)
# _WIN32_WINNT=0x0601 (use Windows 7 SDK)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index d98acc0f..28eb8eb4 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -225,7 +225,7 @@ if (NOT DEFINED INSTALL_DIR_BIN)
if ("${INSTALL_DIR_LAYOUT}" STREQUAL "Unix")
set (INSTALL_DIR_BIN "bin" CACHE PATH "${INSTALL_DIR_BIN_DESCR}")
else()
- set (INSTALL_DIR_BIN "${OS_WITH_BIT}/${COMPILER}/bin" CACHE PATH "${INSTALL_DIR_BIN_DESCR}")
+ set (INSTALL_DIR_BIN "bin/occt" CACHE PATH "${INSTALL_DIR_BIN_DESCR}")
endif()
endif()
@@ -243,11 +243,11 @@ if (NOT DEFINED INSTALL_DIR_LIB)
if ("${INSTALL_DIR_LAYOUT}" STREQUAL "Unix")
set (INSTALL_DIR_LIB "lib" CACHE PATH "${INSTALL_DIR_LIB_DESCR}")
else()
- set (INSTALL_DIR_LIB "${OS_WITH_BIT}/${COMPILER}/lib" CACHE PATH "${INSTALL_DIR_LIB_DESCR}")
+ set (INSTALL_DIR_LIB "lib/occt" CACHE PATH "${INSTALL_DIR_LIB_DESCR}")
endif()
endif()
-# OCCT headers: <prefix>/inc for windows,
+# OCCT headers: <prefix>/include for windows,
# <prefix>/include/opencascade-7.0.0 for unix
if (NOT DEFINED INSTALL_DIR_INCLUDE)
if ("${INSTALL_DIR_LAYOUT}" STREQUAL "Unix")
@@ -256,7 +256,7 @@ if (NOT DEFINED INSTALL_DIR_INCLUDE)
set (INSTALL_DIR_INCLUDE "include/opencascade-${OCC_VERSION_STRING_EXT}" CACHE PATH "${INSTALL_DIR_INCLUDE_DESCR}" FORCE)
endif()
else()
- set (INSTALL_DIR_INCLUDE "inc" CACHE PATH "${INSTALL_DIR_INCLUDE_DESCR}")
+ set (INSTALL_DIR_INCLUDE "include/occt" CACHE PATH "${INSTALL_DIR_INCLUDE_DESCR}")
endif()
endif()
@@ -330,7 +330,7 @@ if (NOT DEFINED INSTALL_DIR_CMAKE)
set (INSTALL_DIR_CMAKE "lib/cmake/opencascade" CACHE PATH "${INSTALL_DIR_CMAKE_DESCR}")
endif()
else()
- set (INSTALL_DIR_CMAKE "cmake" CACHE PATH "${INSTALL_DIR_CMAKE_DESCR}")
+ set (INSTALL_DIR_CMAKE "lib/cmake/occt" CACHE PATH "${INSTALL_DIR_CMAKE_DESCR}")
endif()
endif()
@@ -338,13 +338,13 @@ endif()
OCCT_INCLUDE_CMAKE_FILE ("adm/cmake/occt_resources")
# install LICENSE_LGPL_21.txt and OCCT_LGPL_EXCEPTION.txt files
-if ("${INSTALL_DIR_LAYOUT}" STREQUAL "Unix")
- OCCT_INSTALL_FILE_OR_DIR ("LICENSE_LGPL_21.txt" "${INSTALL_DIR_DOC}")
- OCCT_INSTALL_FILE_OR_DIR ("OCCT_LGPL_EXCEPTION.txt" "${INSTALL_DIR_DOC}")
-else()
- OCCT_INSTALL_FILE_OR_DIR ("LICENSE_LGPL_21.txt" ".")
- OCCT_INSTALL_FILE_OR_DIR ("OCCT_LGPL_EXCEPTION.txt" ".")
-endif()
+#if ("${INSTALL_DIR_LAYOUT}" STREQUAL "Unix")
+# OCCT_INSTALL_FILE_OR_DIR ("LICENSE_LGPL_21.txt" "${INSTALL_DIR_DOC}")
+# OCCT_INSTALL_FILE_OR_DIR ("OCCT_LGPL_EXCEPTION.txt" "${INSTALL_DIR_DOC}")
+#else()
+# OCCT_INSTALL_FILE_OR_DIR ("LICENSE_LGPL_21.txt" ".")
+# OCCT_INSTALL_FILE_OR_DIR ("OCCT_LGPL_EXCEPTION.txt" ".")
+#endif()
if(APPLE)
set (INSTALL_NAME_DIR "" CACHE STRING "install_name library suffix on OS X (e.g. @executable_path/../Frameworks)")
@@ -850,34 +850,34 @@ endif()
# build directories
if (SINGLE_GENERATOR)
- set (CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${OS_WITH_BIT}/${COMPILER}/lib${BIN_LETTER}")
- set (CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${OS_WITH_BIT}/${COMPILER}/bin${BIN_LETTER}")
- set (CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${OS_WITH_BIT}/${COMPILER}/lib${BIN_LETTER}")
+ set (CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib/occt")
+ set (CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/occt")
+ set (CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib/occt")
if (WIN32)
- set (CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${OS_WITH_BIT}/${COMPILER}/bin${BIN_LETTER}")
+ set (CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin/occt")
endif()
endif()
-set (CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/${OS_WITH_BIT}/${COMPILER}/lib")
-set (CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/${OS_WITH_BIT}/${COMPILER}/bin")
-set (CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/${OS_WITH_BIT}/${COMPILER}/lib")
+set (CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/lib/occt")
+set (CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/bin/occt")
+set (CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/lib/occt")
-set (CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}/${OS_WITH_BIT}/${COMPILER}/libi")
-set (CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}/${OS_WITH_BIT}/${COMPILER}/bini")
-set (CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}/${OS_WITH_BIT}/${COMPILER}/libi")
+set (CMAKE_ARCHIVE_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}/lib/occt/RelWithDebInfo")
+set (CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}/bin/occt/RelWithDebInfo")
+set (CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}/lib/occt/RelWithDebInfo")
-set (CMAKE_ARCHIVE_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/${OS_WITH_BIT}/${COMPILER}/libd")
-set (CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/${OS_WITH_BIT}/${COMPILER}/bind")
-set (CMAKE_LIBRARY_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/${OS_WITH_BIT}/${COMPILER}/libd")
+set (CMAKE_ARCHIVE_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/lib/occt/Debug")
+set (CMAKE_RUNTIME_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/bin/occt/Debug")
+set (CMAKE_LIBRARY_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/lib/occt/Debug")
if (WIN32)
- set (CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/${OS_WITH_BIT}/${COMPILER}/bin")
- set (CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}/${OS_WITH_BIT}/${COMPILER}/bini")
- set (CMAKE_LIBRARY_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/${OS_WITH_BIT}/${COMPILER}/bind")
+ set (CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE "${CMAKE_BINARY_DIR}/bin/occt")
+ set (CMAKE_LIBRARY_OUTPUT_DIRECTORY_RELWITHDEBINFO "${CMAKE_BINARY_DIR}/bin/occt/RelWithDebInfo")
+ set (CMAKE_LIBRARY_OUTPUT_DIRECTORY_DEBUG "${CMAKE_BINARY_DIR}/bin/occt/Debug")
endif()
string(TIMESTAMP CURRENT_TIME "%H:%M:%S")
-message (STATUS "\nInfo: \(${CURRENT_TIME}\) Start collecting all OCCT header files into ${CMAKE_BINARY_DIR}/inc ...")
+message (STATUS "\nInfo: \(${CURRENT_TIME}\) Start collecting all OCCT header files into ${CMAKE_BINARY_DIR}/include/occt ...")
# collect all the headers to <binary dir>/inc folder
COLLECT_AND_INSTALL_OCCT_HEADER_FILES ("${CMAKE_BINARY_DIR}" "${BUILD_TOOLKITS}" "${CMAKE_SOURCE_DIR}/src" "${INSTALL_DIR_INCLUDE}")
@@ -984,9 +984,9 @@ if (EXISTS "${INSTALL_DIR}/${INSTALL_DIR_SCRIPT}/custom.${SCRIPT_EXT}")
set (CUSTOM_CONTENT "${CUSTOM_CONTENT} ${ADDITIONAL_CUSTOM_CONTENT}")
- file (WRITE "${INSTALL_DIR}/${INSTALL_DIR_SCRIPT}/custom.${SCRIPT_EXT}" "${CUSTOM_CONTENT}")
+ #file (WRITE "${INSTALL_DIR}/${INSTALL_DIR_SCRIPT}/custom.${SCRIPT_EXT}" "${CUSTOM_CONTENT}")
else()
- OCCT_CONFIGURE_AND_INSTALL ("adm/templates/custom.${SCRIPT_EXT}.main" "custom.${SCRIPT_EXT}" "custom.${SCRIPT_EXT}" "${INSTALL_DIR_SCRIPT}")
+ #OCCT_CONFIGURE_AND_INSTALL ("adm/templates/custom.${SCRIPT_EXT}.main" "custom.${SCRIPT_EXT}" "custom.${SCRIPT_EXT}" "${INSTALL_DIR_SCRIPT}")
endif()
if (WIN32)
@@ -1007,7 +1007,7 @@ endforeach()
# write current custom.bat/sh (for install directory)
set (SUB_CUSTOM_BUILD_NAME "custom_${COMPILER}_${COMPILER_BITNESS}.install.${SCRIPT_EXT}")
-OCCT_CONFIGURE_AND_INSTALL ("adm/templates/custom.install.${SCRIPT_EXT}.in" "${SUB_CUSTOM_BUILD_NAME}" "${SUB_CUSTOM_NAME}" "${INSTALL_DIR_SCRIPT}")
+#OCCT_CONFIGURE_AND_INSTALL ("adm/templates/custom.install.${SCRIPT_EXT}.in" "${SUB_CUSTOM_BUILD_NAME}" "${SUB_CUSTOM_NAME}" "${INSTALL_DIR_SCRIPT}")
# write current custom.bat/sh (for build directory)
OCCT_CONFIGURE ("adm/templates/custom.build.${SCRIPT_EXT}.in" "${SUB_CUSTOM_NAME}")
@@ -1019,9 +1019,9 @@ endif()
if (WIN32)
# env script for draw in building environment
- OCCT_CONFIGURE ("adm/templates/env.${SCRIPT_EXT}.in" "env.${SCRIPT_EXT}")
+ #OCCT_CONFIGURE ("adm/templates/env.${SCRIPT_EXT}.in" "env.${SCRIPT_EXT}")
# install env script
- install (FILES "${CMAKE_BINARY_DIR}/env.${SCRIPT_EXT}" DESTINATION "${INSTALL_DIR_SCRIPT}")
+ #install (FILES "${CMAKE_BINARY_DIR}/env.${SCRIPT_EXT}" DESTINATION "${INSTALL_DIR_SCRIPT}")
# copy build.bat and install.bat scripts to CMake binary folder
OCCT_COPY_FILE_OR_DIR ("adm/templates/build.bat" "${CMAKE_BINARY_DIR}")
OCCT_COPY_FILE_OR_DIR ("adm/templates/install.bat" "${CMAKE_BINARY_DIR}")
@@ -1043,12 +1043,12 @@ endif()
FILE_TO_LIST ("adm/RESOURCES" RESOURCES)
foreach(RESOURCE ${RESOURCES})
get_filename_component(RESOURCE_FOLDER ${RESOURCE} DIRECTORY)
- if(NOT "${RESOURCE_FOLDER}" STREQUAL "")
- get_filename_component(RESOURCE_FOLDER ${RESOURCE_FOLDER} NAME)
- OCCT_INSTALL_FILE_OR_DIR ("src/${RESOURCE}" "${INSTALL_DIR_RESOURCE}/${RESOURCE_FOLDER}")
- else()
- OCCT_INSTALL_FILE_OR_DIR ("src/${RESOURCE}" "${INSTALL_DIR_RESOURCE}")
- endif()
+ #if(NOT "${RESOURCE_FOLDER}" STREQUAL "")
+ # get_filename_component(RESOURCE_FOLDER ${RESOURCE_FOLDER} NAME)
+ # OCCT_INSTALL_FILE_OR_DIR ("src/${RESOURCE}" "${INSTALL_DIR_RESOURCE}/${RESOURCE_FOLDER}")
+ #else()
+ # OCCT_INSTALL_FILE_OR_DIR ("src/${RESOURCE}" "${INSTALL_DIR_RESOURCE}")
+ #endif()
endforeach()
if (BUILD_SAMPLES_QT)
diff --git a/adm/cmake/occt_macros.cmake b/adm/cmake/occt_macros.cmake
index 224c96b1..8c94a1c5 100644
--- a/adm/cmake/occt_macros.cmake
+++ b/adm/cmake/occt_macros.cmake
@@ -608,7 +608,7 @@ macro (OCCT_INSERT_CODE_FOR_TARGET)
install(CODE "if (\"\${CMAKE_INSTALL_CONFIG_NAME}\" MATCHES \"^([Rr][Ee][Ll][Ee][Aa][Ss][Ee])$\")
set (OCCT_INSTALL_BIN_LETTER \"\")
elseif (\"\${CMAKE_INSTALL_CONFIG_NAME}\" MATCHES \"^([Rr][Ee][Ll][Ww][Ii][Tt][Hh][Dd][Ee][Bb][Ii][Nn][Ff][Oo])$\")
- set (OCCT_INSTALL_BIN_LETTER \"i\")
+ set (OCCT_INSTALL_BIN_LETTER \"\")
elseif (\"\${CMAKE_INSTALL_CONFIG_NAME}\" MATCHES \"^([Dd][Ee][Bb][Uu][Gg])$\")
set (OCCT_INSTALL_BIN_LETTER \"d\")
endif()")
diff --git a/adm/cmake/occt_toolkit.cmake b/adm/cmake/occt_toolkit.cmake
index 550e0e2f..7ac1a3b8 100644
--- a/adm/cmake/occt_toolkit.cmake
+++ b/adm/cmake/occt_toolkit.cmake
@@ -241,7 +241,7 @@
else()
set (aReleasePdbConf)
endif()
- install (FILES ${CMAKE_BINARY_DIR}/${OS_WITH_BIT}/${COMPILER}/bin\${OCCT_INSTALL_BIN_LETTER}/${PROJECT_NAME}.pdb
+ install (FILES $<TARGET_PDB_FILE:${PROJECT_NAME}>
CONFIGURATIONS Debug ${aReleasePdbConf} RelWithDebInfo
DESTINATION "${INSTALL_DIR_BIN}\${OCCT_INSTALL_BIN_LETTER}")
endif()
diff --git a/src/Font/Font_FTFont.cxx b/src/Font/Font_FTFont.cxx
index 5ae9899f..0a17372b 100644
--- a/src/Font/Font_FTFont.cxx
+++ b/src/Font/Font_FTFont.cxx
@@ -103,9 +103,11 @@ bool Font_FTFont::Init (const Handle(NCollection_Buffer)& theData,
{
throw Standard_ProgramError ("Font_FTFont, Light and Normal hinting styles are mutually exclusive");
}
+#ifdef HAVE_FREETYPE
setLoadFlag (FT_LOAD_TARGET_LIGHT, (theParams.FontHinting & Font_Hinting_Light) != 0);
setLoadFlag (FT_LOAD_NO_HINTING, (theParams.FontHinting & Font_Hinting_Normal) == 0
&& (theParams.FontHinting & Font_Hinting_Light) == 0);
+#endif
// manage native / autohinting
if ((theParams.FontHinting & Font_Hinting_ForceAutohint) != 0
@@ -113,8 +115,10 @@ bool Font_FTFont::Init (const Handle(NCollection_Buffer)& theData,
{
throw Standard_ProgramError ("Font_FTFont, ForceAutohint and NoAutohint are mutually exclusive");
}
+#ifdef HAVE_FREETYPE
setLoadFlag (FT_LOAD_FORCE_AUTOHINT, (theParams.FontHinting & Font_Hinting_ForceAutohint) != 0);
setLoadFlag (FT_LOAD_NO_AUTOHINT, (theParams.FontHinting & Font_Hinting_NoAutohint) != 0);
+#endif
if (!myFTLib->IsValid())
{
From 7236e83dcc1e7284e66dc61e612154617ef715d6 Mon Sep 17 00:00:00 2001
From: dpasukhi <dpasukhi@opencascade.com>
Date: Tue, 27 Aug 2024 11:33:29 +0100
Subject: [PATCH] 0033808: Coding - FreeType Use unsigned point and contour
indexing in `FT_Outline`
Changes to auto instead of specific type
---
src/StdPrs/StdPrs_BRepFont.cxx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/StdPrs/StdPrs_BRepFont.cxx b/src/StdPrs/StdPrs_BRepFont.cxx
index ab2d9b3c9f..cd701879b1 100644
--- a/src/StdPrs/StdPrs_BRepFont.cxx
+++ b/src/StdPrs/StdPrs_BRepFont.cxx
@@ -457,7 +457,7 @@ Standard_Boolean StdPrs_BRepFont::renderGlyph (const Standard_Utf32Char theChar,
for (short aContour = 0, aStartIndex = 0; aContour < anOutline->n_contours; ++aContour)
{
const FT_Vector* aPntList = &anOutline->points[aStartIndex];
- const char* aTags = &anOutline->tags[aStartIndex];
+ const auto* aTags = &anOutline->tags[aStartIndex];
const short anEndIndex = anOutline->contours[aContour];
const short aPntsNb = (anEndIndex - aStartIndex) + 1;
aStartIndex = anEndIndex + 1;
+21 -14
View File
@@ -1,5 +1,5 @@
# clang-cl cannot emit IGESAppli_GeneralModule.cxx on ARM64
# (llvm/llvm-project#62081). cl and clang-cl share an ABI.
# clang-cl cannot emit some OCCT sources for ARM64 (llvm/llvm-project#62081).
# cl and clang-cl share an ABI.
set(_occt_compiler_args "")
if ("${DEPS_ARCH}" STREQUAL "arm64" AND CMAKE_CXX_COMPILER_ID STREQUAL Clang)
set(_occt_compiler_args -DCMAKE_C_COMPILER:STRING=cl -DCMAKE_CXX_COMPILER:STRING=cl)
@@ -15,31 +15,38 @@ endif()
# (fillet/offset/loft), whose only consumer is the parametric Design/CAD tab. With it OFF
# the deps prefix matches upstream exactly.
#
# With it ON the delta is THREE toolkits, not two: TKFillet (7.40 MiB archive, used via
# BRepFilletAPI), TKOffset (5.38 MiB, used via BRepOffsetAPI) and TKFeat (4.42 MiB), which
# nothing here references but which the module flag builds anyway -- it is all-or-nothing
# per module. The module's other nine toolkits are built either way, because DataExchange
# (the STEP path upstream already ships) depends on them.
# With it ON OCCT also builds TKFillet (used via BRepFilletAPI), TKOffset (used via
# BRepOffsetAPI), and TKFeat, TKHelix, TKXMesh and TKExpress, which nothing here references
# but which the module flag builds anyway, since module flags are all-or-nothing. The
# module's other toolkits are built either way, because DataExchange (the STEP path)
# depends on them.
#
# On macOS/Linux OCCT links statically, so an unreferenced toolkit costs build time and no
# shipped bytes. The Windows figure is a real DLL cost and has NOT been measured -- an
# earlier "3.77 MiB, Windows only" note here covered only two of the three toolkits and is
# not a number to quote. See docs/HLSD/design-tab.md.
# shipped bytes. Windows ships only the DLLs libslic3r links, so the tab adds the TKFillet,
# TKOffset and TKBool DLLs. See docs/HLSD/design-tab.md.
if (IN_GIT_REPO)
set(OCCT_DIRECTORY_FLAG --directory ${BINARY_DIR_REL}/dep_OCCT-prefix/src/dep_OCCT)
endif ()
orcaslicer_add_cmake_project(OCCT
URL https://github.com/Open-Cascade-SAS/OCCT/archive/refs/tags/V7_6_0.zip
URL_HASH SHA256=28334f0e98f1b1629799783e9b4d21e05349d89e695809d7e6dfa45ea43e1dbc
#PATCH_COMMAND ${PATCH_CMD} ${CMAKE_CURRENT_LIST_DIR}/0001-OCCT-fix.patch
PATCH_COMMAND git apply ${OCCT_DIRECTORY_FLAG} --verbose --ignore-space-change --whitespace=fix ${CMAKE_CURRENT_LIST_DIR}/0001-OCCT-fix.patch
URL https://github.com/Open-Cascade-SAS/OCCT/archive/refs/tags/V8_0_1.zip
URL_HASH SHA256=7c033d917ee8f040c0512d289dcc5f02c148889d5bac17c3e25639accb44f0da
# Makes BRepMesh triangulate cone faces whose seam pcurve is slightly tilted
# (Open-Cascade-SAS/OCCT#572); remove the patch once an OCCT release includes the fix.
PATCH_COMMAND git apply ${OCCT_DIRECTORY_FLAG} --verbose --ignore-space-change --whitespace=fix ${CMAKE_CURRENT_LIST_DIR}/0001-BRepMesh-seam-pcurve-at-edge-parameter.patch
#DEPENDS dep_Boost
DEPENDS ${FREETYPE_PKG}
CMAKE_ARGS
-DCMAKE_CXX_STANDARD=17
-DBUILD_LIBRARY_TYPE=${library_build_type}
# With the Unix layout, OCCT's resources and licenses go under share/ and its scripts
# into bin/occt on Windows too. libslic3r finds the CMake package in lib/cmake/occt.
-DINSTALL_DIR_LAYOUT=Unix
-DINSTALL_DIR_BIN=bin/occt
-DINSTALL_DIR_LIB=lib/occt
-DINSTALL_DIR_INCLUDE=include/occt
-DINSTALL_DIR_CMAKE=lib/cmake/occt
-DUSE_TK=OFF
-DUSE_TBB=OFF
#-DUSE_FREETYPE=OFF
-101
View File
@@ -1,101 +0,0 @@
cmake_minimum_required(VERSION 3.0)
project(OpenCSG)
if (NOT BUILD_SHARED_LIBS)
set(GLEW_USE_STATIC_LIBS ON)
elseif (MSVC)
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
endif()
find_package(OpenGL REQUIRED)
set(GLEW_VERBOSE ON)
find_package(GLEW 1.13.0 REQUIRED)
set(_srcfiles
src/area.cpp
src/batch.cpp
src/context.cpp
src/channelManager.cpp
src/frameBufferObject.cpp
src/frameBufferObjectExt.cpp
src/occlusionQuery.cpp
src/opencsgRender.cpp
src/openglHelper.cpp
src/pBufferTexture.cpp
src/primitive.cpp
src/primitiveHelper.cpp
src/renderGoldfeather.cpp
src/renderSCS.cpp
src/scissorMemo.cpp
src/settings.cpp
src/stencilManager.cpp
RenderTexture/RenderTexture.cpp
include/opencsg.h
src/opencsgConfig.h
src/area.h
src/batch.h
src/context.h
src/channelManager.h
src/frameBufferObject.h
src/frameBufferObjectExt.h
src/occlusionQuery.h
src/offscreenBuffer.h
src/opencsgRender.h
src/openglHelper.h
src/pBufferTexture.h
src/primitiveHelper.h
src/scissorMemo.h
src/settings.h
src/stencilManager.h
)
add_library(opencsg ${_srcfiles})
target_include_directories(opencsg PUBLIC $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>)
target_include_directories(opencsg PUBLIC $<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}>)
target_link_libraries(opencsg PRIVATE GLEW::GLEW OpenGL::GL)
include(CMakePackageConfigHelpers)
include(GNUInstallDirs)
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake"
VERSION 1.4.2
COMPATIBILITY AnyNewerVersion
)
install(TARGETS opencsg
EXPORT ${PROJECT_NAME}Targets
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
export(EXPORT ${PROJECT_NAME}Targets
FILE "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake"
NAMESPACE ${PROJECT_NAME}:: )
set(ConfigPackageLocation ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME})
install(EXPORT ${PROJECT_NAME}Targets
FILE
"${PROJECT_NAME}Config.cmake"
NAMESPACE
${PROJECT_NAME}::
DESTINATION
${ConfigPackageLocation}
)
install(
FILES
${PROJECT_SOURCE_DIR}/include/opencsg.h
DESTINATION
${CMAKE_INSTALL_INCLUDEDIR}/opencsg
)
install(
FILES
"${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake"
DESTINATION
${ConfigPackageLocation}
)
-17
View File
@@ -1,17 +0,0 @@
orcaslicer_add_cmake_project(OpenCSG
# GIT_REPOSITORY https://github.com/floriankirsch/OpenCSG.git
# GIT_TAG 83e274457b46c9ad11a4ee599203250b1618f3b9 #v1.4.2
URL https://github.com/floriankirsch/OpenCSG/archive/refs/tags/opencsg-1-4-2-release.zip
URL_HASH SHA256=51afe0db79af8386e2027d56d685177135581e0ee82ade9d7f2caff8deab5ec5
PATCH_COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/CMakeLists.txt.in ./CMakeLists.txt
DEPENDS dep_GLEW
)
if (TARGET ${ZLIB_PKG})
add_dependencies(dep_OpenCSG ${ZLIB_PKG})
endif()
if (MSVC)
add_debug_dep(dep_OpenCSG)
endif ()
+39
View File
@@ -29,12 +29,33 @@ if(WIN32)
# driven through nmake from a Ninja configure step.
set(_conf_cmd ${CMAKE_COMMAND} -E env ${_openssl_msvc_env} perl Configure )
set(_cross_comp_prefix_line "")
if("${DEPS_ARCH}" STREQUAL "arm64")
# OpenSSL's VC configs pass /Gs0, which puts a __chkstk probe in every
# function. MSVC 14.51 and 14.52 (VS 2026) for ARM64 emit that call
# before the prologue saves LR, so the function returns into itself;
# in tls_parse_all_extensions that breaks every TLS handshake. 14.44
# (VS 2022) is unaffected. Restore cl's default threshold: Configure
# appends /Gs4096 after /Gs0, and the later option wins.
set(_openssl_extra_cflags /Gs4096)
endif()
set(_make_cmd ${CMAKE_COMMAND} -E env ${_openssl_msvc_env} nmake)
set(_install_cmd ${CMAKE_COMMAND} -E env ${_openssl_msvc_env} nmake install_sw )
else()
if(APPLE)
set(_conf_cmd export MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} && ./Configure -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET})
else()
# A static library that is embedded into a shared object must not export
# its symbols. On Linux the running process also loads the system OpenSSL
# 3.x (WebKitGTK/gnutls pull in libcrypto.so.3), and CPython's _ssl and
# _hashlib are dlopened (RTLD_LOCAL) DSOs that each embed this OpenSSL.
# With default visibility their unversioned OpenSSL references are
# preempted by that global 3.x copy, mixing the 1.1.1 and 3.x ABIs and
# corrupting the heap (ssl.create_default_context() aborts). Hidden
# visibility makes each embedded copy self-contained. Linux-only: macOS
# binds dylibs with a two-level namespace (no interposition) and ships no
# OpenSSL, and Windows has no equivalent flag and no system OpenSSL to
# collide with.
set(_openssl_extra_cflags -fvisibility=hidden)
set(_conf_cmd env "CC=${CMAKE_C_COMPILER}" "LDFLAGS=${CMAKE_EXE_LINKER_FLAGS}" "./config")
endif()
set(_cross_comp_prefix_line "")
@@ -71,6 +92,7 @@ ExternalProject_Add(dep_OpenSSL
# prefix stays single-layout.
"--libdir=lib"
${_cross_comp_prefix_line}
${_openssl_extra_cflags}
no-shared
no-asm
no-ssl3-method
@@ -92,3 +114,20 @@ ExternalProject_Add_Step(dep_OpenSSL install_cmake_files
COMMAND ${CMAKE_COMMAND} -E copy_directory openssl "${DESTDIR}${CMAKE_INSTALL_LIBDIR}/cmake/openssl"
WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}"
)
if (NOT WIN32 AND NOT APPLE)
# OpenSSL's object rules do not depend on CFLAGS, so reconfiguring it (for
# example to add -fvisibility=hidden) relinks the archives from stale
# objects instead of recompiling them, and the change silently has no
# effect. Drop the objects whenever this recipe changes so the next build
# actually recompiles them.
ExternalProject_Get_Property(dep_OpenSSL SOURCE_DIR)
ExternalProject_Add_Step(dep_OpenSSL clean_objects
DEPENDEES configure
DEPENDERS build
COMMAND make clean
WORKING_DIRECTORY "${SOURCE_DIR}"
DEPENDS "${CMAKE_CURRENT_LIST_FILE}"
COMMENT "OpenSSL: cleaning objects after a recipe change"
)
endif ()
+35 -1
View File
@@ -151,6 +151,12 @@ elseif(APPLE)
# the post-install -add_rpath below.
set(_python_ldflags "${_python_arch_flags} -Wl,-headerpad_max_install_names")
# The macOS 27 SDK declares pipe2() and dup3() as available from macOS 27, so
# configure finds them and CPython 3.12 calls them without a runtime check.
# Below a macOS 27 deployment target they are weak-linked and resolve to NULL
# on older systems, where os.pipe() then segfaults -- in `make install`
# (compileall, ensurepip) and in the shipped app alike. Every configure below
# keeps the pipe()/dup2() fallbacks (python/cpython#153711).
if(IS_CROSS_COMPILE)
set(_python_build_tgt --build=${_python_build_arch}-apple-darwin --host=${_python_host_arch}-apple-darwin)
set(_python_build_arch_flags "-arch ${_python_build_arch_flag} -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}")
@@ -174,7 +180,8 @@ elseif(APPLE)
--enable-shared \
--without-static-libpython \
--disable-test-modules \
--build=${_python_build_arch}-apple-darwin && \
--build=${_python_build_arch}-apple-darwin \
ac_cv_func_pipe2=no ac_cv_func_dup3=no && \
make -j${NPROC} python && \
cd '<SOURCE_DIR>' && \
env \
@@ -191,6 +198,7 @@ elseif(APPLE)
--without-static-libpython \
--with-openssl='${DESTDIR}' \
--disable-test-modules \
ac_cv_func_pipe2=no ac_cv_func_dup3=no \
${_python_build_tgt} \
--with-build-python='${_python_build_python}' \
py_cv_module__tkinter=n/a"
@@ -213,6 +221,8 @@ elseif(APPLE)
--with-openssl=${DESTDIR}
--disable-test-modules
${_python_build_tgt}
ac_cv_func_pipe2=no
ac_cv_func_dup3=no
# Tcl/Tk 9.0 (e.g. from Homebrew) is incompatible with CPython 3.12's
# _tkinter; OrcaSlicer's embedded Python does not need tkinter anyway.
py_cv_module__tkinter=n/a
@@ -289,3 +299,27 @@ endif()
if(TARGET dep_ZLIB)
add_dependencies(dep_python3 dep_ZLIB)
endif()
if (NOT WIN32 AND NOT APPLE)
# CPython's Makefile rules for _ssl and _hashlib depend only on their own
# sources, not on the OpenSSL archives, so a rebuilt OpenSSL does not make
# them relink and they keep the previous symbols. On an incremental tree,
# drop the built modules and relink them against the current OpenSSL; a
# fresh build is left alone (its PGO target builds them). "make" alone is a
# no-op once PGO has run, so sharedmods is invoked explicitly.
ExternalProject_Get_Property(dep_python3 SOURCE_DIR)
file(GLOB _python_ssl_modules
"${SOURCE_DIR}/Modules/_ssl*.so"
"${SOURCE_DIR}/Modules/_hashlib*.so")
if (_python_ssl_modules)
ExternalProject_Add_Step(dep_python3 relink_ssl_extensions
DEPENDEES configure
DEPENDERS build
COMMAND sh -c "rm -f '${SOURCE_DIR}'/Modules/_ssl*.so '${SOURCE_DIR}'/Modules/_hashlib*.so && make -j${NPROC} sharedmods"
WORKING_DIRECTORY "${SOURCE_DIR}"
COMMENT "CPython: relinking _ssl/_hashlib against the current OpenSSL"
DEPENDS "${CMAKE_CURRENT_LIST_FILE}"
"${CMAKE_CURRENT_LIST_DIR}/../OpenSSL/OpenSSL.cmake"
)
endif ()
endif ()
-1
View File
@@ -15,7 +15,6 @@ add_subdirectory(stb_dxt) # Header-only STB DXT compression library
# Static libraries
add_subdirectory(Shiny)
add_subdirectory(admesh)
add_subdirectory(clipper)
add_subdirectory(clipper2)
add_subdirectory(expat)
add_subdirectory(glu-libtess)
+13
View File
@@ -75,6 +75,19 @@ static FILE *stl_open_count_facets(stl_file *stl, const char *file, unsigned int
break;
}
}
// Zero normals and coordinates like 10 or 15 have no byte above 127, so the test above can miss a binary file.
// Its size still matches its facet count; text read as that count would need a file of gigabytes.
if (stl->stats.type == ascii) {
uint32_t header_num_facets;
fseek(fp, custom_header_length, SEEK_SET);
if (fread(&header_num_facets, sizeof(uint32_t), 1, fp) == 1) {
#if BOOST_ENDIAN_BIG_BYTE
stl_internal_reverse_quads((char*)&header_num_facets, 4);
#endif /* BOOST_ENDIAN_BIG_BYTE */
if (header_size + uint64_t(header_num_facets) * SIZEOF_STL_FACET == file_size)
stl->stats.type = binary;
}
}
rewind(fp);
uint32_t num_facets = 0;
-20
View File
@@ -1,20 +0,0 @@
cmake_minimum_required(VERSION 3.13)
project(clipper)
add_library(clipper STATIC
# We are using ClipperLib compiled as part of the libslic3r project using Slic3r::Point as its base type.
# clipper.cpp
# clipper.hpp
clipper_z.cpp
clipper_z.hpp
)
target_include_directories(clipper SYSTEM
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)
target_link_libraries(clipper
PUBLIC Eigen3::Eigen
PRIVATE TBB::tbb TBB::tbbmalloc
)
File diff suppressed because it is too large Load Diff
-606
View File
@@ -1,606 +0,0 @@
/*******************************************************************************
* *
* Author : Angus Johnson *
* Version : 6.4.2 *
* Date : 27 February 2017 *
* Website : http://www.angusj.com *
* Copyright : Angus Johnson 2010-2017 *
* *
* License: *
* Use, modification & distribution is subject to Boost Software License Ver 1. *
* http://www.boost.org/LICENSE_1_0.txt *
* *
* Attributions: *
* The code in this library is an extension of Bala Vatti's clipping algorithm: *
* "A generic solution to polygon clipping" *
* Communications of the ACM, Vol 35, Issue 7 (July 1992) pp 56-63. *
* http://portal.acm.org/citation.cfm?id=129906 *
* *
* Computer graphics and geometric modeling: implementation and algorithms *
* By Max K. Agoston *
* Springer; 1 edition (January 4, 2005) *
* http://books.google.com/books?q=vatti+clipping+agoston *
* *
* See also: *
* "Polygon Offsetting by Computing Winding Numbers" *
* Paper no. DETC2005-85513 pp. 565-575 *
* ASME 2005 International Design Engineering Technical Conferences *
* and Computers and Information in Engineering Conference (IDETC/CIE2005) *
* September 24-28, 2005 , Long Beach, California, USA *
* http://www.me.berkeley.edu/~mcmains/pubs/DAC05OffsetPolygon.pdf *
* *
*******************************************************************************/
#ifndef clipper_hpp
#define clipper_hpp
#include <inttypes.h>
#include <functional>
#include <Eigen/Geometry>
#include <oneapi/tbb/scalable_allocator.h>
#define CLIPPER_VERSION "6.2.6"
//CLIPPERLIB_USE_XYZ: adds a Z member to IntPoint. Adds a minor cost to perfomance.
//#define CLIPPERLIB_USE_XYZ
//use_lines: Enables line clipping. Adds a very minor cost to performance.
#define use_lines
//use_deprecated: Enables temporary support for the obsolete functions
//#define use_deprecated
#include <array>
#include <vector>
#include <deque>
#include <stdexcept>
#include <cstring>
#include <cstdlib>
#include <ostream>
#include <functional>
#include <queue>
#ifdef CLIPPERLIB_NAMESPACE_PREFIX
namespace CLIPPERLIB_NAMESPACE_PREFIX {
#endif // CLIPPERLIB_NAMESPACE_PREFIX
#ifdef CLIPPERLIB_USE_XYZ
namespace ClipperLib_Z {
#else
namespace ClipperLib {
#endif
enum ClipType { ctIntersection, ctUnion, ctDifference, ctXor };
enum PolyType { ptSubject, ptClip };
//By far the most widely used winding rules for polygon filling are
//EvenOdd & NonZero (GDI, GDI+, XLib, OpenGL, Cairo, AGG, Quartz, SVG, Gr32)
//Others rules include Positive, Negative and ABS_GTR_EQ_TWO (only in OpenGL)
//see http://glprogramming.com/red/chapter11.html
enum PolyFillType { pftEvenOdd, pftNonZero, pftPositive, pftNegative };
// If defined, Clipper will work with 32bit signed int coordinates to reduce memory
// consumption and to speed up exact orientation predicate calculation.
// In that case, coordinates and their differences (vectors of the coordinates) have to fit int32_t.
// #define CLIPPERLIB_INT32
// Point coordinate type
#ifdef CLIPPERLIB_INT32
// Coordinates and their differences (vectors of the coordinates) have to fit int32_t.
using cInt = int32_t;
using CrossProductType = int64_t;
#else
using cInt = int64_t;
using CrossProductType = double;
// Maximum cInt value to allow a cross product calculation using 32bit expressions.
static constexpr cInt const loRange = 0x3FFFFFFF; // 0x3FFFFFFF = 1 073 741 823
// Maximum allowed cInt value.
static constexpr cInt const hiRange = 0x3FFFFFFFFFFFFFFFLL;
#endif // CLIPPERLIB_INT32
#ifdef CLIPPERLIB_INTPOINT_TYPE
using IntPoint = CLIPPERLIB_INTPOINT_TYPE;
#else // CLIPPERLIB_INTPOINT_TYPE
using IntPoint = Eigen::Matrix<cInt,
#ifdef CLIPPERLIB_USE_XYZ
3
#else // CLIPPERLIB_USE_XYZ
2
#endif // CLIPPERLIB_USE_XYZ
, 1, Eigen::DontAlign>;
#endif // CLIPPERLIB_INTPOINT_TYPE
using DoublePoint = Eigen::Matrix<double, 2, 1, Eigen::DontAlign>;
//------------------------------------------------------------------------------
template<typename BaseType>
using Allocator = tbb::scalable_allocator<BaseType>;
//using Allocator = std::allocator<BaseType>;
using Path = std::vector<IntPoint, Allocator<IntPoint>>;
using Paths = std::vector<Path, Allocator<Path>>;
inline Path& operator <<(Path& poly, const IntPoint& p) {poly.push_back(p); return poly;}
inline Paths& operator <<(Paths& polys, const Path& p) {polys.push_back(p); return polys;}
std::ostream& operator <<(std::ostream &s, const IntPoint &p);
std::ostream& operator <<(std::ostream &s, const Path &p);
std::ostream& operator <<(std::ostream &s, const Paths &p);
//------------------------------------------------------------------------------
#ifdef CLIPPERLIB_USE_XYZ
typedef std::function<void(const IntPoint& e1bot, const IntPoint& e1top, const IntPoint& e2bot, const IntPoint& e2top, IntPoint& pt)> ZFillCallback;
#endif
enum InitOptions {ioReverseSolution = 1, ioStrictlySimple = 2, ioPreserveCollinear = 4};
enum JoinType {jtSquare, jtRound, jtMiter};
enum EndType {etClosedPolygon, etClosedLine, etOpenButt, etOpenSquare, etOpenRound};
class PolyNode;
typedef std::vector<PolyNode*, Allocator<PolyNode*>> PolyNodes;
class PolyNode
{
public:
PolyNode() : Parent(0), Index(0), m_IsOpen(false) {}
virtual ~PolyNode(){};
Path Contour;
PolyNodes Childs;
PolyNode* Parent;
// Traversal of the polygon tree in a depth first fashion.
PolyNode* GetNext() const { return Childs.empty() ? GetNextSiblingUp() : Childs.front(); }
bool IsHole() const;
bool IsOpen() const { return m_IsOpen; }
int ChildCount() const { return (int)Childs.size(); }
private:
unsigned Index; //node index in Parent.Childs
bool m_IsOpen;
JoinType m_jointype;
EndType m_endtype;
PolyNode* GetNextSiblingUp() const { return Parent ? ((Index == Parent->Childs.size() - 1) ? Parent->GetNextSiblingUp() : Parent->Childs[Index + 1]) : nullptr; }
void AddChild(PolyNode& child);
friend class Clipper; //to access Index
friend class ClipperOffset;
friend class PolyTree; //to implement the PolyTree::move operator
};
class PolyTree: public PolyNode
{
public:
PolyTree() {}
PolyTree(PolyTree &&src) { *this = std::move(src); }
virtual ~PolyTree(){Clear();};
PolyTree& operator=(PolyTree &&src) {
AllNodes = std::move(src.AllNodes);
Contour = std::move(src.Contour);
Childs = std::move(src.Childs);
Parent = nullptr;
Index = src.Index;
m_IsOpen = src.m_IsOpen;
m_jointype = src.m_jointype;
m_endtype = src.m_endtype;
for (size_t i = 0; i < Childs.size(); ++ i)
Childs[i]->Parent = this;
return *this;
}
PolyNode* GetFirst() const { return Childs.empty() ? nullptr : Childs.front(); }
void Clear() { AllNodes.clear(); Childs.clear(); }
int Total() const;
void RemoveOutermostPolygon();
private:
PolyTree(const PolyTree &src) = delete;
PolyTree& operator=(const PolyTree &src) = delete;
std::vector<PolyNode, Allocator<PolyNode>> AllNodes;
friend class Clipper; //to access AllNodes
};
double Area(const Path &poly);
inline bool Orientation(const Path &poly) { return Area(poly) >= 0; }
int PointInPolygon(const IntPoint &pt, const Path &path);
// Union with "strictly simple" fix enabled.
Paths SimplifyPolygon(const Path &in_poly, PolyFillType fillType = pftNonZero, bool strictly_simple = true);
void CleanPolygon(const Path& in_poly, Path& out_poly, double distance = 1.415);
void CleanPolygon(Path& poly, double distance = 1.415);
void CleanPolygons(const Paths& in_polys, Paths& out_polys, double distance = 1.415);
void CleanPolygons(Paths& polys, double distance = 1.415);
void MinkowskiSum(const Path& pattern, const Path& path, Paths& solution, bool pathIsClosed);
void MinkowskiSum(const Path& pattern, const Paths& paths, Paths& solution, bool pathIsClosed);
void MinkowskiDiff(const Path& poly1, const Path& poly2, Paths& solution);
void PolyTreeToPaths(const PolyTree& polytree, Paths& paths);
void PolyTreeToPaths(PolyTree&& polytree, Paths& paths);
void ClosedPathsFromPolyTree(const PolyTree& polytree, Paths& paths);
void OpenPathsFromPolyTree(PolyTree& polytree, Paths& paths);
void ReversePath(Path& p);
void ReversePaths(Paths& p);
struct IntRect { cInt left; cInt top; cInt right; cInt bottom; };
//enums that are used internally ...
enum EdgeSide { esLeft = 1, esRight = 2};
// namespace Internal {
//forward declarations (for stuff used internally) ...
struct TEdge {
// Bottom point of this edge (with minimum Y).
IntPoint Bot;
// Current position.
IntPoint Curr;
// Top point of this edge (with maximum Y).
IntPoint Top;
// Slope (dx/dy). For horiontal edges, the slope is set to HORIZONTAL (-1.0E+40).
double Dx;
PolyType PolyTyp;
EdgeSide Side;
// Winding number delta. 1 or -1 depending on winding direction, 0 for open paths and flat closed paths.
int WindDelta;
int WindCnt;
int WindCnt2; //winding count of the opposite polytype
int OutIdx;
// Next edge in the input path.
TEdge *Next;
// Previous edge in the input path.
TEdge *Prev;
// Next edge in the Local Minima List chain.
TEdge *NextInLML;
TEdge *NextInAEL;
TEdge *PrevInAEL;
TEdge *NextInSEL;
TEdge *PrevInSEL;
};
struct IntersectNode {
IntersectNode(TEdge *Edge1, TEdge *Edge2, IntPoint Pt) :
Edge1(Edge1), Edge2(Edge2), Pt(Pt) {}
TEdge *Edge1;
TEdge *Edge2;
IntPoint Pt;
};
struct LocalMinimum {
cInt Y;
TEdge *LeftBound;
TEdge *RightBound;
};
// Point of an output polygon.
// 36B on 64bit system without CLIPPERLIB_USE_XYZ.
struct OutPt {
// 4B
int Idx;
// 16B without CLIPPERLIB_USE_XYZ / 24B with CLIPPERLIB_USE_XYZ
IntPoint Pt;
// 4B on 32bit system, 8B on 64bit system
OutPt *Next;
// 4B on 32bit system, 8B on 64bit system
OutPt *Prev;
};
using OutPts = std::vector<OutPt, Allocator<OutPt>>;
// Output polygon.
struct OutRec {
int Idx;
bool IsHole;
bool IsOpen;
//The 'FirstLeft' field points to another OutRec that contains or is the
//'parent' of OutRec. It is 'first left' because the ActiveEdgeList (AEL) is
//parsed left from the current edge (owning OutRec) until the owner OutRec
//is found. This field simplifies sorting the polygons into a tree structure
//which reflects the parent/child relationships of all polygons.
//This field should be renamed Parent, and will be later.
OutRec* FirstLeft;
// Used only by void Clipper::BuildResult2(PolyTree& polytree)
PolyNode* PolyNd;
// Linked list of output points, dynamically allocated.
OutPt* Pts;
OutPt* BottomPt;
};
struct Join {
Join(OutPt *OutPt1, OutPt *OutPt2, IntPoint OffPt) :
OutPt1(OutPt1), OutPt2(OutPt2), OffPt(OffPt) {}
OutPt *OutPt1;
OutPt *OutPt2;
IntPoint OffPt;
};
// }; // namespace Internal
//------------------------------------------------------------------------------
//ClipperBase is the ancestor to the Clipper class. It should not be
//instantiated directly. This class simply abstracts the conversion of sets of
//polygon coordinates into edge objects that are stored in a LocalMinima list.
class ClipperBase
{
public:
ClipperBase() :
#ifndef CLIPPERLIB_INT32
m_UseFullRange(false),
#endif // CLIPPERLIB_INT32
m_HasOpenPaths(false) {}
~ClipperBase() { Clear(); }
bool AddPath(const Path &pg, PolyType PolyTyp, bool Closed);
template<typename PathsProvider>
bool AddPaths(PathsProvider &&paths_provider, PolyType PolyTyp, bool Closed)
{
size_t num_paths = paths_provider.size();
if (num_paths == 0)
return false;
if (num_paths == 1)
return AddPath(*paths_provider.begin(), PolyTyp, Closed);
std::vector<int, Allocator<int>> num_edges(num_paths, 0);
int num_edges_total = 0;
size_t i = 0;
for (const Path &pg : paths_provider) {
// Remove duplicate end point from a closed input path.
// Remove duplicate points from the end of the input path.
int highI = (int)pg.size() -1;
if (Closed)
while (highI > 0 && (pg[highI] == pg[0]))
--highI;
while (highI > 0 && (pg[highI] == pg[highI -1]))
--highI;
if ((Closed && highI < 2) || (!Closed && highI < 1))
highI = -1;
num_edges[i ++] = highI + 1;
num_edges_total += highI + 1;
}
if (num_edges_total == 0)
return false;
// Allocate a new edge array.
std::vector<TEdge, Allocator<TEdge>> edges(num_edges_total);
// Fill in the edge array.
bool result = false;
TEdge *p_edge = edges.data();
i = 0;
for (const Path &pg : paths_provider) {
if (num_edges[i] && !pg.empty()) {
bool res = AddPathInternal(pg, num_edges[i] - 1, PolyTyp, Closed, p_edge);
if (res) {
p_edge += num_edges[i];
result = true;
}
}
++ i;
}
if (result)
// At least some edges were generated. Remember the edge array.
m_edges.emplace_back(std::move(edges));
return result;
}
void Clear();
IntRect GetBounds();
// By default, when three or more vertices are collinear in input polygons (subject or clip), the Clipper object removes the 'inner' vertices before clipping.
// When enabled the PreserveCollinear property prevents this default behavior to allow these inner vertices to appear in the solution.
bool PreserveCollinear() const {return m_PreserveCollinear;};
void PreserveCollinear(bool value) {m_PreserveCollinear = value;};
protected:
bool AddPathInternal(const Path &pg, int highI, PolyType PolyTyp, bool Closed, TEdge* edges);
TEdge* AddBoundsToLML(TEdge *e, bool IsClosed);
void Reset();
TEdge* ProcessBound(TEdge* E, bool IsClockwise);
TEdge* DescendToMin(TEdge *&E);
void AscendToMax(TEdge *&E, bool Appending, bool IsClosed);
// Local minima (Y, left edge, right edge) sorted by ascending Y.
std::vector<LocalMinimum, Allocator<LocalMinimum>> m_MinimaList;
#ifdef CLIPPERLIB_INT32
static constexpr const bool m_UseFullRange = false;
#else // CLIPPERLIB_INT32
// True if the input polygons have abs values higher than loRange, but lower than hiRange.
// False if the input polygons have abs values lower or equal to loRange.
bool m_UseFullRange;
#endif // CLIPPERLIB_INT32
// A vector of edges per each input path.
using Edges = std::vector<TEdge, Allocator<TEdge>>;
std::vector<Edges, Allocator<Edges>> m_edges;
// Don't remove intermediate vertices of a collinear sequence of points.
bool m_PreserveCollinear;
// Is any of the paths inserted by AddPath() or AddPaths() open?
bool m_HasOpenPaths;
};
//------------------------------------------------------------------------------
class Clipper : public ClipperBase
{
public:
Clipper(int initOptions = 0);
~Clipper() { Clear(); }
void Clear() { ClipperBase::Clear(); DisposeAllOutRecs(); }
bool Execute(ClipType clipType,
Paths &solution,
PolyFillType fillType = pftEvenOdd)
{ return Execute(clipType, solution, fillType, fillType); }
bool Execute(ClipType clipType,
Paths &solution,
PolyFillType subjFillType,
PolyFillType clipFillType);
bool Execute(ClipType clipType,
PolyTree &polytree,
PolyFillType fillType = pftEvenOdd)
{ return Execute(clipType, polytree, fillType, fillType); }
bool Execute(ClipType clipType,
PolyTree &polytree,
PolyFillType subjFillType,
PolyFillType clipFillType);
bool ReverseSolution() const { return m_ReverseOutput; };
void ReverseSolution(bool value) {m_ReverseOutput = value;};
bool StrictlySimple() const {return m_StrictSimple;};
void StrictlySimple(bool value) {m_StrictSimple = value;};
//set the callback function for z value filling on intersections (otherwise Z is 0)
#ifdef CLIPPERLIB_USE_XYZ
void ZFillFunction(ZFillCallback zFillFunc) { m_ZFill = zFillFunc; }
#endif
protected:
void Reset();
virtual bool ExecuteInternal();
private:
// Output polygons.
std::deque<OutRec, Allocator<OutRec>> m_PolyOuts;
// Output points, allocated by a continuous sets of m_OutPtsChunkSize.
static constexpr const size_t m_OutPtsChunkSize = 32;
std::deque<std::array<OutPt, m_OutPtsChunkSize>, Allocator<std::array<OutPt, m_OutPtsChunkSize>>> m_OutPts;
// List of free output points, to be used before taking a point from m_OutPts or allocating a new chunk.
OutPt *m_OutPtsFree;
size_t m_OutPtsChunkLast;
std::vector<Join, Allocator<Join>> m_Joins;
std::vector<Join, Allocator<Join>> m_GhostJoins;
std::vector<IntersectNode, Allocator<IntersectNode>> m_IntersectList;
ClipType m_ClipType;
// A priority queue (a binary heap) of Y coordinates.
using cInts = std::vector<cInt, Allocator<cInt>>;
std::priority_queue<cInt, cInts> m_Scanbeam;
// Maxima are collected by ProcessEdgesAtTopOfScanbeam(), consumed by ProcessHorizontal().
cInts m_Maxima;
TEdge *m_ActiveEdges;
TEdge *m_SortedEdges;
PolyFillType m_ClipFillType;
PolyFillType m_SubjFillType;
bool m_ReverseOutput;
// Does the result go to a PolyTree or Paths?
bool m_UsingPolyTree;
bool m_StrictSimple;
#ifdef CLIPPERLIB_USE_XYZ
ZFillCallback m_ZFill; //custom callback
#endif
void SetWindingCount(TEdge& edge) const;
bool IsEvenOddFillType(const TEdge& edge) const
{ return (edge.PolyTyp == ptSubject) ? m_SubjFillType == pftEvenOdd : m_ClipFillType == pftEvenOdd; }
bool IsEvenOddAltFillType(const TEdge& edge) const
{ return (edge.PolyTyp == ptSubject) ? m_ClipFillType == pftEvenOdd : m_SubjFillType == pftEvenOdd; }
void InsertLocalMinimaIntoAEL(const cInt botY);
void InsertEdgeIntoAEL(TEdge *edge, TEdge* startEdge);
void AddEdgeToSEL(TEdge *edge);
void CopyAELToSEL();
void DeleteFromSEL(TEdge *e);
void DeleteFromAEL(TEdge *e);
void UpdateEdgeIntoAEL(TEdge *&e);
void SwapPositionsInSEL(TEdge *edge1, TEdge *edge2);
bool IsContributing(const TEdge& edge) const;
bool IsTopHorz(const cInt XPos);
void SwapPositionsInAEL(TEdge *edge1, TEdge *edge2);
void DoMaxima(TEdge *e);
void ProcessHorizontals();
void ProcessHorizontal(TEdge *horzEdge);
void AddLocalMaxPoly(TEdge *e1, TEdge *e2, const IntPoint &pt);
OutPt* AddLocalMinPoly(TEdge *e1, TEdge *e2, const IntPoint &pt);
OutRec* GetOutRec(int idx);
void AppendPolygon(TEdge *e1, TEdge *e2);
void IntersectEdges(TEdge *e1, TEdge *e2, IntPoint &pt);
OutRec* CreateOutRec();
OutPt* AddOutPt(TEdge *e, const IntPoint &pt);
OutPt* GetLastOutPt(TEdge *e);
OutPt* AllocateOutPt();
OutPt* DupOutPt(OutPt* outPt, bool InsertAfter);
// Add the point to a list of free points.
void DisposeOutPt(OutPt *pt) { pt->Next = m_OutPtsFree; m_OutPtsFree = pt; }
void DisposeOutPts(OutPt*& pp) { if (pp != nullptr) { pp->Prev->Next = m_OutPtsFree; m_OutPtsFree = pp; } }
void DisposeAllOutRecs();
bool ProcessIntersections(const cInt topY);
void BuildIntersectList(const cInt topY);
void ProcessEdgesAtTopOfScanbeam(const cInt topY);
void BuildResult(Paths& polys);
void BuildResult2(PolyTree& polytree);
void SetHoleState(TEdge *e, OutRec *outrec);
bool FixupIntersectionOrder();
void FixupOutPolygon(OutRec &outrec);
void FixupOutPolyline(OutRec &outrec);
bool FindOwnerFromSplitRecs(OutRec &outRec, OutRec *&currOrfl);
void FixHoleLinkage(OutRec &outrec);
bool JoinPoints(Join *j, OutRec* outRec1, OutRec* outRec2);
bool JoinHorz(OutPt* op1, OutPt* op1b, OutPt* op2, OutPt* op2b, const IntPoint &Pt, bool DiscardLeft);
void JoinCommonEdges();
void DoSimplePolygons();
void FixupFirstLefts1(OutRec* OldOutRec, OutRec* NewOutRec);
void FixupFirstLefts2(OutRec* InnerOutRec, OutRec* OuterOutRec);
void FixupFirstLefts3(OutRec* OldOutRec, OutRec* NewOutRec);
#ifdef CLIPPERLIB_USE_XYZ
void SetZ(IntPoint& pt, TEdge& e1, TEdge& e2);
#endif
};
//------------------------------------------------------------------------------
class ClipperOffset
{
public:
ClipperOffset(double miterLimit = 2.0, double roundPrecision = 0.25, double shortestEdgeLength = 0.) :
MiterLimit(miterLimit), ArcTolerance(roundPrecision), ShortestEdgeLength(shortestEdgeLength), m_lowest(-1, 0) {}
~ClipperOffset() { Clear(); }
void AddPath(const Path& path, JoinType joinType, EndType endType);
template<typename PathsProvider>
void AddPaths(PathsProvider &&paths, JoinType joinType, EndType endType) {
for (const Path &path : paths)
AddPath(path, joinType, endType);
}
void Execute(Paths& solution, double delta);
void Execute(PolyTree& solution, double delta);
void Clear();
double MiterLimit;
double ArcTolerance;
double ShortestEdgeLength;
private:
Paths m_destPolys;
Path m_srcPoly;
Path m_destPoly;
std::vector<DoublePoint, Allocator<DoublePoint>> m_normals;
double m_delta, m_sinA, m_sin, m_cos;
double m_miterLim, m_StepsPerRad;
// x: index of the lowest contour in m_polyNodes
// y: index of the lowest point in the lowest contour
IntPoint m_lowest;
PolyNode m_polyNodes;
void FixOrientations();
void DoOffset(double delta);
void OffsetPoint(int j, int& k, JoinType jointype);
void DoSquare(int j, int k);
void DoMiter(int j, int k, double r);
void DoRound(int j, int k);
};
//------------------------------------------------------------------------------
class clipperException : public std::exception
{
public:
clipperException(const char* description): m_descr(description) {}
virtual ~clipperException() throw() {}
virtual const char* what() const throw() {return m_descr.c_str();}
private:
std::string m_descr;
};
//------------------------------------------------------------------------------
// Union with "strictly simple" fix enabled.
template<typename PathsProvider>
inline Paths SimplifyPolygons(PathsProvider &&in_polys, PolyFillType fillType = pftNonZero, bool strictly_simple = true) {
Clipper c;
c.StrictlySimple(strictly_simple);
c.AddPaths(std::forward<PathsProvider>(in_polys), ptSubject, true);
Paths out;
c.Execute(ctUnion, out, fillType, fillType);
return out;
}
} //ClipperLib namespace
#ifdef CLIPPERLIB_NAMESPACE_PREFIX
} // namespace CLIPPERLIB_NAMESPACE_PREFIX
#endif // CLIPPERLIB_NAMESPACE_PREFIX
#endif //clipper_hpp
-7
View File
@@ -1,7 +0,0 @@
// Hackish wrapper around the ClipperLib library to compile the Clipper library with the Z support.
// Enable the Z coordinate support.
#define CLIPPERLIB_USE_XYZ
// and let it compile
#include "clipper.cpp"
-18
View File
@@ -1,18 +0,0 @@
// Hackish wrapper around the ClipperLib library to compile the Clipper library with the Z support.
#ifndef clipper_z_hpp
#ifdef clipper_hpp
#error "You should include clipper_z.hpp before clipper.hpp"
#endif
#define clipper_z_hpp
// Enable the Z coordinate support.
#define CLIPPERLIB_USE_XYZ
#include "clipper.hpp"
#undef clipper_hpp
#undef CLIPPERLIB_USE_XYZ
#endif // clipper_z_hpp
+6 -1
View File
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.10)
project(Clipper2 VERSION 1.5.2 LANGUAGES C CXX)
project(Clipper2 VERSION 2.0.1 LANGUAGES C CXX)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
set(CMAKE_CXX_STANDARD 17)
@@ -19,6 +19,7 @@ set(CLIPPER2_INC
Clipper2Lib/include/clipper2/clipper.minkowski.h
Clipper2Lib/include/clipper2/clipper.offset.h
Clipper2Lib/include/clipper2/clipper.rectclip.h
Clipper2Lib/include/clipper2/clipper.triangulation.h
Clipper2Lib/include/clipper2/clipper2_z.hpp
)
@@ -26,6 +27,7 @@ set(CLIPPER2_SRC
Clipper2Lib/src/clipper.engine.cpp
Clipper2Lib/src/clipper.offset.cpp
Clipper2Lib/src/clipper.rectclip.cpp
Clipper2Lib/src/clipper.triangulation.cpp
Clipper2Lib/src/clipper2_z.cpp
)
@@ -36,6 +38,9 @@ target_include_directories(Clipper2
PUBLIC Clipper2Lib/include
)
# Engine nodes are allocated through tbbmalloc (see clipper.engine.cpp).
target_link_libraries(Clipper2 PRIVATE TBB::tbbmalloc)
if (WIN32)
if (MSVC AND NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
target_compile_options(Clipper2 PRIVATE /W4 /WX)
@@ -1,8 +1,8 @@
/*******************************************************************************
* Author : Angus Johnson *
* Date : 12 May 2024 *
* Date : 12 October 2025 *
* Website : https://www.angusj.com *
* Copyright : Angus Johnson 2010-2024 *
* Copyright : Angus Johnson 2010-2025 *
* Purpose : Core Clipper Library structures and functions *
* License : https://www.boost.org/LICENSE_1_0.txt *
*******************************************************************************/
@@ -251,6 +251,20 @@ namespace Clipper2Lib {
template <typename T>
using Paths = std::vector<Path<T>>;
template <typename T, typename T2=T>
Path<T>& operator<<(Path<T>& poly, const Point<T2>& p)
{
poly.emplace_back(p);
return poly;
}
template <typename T>
Paths<T>& operator<<(Paths<T>& polys, const Path<T>& p)
{
polys.emplace_back(p);
return polys;
}
using Path64 = Path<int64_t>;
using PathD = Path<double>;
using Paths64 = std::vector< Path64>;
@@ -685,32 +699,31 @@ namespace Clipper2Lib {
inline int TriSign(int64_t x) // returns 0, 1 or -1
{
return (x > 0) - (x < 0);
return (x > 0) - (x < 0);
}
struct MultiplyUInt64Result
struct UInt128Struct
{
const uint64_t result = 0;
const uint64_t carry = 0;
const uint64_t lo = 0;
const uint64_t hi = 0;
bool operator==(const MultiplyUInt64Result& other) const
bool operator==(const UInt128Struct& other) const
{
return result == other.result && carry == other.carry;
return lo == other.lo && hi == other.hi;
};
};
inline MultiplyUInt64Result Multiply(uint64_t a, uint64_t b) // #834, #835
inline UInt128Struct MultiplyUInt64(uint64_t a, uint64_t b) // #834, #835
{
// note to self - lamba expressions follow
const auto lo = [](uint64_t x) { return x & 0xFFFFFFFF; };
const auto hi = [](uint64_t x) { return x >> 32; };
const uint64_t x1 = lo(a) * lo(b);
const uint64_t x2 = hi(a) * lo(b) + hi(x1);
const uint64_t x3 = lo(a) * hi(b) + lo(x2);
const uint64_t result = lo(x3) << 32 | lo(x1);
const uint64_t carry = hi(a) * hi(b) + hi(x2) + hi(x3);
return { result, carry };
return { uint64_t(lo(x3) << 32 | lo(x1)), uint64_t(hi(a) * hi(b) + hi(x2) + hi(x3)) };
}
// returns true if (and only if) a * b == c * d
@@ -727,14 +740,50 @@ namespace Clipper2Lib {
const auto abs_c = static_cast<uint64_t>(std::abs(c));
const auto abs_d = static_cast<uint64_t>(std::abs(d));
const auto abs_ab = Multiply(abs_a, abs_b);
const auto abs_cd = Multiply(abs_c, abs_d);
const auto ab = MultiplyUInt64(abs_a, abs_b);
const auto cd = MultiplyUInt64(abs_c, abs_d);
// nb: it's important to differentiate 0 values here from other values
const auto sign_ab = TriSign(a) * TriSign(b);
const auto sign_cd = TriSign(c) * TriSign(d);
return abs_ab == abs_cd && sign_ab == sign_cd;
return ab == cd && sign_ab == sign_cd;
#endif
}
template <typename T>
inline int CrossProductSign(const Point<T>& pt1, const Point<T>& pt2, const Point<T>& pt3)
{
const auto a = pt2.x - pt1.x;
const auto b = pt3.y - pt2.y;
const auto c = pt2.y - pt1.y;
const auto d = pt3.x - pt2.x;
#if (defined(__clang__) || defined(__GNUC__)) && UINTPTR_MAX >= UINT64_MAX
const auto ab = static_cast<__int128_t>(a) * static_cast<__int128_t>(b);
const auto cd = static_cast<__int128_t>(c) * static_cast<__int128_t>(d);
if (ab > cd) return 1;
else if (ab < cd) return -1;
else return 0;
#else
const auto ab = MultiplyUInt64(std::abs(a), std::abs(b));
const auto cd = MultiplyUInt64(std::abs(c), std::abs(d));
const auto sign_ab = TriSign(a) * TriSign(b);
const auto sign_cd = TriSign(c) * TriSign(d);
if (sign_ab == sign_cd)
{
int result;
if (ab.hi == cd.hi)
{
if (ab.lo == cd.lo) return 0;
result = (ab.lo > cd.lo) ? 1 : -1;
}
else result = (ab.hi > cd.hi) ? 1 : -1;
return (sign_ab > 0) ? result : -result;
}
return (sign_ab > sign_cd) ? 1 : -1;
#endif
}
@@ -838,6 +887,10 @@ namespace Clipper2Lib {
return Area<T>(poly) >= 0;
}
// GetLineIntersectPt - a 'true' result is non-parallel. The 'ip' will also
// be constrained to seg1. However, it's possible that 'ip' won't be inside
// seg2, even when 'ip' hasn't been constrained (ie 'ip' is inside seg1).
#if CLIPPER2_HI_PRECISION
// caution: this will compromise performance
// https://github.com/AngusJohnson/Clipper2/issues/317#issuecomment-1314023253
@@ -845,7 +898,7 @@ namespace Clipper2Lib {
#define CC_MIN(x,y) ((x)>(y)?(y):(x))
#define CC_MAX(x,y) ((x)<(y)?(y):(x))
template<typename T>
inline bool GetSegmentIntersectPt(const Point<T>& ln1a, const Point<T>& ln1b,
inline bool GetLineIntersectPt(const Point<T>& ln1a, const Point<T>& ln1b,
const Point<T>& ln2a, const Point<T>& ln2b, Point<T>& ip)
{
double ln1dy = static_cast<double>(ln1b.y - ln1a.y);
@@ -891,11 +944,14 @@ namespace Clipper2Lib {
ip.x = originx + static_cast<T>(hitx);
ip.y = originy + static_cast<T>(hity);
}
#ifdef USINGZ
ip.z = 0;
#endif
return true;
}
#else
template<typename T>
inline bool GetSegmentIntersectPt(const Point<T>& ln1a, const Point<T>& ln1b,
inline bool GetLineIntersectPt(const Point<T>& ln1a, const Point<T>& ln1b,
const Point<T>& ln2a, const Point<T>& ln2b, Point<T>& ip)
{
// https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
@@ -913,7 +969,10 @@ namespace Clipper2Lib {
{
ip.x = static_cast<T>(ln1a.x + t * dx1);
ip.y = static_cast<T>(ln1a.y + t * dy1);
}
#ifdef USINGZ
ip.z = 0;
#endif
}
return true;
}
#endif
@@ -940,30 +999,53 @@ namespace Clipper2Lib {
}
template<typename T>
inline int GetSign(const T& val)
{
if (!val) return 0;
inline int GetSign(const T& val)
{
if (!val) return 0;
return (val > 0) ? 1 : -1;
}
inline bool SegmentsIntersect(const Point64& seg1a, const Point64& seg1b,
const Point64& seg2a, const Point64& seg2b, bool inclusive = false)
{
double dy1 = static_cast<double>(seg1b.y - seg1a.y);
double dx1 = static_cast<double>(seg1b.x - seg1a.x);
double dy2 = static_cast<double>(seg2b.y - seg2a.y);
double dx2 = static_cast<double>(seg2b.x - seg2a.x);
double cp = dy1 * dx2 - dy2 * dx1;
if (cp == 0) return false; // ie parallel segments
if (inclusive)
{
double res1 = CrossProduct(seg1a, seg2a, seg2b);
double res2 = CrossProduct(seg1b, seg2a, seg2b);
if (res1 * res2 > 0) return false;
double res3 = CrossProduct(seg2a, seg1a, seg1b);
double res4 = CrossProduct(seg2b, seg1a, seg1b);
if (res3 * res4 > 0) return false;
return (res1 || res2 || res3 || res4); // ensures not collinear
//result **includes** segments that touch at an end point
double t = ((seg1a.x - seg2a.x) * dy2 - (seg1a.y - seg2a.y) * dx2);
if (t == 0) return true;
if (t > 0)
{
if (cp < 0 || t > cp) return false;
}
else if (cp > 0 || t < cp) return false; // false when t more neg. than cp
t = ((seg1a.x - seg2a.x) * dy1 - (seg1a.y - seg2a.y) * dx1);
if (t == 0) return true;
if (t > 0) return (cp > 0 && t <= cp);
else return (cp < 0 && t >= cp); // true when t less neg. than cp
}
else {
return (GetSign(CrossProduct(seg1a, seg2a, seg2b)) *
GetSign(CrossProduct(seg1b, seg2a, seg2b)) < 0) &&
(GetSign(CrossProduct(seg2a, seg1a, seg1b)) *
GetSign(CrossProduct(seg2b, seg1a, seg1b)) < 0);
else
{
//result **excludes** segments that touch at an end point
double t = ((seg1a.x - seg2a.x) * dy2 - (seg1a.y - seg2a.y) * dx2);
if (t == 0) return false;
if (t > 0)
{
if (cp < 0 || t >= cp) return false;
}
else if (cp > 0 || t <= cp ) return false; // false when t more neg. than cp
t = ((seg1a.x - seg2a.x) * dy1 - (seg1a.y - seg2a.y) * dx1);
if (t == 0) return false;
if (t > 0) return (cp > 0 && t < cp);
else return (cp < 0 && t > cp); // true when t less neg. than cp
}
}
@@ -1051,7 +1133,7 @@ namespace Clipper2Lib {
val = 1 - val; // toggle val
else
{
double d = CrossProduct(*prev, *curr, pt);
int d = CrossProductSign(*prev, *curr, pt);
if (d == 0) return PointInPolygonResult::IsOn;
if ((d < 0) == is_above) val = 1 - val;
}
@@ -1065,7 +1147,7 @@ namespace Clipper2Lib {
if (curr == cend) curr = cbegin;
if (curr == cbegin) prev = cend - 1;
else prev = curr - 1;
double d = CrossProduct(*prev, *curr, pt);
int d = CrossProductSign(*prev, *curr, pt);
if (d == 0) return PointInPolygonResult::IsOn;
if ((d < 0) == is_above) val = 1 - val;
}
@@ -15,6 +15,13 @@
#include <functional>
#include <memory>
// Orca: engine nodes are allocated through tbbmalloc, see clipper.engine.cpp.
#define CLIPPER2_NODE_ALLOCATOR \
static void* operator new(size_t size); \
static void operator delete(void* ptr) noexcept; \
static void* operator new[](size_t size); \
static void operator delete[](void* ptr) noexcept;
#ifdef USINGZ
namespace Clipper2Lib_Z {
#else
@@ -50,6 +57,7 @@ namespace Clipper2Lib {
}
struct Vertex {
CLIPPER2_NODE_ALLOCATOR
Point64 pt;
Vertex* next = nullptr;
Vertex* prev = nullptr;
@@ -57,6 +65,7 @@ namespace Clipper2Lib {
};
struct OutPt {
CLIPPER2_NODE_ALLOCATOR
Point64 pt;
OutPt* next = nullptr;
OutPt* prev = nullptr;
@@ -81,6 +90,7 @@ namespace Clipper2Lib {
//OutRec: contains a path in the clipping solution. Edges in the AEL will
//have OutRec pointers assigned when they form part of the clipping solution.
struct OutRec {
CLIPPER2_NODE_ALLOCATOR
size_t idx = 0;
OutRec* owner = nullptr;
Active* front_edge = nullptr;
@@ -106,6 +116,7 @@ namespace Clipper2Lib {
///////////////////////////////////////////////////////////////////
struct Active {
CLIPPER2_NODE_ALLOCATOR
Point64 bot;
Point64 top;
int64_t curr_x = 0; //current (updated at every new scanline)
@@ -133,6 +144,7 @@ namespace Clipper2Lib {
};
struct LocalMinima {
CLIPPER2_NODE_ALLOCATOR
Vertex* vertex;
PathType polytype;
bool is_open;
@@ -303,6 +315,7 @@ namespace Clipper2Lib {
protected:
PolyPath* parent_;
public:
CLIPPER2_NODE_ALLOCATOR
PolyPath(PolyPath* parent = nullptr): parent_(parent){}
virtual ~PolyPath() {};
//https://en.cppreference.com/w/cpp/language/rule_of_three
@@ -330,15 +343,16 @@ namespace Clipper2Lib {
//Even levels except level 0
return lvl && !(lvl & 1);
}
template<typename T>
static double Clipper2LibArea(const Path<T> &poly)
{
// Area() of the namespace this header is compiled into (Clipper2Lib or Clipper2Lib_Z).
template<typename T>
static double Clipper2LibArea(const Path<T> &poly)
{
#ifdef USINGZ
return Clipper2Lib_Z::Area<T>(poly);
return Clipper2Lib_Z::Area<T>(poly);
#else
return Clipper2Lib::Area<T>(poly);
return Clipper2Lib::Area<T>(poly);
#endif
}
}
};
typedef typename std::vector<std::unique_ptr<PolyPath64>> PolyPath64List;
@@ -388,7 +402,8 @@ namespace Clipper2Lib {
double Area() const
{
return std::accumulate(childs_.cbegin(), childs_.cend(), Clipper2LibArea<int64_t>(polygon_),
return std::accumulate(childs_.cbegin(), childs_.cend(),
Clipper2LibArea<int64_t>(polygon_),
[](double a, const auto& child) {return a + child->Area(); });
}
@@ -462,7 +477,8 @@ namespace Clipper2Lib {
double Area() const
{
return std::accumulate(childs_.begin(), childs_.end(), Clipper2LibArea<double>(polygon_),
return std::accumulate(childs_.begin(), childs_.end(),
Clipper2LibArea<double>(polygon_),
[](double a, const auto& child) {return a + child->Area(); });
}
};
@@ -19,17 +19,17 @@
The path structures used extensively in other parts of this library are all
based on std::vector classes. Since C++ classes can't be accessed by other
languages, these paths are exported here as very simple array structures
(either of int64_t or double) that can be parsed by just about any
languages, these paths are exported here as very simple array structures
(either of int64_t or double) that can be parsed by just about any
programming language.
These 2D paths are defined by series of x and y coordinates together with an
optional user-defined 'z' value (see Z-values below). Hence, a vertex refers
to a single x and y coordinate (+/- a user-defined value). Data structures
have names with suffixes that indicate the array type (either int64_t or
double). For example, the data structure CPath64 contains an array of int64_t
values, whereas the data structure CPathD contains an array of double.
Where documentation omits the type suffix (eg CPath), it is referring to an
to a single x and y coordinate (+/- a user-defined value). Data structures
have names with suffixes that indicate the array type (either int64_t or
double). For example, the data structure CPath64 contains an array of int64_t
values, whereas the data structure CPathD contains an array of double.
Where documentation omits the type suffix (eg CPath), it is referring to an
array whose data type could be either int64_t or double.
For conciseness, the following letters are used in the diagrams below:
@@ -39,10 +39,10 @@ A: Number of elements in an array
CPath64 and CPathD:
These are arrays of either int64_t or double values. Apart from
the first two elements, these arrays are a series of vertices
that together define a path. The very first element contains the
number of vertices (N) in the path, while second element should
These are arrays of either int64_t or double values. Apart from
the first two elements, these arrays are a series of vertices
that together define a path. The very first element contains the
number of vertices (N) in the path, while second element should
contain a 0 value.
_______________________________________________________________
| counters | vertex1 | vertex2 | ... | vertexN |
@@ -52,9 +52,9 @@ _______________________________________________________________
CPaths64 and CPathsD:
These are also arrays of either int64_t or double values that
contain any number of consecutive CPath structures. However,
contain any number of consecutive CPath structures. However,
preceding the first path is a pair of values. The first value
contains the length of the entire array structure (A), and the
contains the length of the entire array structure (A), and the
second contains the number (ie count) of contained paths (C).
Memory allocation for CPaths64 = A * sizeof(int64_t)
Memory allocation for CPathsD = A * sizeof(double)
@@ -65,12 +65,12 @@ __________________________________________
CPolytree64 and CPolytreeD:
The entire polytree structure is an array of int64_t or double. The
first element in the array indicates the array's total length (A).
The second element indicates the number (C) of CPolyPath structures
The entire polytree structure is an array of int64_t or double. The
first element in the array indicates the array's total length (A).
The second element indicates the number (C) of CPolyPath structures
that are the TOP LEVEL CPolyPath in the polytree, and these top
level CPolyPath immediately follow these first two array elements.
These top level CPolyPath structures may, in turn, contain nested
level CPolyPath immediately follow these first two array elements.
These top level CPolyPath structures may, in turn, contain nested
CPolyPath children, and these collectively make a tree structure.
_________________________________________________________
| counters | CPolyPath1 | CPolyPath2 | ... | CPolyPathC |
@@ -116,13 +116,10 @@ the four vertices that define the two segments that are intersecting.
#include "clipper2/clipper.engine.h"
#include "clipper2/clipper.offset.h"
#include "clipper2/clipper.rectclip.h"
#include "clipper2/clipper.triangulation.h"
#include <cstdlib>
#ifdef USINGZ
namespace Clipper2Lib_Z {
#else
namespace Clipper2Lib {
#endif
typedef int64_t* CPath64;
typedef int64_t* CPaths64;
@@ -254,9 +251,9 @@ ZCallback64 dllCallback64 = nullptr;
ZCallbackD dllCallbackD = nullptr;
constexpr int EXPORT_VERTEX_DIMENSIONALITY = 3;
#else
#else
constexpr int EXPORT_VERTEX_DIMENSIONALITY = 2;
#endif
#endif
template <typename T>
static void GetPathCountAndCPathsArrayLen(const Paths<T>& paths,
@@ -396,7 +393,7 @@ static Path<T> ConvertCPathToPathT(T* path)
#ifdef USINGZ
z_type z = Reinterpret<z_type>(*v++);
result.emplace_back(x, y, z);
#else
#else
result.emplace_back(x, y);
#endif
}
@@ -414,7 +411,7 @@ static Paths<T> ConvertCPathsToPathsT(T* paths)
for (size_t i = 0; i < cnt; ++i)
{
size_t cnt2 = static_cast<size_t>(*v);
v += 2;
v += 2;
Path<T> path;
path.reserve(cnt2);
for (size_t j = 0; j < cnt2; ++j)
@@ -447,7 +444,7 @@ static Path64 ConvertCPathDToPath64WithScale(const CPathD path, double scale)
#ifdef USINGZ
z_type z = Reinterpret<z_type>(*v++);
result.emplace_back(x, y, z);
#else
#else
result.emplace_back(x, y);
#endif
}
@@ -492,7 +489,7 @@ static void CreateCPolyPath64(const PolyPath64* pp, int64_t*& v)
{
*v++ = pt.x;
*v++ = pt.y;
#ifdef USINGZ
#ifdef USINGZ
* v++ = Reinterpret<int64_t>(pt.z); // raw memory copy
#endif
}
@@ -508,7 +505,7 @@ static void CreateCPolyPathD(const PolyPathD* pp, double*& v)
{
*v++ = pt.x;
*v++ = pt.y;
#ifdef USINGZ
#ifdef USINGZ
* v++ = Reinterpret<double>(pt.z); // raw memory copy
#endif
}
@@ -816,6 +813,24 @@ EXTERN_DLL_EXPORT CPaths64 MinkowskiDiff64(const CPath64& cpattern, const CPath6
return CreateCPathsFromPathsT(solution);
}
EXTERN_DLL_EXPORT CPaths64 Triangulate64(const CPaths64 paths, bool use_delaunay)
{
Paths64 pp = ConvertCPathsToPathsT(paths);
Paths64 sol;
if (Triangulate(pp, sol, use_delaunay) != TriangulateResult::success) return nullptr;
return CreateCPathsFromPathsT(sol);
}
EXTERN_DLL_EXPORT CPathsD TriangulateD(const CPathsD paths, int decimal_precison, bool use_delaunay)
{
if (decimal_precison < -8 || decimal_precison > 8) return nullptr;
const double scale = std::pow(10, decimal_precison);
Paths64 pp = ConvertCPathsDToPaths64(paths, scale);
Paths64 sol;
if (Triangulate(pp, sol, use_delaunay) != TriangulateResult::success) return nullptr;
return CreateCPathsDFromPaths64(sol, 1 / scale);
}
#ifdef USINGZ
typedef void (*DLLZCallback64)(const Point64& e1bot, const Point64& e1top, const Point64& e2bot, const Point64& e2top, Point64& pt);
typedef void (*DLLZCallbackD)(const PointD& e1bot, const PointD& e1top, const PointD& e2bot, const PointD& e2top, PointD& pt);
@@ -1,8 +1,8 @@
/*******************************************************************************
* Author : Angus Johnson *
* Date : 27 April 2024 *
* Date : 5 March 2025 *
* Website : https://www.angusj.com *
* Copyright : Angus Johnson 2010-2024 *
* Copyright : Angus Johnson 2010-2025 *
* Purpose : This module provides a simple interface to the Clipper Library *
* License : https://www.boost.org/LICENSE_1_0.txt *
*******************************************************************************/
@@ -13,14 +13,15 @@
#include "clipper2/clipper.core.h"
#include "clipper2/clipper.engine.h"
#include "clipper2/clipper.offset.h"
#include "clipper2/clipper.minkowski.h"
#include "clipper2/clipper.rectclip.h"
#include "clipper2/clipper.minkowski.h"
#include "clipper2/clipper.triangulation.h"
#include <type_traits>
#ifdef USINGZ
namespace Clipper2Lib_Z {
namespace Clipper2Lib_Z {
#else
namespace Clipper2Lib {
namespace Clipper2Lib {
#endif
inline Paths64 BooleanOp(ClipType cliptype, FillRule fillrule,
@@ -154,14 +155,14 @@
if (!delta) return paths;
if (error_code) return PathsD();
const double scale = std::pow(10, precision);
ClipperOffset clip_offset(miter_limit, arc_tolerance);
ClipperOffset clip_offset(miter_limit, arc_tolerance * scale);
clip_offset.AddPaths(ScalePaths<int64_t,double>(paths, scale, error_code), jt, et);
if (error_code) return PathsD();
Paths64 solution;
clip_offset.Execute(delta * scale, solution);
return ScalePaths<double, int64_t>(solution, 1 / scale, error_code);
}
template <typename T>
inline Path<T> TranslatePath(const Path<T>& path, T dx, T dy)
{
@@ -355,6 +356,29 @@
#endif
}
inline size_t GetNext(size_t current, size_t high,
const std::vector<bool>& flags)
{
++current;
while (current <= high && flags[current]) ++current;
if (current <= high) return current;
current = 0;
while (flags[current]) ++current;
return current;
}
inline size_t GetPrior(size_t current, size_t high,
const std::vector<bool>& flags)
{
if (current == 0) current = high;
else --current;
while (current > 0 && flags[current]) --current;
if (!flags[current]) return current;
current = high;
while (flags[current]) --current;
return current;
}
} // end details namespace
inline std::ostream& operator<< (std::ostream& os, const PolyTree64& pp)
@@ -615,29 +639,6 @@
return result;
}
inline size_t GetNext(size_t current, size_t high,
const std::vector<bool>& flags)
{
++current;
while (current <= high && flags[current]) ++current;
if (current <= high) return current;
current = 0;
while (flags[current]) ++current;
return current;
}
inline size_t GetPrior(size_t current, size_t high,
const std::vector<bool>& flags)
{
if (current == 0) current = high;
else --current;
while (current > 0 && flags[current]) --current;
if (!flags[current]) return current;
current = high;
while (flags[current]) --current;
return current;
}
template <typename T>
inline Path<T> SimplifyPath(const Path<T> &path,
double epsilon, bool isClosedPath = true)
@@ -669,13 +670,13 @@
start = curr;
do
{
curr = GetNext(curr, high, flags);
curr = details::GetNext(curr, high, flags);
} while (curr != start && distSqr[curr] > epsSqr);
if (curr == start) break;
}
prior = GetPrior(curr, high, flags);
next = GetNext(curr, high, flags);
prior = details::GetPrior(curr, high, flags);
next = details::GetNext(curr, high, flags);
if (next == prior) break;
// flag for removal the smaller of adjacent 'distances'
@@ -684,14 +685,14 @@
prior2 = prior;
prior = curr;
curr = next;
next = GetNext(next, high, flags);
next = details::GetNext(next, high, flags);
}
else
prior2 = GetPrior(prior, high, flags);
prior2 = details::GetPrior(prior, high, flags);
flags[curr] = true;
curr = next;
next = GetNext(next, high, flags);
next = details::GetNext(next, high, flags);
if (isClosedPath || ((curr != high) && (curr != 0)))
distSqr[curr] = PerpendicDistFromLineSqrd(path[curr], path[prior], path[next]);
@@ -716,6 +717,35 @@
return result;
}
template <typename T>
inline bool Path2ContainsPath1(const Path<T>& path1, const Path<T>& path2)
{
// precondition: paths must not intersect, except for
// transient (and presumed 'micro') path intersections
PointInPolygonResult pip = PointInPolygonResult::IsOn;
for (const Point<T>& pt : path1)
{
switch (PointInPolygon(pt, path2))
{
case PointInPolygonResult::IsOutside:
if (pip == PointInPolygonResult::IsOutside) return false;
pip = PointInPolygonResult::IsOutside;
break;
case PointInPolygonResult::IsInside:
if (pip == PointInPolygonResult::IsInside) return true;
pip = PointInPolygonResult::IsInside;
break;
default:
break;
}
}
if (pip != PointInPolygonResult::IsInside) return false;
// result is likely true but check midpoint
Point<T> mp1 = GetBounds(path1).MidPoint();
return PointInPolygon(mp1, path2) == PointInPolygonResult::IsInside;
}
template <typename T>
inline void RDP(const Path<T> path, std::size_t begin,
std::size_t end, double epsSqrd, std::vector<bool>& flags)
@@ -39,7 +39,7 @@ private:
class Group {
public:
Paths64 paths_in;
std::optional<size_t> lowest_path_idx{};
std::optional<size_t> lowest_path_idx{};
bool is_reversed = false;
JoinType join_type;
EndType end_type;
@@ -100,7 +100,7 @@ public:
void AddPath(const Path64& path, JoinType jt_, EndType et_);
void AddPaths(const Paths64& paths, JoinType jt_, EndType et_);
void Clear() { groups_.clear(); norms.clear(); };
void Execute(double delta, Paths64& sols_64);
void Execute(double delta, PolyTree64& polytree);
void Execute(DeltaCallback64 delta_cb, Paths64& paths);
@@ -114,7 +114,7 @@ public:
bool PreserveCollinear() const { return preserve_collinear_; }
void PreserveCollinear(bool preserve_collinear){preserve_collinear_ = preserve_collinear;}
bool ReverseSolution() const { return reverse_solution_; }
void ReverseSolution(bool reverse_solution) {reverse_solution_ = reverse_solution;}
@@ -0,0 +1,30 @@
/*******************************************************************************
* Author : Angus Johnson *
* Date : 6 December 2025 *
* Release : BETA RELEASE *
* Website : https://www.angusj.com *
* Copyright : Angus Johnson 2010-2025 *
* Purpose : Delaunay Triangulation *
* License : https://www.boost.org/LICENSE_1_0.txt *
*******************************************************************************/
#ifndef CLIPPER_TRIANGULATION_H
#define CLIPPER_TRIANGULATION_H
#include <stack>
#include "clipper2/clipper.core.h"
#ifdef USINGZ
namespace Clipper2Lib_Z {
#else
namespace Clipper2Lib {
#endif
enum class TriangulateResult { success, fail, no_polygons, paths_intersect };
// Triangulate - this function will not accept intesecting paths
TriangulateResult Triangulate(const Paths64& pp, Paths64& solution, bool useDelaunay = true);
TriangulateResult Triangulate(const PathsD& pp, int decPlaces, PathsD& solution, bool useDelaunay = true);
} // Clipper2Lib namespace
#endif // CLIPPER_TRIANGULATION_H
@@ -1,6 +1,6 @@
#ifndef CLIPPER_VERSION_H
#define CLIPPER_VERSION_H
constexpr auto CLIPPER2_VERSION = "1.5.2";
constexpr auto CLIPPER2_VERSION = "2.0.1";
#endif // CLIPPER_VERSION_H
@@ -1,8 +1,8 @@
/*******************************************************************************
* Author : Angus Johnson *
* Date : 17 September 2024 *
* Date : 5 November 2025 *
* Website : https://www.angusj.com *
* Copyright : Angus Johnson 2010-2024 *
* Copyright : Angus Johnson 2010-2025 *
* Purpose : This is the main polygon clipping module *
* License : https://www.boost.org/LICENSE_1_0.txt *
*******************************************************************************/
@@ -10,6 +10,8 @@
#include "clipper2/clipper.engine.h"
#include "clipper2/clipper.h"
#include <stdexcept>
#include <new>
#include <oneapi/tbb/scalable_allocator.h>
// https://github.com/AngusJohnson/Clipper2/discussions/334
// #discussioncomment-4248602
@@ -27,10 +29,30 @@ namespace Clipper2Lib_Z {
namespace Clipper2Lib {
#endif
// Orca: tbbmalloc scales far better than the default heap when all slicing threads clip at once.
static void* NodeAlloc(size_t size)
{
if (void* p = scalable_malloc(size)) return p;
throw std::bad_alloc();
}
#define CLIPPER2_DEFINE_NODE_ALLOCATOR(T) \
void* T::operator new(size_t size) { return NodeAlloc(size); } \
void T::operator delete(void* ptr) noexcept { scalable_free(ptr); } \
void* T::operator new[](size_t size) { return NodeAlloc(size); } \
void T::operator delete[](void* ptr) noexcept { scalable_free(ptr); }
CLIPPER2_DEFINE_NODE_ALLOCATOR(Vertex)
CLIPPER2_DEFINE_NODE_ALLOCATOR(OutPt)
CLIPPER2_DEFINE_NODE_ALLOCATOR(OutRec)
CLIPPER2_DEFINE_NODE_ALLOCATOR(Active)
CLIPPER2_DEFINE_NODE_ALLOCATOR(LocalMinima)
CLIPPER2_DEFINE_NODE_ALLOCATOR(PolyPath)
#undef CLIPPER2_DEFINE_NODE_ALLOCATOR
static const Rect64 invalid_rect = Rect64(false);
// Every closed path (ie polygon) is made up of a series of vertices forming edge
// 'bounds' that alternate between ascending bounds (containing edges going up
// Every closed path (ie polygon) is made up of a series of vertices forming edge
// 'bounds' that alternate between ascending bounds (containing edges going up
// relative to the Y-axis) and descending bounds. 'Local Minima' refers to
// vertices where ascending and descending bounds join at the bottom, and
// 'Local Maxima' are where ascending and descending bounds join at the top.
@@ -482,8 +504,7 @@ namespace Clipper2Lib {
inline void SetOwner(OutRec* outrec, OutRec* new_owner)
{
//precondition1: new_owner is never null
while (new_owner->owner && !new_owner->owner->pts)
new_owner->owner = new_owner->owner->owner;
new_owner->owner = GetRealOutRec(new_owner->owner);
OutRec* tmp = new_owner;
while (tmp && tmp != outrec) tmp = tmp->owner;
if (tmp) new_owner->owner = outrec->owner;
@@ -536,9 +557,9 @@ namespace Clipper2Lib {
val = 1 - val; // toggle val
else
{
double d = CrossProduct(op2->prev->pt, op2->pt, pt);
if (d == 0) return PointInPolygonResult::IsOn;
if ((d < 0) == is_above) val = 1 - val;
int i = CrossProductSign(op2->prev->pt, op2->pt, pt);
if (i == 0) return PointInPolygonResult::IsOn;
if ((i < 0) == is_above) val = 1 - val;
}
is_above = !is_above;
op2 = op2->next;
@@ -546,9 +567,9 @@ namespace Clipper2Lib {
if (is_above != starting_above)
{
double d = CrossProduct(op2->prev->pt, op2->pt, pt);
if (d == 0) return PointInPolygonResult::IsOn;
if ((d < 0) == is_above) val = 1 - val;
int i = CrossProductSign(op2->prev->pt, op2->pt, pt);
if (i == 0) return PointInPolygonResult::IsOn;
if ((i < 0) == is_above) val = 1 - val;
}
if (val == 0) return PointInPolygonResult::IsOutside;
@@ -578,30 +599,31 @@ namespace Clipper2Lib {
return result;
}
inline bool Path1InsidePath2(OutPt* op1, OutPt* op2)
inline bool Path2ContainsPath1(OutPt* op1, OutPt* op2)
{
// we need to make some accommodation for rounding errors
// so we won't jump if the first vertex is found outside
PointInPolygonResult result;
int outside_cnt = 0;
// this function accommodates rounding errors that
// can cause path micro intersections
PointInPolygonResult pip = PointInPolygonResult::IsOn;
OutPt* op = op1;
do
{
result = PointInOpPolygon(op->pt, op2);
if (result == PointInPolygonResult::IsOutside) ++outside_cnt;
else if (result == PointInPolygonResult::IsInside) --outside_cnt;
do {
switch (PointInOpPolygon(op->pt, op2))
{
case PointInPolygonResult::IsOutside:
if (pip == PointInPolygonResult::IsOutside) return false;
pip = PointInPolygonResult::IsOutside;
break;
case PointInPolygonResult::IsInside:
if (pip == PointInPolygonResult::IsInside) return true;
pip = PointInPolygonResult::IsInside;
break;
default: break;
}
op = op->next;
} while (op != op1 && std::abs(outside_cnt) < 2);
if (std::abs(outside_cnt) > 1) return (outside_cnt < 0);
// since path1's location is still equivocal, check its midpoint
Point64 mp = GetBounds(GetCleanPath(op1)).MidPoint();
Path64 path2 = GetCleanPath(op2);
return PointInPolygon(mp, path2) != PointInPolygonResult::IsOutside;
} while (op != op1);
// result unclear, so try again using cleaned paths
return Path2ContainsPath1(GetCleanPath(op1), GetCleanPath(op2)); // (#973)
}
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
void AddLocMin(LocalMinimaList& list,
Vertex& vert, PathType polytype, bool is_open)
{
@@ -1126,21 +1148,19 @@ namespace Clipper2Lib {
return newcomer.curr_x > resident.curr_x;
//get the turning direction a1.top, a2.bot, a2.top
double d = CrossProduct(resident.top, newcomer.bot, newcomer.top);
if (d != 0) return d < 0;
int i = CrossProductSign(resident.top, newcomer.bot, newcomer.top);
if (i != 0) return i < 0;
//edges must be collinear to get here
//for starting open paths, place them according to
//the direction they're about to turn
if (!IsMaxima(resident) && (resident.top.y > newcomer.top.y))
{
return CrossProduct(newcomer.bot,
resident.top, NextVertex(resident)->pt) <= 0;
return (CrossProductSign(newcomer.bot, resident.top, NextVertex(resident)->pt) <= 0);
}
else if (!IsMaxima(newcomer) && (newcomer.top.y > resident.top.y))
{
return CrossProduct(newcomer.bot,
newcomer.top, NextVertex(newcomer)->pt) >= 0;
return (CrossProductSign(newcomer.bot, newcomer.top, NextVertex(newcomer)->pt) >= 0);
}
int64_t y = newcomer.bot.y;
@@ -1155,7 +1175,7 @@ namespace Clipper2Lib {
resident.bot, resident.top)) return true;
else
//compare turning direction of the alternate bound
return (CrossProduct(PrevPrevVertex(resident)->pt,
return (CrossProductSign(PrevPrevVertex(resident)->pt,
newcomer.bot, PrevPrevVertex(newcomer)->pt) > 0) == newcomerIsLeft;
}
@@ -1565,7 +1585,7 @@ namespace Clipper2Lib {
FixSelfIntersects(outrec);
}
void ClipperBase::DoSplitOp(OutRec* outrec, OutPt* splitOp)
void ClipperBase::DoSplitOp (OutRec* outrec, OutPt* splitOp)
{
// splitOp.prev -> splitOp &&
// splitOp.next -> splitOp.next.next are intersecting
@@ -1574,7 +1594,7 @@ namespace Clipper2Lib {
outrec->pts = prevOp;
Point64 ip;
GetSegmentIntersectPt(prevOp->pt, splitOp->pt,
GetLineIntersectPt(prevOp->pt, splitOp->pt,
splitOp->next->pt, nextNextOp->pt, ip);
#ifdef USINGZ
@@ -1630,7 +1650,7 @@ namespace Clipper2Lib {
if (using_polytree_)
{
if (Path1InsidePath2(prevOp, newOp))
if (Path2ContainsPath1(prevOp, newOp))
{
newOr->splits = new OutRecList();
newOr->splits->emplace_back(outrec);
@@ -1652,19 +1672,32 @@ namespace Clipper2Lib {
void ClipperBase::FixSelfIntersects(OutRec* outrec)
{
OutPt* op2 = outrec->pts;
if (op2->prev == op2->next->next)
return; // because triangles can't self-intersect
for (; ; )
{
// triangles can't self-intersect
if (op2->prev == op2->next->next) break;
if (SegmentsIntersect(op2->prev->pt,
op2->pt, op2->next->pt, op2->next->next->pt))
{
if (op2 == outrec->pts || op2->next == outrec->pts)
outrec->pts = outrec->pts->prev;
DoSplitOp(outrec, op2);
if (!outrec->pts) break;
op2 = outrec->pts;
continue;
if (SegmentsIntersect(op2->prev->pt,
op2->pt, op2->next->next->pt, op2->next->next->next->pt))
{
// adjacent intersections (ie a micro self-intersections)
op2 = DuplicateOp(op2, false);
op2->pt = op2->next->next->next->pt;
op2 = op2->next;
}
else
{
if (op2 == outrec->pts || op2->next == outrec->pts)
outrec->pts = outrec->pts->prev;
DoSplitOp(outrec, op2);
if (!outrec->pts) break;
op2 = outrec->pts;
if (op2->prev == op2->next->next)
break; // again, because triangles can't self-intersect
continue;
}
}
else
op2 = op2->next;
@@ -1805,14 +1838,14 @@ namespace Clipper2Lib {
switch (fillrule_)
{
case FillRule::Positive:
if (edge_c->wind_cnt != 1) return;
case FillRule::Positive:
if (edge_c->wind_cnt != 1) return;
break;
case FillRule::Negative:
if (edge_c->wind_cnt != -1) return;
case FillRule::Negative:
if (edge_c->wind_cnt != -1) return;
break;
default:
if (std::abs(edge_c->wind_cnt) != 1) return;
default:
if (std::abs(edge_c->wind_cnt) != 1) return;
}
#ifdef USINGZ
@@ -1933,7 +1966,7 @@ namespace Clipper2Lib {
const bool e1_windcnt_in_01 = old_e1_windcnt == 0 || old_e1_windcnt == 1;
const bool e2_windcnt_in_01 = old_e2_windcnt == 0 || old_e2_windcnt == 1;
if ((!IsHotEdge(e1) && !e1_windcnt_in_01) ||
if ((!IsHotEdge(e1) && !e1_windcnt_in_01) ||
(!IsHotEdge(e2) && !e2_windcnt_in_01))
return;
@@ -2112,10 +2145,9 @@ namespace Clipper2Lib {
e->prev_in_sel = e->prev_in_ael;
e->next_in_sel = e->next_in_ael;
e->jump = e->next_in_sel;
if (e->join_with == JoinWith::Left)
e->curr_x = e->prev_in_ael->curr_x; // also avoids complications
else
e->curr_x = TopX(*e, top_y);
// it is safe to ignore 'joined' edges here because
// if necessary they will be split in IntersectEdges()
e->curr_x = TopX(*e, top_y);
e = e->next_in_ael;
}
}
@@ -2262,15 +2294,14 @@ namespace Clipper2Lib {
void MoveSplits(OutRec* fromOr, OutRec* toOr)
{
if (!fromOr->splits) return;
if (!toOr->splits) toOr->splits = new OutRecList();
OutRecList::iterator orIter = fromOr->splits->begin();
for (; orIter != fromOr->splits->end(); ++orIter)
toOr->splits->emplace_back(*orIter);
if (toOr != *orIter) // #987
toOr->splits->emplace_back(*orIter);
fromOr->splits->clear();
}
void ClipperBase::ProcessHorzJoins()
{
for (const HorzJoin& j : horz_join_list_)
@@ -2299,8 +2330,8 @@ namespace Clipper2Lib {
}
if (using_polytree_) //#498, #520, #584, D#576, #618
{
if (Path1InsidePath2(or1->pts, or2->pts))
{
if (Path2ContainsPath1(or1->pts, or2->pts))
{
//swap or1's & or2's pts
OutPt* tmp = or1->pts;
@@ -2311,7 +2342,7 @@ namespace Clipper2Lib {
//or2 is now inside or1
or2->owner = or1;
}
else if (Path1InsidePath2(or2->pts, or1->pts))
else if (Path2ContainsPath1(or2->pts, or1->pts))
{
or2->owner = or1;
}
@@ -2324,13 +2355,14 @@ namespace Clipper2Lib {
else
or2->owner = or1;
}
else
else // joining, not splitting
{
or2->pts = nullptr;
if (using_polytree_)
{
SetOwner(or2, or1);
MoveSplits(or2, or1); //#618
if (or2->splits)
MoveSplits(or2, or1); //#618
}
else
or2->owner = or1;
@@ -2350,7 +2382,7 @@ namespace Clipper2Lib {
void ClipperBase::AddNewIntersectNode(Active& e1, Active& e2, int64_t top_y)
{
Point64 ip;
if (!GetSegmentIntersectPt(e1.bot, e1.top, e2.bot, e2.top, ip))
if (!GetLineIntersectPt(e1.bot, e1.top, e2.bot, e2.top, ip))
ip = Point64(e1.curr_x, top_y); //parallel edges
//rounding errors can occasionally place the calculated intersection
@@ -2934,22 +2966,28 @@ namespace Clipper2Lib {
bool ClipperBase::CheckSplitOwner(OutRec* outrec, OutRecList* splits)
{
for (auto split : *splits)
// nb: use indexing (not an iterator) in case 'splits' is modified inside this loop (#1029)
for (size_t idx = 0; idx < splits->size(); ++idx)
{
OutRec* split = (*splits)[idx];
if (!split->pts && split->splits &&
CheckSplitOwner(outrec, split->splits)) return true; //#942
split = GetRealOutRec(split);
if(!split || split == outrec || split->recursive_split == outrec) continue;
if (!split || split == outrec || split->recursive_split == outrec) continue;
split->recursive_split = outrec; // prevent infinite loops
if (split->splits && CheckSplitOwner(outrec, split->splits))
return true;
else if (CheckBounds(split) &&
IsValidOwner(outrec, split) &&
split->bounds.Contains(outrec->bounds) &&
Path1InsidePath2(outrec->pts, split->pts))
{
outrec->owner = split; //found in split
return true;
}
return true;
if (!CheckBounds(split) || !split->bounds.Contains(outrec->bounds) ||
!Path2ContainsPath1(outrec->pts, split->pts)) continue;
if (!IsValidOwner(outrec, split)) // split is owned by outrec! (#957)
split->owner = outrec->owner;
outrec->owner = split;
return true;
}
return false;
}
@@ -2960,13 +2998,12 @@ namespace Clipper2Lib {
// post-condition: if a valid path, outrec will have a polypath
if (outrec->polypath || outrec->bounds.IsEmpty()) return;
while (outrec->owner)
{
if (outrec->owner->splits && CheckSplitOwner(outrec, outrec->owner->splits)) break;
if (outrec->owner->pts && CheckBounds(outrec->owner) &&
outrec->owner->bounds.Contains(outrec->bounds) &&
Path1InsidePath2(outrec->pts, outrec->owner->pts)) break;
Path2ContainsPath1(outrec->pts, outrec->owner->pts)) break;
outrec->owner = outrec->owner->owner;
}
@@ -3029,6 +3066,7 @@ namespace Clipper2Lib {
{
OutRec* outrec = outrec_list_[i];
if (!outrec || !outrec->pts) continue;
if (outrec->is_open)
{
Path64 path;
@@ -1,6 +1,6 @@
/*******************************************************************************
* Author : Angus Johnson *
* Date : 22 January 2025 *
* Date : 11 October 2025 *
* Website : https://www.angusj.com *
* Copyright : Angus Johnson 2010-2025 *
* Purpose : Path Offset (Inflate/Shrink) *
@@ -37,29 +37,35 @@ const double arc_const = 0.002; // <-- 1/500
// Miscellaneous methods
//------------------------------------------------------------------------------
std::optional<size_t> GetLowestClosedPathIdx(const Paths64& paths)
void GetLowestClosedPathInfo(const Paths64& paths, std::optional<size_t>& idx, bool& is_neg_area)
{
std::optional<size_t> result;
idx.reset();
Point64 botPt = Point64(INT64_MAX, INT64_MIN);
for (size_t i = 0; i < paths.size(); ++i)
{
double a = MAX_DBL;
for (const Point64& pt : paths[i])
{
if ((pt.y < botPt.y) ||
((pt.y == botPt.y) && (pt.x >= botPt.x))) continue;
result = i;
if (a == MAX_DBL)
{
a = Area(paths[i]);
if (a == 0) break; // invalid closed path, so break from inner loop
is_neg_area = a < 0;
}
idx = i;
botPt.x = pt.x;
botPt.y = pt.y;
}
}
return result;
}
inline double Hypot(double x, double y)
{
// given that this is an internal function, and given the x and y parameters
// will always be coordinate values (or the difference between coordinate values),
// x and y should always be within INT64_MIN to INT64_MAX. Consequently,
// x and y should always be within INT64_MIN to INT64_MAX. Consequently,
// there should be no risk that the following computation will overflow
// see https://stackoverflow.com/a/32436148/359538
return std::sqrt(x * x + y * y);
@@ -145,15 +151,16 @@ ClipperOffset::Group::Group(const Paths64& _paths, JoinType _join_type, EndType
if (end_type == EndType::Polygon)
{
lowest_path_idx = GetLowestClosedPathIdx(paths_in);
bool is_neg_area;
GetLowestClosedPathInfo(paths_in, lowest_path_idx, is_neg_area);
// the lowermost path must be an outer path, so if its orientation is negative,
// then flag the whole group is 'reversed' (will negate delta etc.)
// as this is much more efficient than reversing every path.
is_reversed = (lowest_path_idx.has_value()) && Area(paths_in[lowest_path_idx.value()]) < 0;
is_reversed = lowest_path_idx.has_value() && is_neg_area;
}
else
{
lowest_path_idx = std::nullopt;
lowest_path_idx.reset();
is_reversed = false;
}
}
@@ -236,7 +243,7 @@ void ClipperOffset::DoSquare(const Path64& path, size_t j, size_t k)
{
PointD pt4 = PointD(pt3.x + vec.x * group_delta_, pt3.y + vec.y * group_delta_);
PointD pt = ptQ;
GetSegmentIntersectPt(pt1, pt2, pt3, pt4, pt);
GetLineIntersectPt(pt1, pt2, pt3, pt4, pt);
//get the second intersect point through reflecion
path_out.emplace_back(ReflectPoint(pt, ptQ));
path_out.emplace_back(pt);
@@ -245,7 +252,7 @@ void ClipperOffset::DoSquare(const Path64& path, size_t j, size_t k)
{
PointD pt4 = GetPerpendicD(path[j], norms[k], group_delta_);
PointD pt = ptQ;
GetSegmentIntersectPt(pt1, pt2, pt3, pt4, pt);
GetLineIntersectPt(pt1, pt2, pt3, pt4, pt);
path_out.emplace_back(pt);
//get the second intersect point through reflecion
path_out.emplace_back(ReflectPoint(pt, ptQ));
@@ -291,7 +298,8 @@ void ClipperOffset::DoRound(const Path64& path, size_t j, size_t k, double angle
#else
path_out.emplace_back(pt.x + offsetVec.x, pt.y + offsetVec.y);
#endif
int steps = static_cast<int>(std::ceil(steps_per_rad_ * std::abs(angle))); // #448, #456
// Orca: round the step count like Clipper1 did, so round offsets keep their vertices.
int steps = std::max(static_cast<int>(std::round(steps_per_rad_ * std::abs(angle))), 1);
for (int i = 1; i < steps; ++i) // ie 1 less than steps
{
offsetVec = PointD(offsetVec.x * step_cos_ - step_sin_ * offsetVec.y,
@@ -333,9 +341,9 @@ void ClipperOffset::OffsetPoint(Group& group, const Path64& path, size_t j, size
if (cos_a > -0.999 && (sin_a * group_delta_ < 0)) // test for concavity first (#593)
{
// is concave
// by far the simplest way to construct concave joins, especially those joining very
// short segments, is to insert 3 points that produce negative regions. These regions
// will be removed later by the finishing union operation. This is also the best way
// by far the simplest way to construct concave joins, especially those joining very
// short segments, is to insert 3 points that produce negative regions. These regions
// will be removed later by the finishing union operation. This is also the best way
// to ensure that path reversals (ie over-shrunk paths) are removed.
#ifdef USINGZ
path_out.emplace_back(GetPerpendic(path[j], norms[k], group_delta_), path[j].z);
@@ -366,11 +374,31 @@ void ClipperOffset::OffsetPoint(Group& group, const Path64& path, size_t j, size
DoSquare(path, j, k);
}
// Orca: join concave corners at the crossing of both edge offsets where safe, 3-point loops make dense inward offsets slow.
static bool OffsetConcaveCrossing(const Path64& path, const PathD& norms, size_t j, size_t k, size_t next,
double delta, Path64& path_out)
{
const double sin_a = CrossProduct(norms[j], norms[k]);
const double cos_a = DotProduct(norms[j], norms[k]);
if (cos_a <= -0.999 || sin_a * delta >= 0) return false;
const double x = std::fabs(delta * sin_a) / (1 + cos_a);
if (4 * x * x > DistanceSqr(path[k], path[j]) || 4 * x * x > DistanceSqr(path[j], path[next])) return false;
const double q = delta / (1 + cos_a);
#ifdef USINGZ
path_out.emplace_back(path[j].x + (norms[k].x + norms[j].x) * q, path[j].y + (norms[k].y + norms[j].y) * q, path[j].z);
#else
path_out.emplace_back(path[j].x + (norms[k].x + norms[j].x) * q, path[j].y + (norms[k].y + norms[j].y) * q);
#endif
return true;
}
void ClipperOffset::OffsetPolygon(Group& group, const Path64& path)
{
path_out.clear();
for (Path64::size_type j = 0, k = path.size() - 1; j < path.size(); k = j, ++j)
OffsetPoint(group, path, j, k);
if (deltaCallback64_ || path[j] == path[k] ||
!OffsetConcaveCrossing(path, norms, j, k, j + 1 == path.size() ? 0 : j + 1, group_delta_, path_out))
OffsetPoint(group, path, j, k);
solution->emplace_back(path_out);
}
@@ -380,7 +408,7 @@ void ClipperOffset::OffsetOpenJoined(Group& group, const Path64& path)
Path64 reverse_path(path);
std::reverse(reverse_path.begin(), reverse_path.end());
//rebuild normals
//rebuild normals
std::reverse(norms.begin(), norms.end());
norms.emplace_back(norms[0]);
norms.erase(norms.begin());
@@ -601,10 +629,10 @@ void ClipperOffset::ExecuteInternal(double delta)
if (!solution->size()) return;
bool paths_reversed = CheckReverseOrientation();
bool paths_reversed = CheckReverseOrientation();
//clean up self-intersections ...
Clipper64 c;
c.PreserveCollinear(false);
c.PreserveCollinear(preserve_collinear_);
//the solution should retain the orientation of the input
c.ReverseSolution(reverse_solution_ != paths_reversed);
#ifdef USINGZ
@@ -1,8 +1,8 @@
/*******************************************************************************
* Author : Angus Johnson *
* Date : 5 July 2024 *
* Date : 11 October 2025 *
* Website : https://www.angusj.com *
* Copyright : Angus Johnson 2010-2024 *
* Copyright : Angus Johnson 2010-2025 *
* Purpose : FAST rectangular clipping *
* License : https://www.boost.org/LICENSE_1_0.txt *
*******************************************************************************/
@@ -77,8 +77,8 @@ namespace Clipper2Lib {
bool GetSegmentIntersection(const Point64& p1,
const Point64& p2, const Point64& p3, const Point64& p4, Point64& ip)
{
double res1 = CrossProduct(p1, p3, p4);
double res2 = CrossProduct(p2, p3, p4);
int res1 = CrossProductSign(p1, p3, p4);
int res2 = CrossProductSign(p2, p3, p4);
if (res1 == 0)
{
ip = p1;
@@ -97,8 +97,8 @@ namespace Clipper2Lib {
}
if ((res1 > 0) == (res2 > 0)) return false;
double res3 = CrossProduct(p3, p1, p2);
double res4 = CrossProduct(p4, p1, p2);
int res3 = CrossProductSign(p3, p1, p2);
int res4 = CrossProductSign(p4, p1, p2);
if (res3 == 0)
{
ip = p3;
@@ -116,7 +116,7 @@ namespace Clipper2Lib {
if ((res3 > 0) == (res4 > 0)) return false;
// segments must intersect to get here
return GetSegmentIntersectPt(p1, p2, p3, p4, ip);
return GetLineIntersectPt(p1, p2, p3, p4, ip);
}
inline bool GetIntersection(const Path64& rectPath,
@@ -227,7 +227,7 @@ namespace Clipper2Lib {
const Point64& prev_pt, const Point64& curr_pt, const Point64& rect_mp)
{
if (AreOpposites(prev, curr))
return CrossProduct(prev_pt, rect_mp, curr_pt) < 0;
return CrossProductSign(prev_pt, rect_mp, curr_pt) < 0;
else
return HeadingClockwise(prev, curr);
}
File diff suppressed because it is too large Load Diff
@@ -6,3 +6,4 @@
#include "clipper.engine.cpp"
#include "clipper.offset.cpp"
#include "clipper.rectclip.cpp"
#include "clipper.triangulation.cpp"
@@ -33,8 +33,8 @@
// Axis-aligned bounding box tree for tet tri intersection
#include <CGAL/AABB_tree.h>
#include <CGAL/AABB_traits.h>
#include <CGAL/AABB_triangle_primitive.h>
#include <CGAL/AABB_traits_3.h>
#include <CGAL/AABB_triangle_primitive_3.h>
// Boolean operations
#include <CGAL/Polyhedron_3.h>
@@ -41,9 +41,9 @@ IGL_INLINE void igl::copyleft::cgal::closest_facet(
const std::vector<std::vector<size_t> > & VF,
const std::vector<std::vector<size_t> > & VFi,
const CGAL::AABB_tree<
CGAL::AABB_traits<
CGAL::AABB_traits_3<
Kernel,
CGAL::AABB_triangle_primitive<
CGAL::AABB_triangle_primitive_3<
Kernel, typename std::vector<
typename Kernel::Triangle_3 >::iterator > > > & tree,
const std::vector<typename Kernel::Triangle_3 > & triangles,
@@ -56,8 +56,8 @@ IGL_INLINE void igl::copyleft::cgal::closest_facet(
typedef typename Kernel::Segment_3 Segment_3;
typedef typename Kernel::Triangle_3 Triangle;
typedef typename std::vector<Triangle>::iterator Iterator;
typedef typename CGAL::AABB_triangle_primitive<Kernel, Iterator> Primitive;
typedef typename CGAL::AABB_traits<Kernel, Primitive> AABB_triangle_traits;
typedef typename CGAL::AABB_triangle_primitive_3<Kernel, Iterator> Primitive;
typedef typename CGAL::AABB_traits_3<Kernel, Primitive> AABB_triangle_traits;
typedef typename CGAL::AABB_tree<AABB_triangle_traits> Tree;
if (F.rows() <= 0 || I.rows() <= 0) {
@@ -451,8 +451,8 @@ IGL_INLINE void igl::copyleft::cgal::closest_facet(
typedef CGAL::Exact_predicates_exact_constructions_kernel Kernel;
typedef Kernel::Triangle_3 Triangle;
typedef std::vector<Triangle>::iterator Iterator;
typedef CGAL::AABB_triangle_primitive<Kernel, Iterator> Primitive;
typedef CGAL::AABB_traits<Kernel, Primitive> AABB_triangle_traits;
typedef CGAL::AABB_triangle_primitive_3<Kernel, Iterator> Primitive;
typedef CGAL::AABB_traits_3<Kernel, Primitive> AABB_triangle_traits;
typedef CGAL::AABB_tree<AABB_triangle_traits> Tree;
if (F.rows() <= 0 || I.rows() <= 0) {
@@ -498,9 +498,9 @@ IGL_INLINE void igl::copyleft::cgal::closest_facet(
#ifdef IGL_STATIC_LIBRARY
// Explicit template instantiation
// generated by autoexplicit.sh
template void igl::copyleft::cgal::closest_facet<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 1, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<CGAL::Epeck::FT, -1, 3, 1, -1, 3>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, CGAL::Epeck, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>>(Eigen::MatrixBase<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 1, -1, -1>> const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, -1, 0, -1, -1>> const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>> const&, Eigen::MatrixBase<Eigen::Matrix<CGAL::Epeck::FT, -1, 3, 1, -1, 3>> const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>> const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>> const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>> const&, std::vector<std::vector<size_t, std::allocator<size_t>>, std::allocator<std::vector<size_t, std::allocator<size_t>>>> const&, std::vector<std::vector<size_t, std::allocator<size_t>>, std::allocator<std::vector<size_t, std::allocator<size_t>>>> const&, CGAL::AABB_tree<CGAL::AABB_traits<CGAL::Epeck, CGAL::AABB_triangle_primitive<CGAL::Epeck, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3>>::iterator, CGAL::Boolean_tag<false>>, CGAL::Default>> const&, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3>> const&, std::vector<bool, std::allocator<bool>> const&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>>&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>>&);
template void igl::copyleft::cgal::closest_facet<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 1, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<CGAL::Epeck::FT, -1, 3, 1, -1, 3>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, CGAL::Epeck, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>>(Eigen::MatrixBase<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 1, -1, -1>> const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, -1, 0, -1, -1>> const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>> const&, Eigen::MatrixBase<Eigen::Matrix<CGAL::Epeck::FT, -1, 3, 1, -1, 3>> const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>> const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>> const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>> const&, std::vector<std::vector<size_t, std::allocator<size_t>>, std::allocator<std::vector<size_t, std::allocator<size_t>>>> const&, std::vector<std::vector<size_t, std::allocator<size_t>>, std::allocator<std::vector<size_t, std::allocator<size_t>>>> const&, CGAL::AABB_tree<CGAL::AABB_traits_3<CGAL::Epeck, CGAL::AABB_triangle_primitive_3<CGAL::Epeck, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3>>::iterator, CGAL::Boolean_tag<false>>, CGAL::Default>> const&, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3>> const&, std::vector<bool, std::allocator<bool>> const&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>>&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>>&);
// generated by autoexplicit.sh
template void igl::copyleft::cgal::closest_facet<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<CGAL::Epeck::FT, -1, 3, 0, -1, 3>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, CGAL::Epeck, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>>(Eigen::MatrixBase<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 0, -1, -1>> const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, -1, 0, -1, -1>> const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>> const&, Eigen::MatrixBase<Eigen::Matrix<CGAL::Epeck::FT, -1, 3, 0, -1, 3>> const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>> const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>> const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>> const&, std::vector<std::vector<size_t, std::allocator<size_t>>, std::allocator<std::vector<size_t, std::allocator<size_t>>>> const&, std::vector<std::vector<size_t, std::allocator<size_t>>, std::allocator<std::vector<size_t, std::allocator<size_t>>>> const&, CGAL::AABB_tree<CGAL::AABB_traits<CGAL::Epeck, CGAL::AABB_triangle_primitive<CGAL::Epeck, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3>>::iterator, CGAL::Boolean_tag<false>>, CGAL::Default>> const&, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3>> const&, std::vector<bool, std::allocator<bool>> const&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>>&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>>&);
template void igl::copyleft::cgal::closest_facet<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<CGAL::Epeck::FT, -1, 3, 0, -1, 3>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, CGAL::Epeck, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>>(Eigen::MatrixBase<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 0, -1, -1>> const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, -1, 0, -1, -1>> const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>> const&, Eigen::MatrixBase<Eigen::Matrix<CGAL::Epeck::FT, -1, 3, 0, -1, 3>> const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>> const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>> const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>> const&, std::vector<std::vector<size_t, std::allocator<size_t>>, std::allocator<std::vector<size_t, std::allocator<size_t>>>> const&, std::vector<std::vector<size_t, std::allocator<size_t>>, std::allocator<std::vector<size_t, std::allocator<size_t>>>> const&, CGAL::AABB_tree<CGAL::AABB_traits_3<CGAL::Epeck, CGAL::AABB_triangle_primitive_3<CGAL::Epeck, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3>>::iterator, CGAL::Boolean_tag<false>>, CGAL::Default>> const&, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3>> const&, std::vector<bool, std::allocator<bool>> const&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>>&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>>&);
// generated by autoexplicit.sh
template void igl::copyleft::cgal::closest_facet<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 1, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 1, -1, -1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, CGAL::Epeck, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<int, -1, 1, 0, -1, 1> >(
Eigen::MatrixBase<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 1, -1, -1> > const&,
@@ -509,7 +509,7 @@ template void igl::copyleft::cgal::closest_facet<Eigen::Matrix<CGAL::Epeck::FT,
Eigen::MatrixBase<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 1, -1, -1> > const&,
Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> > const&,
Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> > const&,
Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> > const&, std::vector<std::vector<size_t, std::allocator<size_t> >, std::allocator<std::vector<size_t, std::allocator<size_t> > > > const&, std::vector<std::vector<size_t, std::allocator<size_t> >, std::allocator<std::vector<size_t, std::allocator<size_t> > > > const&, CGAL::AABB_tree<CGAL::AABB_traits<CGAL::Epeck, CGAL::AABB_triangle_primitive<CGAL::Epeck, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3> >::iterator, CGAL::Boolean_tag<false> >, CGAL::Default> > const&, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3> > const&, std::vector<bool, std::allocator<bool> > const&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> >&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> >&);
Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> > const&, std::vector<std::vector<size_t, std::allocator<size_t> >, std::allocator<std::vector<size_t, std::allocator<size_t> > > > const&, std::vector<std::vector<size_t, std::allocator<size_t> >, std::allocator<std::vector<size_t, std::allocator<size_t> > > > const&, CGAL::AABB_tree<CGAL::AABB_traits_3<CGAL::Epeck, CGAL::AABB_triangle_primitive_3<CGAL::Epeck, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3> >::iterator, CGAL::Boolean_tag<false> >, CGAL::Default> > const&, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3> > const&, std::vector<bool, std::allocator<bool> > const&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> >&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> >&);
#include <cstdint>
template void igl::copyleft::cgal::closest_facet<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, CGAL::Epeck, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<int, -1, 1, 0, -1, 1> >(
Eigen::MatrixBase<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 0, -1, -1> > const&,
@@ -518,7 +518,7 @@ Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> > const&,
Eigen::MatrixBase<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 0, -1, -1> > const&,
Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> > const&,
Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> > const&,
Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> > const&, std::vector<std::vector<size_t, std::allocator<size_t> >, std::allocator<std::vector<size_t, std::allocator<size_t> > > > const&, std::vector<std::vector<size_t, std::allocator<size_t> >, std::allocator<std::vector<size_t, std::allocator<size_t> > > > const&, CGAL::AABB_tree<CGAL::AABB_traits<CGAL::Epeck, CGAL::AABB_triangle_primitive<CGAL::Epeck, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3> >::iterator, CGAL::Boolean_tag<false> >, CGAL::Default> > const&, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3> > const&, std::vector<bool, std::allocator<bool> > const&,
Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> > const&, std::vector<std::vector<size_t, std::allocator<size_t> >, std::allocator<std::vector<size_t, std::allocator<size_t> > > > const&, std::vector<std::vector<size_t, std::allocator<size_t> >, std::allocator<std::vector<size_t, std::allocator<size_t> > > > const&, CGAL::AABB_tree<CGAL::AABB_traits_3<CGAL::Epeck, CGAL::AABB_triangle_primitive_3<CGAL::Epeck, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3> >::iterator, CGAL::Boolean_tag<false> >, CGAL::Default> > const&, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3> > const&, std::vector<bool, std::allocator<bool> > const&,
Eigen::PlainObjectBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> >&,
Eigen::PlainObjectBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> >&);
@@ -14,8 +14,8 @@
#include <vector>
#include <CGAL/AABB_tree.h>
#include <CGAL/AABB_traits.h>
#include <CGAL/AABB_triangle_primitive.h>
#include <CGAL/AABB_traits_3.h>
#include <CGAL/AABB_triangle_primitive_3.h>
#include <CGAL/intersections.h>
#include <CGAL/Exact_predicates_exact_constructions_kernel.h>
@@ -67,9 +67,9 @@ namespace igl
const std::vector<std::vector<size_t> > & VF,
const std::vector<std::vector<size_t> > & VFi,
const CGAL::AABB_tree<
CGAL::AABB_traits<
CGAL::AABB_traits_3<
Kernel,
CGAL::AABB_triangle_primitive<
CGAL::AABB_triangle_primitive_3<
Kernel, typename std::vector<
typename Kernel::Triangle_3 >::iterator > > > & tree,
const std::vector<typename Kernel::Triangle_3 > & triangles,
@@ -139,9 +139,9 @@ namespace igl
const std::vector<std::vector<size_t> > & VF,
const std::vector<std::vector<size_t> > & VFi,
const CGAL::AABB_tree<
CGAL::AABB_traits<
CGAL::AABB_traits_3<
Kernel,
CGAL::AABB_triangle_primitive<
CGAL::AABB_triangle_primitive_3<
Kernel, typename std::vector<
typename Kernel::Triangle_3 >::iterator > > > & tree,
const std::vector<typename Kernel::Triangle_3 > & triangles,
@@ -12,8 +12,8 @@
#include "points_inside_component.h"
#include <CGAL/AABB_tree.h>
#include <CGAL/AABB_traits.h>
#include <CGAL/AABB_triangle_primitive.h>
#include <CGAL/AABB_traits_3.h>
#include <CGAL/AABB_triangle_primitive_3.h>
#include <CGAL/Exact_predicates_exact_constructions_kernel.h>
#include <cassert>
@@ -23,8 +23,8 @@
#include "../../vertex_triangle_adjacency.h"
#include <CGAL/AABB_tree.h>
#include <CGAL/AABB_traits.h>
#include <CGAL/AABB_triangle_primitive.h>
#include <CGAL/AABB_traits_3.h>
#include <CGAL/AABB_triangle_primitive_3.h>
#include <CGAL/intersections.h>
#include <CGAL/Exact_predicates_exact_constructions_kernel.h>
@@ -156,9 +156,9 @@ IGL_INLINE size_t igl::copyleft::cgal::extract_cells(
std::vector<VectorXI> Is(num_components);
std::vector<
CGAL::AABB_tree<
CGAL::AABB_traits<
CGAL::AABB_traits_3<
Kernel,
CGAL::AABB_triangle_primitive<
CGAL::AABB_triangle_primitive_3<
Kernel, std::vector<
Kernel::Triangle_3 >::iterator > > > > trees(num_components);
std::vector< std::vector<Kernel::Triangle_3 > >
@@ -9,8 +9,8 @@ template <
IGL_INLINE void igl::copyleft::cgal::hausdorff(
const Eigen::MatrixBase<DerivedV>& V,
const CGAL::AABB_tree<
CGAL::AABB_traits<Kernel,
CGAL::AABB_triangle_primitive<Kernel,
CGAL::AABB_traits_3<Kernel,
CGAL::AABB_triangle_primitive_3<Kernel,
typename std::vector<CGAL::Triangle_3<Kernel> >::iterator
>
>
@@ -27,8 +27,8 @@ IGL_INLINE void igl::copyleft::cgal::hausdorff(
{
CGAL::Point_3<Kernel> query(x,y,z);
typename CGAL::AABB_tree<
CGAL::AABB_traits<Kernel,
CGAL::AABB_triangle_primitive<Kernel,
CGAL::AABB_traits_3<Kernel,
CGAL::AABB_triangle_primitive_3<Kernel,
typename std::vector<CGAL::Triangle_3<Kernel> >::iterator
>
>
@@ -41,5 +41,5 @@ IGL_INLINE void igl::copyleft::cgal::hausdorff(
#ifdef IGL_STATIC_LIBRARY
// Explicit template instantiation
// generated by autoexplicit.sh
template void igl::copyleft::cgal::hausdorff<Eigen::Matrix<double, -1, -1, 0, -1, -1>, CGAL::Simple_cartesian<double>, double>(Eigen::MatrixBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> > const&, CGAL::AABB_tree<CGAL::AABB_traits<CGAL::Simple_cartesian<double>, CGAL::AABB_triangle_primitive<CGAL::Simple_cartesian<double>, std::vector<CGAL::Triangle_3<CGAL::Simple_cartesian<double> >, std::allocator<CGAL::Triangle_3<CGAL::Simple_cartesian<double> > > >::iterator, CGAL::Boolean_tag<false> >, CGAL::Default> > const&, std::vector<CGAL::Triangle_3<CGAL::Simple_cartesian<double> >, std::allocator<CGAL::Triangle_3<CGAL::Simple_cartesian<double> > > > const&, double&, double&);
template void igl::copyleft::cgal::hausdorff<Eigen::Matrix<double, -1, -1, 0, -1, -1>, CGAL::Simple_cartesian<double>, double>(Eigen::MatrixBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> > const&, CGAL::AABB_tree<CGAL::AABB_traits_3<CGAL::Simple_cartesian<double>, CGAL::AABB_triangle_primitive_3<CGAL::Simple_cartesian<double>, std::vector<CGAL::Triangle_3<CGAL::Simple_cartesian<double> >, std::allocator<CGAL::Triangle_3<CGAL::Simple_cartesian<double> > > >::iterator, CGAL::Boolean_tag<false> >, CGAL::Default> > const&, std::vector<CGAL::Triangle_3<CGAL::Simple_cartesian<double> >, std::allocator<CGAL::Triangle_3<CGAL::Simple_cartesian<double> > > > const&, double&, double&);
#endif
@@ -38,8 +38,8 @@ namespace igl
IGL_INLINE void hausdorff(
const Eigen::MatrixBase<DerivedV>& V,
const CGAL::AABB_tree<
CGAL::AABB_traits<Kernel,
CGAL::AABB_triangle_primitive<Kernel,
CGAL::AABB_traits_3<Kernel,
CGAL::AABB_triangle_primitive_3<Kernel,
typename std::vector<CGAL::Triangle_3<Kernel> >::iterator
>
>
@@ -12,8 +12,8 @@
#include "../../remove_unreferenced.h"
#include <CGAL/AABB_tree.h>
#include <CGAL/AABB_traits.h>
#include <CGAL/AABB_triangle_primitive.h>
#include <CGAL/AABB_traits_3.h>
#include <CGAL/AABB_triangle_primitive_3.h>
#include <CGAL/intersections.h>
#include <CGAL/Exact_predicates_exact_constructions_kernel.h>
@@ -12,8 +12,8 @@
#include "../../remove_unreferenced.h"
#include <CGAL/AABB_tree.h>
#include <CGAL/AABB_traits.h>
#include <CGAL/AABB_triangle_primitive.h>
#include <CGAL/AABB_traits_3.h>
#include <CGAL/AABB_triangle_primitive_3.h>
#include <CGAL/intersections.h>
#include <CGAL/Exact_predicates_exact_constructions_kernel.h>
@@ -29,8 +29,8 @@ IGL_INLINE void igl::copyleft::cgal::point_mesh_squared_distance(
using namespace std;
typedef CGAL::Triangle_3<Kernel> Triangle_3;
typedef typename std::vector<Triangle_3>::iterator Iterator;
typedef CGAL::AABB_triangle_primitive<Kernel, Iterator> Primitive;
typedef CGAL::AABB_traits<Kernel, Primitive> AABB_triangle_traits;
typedef CGAL::AABB_triangle_primitive_3<Kernel, Iterator> Primitive;
typedef CGAL::AABB_traits_3<Kernel, Primitive> AABB_triangle_traits;
typedef CGAL::AABB_tree<AABB_triangle_traits> Tree;
Tree tree;
vector<Triangle_3> T;
@@ -43,8 +43,8 @@ IGL_INLINE void igl::copyleft::cgal::point_mesh_squared_distance_precompute(
const Eigen::MatrixBase<DerivedV> & V,
const Eigen::MatrixBase<DerivedF> & F,
CGAL::AABB_tree<
CGAL::AABB_traits<Kernel,
CGAL::AABB_triangle_primitive<Kernel,
CGAL::AABB_traits_3<Kernel,
CGAL::AABB_triangle_primitive_3<Kernel,
typename std::vector<CGAL::Triangle_3<Kernel> >::iterator
>
>
@@ -90,8 +90,8 @@ template <
IGL_INLINE void igl::copyleft::cgal::point_mesh_squared_distance(
const Eigen::MatrixBase<DerivedP> & P,
const CGAL::AABB_tree<
CGAL::AABB_traits<Kernel,
CGAL::AABB_triangle_primitive<Kernel,
CGAL::AABB_traits_3<Kernel,
CGAL::AABB_triangle_primitive_3<Kernel,
typename std::vector<CGAL::Triangle_3<Kernel> >::iterator
>
>
@@ -103,8 +103,8 @@ IGL_INLINE void igl::copyleft::cgal::point_mesh_squared_distance(
{
typedef CGAL::Triangle_3<Kernel> Triangle_3;
typedef typename std::vector<Triangle_3>::iterator Iterator;
typedef CGAL::AABB_triangle_primitive<Kernel, Iterator> Primitive;
typedef CGAL::AABB_traits<Kernel, Primitive> AABB_triangle_traits;
typedef CGAL::AABB_triangle_primitive_3<Kernel, Iterator> Primitive;
typedef CGAL::AABB_traits_3<Kernel, Primitive> AABB_triangle_traits;
typedef CGAL::AABB_tree<AABB_triangle_traits> Tree;
typedef typename Tree::Point_and_primitive_id Point_and_primitive_id;
typedef CGAL::Point_3<Kernel> Point_3;
@@ -134,7 +134,7 @@ IGL_INLINE void igl::copyleft::cgal::point_mesh_squared_distance(
template void igl::copyleft::cgal::point_mesh_squared_distance<CGAL::Epeck, Eigen::Matrix<CGAL::Epeck::FT, -1, 3, 0, -1, 3>, Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<CGAL::Epeck::FT, -1, 1, 0, -1, 1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<double, -1, 1, 0, -1, 1>>(Eigen::MatrixBase<Eigen::Matrix<CGAL::Epeck::FT, -1, 3, 0, -1, 3>> const&, Eigen::MatrixBase<Eigen::Matrix<double, -1, -1, 0, -1, -1>> const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, -1, 0, -1, -1>> const&, Eigen::PlainObjectBase<Eigen::Matrix<CGAL::Epeck::FT, -1, 1, 0, -1, 1>>&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, 1, 0, -1, 1>>&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, 1, 0, -1, 1>>&);
template void igl::copyleft::cgal::point_mesh_squared_distance<CGAL::Epeck, Eigen::Matrix<CGAL::Epeck::FT, -1, 3, 0, -1, 3>, Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<CGAL::Epeck::FT, -1, 1, 0, -1, 1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<double, -1, -1, 0, -1, -1> >(Eigen::MatrixBase<Eigen::Matrix<CGAL::Epeck::FT, -1, 3, 0, -1, 3> > const&, Eigen::MatrixBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> > const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> > const&, Eigen::PlainObjectBase<Eigen::Matrix<CGAL::Epeck::FT, -1, 1, 0, -1, 1> >&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> >&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&);
template void igl::copyleft::cgal::point_mesh_squared_distance<CGAL::Epeck, Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<double, -1, 1, 0, -1, 1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<double, -1, -1, 0, -1, -1> >(Eigen::MatrixBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> > const&, Eigen::MatrixBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> > const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> > const&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, 1, 0, -1, 1> >&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> >&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> >&);
template void igl::copyleft::cgal::point_mesh_squared_distance<CGAL::Simple_cartesian<double>, Eigen::Matrix<double, -1, 3, 1, -1, 3>, Eigen::Matrix<double, -1, 1, 0, -1, 1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<double, -1, 3, 1, -1, 3> >(Eigen::MatrixBase<Eigen::Matrix<double, -1, 3, 1, -1, 3> > const&, CGAL::AABB_tree<CGAL::AABB_traits<CGAL::Simple_cartesian<double>, CGAL::AABB_triangle_primitive<CGAL::Simple_cartesian<double>, std::vector<CGAL::Triangle_3<CGAL::Simple_cartesian<double> >, std::allocator<CGAL::Triangle_3<CGAL::Simple_cartesian<double> > > >::iterator, CGAL::Boolean_tag<false> >, CGAL::Default> > const&, std::vector<CGAL::Triangle_3<CGAL::Simple_cartesian<double> >, std::allocator<CGAL::Triangle_3<CGAL::Simple_cartesian<double> > > > const&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, 1, 0, -1, 1> >&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> >&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, 3, 1, -1, 3> >&);
template void igl::copyleft::cgal::point_mesh_squared_distance_precompute<CGAL::Simple_cartesian<double>, Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1> >(Eigen::MatrixBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> > const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> > const&, CGAL::AABB_tree<CGAL::AABB_traits<CGAL::Simple_cartesian<double>, CGAL::AABB_triangle_primitive<CGAL::Simple_cartesian<double>, std::vector<CGAL::Triangle_3<CGAL::Simple_cartesian<double> >, std::allocator<CGAL::Triangle_3<CGAL::Simple_cartesian<double> > > >::iterator, CGAL::Boolean_tag<false> >, CGAL::Default> >&, std::vector<CGAL::Triangle_3<CGAL::Simple_cartesian<double> >, std::allocator<CGAL::Triangle_3<CGAL::Simple_cartesian<double> > > >&);
template void igl::copyleft::cgal::point_mesh_squared_distance_precompute<CGAL::Simple_cartesian<double>, Eigen::Matrix<double, -1, 3, 1, -1, 3>, Eigen::Matrix<int, -1, -1, 0, -1, -1> >(Eigen::MatrixBase<Eigen::Matrix<double, -1, 3, 1, -1, 3> > const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> > const&, CGAL::AABB_tree<CGAL::AABB_traits<CGAL::Simple_cartesian<double>, CGAL::AABB_triangle_primitive<CGAL::Simple_cartesian<double>, std::vector<CGAL::Triangle_3<CGAL::Simple_cartesian<double> >, std::allocator<CGAL::Triangle_3<CGAL::Simple_cartesian<double> > > >::iterator, CGAL::Boolean_tag<false> >, CGAL::Default> >&, std::vector<CGAL::Triangle_3<CGAL::Simple_cartesian<double> >, std::allocator<CGAL::Triangle_3<CGAL::Simple_cartesian<double> > > >&);
template void igl::copyleft::cgal::point_mesh_squared_distance<CGAL::Simple_cartesian<double>, Eigen::Matrix<double, -1, 3, 1, -1, 3>, Eigen::Matrix<double, -1, 1, 0, -1, 1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, Eigen::Matrix<double, -1, 3, 1, -1, 3> >(Eigen::MatrixBase<Eigen::Matrix<double, -1, 3, 1, -1, 3> > const&, CGAL::AABB_tree<CGAL::AABB_traits_3<CGAL::Simple_cartesian<double>, CGAL::AABB_triangle_primitive_3<CGAL::Simple_cartesian<double>, std::vector<CGAL::Triangle_3<CGAL::Simple_cartesian<double> >, std::allocator<CGAL::Triangle_3<CGAL::Simple_cartesian<double> > > >::iterator, CGAL::Boolean_tag<false> >, CGAL::Default> > const&, std::vector<CGAL::Triangle_3<CGAL::Simple_cartesian<double> >, std::allocator<CGAL::Triangle_3<CGAL::Simple_cartesian<double> > > > const&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, 1, 0, -1, 1> >&, Eigen::PlainObjectBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> >&, Eigen::PlainObjectBase<Eigen::Matrix<double, -1, 3, 1, -1, 3> >&);
template void igl::copyleft::cgal::point_mesh_squared_distance_precompute<CGAL::Simple_cartesian<double>, Eigen::Matrix<double, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1> >(Eigen::MatrixBase<Eigen::Matrix<double, -1, -1, 0, -1, -1> > const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> > const&, CGAL::AABB_tree<CGAL::AABB_traits_3<CGAL::Simple_cartesian<double>, CGAL::AABB_triangle_primitive_3<CGAL::Simple_cartesian<double>, std::vector<CGAL::Triangle_3<CGAL::Simple_cartesian<double> >, std::allocator<CGAL::Triangle_3<CGAL::Simple_cartesian<double> > > >::iterator, CGAL::Boolean_tag<false> >, CGAL::Default> >&, std::vector<CGAL::Triangle_3<CGAL::Simple_cartesian<double> >, std::allocator<CGAL::Triangle_3<CGAL::Simple_cartesian<double> > > >&);
template void igl::copyleft::cgal::point_mesh_squared_distance_precompute<CGAL::Simple_cartesian<double>, Eigen::Matrix<double, -1, 3, 1, -1, 3>, Eigen::Matrix<int, -1, -1, 0, -1, -1> >(Eigen::MatrixBase<Eigen::Matrix<double, -1, 3, 1, -1, 3> > const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> > const&, CGAL::AABB_tree<CGAL::AABB_traits_3<CGAL::Simple_cartesian<double>, CGAL::AABB_triangle_primitive_3<CGAL::Simple_cartesian<double>, std::vector<CGAL::Triangle_3<CGAL::Simple_cartesian<double> >, std::allocator<CGAL::Triangle_3<CGAL::Simple_cartesian<double> > > >::iterator, CGAL::Boolean_tag<false> >, CGAL::Default> >&, std::vector<CGAL::Triangle_3<CGAL::Simple_cartesian<double> >, std::allocator<CGAL::Triangle_3<CGAL::Simple_cartesian<double> > > >&);
#endif
@@ -63,8 +63,8 @@ namespace igl
const Eigen::MatrixBase<DerivedV> & V,
const Eigen::MatrixBase<DerivedF> & F,
CGAL::AABB_tree<
CGAL::AABB_traits<Kernel,
CGAL::AABB_triangle_primitive<Kernel,
CGAL::AABB_traits_3<Kernel,
CGAL::AABB_triangle_primitive_3<Kernel,
typename std::vector<CGAL::Triangle_3<Kernel> >::iterator
>
>
@@ -89,8 +89,8 @@ namespace igl
IGL_INLINE void point_mesh_squared_distance(
const Eigen::MatrixBase<DerivedP> & P,
const CGAL::AABB_tree<
CGAL::AABB_traits<Kernel,
CGAL::AABB_triangle_primitive<Kernel,
CGAL::AABB_traits_3<Kernel,
CGAL::AABB_triangle_primitive_3<Kernel,
typename std::vector<CGAL::Triangle_3<Kernel> >::iterator
>
>
@@ -13,8 +13,8 @@
#include "assign_scalar.h"
#include <CGAL/AABB_tree.h>
#include <CGAL/AABB_traits.h>
#include <CGAL/AABB_triangle_primitive.h>
#include <CGAL/AABB_traits_3.h>
#include <CGAL/AABB_triangle_primitive_3.h>
#include <CGAL/Exact_predicates_exact_constructions_kernel.h>
#include <cassert>
@@ -35,8 +35,8 @@ namespace igl {
typedef Kernel::Triangle_3 Triangle;
typedef Kernel::Plane_3 Plane_3;
typedef std::vector<Triangle>::iterator Iterator;
typedef CGAL::AABB_triangle_primitive<Kernel, Iterator> Primitive;
typedef CGAL::AABB_traits<Kernel, Primitive> AABB_triangle_traits;
typedef CGAL::AABB_triangle_primitive_3<Kernel, Iterator> Primitive;
typedef CGAL::AABB_traits_3<Kernel, Primitive> AABB_triangle_traits;
typedef CGAL::AABB_tree<AABB_triangle_traits> Tree;
template<typename DerivedF, typename DerivedI>
@@ -23,8 +23,8 @@
#include <CGAL/IO/output_surface_facets_to_polyhedron.h>
// Axis-aligned bounding box tree for tet tri intersection
#include <CGAL/AABB_tree.h>
#include <CGAL/AABB_traits.h>
#include <CGAL/AABB_triangle_primitive.h>
#include <CGAL/AABB_traits_3.h>
#include <CGAL/AABB_triangle_primitive_3.h>
#include <vector>
IGL_INLINE bool igl::copyleft::cgal::signed_distance_isosurface(
@@ -18,9 +18,9 @@ IGL_INLINE void igl::copyleft::cgal::submesh_aabb_tree(
const Eigen::MatrixBase<DerivedF>& F,
const Eigen::MatrixBase<DerivedI>& I,
CGAL::AABB_tree<
CGAL::AABB_traits<
CGAL::AABB_traits_3<
Kernel,
CGAL::AABB_triangle_primitive<
CGAL::AABB_triangle_primitive_3<
Kernel, typename std::vector<
typename Kernel::Triangle_3 >::iterator > > > & tree,
std::vector<typename Kernel::Triangle_3 > & triangles,
@@ -51,7 +51,7 @@ IGL_INLINE void igl::copyleft::cgal::submesh_aabb_tree(
#ifdef IGL_STATIC_LIBRARY
// Explicit template instantiation
// generated by autoexplicit.sh
template void igl::copyleft::cgal::submesh_aabb_tree<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, CGAL::Epeck>(Eigen::MatrixBase<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 0, -1, -1> > const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> > const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> > const&, CGAL::AABB_tree<CGAL::AABB_traits<CGAL::Epeck, CGAL::AABB_triangle_primitive<CGAL::Epeck, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3> >::iterator, CGAL::Boolean_tag<false> > > >&, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3> >&, std::vector<bool, std::allocator<bool> >&);
template void igl::copyleft::cgal::submesh_aabb_tree<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 1, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, CGAL::Epeck>(Eigen::MatrixBase<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 1, -1, -1> > const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> > const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> > const&, CGAL::AABB_tree<CGAL::AABB_traits<CGAL::Epeck, CGAL::AABB_triangle_primitive<CGAL::Epeck, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3> >::iterator, CGAL::Boolean_tag<false> > > >&, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3> >&, std::vector<bool, std::allocator<bool> >&);
template void igl::copyleft::cgal::submesh_aabb_tree<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 1, -1, -1>, Eigen::Matrix<int, -1, 3, 1, -1, 3>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, CGAL::Epeck>(Eigen::MatrixBase<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 1, -1, -1> > const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, 3, 1, -1, 3> > const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> > const&, CGAL::AABB_tree<CGAL::AABB_traits<CGAL::Epeck, CGAL::AABB_triangle_primitive<CGAL::Epeck, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3> >::iterator, CGAL::Boolean_tag<false> > > >&, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3> >&, std::vector<bool, std::allocator<bool> >&);
template void igl::copyleft::cgal::submesh_aabb_tree<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, CGAL::Epeck>(Eigen::MatrixBase<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 0, -1, -1> > const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> > const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> > const&, CGAL::AABB_tree<CGAL::AABB_traits_3<CGAL::Epeck, CGAL::AABB_triangle_primitive_3<CGAL::Epeck, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3> >::iterator, CGAL::Boolean_tag<false> > > >&, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3> >&, std::vector<bool, std::allocator<bool> >&);
template void igl::copyleft::cgal::submesh_aabb_tree<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 1, -1, -1>, Eigen::Matrix<int, -1, -1, 0, -1, -1>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, CGAL::Epeck>(Eigen::MatrixBase<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 1, -1, -1> > const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, -1, 0, -1, -1> > const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> > const&, CGAL::AABB_tree<CGAL::AABB_traits_3<CGAL::Epeck, CGAL::AABB_triangle_primitive_3<CGAL::Epeck, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3> >::iterator, CGAL::Boolean_tag<false> > > >&, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3> >&, std::vector<bool, std::allocator<bool> >&);
template void igl::copyleft::cgal::submesh_aabb_tree<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 1, -1, -1>, Eigen::Matrix<int, -1, 3, 1, -1, 3>, Eigen::Matrix<int, -1, 1, 0, -1, 1>, CGAL::Epeck>(Eigen::MatrixBase<Eigen::Matrix<CGAL::Epeck::FT, -1, -1, 1, -1, -1> > const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, 3, 1, -1, 3> > const&, Eigen::MatrixBase<Eigen::Matrix<int, -1, 1, 0, -1, 1> > const&, CGAL::AABB_tree<CGAL::AABB_traits_3<CGAL::Epeck, CGAL::AABB_triangle_primitive_3<CGAL::Epeck, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3> >::iterator, CGAL::Boolean_tag<false> > > >&, std::vector<CGAL::Epeck::Triangle_3, std::allocator<CGAL::Epeck::Triangle_3> >&, std::vector<bool, std::allocator<bool> >&);
#endif
@@ -14,8 +14,8 @@
#include <vector>
#include <CGAL/AABB_tree.h>
#include <CGAL/AABB_traits.h>
#include <CGAL/AABB_triangle_primitive.h>
#include <CGAL/AABB_traits_3.h>
#include <CGAL/AABB_triangle_primitive_3.h>
#include <CGAL/intersections.h>
#include <CGAL/Exact_predicates_exact_constructions_kernel.h>
@@ -44,9 +44,9 @@ namespace igl
const Eigen::MatrixBase<DerivedF>& F,
const Eigen::MatrixBase<DerivedI>& I,
CGAL::AABB_tree<
CGAL::AABB_traits<
CGAL::AABB_traits_3<
Kernel,
CGAL::AABB_triangle_primitive<
CGAL::AABB_triangle_primitive_3<
Kernel, typename std::vector<
typename Kernel::Triangle_3 >::iterator > > > & tree,
std::vector<typename Kernel::Triangle_3 > & triangles,

Some files were not shown because too many files have changed in this diff Show More