Compare commits

..
Author SHA1 Message Date
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 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
yw4z c23ea82ad1 revert extruder term on printer settings and optimize dynamic paddings 2026-10-10 01:17:33 +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
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
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
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
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
TheLegendTubaGuy a3005a96ac Merge branch 'main' into fix/16194-bbl-infill-retraction 2026-10-09 02:33:14 -05: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
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
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
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
Rodrigo Faselli d09850c528 Merge branch 'main' into main 2026-10-08 10:45:49 -03:00
SoftFever bf59db1053 Merge branch 'main' into extruder-variants-ui-2 2026-10-08 20:31:57 +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
TheLegendTubaGuy ff3c151417 Fix BBL infill retraction reduction default 2026-10-06 03:26:24 -05:00
Ioannis Giannakas 6561c4d35b Merge branch 'main' into main 2026-10-06 08:01:59 +01: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
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
Rodrigo Faselli 88eb219868 Merge branch 'main' into main 2026-10-03 14:41:20 -03: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
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
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
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
yw4z cffd1ab053 Update UnsavedChangesDialog.cpp 2026-10-02 17:07:02 +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
yw4z e7f5d7be30 init 2026-10-02 14:39:05 +03: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
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
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
Clifford Garwood b17414b160 Merge remote-tracking branch 'upstream/main' into integration/upstream-main-sync 2026-09-13 02:21:43 -04: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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
161 changed files with 18461 additions and 645 deletions
+2
View File
@@ -70,6 +70,8 @@ If you come across any of these in search results, please <b>report them</b> as
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
+2
View File
@@ -164,6 +164,7 @@ src/slic3r/GUI/OptionsGroup.cpp
src/slic3r/GUI/PrintOptionsDialog.cpp
src/slic3r/GUI/SafetyOptionsDialog.cpp
src/slic3r/GUI/ParamsPanel.cpp
src/slic3r/GUI/IMEXModesCtrl.cpp
src/slic3r/GUI/PartPlate.cpp
src/slic3r/GUI/Plater.cpp
src/slic3r/GUI/Preferences.cpp
@@ -212,6 +213,7 @@ src/libslic3r/ExtrusionEntity.cpp
src/libslic3r/Flow.cpp
src/libslic3r/Format/AMF.cpp
src/libslic3r/miniz_extension.cpp
src/libslic3r/IMEXHelpers.cpp
src/libslic3r/Preset.cpp
src/libslic3r/Print.cpp
src/libslic3r/PrintBase.cpp
+1 -3
View File
@@ -1,3 +1 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.2992 1.42627C10.7774 1.42627 8.73315 3.47056 8.73315 5.99231C8.73315 8.51406 10.7774 10.5583 13.2992 10.5583C13.3609 10.5583 13.4223 10.5571 13.4834 10.5547C15.9197 10.4581 17.8652 8.45238 17.8652 5.99231C17.8652 3.47056 15.8209 1.42627 13.2992 1.42627ZM15.6185 11.0536C17.5346 10.1741 18.8652 8.23861 18.8652 5.99231C18.8652 2.91827 16.3732 0.42627 13.2992 0.42627C11.5793 0.42627 10.0417 1.2063 9.02067 2.43188H2.31853C1.93193 2.43188 1.61853 2.74528 1.61853 3.13188V12.4712C1.61853 12.6313 1.6723 12.7789 1.76278 12.8968C1.76671 13.0046 1.79621 13.1144 1.85576 13.2185L3.48408 16.0649C3.60875 16.2829 3.8406 16.4173 4.09168 16.4173H6.75859L7.97903 19.1586C8.22556 19.7123 9.01148 19.7123 9.25801 19.1586L10.4784 16.4173H13.1454C13.3965 16.4173 13.6283 16.2829 13.753 16.0649L15.3813 13.2185C15.4409 13.1144 15.4704 13.0046 15.4743 12.8968C15.5648 12.7789 15.6185 12.6313 15.6185 12.4712V11.0536ZM13.4429 11.5565C13.8469 11.5463 14.2403 11.493 14.6185 11.4011V12.1709H2.61853V3.43188H8.35573C7.95792 4.19837 7.73315 5.06909 7.73315 5.99231C7.73315 9.06635 10.2252 11.5583 13.2992 11.5583C13.3472 11.5583 13.3951 11.5577 13.4429 11.5565ZM2.98076 13.1712H14.2563L12.9714 15.4173H9.85194L9.84578 15.4173H7.39126L7.3851 15.4173H4.26569L2.98076 13.1712ZM9.38381 16.4173L8.61852 18.1363L7.85322 16.4173H9.38381ZM12.2021 3.36422C12.3973 3.55948 12.3973 3.87606 12.2021 4.07133L11.743 4.53041H16.0625C16.3386 4.53041 16.5625 4.75426 16.5625 5.03041C16.5625 5.30655 16.3386 5.53041 16.0625 5.53041H10.7773C10.2428 5.53041 9.97507 4.88412 10.353 4.50614L11.495 3.36422C11.6902 3.16896 12.0068 3.16896 12.2021 3.36422ZM14.3963 7.91305C14.2011 8.10831 14.2011 8.42489 14.3963 8.62015C14.5916 8.81542 14.9082 8.81542 15.1034 8.62015L16.2453 7.47823C16.6233 7.10026 16.3556 6.45397 15.8211 6.45397L10.5359 6.45397C10.2597 6.45397 10.0359 6.67783 10.0359 6.95397C10.0359 7.23011 10.2597 7.45397 10.5359 7.45397L14.8554 7.45397L14.3963 7.91305Z" fill="#262E30"/>
</svg>
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M3.5,11.5v1l2,2,2-2v-1" style="fill:none; stroke:#949494; stroke-linecap:square; stroke-linejoin:round;"/><circle cx="10.5" cy="4.5" r="4" style="fill:none; stroke:#009688; stroke-linecap:round; stroke-linejoin:round;"/><polyline points="11.5 2.5 12.5 3.5 8.5 3.5" style="fill:none; stroke:#009688; stroke-linecap:round; stroke-linejoin:round;"/><polyline points="12.5 5.5 8.5 5.5 9.5 6.5" style="fill:none; stroke:#009688; stroke-linecap:square; stroke-linejoin:round;"/><path d="M5.5.5H1.5C.95.5.5.95.5,1.5v9c0,.55.45,1,1,1h8c.55,0,1-.45,1-1" style="fill:none; stroke:#949494; stroke-linecap:square; stroke-linejoin:round;"/></svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 747 B

+1 -10
View File
@@ -1,10 +1 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1.73486" y="1" width="10.53" height="5.12169" fill="#C2C2C2"/>
<rect x="1.73486" y="1" width="10.53" height="0.497608" fill="#858585"/>
<rect x="1.73486" y="2.70703" width="10.53" height="0.497608" fill="#858585"/>
<rect x="1.73486" y="4.41602" width="10.53" height="0.497608" fill="#858585"/>
<rect x="1.73486" y="6.12305" width="10.53" height="0.497608" fill="#858585"/>
<path d="M6.10107 11.9961H7.77047L7.30675 12.9991H6.61117L6.10107 11.9961Z" fill="#8C8C8C"/>
<rect x="4.1748" y="6.62109" width="5.65026" height="5.3765" fill="#646464"/>
<rect x="6.61475" y="6.62109" width="0.642075" height="5.35947" fill="#1D1D1D"/>
</svg>
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="M3.5,3.5h8M3.5,5.5h8M3.5,1.5h8M6.5,5.5v7l1,1,1-1v-7" style="fill:none; stroke:#949494; stroke-linecap:square; stroke-linejoin:round;"/></svg>

Before

Width:  |  Height:  |  Size: 737 B

After

Width:  |  Height:  |  Size: 255 B

@@ -0,0 +1,7 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#e9e9e9" rx="3.5" height="40" width="40" y="1.5" x="1.5"/>
<path stroke-width="1.0" fill="#545454" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#545454" fill="#545454" d="m21.5275,7.14657l0,28.59908"/>
<path stroke-width="1.0" stroke="#545454" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m14.5112,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.71896,0.9102l-0.81298,1.0906l-0.77183,2.014l-0.22958,2.4972l0.25693,2.9999l0.89251,0.5883l0.21446,1.5893l-0.6172,0l-0.66057,1.2606l-0.51219,1.9929l8.95941,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1.0" stroke="#545454" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m34.77509,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.719,0.9102l-0.8129,1.0906l-0.7719,2.014l-0.2296,2.4972l0.257,2.9999l0.8925,0.5883l0.2145,1.5893l-0.6173,0l-0.6605,1.2606l-0.5122,1.9929l8.9594,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,7 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#54545a" rx="3.5" height="40" width="40" y="1.5" x="1.5"/>
<path stroke-width="1.0" fill="#b3b3b3" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#c4c4c4" fill="#c4c4c4" d="m21.5275,7.14657l0,28.59908"/>
<path stroke-width="1.0" stroke="#c4c4c4" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m14.5112,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.71896,0.9102l-0.81298,1.0906l-0.77183,2.014l-0.22958,2.4972l0.25693,2.9999l0.89251,0.5883l0.21446,1.5893l-0.6172,0l-0.66057,1.2606l-0.51219,1.9929l8.95941,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1.0" stroke="#c4c4c4" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m34.77509,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.719,0.9102l-0.8129,1.0906l-0.7719,2.014l-0.2296,2.4972l0.257,2.9999l0.8925,0.5883l0.2145,1.5893l-0.6173,0l-0.6605,1.2606l-0.5122,1.9929l8.9594,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,7 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#e9e9e9" rx="3.5" height="40" width="40" x="1.5" y="1.5"/>
<path stroke-width="1.0" fill="#009688" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#009688" fill="#009688" d="m21.5275,7.14657l0,28.59908"/>
<path stroke-width="1.0" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m14.5112,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.71896,0.9102l-0.81298,1.0906l-0.77183,2.014l-0.22958,2.4972l0.25693,2.9999l0.89251,0.5883l0.21446,1.5893l-0.6172,0l-0.66057,1.2606l-0.51219,1.9929l8.95941,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1.0" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m34.77509,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.719,0.9102l-0.8129,1.0906l-0.7719,2.014l-0.2296,2.4972l0.257,2.9999l0.8925,0.5883l0.2145,1.5893l-0.6173,0l-0.6605,1.2606l-0.5122,1.9929l8.9594,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,7 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#54545a" rx="3.5" height="40" width="40" y="1.5" x="1.5"/>
<path stroke-width="1.0" fill="#009688" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#009688" fill="#009688" d="m21.5275,7.14657l0,28.59908"/>
<path stroke-width="1.0" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m14.5112,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.71896,0.9102l-0.81298,1.0906l-0.77183,2.014l-0.22958,2.4972l0.25693,2.9999l0.89251,0.5883l0.21446,1.5893l-0.6172,0l-0.66057,1.2606l-0.51219,1.9929l8.95941,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1.0" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m34.77509,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.719,0.9102l-0.8129,1.0906l-0.7719,2.014l-0.2296,2.4972l0.257,2.9999l0.8925,0.5883l0.2145,1.5893l-0.6173,0l-0.6605,1.2606l-0.5122,1.9929l8.9594,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,10 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#e9e9e9" rx="3.5" height="40" width="40" y="1.5" x="1.5"/>
<path stroke-width="1.0" fill="#545454" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#545454" fill="#545454" d="m21.5275,5.5l0,31.9"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#545454" fill="#545454" d="m6.7,21.575891l29.2,0"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1" stroke="#545454" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z" transform="translate(11.75,11.75) scale(0.7) translate(-31.15,-20.94)"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1" stroke="#545454" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z" transform="translate(31.25,11.75) scale(-0.7,0.7) translate(-31.15,-20.94)"/>
<path stroke-width="1" stroke="#545454" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m14.5112,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.71896,0.9102l-0.81298,1.0906l-0.77183,2.014l-0.22958,2.4972l0.25693,2.9999l0.89251,0.5883l0.21446,1.5893l-0.6172,0l-0.66057,1.2606l-0.51219,1.9929l8.95941,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z" transform="translate(11.75,31.25) scale(0.7) translate(-11.85,-20.93)"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1" stroke="#545454" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z" transform="translate(31.25,31.25) scale(0.7) translate(-31.15,-20.94)"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

@@ -0,0 +1,10 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#54545a" rx="3.5" height="40" width="40" y="1.5" x="1.5"/>
<path stroke-width="1.0" fill="#b3b3b3" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#c4c4c4" fill="#c4c4c4" d="m21.5275,5.5l0,31.9"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#c4c4c4" fill="#c4c4c4" d="m6.7,21.575891l29.2,0"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1" stroke="#c4c4c4" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z" transform="translate(11.75,11.75) scale(0.7) translate(-31.15,-20.94)"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1" stroke="#c4c4c4" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z" transform="translate(31.25,11.75) scale(-0.7,0.7) translate(-31.15,-20.94)"/>
<path stroke-width="1" stroke="#c4c4c4" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m14.5112,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.71896,0.9102l-0.81298,1.0906l-0.77183,2.014l-0.22958,2.4972l0.25693,2.9999l0.89251,0.5883l0.21446,1.5893l-0.6172,0l-0.66057,1.2606l-0.51219,1.9929l8.95941,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z" transform="translate(11.75,31.25) scale(0.7) translate(-11.85,-20.93)"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1" stroke="#c4c4c4" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z" transform="translate(31.25,31.25) scale(0.7) translate(-31.15,-20.94)"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

@@ -0,0 +1,10 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#e9e9e9" rx="3.5" height="40" width="40" y="1.5" x="1.5"/>
<path stroke-width="1.0" fill="#009688" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#009688" fill="#009688" d="m21.5275,5.5l0,31.9"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#009688" fill="#009688" d="m6.7,21.575891l29.2,0"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z" transform="translate(11.75,11.75) scale(0.7) translate(-31.15,-20.94)"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z" transform="translate(31.25,11.75) scale(-0.7,0.7) translate(-31.15,-20.94)"/>
<path stroke-width="1" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m14.5112,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.71896,0.9102l-0.81298,1.0906l-0.77183,2.014l-0.22958,2.4972l0.25693,2.9999l0.89251,0.5883l0.21446,1.5893l-0.6172,0l-0.66057,1.2606l-0.51219,1.9929l8.95941,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z" transform="translate(11.75,31.25) scale(0.7) translate(-11.85,-20.93)"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z" transform="translate(31.25,31.25) scale(0.7) translate(-31.15,-20.94)"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

@@ -0,0 +1,10 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#54545a" rx="3.5" height="40" width="40" y="1.5" x="1.5"/>
<path stroke-width="1.0" fill="#009688" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#009688" fill="#009688" d="m21.5275,5.5l0,31.9"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#009688" fill="#009688" d="m6.7,21.575891l29.2,0"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z" transform="translate(11.75,11.75) scale(0.7) translate(-31.15,-20.94)"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z" transform="translate(31.25,11.75) scale(-0.7,0.7) translate(-31.15,-20.94)"/>
<path stroke-width="1" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m14.5112,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.71896,0.9102l-0.81298,1.0906l-0.77183,2.014l-0.22958,2.4972l0.25693,2.9999l0.89251,0.5883l0.21446,1.5893l-0.6172,0l-0.66057,1.2606l-0.51219,1.9929l8.95941,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z" transform="translate(11.75,31.25) scale(0.7) translate(-11.85,-20.93)"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z" transform="translate(31.25,31.25) scale(0.7) translate(-31.15,-20.94)"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

@@ -0,0 +1,7 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#e9e9e9" rx="3.5" height="40" width="40" y="1.5" x="1.5"/>
<path stroke-width="1.0" fill="#545454" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#545454" fill="#545454" d="m21.5275,7.14657l0,28.59908"/>
<path stroke-width="1.0" stroke="#545454" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m14.5112,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.71896,0.9102l-0.81298,1.0906l-0.77183,2.014l-0.22958,2.4972l0.25693,2.9999l0.89251,0.5883l0.21446,1.5893l-0.6172,0l-0.66057,1.2606l-0.51219,1.9929l8.95941,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1.0" stroke="#545454" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,7 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#54545a" rx="3.5" height="40" width="40" y="1.5" x="1.5"/>
<path stroke-width="1.0" fill="#b3b3b3" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#c4c4c4" fill="#c4c4c4" d="m21.5275,7.14657l0,28.59908"/>
<path stroke-width="1.0" stroke="#c4c4c4" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m14.5112,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.71896,0.9102l-0.81298,1.0906l-0.77183,2.014l-0.22958,2.4972l0.25693,2.9999l0.89251,0.5883l0.21446,1.5893l-0.6172,0l-0.66057,1.2606l-0.51219,1.9929l8.95941,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1.0" stroke="#c4c4c4" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,7 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#e9e9e9" rx="3.5" height="40" width="40" y="1.5" x="1.5"/>
<path stroke-width="1.0" fill="#009688" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#009688" fill="#009688" d="m21.5275,7.14657l0,28.59908"/>
<path stroke-width="1.0" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m14.5112,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.71896,0.9102l-0.81298,1.0906l-0.77183,2.014l-0.22958,2.4972l0.25693,2.9999l0.89251,0.5883l0.21446,1.5893l-0.6172,0l-0.66057,1.2606l-0.51219,1.9929l8.95941,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1.0" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,7 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#54545a" rx="3.5" height="40" width="40" y="1.5" x="1.5"/>
<path stroke-width="1.0" fill="#009688" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke-linecap="round" stroke-width="0.8" stroke="#009688" fill="#009688" d="m21.5275,7.14657l0,28.59908"/>
<path stroke-width="1.0" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m14.5112,25.5943l-0.632,0l0.632,-1.1115l-0.0305,-1.3114l-0.6445,-1.103l-0.7745,-0.632l-0.3502,-0.7142l-0.9981,-1.8354l1.4221,0.9295l1.6267,0.1116l0.8272,0.5112l0.9667,-0.4926l0.2509,-1.3664l-1.0317,-0.5763l-0.9945,-1.5429l0.1999,-0.7236l-1.1015,-1.0425l0.4275,-1.608l-0.7064,-0.0651l-0.9388,0.7157l-1.8125,0.1674l-1.71896,0.9102l-0.81298,1.0906l-0.77183,2.014l-0.22958,2.4972l0.25693,2.9999l0.89251,0.5883l0.21446,1.5893l-0.6172,0l-0.66057,1.2606l-0.51219,1.9929l8.95941,0l-0.5289,-1.9929l-0.8089,-1.2606l0,0z"/>
<path stroke-dasharray="1,2" stroke-linecap="round" stroke-width="1.0" stroke="#009688" fill-opacity="0" fill="#ffffff" fill-rule="nonzero" d="m28.4888,25.5943l0.632,0l-0.632,-1.1115l0.0305,-1.3114l0.6445,-1.103l0.7745,-0.632l0.3502,-0.7142l0.9981,-1.8354l-1.4221,0.9295l-1.6267,0.1116l-0.8272,0.5112l-0.9667,-0.4926l-0.2509,-1.3664l1.0317,-0.5763l0.9945,-1.5429l-0.1999,-0.7236l1.1015,-1.0425l-0.4275,-1.608l0.7064,-0.0651l0.9388,0.7157l1.8125,0.1674l1.719,0.9102l0.8129,1.0906l0.7719,2.014l0.2296,2.4972l-0.257,2.9999l-0.8925,0.5883l-0.2145,1.5893l0.6173,0l0.6605,1.2606l0.5122,1.9929l-8.9594,0l0.5289,-1.9929l0.8089,-1.2606l0,0z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,10 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#e9e9e9" rx="3.5" height="40" width="40" x="1.5" y="1.5"/>
<path fill="#545454" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke="#545454" fill="#ffffff" fill-opacity="0" fill-rule="nonzero" stroke-width="1.0" d="m27.20643,29.4739l-1.21802,0l1.21802,-2.14213l-0.05879,-2.52738l-1.2421,-2.12576l-1.49265,-1.21801l-0.67492,-1.37644l-1.92358,-3.53726l2.74073,1.79137l3.13505,0.21508l1.59421,0.98521l1.86307,-0.94935l0.48354,-2.63339l-1.98833,-1.11067l-1.91665,-2.97354l0.38526,-1.39456l-2.12286,-2.00915l0.82389,-3.09901l-1.3614,-0.12546l-1.80929,1.37933l-3.49313,0.32262l-3.31285,1.75417l-1.56681,2.10186l-1.48751,3.88146l-0.44245,4.81271l0.49516,5.78154l1.72009,1.13379l0.41331,3.06297l-1.1895,0l-1.27306,2.42948l-0.98712,3.8408l17.26695,0l-1.01931,-3.8408l-1.55895,-2.42948l0,0z"/>
<path stroke="#545454" stroke-dasharray="2,2" d="m23.61156,6.63793l-1.16183,2.2557l-1.89806,0.27415l-2.4104,1.37699l-1.46486,2.1687l-1.05714,3.57465l-0.28913,4.51495l0.30214,6.3633" fill-opacity="0" stroke-width="1.0" fill="none"/>
<line stroke="#545454" stroke-dasharray="2,2" y2="32.58856" x2="28.96602" y1="32.58856" x1="13.5093" fill-opacity="0" stroke-width="1.0" fill="none"/>
<line stroke="#545454" y2="29.47069" x2="25.98988" y1="29.47069" x1="16.06028" fill-opacity="0" stroke-width="1.0" fill="none"/>
<path stroke="#545454" stroke-dasharray="2,2" d="m27.94317,10.33418l-2.21519,0.02258l-0.32541,0.65079l2.39086,0.92312" fill-opacity="0" stroke-width="1.0" fill="none"/>
<path stroke="#545454" stroke-dasharray="2,2" d="m31.36607,16.48593l-1.47286,-0.34296l0.38557,-0.77114" stroke-width="1.0" fill-opacity="0" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,10 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#54545a" rx="3.5" height="40" width="40" x="1.5" y="1.5"/>
<path fill="#b3b3b3" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke="#c4c4c4" fill="#ffffff" fill-opacity="0" fill-rule="nonzero" stroke-width="1.0" d="m27.20643,29.4739l-1.21802,0l1.21802,-2.14213l-0.05879,-2.52738l-1.2421,-2.12576l-1.49265,-1.21801l-0.67492,-1.37644l-1.92358,-3.53726l2.74073,1.79137l3.13505,0.21508l1.59421,0.98521l1.86307,-0.94935l0.48354,-2.63339l-1.98833,-1.11067l-1.91665,-2.97354l0.38526,-1.39456l-2.12286,-2.00915l0.82389,-3.09901l-1.3614,-0.12546l-1.80929,1.37933l-3.49313,0.32262l-3.31285,1.75417l-1.56681,2.10186l-1.48751,3.88146l-0.44245,4.81271l0.49516,5.78154l1.72009,1.13379l0.41331,3.06297l-1.1895,0l-1.27306,2.42948l-0.98712,3.8408l17.26695,0l-1.01931,-3.8408l-1.55895,-2.42948l0,0z"/>
<path stroke="#c4c4c4" stroke-dasharray="2,2" d="m23.61156,6.63793l-1.16183,2.2557l-1.89806,0.27415l-2.4104,1.37699l-1.46486,2.1687l-1.05714,3.57465l-0.28913,4.51495l0.30214,6.3633" fill-opacity="0" stroke-width="1.0" fill="none"/>
<line stroke="#c4c4c4" stroke-dasharray="2,2" y2="32.58856" x2="28.96602" y1="32.58856" x1="13.5093" fill-opacity="0" stroke-width="1.0" fill="none"/>
<line stroke="#c4c4c4" y2="29.47069" x2="25.98988" y1="29.47069" x1="16.06028" fill-opacity="0" stroke-width="1.0" fill="none"/>
<path stroke="#c4c4c4" stroke-dasharray="2,2" d="m27.94317,10.33418l-2.21519,0.02258l-0.32541,0.65079l2.39086,0.92312" fill-opacity="0" stroke-width="1.0" fill="none"/>
<path stroke="#c4c4c4" stroke-dasharray="2,2" d="m31.36607,16.48593l-1.47286,-0.34296l0.38557,-0.77114" stroke-width="1.0" fill-opacity="0" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,10 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#e9e9e9" rx="3.5" height="40" width="40" x="1.5" y="1.5"/>
<path fill="#009688" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke="#009688" fill="#ffffff" fill-opacity="0" fill-rule="nonzero" stroke-width="1.0" d="m27.20643,29.4739l-1.21802,0l1.21802,-2.14213l-0.05879,-2.52738l-1.2421,-2.12576l-1.49265,-1.21801l-0.67492,-1.37644l-1.92358,-3.53726l2.74073,1.79137l3.13505,0.21508l1.59421,0.98521l1.86307,-0.94935l0.48354,-2.63339l-1.98833,-1.11067l-1.91665,-2.97354l0.38526,-1.39456l-2.12286,-2.00915l0.82389,-3.09901l-1.3614,-0.12546l-1.80929,1.37933l-3.49313,0.32262l-3.31285,1.75417l-1.56681,2.10186l-1.48751,3.88146l-0.44245,4.81271l0.49516,5.78154l1.72009,1.13379l0.41331,3.06297l-1.1895,0l-1.27306,2.42948l-0.98712,3.8408l17.26695,0l-1.01931,-3.8408l-1.55895,-2.42948l0,0z"/>
<path stroke="#009688" stroke-dasharray="2,2" d="m23.61156,6.63793l-1.16183,2.2557l-1.89806,0.27415l-2.4104,1.37699l-1.46486,2.1687l-1.05714,3.57465l-0.28913,4.51495l0.30214,6.3633" fill-opacity="0" stroke-width="1.0" fill="none"/>
<line stroke="#009688" stroke-dasharray="2,2" y2="32.58856" x2="28.96602" y1="32.58856" x1="13.5093" fill-opacity="0" stroke-width="1.0" fill="none"/>
<line stroke="#009688" y2="29.47069" x2="25.98988" y1="29.47069" x1="16.06028" fill-opacity="0" stroke-width="1.0" fill="none"/>
<path stroke="#009688" stroke-dasharray="2,2" d="m27.94317,10.33418l-2.21519,0.02258l-0.32541,0.65079l2.39086,0.92312" fill-opacity="0" stroke-width="1.0" fill="none"/>
<path stroke="#009688" stroke-dasharray="2,2" d="m31.36607,16.48593l-1.47286,-0.34296l0.38557,-0.77114" stroke-width="1.0" fill-opacity="0" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -0,0 +1,10 @@
<svg width="43" height="43" xmlns="http://www.w3.org/2000/svg">
<rect fill="#54545a" rx="3.5" height="40" width="40" x="1.5" y="1.5"/>
<path fill="#009688" d="m38,2a3,3 0 0 1 3,3l0,33a3,3 0 0 1 -3,3l-33,0a3,3 0 0 1 -3,-3l0,-33a3,3 0 0 1 3,-3l33,0m0,-1l-33,0a4,4 0 0 0 -4,4l0,33a4,4 0 0 0 4,4l33,0a4,4 0 0 0 4,-4l0,-33a4,4 0 0 0 -4,-4z"/>
<path stroke="#009688" fill="#ffffff" fill-opacity="0" fill-rule="nonzero" stroke-width="1.0" d="m27.20643,29.4739l-1.21802,0l1.21802,-2.14213l-0.05879,-2.52738l-1.2421,-2.12576l-1.49265,-1.21801l-0.67492,-1.37644l-1.92358,-3.53726l2.74073,1.79137l3.13505,0.21508l1.59421,0.98521l1.86307,-0.94935l0.48354,-2.63339l-1.98833,-1.11067l-1.91665,-2.97354l0.38526,-1.39456l-2.12286,-2.00915l0.82389,-3.09901l-1.3614,-0.12546l-1.80929,1.37933l-3.49313,0.32262l-3.31285,1.75417l-1.56681,2.10186l-1.48751,3.88146l-0.44245,4.81271l0.49516,5.78154l1.72009,1.13379l0.41331,3.06297l-1.1895,0l-1.27306,2.42948l-0.98712,3.8408l17.26695,0l-1.01931,-3.8408l-1.55895,-2.42948l0,0z"/>
<path stroke="#009688" stroke-dasharray="2,2" d="m23.61156,6.63793l-1.16183,2.2557l-1.89806,0.27415l-2.4104,1.37699l-1.46486,2.1687l-1.05714,3.57465l-0.28913,4.51495l0.30214,6.3633" fill-opacity="0" stroke-width="1.0" fill="none"/>
<line stroke="#009688" stroke-dasharray="2,2" y2="32.58856" x2="28.96602" y1="32.58856" x1="13.5093" fill-opacity="0" stroke-width="1.0" fill="none"/>
<line stroke="#009688" y2="29.47069" x2="25.98988" y1="29.47069" x1="16.06028" fill-opacity="0" stroke-width="1.0" fill="none"/>
<path stroke="#009688" stroke-dasharray="2,2" d="m27.94317,10.33418l-2.21519,0.02258l-0.32541,0.65079l2.39086,0.92312" fill-opacity="0" stroke-width="1.0" fill="none"/>
<path stroke="#009688" stroke-dasharray="2,2" d="m31.36607,16.48593l-1.47286,-0.34296l0.38557,-0.77114" stroke-width="1.0" fill-opacity="0" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "Bambulab",
"url": "http://www.bambulab.com/Parameters/vendor/BBL.json",
"version": "02.08.00.12",
"version": "02.08.00.13",
"force_update": "0",
"description": "the initial version of BBL configurations",
"machine_model_list": [
@@ -94,7 +94,7 @@
"prime_tower_flat_ironing": "0",
"raft_layers": "0",
"reduce_crossing_wall": "0",
"reduce_infill_retraction_mode": "Auto",
"reduce_infill_retraction": "1",
"resolution": "0.012",
"scarf_angle_threshold": "155",
"seam_position": "aligned",
+149 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Vivedino",
"version": "02.04.00.02",
"version": "02.04.00.13",
"force_update": "0",
"description": "Vivedino configurations",
"machine_model_list": [
@@ -11,6 +11,22 @@
{
"name": "Troodon 2.0 - RRF",
"sub_path": "machine/Troodon2RRF.json"
},
{
"name": "Xplorer Dual Gantry",
"sub_path": "machine/XplorerDualGantry.json"
},
{
"name": "Xplorer IDEX",
"sub_path": "machine/XplorerIDEX.json"
},
{
"name": "Xplorer IQEX",
"sub_path": "machine/XplorerIQEX.json"
},
{
"name": "Xplorer Single",
"sub_path": "machine/XplorerSingle.json"
}
],
"process_list": [
@@ -45,6 +61,86 @@
{
"name": "0.28mm Extra Draft @Troodon2",
"sub_path": "process/0.28mm Extra Draft @Troodon2.json"
},
{
"name": "fdm_process_xplorer_common",
"sub_path": "process/fdm_process_xplorer_common.json"
},
{
"name": "0.04mm Ultra Fine @Xplorer 0.4",
"sub_path": "process/0.04mm Ultra Fine @Xplorer 0.4.json"
},
{
"name": "0.08mm Extra Fine @Xplorer 0.4",
"sub_path": "process/0.08mm Extra Fine @Xplorer 0.4.json"
},
{
"name": "0.12mm Fine @Xplorer 0.4",
"sub_path": "process/0.12mm Fine @Xplorer 0.4.json"
},
{
"name": "0.16mm Optimal @Xplorer 0.4",
"sub_path": "process/0.16mm Optimal @Xplorer 0.4.json"
},
{
"name": "0.20mm Standard @Xplorer 0.4",
"sub_path": "process/0.20mm Standard @Xplorer 0.4.json"
},
{
"name": "0.28mm Draft @Xplorer 0.4",
"sub_path": "process/0.28mm Draft @Xplorer 0.4.json"
},
{
"name": "0.32mm Extra Draft @Xplorer 0.4",
"sub_path": "process/0.32mm Extra Draft @Xplorer 0.4.json"
},
{
"name": "fdm_process_xplorer_common_0_6",
"sub_path": "process/fdm_process_xplorer_common_0_6.json"
},
{
"name": "fdm_process_xplorer_common_0_8",
"sub_path": "process/fdm_process_xplorer_common_0_8.json"
},
{
"name": "0.18mm Fine @Xplorer 0.6",
"sub_path": "process/0.18mm Fine @Xplorer 0.6.json"
},
{
"name": "0.24mm Optimal @Xplorer 0.6",
"sub_path": "process/0.24mm Optimal @Xplorer 0.6.json"
},
{
"name": "0.30mm Standard @Xplorer 0.6",
"sub_path": "process/0.30mm Standard @Xplorer 0.6.json"
},
{
"name": "0.36mm Draft @Xplorer 0.6",
"sub_path": "process/0.36mm Draft @Xplorer 0.6.json"
},
{
"name": "0.42mm Extra Draft @Xplorer 0.6",
"sub_path": "process/0.42mm Extra Draft @Xplorer 0.6.json"
},
{
"name": "0.24mm Fine @Xplorer 0.8",
"sub_path": "process/0.24mm Fine @Xplorer 0.8.json"
},
{
"name": "0.32mm Optimal @Xplorer 0.8",
"sub_path": "process/0.32mm Optimal @Xplorer 0.8.json"
},
{
"name": "0.40mm Standard @Xplorer 0.8",
"sub_path": "process/0.40mm Standard @Xplorer 0.8.json"
},
{
"name": "0.48mm Draft @Xplorer 0.8",
"sub_path": "process/0.48mm Draft @Xplorer 0.8.json"
},
{
"name": "0.56mm Extra Draft @Xplorer 0.8",
"sub_path": "process/0.56mm Extra Draft @Xplorer 0.8.json"
}
],
"filament_list": [],
@@ -65,9 +161,61 @@
"name": "Troodon 2.0 Klipper 0.4 nozzle",
"sub_path": "machine/Troodon 2.0 Klipper 0.4 nozzle.json"
},
{
"name": "Xplorer common",
"sub_path": "machine/Xplorer common.json"
},
{
"name": "Troodon 2.0 RRF 0.4 nozzle",
"sub_path": "machine/Troodon 2.0 RRF 0.4 nozzle.json"
},
{
"name": "Xplorer Dual Gantry 0.4 nozzle",
"sub_path": "machine/Xplorer Dual Gantry 0.4 nozzle.json"
},
{
"name": "Xplorer Dual Gantry 0.6 nozzle",
"sub_path": "machine/Xplorer Dual Gantry 0.6 nozzle.json"
},
{
"name": "Xplorer Dual Gantry 0.8 nozzle",
"sub_path": "machine/Xplorer Dual Gantry 0.8 nozzle.json"
},
{
"name": "Xplorer IDEX 0.4 nozzle",
"sub_path": "machine/Xplorer IDEX 0.4 nozzle.json"
},
{
"name": "Xplorer IDEX 0.6 nozzle",
"sub_path": "machine/Xplorer IDEX 0.6 nozzle.json"
},
{
"name": "Xplorer IDEX 0.8 nozzle",
"sub_path": "machine/Xplorer IDEX 0.8 nozzle.json"
},
{
"name": "Xplorer IQEX 0.4 nozzle",
"sub_path": "machine/Xplorer IQEX 0.4 nozzle.json"
},
{
"name": "Xplorer IQEX 0.6 nozzle",
"sub_path": "machine/Xplorer IQEX 0.6 nozzle.json"
},
{
"name": "Xplorer IQEX 0.8 nozzle",
"sub_path": "machine/Xplorer IQEX 0.8 nozzle.json"
},
{
"name": "Xplorer Single 0.4 nozzle",
"sub_path": "machine/Xplorer Single 0.4 nozzle.json"
},
{
"name": "Xplorer Single 0.6 nozzle",
"sub_path": "machine/Xplorer Single 0.6 nozzle.json"
},
{
"name": "Xplorer Single 0.8 nozzle",
"sub_path": "machine/Xplorer Single 0.8 nozzle.json"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Vivedino Xplorer Dual Gantry build plate. Dashed red outlines mark real reach
limits; the viewBox matches printable_area so the texture is not stretched.
Print zones are drawn by the slicer per plate, not by this texture. -->
<svg
width="400mm"
height="342.5mm"
viewBox="0 0 400 342.5"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1">
<rect
style="fill:none;stroke:#ff0e0e;stroke-width:1;stroke-dasharray:1, 2;stroke-dashoffset:0;stroke-opacity:1"
id="bound-rear"
width="400"
height="26"
x="0"
y="0" />
<rect
style="fill:none;stroke:#ff0e0e;stroke-width:1;stroke-dasharray:1, 2;stroke-dashoffset:0;stroke-opacity:1"
id="bound-front"
width="400"
height="77.5"
x="0"
y="265" />
<g
id="text3-6"
style="opacity:1">
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;fill:#ff0000;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="M 22.60772,9.2135632 H 19.221048 V 5.4247238 h -3.831173 v 3.7888394 h -2.582337 v 0.1693336 l 1.058335,2.7516712 h 1.524002 v 8.699514 h 3.831173 v -8.699514 h 3.386672 z m 1.227659,1.1006688 v 6.096009 c 0,2.878672 1.375835,4.423841 4.762507,4.423841 h 8.805348 c 3.407838,0 4.762507,-1.545169 4.762507,-4.423841 v -6.096009 c 0,-3.0903387 -1.354669,-4.8895082 -4.762507,-4.8895082 h -8.805348 c -3.386672,0 -4.762507,1.7991695 -4.762507,4.8895082 z m 4.191006,0.656167 c 0,-1.2700017 0.592668,-1.820336 1.566336,-1.820336 h 6.582844 c 1.227669,0 1.799169,0.6138344 1.799169,1.98967 v 3.915839 c 0,1.333503 -0.5715,1.905003 -1.799169,1.905003 h -6.582844 c -1.227668,0 -1.566336,-0.719667 -1.566336,-2.159003 z"
id="path25" />
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 14.890625,4.9238281 v 3.7890625 h -2.583984 v 0.7636719 l 1.216797,3.1582035 h 1.367187 v 8.699218 h 4.830078 v -8.699218 h 3.386719 V 8.7128906 H 19.720703 V 4.9238281 Z m 13.707031,0 c -1.781877,0 -3.138417,0.4891086 -4.019531,1.4492188 -0.881114,0.9601101 -1.242188,2.322036 -1.242188,3.9414061 v 6.095703 c 0,1.518993 0.370925,2.78417 1.261719,3.650391 0.890794,0.86622 2.230623,1.273437 4,1.273437 h 8.804688 c 1.779488,0 3.121173,-0.408321 4.009765,-1.27539 0.888593,-0.86707 1.253907,-2.13067 1.253907,-3.648438 v -6.095703 c 0,-1.6182286 -0.355554,-2.9804801 -1.234375,-3.9414061 -0.878822,-0.960926 -2.237387,-1.4492188 -4.029297,-1.4492188 z m -12.707031,1 h 2.830078 v 3.7890625 h 3.386719 v 1.9218754 h -3.386719 v 8.699218 H 15.890625 V 11.634766 H 14.208984 L 13.46875,9.7128906 h 2.421875 z m 12.707031,0 h 8.804688 c 1.615927,0 2.640543,0.4137566 3.291015,1.125 0.650473,0.7112434 0.972657,1.793515 0.972657,3.2656249 v 6.095703 c 0,1.360905 -0.312424,2.308411 -0.953125,2.933594 -0.640702,0.625183 -1.682198,0.990234 -3.310547,0.990234 h -8.804688 c -1.617294,0 -2.658943,-0.364202 -3.302734,-0.990234 -0.643792,-0.626032 -0.958984,-1.573915 -0.958985,-2.933594 v -6.095703 c 0,-1.4709684 0.325044,-2.5535656 0.978516,-3.2656249 0.653472,-0.7120593 1.678409,-1.125 3.283203,-1.125 z m 0.994141,2.7265625 c -0.581037,0 -1.137457,0.1802682 -1.517578,0.5976563 -0.380122,0.4173881 -0.546875,1.0098771 -0.546875,1.7226561 v 3.830078 c 0,0.758884 0.06928,1.390241 0.404297,1.896485 0.335014,0.506243 0.948621,0.763671 1.660156,0.763671 h 6.583984 c 0.691421,0 1.302744,-0.170803 1.720703,-0.607421 0.417959,-0.436618 0.578125,-1.060649 0.578125,-1.798828 v -3.914063 c 0,-0.757148 -0.157083,-1.3938421 -0.570312,-1.8476562 C 37.491068,8.8391546 36.872949,8.6503906 36.175781,8.6503906 Z m 0,1 h 6.583984 c 0.530501,0 0.810796,0.1173912 0.990235,0.3144532 0.179438,0.1970622 0.308593,0.5570942 0.308593,1.1757812 v 3.914063 c 0,0.595323 -0.126073,0.924914 -0.300781,1.107421 -0.174708,0.182508 -0.461798,0.298828 -0.998047,0.298828 h -6.583984 c -0.516133,0 -0.684935,-0.102981 -0.826172,-0.316406 -0.141237,-0.213424 -0.238281,-0.663298 -0.238281,-1.34375 v -3.830078 c 0,-0.557223 0.12748,-0.875502 0.287109,-1.0507811 0.15963,-0.1752795 0.384713,-0.2695313 0.777344,-0.2695313 z"
id="path26" />
</g>
<g
transform="translate(-356.014,288.234)">
<g
id="text3-8"
style="opacity:1">
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;fill:#ff0000;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 378.62465,9.2135632 h -3.38667 V 5.4247238 h -3.83117 v 3.7888394 h -2.58234 v 0.1693336 l 1.05834,2.7516712 h 1.524 v 8.699514 h 3.83117 v -8.699514 h 3.38667 z m 3.70417,-0.0635 c 0.74083,0 0.97367,0.2751672 0.97367,1.0795018 v 10.604517 h 4.191 V 9.2770633 c 0,-2.7940045 -0.762,-3.8523395 -3.70417,-3.8523395 h -4.65667 v 0.1693336 l 1.37583,3.5560056 z"
id="path23" />
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 370.90625,4.9238281 v 3.7890625 h -2.58203 v 0.7636719 l 1.21484,3.1582035 h 1.36719 v 8.699218 h 4.83203 V 12.634766 H 379.125 V 8.7128906 h -3.38672 V 4.9238281 Z m 7.72656,0 V 5.6875 l 1.53321,3.9628906 h 2.1621 c 0.30851,0 0.37476,0.050297 0.39258,0.070312 0.0178,0.020016 0.082,0.1558452 0.082,0.5097654 v 11.103515 h 5.19141 V 9.2773438 c 0,-1.4424568 -0.18358,-2.5460127 -0.87891,-3.3105469 -0.69533,-0.7645343 -1.79513,-1.0429688 -3.32617,-1.0429688 z m -6.72656,1 h 2.83203 v 3.7890625 h 3.38672 v 1.9218754 h -3.38672 v 8.699218 h -2.83203 v -8.699218 h -1.67969 l -0.74023,-1.9218754 h 2.41992 z m 7.89062,0 h 3.99219 c 1.41114,0 2.16473,0.2517094 2.58594,0.7148438 0.42121,0.4631344 0.61914,1.2871241 0.61914,2.6386719 V 20.333984 h -3.19141 V 10.230469 c 0,-0.4504146 -0.0521,-0.85713 -0.33593,-1.1757815 -0.2838,-0.3186515 -0.70635,-0.4042969 -1.13868,-0.4042969 h -1.47656 z"
id="path24" />
</g>
</g>
<g
transform="translate(0.020506,27.6309)">
<g
id="text6-8"
style="opacity:1">
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;fill:#ff0000;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 198.87817,149.53483 h 2.20134 v -4.826 h 4.80484 v -2.18017 h -4.80484 v -4.82601 h -2.20134 v 4.82601 h -4.80484 v 2.18017 h 4.80484 z"
id="path17" />
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 198.37891,137.20312 v 4.82618 h -4.80469 v 3.17968 h 4.80469 v 4.82618 h 3.20117 v -4.82618 h 4.80469 v -3.17968 h -4.80469 v -4.82618 z m 1,1 h 1.20117 v 4.82618 h 4.80469 v 1.17968 h -4.80469 v 4.82618 h -1.20117 v -4.82618 h -4.80469 v -1.17968 h 4.80469 z"
id="path18" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.9 KiB

@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Vivedino Xplorer IDEX build plate. Dashed red outlines mark real reach
limits; the viewBox matches printable_area so the texture is not stretched.
Print zones are drawn by the slicer per plate, not by this texture. -->
<svg
width="400mm"
height="400mm"
viewBox="0 0 400 400"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1">
<rect
style="fill:none;stroke:#ff0e0e;stroke-width:1;stroke-dasharray:1, 2;stroke-dashoffset:0;stroke-opacity:1"
id="bound-left"
width="66"
height="400"
x="0"
y="0" />
<rect
style="fill:none;stroke:#ff0e0e;stroke-width:1;stroke-dasharray:1, 2;stroke-dashoffset:0;stroke-opacity:1"
id="bound-right"
width="68"
height="400"
x="332"
y="0" />
<g
transform="translate(0,345.734)">
<g
id="text3-6"
style="opacity:1">
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;fill:#ff0000;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="M 22.60772,9.2135632 H 19.221048 V 5.4247238 h -3.831173 v 3.7888394 h -2.582337 v 0.1693336 l 1.058335,2.7516712 h 1.524002 v 8.699514 h 3.831173 v -8.699514 h 3.386672 z m 1.227659,1.1006688 v 6.096009 c 0,2.878672 1.375835,4.423841 4.762507,4.423841 h 8.805348 c 3.407838,0 4.762507,-1.545169 4.762507,-4.423841 v -6.096009 c 0,-3.0903387 -1.354669,-4.8895082 -4.762507,-4.8895082 h -8.805348 c -3.386672,0 -4.762507,1.7991695 -4.762507,4.8895082 z m 4.191006,0.656167 c 0,-1.2700017 0.592668,-1.820336 1.566336,-1.820336 h 6.582844 c 1.227669,0 1.799169,0.6138344 1.799169,1.98967 v 3.915839 c 0,1.333503 -0.5715,1.905003 -1.799169,1.905003 h -6.582844 c -1.227668,0 -1.566336,-0.719667 -1.566336,-2.159003 z"
id="path25" />
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 14.890625,4.9238281 v 3.7890625 h -2.583984 v 0.7636719 l 1.216797,3.1582035 h 1.367187 v 8.699218 h 4.830078 v -8.699218 h 3.386719 V 8.7128906 H 19.720703 V 4.9238281 Z m 13.707031,0 c -1.781877,0 -3.138417,0.4891086 -4.019531,1.4492188 -0.881114,0.9601101 -1.242188,2.322036 -1.242188,3.9414061 v 6.095703 c 0,1.518993 0.370925,2.78417 1.261719,3.650391 0.890794,0.86622 2.230623,1.273437 4,1.273437 h 8.804688 c 1.779488,0 3.121173,-0.408321 4.009765,-1.27539 0.888593,-0.86707 1.253907,-2.13067 1.253907,-3.648438 v -6.095703 c 0,-1.6182286 -0.355554,-2.9804801 -1.234375,-3.9414061 -0.878822,-0.960926 -2.237387,-1.4492188 -4.029297,-1.4492188 z m -12.707031,1 h 2.830078 v 3.7890625 h 3.386719 v 1.9218754 h -3.386719 v 8.699218 H 15.890625 V 11.634766 H 14.208984 L 13.46875,9.7128906 h 2.421875 z m 12.707031,0 h 8.804688 c 1.615927,0 2.640543,0.4137566 3.291015,1.125 0.650473,0.7112434 0.972657,1.793515 0.972657,3.2656249 v 6.095703 c 0,1.360905 -0.312424,2.308411 -0.953125,2.933594 -0.640702,0.625183 -1.682198,0.990234 -3.310547,0.990234 h -8.804688 c -1.617294,0 -2.658943,-0.364202 -3.302734,-0.990234 -0.643792,-0.626032 -0.958984,-1.573915 -0.958985,-2.933594 v -6.095703 c 0,-1.4709684 0.325044,-2.5535656 0.978516,-3.2656249 0.653472,-0.7120593 1.678409,-1.125 3.283203,-1.125 z m 0.994141,2.7265625 c -0.581037,0 -1.137457,0.1802682 -1.517578,0.5976563 -0.380122,0.4173881 -0.546875,1.0098771 -0.546875,1.7226561 v 3.830078 c 0,0.758884 0.06928,1.390241 0.404297,1.896485 0.335014,0.506243 0.948621,0.763671 1.660156,0.763671 h 6.583984 c 0.691421,0 1.302744,-0.170803 1.720703,-0.607421 0.417959,-0.436618 0.578125,-1.060649 0.578125,-1.798828 v -3.914063 c 0,-0.757148 -0.157083,-1.3938421 -0.570312,-1.8476562 C 37.491068,8.8391546 36.872949,8.6503906 36.175781,8.6503906 Z m 0,1 h 6.583984 c 0.530501,0 0.810796,0.1173912 0.990235,0.3144532 0.179438,0.1970622 0.308593,0.5570942 0.308593,1.1757812 v 3.914063 c 0,0.595323 -0.126073,0.924914 -0.300781,1.107421 -0.174708,0.182508 -0.461798,0.298828 -0.998047,0.298828 h -6.583984 c -0.516133,0 -0.684935,-0.102981 -0.826172,-0.316406 -0.141237,-0.213424 -0.238281,-0.663298 -0.238281,-1.34375 v -3.830078 c 0,-0.557223 0.12748,-0.875502 0.287109,-1.0507811 0.15963,-0.1752795 0.384713,-0.2695313 0.777344,-0.2695313 z"
id="path26" />
</g>
</g>
<g
transform="translate(0,345.734)">
<g
id="text3-8"
style="opacity:1">
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;fill:#ff0000;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 378.62465,9.2135632 h -3.38667 V 5.4247238 h -3.83117 v 3.7888394 h -2.58234 v 0.1693336 l 1.05834,2.7516712 h 1.524 v 8.699514 h 3.83117 v -8.699514 h 3.38667 z m 3.70417,-0.0635 c 0.74083,0 0.97367,0.2751672 0.97367,1.0795018 v 10.604517 h 4.191 V 9.2770633 c 0,-2.7940045 -0.762,-3.8523395 -3.70417,-3.8523395 h -4.65667 v 0.1693336 l 1.37583,3.5560056 z"
id="path23" />
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 370.90625,4.9238281 v 3.7890625 h -2.58203 v 0.7636719 l 1.21484,3.1582035 h 1.36719 v 8.699218 h 4.83203 V 12.634766 H 379.125 V 8.7128906 h -3.38672 V 4.9238281 Z m 7.72656,0 V 5.6875 l 1.53321,3.9628906 h 2.1621 c 0.30851,0 0.37476,0.050297 0.39258,0.070312 0.0178,0.020016 0.082,0.1558452 0.082,0.5097654 v 11.103515 h 5.19141 V 9.2773438 c 0,-1.4424568 -0.18358,-2.5460127 -0.87891,-3.3105469 -0.69533,-0.7645343 -1.79513,-1.0429688 -3.32617,-1.0429688 z m -6.72656,1 h 2.83203 v 3.7890625 h 3.38672 v 1.9218754 h -3.38672 v 8.699218 h -2.83203 v -8.699218 h -1.67969 l -0.74023,-1.9218754 h 2.41992 z m 7.89062,0 h 3.99219 c 1.41114,0 2.16473,0.2517094 2.58594,0.7148438 0.42121,0.4631344 0.61914,1.2871241 0.61914,2.6386719 V 20.333984 h -3.19141 V 10.230469 c 0,-0.4504146 -0.0521,-0.85713 -0.33593,-1.1757815 -0.2838,-0.3186515 -0.70635,-0.4042969 -1.13868,-0.4042969 h -1.47656 z"
id="path24" />
</g>
</g>
<g
transform="translate(0.020506,56.3809)">
<g
id="text6-8"
style="opacity:1">
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;fill:#ff0000;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 198.87817,149.53483 h 2.20134 v -4.826 h 4.80484 v -2.18017 h -4.80484 v -4.82601 h -2.20134 v 4.82601 h -4.80484 v 2.18017 h 4.80484 z"
id="path17" />
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 198.37891,137.20312 v 4.82618 h -4.80469 v 3.17968 h 4.80469 v 4.82618 h 3.20117 v -4.82618 h 4.80469 v -3.17968 h -4.80469 v -4.82618 z m 1,1 h 1.20117 v 4.82618 h 4.80469 v 1.17968 h -4.80469 v 4.82618 h -1.20117 v -4.82618 h -4.80469 v -1.17968 h 4.80469 z"
id="path18" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.0 KiB

@@ -0,0 +1,154 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Vivedino Xplorer IQEX build plate. Dashed red outlines mark real reach
limits; the viewBox matches printable_area so the texture is not stretched.
Print zones are drawn by the slicer per plate, not by this texture. -->
<svg
width="400mm"
height="342.5mm"
viewBox="0 0 400 342.5"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<g
id="layer1">
<rect
style="fill:none;stroke:#ff0e0e;stroke-width:1;stroke-dasharray:1, 2;stroke-dashoffset:0;stroke-opacity:1"
id="bound-left"
width="66"
height="342.5"
x="0"
y="0" />
<rect
style="fill:none;stroke:#ff0e0e;stroke-width:1;stroke-dasharray:1, 2;stroke-dashoffset:0;stroke-opacity:1"
id="bound-right"
width="68"
height="342.5"
x="332"
y="0" />
<rect
style="fill:none;stroke:#ff0e0e;stroke-width:1;stroke-dasharray:1, 2;stroke-dashoffset:0;stroke-opacity:1"
id="bound-rear"
width="400"
height="26"
x="0"
y="0" />
<rect
style="fill:none;stroke:#ff0e0e;stroke-width:1;stroke-dasharray:1, 2;stroke-dashoffset:0;stroke-opacity:1"
id="bound-front"
width="400"
height="77.5"
x="0"
y="265" />
<g
id="text3-6"
style="opacity:1">
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;fill:#ff0000;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="M 22.60772,9.2135632 H 19.221048 V 5.4247238 h -3.831173 v 3.7888394 h -2.582337 v 0.1693336 l 1.058335,2.7516712 h 1.524002 v 8.699514 h 3.831173 v -8.699514 h 3.386672 z m 1.227659,1.1006688 v 6.096009 c 0,2.878672 1.375835,4.423841 4.762507,4.423841 h 8.805348 c 3.407838,0 4.762507,-1.545169 4.762507,-4.423841 v -6.096009 c 0,-3.0903387 -1.354669,-4.8895082 -4.762507,-4.8895082 h -8.805348 c -3.386672,0 -4.762507,1.7991695 -4.762507,4.8895082 z m 4.191006,0.656167 c 0,-1.2700017 0.592668,-1.820336 1.566336,-1.820336 h 6.582844 c 1.227669,0 1.799169,0.6138344 1.799169,1.98967 v 3.915839 c 0,1.333503 -0.5715,1.905003 -1.799169,1.905003 h -6.582844 c -1.227668,0 -1.566336,-0.719667 -1.566336,-2.159003 z"
id="path25" />
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 14.890625,4.9238281 v 3.7890625 h -2.583984 v 0.7636719 l 1.216797,3.1582035 h 1.367187 v 8.699218 h 4.830078 v -8.699218 h 3.386719 V 8.7128906 H 19.720703 V 4.9238281 Z m 13.707031,0 c -1.781877,0 -3.138417,0.4891086 -4.019531,1.4492188 -0.881114,0.9601101 -1.242188,2.322036 -1.242188,3.9414061 v 6.095703 c 0,1.518993 0.370925,2.78417 1.261719,3.650391 0.890794,0.86622 2.230623,1.273437 4,1.273437 h 8.804688 c 1.779488,0 3.121173,-0.408321 4.009765,-1.27539 0.888593,-0.86707 1.253907,-2.13067 1.253907,-3.648438 v -6.095703 c 0,-1.6182286 -0.355554,-2.9804801 -1.234375,-3.9414061 -0.878822,-0.960926 -2.237387,-1.4492188 -4.029297,-1.4492188 z m -12.707031,1 h 2.830078 v 3.7890625 h 3.386719 v 1.9218754 h -3.386719 v 8.699218 H 15.890625 V 11.634766 H 14.208984 L 13.46875,9.7128906 h 2.421875 z m 12.707031,0 h 8.804688 c 1.615927,0 2.640543,0.4137566 3.291015,1.125 0.650473,0.7112434 0.972657,1.793515 0.972657,3.2656249 v 6.095703 c 0,1.360905 -0.312424,2.308411 -0.953125,2.933594 -0.640702,0.625183 -1.682198,0.990234 -3.310547,0.990234 h -8.804688 c -1.617294,0 -2.658943,-0.364202 -3.302734,-0.990234 -0.643792,-0.626032 -0.958984,-1.573915 -0.958985,-2.933594 v -6.095703 c 0,-1.4709684 0.325044,-2.5535656 0.978516,-3.2656249 0.653472,-0.7120593 1.678409,-1.125 3.283203,-1.125 z m 0.994141,2.7265625 c -0.581037,0 -1.137457,0.1802682 -1.517578,0.5976563 -0.380122,0.4173881 -0.546875,1.0098771 -0.546875,1.7226561 v 3.830078 c 0,0.758884 0.06928,1.390241 0.404297,1.896485 0.335014,0.506243 0.948621,0.763671 1.660156,0.763671 h 6.583984 c 0.691421,0 1.302744,-0.170803 1.720703,-0.607421 0.417959,-0.436618 0.578125,-1.060649 0.578125,-1.798828 v -3.914063 c 0,-0.757148 -0.157083,-1.3938421 -0.570312,-1.8476562 C 37.491068,8.8391546 36.872949,8.6503906 36.175781,8.6503906 Z m 0,1 h 6.583984 c 0.530501,0 0.810796,0.1173912 0.990235,0.3144532 0.179438,0.1970622 0.308593,0.5570942 0.308593,1.1757812 v 3.914063 c 0,0.595323 -0.126073,0.924914 -0.300781,1.107421 -0.174708,0.182508 -0.461798,0.298828 -0.998047,0.298828 h -6.583984 c -0.516133,0 -0.684935,-0.102981 -0.826172,-0.316406 -0.141237,-0.213424 -0.238281,-0.663298 -0.238281,-1.34375 v -3.830078 c 0,-0.557223 0.12748,-0.875502 0.287109,-1.0507811 0.15963,-0.1752795 0.384713,-0.2695313 0.777344,-0.2695313 z"
id="path26" />
</g>
<g
id="text3-8"
style="opacity:1">
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;fill:#ff0000;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 378.62465,9.2135632 h -3.38667 V 5.4247238 h -3.83117 v 3.7888394 h -2.58234 v 0.1693336 l 1.05834,2.7516712 h 1.524 v 8.699514 h 3.83117 v -8.699514 h 3.38667 z m 3.70417,-0.0635 c 0.74083,0 0.97367,0.2751672 0.97367,1.0795018 v 10.604517 h 4.191 V 9.2770633 c 0,-2.7940045 -0.762,-3.8523395 -3.70417,-3.8523395 h -4.65667 v 0.1693336 l 1.37583,3.5560056 z"
id="path23" />
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 370.90625,4.9238281 v 3.7890625 h -2.58203 v 0.7636719 l 1.21484,3.1582035 h 1.36719 v 8.699218 h 4.83203 V 12.634766 H 379.125 V 8.7128906 h -3.38672 V 4.9238281 Z m 7.72656,0 V 5.6875 l 1.53321,3.9628906 h 2.1621 c 0.30851,0 0.37476,0.050297 0.39258,0.070312 0.0178,0.020016 0.082,0.1558452 0.082,0.5097654 v 11.103515 h 5.19141 V 9.2773438 c 0,-1.4424568 -0.18358,-2.5460127 -0.87891,-3.3105469 -0.69533,-0.7645343 -1.79513,-1.0429688 -3.32617,-1.0429688 z m -6.72656,1 h 2.83203 v 3.7890625 h 3.38672 v 1.9218754 h -3.38672 v 8.699218 h -2.83203 v -8.699218 h -1.67969 l -0.74023,-1.9218754 h 2.41992 z m 7.89062,0 h 3.99219 c 1.41114,0 2.16473,0.2517094 2.58594,0.7148438 0.42121,0.4631344 0.61914,1.2871241 0.61914,2.6386719 V 20.333984 h -3.19141 V 10.230469 c 0,-0.4504146 -0.0521,-0.85713 -0.33593,-1.1757815 -0.2838,-0.3186515 -0.70635,-0.4042969 -1.13868,-0.4042969 h -1.47656 z"
id="path24" />
</g>
<g
id="text3"
style="opacity:1">
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;fill:#ff0000;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 22.61014,297.44787 h -3.386672 v -3.78884 h -3.831173 v 3.78884 h -2.582337 v 0.16933 l 1.058335,2.75168 h 1.524002 v 8.69951 h 3.831173 v -8.69951 h 3.386672 z m 18.944187,7.81051 h -8.001013 c -1.227669,0 -2.286004,0.0212 -4.064006,0.127 v -0.14816 c 0.952501,-0.23284 1.735669,-0.46567 3.810006,-1.143 l 5.969009,-1.92617 c 1.98967,-0.63501 2.455337,-1.35467 2.455337,-3.47134 v -1.54517 c 0,-2.09551 -0.952501,-3.49251 -3.661839,-3.49251 H 23.753132 v 0.16934 l 1.312335,3.429 h 11.408852 c 0.698501,0 0.867834,0.27517 0.867834,0.78317 v 0.5715 c 0,0.55033 -0.211667,0.8255 -0.825501,1.03717 l -11.408851,3.89467 c -0.783168,0.27517 -1.016002,0.61384 -1.016002,1.56634 v 3.19617 c 0,0.55033 0.254,0.762 0.592668,0.762 h 16.86986 z"
id="path29" />
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 14.892578,293.1582 v 3.78907 h -2.582031 v 0.76367 l 1.214844,3.1582 h 1.367187 v 8.69922 h 4.830078 v -8.69922 h 3.386719 v -3.92187 h -3.386719 v -3.78907 z m 8.361328,0 v 0.76172 l 1.466797,3.83789 h 11.753906 c 0.283594,0 0.342775,0.0558 0.337891,0.0508 -0.0049,-0.005 0.0293,0.034 0.0293,0.23243 v 0.57031 c 0,0.21106 -0.03003,0.28002 -0.07422,0.33594 -0.04419,0.0559 -0.153527,0.13867 -0.414062,0.22851 l -11.410157,3.89649 h -0.002 c -0.437489,0.15371 -0.814289,0.36283 -1.052734,0.72656 -0.238445,0.36373 -0.296875,0.79355 -0.296875,1.31054 v 3.19727 c 0,0.35209 0.0826,0.67409 0.292969,0.91602 0.210369,0.24192 0.525326,0.3457 0.798828,0.3457 h 17.371093 v -4.81055 h -8.501953 c -0.243624,0 -0.570664,0.0232 -0.808593,0.0254 0.305142,-0.0978 0.326312,-0.0879 0.708984,-0.21289 l 5.966797,-1.92578 c 1.036192,-0.3307 1.774785,-0.71448 2.232422,-1.37109 0.457636,-0.65661 0.572265,-1.48117 0.572265,-2.57617 v -1.54493 c 0,-1.12351 -0.260337,-2.14285 -0.953125,-2.87695 -0.692788,-0.7341 -1.768378,-1.11719 -3.208984,-1.11719 z m -7.361328,1 h 2.830078 v 3.78907 h 3.386719 v 1.92187 h -3.386719 v 8.69922 h -2.830078 v -8.69922 h -1.68164 l -0.738282,-1.92187 h 2.419922 z m 8.523438,0 H 38.0625 c 1.268732,0 2.021625,0.31641 2.482422,0.80469 0.460797,0.48828 0.679687,1.21746 0.679687,2.18945 v 1.54493 c 0,1.02166 -0.119963,1.61276 -0.392578,2.0039 -0.272614,0.39114 -0.761365,0.68593 -1.714844,0.99024 h -0.002 l -5.970703,1.92773 c -2.071176,0.6763 -2.834211,0.90322 -3.773437,1.13281 l -0.38086,0.0918 v 1.07227 l 0.529297,-0.0312 c 1.770212,-0.10534 2.811291,-0.12696 4.033203,-0.12696 h 7.501953 v 2.81055 H 24.683594 c -0.06517,0 -0.04362,-4.6e-4 -0.04492,-0.002 -0.0013,-0.002 -0.04687,-0.0615 -0.04687,-0.25977 v -3.19727 c 0,-0.4355 0.05905,-0.64919 0.132812,-0.76171 0.07376,-0.11253 0.20315,-0.21058 0.548828,-0.33204 l 11.40625,-3.89257 v -0.002 c 0.353299,-0.12183 0.657948,-0.28056 0.873047,-0.55273 0.215099,-0.27217 0.289063,-0.61776 0.289063,-0.95703 v -0.57031 c 0,-0.30956 -0.05028,-0.66208 -0.314453,-0.93165 -0.264176,-0.26956 -0.637827,-0.35156 -1.052735,-0.35156 h -11.06446 z"
id="path30" />
</g>
<g
id="text3-1"
style="opacity:1">
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;fill:#ff0000;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 368.78211,297.97579 h -3.38667 v -3.78883 h -3.83117 v 3.78883 h -2.58234 v 0.16934 l 1.05833,2.75167 h 1.52401 v 8.69951 h 3.83117 v -8.69951 h 3.38667 z m 13.52551,-0.27516 c 0.61384,0 0.889,0.254 0.889,0.762 v 0.74083 c 0,0.5715 -0.254,0.91017 -0.91016,0.91017 h -10.11769 v 0.16934 l 0.99484,2.70933 h 9.05935 c 0.6985,0 1.03716,0.29634 1.03716,0.9525 v 0.76201 c 0,0.71966 -0.29633,1.05833 -1.16416,1.05833 h -11.74752 v 3.83117 h 13.03868 c 3.00568,0 4.10634,-1.50283 4.10634,-4.064 v -1.16417 c 0,-1.3335 -0.6985,-2.30717 -1.56633,-2.73051 0.635,-0.46566 1.22767,-1.22766 1.22767,-2.56117 v -1.29117 c 0,-2.159 -1.03717,-3.59833 -3.89468,-3.59833 h -13.50435 v 0.16933 l 1.35467,3.34434 z"
id="path27" />
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 361.06445,293.6875 v 3.78906 h -2.58203 v 0.76172 l 1.21485,3.1582 h 1.36718 v 8.69922 h 4.83008 v -8.69922 h 3.38672 v -3.91992 h -3.38672 v -3.78906 z m 8.19141,0 v 0.76562 l 1.51758,3.74805 h 11.53515 c 0.23796,0 0.328,0.051 0.34571,0.0664 0.0177,0.0154 0.043,0.0316 0.043,0.19531 v 0.74023 c 0,0.21168 -0.0464,0.29622 -0.0762,0.32813 -0.0297,0.0319 -0.0919,0.082 -0.33398,0.082 h -10.61914 v 0.75781 l 1.14648,3.1211 h 9.40821 c 0.27854,0 0.40023,0.0587 0.44531,0.0996 0.0451,0.0409 0.0918,0.11145 0.0918,0.35351 v 0.76172 c 0,0.2912 -0.0525,0.39055 -0.10157,0.4375 -0.0491,0.047 -0.19362,0.12109 -0.5625,0.12109 h -12.24804 v 4.83008 h 13.53906 c 1.58617,0 2.77621,-0.40668 3.54492,-1.22851 0.76871,-0.82184 1.0625,-1.98178 1.0625,-3.33399 v -1.16601 c 0,-1.24687 -0.61226,-2.15723 -1.39258,-2.75782 0.56011,-0.56303 1.05274,-1.33541 1.05274,-2.5332 v -1.29101 c 0,-1.15957 -0.2836,-2.20926 -1.01758,-2.95899 -0.73398,-0.74972 -1.86425,-1.13867 -3.37695,-1.13867 z m -7.19141,1 h 2.83008 v 3.78906 h 3.38672 v 1.91992 h -3.38672 v 8.69922 h -2.83008 v -8.69922 h -1.68164 l -0.73828,-1.91992 h 2.41992 z m 8.36524,0 h 12.83008 c 1.3448,0 2.16312,0.3282 2.6621,0.83789 0.49899,0.50969 0.73243,1.26033 0.73243,2.25977 v 1.29101 c 0,1.18536 -0.47856,1.75863 -1.02344,2.1582 l -0.66797,0.49024 0.74414,0.36328 c 0.69133,0.33724 1.28711,1.1204 1.28711,2.2793 v 1.16601 c 0,1.20896 -0.25798,2.08052 -0.79102,2.65039 -0.53303,0.56988 -1.39689,0.91211 -2.8164,0.91211 h -12.53906 v -2.83008 h 11.24804 c 0.49896,0 0.93787,-0.0961 1.25391,-0.39843 0.31604,-0.3023 0.41016,-0.7317 0.41016,-1.16016 v -0.76172 c 0,-0.4141 -0.12105,-0.82246 -0.41993,-1.09375 -0.29887,-0.27129 -0.69723,-0.35937 -1.11718,-0.35937 h -8.71094 l -0.68945,-1.87891 h 9.46484 c 0.4141,0 0.80511,-0.12009 1.06641,-0.40039 0.26129,-0.2803 0.34375,-0.64994 0.34375,-1.00977 v -0.74023 c 0,-0.34427 -0.11339,-0.71067 -0.38672,-0.94922 -0.27333,-0.23854 -0.62607,-0.3125 -1.00196,-0.3125 h -10.86132 z"
id="path28" />
</g>
<g
id="text6-84"
style="opacity:1">
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;fill:#ff0000;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 198.87817,19.323245 h 2.20134 v -4.826008 h 4.80484 v -2.18017 h -4.80484 V 7.4910596 h -2.20134 v 4.8260074 h -4.80484 v 2.18017 h 4.80484 z"
id="path13" />
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 198.37891,6.9902344 v 4.8261716 h -4.80469 v 3.181641 h 4.80469 v 4.826172 h 3.20117 v -4.826172 h 4.80469 v -3.181641 h -4.80469 V 6.9902344 Z m 1,1 h 1.20117 v 4.8261716 h 4.80469 v 1.181641 h -4.80469 v 4.826172 h -1.20117 v -4.826172 h -4.80469 v -1.181641 h 4.80469 z"
id="path14" />
</g>
<g
id="text6"
style="opacity:1">
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;fill:#ff0000;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 28.866225,149.41589 h 2.201337 v -4.82601 h 4.804841 v -2.18017 h -4.804841 v -4.826 h -2.201337 v 4.826 h -4.804841 v 2.18017 h 4.804841 z"
id="path21" />
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 28.367187,137.08398 v 4.82618 h -4.80664 v 3.17968 h 4.80664 v 4.82618 h 3.201172 v -4.82618 h 4.804688 v -3.17968 h -4.804688 v -4.82618 z m 1,1 h 1.201172 v 4.82618 h 4.804688 v 1.17968 h -4.804688 v 4.82618 h -1.201172 v -4.82618 h -4.80664 v -1.17968 h 4.80664 z"
id="path22" />
</g>
<g
id="text6-8"
style="opacity:1">
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;fill:#ff0000;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 198.87817,149.53483 h 2.20134 v -4.826 h 4.80484 v -2.18017 h -4.80484 v -4.82601 h -2.20134 v 4.82601 h -4.80484 v 2.18017 h 4.80484 z"
id="path17" />
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 198.37891,137.20312 v 4.82618 h -4.80469 v 3.17968 h 4.80469 v 4.82618 h 3.20117 v -4.82618 h 4.80469 v -3.17968 h -4.80469 v -4.82618 z m 1,1 h 1.20117 v 4.82618 h 4.80469 v 1.17968 h -4.80469 v 4.82618 h -1.20117 v -4.82618 h -4.80469 v -1.17968 h 4.80469 z"
id="path18" />
</g>
<g
id="text6-3"
style="opacity:1">
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;fill:#ff0000;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 368.87506,149.45341 h 2.20133 v -4.82601 h 4.80484 v -2.18017 h -4.80484 v -4.826 h -2.20133 v 4.826 h -4.80484 v 2.18017 h 4.80484 z"
id="path19" />
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 368.375,137.12109 v 4.82618 h -4.80469 v 3.17968 h 4.80469 v 4.82617 h 3.20117 v -4.82617 h 4.80469 v -3.17968 h -4.80469 v -4.82618 z m 1,1 h 1.20117 v 4.82618 h 4.80469 v 1.17968 h -4.80469 v 4.82617 H 369.375 v -4.82617 h -4.80469 v -1.17968 h 4.80469 z"
id="path20" />
</g>
<g
id="text6-2"
style="opacity:1">
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;fill:#ff0000;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 198.8614,307.41443 h 2.20134 v -4.82601 h 4.80484 v -2.18017 h -4.80484 v -4.82601 h -2.20134 v 4.82601 h -4.80484 v 2.18017 h 4.80484 z"
id="path15" />
<path
style="font-weight:bold;font-size:21.1667px;font-family:Recharge;-inkscape-font-specification:'Recharge Bold';text-align:center;baseline-shift:baseline;text-anchor:middle;display:inline;overflow:visible;vector-effect:none;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 198.36133,295.08203 v 4.82617 h -4.80469 v 3.17969 h 4.80469 v 4.82617 h 3.20117 v -4.82617 h 4.80469 v -3.17969 h -4.80469 v -4.82617 z m 1,1 h 1.20117 v 4.82617 h 4.80469 v 1.17969 h -4.80469 v 4.82617 h -1.20117 v -4.82617 h -4.80469 v -1.17969 h 4.80469 z"
id="path16" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 20 KiB

@@ -0,0 +1,261 @@
{
"type": "machine",
"name": "Xplorer Dual Gantry 0.4 nozzle",
"inherits": "Xplorer common",
"from": "system",
"setting_id": "DgYk8X6L1R0MKcfg",
"instantiation": "true",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\n{if layer_num != 0}\nG92 E0\n{endif}\n{if layer_num <= 0}\nG91\nG1 Z+10 F1200\nG90\n{endif}",
"change_filament_gcode": ";CHANGE_FILAMENT - Start\n;Layer#=[layer_num] - Z=[layer_z]\n{if layer_z >first_layer_height}\nG91\nG1 Z+2 F1200\nG90\n{endif}\n{if layer_num != -1 || next_extruder != 0}\n Tool{next_extruder}\n{endif}\n; <----\nG1 Z[layer_z] F1200\n{if layer_num == -1 || layer_num == 0}\n M109 S[first_layer_temperature[next_extruder]] T[next_extruder]\n{else}\n M104 S[temperature[next_extruder]] T[next_extruder]\n{endif}\n; ---->\nM83\n;Layer#=[layer_num] - Z=[layer_z]\nLOG_EXTR\n;CHANGE_FILAMENT - End",
"default_nozzle_volume_type": [
"High Flow",
"High Flow"
],
"default_print_profile": "0.20mm Standard @Xplorer 0.4",
"deretraction_speed": [
"120",
"120"
],
"extruder_colour": [
"#FCE94F",
"#729FCF"
],
"extruder_offset": [
"0x0",
"0x0"
],
"extruder_type": [
"Direct Drive",
"Direct Drive"
],
"extruder_variant_list": [
"Direct Drive High Flow",
"Direct Drive High Flow"
],
"imex_gantry_count": "2",
"imex_mode_active_tools": [
"0:P",
"0:P,1:C",
"0:P,1:M"
],
"imex_mode_gcodes": [
"SET_PRINT_MODE MODE=PRIMARY",
"SET_PRINT_MODE MODE=COPY",
"SET_PRINT_MODE MODE=MIRROR"
],
"imex_mode_names": [
"primary",
"copy",
"mirror"
],
"imex_tool_layout": "rear-left",
"imex_tools_per_gantry": "1",
"is_imex": "1",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]\nLOG_RESUME\nLOG_EXTR",
"long_retractions_when_cut": [
"0",
"0"
],
"machine_max_acceleration_e": [
"6000",
"5000",
"6000",
"5000"
],
"machine_max_acceleration_extruding": [
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_retracting": [
"5000",
"5000",
"5000",
"5000"
],
"machine_max_acceleration_travel": [
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_x": [
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_y": [
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_z": [
"100",
"50",
"100",
"50"
],
"machine_max_jerk_e": [
"2.5",
"1.25",
"2.5",
"1.25"
],
"machine_max_jerk_x": [
"5",
"2.5",
"5",
"2.5"
],
"machine_max_jerk_y": [
"5",
"2.5",
"5",
"2.5"
],
"machine_max_jerk_z": [
"0.4",
"0.2",
"0.4",
"0.2"
],
"machine_max_junction_deviation": [
"0",
"0",
"0",
"0"
],
"machine_max_speed_e": [
"120",
"60",
"120",
"60"
],
"machine_max_speed_x": [
"300",
"150",
"300",
"150"
],
"machine_max_speed_y": [
"300",
"150",
"300",
"150"
],
"machine_max_speed_z": [
"40",
"20",
"40",
"20"
],
"machine_start_gcode": "M104 S{first_layer_temperature[0]} T0\nM104 S{first_layer_temperature[1]} T1\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S{first_layer_temperature[0]} T0\nM109 S{first_layer_temperature[1]} T1\nstart_print\nG1 F5000\n{if first_layer_temperature[1] !=0 }\nTool1\nbrush_Tool1\n{endif}\n{if first_layer_temperature[0] !=0 }\nTool0\nbrush_Tool0\n{endif}\n{if first_layer_temperature[1] !=0 }\nTool1\nG1 Y280 X10.0 Z0.38 F5000.0 \nG1 Y100 X10.0 Z0.38 F1500.0 E15 \nG1 Y100 X10.4 Z0.38 F5000.0 \nG1 Y280 X10.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z2 F2000\n{endif}\nG1 F5000\n{if first_layer_temperature[0] !=0 }\nTool0\nG1 Y280 X11.0 Z0.38 F5000.0 \nG1 Y100 X11.0 Z0.38 F1500.0 E15 \nG1 Y100 X11.4 Z0.38 F5000.0 \nG1 Y280 X11.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z2 F2000\n{endif}\nG1 F5000\nM400\nSKEW_PROFILE LOAD=SKEW1\nG28 X\nG28 Y\nTool{initial_extruder}\nSTART_PRINTING_MODE\n\n",
"max_layer_height": [
"0.32",
"0.32"
],
"min_layer_height": [
"0.04",
"0.04"
],
"nozzle_diameter": [
"0.4",
"0.4"
],
"nozzle_type": [
"hardened_steel",
"hardened_steel"
],
"nozzle_volume": [
"0",
"0"
],
"printable_area": [
"10x0",
"410x0",
"410x342.5",
"10x342.5"
],
"printable_height": "401",
"printer_extruder_id": [
"1",
"2"
],
"printer_extruder_variant": [
"Direct Drive High Flow",
"Direct Drive High Flow"
],
"printer_model": "Xplorer Dual Gantry",
"printer_variant": "0.4",
"retract_before_wipe": [
"70%",
"70%"
],
"retract_length_toolchange": [
"2",
"2"
],
"retract_lift_above": [
"0.2",
"0.2"
],
"retract_lift_below": [
"400",
"400"
],
"retract_lift_enforce": [
"All Surfaces",
"All Surfaces"
],
"retract_restart_extra": [
"0",
"0"
],
"retract_restart_extra_toolchange": [
"0",
"0"
],
"retract_when_changing_layer": [
"1",
"1"
],
"retraction_distances_when_cut": [
"18",
"18"
],
"retraction_length": [
"1.35",
"1.35"
],
"retraction_minimum_travel": [
"2",
"2"
],
"retraction_speed": [
"120",
"120"
],
"travel_slope": [
"1",
"1"
],
"wipe": [
"1",
"1"
],
"wipe_distance": [
"1",
"1"
],
"z_hop": [
"1",
"1"
],
"z_hop_types": [
"Normal Lift",
"Normal Lift"
]
}
@@ -0,0 +1,261 @@
{
"type": "machine",
"name": "Xplorer Dual Gantry 0.6 nozzle",
"inherits": "Xplorer common",
"from": "system",
"setting_id": "M1ZyjMxGz0TpRDcv",
"instantiation": "true",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\n{if layer_num != 0}\nG92 E0\n{endif}\n{if layer_num <= 0}\nG91\nG1 Z+10 F1200\nG90\n{endif}",
"change_filament_gcode": ";CHANGE_FILAMENT - Start\n;Layer#=[layer_num] - Z=[layer_z]\n{if layer_z >first_layer_height}\nG91\nG1 Z+2 F1200\nG90\n{endif}\n{if layer_num != -1 || next_extruder != 0}\n Tool{next_extruder}\n{endif}\n; <----\nG1 Z[layer_z] F1200\n{if layer_num == -1 || layer_num == 0}\n M109 S[first_layer_temperature[next_extruder]] T[next_extruder]\n{else}\n M104 S[temperature[next_extruder]] T[next_extruder]\n{endif}\n; ---->\nM83\n;Layer#=[layer_num] - Z=[layer_z]\nLOG_EXTR\n;CHANGE_FILAMENT - End",
"default_nozzle_volume_type": [
"High Flow",
"High Flow"
],
"default_print_profile": "0.30mm Standard @Xplorer 0.6",
"deretraction_speed": [
"120",
"120"
],
"extruder_colour": [
"#FCE94F",
"#729FCF"
],
"extruder_offset": [
"0x0",
"0x0"
],
"extruder_type": [
"Direct Drive",
"Direct Drive"
],
"extruder_variant_list": [
"Direct Drive High Flow",
"Direct Drive High Flow"
],
"imex_gantry_count": "2",
"imex_mode_active_tools": [
"0:P",
"0:P,1:C",
"0:P,1:M"
],
"imex_mode_gcodes": [
"SET_PRINT_MODE MODE=PRIMARY",
"SET_PRINT_MODE MODE=COPY",
"SET_PRINT_MODE MODE=MIRROR"
],
"imex_mode_names": [
"primary",
"copy",
"mirror"
],
"imex_tool_layout": "rear-left",
"imex_tools_per_gantry": "1",
"is_imex": "1",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]\nLOG_RESUME\nLOG_EXTR",
"long_retractions_when_cut": [
"0",
"0"
],
"machine_max_acceleration_e": [
"6000",
"5000",
"6000",
"5000"
],
"machine_max_acceleration_extruding": [
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_retracting": [
"5000",
"5000",
"5000",
"5000"
],
"machine_max_acceleration_travel": [
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_x": [
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_y": [
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_z": [
"100",
"50",
"100",
"50"
],
"machine_max_jerk_e": [
"2.5",
"1.25",
"2.5",
"1.25"
],
"machine_max_jerk_x": [
"5",
"2.5",
"5",
"2.5"
],
"machine_max_jerk_y": [
"5",
"2.5",
"5",
"2.5"
],
"machine_max_jerk_z": [
"0.4",
"0.2",
"0.4",
"0.2"
],
"machine_max_junction_deviation": [
"0",
"0",
"0",
"0"
],
"machine_max_speed_e": [
"120",
"60",
"120",
"60"
],
"machine_max_speed_x": [
"300",
"150",
"300",
"150"
],
"machine_max_speed_y": [
"300",
"150",
"300",
"150"
],
"machine_max_speed_z": [
"40",
"20",
"40",
"20"
],
"machine_start_gcode": "M104 S{first_layer_temperature[0]} T0\nM104 S{first_layer_temperature[1]} T1\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S{first_layer_temperature[0]} T0\nM109 S{first_layer_temperature[1]} T1\nstart_print\nG1 F5000\n{if first_layer_temperature[1] !=0 }\nTool1\nbrush_Tool1\n{endif}\n{if first_layer_temperature[0] !=0 }\nTool0\nbrush_Tool0\n{endif}\n{if first_layer_temperature[1] !=0 }\nTool1\nG1 Y280 X10.0 Z0.38 F5000.0 \nG1 Y100 X10.0 Z0.38 F1500.0 E15 \nG1 Y100 X10.4 Z0.38 F5000.0 \nG1 Y280 X10.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z2 F2000\n{endif}\nG1 F5000\n{if first_layer_temperature[0] !=0 }\nTool0\nG1 Y280 X11.0 Z0.38 F5000.0 \nG1 Y100 X11.0 Z0.38 F1500.0 E15 \nG1 Y100 X11.4 Z0.38 F5000.0 \nG1 Y280 X11.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z2 F2000\n{endif}\nG1 F5000\nM400\nSKEW_PROFILE LOAD=SKEW1\nG28 X\nG28 Y\nTool{initial_extruder}\nSTART_PRINTING_MODE\n\n",
"max_layer_height": [
"0.45",
"0.45"
],
"min_layer_height": [
"0.12",
"0.12"
],
"nozzle_diameter": [
"0.6",
"0.6"
],
"nozzle_type": [
"hardened_steel",
"hardened_steel"
],
"nozzle_volume": [
"0",
"0"
],
"printable_area": [
"10x0",
"410x0",
"410x342.5",
"10x342.5"
],
"printable_height": "401",
"printer_extruder_id": [
"1",
"2"
],
"printer_extruder_variant": [
"Direct Drive High Flow",
"Direct Drive High Flow"
],
"printer_model": "Xplorer Dual Gantry",
"printer_variant": "0.6",
"retract_before_wipe": [
"70%",
"70%"
],
"retract_length_toolchange": [
"2",
"2"
],
"retract_lift_above": [
"0.2",
"0.2"
],
"retract_lift_below": [
"400",
"400"
],
"retract_lift_enforce": [
"All Surfaces",
"All Surfaces"
],
"retract_restart_extra": [
"0",
"0"
],
"retract_restart_extra_toolchange": [
"0",
"0"
],
"retract_when_changing_layer": [
"1",
"1"
],
"retraction_distances_when_cut": [
"18",
"18"
],
"retraction_length": [
"1.35",
"1.35"
],
"retraction_minimum_travel": [
"2",
"2"
],
"retraction_speed": [
"120",
"120"
],
"travel_slope": [
"1",
"1"
],
"wipe": [
"1",
"1"
],
"wipe_distance": [
"1",
"1"
],
"z_hop": [
"1",
"1"
],
"z_hop_types": [
"Normal Lift",
"Normal Lift"
]
}
@@ -0,0 +1,261 @@
{
"type": "machine",
"name": "Xplorer Dual Gantry 0.8 nozzle",
"inherits": "Xplorer common",
"from": "system",
"setting_id": "fWRcMcf9H5NP6zrX",
"instantiation": "true",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\n{if layer_num != 0}\nG92 E0\n{endif}\n{if layer_num <= 0}\nG91\nG1 Z+10 F1200\nG90\n{endif}",
"change_filament_gcode": ";CHANGE_FILAMENT - Start\n;Layer#=[layer_num] - Z=[layer_z]\n{if layer_z >first_layer_height}\nG91\nG1 Z+2 F1200\nG90\n{endif}\n{if layer_num != -1 || next_extruder != 0}\n Tool{next_extruder}\n{endif}\n; <----\nG1 Z[layer_z] F1200\n{if layer_num == -1 || layer_num == 0}\n M109 S[first_layer_temperature[next_extruder]] T[next_extruder]\n{else}\n M104 S[temperature[next_extruder]] T[next_extruder]\n{endif}\n; ---->\nM83\n;Layer#=[layer_num] - Z=[layer_z]\nLOG_EXTR\n;CHANGE_FILAMENT - End",
"default_nozzle_volume_type": [
"High Flow",
"High Flow"
],
"default_print_profile": "0.40mm Standard @Xplorer 0.8",
"deretraction_speed": [
"120",
"120"
],
"extruder_colour": [
"#FCE94F",
"#729FCF"
],
"extruder_offset": [
"0x0",
"0x0"
],
"extruder_type": [
"Direct Drive",
"Direct Drive"
],
"extruder_variant_list": [
"Direct Drive High Flow",
"Direct Drive High Flow"
],
"imex_gantry_count": "2",
"imex_mode_active_tools": [
"0:P",
"0:P,1:C",
"0:P,1:M"
],
"imex_mode_gcodes": [
"SET_PRINT_MODE MODE=PRIMARY",
"SET_PRINT_MODE MODE=COPY",
"SET_PRINT_MODE MODE=MIRROR"
],
"imex_mode_names": [
"primary",
"copy",
"mirror"
],
"imex_tool_layout": "rear-left",
"imex_tools_per_gantry": "1",
"is_imex": "1",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]\nLOG_RESUME\nLOG_EXTR",
"long_retractions_when_cut": [
"0",
"0"
],
"machine_max_acceleration_e": [
"6000",
"5000",
"6000",
"5000"
],
"machine_max_acceleration_extruding": [
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_retracting": [
"5000",
"5000",
"5000",
"5000"
],
"machine_max_acceleration_travel": [
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_x": [
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_y": [
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_z": [
"100",
"50",
"100",
"50"
],
"machine_max_jerk_e": [
"2.5",
"1.25",
"2.5",
"1.25"
],
"machine_max_jerk_x": [
"5",
"2.5",
"5",
"2.5"
],
"machine_max_jerk_y": [
"5",
"2.5",
"5",
"2.5"
],
"machine_max_jerk_z": [
"0.4",
"0.2",
"0.4",
"0.2"
],
"machine_max_junction_deviation": [
"0",
"0",
"0",
"0"
],
"machine_max_speed_e": [
"120",
"60",
"120",
"60"
],
"machine_max_speed_x": [
"300",
"150",
"300",
"150"
],
"machine_max_speed_y": [
"300",
"150",
"300",
"150"
],
"machine_max_speed_z": [
"40",
"20",
"40",
"20"
],
"machine_start_gcode": "M104 S{first_layer_temperature[0]} T0\nM104 S{first_layer_temperature[1]} T1\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S{first_layer_temperature[0]} T0\nM109 S{first_layer_temperature[1]} T1\nstart_print\nG1 F5000\n{if first_layer_temperature[1] !=0 }\nTool1\nbrush_Tool1\n{endif}\n{if first_layer_temperature[0] !=0 }\nTool0\nbrush_Tool0\n{endif}\n{if first_layer_temperature[1] !=0 }\nTool1\nG1 Y280 X10.0 Z0.38 F5000.0 \nG1 Y100 X10.0 Z0.38 F1500.0 E15 \nG1 Y100 X10.4 Z0.38 F5000.0 \nG1 Y280 X10.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z2 F2000\n{endif}\nG1 F5000\n{if first_layer_temperature[0] !=0 }\nTool0\nG1 Y280 X11.0 Z0.38 F5000.0 \nG1 Y100 X11.0 Z0.38 F1500.0 E15 \nG1 Y100 X11.4 Z0.38 F5000.0 \nG1 Y280 X11.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z2 F2000\n{endif}\nG1 F5000\nM400\nSKEW_PROFILE LOAD=SKEW1\nG28 X\nG28 Y\nTool{initial_extruder}\nSTART_PRINTING_MODE\n\n",
"max_layer_height": [
"0.64",
"0.64"
],
"min_layer_height": [
"0.2",
"0.2"
],
"nozzle_diameter": [
"0.8",
"0.8"
],
"nozzle_type": [
"hardened_steel",
"hardened_steel"
],
"nozzle_volume": [
"0",
"0"
],
"printable_area": [
"10x0",
"410x0",
"410x342.5",
"10x342.5"
],
"printable_height": "401",
"printer_extruder_id": [
"1",
"2"
],
"printer_extruder_variant": [
"Direct Drive High Flow",
"Direct Drive High Flow"
],
"printer_model": "Xplorer Dual Gantry",
"printer_variant": "0.8",
"retract_before_wipe": [
"70%",
"70%"
],
"retract_length_toolchange": [
"2",
"2"
],
"retract_lift_above": [
"0.2",
"0.2"
],
"retract_lift_below": [
"400",
"400"
],
"retract_lift_enforce": [
"All Surfaces",
"All Surfaces"
],
"retract_restart_extra": [
"0",
"0"
],
"retract_restart_extra_toolchange": [
"0",
"0"
],
"retract_when_changing_layer": [
"1",
"1"
],
"retraction_distances_when_cut": [
"18",
"18"
],
"retraction_length": [
"1.35",
"1.35"
],
"retraction_minimum_travel": [
"2",
"2"
],
"retraction_speed": [
"120",
"120"
],
"travel_slope": [
"1",
"1"
],
"wipe": [
"1",
"1"
],
"wipe_distance": [
"1",
"1"
],
"z_hop": [
"1",
"1"
],
"z_hop_types": [
"Normal Lift",
"Normal Lift"
]
}
@@ -0,0 +1,254 @@
{
"type": "machine",
"name": "Xplorer IDEX 0.4 nozzle",
"inherits": "Xplorer common",
"from": "system",
"setting_id": "lOZ2qJY7SsNRW03z",
"instantiation": "true",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\n{if layer_num != 0}\nG92 E0\n{endif}\n{if layer_num <= 0}\nG91\nG1 Z+10 F1200\nG90\n{endif}",
"change_filament_gcode": ";CHANGE_FILAMENT - Start\n;Layer#=[layer_num] - Z=[layer_z]\n{if layer_z >first_layer_height}\nG91\nG1 Z+2 F1200\nG90\n{endif}\n{if layer_num != -1 || next_extruder != 0}\n Tool{next_extruder}\n{endif}\n; <----\nG1 Z[layer_z] F1200\n{if layer_num == -1 || layer_num == 0}\n M109 S[first_layer_temperature[next_extruder]] T[next_extruder]\n{else}\n M104 S[temperature[next_extruder]] T[next_extruder]\n{endif}\n; ---->\nM83\n;Layer#=[layer_num] - Z=[layer_z]\nLOG_EXTR\n;CHANGE_FILAMENT - End",
"default_nozzle_volume_type": [
"High Flow",
"High Flow"
],
"default_print_profile": "0.20mm Standard @Xplorer 0.4",
"deretraction_speed": [
"120",
"120"
],
"extruder_colour": [
"#FCE94F",
"#729FCF"
],
"extruder_offset": [
"0x0",
"0x0"
],
"extruder_type": [
"Direct Drive",
"Direct Drive"
],
"extruder_variant_list": [
"Direct Drive High Flow",
"Direct Drive High Flow"
],
"imex_mode_active_tools": [
"0:P",
"0:P,1:C",
"0:P,1:M"
],
"imex_mode_gcodes": [
"SET_PRINT_MODE MODE=PRIMARY",
"SET_PRINT_MODE MODE=COPY",
"SET_PRINT_MODE MODE=MIRROR"
],
"imex_mode_names": [
"primary",
"copy",
"mirror"
],
"imex_nozzle_clearance_x": "40",
"imex_tool_layout": "front-left",
"is_imex": "1",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]\nLOG_RESUME\nLOG_EXTR",
"long_retractions_when_cut": [
"0",
"0"
],
"machine_max_acceleration_e": [
"6000",
"5000",
"6000",
"5000"
],
"machine_max_acceleration_extruding": [
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_retracting": [
"5000",
"5000",
"5000",
"5000"
],
"machine_max_acceleration_travel": [
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_x": [
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_y": [
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_z": [
"100",
"50",
"100",
"50"
],
"machine_max_jerk_e": [
"2.5",
"1.25",
"2.5",
"1.25"
],
"machine_max_jerk_x": [
"5",
"2.5",
"5",
"2.5"
],
"machine_max_jerk_y": [
"5",
"2.5",
"5",
"2.5"
],
"machine_max_jerk_z": [
"0.4",
"0.2",
"0.4",
"0.2"
],
"machine_max_junction_deviation": [
"0",
"0",
"0",
"0"
],
"machine_max_speed_e": [
"120",
"60",
"120",
"60"
],
"machine_max_speed_x": [
"300",
"150",
"300",
"150"
],
"machine_max_speed_y": [
"300",
"150",
"300",
"150"
],
"machine_max_speed_z": [
"40",
"20",
"40",
"20"
],
"machine_start_gcode": "M104 S{first_layer_temperature[0]} T0\nM104 S{first_layer_temperature[1]} T1\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S{first_layer_temperature[0]} T0\nM109 S{first_layer_temperature[1]} T1\nstart_print\nG1 F5000\n{if first_layer_temperature[1] !=0 }\nTool1\nbrush_Tool1\n{endif}\n{if first_layer_temperature[0] !=0 }\nTool0\nbrush_Tool0\n{endif}\n{if first_layer_temperature[1] !=0 }\nTool1\nG1 X280.1 Y3.0 Z0.38 F5000.0 \nG1 X100.1 Y3.0 Z0.38 F1500.0 E15 \nG1 X100.1 Y3.4 Z0.38 F5000.0 \nG1 X280.1 Y3.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z2 F2000\n{endif}\n{if first_layer_temperature[0] !=0 }\nTool0\nG1 X100.1 Y2.0 Z0.38 F5000.0 \nG1 X280.1 Y2.0 Z0.38 F1500.0 E15 \nG1 X280.1 Y2.4 Z0.38 F5000.0 \nG1 X100.1 Y2.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z2 F2000\n{endif}\nM400\nSKEW_PROFILE LOAD=SKEW1\nG28 X\nTool{initial_extruder}\nSTART_PRINTING_MODE\n\n",
"max_layer_height": [
"0.32",
"0.32"
],
"min_layer_height": [
"0.04",
"0.04"
],
"nozzle_diameter": [
"0.4",
"0.4"
],
"nozzle_type": [
"hardened_steel",
"hardened_steel"
],
"nozzle_volume": [
"0",
"0"
],
"printable_height": "401",
"printer_extruder_id": [
"1",
"2"
],
"printer_extruder_variant": [
"Direct Drive High Flow",
"Direct Drive High Flow"
],
"printer_model": "Xplorer IDEX",
"printer_variant": "0.4",
"retract_before_wipe": [
"70%",
"70%"
],
"retract_length_toolchange": [
"2",
"2"
],
"retract_lift_above": [
"0.2",
"0.2"
],
"retract_lift_below": [
"400",
"400"
],
"retract_lift_enforce": [
"All Surfaces",
"All Surfaces"
],
"retract_restart_extra": [
"0",
"0"
],
"retract_restart_extra_toolchange": [
"0",
"0"
],
"retract_when_changing_layer": [
"1",
"1"
],
"retraction_distances_when_cut": [
"18",
"18"
],
"retraction_length": [
"1.35",
"1.35"
],
"retraction_minimum_travel": [
"2",
"2"
],
"retraction_speed": [
"120",
"120"
],
"travel_slope": [
"1",
"1"
],
"wipe": [
"1",
"1"
],
"wipe_distance": [
"1",
"1"
],
"z_hop": [
"1",
"1"
],
"z_hop_types": [
"Normal Lift",
"Normal Lift"
]
}
@@ -0,0 +1,254 @@
{
"type": "machine",
"name": "Xplorer IDEX 0.6 nozzle",
"inherits": "Xplorer common",
"from": "system",
"setting_id": "S4ZLGZuPtR3yC4IZ",
"instantiation": "true",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\n{if layer_num != 0}\nG92 E0\n{endif}\n{if layer_num <= 0}\nG91\nG1 Z+10 F1200\nG90\n{endif}",
"change_filament_gcode": ";CHANGE_FILAMENT - Start\n;Layer#=[layer_num] - Z=[layer_z]\n{if layer_z >first_layer_height}\nG91\nG1 Z+2 F1200\nG90\n{endif}\n{if layer_num != -1 || next_extruder != 0}\n Tool{next_extruder}\n{endif}\n; <----\nG1 Z[layer_z] F1200\n{if layer_num == -1 || layer_num == 0}\n M109 S[first_layer_temperature[next_extruder]] T[next_extruder]\n{else}\n M104 S[temperature[next_extruder]] T[next_extruder]\n{endif}\n; ---->\nM83\n;Layer#=[layer_num] - Z=[layer_z]\nLOG_EXTR\n;CHANGE_FILAMENT - End",
"default_nozzle_volume_type": [
"High Flow",
"High Flow"
],
"default_print_profile": "0.30mm Standard @Xplorer 0.6",
"deretraction_speed": [
"120",
"120"
],
"extruder_colour": [
"#FCE94F",
"#729FCF"
],
"extruder_offset": [
"0x0",
"0x0"
],
"extruder_type": [
"Direct Drive",
"Direct Drive"
],
"extruder_variant_list": [
"Direct Drive High Flow",
"Direct Drive High Flow"
],
"imex_mode_active_tools": [
"0:P",
"0:P,1:C",
"0:P,1:M"
],
"imex_mode_gcodes": [
"SET_PRINT_MODE MODE=PRIMARY",
"SET_PRINT_MODE MODE=COPY",
"SET_PRINT_MODE MODE=MIRROR"
],
"imex_mode_names": [
"primary",
"copy",
"mirror"
],
"imex_nozzle_clearance_x": "40",
"imex_tool_layout": "front-left",
"is_imex": "1",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]\nLOG_RESUME\nLOG_EXTR",
"long_retractions_when_cut": [
"0",
"0"
],
"machine_max_acceleration_e": [
"6000",
"5000",
"6000",
"5000"
],
"machine_max_acceleration_extruding": [
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_retracting": [
"5000",
"5000",
"5000",
"5000"
],
"machine_max_acceleration_travel": [
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_x": [
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_y": [
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_z": [
"100",
"50",
"100",
"50"
],
"machine_max_jerk_e": [
"2.5",
"1.25",
"2.5",
"1.25"
],
"machine_max_jerk_x": [
"5",
"2.5",
"5",
"2.5"
],
"machine_max_jerk_y": [
"5",
"2.5",
"5",
"2.5"
],
"machine_max_jerk_z": [
"0.4",
"0.2",
"0.4",
"0.2"
],
"machine_max_junction_deviation": [
"0",
"0",
"0",
"0"
],
"machine_max_speed_e": [
"120",
"60",
"120",
"60"
],
"machine_max_speed_x": [
"300",
"150",
"300",
"150"
],
"machine_max_speed_y": [
"300",
"150",
"300",
"150"
],
"machine_max_speed_z": [
"40",
"20",
"40",
"20"
],
"machine_start_gcode": "M104 S{first_layer_temperature[0]} T0\nM104 S{first_layer_temperature[1]} T1\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S{first_layer_temperature[0]} T0\nM109 S{first_layer_temperature[1]} T1\nstart_print\nG1 F5000\n{if first_layer_temperature[1] !=0 }\nTool1\nbrush_Tool1\n{endif}\n{if first_layer_temperature[0] !=0 }\nTool0\nbrush_Tool0\n{endif}\n{if first_layer_temperature[1] !=0 }\nTool1\nG1 X280.1 Y3.0 Z0.38 F5000.0 \nG1 X100.1 Y3.0 Z0.38 F1500.0 E15 \nG1 X100.1 Y3.4 Z0.38 F5000.0 \nG1 X280.1 Y3.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z2 F2000\n{endif}\n{if first_layer_temperature[0] !=0 }\nTool0\nG1 X100.1 Y2.0 Z0.38 F5000.0 \nG1 X280.1 Y2.0 Z0.38 F1500.0 E15 \nG1 X280.1 Y2.4 Z0.38 F5000.0 \nG1 X100.1 Y2.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z2 F2000\n{endif}\nM400\nSKEW_PROFILE LOAD=SKEW1\nG28 X\nTool{initial_extruder}\nSTART_PRINTING_MODE\n\n",
"max_layer_height": [
"0.45",
"0.45"
],
"min_layer_height": [
"0.12",
"0.12"
],
"nozzle_diameter": [
"0.6",
"0.6"
],
"nozzle_type": [
"hardened_steel",
"hardened_steel"
],
"nozzle_volume": [
"0",
"0"
],
"printable_height": "401",
"printer_extruder_id": [
"1",
"2"
],
"printer_extruder_variant": [
"Direct Drive High Flow",
"Direct Drive High Flow"
],
"printer_model": "Xplorer IDEX",
"printer_variant": "0.6",
"retract_before_wipe": [
"70%",
"70%"
],
"retract_length_toolchange": [
"2",
"2"
],
"retract_lift_above": [
"0.2",
"0.2"
],
"retract_lift_below": [
"400",
"400"
],
"retract_lift_enforce": [
"All Surfaces",
"All Surfaces"
],
"retract_restart_extra": [
"0",
"0"
],
"retract_restart_extra_toolchange": [
"0",
"0"
],
"retract_when_changing_layer": [
"1",
"1"
],
"retraction_distances_when_cut": [
"18",
"18"
],
"retraction_length": [
"1.35",
"1.35"
],
"retraction_minimum_travel": [
"2",
"2"
],
"retraction_speed": [
"120",
"120"
],
"travel_slope": [
"1",
"1"
],
"wipe": [
"1",
"1"
],
"wipe_distance": [
"1",
"1"
],
"z_hop": [
"1",
"1"
],
"z_hop_types": [
"Normal Lift",
"Normal Lift"
]
}
@@ -0,0 +1,254 @@
{
"type": "machine",
"name": "Xplorer IDEX 0.8 nozzle",
"inherits": "Xplorer common",
"from": "system",
"setting_id": "U50ftmONKAXI4OQ9",
"instantiation": "true",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\n{if layer_num != 0}\nG92 E0\n{endif}\n{if layer_num <= 0}\nG91\nG1 Z+10 F1200\nG90\n{endif}",
"change_filament_gcode": ";CHANGE_FILAMENT - Start\n;Layer#=[layer_num] - Z=[layer_z]\n{if layer_z >first_layer_height}\nG91\nG1 Z+2 F1200\nG90\n{endif}\n{if layer_num != -1 || next_extruder != 0}\n Tool{next_extruder}\n{endif}\n; <----\nG1 Z[layer_z] F1200\n{if layer_num == -1 || layer_num == 0}\n M109 S[first_layer_temperature[next_extruder]] T[next_extruder]\n{else}\n M104 S[temperature[next_extruder]] T[next_extruder]\n{endif}\n; ---->\nM83\n;Layer#=[layer_num] - Z=[layer_z]\nLOG_EXTR\n;CHANGE_FILAMENT - End",
"default_nozzle_volume_type": [
"High Flow",
"High Flow"
],
"default_print_profile": "0.40mm Standard @Xplorer 0.8",
"deretraction_speed": [
"120",
"120"
],
"extruder_colour": [
"#FCE94F",
"#729FCF"
],
"extruder_offset": [
"0x0",
"0x0"
],
"extruder_type": [
"Direct Drive",
"Direct Drive"
],
"extruder_variant_list": [
"Direct Drive High Flow",
"Direct Drive High Flow"
],
"imex_mode_active_tools": [
"0:P",
"0:P,1:C",
"0:P,1:M"
],
"imex_mode_gcodes": [
"SET_PRINT_MODE MODE=PRIMARY",
"SET_PRINT_MODE MODE=COPY",
"SET_PRINT_MODE MODE=MIRROR"
],
"imex_mode_names": [
"primary",
"copy",
"mirror"
],
"imex_nozzle_clearance_x": "40",
"imex_tool_layout": "front-left",
"is_imex": "1",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]\nLOG_RESUME\nLOG_EXTR",
"long_retractions_when_cut": [
"0",
"0"
],
"machine_max_acceleration_e": [
"6000",
"5000",
"6000",
"5000"
],
"machine_max_acceleration_extruding": [
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_retracting": [
"5000",
"5000",
"5000",
"5000"
],
"machine_max_acceleration_travel": [
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_x": [
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_y": [
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_z": [
"100",
"50",
"100",
"50"
],
"machine_max_jerk_e": [
"2.5",
"1.25",
"2.5",
"1.25"
],
"machine_max_jerk_x": [
"5",
"2.5",
"5",
"2.5"
],
"machine_max_jerk_y": [
"5",
"2.5",
"5",
"2.5"
],
"machine_max_jerk_z": [
"0.4",
"0.2",
"0.4",
"0.2"
],
"machine_max_junction_deviation": [
"0",
"0",
"0",
"0"
],
"machine_max_speed_e": [
"120",
"60",
"120",
"60"
],
"machine_max_speed_x": [
"300",
"150",
"300",
"150"
],
"machine_max_speed_y": [
"300",
"150",
"300",
"150"
],
"machine_max_speed_z": [
"40",
"20",
"40",
"20"
],
"machine_start_gcode": "M104 S{first_layer_temperature[0]} T0\nM104 S{first_layer_temperature[1]} T1\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S{first_layer_temperature[0]} T0\nM109 S{first_layer_temperature[1]} T1\nstart_print\nG1 F5000\n{if first_layer_temperature[1] !=0 }\nTool1\nbrush_Tool1\n{endif}\n{if first_layer_temperature[0] !=0 }\nTool0\nbrush_Tool0\n{endif}\n{if first_layer_temperature[1] !=0 }\nTool1\nG1 X280.1 Y3.0 Z0.38 F5000.0 \nG1 X100.1 Y3.0 Z0.38 F1500.0 E15 \nG1 X100.1 Y3.4 Z0.38 F5000.0 \nG1 X280.1 Y3.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z2 F2000\n{endif}\n{if first_layer_temperature[0] !=0 }\nTool0\nG1 X100.1 Y2.0 Z0.38 F5000.0 \nG1 X280.1 Y2.0 Z0.38 F1500.0 E15 \nG1 X280.1 Y2.4 Z0.38 F5000.0 \nG1 X100.1 Y2.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z2 F2000\n{endif}\nM400\nSKEW_PROFILE LOAD=SKEW1\nG28 X\nTool{initial_extruder}\nSTART_PRINTING_MODE\n\n",
"max_layer_height": [
"0.64",
"0.64"
],
"min_layer_height": [
"0.2",
"0.2"
],
"nozzle_diameter": [
"0.8",
"0.8"
],
"nozzle_type": [
"hardened_steel",
"hardened_steel"
],
"nozzle_volume": [
"0",
"0"
],
"printable_height": "401",
"printer_extruder_id": [
"1",
"2"
],
"printer_extruder_variant": [
"Direct Drive High Flow",
"Direct Drive High Flow"
],
"printer_model": "Xplorer IDEX",
"printer_variant": "0.8",
"retract_before_wipe": [
"70%",
"70%"
],
"retract_length_toolchange": [
"2",
"2"
],
"retract_lift_above": [
"0.2",
"0.2"
],
"retract_lift_below": [
"400",
"400"
],
"retract_lift_enforce": [
"All Surfaces",
"All Surfaces"
],
"retract_restart_extra": [
"0",
"0"
],
"retract_restart_extra_toolchange": [
"0",
"0"
],
"retract_when_changing_layer": [
"1",
"1"
],
"retraction_distances_when_cut": [
"18",
"18"
],
"retraction_length": [
"1.35",
"1.35"
],
"retraction_minimum_travel": [
"2",
"2"
],
"retraction_speed": [
"120",
"120"
],
"travel_slope": [
"1",
"1"
],
"wipe": [
"1",
"1"
],
"wipe_distance": [
"1",
"1"
],
"z_hop": [
"1",
"1"
],
"z_hop_types": [
"Normal Lift",
"Normal Lift"
]
}
@@ -0,0 +1,399 @@
{
"type": "machine",
"name": "Xplorer IQEX 0.4 nozzle",
"inherits": "Xplorer common",
"from": "system",
"setting_id": "fbUJR4RMoMczxSkH",
"instantiation": "true",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\n{if layer_num != 0}\nG92 E0\n{endif}\n{if layer_num <= 0}\nG91\nG1 Z+10 F1200\nG90\n{endif}",
"change_filament_gcode": ";CHANGE_FILAMENT - Start\n;Layer#=[layer_num] - Z=[layer_z]\n{if layer_z >first_layer_height}\nG91\nG1 Z+2 F1200\nG90\n{endif}\n{if layer_num != -1 || next_extruder != 0}\n Tool{next_extruder}\n{endif}\n; <----\nG1 Z[layer_z] F1200\n{if layer_num == -1 || layer_num == 0}\n M109 S[first_layer_temperature[next_extruder]] T[next_extruder]\n{else}\n M104 S[temperature[next_extruder]] T[next_extruder]\n{endif}\n; ---->\nM83\n;Layer#=[layer_num] - Z=[layer_z]\nLOG_EXTR\n;CHANGE_FILAMENT - End",
"default_nozzle_volume_type": [
"High Flow",
"High Flow",
"High Flow",
"High Flow"
],
"default_print_profile": "0.20mm Standard @Xplorer 0.4",
"deretraction_speed": [
"120",
"120",
"120",
"120"
],
"extruder_colour": [
"#FCE94F",
"#729FCF",
"#AD7FA8",
"#F57900"
],
"extruder_offset": [
"0x0",
"0x0",
"0x0",
"0x0"
],
"extruder_type": [
"Direct Drive",
"Direct Drive",
"Direct Drive",
"Direct Drive"
],
"extruder_variant_list": [
"Direct Drive High Flow",
"Direct Drive High Flow",
"Direct Drive High Flow",
"Direct Drive High Flow"
],
"imex_gantry_count": "2",
"imex_mode_active_tools": [
"0:P",
"0:P,1:C",
"0:P,1:M",
"0:P,1:C,2:C,3:C",
"0:P,1:C,2:M,3:M",
"0:P,1:S,2:C,3:C",
"0:P,1:S,2:M,3:M"
],
"imex_mode_gcodes": [
"SET_PRINT_MODE MODE=PRIMARY",
"SET_PRINT_MODE MODE=COPY01",
"SET_PRINT_MODE MODE=MIRROR01",
"SET_PRINT_MODE MODE=COPY0123",
"SET_PRINT_MODE MODE=MIRROR0123",
"SET_PRINT_MODE MODE=MULTICOLOR_COPY01",
"SET_PRINT_MODE MODE=MULTICOLOR_MIRROR01"
],
"imex_mode_names": [
"primary",
"copy",
"mirror",
"iq-copy",
"iq-mirror",
"mc-copy",
"mc-mirror"
],
"imex_nozzle_clearance_x": "40",
"imex_tool_layout": "rear-left",
"is_imex": "1",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]\nLOG_RESUME\nLOG_EXTR",
"long_retractions_when_cut": [
"0",
"0",
"0",
"0"
],
"machine_max_acceleration_e": [
"6000",
"5000",
"6000",
"5000",
"6000",
"5000",
"6000",
"5000"
],
"machine_max_acceleration_extruding": [
"5000",
"2500",
"5000",
"2500",
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_retracting": [
"5000",
"5000",
"5000",
"5000",
"5000",
"5000",
"5000",
"5000"
],
"machine_max_acceleration_travel": [
"5000",
"2500",
"5000",
"2500",
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_x": [
"5000",
"2500",
"5000",
"2500",
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_y": [
"5000",
"2500",
"5000",
"2500",
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_z": [
"100",
"50",
"100",
"50",
"100",
"50",
"100",
"50"
],
"machine_max_jerk_e": [
"2.5",
"1.25",
"2.5",
"1.25",
"2.5",
"1.25",
"2.5",
"1.25"
],
"machine_max_jerk_x": [
"5",
"2.5",
"5",
"2.5",
"5",
"2.5",
"5",
"2.5"
],
"machine_max_jerk_y": [
"5",
"2.5",
"5",
"2.5",
"5",
"2.5",
"5",
"2.5"
],
"machine_max_jerk_z": [
"0.4",
"0.2",
"0.4",
"0.2",
"0.4",
"0.2",
"0.4",
"0.2"
],
"machine_max_junction_deviation": [
"0",
"0",
"0",
"0",
"0",
"0",
"0",
"0"
],
"machine_max_speed_e": [
"120",
"60",
"120",
"60",
"120",
"60",
"120",
"60"
],
"machine_max_speed_x": [
"300",
"150",
"300",
"150",
"300",
"150",
"300",
"150"
],
"machine_max_speed_y": [
"300",
"150",
"300",
"150",
"300",
"150",
"300",
"150"
],
"machine_max_speed_z": [
"40",
"20",
"40",
"20",
"40",
"20",
"40",
"20"
],
"machine_start_gcode": "M104 S{first_layer_temperature[0]} T0\nM104 S{first_layer_temperature[1]} T1\nM104 S{first_layer_temperature[2]} T2\nM104 S{first_layer_temperature[3]} T3\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S{first_layer_temperature[0]} T0\nM109 S{first_layer_temperature[1]} T1\nM109 S{first_layer_temperature[2]} T2\nM109 S{first_layer_temperature[3]} T3\nstart_print\nG1 F5000\n{if first_layer_temperature[3] !=0 }\nTool3\nbrush_Tool3\n{endif}\n{if first_layer_temperature[2] !=0 }\nTool2\nbrush_Tool2\n{endif}\n{if first_layer_temperature[1] !=0 }\nTool1\nbrush_Tool1\n{endif}\n{if first_layer_temperature[0] !=0 }\nTool0\nbrush_Tool0\n{endif}\n{if first_layer_temperature[3] !=0 }\nTool3\nG1 Y280 X407.0 Z0.38 F5000.0 \nG1 Y175 X407.0 Z0.38 F1500.0 E15 \nG1 Y175 X407.4 Z0.38 F5000.0 \nG1 Y280 X407.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z2 F2000\n{endif}\n{if first_layer_temperature[2] !=0 }\nTool2\nG1 Y280 X12.0 Z0.38 F5000.0 \nG1 Y175 X12.0 Z0.38 F1500.0 E15 \nG1 Y175 X12.4 Z0.38 F5000.0 \nG1 Y280 X12.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z2 F2000\n{endif}\n{if first_layer_temperature[1] !=0 }\nTool1\nG1 Y280 X408.0 Z0.38 F5000.0 \nG1 Y175 X408.0 Z0.38 F1500.0 E15 \nG1 Y175 X408.4 Z0.38 F5000.0 \nG1 Y280 X408.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z2 F2000\n{endif}\nG1 F5000\n{if first_layer_temperature[0] !=0 }\nTool0\nG1 Y280 X11.0 Z0.38 F5000.0 \nG1 Y175 X11.0 Z0.38 F1500.0 E15 \nG1 Y175 X11.4 Z0.38 F5000.0 \nG1 Y280 X11.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z2 F2000\n{endif}\nG1 F5000\nM400\nSKEW_PROFILE LOAD=SKEW1\nG28 X\nG28 Y\nTool{initial_extruder}\nSTART_PRINTING_MODE\n\n",
"max_layer_height": [
"0.32",
"0.32",
"0.32",
"0.32"
],
"min_layer_height": [
"0.04",
"0.04",
"0.04",
"0.04"
],
"nozzle_diameter": [
"0.4",
"0.4",
"0.4",
"0.4"
],
"nozzle_type": [
"hardened_steel",
"hardened_steel",
"hardened_steel",
"hardened_steel"
],
"nozzle_volume": [
"0",
"0",
"0",
"0"
],
"printable_area": [
"10x0",
"410x0",
"410x342.5",
"10x342.5"
],
"printable_height": "401",
"printer_extruder_id": [
"1",
"2",
"3",
"4"
],
"printer_extruder_variant": [
"Direct Drive High Flow",
"Direct Drive High Flow",
"Direct Drive High Flow",
"Direct Drive High Flow"
],
"printer_model": "Xplorer IQEX",
"printer_variant": "0.4",
"retract_before_wipe": [
"70%",
"70%",
"70%",
"70%"
],
"retract_length_toolchange": [
"2",
"2",
"2",
"2"
],
"retract_lift_above": [
"0.2",
"0.2",
"0.2",
"0.2"
],
"retract_lift_below": [
"400",
"400",
"400",
"400"
],
"retract_lift_enforce": [
"All Surfaces",
"All Surfaces",
"All Surfaces",
"All Surfaces"
],
"retract_restart_extra": [
"0",
"0",
"0",
"0"
],
"retract_restart_extra_toolchange": [
"0",
"0",
"0",
"0"
],
"retract_when_changing_layer": [
"1",
"1",
"1",
"1"
],
"retraction_distances_when_cut": [
"18",
"18",
"18",
"18"
],
"retraction_length": [
"1.35",
"1.35",
"1.35",
"1.35"
],
"retraction_minimum_travel": [
"2",
"2",
"2",
"2"
],
"retraction_speed": [
"120",
"120",
"120",
"120"
],
"travel_slope": [
"1",
"1",
"1",
"1"
],
"wipe": [
"1",
"1",
"1",
"1"
],
"wipe_distance": [
"1",
"1",
"1",
"1"
],
"z_hop": [
"1",
"1",
"1",
"1"
],
"z_hop_types": [
"Normal Lift",
"Normal Lift",
"Normal Lift",
"Normal Lift"
]
}
@@ -0,0 +1,399 @@
{
"type": "machine",
"name": "Xplorer IQEX 0.6 nozzle",
"inherits": "Xplorer common",
"from": "system",
"setting_id": "hOYAHBik6rxU1B0d",
"instantiation": "true",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\n{if layer_num != 0}\nG92 E0\n{endif}\n{if layer_num <= 0}\nG91\nG1 Z+10 F1200\nG90\n{endif}",
"change_filament_gcode": ";CHANGE_FILAMENT - Start\n;Layer#=[layer_num] - Z=[layer_z]\n{if layer_z >first_layer_height}\nG91\nG1 Z+2 F1200\nG90\n{endif}\n{if layer_num != -1 || next_extruder != 0}\n Tool{next_extruder}\n{endif}\n; <----\nG1 Z[layer_z] F1200\n{if layer_num == -1 || layer_num == 0}\n M109 S[first_layer_temperature[next_extruder]] T[next_extruder]\n{else}\n M104 S[temperature[next_extruder]] T[next_extruder]\n{endif}\n; ---->\nM83\n;Layer#=[layer_num] - Z=[layer_z]\nLOG_EXTR\n;CHANGE_FILAMENT - End",
"default_nozzle_volume_type": [
"High Flow",
"High Flow",
"High Flow",
"High Flow"
],
"default_print_profile": "0.30mm Standard @Xplorer 0.6",
"deretraction_speed": [
"120",
"120",
"120",
"120"
],
"extruder_colour": [
"#FCE94F",
"#729FCF",
"#AD7FA8",
"#F57900"
],
"extruder_offset": [
"0x0",
"0x0",
"0x0",
"0x0"
],
"extruder_type": [
"Direct Drive",
"Direct Drive",
"Direct Drive",
"Direct Drive"
],
"extruder_variant_list": [
"Direct Drive High Flow",
"Direct Drive High Flow",
"Direct Drive High Flow",
"Direct Drive High Flow"
],
"imex_gantry_count": "2",
"imex_mode_active_tools": [
"0:P",
"0:P,1:C",
"0:P,1:M",
"0:P,1:C,2:C,3:C",
"0:P,1:C,2:M,3:M",
"0:P,1:S,2:C,3:C",
"0:P,1:S,2:M,3:M"
],
"imex_mode_gcodes": [
"SET_PRINT_MODE MODE=PRIMARY",
"SET_PRINT_MODE MODE=COPY01",
"SET_PRINT_MODE MODE=MIRROR01",
"SET_PRINT_MODE MODE=COPY0123",
"SET_PRINT_MODE MODE=MIRROR0123",
"SET_PRINT_MODE MODE=MULTICOLOR_COPY01",
"SET_PRINT_MODE MODE=MULTICOLOR_MIRROR01"
],
"imex_mode_names": [
"primary",
"copy",
"mirror",
"iq-copy",
"iq-mirror",
"mc-copy",
"mc-mirror"
],
"imex_nozzle_clearance_x": "40",
"imex_tool_layout": "rear-left",
"is_imex": "1",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]\nLOG_RESUME\nLOG_EXTR",
"long_retractions_when_cut": [
"0",
"0",
"0",
"0"
],
"machine_max_acceleration_e": [
"6000",
"5000",
"6000",
"5000",
"6000",
"5000",
"6000",
"5000"
],
"machine_max_acceleration_extruding": [
"5000",
"2500",
"5000",
"2500",
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_retracting": [
"5000",
"5000",
"5000",
"5000",
"5000",
"5000",
"5000",
"5000"
],
"machine_max_acceleration_travel": [
"5000",
"2500",
"5000",
"2500",
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_x": [
"5000",
"2500",
"5000",
"2500",
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_y": [
"5000",
"2500",
"5000",
"2500",
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_z": [
"100",
"50",
"100",
"50",
"100",
"50",
"100",
"50"
],
"machine_max_jerk_e": [
"2.5",
"1.25",
"2.5",
"1.25",
"2.5",
"1.25",
"2.5",
"1.25"
],
"machine_max_jerk_x": [
"5",
"2.5",
"5",
"2.5",
"5",
"2.5",
"5",
"2.5"
],
"machine_max_jerk_y": [
"5",
"2.5",
"5",
"2.5",
"5",
"2.5",
"5",
"2.5"
],
"machine_max_jerk_z": [
"0.4",
"0.2",
"0.4",
"0.2",
"0.4",
"0.2",
"0.4",
"0.2"
],
"machine_max_junction_deviation": [
"0",
"0",
"0",
"0",
"0",
"0",
"0",
"0"
],
"machine_max_speed_e": [
"120",
"60",
"120",
"60",
"120",
"60",
"120",
"60"
],
"machine_max_speed_x": [
"300",
"150",
"300",
"150",
"300",
"150",
"300",
"150"
],
"machine_max_speed_y": [
"300",
"150",
"300",
"150",
"300",
"150",
"300",
"150"
],
"machine_max_speed_z": [
"40",
"20",
"40",
"20",
"40",
"20",
"40",
"20"
],
"machine_start_gcode": "M104 S{first_layer_temperature[0]} T0\nM104 S{first_layer_temperature[1]} T1\nM104 S{first_layer_temperature[2]} T2\nM104 S{first_layer_temperature[3]} T3\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S{first_layer_temperature[0]} T0\nM109 S{first_layer_temperature[1]} T1\nM109 S{first_layer_temperature[2]} T2\nM109 S{first_layer_temperature[3]} T3\nstart_print\nG1 F5000\n{if first_layer_temperature[3] !=0 }\nTool3\nbrush_Tool3\n{endif}\n{if first_layer_temperature[2] !=0 }\nTool2\nbrush_Tool2\n{endif}\n{if first_layer_temperature[1] !=0 }\nTool1\nbrush_Tool1\n{endif}\n{if first_layer_temperature[0] !=0 }\nTool0\nbrush_Tool0\n{endif}\n{if first_layer_temperature[3] !=0 }\nTool3\nG1 Y280 X407.0 Z0.38 F5000.0 \nG1 Y175 X407.0 Z0.38 F1500.0 E15 \nG1 Y175 X407.4 Z0.38 F5000.0 \nG1 Y280 X407.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z2 F2000\n{endif}\n{if first_layer_temperature[2] !=0 }\nTool2\nG1 Y280 X12.0 Z0.38 F5000.0 \nG1 Y175 X12.0 Z0.38 F1500.0 E15 \nG1 Y175 X12.4 Z0.38 F5000.0 \nG1 Y280 X12.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z2 F2000\n{endif}\n{if first_layer_temperature[1] !=0 }\nTool1\nG1 Y280 X408.0 Z0.38 F5000.0 \nG1 Y175 X408.0 Z0.38 F1500.0 E15 \nG1 Y175 X408.4 Z0.38 F5000.0 \nG1 Y280 X408.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z2 F2000\n{endif}\nG1 F5000\n{if first_layer_temperature[0] !=0 }\nTool0\nG1 Y280 X11.0 Z0.38 F5000.0 \nG1 Y175 X11.0 Z0.38 F1500.0 E15 \nG1 Y175 X11.4 Z0.38 F5000.0 \nG1 Y280 X11.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z2 F2000\n{endif}\nG1 F5000\nM400\nSKEW_PROFILE LOAD=SKEW1\nG28 X\nG28 Y\nTool{initial_extruder}\nSTART_PRINTING_MODE\n\n",
"max_layer_height": [
"0.45",
"0.45",
"0.45",
"0.45"
],
"min_layer_height": [
"0.12",
"0.12",
"0.12",
"0.12"
],
"nozzle_diameter": [
"0.6",
"0.6",
"0.6",
"0.6"
],
"nozzle_type": [
"hardened_steel",
"hardened_steel",
"hardened_steel",
"hardened_steel"
],
"nozzle_volume": [
"0",
"0",
"0",
"0"
],
"printable_area": [
"10x0",
"410x0",
"410x342.5",
"10x342.5"
],
"printable_height": "401",
"printer_extruder_id": [
"1",
"2",
"3",
"4"
],
"printer_extruder_variant": [
"Direct Drive High Flow",
"Direct Drive High Flow",
"Direct Drive High Flow",
"Direct Drive High Flow"
],
"printer_model": "Xplorer IQEX",
"printer_variant": "0.6",
"retract_before_wipe": [
"70%",
"70%",
"70%",
"70%"
],
"retract_length_toolchange": [
"2",
"2",
"2",
"2"
],
"retract_lift_above": [
"0.2",
"0.2",
"0.2",
"0.2"
],
"retract_lift_below": [
"400",
"400",
"400",
"400"
],
"retract_lift_enforce": [
"All Surfaces",
"All Surfaces",
"All Surfaces",
"All Surfaces"
],
"retract_restart_extra": [
"0",
"0",
"0",
"0"
],
"retract_restart_extra_toolchange": [
"0",
"0",
"0",
"0"
],
"retract_when_changing_layer": [
"1",
"1",
"1",
"1"
],
"retraction_distances_when_cut": [
"18",
"18",
"18",
"18"
],
"retraction_length": [
"1.35",
"1.35",
"1.35",
"1.35"
],
"retraction_minimum_travel": [
"2",
"2",
"2",
"2"
],
"retraction_speed": [
"120",
"120",
"120",
"120"
],
"travel_slope": [
"1",
"1",
"1",
"1"
],
"wipe": [
"1",
"1",
"1",
"1"
],
"wipe_distance": [
"1",
"1",
"1",
"1"
],
"z_hop": [
"1",
"1",
"1",
"1"
],
"z_hop_types": [
"Normal Lift",
"Normal Lift",
"Normal Lift",
"Normal Lift"
]
}
@@ -0,0 +1,399 @@
{
"type": "machine",
"name": "Xplorer IQEX 0.8 nozzle",
"inherits": "Xplorer common",
"from": "system",
"setting_id": "e048HgBDJy6XnaaO",
"instantiation": "true",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\n{if layer_num != 0}\nG92 E0\n{endif}\n{if layer_num <= 0}\nG91\nG1 Z+10 F1200\nG90\n{endif}",
"change_filament_gcode": ";CHANGE_FILAMENT - Start\n;Layer#=[layer_num] - Z=[layer_z]\n{if layer_z >first_layer_height}\nG91\nG1 Z+2 F1200\nG90\n{endif}\n{if layer_num != -1 || next_extruder != 0}\n Tool{next_extruder}\n{endif}\n; <----\nG1 Z[layer_z] F1200\n{if layer_num == -1 || layer_num == 0}\n M109 S[first_layer_temperature[next_extruder]] T[next_extruder]\n{else}\n M104 S[temperature[next_extruder]] T[next_extruder]\n{endif}\n; ---->\nM83\n;Layer#=[layer_num] - Z=[layer_z]\nLOG_EXTR\n;CHANGE_FILAMENT - End",
"default_nozzle_volume_type": [
"High Flow",
"High Flow",
"High Flow",
"High Flow"
],
"default_print_profile": "0.40mm Standard @Xplorer 0.8",
"deretraction_speed": [
"120",
"120",
"120",
"120"
],
"extruder_colour": [
"#FCE94F",
"#729FCF",
"#AD7FA8",
"#F57900"
],
"extruder_offset": [
"0x0",
"0x0",
"0x0",
"0x0"
],
"extruder_type": [
"Direct Drive",
"Direct Drive",
"Direct Drive",
"Direct Drive"
],
"extruder_variant_list": [
"Direct Drive High Flow",
"Direct Drive High Flow",
"Direct Drive High Flow",
"Direct Drive High Flow"
],
"imex_gantry_count": "2",
"imex_mode_active_tools": [
"0:P",
"0:P,1:C",
"0:P,1:M",
"0:P,1:C,2:C,3:C",
"0:P,1:C,2:M,3:M",
"0:P,1:S,2:C,3:C",
"0:P,1:S,2:M,3:M"
],
"imex_mode_gcodes": [
"SET_PRINT_MODE MODE=PRIMARY",
"SET_PRINT_MODE MODE=COPY01",
"SET_PRINT_MODE MODE=MIRROR01",
"SET_PRINT_MODE MODE=COPY0123",
"SET_PRINT_MODE MODE=MIRROR0123",
"SET_PRINT_MODE MODE=MULTICOLOR_COPY01",
"SET_PRINT_MODE MODE=MULTICOLOR_MIRROR01"
],
"imex_mode_names": [
"primary",
"copy",
"mirror",
"iq-copy",
"iq-mirror",
"mc-copy",
"mc-mirror"
],
"imex_nozzle_clearance_x": "40",
"imex_tool_layout": "rear-left",
"is_imex": "1",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]\nLOG_RESUME\nLOG_EXTR",
"long_retractions_when_cut": [
"0",
"0",
"0",
"0"
],
"machine_max_acceleration_e": [
"6000",
"5000",
"6000",
"5000",
"6000",
"5000",
"6000",
"5000"
],
"machine_max_acceleration_extruding": [
"5000",
"2500",
"5000",
"2500",
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_retracting": [
"5000",
"5000",
"5000",
"5000",
"5000",
"5000",
"5000",
"5000"
],
"machine_max_acceleration_travel": [
"5000",
"2500",
"5000",
"2500",
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_x": [
"5000",
"2500",
"5000",
"2500",
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_y": [
"5000",
"2500",
"5000",
"2500",
"5000",
"2500",
"5000",
"2500"
],
"machine_max_acceleration_z": [
"100",
"50",
"100",
"50",
"100",
"50",
"100",
"50"
],
"machine_max_jerk_e": [
"2.5",
"1.25",
"2.5",
"1.25",
"2.5",
"1.25",
"2.5",
"1.25"
],
"machine_max_jerk_x": [
"5",
"2.5",
"5",
"2.5",
"5",
"2.5",
"5",
"2.5"
],
"machine_max_jerk_y": [
"5",
"2.5",
"5",
"2.5",
"5",
"2.5",
"5",
"2.5"
],
"machine_max_jerk_z": [
"0.4",
"0.2",
"0.4",
"0.2",
"0.4",
"0.2",
"0.4",
"0.2"
],
"machine_max_junction_deviation": [
"0",
"0",
"0",
"0",
"0",
"0",
"0",
"0"
],
"machine_max_speed_e": [
"120",
"60",
"120",
"60",
"120",
"60",
"120",
"60"
],
"machine_max_speed_x": [
"300",
"150",
"300",
"150",
"300",
"150",
"300",
"150"
],
"machine_max_speed_y": [
"300",
"150",
"300",
"150",
"300",
"150",
"300",
"150"
],
"machine_max_speed_z": [
"40",
"20",
"40",
"20",
"40",
"20",
"40",
"20"
],
"machine_start_gcode": "M104 S{first_layer_temperature[0]} T0\nM104 S{first_layer_temperature[1]} T1\nM104 S{first_layer_temperature[2]} T2\nM104 S{first_layer_temperature[3]} T3\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nM109 S{first_layer_temperature[0]} T0\nM109 S{first_layer_temperature[1]} T1\nM109 S{first_layer_temperature[2]} T2\nM109 S{first_layer_temperature[3]} T3\nstart_print\nG1 F5000\n{if first_layer_temperature[3] !=0 }\nTool3\nbrush_Tool3\n{endif}\n{if first_layer_temperature[2] !=0 }\nTool2\nbrush_Tool2\n{endif}\n{if first_layer_temperature[1] !=0 }\nTool1\nbrush_Tool1\n{endif}\n{if first_layer_temperature[0] !=0 }\nTool0\nbrush_Tool0\n{endif}\n{if first_layer_temperature[3] !=0 }\nTool3\nG1 Y280 X407.0 Z0.38 F5000.0 \nG1 Y175 X407.0 Z0.38 F1500.0 E15 \nG1 Y175 X407.4 Z0.38 F5000.0 \nG1 Y280 X407.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z2 F2000\n{endif}\n{if first_layer_temperature[2] !=0 }\nTool2\nG1 Y280 X12.0 Z0.38 F5000.0 \nG1 Y175 X12.0 Z0.38 F1500.0 E15 \nG1 Y175 X12.4 Z0.38 F5000.0 \nG1 Y280 X12.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z2 F2000\n{endif}\n{if first_layer_temperature[1] !=0 }\nTool1\nG1 Y280 X408.0 Z0.38 F5000.0 \nG1 Y175 X408.0 Z0.38 F1500.0 E15 \nG1 Y175 X408.4 Z0.38 F5000.0 \nG1 Y280 X408.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z2 F2000\n{endif}\nG1 F5000\n{if first_layer_temperature[0] !=0 }\nTool0\nG1 Y280 X11.0 Z0.38 F5000.0 \nG1 Y175 X11.0 Z0.38 F1500.0 E15 \nG1 Y175 X11.4 Z0.38 F5000.0 \nG1 Y280 X11.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z2 F2000\n{endif}\nG1 F5000\nM400\nSKEW_PROFILE LOAD=SKEW1\nG28 X\nG28 Y\nTool{initial_extruder}\nSTART_PRINTING_MODE\n\n",
"max_layer_height": [
"0.64",
"0.64",
"0.64",
"0.64"
],
"min_layer_height": [
"0.2",
"0.2",
"0.2",
"0.2"
],
"nozzle_diameter": [
"0.8",
"0.8",
"0.8",
"0.8"
],
"nozzle_type": [
"hardened_steel",
"hardened_steel",
"hardened_steel",
"hardened_steel"
],
"nozzle_volume": [
"0",
"0",
"0",
"0"
],
"printable_area": [
"10x0",
"410x0",
"410x342.5",
"10x342.5"
],
"printable_height": "401",
"printer_extruder_id": [
"1",
"2",
"3",
"4"
],
"printer_extruder_variant": [
"Direct Drive High Flow",
"Direct Drive High Flow",
"Direct Drive High Flow",
"Direct Drive High Flow"
],
"printer_model": "Xplorer IQEX",
"printer_variant": "0.8",
"retract_before_wipe": [
"70%",
"70%",
"70%",
"70%"
],
"retract_length_toolchange": [
"2",
"2",
"2",
"2"
],
"retract_lift_above": [
"0.2",
"0.2",
"0.2",
"0.2"
],
"retract_lift_below": [
"400",
"400",
"400",
"400"
],
"retract_lift_enforce": [
"All Surfaces",
"All Surfaces",
"All Surfaces",
"All Surfaces"
],
"retract_restart_extra": [
"0",
"0",
"0",
"0"
],
"retract_restart_extra_toolchange": [
"0",
"0",
"0",
"0"
],
"retract_when_changing_layer": [
"1",
"1",
"1",
"1"
],
"retraction_distances_when_cut": [
"18",
"18",
"18",
"18"
],
"retraction_length": [
"1.35",
"1.35",
"1.35",
"1.35"
],
"retraction_minimum_travel": [
"2",
"2",
"2",
"2"
],
"retraction_speed": [
"120",
"120",
"120",
"120"
],
"travel_slope": [
"1",
"1",
"1",
"1"
],
"wipe": [
"1",
"1",
"1",
"1"
],
"wipe_distance": [
"1",
"1",
"1",
"1"
],
"z_hop": [
"1",
"1",
"1",
"1"
],
"z_hop_types": [
"Normal Lift",
"Normal Lift",
"Normal Lift",
"Normal Lift"
]
}
@@ -0,0 +1,167 @@
{
"type": "machine",
"name": "Xplorer Single 0.4 nozzle",
"inherits": "Xplorer common",
"from": "system",
"setting_id": "P4sDniCHVUne9JJF",
"instantiation": "true",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\n{if layer_num != 0}\nG92 E0\n{endif}",
"change_filament_gcode": "M600",
"default_nozzle_volume_type": [
"High Flow"
],
"default_print_profile": "0.20mm Standard @Xplorer 0.4",
"deretraction_speed": [
"120"
],
"extruder_colour": [
"#FCE94F"
],
"extruder_offset": [
"0x0"
],
"extruder_type": [
"Direct Drive"
],
"extruder_variant_list": [
"Direct Drive High Flow"
],
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]\nLOG_RESUME\nLOG_EXTR",
"long_retractions_when_cut": [
"0"
],
"machine_max_acceleration_e": [
"6000",
"5000"
],
"machine_max_acceleration_extruding": [
"5000",
"2500"
],
"machine_max_acceleration_retracting": [
"5000",
"5000"
],
"machine_max_acceleration_travel": [
"5000",
"2500"
],
"machine_max_acceleration_x": [
"5000",
"2500"
],
"machine_max_acceleration_y": [
"5000",
"2500"
],
"machine_max_acceleration_z": [
"100",
"50"
],
"machine_max_jerk_e": [
"2.5",
"1.25"
],
"machine_max_jerk_x": [
"5",
"2.5"
],
"machine_max_jerk_y": [
"5",
"2.5"
],
"machine_max_jerk_z": [
"0.4",
"0.2"
],
"machine_max_junction_deviation": [
"0",
"0"
],
"machine_max_speed_e": [
"120",
"60"
],
"machine_max_speed_x": [
"300",
"150"
],
"machine_max_speed_y": [
"300",
"150"
],
"machine_max_speed_z": [
"40",
"20"
],
"machine_start_gcode": "M104 S[first_layer_temperature] T[initial_tool]\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nG4 P10000 ; wait for temp stabilisation\nstart_print \nTool[initial_tool]\nG90\nM109 S[first_layer_temperature] T[initial_tool]\nBrush_Tool[initial_tool]\nG1 Z5.0 F3000 \nG1 X100 Y1.0 Z0.38 F5000.0 \nG1 X280 Y1.0 Z0.38 F1500.0 E15 \nG1 X280 Y1.4 Z0.38 F5000.0 \nG1 X100 Y1.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z5.0 F3000\nSKEW_PROFILE LOAD=SKEW1\n",
"max_layer_height": [
"0.32"
],
"min_layer_height": [
"0.04"
],
"nozzle_diameter": [
"0.4"
],
"printable_height": "401",
"printer_extruder_id": [
"1"
],
"printer_extruder_variant": [
"Direct Drive High Flow"
],
"printer_model": "Xplorer Single",
"printer_variant": "0.4",
"retract_before_wipe": [
"70%"
],
"retract_length_toolchange": [
"2"
],
"retract_lift_above": [
"0.2"
],
"retract_lift_below": [
"400"
],
"retract_lift_enforce": [
"All Surfaces"
],
"retract_restart_extra": [
"0"
],
"retract_restart_extra_toolchange": [
"0"
],
"retract_when_changing_layer": [
"1"
],
"retraction_distances_when_cut": [
"18"
],
"retraction_length": [
"1.35"
],
"retraction_minimum_travel": [
"2"
],
"retraction_speed": [
"120"
],
"travel_slope": [
"1"
],
"wipe": [
"1"
],
"wipe_distance": [
"1"
],
"z_hop": [
"1"
],
"z_hop_types": [
"Normal Lift"
]
}
@@ -0,0 +1,167 @@
{
"type": "machine",
"name": "Xplorer Single 0.6 nozzle",
"inherits": "Xplorer common",
"from": "system",
"setting_id": "AN4SjJRQBVxztQoT",
"instantiation": "true",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\n{if layer_num != 0}\nG92 E0\n{endif}",
"change_filament_gcode": "M600",
"default_nozzle_volume_type": [
"High Flow"
],
"default_print_profile": "0.30mm Standard @Xplorer 0.6",
"deretraction_speed": [
"120"
],
"extruder_colour": [
"#FCE94F"
],
"extruder_offset": [
"0x0"
],
"extruder_type": [
"Direct Drive"
],
"extruder_variant_list": [
"Direct Drive High Flow"
],
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]\nLOG_RESUME\nLOG_EXTR",
"long_retractions_when_cut": [
"0"
],
"machine_max_acceleration_e": [
"6000",
"5000"
],
"machine_max_acceleration_extruding": [
"5000",
"2500"
],
"machine_max_acceleration_retracting": [
"5000",
"5000"
],
"machine_max_acceleration_travel": [
"5000",
"2500"
],
"machine_max_acceleration_x": [
"5000",
"2500"
],
"machine_max_acceleration_y": [
"5000",
"2500"
],
"machine_max_acceleration_z": [
"100",
"50"
],
"machine_max_jerk_e": [
"2.5",
"1.25"
],
"machine_max_jerk_x": [
"5",
"2.5"
],
"machine_max_jerk_y": [
"5",
"2.5"
],
"machine_max_jerk_z": [
"0.4",
"0.2"
],
"machine_max_junction_deviation": [
"0",
"0"
],
"machine_max_speed_e": [
"120",
"60"
],
"machine_max_speed_x": [
"300",
"150"
],
"machine_max_speed_y": [
"300",
"150"
],
"machine_max_speed_z": [
"40",
"20"
],
"machine_start_gcode": "M104 S[first_layer_temperature] T[initial_tool]\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nG4 P10000 ; wait for temp stabilisation\nstart_print \nTool[initial_tool]\nG90\nM109 S[first_layer_temperature] T[initial_tool]\nBrush_Tool[initial_tool]\nG1 Z5.0 F3000 \nG1 X100 Y1.0 Z0.38 F5000.0 \nG1 X280 Y1.0 Z0.38 F1500.0 E15 \nG1 X280 Y1.4 Z0.38 F5000.0 \nG1 X100 Y1.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z5.0 F3000\nSKEW_PROFILE LOAD=SKEW1\n",
"max_layer_height": [
"0.45"
],
"min_layer_height": [
"0.12"
],
"nozzle_diameter": [
"0.6"
],
"printable_height": "401",
"printer_extruder_id": [
"1"
],
"printer_extruder_variant": [
"Direct Drive High Flow"
],
"printer_model": "Xplorer Single",
"printer_variant": "0.6",
"retract_before_wipe": [
"70%"
],
"retract_length_toolchange": [
"2"
],
"retract_lift_above": [
"0.2"
],
"retract_lift_below": [
"400"
],
"retract_lift_enforce": [
"All Surfaces"
],
"retract_restart_extra": [
"0"
],
"retract_restart_extra_toolchange": [
"0"
],
"retract_when_changing_layer": [
"1"
],
"retraction_distances_when_cut": [
"18"
],
"retraction_length": [
"1.35"
],
"retraction_minimum_travel": [
"2"
],
"retraction_speed": [
"120"
],
"travel_slope": [
"1"
],
"wipe": [
"1"
],
"wipe_distance": [
"1"
],
"z_hop": [
"1"
],
"z_hop_types": [
"Normal Lift"
]
}
@@ -0,0 +1,167 @@
{
"type": "machine",
"name": "Xplorer Single 0.8 nozzle",
"inherits": "Xplorer common",
"from": "system",
"setting_id": "rGCuBhLqBfV0zFLJ",
"instantiation": "true",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\n{if layer_num != 0}\nG92 E0\n{endif}",
"change_filament_gcode": "M600",
"default_nozzle_volume_type": [
"High Flow"
],
"default_print_profile": "0.40mm Standard @Xplorer 0.8",
"deretraction_speed": [
"120"
],
"extruder_colour": [
"#FCE94F"
],
"extruder_offset": [
"0x0"
],
"extruder_type": [
"Direct Drive"
],
"extruder_variant_list": [
"Direct Drive High Flow"
],
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]\nLOG_RESUME\nLOG_EXTR",
"long_retractions_when_cut": [
"0"
],
"machine_max_acceleration_e": [
"6000",
"5000"
],
"machine_max_acceleration_extruding": [
"5000",
"2500"
],
"machine_max_acceleration_retracting": [
"5000",
"5000"
],
"machine_max_acceleration_travel": [
"5000",
"2500"
],
"machine_max_acceleration_x": [
"5000",
"2500"
],
"machine_max_acceleration_y": [
"5000",
"2500"
],
"machine_max_acceleration_z": [
"100",
"50"
],
"machine_max_jerk_e": [
"2.5",
"1.25"
],
"machine_max_jerk_x": [
"5",
"2.5"
],
"machine_max_jerk_y": [
"5",
"2.5"
],
"machine_max_jerk_z": [
"0.4",
"0.2"
],
"machine_max_junction_deviation": [
"0",
"0"
],
"machine_max_speed_e": [
"120",
"60"
],
"machine_max_speed_x": [
"300",
"150"
],
"machine_max_speed_y": [
"300",
"150"
],
"machine_max_speed_z": [
"40",
"20"
],
"machine_start_gcode": "M104 S[first_layer_temperature] T[initial_tool]\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nG4 P10000 ; wait for temp stabilisation\nstart_print \nTool[initial_tool]\nG90\nM109 S[first_layer_temperature] T[initial_tool]\nBrush_Tool[initial_tool]\nG1 Z5.0 F3000 \nG1 X100 Y1.0 Z0.38 F5000.0 \nG1 X280 Y1.0 Z0.38 F1500.0 E15 \nG1 X280 Y1.4 Z0.38 F5000.0 \nG1 X100 Y1.4 Z0.38 F1500.0 E30\nG92 E0\nG1 Z5.0 F3000\nSKEW_PROFILE LOAD=SKEW1\n",
"max_layer_height": [
"0.64"
],
"min_layer_height": [
"0.2"
],
"nozzle_diameter": [
"0.8"
],
"printable_height": "401",
"printer_extruder_id": [
"1"
],
"printer_extruder_variant": [
"Direct Drive High Flow"
],
"printer_model": "Xplorer Single",
"printer_variant": "0.8",
"retract_before_wipe": [
"70%"
],
"retract_length_toolchange": [
"2"
],
"retract_lift_above": [
"0.2"
],
"retract_lift_below": [
"400"
],
"retract_lift_enforce": [
"All Surfaces"
],
"retract_restart_extra": [
"0"
],
"retract_restart_extra_toolchange": [
"0"
],
"retract_when_changing_layer": [
"1"
],
"retraction_distances_when_cut": [
"18"
],
"retraction_length": [
"1.35"
],
"retraction_minimum_travel": [
"2"
],
"retraction_speed": [
"120"
],
"travel_slope": [
"1"
],
"wipe": [
"1"
],
"wipe_distance": [
"1"
],
"z_hop": [
"1"
],
"z_hop_types": [
"Normal Lift"
]
}
@@ -0,0 +1,51 @@
{
"type": "machine",
"name": "Xplorer common",
"inherits": "fdm_klipper_common",
"from": "system",
"instantiation": "false",
"default_filament_profile": [
"Generic PLA @System"
],
"adaptive_bed_mesh_margin": "0",
"bed_mesh_max": "99999,99999",
"bed_mesh_min": "-99999,-99999",
"bed_mesh_probe_distance": "50,50",
"best_object_pos": "0.5,0.5",
"disable_m73": "0",
"emit_machine_limits_to_gcode": "0",
"enable_long_retraction_when_cut": "0",
"extruder_clearance_height_to_rod": "43",
"extruder_clearance_radius": "70",
"fan_kickstart": "0",
"fan_speedup_overhangs": "1",
"fan_speedup_time": "0",
"head_wrap_detect_zone": [],
"machine_end_gcode": "end_print",
"max_resonance_avoidance_speed": "120",
"min_resonance_avoidance_speed": "70",
"nozzle_height": "2.5",
"nozzle_hrc": "0",
"nozzle_type": "hardened_steel",
"nozzle_volume": "0",
"pellet_modded_printer": "0",
"preferred_orientation": "0",
"printable_area": [
"10x0",
"410x0",
"410x400",
"10x400"
],
"purge_in_prime_tower": "1",
"resonance_avoidance": "0",
"single_extruder_multi_material": "0",
"support_air_filtration": "1",
"support_chamber_temp_control": "1",
"support_multi_bed_types": "0",
"thumbnails": "48x48/PNG, 300x300/PNG",
"thumbnails_format": "PNG",
"time_cost": "0",
"use_firmware_retraction": "0",
"use_relative_e_distances": "1",
"z_offset": "0"
}
@@ -0,0 +1,12 @@
{
"type": "machine_model",
"name": "Xplorer Dual Gantry",
"model_id": "XplorerDualGantry",
"nozzle_diameter": "0.4;0.6;0.8",
"machine_tech": "FFF",
"family": "Vivedino",
"bed_model": "Xplorer_buildplate_model.stl",
"bed_texture": "Xplorer_bed_texture_dualgantry.svg",
"hotend_model": "",
"default_materials": "Generic ABS @System;Generic PLA @System;Generic PLA-CF @System;Generic PETG @System;Generic TPU @System;Generic ASA @System;Generic PC @System;Generic PVA @System;Generic PA @System;Generic PA-CF @System"
}
@@ -0,0 +1,12 @@
{
"type": "machine_model",
"name": "Xplorer IDEX",
"model_id": "XplorerIDEX",
"nozzle_diameter": "0.4;0.6;0.8",
"machine_tech": "FFF",
"family": "Vivedino",
"bed_model": "Xplorer_buildplate_model.stl",
"bed_texture": "Xplorer_bed_texture_idex.svg",
"hotend_model": "",
"default_materials": "Generic ABS @System;Generic PLA @System;Generic PLA-CF @System;Generic PETG @System;Generic TPU @System;Generic ASA @System;Generic PC @System;Generic PVA @System;Generic PA @System;Generic PA-CF @System"
}
@@ -0,0 +1,12 @@
{
"type": "machine_model",
"name": "Xplorer IQEX",
"model_id": "XplorerIQEX",
"nozzle_diameter": "0.4;0.6;0.8",
"machine_tech": "FFF",
"family": "Vivedino",
"bed_model": "Xplorer_buildplate_model.stl",
"bed_texture": "Xplorer_bed_texture_iqex.svg",
"hotend_model": "",
"default_materials": "Generic ABS @System;Generic PLA @System;Generic PLA-CF @System;Generic PETG @System;Generic TPU @System;Generic ASA @System;Generic PC @System;Generic PVA @System;Generic PA @System;Generic PA-CF @System"
}
@@ -0,0 +1,12 @@
{
"type": "machine_model",
"name": "Xplorer Single",
"model_id": "XplorerSingle",
"nozzle_diameter": "0.4;0.6;0.8",
"machine_tech": "FFF",
"family": "Vivedino",
"bed_model": "Xplorer_buildplate_model.stl",
"bed_texture": "",
"hotend_model": "",
"default_materials": "Generic ABS @System;Generic PLA @System;Generic PLA-CF @System;Generic PETG @System;Generic TPU @System;Generic ASA @System;Generic PC @System;Generic PVA @System;Generic PA @System;Generic PA-CF @System"
}
@@ -0,0 +1,28 @@
{
"type": "process",
"name": "0.04mm Ultra Fine @Xplorer 0.4",
"inherits": "fdm_process_xplorer_common",
"from": "system",
"setting_id": "7SZOHFM1nfZsGYHe",
"instantiation": "true",
"bottom_shell_layers": "7",
"initial_layer_print_height": "0.3",
"inner_wall_speed": "190",
"internal_solid_infill_acceleration": "100%",
"min_skirt_length": "0",
"outer_wall_speed": "130",
"skirt_distance": "3",
"skirt_loops": "1",
"slowdown_for_curled_perimeters": "1",
"small_perimeter_speed": "50%",
"sparse_infill_acceleration": "100%",
"top_shell_layers": "9",
"top_solid_infill_flow_ratio": "1",
"layer_height": "0.04",
"compatible_printers": [
"Xplorer Single 0.4 nozzle",
"Xplorer IDEX 0.4 nozzle",
"Xplorer Dual Gantry 0.4 nozzle",
"Xplorer IQEX 0.4 nozzle"
]
}
@@ -0,0 +1,28 @@
{
"type": "process",
"name": "0.08mm Extra Fine @Xplorer 0.4",
"inherits": "fdm_process_xplorer_common",
"from": "system",
"setting_id": "OvD7mwbTHKOUsgiT",
"instantiation": "true",
"bottom_shell_layers": "4",
"initial_layer_print_height": "0.3",
"inner_wall_speed": "190",
"internal_solid_infill_acceleration": "100%",
"min_skirt_length": "0",
"outer_wall_speed": "130",
"skirt_distance": "3",
"skirt_loops": "1",
"slowdown_for_curled_perimeters": "1",
"small_perimeter_speed": "50%",
"sparse_infill_acceleration": "100%",
"top_shell_layers": "5",
"top_solid_infill_flow_ratio": "0.87",
"layer_height": "0.08",
"compatible_printers": [
"Xplorer Single 0.4 nozzle",
"Xplorer IDEX 0.4 nozzle",
"Xplorer Dual Gantry 0.4 nozzle",
"Xplorer IQEX 0.4 nozzle"
]
}
@@ -0,0 +1,28 @@
{
"type": "process",
"name": "0.12mm Fine @Xplorer 0.4",
"inherits": "fdm_process_xplorer_common",
"from": "system",
"setting_id": "9iNid6X4xIPh267R",
"instantiation": "true",
"bottom_shell_layers": "4",
"initial_layer_print_height": "0.3",
"inner_wall_speed": "190",
"internal_solid_infill_acceleration": "100%",
"min_skirt_length": "0",
"outer_wall_speed": "130",
"skirt_distance": "3",
"skirt_loops": "1",
"slowdown_for_curled_perimeters": "1",
"small_perimeter_speed": "50%",
"sparse_infill_acceleration": "100%",
"top_shell_layers": "5",
"top_solid_infill_flow_ratio": "0.87",
"layer_height": "0.12",
"compatible_printers": [
"Xplorer Single 0.4 nozzle",
"Xplorer IDEX 0.4 nozzle",
"Xplorer Dual Gantry 0.4 nozzle",
"Xplorer IQEX 0.4 nozzle"
]
}
@@ -0,0 +1,28 @@
{
"type": "process",
"name": "0.16mm Optimal @Xplorer 0.4",
"inherits": "fdm_process_xplorer_common",
"from": "system",
"setting_id": "6eUjAhfHVuxRSCQr",
"instantiation": "true",
"bottom_shell_layers": "4",
"initial_layer_print_height": "0.3",
"inner_wall_speed": "190",
"internal_solid_infill_acceleration": "100%",
"min_skirt_length": "0",
"outer_wall_speed": "130",
"skirt_distance": "3",
"skirt_loops": "1",
"slowdown_for_curled_perimeters": "1",
"small_perimeter_speed": "50%",
"sparse_infill_acceleration": "100%",
"top_shell_layers": "5",
"top_solid_infill_flow_ratio": "0.87",
"layer_height": "0.16",
"compatible_printers": [
"Xplorer Single 0.4 nozzle",
"Xplorer IDEX 0.4 nozzle",
"Xplorer Dual Gantry 0.4 nozzle",
"Xplorer IQEX 0.4 nozzle"
]
}
@@ -0,0 +1,28 @@
{
"type": "process",
"name": "0.18mm Fine @Xplorer 0.6",
"inherits": "fdm_process_xplorer_common_0_6",
"from": "system",
"setting_id": "tQHKzSbBNyoPQkup",
"instantiation": "true",
"bottom_shell_layers": "4",
"initial_layer_print_height": "0.3",
"inner_wall_speed": "190",
"internal_solid_infill_acceleration": "100%",
"min_skirt_length": "0",
"outer_wall_speed": "130",
"skirt_distance": "3",
"skirt_loops": "1",
"slowdown_for_curled_perimeters": "1",
"small_perimeter_speed": "50%",
"sparse_infill_acceleration": "100%",
"top_shell_layers": "5",
"top_solid_infill_flow_ratio": "0.87",
"layer_height": "0.18",
"compatible_printers": [
"Xplorer Single 0.6 nozzle",
"Xplorer IDEX 0.6 nozzle",
"Xplorer Dual Gantry 0.6 nozzle",
"Xplorer IQEX 0.6 nozzle"
]
}
@@ -0,0 +1,26 @@
{
"type": "process",
"name": "0.20mm Standard @Xplorer 0.4",
"inherits": "fdm_process_xplorer_common",
"from": "system",
"setting_id": "CiJAkwpq5pR6MfYD",
"instantiation": "true",
"initial_layer_print_height": "0.3",
"inner_wall_speed": "190",
"internal_solid_infill_acceleration": "100%",
"min_skirt_length": "10",
"outer_wall_speed": "130",
"skirt_loops": "2",
"slowdown_for_curled_perimeters": "1",
"small_perimeter_speed": "50%",
"sparse_infill_acceleration": "100%",
"top_shell_layers": "4",
"top_solid_infill_flow_ratio": "0.87",
"layer_height": "0.20",
"compatible_printers": [
"Xplorer Single 0.4 nozzle",
"Xplorer IDEX 0.4 nozzle",
"Xplorer Dual Gantry 0.4 nozzle",
"Xplorer IQEX 0.4 nozzle"
]
}
@@ -0,0 +1,26 @@
{
"type": "process",
"name": "0.24mm Fine @Xplorer 0.8",
"inherits": "fdm_process_xplorer_common_0_8",
"from": "system",
"setting_id": "yv9TMduh7bgD1IjO",
"instantiation": "true",
"initial_layer_print_height": "0.3",
"inner_wall_speed": "190",
"internal_solid_infill_acceleration": "100%",
"min_skirt_length": "10",
"outer_wall_speed": "130",
"skirt_loops": "2",
"slowdown_for_curled_perimeters": "1",
"small_perimeter_speed": "50%",
"sparse_infill_acceleration": "100%",
"top_shell_layers": "4",
"top_solid_infill_flow_ratio": "0.87",
"layer_height": "0.24",
"compatible_printers": [
"Xplorer Single 0.8 nozzle",
"Xplorer IDEX 0.8 nozzle",
"Xplorer Dual Gantry 0.8 nozzle",
"Xplorer IQEX 0.8 nozzle"
]
}
@@ -0,0 +1,26 @@
{
"type": "process",
"name": "0.24mm Optimal @Xplorer 0.6",
"inherits": "fdm_process_xplorer_common_0_6",
"from": "system",
"setting_id": "WKjOeNEZndeHrbQw",
"instantiation": "true",
"initial_layer_print_height": "0.3",
"inner_wall_speed": "190",
"internal_solid_infill_acceleration": "100%",
"min_skirt_length": "10",
"outer_wall_speed": "130",
"skirt_loops": "2",
"slowdown_for_curled_perimeters": "1",
"small_perimeter_speed": "50%",
"sparse_infill_acceleration": "100%",
"top_shell_layers": "4",
"top_solid_infill_flow_ratio": "0.87",
"layer_height": "0.24",
"compatible_printers": [
"Xplorer Single 0.6 nozzle",
"Xplorer IDEX 0.6 nozzle",
"Xplorer Dual Gantry 0.6 nozzle",
"Xplorer IQEX 0.6 nozzle"
]
}
@@ -0,0 +1,26 @@
{
"type": "process",
"name": "0.28mm Draft @Xplorer 0.4",
"inherits": "fdm_process_xplorer_common",
"from": "system",
"setting_id": "zjvcZ59oVnh73fij",
"instantiation": "true",
"initial_layer_print_height": "0.32",
"inner_wall_speed": "190",
"internal_solid_infill_acceleration": "100%",
"min_skirt_length": "0",
"outer_wall_speed": "130",
"skirt_loops": "1",
"slowdown_for_curled_perimeters": "0",
"small_perimeter_speed": "50%",
"sparse_infill_acceleration": "100%",
"top_shell_layers": "4",
"top_solid_infill_flow_ratio": "0.87",
"layer_height": "0.28",
"compatible_printers": [
"Xplorer Single 0.4 nozzle",
"Xplorer IDEX 0.4 nozzle",
"Xplorer Dual Gantry 0.4 nozzle",
"Xplorer IQEX 0.4 nozzle"
]
}
@@ -0,0 +1,26 @@
{
"type": "process",
"name": "0.30mm Standard @Xplorer 0.6",
"inherits": "fdm_process_xplorer_common_0_6",
"from": "system",
"setting_id": "1NEAZrryKhhZNWoB",
"instantiation": "true",
"initial_layer_print_height": "0.32",
"inner_wall_speed": "190",
"internal_solid_infill_acceleration": "100%",
"min_skirt_length": "0",
"outer_wall_speed": "130",
"skirt_loops": "1",
"slowdown_for_curled_perimeters": "0",
"small_perimeter_speed": "50%",
"sparse_infill_acceleration": "100%",
"top_shell_layers": "4",
"top_solid_infill_flow_ratio": "0.87",
"layer_height": "0.30",
"compatible_printers": [
"Xplorer Single 0.6 nozzle",
"Xplorer IDEX 0.6 nozzle",
"Xplorer Dual Gantry 0.6 nozzle",
"Xplorer IQEX 0.6 nozzle"
]
}
@@ -0,0 +1,27 @@
{
"type": "process",
"name": "0.32mm Extra Draft @Xplorer 0.4",
"inherits": "fdm_process_xplorer_common",
"from": "system",
"setting_id": "IrvXoa4zZ1b2Y5cX",
"instantiation": "true",
"initial_layer_print_height": "0.32",
"inner_wall_speed": "190",
"internal_solid_infill_acceleration": "100%",
"min_skirt_length": "0",
"outer_wall_speed": "130",
"skirt_loops": "1",
"slowdown_for_curled_perimeters": "0",
"small_perimeter_speed": "50%",
"sparse_infill_acceleration": "100%",
"top_shell_layers": "4",
"top_solid_infill_flow_ratio": "0.87",
"top_surface_line_width": "0.45",
"layer_height": "0.32",
"compatible_printers": [
"Xplorer Single 0.4 nozzle",
"Xplorer IDEX 0.4 nozzle",
"Xplorer Dual Gantry 0.4 nozzle",
"Xplorer IQEX 0.4 nozzle"
]
}
@@ -0,0 +1,26 @@
{
"type": "process",
"name": "0.32mm Optimal @Xplorer 0.8",
"inherits": "fdm_process_xplorer_common_0_8",
"from": "system",
"setting_id": "N3z9IBRaaeXwVLbl",
"instantiation": "true",
"initial_layer_print_height": "0.32",
"inner_wall_speed": "190",
"internal_solid_infill_acceleration": "100%",
"min_skirt_length": "0",
"outer_wall_speed": "130",
"skirt_loops": "1",
"slowdown_for_curled_perimeters": "0",
"small_perimeter_speed": "50%",
"sparse_infill_acceleration": "100%",
"top_shell_layers": "4",
"top_solid_infill_flow_ratio": "0.87",
"layer_height": "0.32",
"compatible_printers": [
"Xplorer Single 0.8 nozzle",
"Xplorer IDEX 0.8 nozzle",
"Xplorer Dual Gantry 0.8 nozzle",
"Xplorer IQEX 0.8 nozzle"
]
}
@@ -0,0 +1,26 @@
{
"type": "process",
"name": "0.36mm Draft @Xplorer 0.6",
"inherits": "fdm_process_xplorer_common_0_6",
"from": "system",
"setting_id": "uv3jUBdJRkJ4dWVd",
"instantiation": "true",
"initial_layer_print_height": "0.32",
"inner_wall_speed": "190",
"internal_solid_infill_acceleration": "100%",
"min_skirt_length": "0",
"outer_wall_speed": "130",
"skirt_loops": "1",
"slowdown_for_curled_perimeters": "0",
"small_perimeter_speed": "50%",
"sparse_infill_acceleration": "100%",
"top_shell_layers": "4",
"top_solid_infill_flow_ratio": "0.87",
"layer_height": "0.36",
"compatible_printers": [
"Xplorer Single 0.6 nozzle",
"Xplorer IDEX 0.6 nozzle",
"Xplorer Dual Gantry 0.6 nozzle",
"Xplorer IQEX 0.6 nozzle"
]
}
@@ -0,0 +1,26 @@
{
"type": "process",
"name": "0.40mm Standard @Xplorer 0.8",
"inherits": "fdm_process_xplorer_common_0_8",
"from": "system",
"setting_id": "ZP9WXfiKDvSAjqU2",
"instantiation": "true",
"initial_layer_print_height": "0.32",
"inner_wall_speed": "190",
"internal_solid_infill_acceleration": "100%",
"min_skirt_length": "0",
"outer_wall_speed": "130",
"skirt_loops": "1",
"slowdown_for_curled_perimeters": "0",
"small_perimeter_speed": "50%",
"sparse_infill_acceleration": "100%",
"top_shell_layers": "4",
"top_solid_infill_flow_ratio": "0.87",
"layer_height": "0.40",
"compatible_printers": [
"Xplorer Single 0.8 nozzle",
"Xplorer IDEX 0.8 nozzle",
"Xplorer Dual Gantry 0.8 nozzle",
"Xplorer IQEX 0.8 nozzle"
]
}
@@ -0,0 +1,26 @@
{
"type": "process",
"name": "0.42mm Extra Draft @Xplorer 0.6",
"inherits": "fdm_process_xplorer_common_0_6",
"from": "system",
"setting_id": "JMbldCAibS7WnG1D",
"instantiation": "true",
"initial_layer_print_height": "0.32",
"inner_wall_speed": "190",
"internal_solid_infill_acceleration": "100%",
"min_skirt_length": "0",
"outer_wall_speed": "130",
"skirt_loops": "1",
"slowdown_for_curled_perimeters": "0",
"small_perimeter_speed": "50%",
"sparse_infill_acceleration": "100%",
"top_shell_layers": "4",
"top_solid_infill_flow_ratio": "0.87",
"layer_height": "0.42",
"compatible_printers": [
"Xplorer Single 0.6 nozzle",
"Xplorer IDEX 0.6 nozzle",
"Xplorer Dual Gantry 0.6 nozzle",
"Xplorer IQEX 0.6 nozzle"
]
}
@@ -0,0 +1,26 @@
{
"type": "process",
"name": "0.48mm Draft @Xplorer 0.8",
"inherits": "fdm_process_xplorer_common_0_8",
"from": "system",
"setting_id": "OhseWRzfSjom4gC9",
"instantiation": "true",
"initial_layer_print_height": "0.32",
"inner_wall_speed": "190",
"internal_solid_infill_acceleration": "100%",
"min_skirt_length": "0",
"outer_wall_speed": "130",
"skirt_loops": "1",
"slowdown_for_curled_perimeters": "0",
"small_perimeter_speed": "50%",
"sparse_infill_acceleration": "100%",
"top_shell_layers": "4",
"top_solid_infill_flow_ratio": "0.87",
"layer_height": "0.48",
"compatible_printers": [
"Xplorer Single 0.8 nozzle",
"Xplorer IDEX 0.8 nozzle",
"Xplorer Dual Gantry 0.8 nozzle",
"Xplorer IQEX 0.8 nozzle"
]
}
@@ -0,0 +1,26 @@
{
"type": "process",
"name": "0.56mm Extra Draft @Xplorer 0.8",
"inherits": "fdm_process_xplorer_common_0_8",
"from": "system",
"setting_id": "EXEeYQRkvcwYeLDA",
"instantiation": "true",
"initial_layer_print_height": "0.32",
"inner_wall_speed": "190",
"internal_solid_infill_acceleration": "100%",
"min_skirt_length": "0",
"outer_wall_speed": "130",
"skirt_loops": "1",
"slowdown_for_curled_perimeters": "0",
"small_perimeter_speed": "50%",
"sparse_infill_acceleration": "100%",
"top_shell_layers": "4",
"top_solid_infill_flow_ratio": "0.87",
"layer_height": "0.56",
"compatible_printers": [
"Xplorer Single 0.8 nozzle",
"Xplorer IDEX 0.8 nozzle",
"Xplorer Dual Gantry 0.8 nozzle",
"Xplorer IQEX 0.8 nozzle"
]
}
@@ -0,0 +1,265 @@
{
"type": "process",
"name": "fdm_process_xplorer_common",
"inherits": "fdm_process_klipper_common",
"from": "system",
"instantiation": "false",
"accel_to_decel_enable": "1",
"accel_to_decel_factor": "50%",
"align_infill_direction_to_model": "0",
"alternate_extra_wall": "0",
"bottom_shell_thickness": "1",
"bottom_solid_infill_flow_ratio": "1",
"bottom_surface_density": "100%",
"bottom_surface_pattern": "monotonicline",
"bridge_acceleration": "1500",
"bridge_angle": "0",
"bridge_density": "100%",
"bridge_flow": "1.5",
"brim_ears_detection_length": "1",
"brim_ears_max_angle": "125",
"brim_type": "no_brim",
"calib_flowrate_topinfill_special_order": "0",
"counterbore_hole_bridging": "none",
"default_acceleration": "5000",
"default_jerk": "5",
"default_junction_deviation": "0",
"detect_narrow_internal_solid_infill": "0",
"dont_filter_internal_bridges": "limited",
"elefant_foot_compensation": "0.1",
"elefant_foot_compensation_layers": "2",
"enable_extra_bridge_layer": "disabled",
"enable_overhang_speed": "1",
"enable_prime_tower": "0",
"enforce_support_layers": "0",
"ensure_vertical_shell_thickness": "ensure_moderate",
"extra_perimeters_on_overhangs": "1",
"extra_solid_infills": "",
"extrusion_rate_smoothing_external_perimeter_only": "0",
"fill_multiline": "1",
"filter_out_gap_fill": "2",
"flush_into_infill": "0",
"flush_into_objects": "0",
"flush_into_support": "1",
"fuzzy_skin": "none",
"fuzzy_skin_first_layer": "0",
"fuzzy_skin_mode": "displacement",
"fuzzy_skin_noise_type": "classic",
"fuzzy_skin_octaves": "4",
"fuzzy_skin_persistence": "0.5",
"fuzzy_skin_point_distance": "0.8",
"fuzzy_skin_scale": "1",
"fuzzy_skin_thickness": "0.3",
"gap_fill_target": "nowhere",
"gap_infill_speed": "10",
"gcode_add_line_number": "0",
"gcode_comments": "0",
"gcode_label_objects": "1",
"hole_to_polyhole": "0",
"hole_to_polyhole_threshold": "0.01",
"hole_to_polyhole_twisted": "1",
"independent_support_layer_height": "1",
"infill_anchor": "10",
"infill_anchor_max": "10",
"infill_combination_max_layer_height": "100%",
"infill_jerk": "0",
"infill_lock_depth": "1",
"infill_overhang_angle": "60",
"infill_shift_step": "0.4",
"infill_wall_overlap": "20%",
"initial_layer_acceleration": "1000",
"initial_layer_jerk": "0",
"initial_layer_line_width": "120%",
"initial_layer_min_bead_width": "85%",
"initial_layer_travel_speed": "200",
"inner_wall_acceleration": "3000",
"inner_wall_jerk": "0",
"inner_wall_line_width": "110%",
"interlocking_beam": "0",
"interlocking_beam_layer_count": "2",
"interlocking_beam_width": "0.8",
"interlocking_boundary_avoidance": "2",
"interlocking_depth": "2",
"interlocking_orientation": "22.5",
"internal_bridge_angle": "0",
"internal_bridge_density": "100%",
"internal_bridge_flow": "1.5",
"internal_bridge_speed": "150%",
"internal_solid_infill_line_width": "120%",
"internal_solid_infill_pattern": "monotonicline",
"ironing_angle": "-1",
"ironing_inset": "0",
"ironing_pattern": "rectilinear",
"is_infill_first": "0",
"lateral_lattice_angle_1": "-45",
"lateral_lattice_angle_2": "45",
"line_width": "110%",
"make_overhang_printable": "0",
"make_overhang_printable_angle": "55",
"make_overhang_printable_hole_size": "0",
"max_bridge_length": "10",
"max_volumetric_extrusion_rate_slope": "0",
"max_volumetric_extrusion_rate_slope_segment_length": "1",
"min_bead_width": "85%",
"min_feature_size": "25%",
"min_length_factor": "0.5",
"min_width_top_surface": "0",
"mmu_segmented_region_interlocking_depth": "0",
"mmu_segmented_region_max_width": "0",
"notes": "",
"only_one_wall_first_layer": "0",
"only_one_wall_top": "1",
"ooze_prevention": "0",
"outer_wall_acceleration": "1000",
"outer_wall_jerk": "0",
"outer_wall_line_width": "100%",
"overhang_1_4_speed": "100",
"overhang_reverse": "0",
"overhang_reverse_internal_only": "0",
"overhang_reverse_threshold": "50%",
"post_process": [],
"precise_outer_wall": "1",
"precise_z_height": "0",
"preheat_steps": "1",
"preheat_time": "10",
"prime_tower_brim_width": "10",
"prime_tower_width": "30",
"prime_volume": "10",
"print_flow_ratio": "1",
"print_order": "default",
"raft_contact_distance": "0.1",
"raft_expansion": "1.5",
"raft_first_layer_density": "90%",
"raft_first_layer_expansion": "2",
"reduce_crossing_wall": "1",
"role_based_wipe_speed": "1",
"scarf_angle_threshold": "155",
"scarf_joint_flow_ratio": "1",
"scarf_joint_speed": "100%",
"scarf_overhang_threshold": "40%",
"seam_gap": "10%",
"seam_slope_conditional": "0",
"seam_slope_entire_loop": "0",
"seam_slope_inner_walls": "0",
"seam_slope_min_length": "20",
"seam_slope_start_height": "0",
"seam_slope_steps": "10",
"seam_slope_type": "none",
"single_extruder_multi_material_priming": "0",
"single_loop_draft_shield": "0",
"skeleton_infill_density": "25%",
"skeleton_infill_line_width": "100%",
"skin_infill_density": "25%",
"skin_infill_depth": "2",
"skin_infill_line_width": "100%",
"skirt_speed": "50",
"skirt_start_angle": "-135",
"skirt_type": "combined",
"slice_closing_radius": "0.049",
"slicing_mode": "regular",
"slow_down_layers": "0",
"small_area_infill_flow_compensation": "1",
"small_area_infill_flow_compensation_model": [
"0,0",
"\n0.2,0.4444",
"\n0.4,0.6145",
"\n0.6,0.7059",
"\n0.8,0.7619",
"\n1.5,0.8571",
"\n2,0.8889",
"\n3,0.9231",
"\n5,0.9520",
"\n10,1"
],
"small_perimeter_threshold": "0",
"solid_infill_direction": "45",
"solid_infill_filament": "1",
"solid_infill_rotate_template": "45,135",
"sparse_infill_filament": "1",
"sparse_infill_line_width": "110%",
"sparse_infill_pattern": "gyroid",
"sparse_infill_rotate_template": "",
"spiral_finishing_flow_ratio": "0",
"spiral_mode_max_xy_smoothing": "200%",
"spiral_mode_smooth": "0",
"spiral_starting_flow_ratio": "0",
"staggered_inner_seams": "1",
"support_angle": "0",
"support_bottom_interface_spacing": "0.5",
"support_bottom_z_distance": "0.2",
"support_critical_regions_only": "0",
"support_expansion": "0",
"support_interface_not_for_body": "1",
"support_interface_pattern": "auto",
"support_ironing": "0",
"support_ironing_flow": "10%",
"support_ironing_pattern": "rectilinear",
"support_ironing_spacing": "0.1",
"support_line_width": "100%",
"support_object_first_layer_gap": "0.2",
"support_on_build_plate_only": "1",
"support_remove_small_overhang": "1",
"support_threshold_overlap": "50%",
"support_type": "tree(auto)",
"symmetric_infill_y_axis": "0",
"thick_bridges": "1",
"thick_internal_bridges": "1",
"timelapse_type": "0",
"top_bottom_infill_wall_overlap": "15%",
"top_surface_acceleration": "1000",
"top_surface_density": "100%",
"top_surface_jerk": "0",
"top_surface_line_width": "80%",
"top_surface_speed": "50",
"travel_acceleration": "5000",
"travel_jerk": "0",
"travel_speed": "250",
"travel_speed_z": "0",
"tree_support_adaptive_layer_height": "1",
"tree_support_angle_slow": "25",
"tree_support_auto_brim": "1",
"tree_support_branch_angle_organic": "40",
"tree_support_branch_diameter": "5",
"tree_support_branch_diameter_angle": "5",
"tree_support_branch_diameter_organic": "2",
"tree_support_branch_distance": "5",
"tree_support_branch_distance_organic": "1",
"tree_support_brim_width": "3",
"tree_support_tip_diameter": "0.8",
"tree_support_top_rate": "30%",
"wall_direction": "auto",
"wall_distribution_count": "1",
"wall_filament": "1",
"wall_generator": "arachne",
"wall_sequence": "inner wall/outer wall",
"wall_transition_angle": "10",
"wall_transition_filter_deviation": "25%",
"wall_transition_length": "100%",
"wipe_before_external_loop": "0",
"wipe_on_loops": "1",
"wipe_speed": "80%",
"wipe_tower_bridging": "10",
"wipe_tower_cone_angle": "10",
"wipe_tower_extra_flow": "100%",
"wipe_tower_extra_rib_length": "0",
"wipe_tower_extra_spacing": "100%",
"wipe_tower_filament": "0",
"wipe_tower_fillet_wall": "1",
"wipe_tower_max_purge_speed": "90",
"wipe_tower_rib_width": "8",
"wipe_tower_rotation_angle": "0",
"wipe_tower_wall_type": "rectangle",
"wiping_volumes_extruders": [
"70",
"70",
"70",
"70",
"70",
"70",
"70",
"70",
"70",
"70"
],
"xy_hole_compensation": "0.05"
}
@@ -0,0 +1,8 @@
{
"type": "process",
"name": "fdm_process_xplorer_common_0_6",
"inherits": "fdm_process_xplorer_common",
"from": "system",
"instantiation": "false",
"initial_layer_print_height": "0.3"
}
@@ -0,0 +1,8 @@
{
"type": "process",
"name": "fdm_process_xplorer_common_0_8",
"inherits": "fdm_process_xplorer_common",
"from": "system",
"instantiation": "false",
"initial_layer_print_height": "0.4"
}
@@ -29,13 +29,11 @@ uniform vec3 palette_lab[64];
uniform vec3 palette_rgb[64];
uniform int palette_count;
uniform bool pure_only; // match against single filaments only (flat-colour image)
// How each entry prints. Every entry names a single filament: a mix is given its own mixed filament
// slot, whose components the slicer alternates per print layer, so the fragment just looks that slot's
// colour up.
// The entry's two filaments, equal for a single filament - only so a mix can be told apart. An entry's
// palette_rgb is already the colour it prints in (for a mix, its mixed filament slot's).
uniform int palette_a[64];
uniform int palette_b[64];
uniform vec3 filament_rgb[16];
uniform int filament_count;
uniform float prefer_pure_de; // PREFER_PURE_DE: how much better than a single filament a mix must be
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
uniform bool has_color_tex;
uniform bool volume_mirrored;
@@ -210,31 +208,16 @@ int nearest_palette_entry(vec3 rgb)
best = i;
}
}
// The same bias make_palette_quantizer() applies (PREFER_PURE_DE = 10): a mix is an interleave, so
// it is only worth taking when it beats the nearest single filament by a visible step. Without it
// this picked a mix for almost every fragment - with four filaments the palette is 4 pure entries
// against 30 mixes - while the bake picked a single filament for most of them, so the preview
// interleaved the whole wall where the bake interleaves only patches. Compared on the distances
// rather than their squares, so the threshold means the same thing as it does on the CPU (up to
// CIE76 against CIEDE2000, the approximation already noted above).
if (best_pure >= 0 && palette_a[best] != palette_b[best] && sqrt(bd_pure) - sqrt(bd) < 10.0)
// The same bias make_palette_quantizer() applies: a mix is an interleave, so it is only worth taking
// when it beats the nearest single filament by a visible step - otherwise the preview shows mixes
// where the bake prints a single filament. Compared on the distances rather than their squares, so
// the margin means the same thing as it does on the CPU (up to CIE76 against CIEDE2000, the
// approximation already noted above).
if (best_pure >= 0 && palette_a[best] != palette_b[best] && sqrt(bd_pure) - sqrt(bd) < prefer_pure_de)
best = best_pure;
return best;
}
// One 2x2 Bayer cell, {0, 2; 3, 1}, for x and y in {0, 1}.
// The colour the printer lays down at world point `pos` for palette entry `index`. Every entry names a
// single filament: a mix is given its own mixed filament slot, whose components the slicer alternates
// per print layer, so there is nothing left to interleave here.
vec3 printed_color(int index)
{
int a = palette_a[index];
if (a < 0 || a >= filament_count)
return palette_rgb[index]; // no filament to resolve to: the entry's own colour
return filament_rgb[a];
}
void main()
{
if (any(lessThan(clipping_planes_dots, ZERO)))
@@ -354,16 +337,11 @@ void main()
NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0);
intensity.x += NdotL * LIGHT_FRONT_DIFFUSE;
// Diffuse albedo: the image's colour at this fragment, snapped to the nearest printable colour -
// and, where that is a mix, the filament the interleave puts here, so the pattern that prints shows.
// Diffuse albedo: the image's colour at this fragment, snapped to the nearest printable colour.
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
// the same material under the same light, and the relief this preview exists to show is unaffected.
vec3 albedo = uniform_color.rgb;
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
// tex_pos, not world_pos: the bake resolves the interleave in the bake frame (world
// orientation and scale about the volume's origin, see texture_displacement_bake_frame()), so
// measuring z from the bed instead shifted the band phase by the volume origin's height - a
// different filament in the same place than the bake produces.
albedo = printed_color(nearest_palette_entry(texture2D(color_tex, color_uv).rgb));
albedo = palette_rgb[nearest_palette_entry(texture2D(color_tex, color_uv).rgb)];
gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
}
@@ -88,13 +88,11 @@ uniform vec3 palette_lab[64];
uniform vec3 palette_rgb[64];
uniform int palette_count;
uniform bool pure_only; // match against single filaments only (flat-colour image)
// How each entry prints. Every entry names a single filament: a mix is given its own mixed filament
// slot, whose components the slicer alternates per print layer, so the fragment just looks that slot's
// colour up.
// The entry's two filaments, equal for a single filament - only so a mix can be told apart. An entry's
// palette_rgb is already the colour it prints in (for a mix, its mixed filament slot's).
uniform int palette_a[64];
uniform int palette_b[64];
uniform vec3 filament_rgb[16];
uniform int filament_count;
uniform float prefer_pure_de; // PREFER_PURE_DE: how much better than a single filament a mix must be
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
uniform bool has_color_tex;
uniform bool volume_mirrored;
@@ -276,31 +274,16 @@ int nearest_palette_entry(vec3 rgb)
best = i;
}
}
// The same bias make_palette_quantizer() applies (PREFER_PURE_DE = 10): a mix is an interleave, so
// it is only worth taking when it beats the nearest single filament by a visible step. Without it
// this picked a mix for almost every fragment - with four filaments the palette is 4 pure entries
// against 30 mixes - while the bake picked a single filament for most of them, so the preview
// interleaved the whole wall where the bake interleaves only patches. Compared on the distances
// rather than their squares, so the threshold means the same thing as it does on the CPU (up to
// CIE76 against CIEDE2000, the approximation already noted above).
if (best_pure >= 0 && palette_a[best] != palette_b[best] && sqrt(bd_pure) - sqrt(bd) < 10.0)
// The same bias make_palette_quantizer() applies: a mix is an interleave, so it is only worth taking
// when it beats the nearest single filament by a visible step - otherwise the preview shows mixes
// where the bake prints a single filament. Compared on the distances rather than their squares, so
// the margin means the same thing as it does on the CPU (up to CIE76 against CIEDE2000, the
// approximation already noted above).
if (best_pure >= 0 && palette_a[best] != palette_b[best] && sqrt(bd_pure) - sqrt(bd) < prefer_pure_de)
best = best_pure;
return best;
}
// One 2x2 Bayer cell, {0, 2; 3, 1}, for x and y in {0, 1}.
// The colour the printer lays down at world point `pos` for palette entry `index`. Every entry names a
// single filament: a mix is given its own mixed filament slot, whose components the slicer alternates
// per print layer, so there is nothing left to interleave here.
vec3 printed_color(int index)
{
int a = palette_a[index];
if (a < 0 || a >= filament_count)
return palette_rgb[index]; // no filament to resolve to: the entry's own colour
return filament_rgb[a];
}
void main()
{
if (any(lessThan(clipping_planes_dots, ZERO)))
@@ -442,16 +425,11 @@ void main()
NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0);
intensity.x += NdotL * LIGHT_FRONT_DIFFUSE;
// Diffuse albedo: the image's colour at this fragment, snapped to the nearest printable colour -
// and, where that is a mix, the filament the interleave puts here, so the pattern that prints shows.
// Diffuse albedo: the image's colour at this fragment, snapped to the nearest printable colour.
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
// the same material under the same light, and the relief this preview exists to show is unaffected.
vec3 albedo = uniform_color.rgb;
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
// tex_pos, not world_pos: the bake resolves the interleave in the bake frame (world
// orientation and scale about the volume's origin, see texture_displacement_bake_frame()), so
// measuring z from the bed instead shifted the band phase by the volume origin's height - a
// different filament in the same place than the bake produces.
albedo = printed_color(nearest_palette_entry(texture(color_tex, color_uv).rgb));
albedo = palette_rgb[nearest_palette_entry(texture(color_tex, color_uv).rgb)];
out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
}
+6
View File
@@ -325,6 +325,12 @@ set(lisbslic3r_sources
Geometry/VoronoiUtils.cpp
Geometry/VoronoiUtils.hpp
Geometry/VoronoiVisualUtils.hpp
IMEXArrange.cpp
IMEXArrange.hpp
IMEXHelpers.cpp
IMEXHelpers.hpp
IMEXZones.cpp
IMEXZones.hpp
InstanceLock.cpp
InstanceLock.hpp
Int128.hpp
+54
View File
@@ -18,6 +18,7 @@
#include <utility>
#include "ColorDecomposeRecipe.hpp"
#include "Config.hpp"
#include "FilamentMixerModel.hpp"
#include "LocalesUtils.hpp"
@@ -427,6 +428,59 @@ std::vector<double> parse_mixed_ratios(const std::string &str, size_t n_componen
return ratios;
}
std::string format_mixed_components(const std::vector<unsigned int> &components)
{
std::string out;
for (size_t i = 0; i < components.size(); ++i) {
if (i > 0)
out += ",";
out += std::to_string(components[i]);
}
return out;
}
std::string format_mixed_ratios(const std::vector<int> &weights)
{
int sum = std::accumulate(weights.begin(), weights.end(), 0);
if (sum <= 0)
sum = 100;
CNumericLocalesSetter c_locale_setter;
std::string out;
for (size_t i = 0; i < weights.size(); ++i) {
if (i > 0)
out += ",";
char buf[32];
std::snprintf(buf, sizeof(buf), "%.4f", float(weights[i]) / float(sum));
out += buf;
}
return out;
}
int find_fixed_mixed_filament(const ConfigBase &project_config,
const std::vector<unsigned int> &components,
const std::vector<int> &weights)
{
const auto *is_mixed = project_config.option<ConfigOptionBools>("filament_is_mixed");
const auto *comps = project_config.option<ConfigOptionStrings>("filament_mixed_components");
const auto *ratios = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios");
if (is_mixed == nullptr || comps == nullptr || ratios == nullptr)
return -1;
// Created lazily with the first mixed slot, so an older project may not have it at all.
const auto *gradient = project_config.option<ConfigOptionBools>("filament_mixed_gradient");
const std::string comp_str = format_mixed_components(components);
const std::string ratio_str = format_mixed_ratios(weights);
for (size_t i = 0; i < is_mixed->values.size(); ++i) {
if (!is_mixed->values[i] || i >= comps->values.size() || i >= ratios->values.size())
continue;
if (gradient != nullptr && i < gradient->values.size() && gradient->values[i])
continue;
if (comps->values[i] == comp_str && ratios->values[i] == ratio_str)
return int(i);
}
return -1;
}
bool has_any_mixed_filament(const std::vector<unsigned char> &is_mixed)
{
for (unsigned char v : is_mixed)
+18
View File
@@ -11,6 +11,8 @@
namespace Slic3r {
class ConfigBase;
// Photoshop-style gradient curve control point in [0,1] x [0,1].
// (x, y) is the anchor position; (m_in, m_out) are optional cubic Hermite tangent
// overrides. NaN means "use the PCHIP-computed default", which is the case for plain
@@ -94,6 +96,22 @@ std::vector<unsigned int> parse_mixed_components(const std::string &str);
// Normalizes so the sum equals 1.0.
std::vector<double> parse_mixed_ratios(const std::string &str, size_t n_components);
// The text a mixed slot stores in filament_mixed_components, e.g. {1, 3} → "1,3".
std::string format_mixed_components(const std::vector<unsigned int> &components);
// The text a mixed slot stores in filament_mixed_sublayer_ratios: the weights normalised to sum to 1,
// four decimals, e.g. {1, 2} → "0.3333,0.6667". A non-positive sum is read as 100.
std::string format_mixed_ratios(const std::vector<int> &weights);
// The 0-based index of the mixed slot in `project_config` that blends `components` (1-based physical
// filaments) in `weights` at a fixed ratio, or -1 when there is none. Matched on the stored text, as
// format_mixed_components() and format_mixed_ratios() write it. A gradient slot never matches: its
// ratio drifts from layer to layer, so it is not the blend asked for even where its stored ratios
// are the same.
int find_fixed_mixed_filament(const ConfigBase &project_config,
const std::vector<unsigned int> &components,
const std::vector<int> &weights);
// Returns true if any element in is_mixed is true.
// ConfigOptionBools stores values as std::vector<unsigned char>.
bool has_any_mixed_filament(const std::vector<unsigned char> &is_mixed);
+30
View File
@@ -1,5 +1,6 @@
#include "../libslic3r.h"
#include "../Exception.hpp"
#include "../IMEXHelpers.hpp"
#include "../Model.hpp"
#include "../Preset.hpp"
#include "../Utils.hpp"
@@ -368,6 +369,12 @@ static constexpr const char* FIRST_LAYER_PRINT_SEQUENCE_ATTR = "first_layer_prin
static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_ATTR = "other_layers_print_sequence";
static constexpr const char* OTHER_LAYERS_PRINT_SEQUENCE_NUMS_ATTR = "other_layers_print_sequence_nums";
static constexpr const char* SPIRAL_VASE_MODE = "spiral_mode";
static constexpr const char* IMEX_PARALLEL_MODE_ATTR = "imex_parallel_mode";
// The same attribute before the feature was renamed IMEX. Plate metadata is matched by exact
// string and written with set_key_value, so it never passes through handle_legacy: without this
// a project saved in that window loads every plate back on the Primary mode, silently.
static constexpr const char* IXEX_PARALLEL_MODE_ATTR_LEGACY = "ixex_parallel_mode";
static constexpr const char* IMEX_HEAD_FILAMENT_MAP_ATTR = "imex_head_filament_map";
static constexpr const char* FILAMENT_MAP_MODE_ATTR = "filament_map_mode";
static constexpr const char* FILAMENT_MAP_ATTR = "filament_maps";
static constexpr const char* FILAMENT_VOL_MAP_ATTR = "filament_volume_maps";
@@ -4576,6 +4583,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
std::istringstream(value) >> std::boolalpha >> spiral_mode;
m_curr_plater->config.set_key_value("spiral_mode", new ConfigOptionBool(spiral_mode));
}
else if (key == IMEX_PARALLEL_MODE_ATTR || key == IXEX_PARALLEL_MODE_ATTR_LEGACY) {
m_curr_plater->config.set_key_value("imex_parallel_mode", new ConfigOptionString(value));
}
else if (key == IMEX_HEAD_FILAMENT_MAP_ATTR) {
m_curr_plater->config.set_key_value("imex_head_filament_map", new ConfigOptionString(value));
}
else if (key == FILAMENT_MAP_MODE_ATTR)
{
FilamentMapMode map_mode = FilamentMapMode::fmmAutoForFlush;
@@ -8277,6 +8290,23 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (spiral_mode_opt)
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << SPIRAL_VASE_MODE << "\" " << VALUE_ATTR << "=\"" << spiral_mode_opt->getBool() << "\"/>\n";
{
// Mode names are user supplied free text, so the value has to be escaped for an
// attribute. xml_escape_double_quotes_attribute_value() is used rather than
// xml_escape() because it also emits tab/CR/LF as numeric character references:
// XML normalizes literal whitespace in attribute values on read, which would
// silently rename the mode. The reader takes the value straight from expat,
// which resolves both entities and character references, so this round-trips.
auto* imex_mode_opt = plate_data->config.option<ConfigOptionString>("imex_parallel_mode");
if (imex_mode_opt && !imex_mode_opt->value.empty() && imex_mode_opt->value != kImexPrimaryMode)
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << IMEX_PARALLEL_MODE_ATTR << "\" " << VALUE_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(imex_mode_opt->value) << "\"/>\n";
}
{
auto* imex_hfm_opt = plate_data->config.option<ConfigOptionString>("imex_head_filament_map");
if (imex_hfm_opt && !imex_hfm_opt->value.empty())
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << IMEX_HEAD_FILAMENT_MAP_ATTR << "\" " << VALUE_ATTR << "=\"" << xml_escape_double_quotes_attribute_value(imex_hfm_opt->value) << "\"/>\n";
}
//filament map related
ConfigOption* filament_map_mode_opt = plate_data->config.option("filament_map_mode");
t_config_enum_names filament_map_mode_names = ConfigOptionEnum<FilamentMapMode>::get_enum_names();
+360 -58
View File
@@ -29,6 +29,7 @@
#include "enum_bitmask.hpp"
#include "libslic3r.h"
#include "I18N.hpp"
#include "IMEXHelpers.hpp"
#include "GCode.hpp"
#include <cstdio>
#include "Exception.hpp"
@@ -87,6 +88,7 @@
#include <type_traits>
#include <utility>
#include <string_view>
#include <sstream>
#include <regex>
#include <boost/algorithm/string.hpp>
@@ -1710,7 +1712,16 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
: gcodegen.config().nozzle_temperature.get_at(new_fi);
if (std::abs(tcr.print_z) < EPSILON)
base_temp = gcodegen.config().nozzle_temperature_initial_layer.get_at(new_fi);
const std::string t_token = " T" + std::to_string(new_extruder_id);
// This pass strips the M109 that OozePrevention::post_toolchange just emitted into
// toolchange_gcode_str, so it must look for the tool index that emission actually
// wrote -- physical, translated by GCodeWriter::set_temperature -- not the logical
// id the temperature lookups above use. Matching on the logical id would leave the
// blocking M109 in place and silently defeat the tower interface temperature.
// (The sibling scan further down reads WipeTower2 output, whose set_extruder_temp
// emits no T at all, so it is deliberately left alone.)
const int heater_id = imex_physical_heater_for(
gcodegen.config().is_imex.value, gcodegen.config().physical_extruder_map, new_extruder_id);
const std::string t_token = " T" + std::to_string(heater_id);
std::string out;
out.reserve(toolchange_gcode_str.size());
size_t pos = 0;
@@ -1731,7 +1742,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
const std::string t_val = trimmed.substr(t_pos + 1, t_end == std::string::npos ? std::string::npos : t_end - (t_pos + 1));
if (!t_val.empty()) {
try {
matches_extruder = std::stoi(t_val) == new_extruder_id;
matches_extruder = std::stoi(t_val) == heater_id;
} catch (...) {
matches_extruder = false;
}
@@ -1763,44 +1774,6 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
toolchange_gcode_str.swap(out);
}
if (toolchange_temp_override > 0) {
const std::string preheat_token = "preheat T" + std::to_string(new_extruder_id);
const int preheat_temp = interface_temp > 0 ? interface_temp : toolchange_temp_override;
std::string out;
out.reserve(tcr_rotated_gcode.size());
size_t pos = 0;
while (pos < tcr_rotated_gcode.size()) {
size_t line_end = tcr_rotated_gcode.find('\n', pos);
if (line_end == std::string::npos)
line_end = tcr_rotated_gcode.size();
std::string line = tcr_rotated_gcode.substr(pos, line_end - pos);
std::string trimmed = line;
trimmed.erase(0, trimmed.find_first_not_of(" \t"));
const bool is_preheat_line = (trimmed.find(preheat_token) != std::string::npos);
if (is_preheat_line) {
// Preserve early-preheat timing while forcing interface temp for contact toolchanges.
size_t s_pos = trimmed.find('S');
if (s_pos != std::string::npos) {
size_t s_end = trimmed.find_first_not_of("0123456789", s_pos + 1);
trimmed.replace(s_pos + 1,
(s_end == std::string::npos ? trimmed.size() : s_end) - (s_pos + 1),
std::to_string(preheat_temp));
// Reapply left indentation from the original line.
size_t line_prefix = line.find_first_not_of(" \t");
if (line_prefix != std::string::npos)
line = line.substr(0, line_prefix) + trimmed;
else
line = trimmed;
}
}
out.append(line);
if (line_end < tcr_rotated_gcode.size())
out.push_back('\n');
pos = line_end + 1;
}
tcr_rotated_gcode.swap(out);
}
if (toolchange_temp_override > 0 && interface_temp > 0) {
const std::string t_token = " T" + std::to_string(new_extruder_id);
std::string out;
@@ -3251,6 +3224,38 @@ static BambuBedType to_bambu_bed_type(BedType type)
return bambu_bed_type;
}
// Orca IMEX: Returns the PHYSICAL tool indices the active IMEX mode drives, or an EMPTY
// vector in primary mode -- a single tool, so there are no parallel carriages to list.
// The mode's Primary head IS included in parallel modes; callers handle it themselves.
// In copy/mirror parallel modes, secondary carriages never receive tool-change
// commands — the firmware mirrors the primary's moves — so they don't appear in
// tool_ordering.all_extruders(). This helper extracts the active mode's tool string
// from the Print and delegates parsing to IMEXHelpers::parse_imex_active_tools so the
// "phys[:role]" tokenization matches every other IMEX consumer (PartPlate zones,
// GCodeViewer legend, Plater warnings).
// Indices are PHYSICAL T-indices. Callers that need a filament-slot (for PA / temp
// lookups) must resolve via first_filament_for_physical_head or resolve_filament_for_head.
static std::vector<int> get_imex_active_tools(const Print& print)
{
std::vector<int> active_tools;
if (!print.config().is_imex.value || print.objects().empty())
return active_tools;
const std::string& raw_mode = print.objects().front()->config().imex_parallel_mode.value;
const std::string active_mode = raw_mode.empty() ? kImexPrimaryMode : raw_mode;
// Primary mode drives a single tool, so there are no parallel carriages to list.
if (active_mode == kImexPrimaryMode)
return active_tools;
// An unresolved mode, and a mode the tools array is too short to cover, both hand back an
// empty tools string, which parses to no tools.
for (const auto& [phys, role] : parse_imex_active_tools(find_imex_mode(print.config(), active_mode).active_tools))
active_tools.push_back(phys);
return active_tools;
}
void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGeneratorCallback thumbnail_cb)
{
PROFILE_FUNC();
@@ -3660,6 +3665,51 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
std::vector<unsigned char> is_extruder_used(std::max(size_t(MAXIMUM_EXTRUDER_NUMBER), print.config().filament_diameter.size()), 0);
for (unsigned int extruder : tool_ordering.all_extruders())
is_extruder_used[extruder] = true;
// Orca IMEX: mark the LOGICAL filament slot each active SECONDARY carriage
// will load during the print. is_extruder_used is logical-slot indexed
// (consumed as `is_extruder_used[N]` in machine_start_gcode templates), but
// get_imex_active_tools returns PHYSICAL extruder indices. On any printer
// with physical_extruder_map size > 1 (MMU/AFC), writing the physical index
// into a logical-indexed array marks the wrong slot.
//
// The primary is intentionally SKIPPED here — its filament is already
// covered by tool_ordering.all_extruders() above (which lists the slots the
// objects on the plate are actually assigned to). Marking the primary
// again via the pem `first_filament_for_physical_head` fallback would
// pollute is_extruder_used with the *first* slot routed to the primary's
// physical extruder, which is generally not the slot the user assigned to
// the printing object. Same skip-primary pattern as the IMEX PA emission
// path in _do_export().
//
// For secondaries: translate physical -> logical via the per-plate
// imex_head_filament_map (set by the IMEX ghost picker), with
// first_filament_for_physical_head as the fallback when no override is set.
if (print.config().is_imex.value && !print.objects().empty()
&& initial_extruder_id != (unsigned int)-1) {
const auto plate_head_map = parse_imex_head_filament_map(
print.objects().front()->config().imex_head_filament_map.value);
const ConfigOptionInts& pem = print.config().physical_extruder_map;
// Bounds-checked, not get_at(): a clamp would suppress whichever head sits at pem[0].
// A miss stays -1 and matches no head, so nothing is skipped -- see IMEXHelpers.hpp.
const int primary_physical =
((int) initial_extruder_id >= 0 &&
(int) initial_extruder_id < (int) pem.values.size())
? pem.values[(int) initial_extruder_id]
: -1;
// Bound by the array, not by the filament count. The array is padded to
// MAXIMUM_EXTRUDER_NUMBER on purpose: start G-code addresses HEADS through it
// (fdm_toolchanger_common.json gates M104 T0..T5 on it), and a parallel copy print has
// more heads than filaments by definition. PlaceholderParser clamps an out-of-range
// first_layer_temperature read to filament 0, which is the right temperature when every
// head is printing the same filament. Narrowing this to the filament count leaves the
// secondary carriages unheated.
for (int logical : imex_secondary_logical_slots(
get_imex_active_tools(print), primary_physical, plate_head_map, pem))
if (logical >= 0 && logical < (int) is_extruder_used.size())
is_extruder_used[logical] = true;
}
this->placeholder_parser().set("is_extruder_used", new ConfigOptionBools(is_extruder_used));
{
@@ -3900,6 +3950,103 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
// Sync variant-mapped params into placeholder_parser before processing start gcode
update_placeholder_parser_with_variant_params();
// IDEX/IQEX: set {imex_mode}, {imex_mode_index}, {imex_mode_gcode} placeholders before
// any G-code script is processed so they are available in machine_start_gcode,
// filament_start_gcode, filament_end_gcode, change_filament_gcode, etc.
// {imex_mode_names} and {imex_mode_gcodes} array placeholders are already accessible
// via the printer config (e.g. {imex_mode_names[0]}, {imex_mode_gcodes[1]}).
std::string imex_active_mode;
int imex_active_mode_index = 0;
std::string imex_active_mode_gcode;
m_imex_parallel_mode.clear();
m_imex_head_filament_map.clear();
if (print.config().is_imex.value && !print.objects().empty()) {
const std::string& raw = print.objects().front()->config().imex_parallel_mode.value;
imex_active_mode = raw.empty() ? kImexPrimaryMode : raw;
m_imex_head_filament_map = parse_imex_head_filament_map(
print.objects().front()->config().imex_head_filament_map.value);
// Look `name` up in the printer's mode table, filling in its index, script and
// tool roster. False means the name matches no row, i.e. the mode is unresolved.
// find_imex_mode() owns the "which row, and what if a sibling array is short"
// rule for every IMEX consumer; see IMEXHelpers.hpp.
std::string imex_active_mode_tools;
auto resolve_mode = [&](const std::string& name) {
const ImexMode mode = find_imex_mode(print.config(), name);
if (!mode.found())
return false;
imex_active_mode_index = mode.index;
imex_active_mode_gcode = mode.gcode;
imex_active_mode_tools = mode.active_tools;
return true;
};
// An unresolved mode name falls back to Primary rather than being carried into the
// parallel-mode paths. `imex_parallel_mode` is a plain string stored on the plate and
// matched against the printer's `imex_mode_names` by value, so it goes stale whenever
// the two drift apart -- a mode renamed or deleted after a plate was set to it, or a
// project shared between printer presets that name their modes differently.
//
// Without the fallback the exporter took every "not Primary" branch while every lookup
// keyed on the mode name came back empty, and wrote a file that is wrong rather than
// merely unconfigured: the 1st->2nd layer temperature branch is mutually exclusive with
// the standard one, so an empty active-tool roster meant NO head got its transition and
// all of them held nozzle_temperature_initial_layer for the whole print; and
// imex_suppresses_bare_toolchange() dropped the initial T<n> on the assumption that a
// mode script would select the tool, while the mode script -- also looked up by name --
// did not exist. Print::validate()'s IMEX rules did not catch it either: they resolve
// the same name to an empty tools string, which yields no declared primary and skips
// the guard. Primary is the one interpretation that is always well-formed: single
// carriage, ordinary temperatures, ordinary tool changes.
//
// Warned rather than silent, and warned rather than blocked. Silent is not an option:
// the plate still labels itself with the stale mode and draws no zones (PartPlate's
// zone builder returns early on the same unresolved state), so a user who asked for two
// parts in copy mode would get one with nothing anywhere saying why. Blocking is not an
// option either -- opening someone else's 3MF on a differently-named preset is a
// legitimate way to arrive here, and the Primary interpretation prints correctly, so
// refusing to slice would be a regression for a case that has a good answer.
//
// The second trigger below is the ragged-table case, and it lands here rather than
// anywhere else because the damage is identical: the mode's name resolves, but the
// printer's `imex_mode_active_tools` is too short to reach its row (or the row is
// explicitly empty), so the roster every branch above keys on comes back empty and
// the export takes the parallel path with nothing in it. Nothing used to report
// that -- half the lookup sites in the tree turned a short tools array into "mode
// not found" and the other half did not, and this one did not. One rule now, in
// find_imex_mode(), and one warning, here.
const bool imex_mode_resolved = resolve_mode(imex_active_mode);
if (imex_active_mode != kImexPrimaryMode && (!imex_mode_resolved || imex_active_mode_tools.empty())) {
print.active_step_add_warning(
PrintStateBase::WarningLevel::NON_CRITICAL,
imex_mode_resolved
? Slic3r::format(_(L("The IDEX/IQEX mode \"%1%\" has no tools assigned on the selected printer. "
"Printing in Primary mode instead. Assign the mode's tools in Printer "
"Settings, or pick another mode from the plate's IDEX/IQEX button.")),
imex_active_mode)
: Slic3r::format(_(L("This plate is set to the IDEX/IQEX mode \"%1%\", which the selected printer "
"does not define. Printing in Primary mode instead. Pick a mode from the "
"plate's IDEX/IQEX button, or restore the mode in Printer Settings.")),
imex_active_mode));
imex_active_mode = kImexPrimaryMode;
imex_active_mode_index = 0;
imex_active_mode_gcode.clear();
imex_active_mode_tools.clear();
// Primary is a real row in the table (always the first one), so it may carry its own
// setup script; resolve it the same way any other selected mode would be.
resolve_mode(imex_active_mode);
}
m_imex_parallel_mode = imex_active_mode;
}
this->placeholder_parser().set("imex_mode", imex_active_mode);
this->placeholder_parser().set("imex_mode_index", imex_active_mode_index);
this->placeholder_parser().set("imex_mode_gcode", imex_active_mode_gcode);
// IDEX/IQEX: process mode G-code BEFORE machine_start_gcode so that any
// {global abc = 1} declarations in the mode script are visible to machine_start_gcode.
// The processed result is buffered here and written to the file after temp setup below.
std::string imex_processed_gcode;
if (!imex_active_mode_gcode.empty())
imex_processed_gcode = this->placeholder_parser_process(
"imex_mode_gcode", imex_active_mode_gcode, initial_extruder_id);
// Expand the file header only after the start-up placeholders and writer state are ready.
// Use the regular custom G-code path so invalid placeholders report the template name.
if (!print.config().file_start_gcode.value.empty())
@@ -4073,6 +4220,10 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
file.write(m_writer.set_chamber_temperature(max_chamber_temp, true)); // set chamber_temperature
}
// Write IMEX mode G-code first (processed above), then machine_start_gcode.
if (!imex_processed_gcode.empty())
file.writeln(imex_processed_gcode);
// Write the custom start G-code
file.writeln(machine_start_gcode);
// Mark the end of the machine start g-code so the GCodeProcessor usage-block builder knows where user
@@ -4189,6 +4340,49 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
// Set initial extruder only after custom start G-code.
// Ugly hack: Do not set the initial extruder if the extruder is primed using the MMU priming towers at the edge of the print bed.
file.write(this->set_extruder(initial_extruder_id, 0.));
// IMEX parallel modes only: emit PA for all secondary active tools.
// In primary mode, regular tool-change PA handles each tool as it becomes active.
// In parallel modes no tool changes occur, so every carriage must be addressed
// explicitly here before printing starts.
if (!m_imex_parallel_mode.empty() && m_imex_parallel_mode != kImexPrimaryMode
&& !m_config.physical_extruder_map.values.empty()) {
// initial_physical: pem-translate the print's initial logical extruder so the
// loop can skip the primary head (which emitted PA via the normal path).
// Then pem-invert each active physical head back to its first routed filament
// for the PA setting lookup. Guarded on non-empty pem above.
// Bounds-checked, not get_at() -- see the note on imex_pem_tool_for in
// IMEXHelpers.hpp. A clamped initial_physical would skip whichever active head
// equals pem[0], leaving it with no PA at all.
const int initial_physical =
((int) initial_extruder_id >= 0 &&
(int) initial_extruder_id < (int) m_config.physical_extruder_map.values.size())
? m_config.physical_extruder_map.values[(int) initial_extruder_id]
: -1;
// enable_pressure_advance and pressure_advance are indexed by COLUMN, not by
// filament slot. filament_diameter is one entry per slot and is never
// variant-expanded, so it is the slot-space bound; translate the slot to its column
// with get_filament_config_index(), as the second-layer temperature loop below does.
const int num_pa_filament_slots = (int) print.config().filament_diameter.values.size();
for (int tool_idx : get_imex_active_tools(print)) {
// Unlike the second-layer temperature loop, the primary is skipped here:
// set_extruder() above already emitted its PA with the pem tool qualifier.
if (tool_idx == initial_physical) continue;
const int logical = resolve_filament_for_head(
m_imex_head_filament_map, m_config.physical_extruder_map, tool_idx);
// resolve_filament_for_head() answers in pem's index space -- one entry per
// NOZZLE -- while these two options are indexed per FILAMENT SLOT. The spaces
// diverge when the printer has more nozzles than the project has filaments, and
// get_at() would clamp a past-the-end index to filament 0 and pin its PA onto a
// secondary carriage. Same guard the second-layer temperature loop uses.
if (logical < 0 || logical >= num_pa_filament_slots) continue;
const size_t pa_fi = get_filament_config_index(logical);
if (!print.config().enable_pressure_advance.get_at(pa_fi)) continue;
file.write(m_writer.set_pressure_advance(
print.config().pressure_advance.get_at(pa_fi),
tool_idx));
}
}
}
this->m_objsWithBrim.clear();
@@ -6142,6 +6336,7 @@ std::string GCode::generate_timelapse_gcode(const Print &print, coordf_t print_z
// In non-sequential mode, process_layer is called per each print_z height with all object and support layers accumulated.
// For multi-material prints, this routine minimizes extruder switches by gathering extruder specific extrusion paths
// and performing the extruder specific extrusions together.
LayerResult GCode::process_layer(
const Print &print,
// Set of object & print layers of the same PrintObject and with the same print_z.
@@ -6343,6 +6538,7 @@ LayerResult GCode::process_layer(
+ "\n";
config.set_key_value("max_layer_z", new ConfigOptionFloat(m_max_layer_z));
}
//BBS: set layer time fan speed after layer change gcode
gcode += ";_SET_FAN_SPEED_CHANGING_LAYER\n";
@@ -6506,17 +6702,56 @@ LayerResult GCode::process_layer(
gcode += m_writer.set_jerk_xy(NOZZLE_CONFIG(default_jerk));
}
// Transition from 1st to 2nd layer. Adjust nozzle temperatures as prescribed by the nozzle dependent
// nozzle_temperature_initial_layer vs. temperature settings.
for (const Extruder& extruder : m_writer.extruders()) {
if ((print.config().single_extruder_multi_material.value || m_ooze_prevention.enable) &&
extruder.id() != m_writer.filament()->id())
// In single extruder multi material mode, set the temperature for the current extruder only.
continue;
size_t fi = get_filament_config_index((int)extruder.id());
int temperature = print.config().nozzle_temperature.get_at(fi);
if (temperature > 0 && temperature != print.config().nozzle_temperature_initial_layer.get_at(fi))
gcode += m_writer.set_temperature(temperature, false, extruder.id());
// Transition from 1st to 2nd layer: set non-initial-layer nozzle temperatures.
// IMEX parallel modes: secondary carriages (T1-T3) never receive tool-change commands,
// so they're not in m_writer.extruders() and multiple_extruders==false (max id==0).
// Use the static set_temperature path for IMEX so the T index is always emitted.
// In primary mode, fall through to the standard path — tool changes handle temps normally.
if (!m_imex_parallel_mode.empty() && m_imex_parallel_mode != kImexPrimaryMode) {
// All active tools need explicit temps — none receive tool-change commands,
// so we can't rely on the condition used for non-IMEX (temp != initial_layer_temp).
// A tool whose initial and regular temps are the same still needs to be set here.
// Mutually exclusive with the `else` below, so a head skipped here gets no
// transition at all. `tool_idx` is physical; the printing head uses this layer's
// own filament, the parallel carriages resolve through the head map.
// nozzle_temperature is indexed by column: bound in slot space, or an out-of-slot
// logical reaches get_filament_config_index and comes back as filament 0.
// filament_diameter is the slot-space yardstick -- one entry per filament slot,
// never variant-expanded -- and is the bound the pressure advance loop uses too.
const int num_filament_slots = (int) print.config().filament_diameter.values.size();
// Bounds-checked, not get_at() -- see IMEXHelpers.hpp. A clamp would hand the
// "initial" branch below to whichever secondary sits on pem[0], giving it the wrong
// filament's transition temperature and never its own.
const int initial_physical =
((int) first_extruder_id >= 0 &&
(int) first_extruder_id < (int) m_config.physical_extruder_map.values.size())
? m_config.physical_extruder_map.values[(int) first_extruder_id]
: -1;
for (int tool_idx : get_imex_active_tools(print)) {
const int logical = (tool_idx == initial_physical)
? (int)first_extruder_id
: resolve_filament_for_head(
m_imex_head_filament_map, m_config.physical_extruder_map, tool_idx);
if (logical < 0 || logical >= num_filament_slots) continue;
// Variant-expanded printers column each filament; index as the `else` does.
int temperature = print.config().nozzle_temperature.get_at(get_filament_config_index(logical));
if (temperature > 0)
gcode += GCodeWriter::set_temperature(temperature, m_writer.config.gcode_flavor, false,
tool_idx, "set IMEX tool temperature");
}
} else {
// Adjust nozzle temperatures as prescribed by the nozzle dependent
// nozzle_temperature_initial_layer vs. temperature settings.
for (const Extruder& extruder : m_writer.extruders()) {
if ((print.config().single_extruder_multi_material.value || m_ooze_prevention.enable) &&
extruder.id() != m_writer.filament()->id())
// In single extruder multi material mode, set the temperature for the current extruder only.
continue;
size_t fi = get_filament_config_index((int)extruder.id());
int temperature = print.config().nozzle_temperature.get_at(fi);
if (temperature > 0 && temperature != print.config().nozzle_temperature_initial_layer.get_at(fi))
gcode += m_writer.set_temperature(temperature, false, extruder.id());
}
}
// BBS
@@ -7898,6 +8133,31 @@ void GCode::append_full_config(const Print &print, std::string &str)
// banning it from the dump has no effect on the feature; the only H2C/H2D delta is the M9711/M971
// snapshot reposition.
"farthest_point_timelapse"sv,
// The IMEX (IDEX/IQEX parallel printing) keys are newly-registered printer/process keys whose
// defaults are non-nil, so leaving them in the dump would add fourteen `; imex_* = <default>`
// lines to every printer's config block — an ordinary single-nozzle machine included. Excluding
// them keeps the config-dump byte-identical for the whole shipping fleet; the IMEX printers pay
// the same price the timelapse/prime-volume keys above already pay, and the active mode is still
// visible in the body of their g-code through the injected imex_mode_gcodes macro. Every key is
// read at slice time from m_config (get_imex_active_tools / find_imex_mode / the zone layout
// helpers) and never parsed back out of the dump, so banning them costs the feature nothing:
// the g-code viewer's overlay reads the loaded presets, not the config block, and the two
// per-plate process keys round-trip through the 3MF's model_settings.config plate metadata
// (bbs_3mf.cpp IMEX_PARALLEL_MODE_ATTR / IMEX_HEAD_FILAMENT_MAP_ATTR), not through this dump.
"is_imex"sv,
"imex_firmware_managed_zones"sv,
"imex_gantry_count"sv,
"imex_tools_per_gantry"sv,
"imex_tool_layout"sv,
"imex_nozzle_clearance_x"sv,
"imex_nozzle_clearance_y"sv,
"imex_carriage_margin"sv,
"imex_viz_theme"sv,
"imex_mode_names"sv,
"imex_mode_active_tools"sv,
"imex_mode_gcodes"sv,
"imex_parallel_mode"sv,
"imex_head_filament_map"sv,
"compatible_printers"sv,
"compatible_prints"sv,
"filament_colour_type"sv,
@@ -10304,9 +10564,21 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
gcode += this->placeholder_parser_process("filament_start_gcode", filament_start_gcode, new_filament_id, &config);
check_add_eol(gcode);
}
gcode += set_filament_pressure_advance(new_filament_id);
// In IMEX parallel modes each carriage needs an explicit tool address. In primary
// mode, and on a non-IMEX printer, imex_pem_tool_for() returns -1 and the qualifier
// is omitted, because regular tool changes already handle PA there.
gcode += set_filament_pressure_advance(new_filament_id,
imex_pem_tool_for((int) new_filament_id, m_imex_parallel_mode, m_config.physical_extruder_map));
gcode += m_writer.toolchange(new_filament_id, new_extruder_id);
// Same suppression as the long multi-extruder path: at print-start in IMEX
// parallel modes the user's mode_gcode + machine_start_gcode is responsible
// for tool selection, so the slicer's bare T<n> would be a duplicate. Mid-
// print toolchanges (count > 1) emit normally — Print::validate() blocks
// multi-color in configurations where mid-print T<n> wouldn't make sense
// (see imex_multicolor_block_reason).
const std::string toolchange_command = m_writer.toolchange(new_filament_id, new_extruder_id);
if (!imex_suppresses_bare_toolchange(m_imex_parallel_mode, m_toolchange_count))
gcode += toolchange_command;
if (Extruder *fil = m_writer.filament())
fil->set_config_index((int)get_filament_config_index((int)fil->id()));
return gcode;
@@ -10350,7 +10622,27 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
// If ooze prevention is enabled, park current extruder in the nearest
// standby point and set it to the standby temperature.
if (m_ooze_prevention.enable && m_writer.filament() != nullptr)
//
// Same-physical short-circuit: on an IMEX printer, skip the standby cool-down when the
// old and new filament both route to the same physical extruder via physical_extruder_map
// (AFC/MMU lane swaps where the active heater stays selected and only the lane changes).
// The cool-down → re-heat round trip is pointless there — same nozzle, same heater, just a
// different filament feeding it. post_toolchange below still emits M109 to the new
// filament's print temp, so per-lane temperature differences are still handled.
//
// Gated on is_imex so ooze prevention behaves exactly as upstream on every other printer.
// 73 shipping profiles enable it by default (Snapmaker, Flashforge, Prusa and others) and
// none of them are asking for this optimisation.
auto same_physical_extruder = [&](int a, int b) {
if (!m_config.is_imex.value)
return false;
const auto& pem = m_config.physical_extruder_map.values;
return !pem.empty() && a >= 0 && b >= 0
&& a < (int)pem.size() && b < (int)pem.size()
&& pem[a] == pem[b];
};
if (m_ooze_prevention.enable && m_writer.filament() != nullptr
&& !same_physical_extruder(m_writer.filament()->id(), (int)new_filament_id))
gcode += m_ooze_prevention.pre_toolchange(*this);
// BBS
@@ -10620,7 +10912,13 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
std::string toolchange_command = m_writer.toolchange(new_filament_id, next_nozzle_id);
if (Extruder *fil = m_writer.filament())
fil->set_config_index((int)get_filament_config_index((int)fil->id()));
if (!custom_gcode_changes_tool(toolchange_gcode_parsed, m_writer.toolchange_prefix(), new_filament_id))
// See imex_suppresses_bare_toolchange() — only suppress the print-start initial
// T<n> in parallel modes (the user's imex_mode_gcode + machine_start_gcode owns
// tool activation there). Mid-print T<n> emits normally; Print::validate blocks
// multi-color setups where it wouldn't make sense.
const bool suppress_imex_bare = imex_suppresses_bare_toolchange(m_imex_parallel_mode, m_toolchange_count);
if (!custom_gcode_changes_tool(toolchange_gcode_parsed, m_writer.toolchange_prefix(), new_filament_id)
&& !suppress_imex_bare)
gcode += toolchange_command;
else {
// user provided his own toolchange gcode, no need to do anything
@@ -10693,14 +10991,16 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
if (m_ooze_prevention.enable && !defer_temp_wait)
gcode += m_ooze_prevention.post_toolchange(*this);
gcode += set_filament_pressure_advance(new_filament_id);
// See the note on the single-extruder path above: -1 outside IMEX parallel modes.
gcode += set_filament_pressure_advance(new_filament_id,
imex_pem_tool_for((int) new_filament_id, m_imex_parallel_mode, m_config.physical_extruder_map));
//Orca: tool changer or IDEX's firmware may change Z position, so we set it to unknown/undefined
m_last_pos_defined = false;
return gcode;
}
std::string GCode::set_filament_pressure_advance(unsigned int filament_id)
std::string GCode::set_filament_pressure_advance(unsigned int filament_id, int tool)
{
const size_t fi = get_filament_config_index(filament_id);
if (!m_config.enable_pressure_advance.get_at(fi))
@@ -10708,7 +11008,9 @@ std::string GCode::set_filament_pressure_advance(unsigned int filament_id)
// Orca: Adaptive PA
// Reset Adaptive PA processor last PA value
m_pa_processor->resetPreviousPA(m_config.pressure_advance.get_at(fi));
return m_writer.set_pressure_advance(m_config.pressure_advance.get_at(fi));
// tool >= 0 addresses one IMEX carriage explicitly; -1 omits the qualifier, so every
// caller that does not pass one emits exactly what it did before.
return m_writer.set_pressure_advance(m_config.pressure_advance.get_at(fi), tool);
}
inline std::string polygon_to_string(const Polygon &polygon, Print *print, bool is_print_space = false) {
+22 -1
View File
@@ -262,6 +262,15 @@ public:
void export_layer_filaments(GCodeProcessorResult* result);
//BBS: set offset for gcode writer
void set_gcode_offset(double x, double y) { m_writer.set_xy_offset(x, y); m_processor.set_xy_offset(x, y);}
// IMEX firmware-managed slice: writer offset is augmented by the IMEX shift so the
// emitted gcode is centered at bed origin (firmware fans copies/mirrors out from there).
// Processor offset stays at plate_origin only so the gcode-preview visualizer renders
// the centered toolpath at bed center, not at the prepare-view zone placement. When
// imex_x/imex_y are zero this is byte-identical to set_gcode_offset(x, y).
void set_gcode_offset_with_imex_shift(double x, double y, double imex_x, double imex_y) {
m_writer.set_xy_offset(x + imex_x, y + imex_y);
m_processor.set_xy_offset(x, y);
}
// Exported for the helper classes (OozePrevention, Wipe) and for the Perl binding for unit tests.
const Vec2d& origin() const { return m_origin; }
@@ -302,7 +311,11 @@ public:
std::string unretract(float extra_retract = 0.f) { return m_writer.unlift() + m_writer.unretract(extra_retract); }
std::string set_extruder(unsigned int extruder_id, double print_z, bool by_object=false, int toolchange_temp_override = -1, bool defer_temp_wait = false);
// Sets the pressure advance of the filament's extruder variant, if enabled for it.
std::string set_filament_pressure_advance(unsigned int filament_id);
// tool addresses one IMEX carriage explicitly. -1 means no carriage: it omits the tool
// qualifier on Klipper, Marlin and BBL, and keeps RepRapFirmware's historical `D0`, since a
// bare M572 there applies to whatever tool is selected and errors when none is. That is
// what every non-IMEX caller wants, and what imex_pem_tool_for() returns off IMEX.
std::string set_filament_pressure_advance(unsigned int filament_id, int tool = -1);
bool is_BBL_Printer();
WipeTowerType wipe_tower_type();
@@ -836,6 +849,14 @@ protected:
std::unique_ptr<AdaptivePAProcessor> m_pa_processor;
// IMEX: active parallel mode name ("primary", "copy", "iq-copy", etc.).
// Set at the start of export. Empty string means non-IMEX or not yet set.
// PA and temperature tool-qualification is only applied when this is not "primary".
std::string m_imex_parallel_mode;
// IMEX: parsed per-plate head→filament overrides (physical T-index → 1-based filament slot).
// Cached from imex_head_filament_map at print start. Empty map means "fall back to pem".
std::map<int,int> m_imex_head_filament_map;
std::unique_ptr<WipeTowerIntegration> m_wipe_tower;
std::unique_ptr<SmallAreaInfillFlowCompensator> m_small_area_infill_flow_compensator;
+4
View File
@@ -1472,6 +1472,10 @@ public:
void set_multi_nozzle_group_result(const MultiNozzleUtils::LayeredNozzleGroupResult *multi_nozzle_group_result) { m_multi_nozzle_group_result = multi_nozzle_group_result; }
void set_physical_extruder_map(const std::vector<int> &physical_extruder_map) { m_physical_extruder_map = physical_extruder_map; }
// physical_extruder_map defaults to the single element {0} and is only widened to one entry
// per extruder on IMEX printers, so indexing it by tool is out of range on any other
// multi-extruder machine. Fall back to the tool's own index, matching the bounds-checked
// form GCodeProcessor uses for the same map.
private:
std::string set_normal_acceleration() {
std::vector<unsigned int> accelerations = m_is_first_layer ? m_first_layer_normal_accelerations : m_normal_accelerations;
+22
View File
@@ -28,6 +28,28 @@ static const ConfigOption *option_of(const ConfigBase &config, const char *key)
return nullptr;
}
bool prime_tower_is_printed(const ConfigBase &config, int used_filaments, int num_objects, bool has_mixed_filament)
{
const auto *ept = config.option<ConfigOptionBool>("enable_prime_tower");
if (ept == nullptr || !ept->value)
return false;
// normalize_fdm_2 only reconsiders the option when the plate uses a filament; below that it
// leaves the user's choice alone, and there is no tower to reason about either way.
if (used_filaments <= 0)
return true;
const ConfigOption *timelapse = option_of(config, "timelapse_type");
const bool smooth_timelapse = timelapse != nullptr && timelapse->getInt() == int(TimelapseType::tlSmooth);
const auto *wrapping = config.option<ConfigOptionBool>("enable_wrapping_detection");
const bool enable_wrapping = wrapping != nullptr && wrapping->value;
if (smooth_timelapse || enable_wrapping)
return true;
const ConfigOption *sequence = option_of(config, "print_sequence");
const bool by_object = sequence != nullptr && sequence->getInt() == int(PrintSequence::ByObject);
return !((used_filaments == 1 && !has_mixed_filament) || (by_object && num_objects > 1));
}
WipeTowerType resolve_wipe_tower_type(const ConfigBase &config)
{
// printer_model is what the CLI keys its Bambu Lab detection on; the GUI's vendor flag
+13
View File
@@ -44,4 +44,17 @@ WipeTowerFootprint estimate_wipe_tower_footprint(const ConfigBase
double layer_height,
double max_object_height);
// Whether a prime tower is PRINTED for a plate. The footprint estimate above answers how big a
// tower is and never reads enable_prime_tower or print_sequence, so it reports a size for a plate
// that prints no tower at all; the authority is DynamicPrintConfig::normalize_fdm_2(), which clears
// enable_prime_tower before the plate is sliced. This mirrors that rule so pre-slice consumers can
// ask it without mutating a config, and a test pins the two together.
//
// used_filaments: the plate's filament SLOTS as authored - a mixed slot counts once, matching
// Print::extruders(), which is what normalize_fdm_2 is handed.
// num_objects: objects printed on the plate; only ByObject sequencing looks at it.
// has_mixed_filament: filament_is_mixed is a PROJECT option, so it is passed rather than read -
// `config` only has to carry the print preset's own keys.
bool prime_tower_is_printed(const ConfigBase &config, int used_filaments, int num_objects, bool has_mixed_filament);
} // namespace Slic3r
+43 -17
View File
@@ -6,6 +6,7 @@
#include "Point.hpp"
#include "Polygon.hpp"
#include "PrintConfig.hpp"
#include "IMEXHelpers.hpp"
#include "ClipperUtils.hpp"
#include "Geometry/ArcWelder.hpp"
#include "Line.hpp"
@@ -358,8 +359,16 @@ std::string GCodeWriter::set_temperature(unsigned int temperature, GCodeFlavor f
std::string GCodeWriter::set_temperature(unsigned int temperature, bool wait, int tool) const
{
// set tool to -1 to make sure we won't emit T parameter for single extruder or SEMM
if (!this->multiple_extruders || m_single_extruder_multi_material)
if (!this->multiple_extruders || m_single_extruder_multi_material) {
tool = -1;
} else {
// Every caller of this overload addresses filaments by LOGICAL id, but M104/M109
// name a physical heater -- so translate at the one point they all pass through.
// The static overload below is already physical-in (GCode.cpp:5952,
// GCode/GCodeProcessor.cpp:1410) and must not be remapped, which is why the
// translation lives here and not there.
tool = imex_physical_heater_for(this->config.is_imex.value, this->config.physical_extruder_map, tool);
}
return set_temperature(temperature, this->config.gcode_flavor, wait, tool);
}
@@ -582,26 +591,43 @@ std::string GCodeWriter::set_junction_deviation(double junction_deviation){
return gcode.str();
}
std::string GCodeWriter::set_pressure_advance(double pa) const
std::string GCodeWriter::set_pressure_advance(double pa, int tool) const
{
std::ostringstream gcode;
if (pa < 0)
return gcode.str();
if(m_is_bbl_printers){
//SoftFever: set L1000 to use linear model
gcode << "M900 K" <<std::setprecision(4)<< pa << " L1000 M10 ; Override pressure advance value\n";
}
else{
if (FLAVOR_IS(gcfKlipper))
gcode << "SET_PRESSURE_ADVANCE ADVANCE=" << std::setprecision(4) << pa << "; Override pressure advance value\n";
else if(FLAVOR_IS(gcfRepRapFirmware))
gcode << ("M572 D0 S") << std::setprecision(4) << pa << "; Override pressure advance value\n";
else if (FLAVOR_IS(gcfRepetier))
// Repetier M233: X is quadratic (K), Y is linear (L).
// Applying the value to both parameters simultaneously.
gcode << "M233 X" << std::setprecision(4) << pa << " Y" << std::setprecision(4) << pa << " ; Override pressure advance value\n";
else
gcode << "M900 K" <<std::setprecision(4)<< pa << "; Override pressure advance value\n";
if (m_is_bbl_printers) {
// SoftFever: set L1000 to use linear model
gcode << "M900 K" << std::setprecision(4) << pa << " L1000 M10 ; Override pressure advance value\n";
} else if (FLAVOR_IS(gcfKlipper)) {
// Klipper routes PA by extruder name, not active tool.
// Convention: first extruder is "extruder", subsequent are "extruder1", "extruder2", etc.
gcode << "SET_PRESSURE_ADVANCE ADVANCE=" << std::setprecision(4) << pa;
if (tool > 0)
gcode << " EXTRUDER=extruder" << tool;
else if (tool == 0)
gcode << " EXTRUDER=extruder";
gcode << "; Override pressure advance value\n";
} else if (FLAVOR_IS(gcfRepRapFirmware)) {
// RRF: M572 D<n> targets a specific extruder; a bare M572 applies to whatever tool is
// currently selected and errors when there isn't one. Callers with no tool index keep
// the historical D0 rather than the bare form: PA would otherwise depend on
// tool-selection state for every RRF user, none of whom are asking for IMEX.
gcode << "M572 D" << (tool >= 0 ? tool : 0)
<< " S" << std::setprecision(4) << pa << "; Override pressure advance value\n";
} else if (FLAVOR_IS(gcfRepetier)) {
// Repetier M233: X is quadratic (K), Y is linear (L).
// Applying the value to both parameters simultaneously.
gcode << "M233 X" << std::setprecision(4) << pa << " Y" << std::setprecision(4) << pa << " ; Override pressure advance value\n";
} else if (FLAVOR_IS(gcfMarlinFirmware)) {
// Marlin 2.x supports T parameter for per-extruder LA
gcode << "M900 K" << std::setprecision(4) << pa;
if (tool >= 0)
gcode << " T" << tool;
gcode << "; Override pressure advance value\n";
} else {
// Marlin Legacy and everything else: single-extruder M900, no tool parameter
gcode << "M900 K" << std::setprecision(4) << pa << "; Override pressure advance value\n";
}
return gcode.str();
}
+1 -1
View File
@@ -72,7 +72,7 @@ public:
// Orca: set acceleration and jerk in one command for Klipper
std::string set_accel_and_jerk(unsigned int acceleration, double jerk);
std::string set_junction_deviation(double junction_deviation);
std::string set_pressure_advance(double pa) const;
std::string set_pressure_advance(double pa, int tool = -1) const;
std::string set_input_shaping(char axis, float damp, float freq, std::string type) const;
std::string reset_e(bool force = false);
std::string update_progress(unsigned int num, unsigned int tot, bool allow_100 = false) const;
+220
View File
@@ -0,0 +1,220 @@
#include "libslic3r/IMEXArrange.hpp"
#include <algorithm>
#include <cstddef>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include <libnest2d/common.hpp>
#include "libslic3r/Arrange.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/IMEXHelpers.hpp"
#include "libslic3r/IMEXZones.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/libslic3r.h"
namespace Slic3r {
namespace {
// The arranger's names for the keep-outs. finish() leaves the fences out when it arranges a
// plate inside its zone, where the zone itself is the bed shape.
constexpr const char* kFenceName = "IMEXOutsidePrimaryZone";
constexpr const char* kStripName = "IMEXCollisionZone";
// Each plate computes its zones from where it sits, so the same zone differs in the last bits.
bool same_zone(const BoundingBoxf& a, const BoundingBoxf& b)
{
return is_approx(a.min, b.min) && is_approx(a.max, b.max);
}
BoundingBox inset(BoundingBox box, const Point& by)
{
box.min += by;
box.max -= by;
return box;
}
// The parts of `bed` outside `zone`: full-depth boxes left and right of it, and the pieces in
// front of and behind it. Empty pieces are left out.
std::vector<BoundingBox> boxes_around(const BoundingBox& bed, const BoundingBox& zone)
{
std::vector<BoundingBox> out;
auto add = [&out](coord_t x0, coord_t y0, coord_t x1, coord_t y1) {
if (x1 > x0 && y1 > y0)
out.emplace_back(Point(x0, y0), Point(x1, y1));
};
const coord_t x0 = std::max(zone.min.x(), bed.min.x());
const coord_t x1 = std::min(zone.max.x(), bed.max.x());
add(bed.min.x(), bed.min.y(), zone.min.x(), bed.max.y());
add(zone.max.x(), bed.min.y(), bed.max.x(), bed.max.y());
add(x0, bed.min.y(), x1, zone.min.y());
add(x0, zone.max.y(), x1, bed.max.y());
return out;
}
} // namespace
std::optional<ImexArrangeZones> imex_arrange_zones(const ImexZoneLayout& layout)
{
if (!layout.primary_zone_box)
return std::nullopt;
ImexArrangeZones zones{*layout.primary_zone_box, {}};
for (const BoundingBoxf3& strip : layout.collision_zones)
zones.collision_zones.emplace_back(Vec2d(strip.min.head<2>()), Vec2d(strip.max.head<2>()));
return zones;
}
void ImexArrangeInput::read_config(const DynamicPrintConfig& full_config)
{
// A plate the arrange adds has no mode of its own, so it takes the process preset's.
added_plate.reset();
if (adds_plates) {
const auto* mode = full_config.option<ConfigOptionString>("imex_parallel_mode");
added_plate = imex_arrange_zones(compute_imex_zone_layout(full_config, kImexPrimaryMode, mode ? mode->value : std::string(),
get_extents(full_config.opt<ConfigOptionPoints>("printable_area")->values)));
}
// An estimate: auto brim picks its own width, which brim_width stands in for.
const auto* brim_type = full_config.option<ConfigOptionEnum<BrimType>>("brim_type");
brim = !brim_type || brim_type->value == btNoBrim || brim_type->value == btInnerOnly ?
0. :
full_config.opt_float("brim_width") + full_config.opt_float("brim_object_gap");
}
ImexArranger::ImexArranger(const ImexArrangeInput& input, const arrangement::ArrangeParams& params, const Points& bed)
: m_bed(bed)
, m_bed_bb(bed)
, m_margin(scaled(std::max(0.f, params.bed_shrink_x)), scaled(std::max(0.f, params.bed_shrink_y)))
, m_adds_plates(input.adds_plates)
{
for (size_t i = 0; i < input.beds.size(); ++i)
if (input.beds[i])
m_beds.push_back({int(i), *input.beds[i]});
// The bed shape can be a zone only when every plate being arranged has it, and so does every
// plate the arrange may add.
const auto& first = input.beds.empty() ? std::nullopt : input.beds.front();
if (first &&
std::all_of(input.beds.begin(), input.beds.end(),
[&](const std::optional<ImexArrangeZones>& b) { return b && same_zone(b->primary_zone, first->primary_zone); }) &&
(!input.adds_plates || (input.added_plate && same_zone(input.added_plate->primary_zone, first->primary_zone))))
m_shared_room = room_in(first->primary_zone);
// The beds past the plates being arranged become new plates.
if (input.adds_plates && input.added_plate)
for (int bed_idx = int(input.beds.size()); bed_idx < MAX_NUM_PLATES; ++bed_idx)
m_beds.push_back({bed_idx, *input.added_plate});
// A zone narrower than its margins leaves no room, and its fences then fill the whole bed.
const Point strip_margin = m_margin + Point::Constant(scaled(input.brim));
for (const Bed& b : m_beds) {
if (!m_shared_room)
for (const BoundingBox& box : boxes_around(m_bed_bb, inset(scaled(b.zones.primary_zone), m_margin)))
m_keep_outs.push_back({b.bed_idx, box, true});
for (const BoundingBoxf& strip : b.zones.collision_zones)
m_keep_outs.push_back({b.bed_idx, inset(scaled(strip), -strip_margin), false});
}
}
std::optional<BoundingBox> ImexArranger::room_in(const BoundingBoxf& zone) const
{
BoundingBox room = inset(scaled(zone), m_margin);
room.min = room.min.cwiseMax(m_bed_bb.min);
room.max = room.max.cwiseMin(m_bed_bb.max);
if (room.min.x() >= room.max.x() || room.min.y() >= room.max.y())
return std::nullopt;
return room;
}
Points ImexArranger::bed_shape() const { return m_shared_room ? m_shared_room->polygon().points : m_bed; }
void ImexArranger::add_keep_outs(arrangement::ArrangePolygons& fixed) const
{
for (const KeepOut& keep_out : m_keep_outs) {
arrangement::ArrangePolygon ap;
ap.poly.contour = keep_out.box.polygon();
ap.is_virt_object = true;
ap.bed_idx = keep_out.bed_idx;
ap.height = 1;
ap.name = keep_out.fence ? kFenceName : kStripName;
fixed.push_back(std::move(ap));
}
}
bool ImexArranger::crosses_keep_out(const arrangement::ArrangePolygon& part, int bed_idx) const
{
const Polygon hull = part.transformed_poly().contour;
return std::any_of(m_keep_outs.begin(), m_keep_outs.end(), [&](const KeepOut& keep_out) {
return keep_out.bed_idx == bed_idx && !intersection(hull, keep_out.box.polygon()).empty();
});
}
void ImexArranger::finish(arrangement::ArrangePolygons& items,
const arrangement::ArrangePolygons& before,
const arrangement::ArrangePolygons& fixed,
const arrangement::ArrangeParams& params) const
{
for (arrangement::ArrangePolygon& part : items)
if (crosses_keep_out(part, part.bed_idx))
part.bed_idx = arrangement::UNARRANGED;
// Only fenced plates need centering, and arranging one plate never fences.
if (m_shared_room || !m_adds_plates)
return;
arrangement::ArrangeParams one_plate = params;
one_plate.progressind = {};
for (const Bed& b : m_beds) {
const std::optional<BoundingBox> room = room_in(b.zones.primary_zone);
std::vector<size_t> on_bed;
for (size_t i = 0; i < items.size(); ++i)
if (items[i].bed_idx == b.bed_idx)
on_bed.push_back(i);
if (on_bed.empty() || !room)
continue;
arrangement::ArrangePolygons parts, bed_fixed;
// Bed 0, where the nester takes a part as placeable rather than as one that does not fit.
for (size_t i : on_bed) {
parts.push_back(before[i]);
parts.back().bed_idx = 0;
}
for (const arrangement::ArrangePolygon& ap : fixed)
if (ap.bed_idx == b.bed_idx && ap.name != kFenceName) {
bed_fixed.push_back(ap);
bed_fixed.back().bed_idx = 0;
}
arrangement::arrange(parts, bed_fixed, room->polygon().points, one_plate);
// A canceled pass leaves parts it never reached on bed 0 with their old ids.
if (one_plate.stopcondition && one_plate.stopcondition())
return;
// Keep the first layout when this one does not fit.
if (!std::all_of(parts.begin(), parts.end(), [&](const arrangement::ArrangePolygon& part) {
return part.bed_idx == 0 && !crosses_keep_out(part, b.bed_idx);
}))
continue;
// By-object printing follows the packing order, which this pass numbered 0..n-1: carry
// it over onto the ids the first pass gave these parts.
std::vector<int> ids;
for (size_t i : on_bed)
ids.push_back(items[i].itemid);
std::sort(ids.begin(), ids.end());
for (size_t k = 0; k < on_bed.size(); ++k) {
items[on_bed[k]].translation = parts[k].translation;
items[on_bed[k]].rotation = parts[k].rotation;
items[on_bed[k]].itemid = ids[parts[k].itemid];
}
}
}
} // namespace Slic3r
+101
View File
@@ -0,0 +1,101 @@
#pragma once
#include <optional>
#include <vector>
#include "libslic3r/Arrange.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Point.hpp"
namespace Slic3r {
class DynamicPrintConfig;
struct ImexZoneLayout;
// A plate's IDEX/IQEX zones as Arrange sees them, in plate-local millimeters.
struct ImexArrangeZones
{
BoundingBoxf primary_zone;
std::vector<BoundingBoxf> collision_zones;
};
// The zones of `layout`, none when it has no primary zone, i.e. outside a parallel mode.
std::optional<ImexArrangeZones> imex_arrange_zones(const ImexZoneLayout& layout);
// What an arrange needs to know about the plates it packs into.
struct ImexArrangeInput
{
// Per arranger bed, in order: the zones of the plate it packs into, none for a plate whose
// parts may go anywhere on the bed.
std::vector<std::optional<ImexArrangeZones>> beds;
// Whether the beds past `beds` become new plates, as when arranging every plate, and the
// zones such a plate gets.
bool adds_plates = false;
std::optional<ImexArrangeZones> added_plate;
// Room a strip keeps for the brim: brim width plus its gap to the part, in mm.
double brim = 0.;
// Sets `added_plate` and `brim` from the full config. Call after setting `adds_plates`.
void read_config(const DynamicPrintConfig& full_config);
};
// Keeps an arrange inside each IDEX/IQEX plate's primary zone and out of that zone's carriage
// collision strips.
//
// The arranger packs every bed into one bed shape. When every bed has the same primary zone,
// that shape becomes the zone, which keeps the parts centered in it. Otherwise each plate in a
// parallel mode fences off the rest of its bed with fixed items on its own bed, leaving the
// other plates the whole bed, and finish() arranges each fenced plate again inside its zone,
// since a fenced pile sits against the zone edge nearest the bed's center.
//
// A zone edge inside the bed gets the margin the bed's own edges get, since a copy printed from
// a part at the zone edge sits at the far edge of the bed. A strip also gets the brim, which
// would take the nozzle into it.
class ImexArranger
{
public:
// `bed` is the arranger's bed shape, shrunk by params.bed_shrink_x/y.
ImexArranger(const ImexArrangeInput& input, const arrangement::ArrangeParams& params, const Points& bed);
// Whether any bed has a zone. Nothing below has an effect otherwise.
bool active() const { return !m_beds.empty(); }
// The bed shape to arrange in.
Points bed_shape() const;
// Adds the fixed items that keep parts out of each bed's keep-outs.
void add_keep_outs(arrangement::ArrangePolygons& fixed) const;
// Call after arrangement::arrange(items, fixed, bed_shape(), params), with `before` holding
// `items` as they were before it. Leaves unarranged any part across a keep-out: one that does
// not fit an empty bed is retried there without the bed's fixed items, so a part too big for
// its zone comes back across them. Then centers each fenced plate's parts in its zone.
void finish(arrangement::ArrangePolygons& items, const arrangement::ArrangePolygons& before,
const arrangement::ArrangePolygons& fixed, const arrangement::ArrangeParams& params) const;
private:
struct Bed
{
int bed_idx;
ImexArrangeZones zones;
};
struct KeepOut
{
int bed_idx;
BoundingBox box;
bool fence; // outside the primary zone, rather than a strip
};
std::optional<BoundingBox> room_in(const BoundingBoxf& zone) const;
bool crosses_keep_out(const arrangement::ArrangePolygon& part, int bed_idx) const;
Points m_bed;
BoundingBox m_bed_bb;
Point m_margin;
bool m_adds_plates = false;
std::vector<Bed> m_beds;
std::optional<BoundingBox> m_shared_room;
std::vector<KeepOut> m_keep_outs;
};
} // namespace Slic3r
+712
View File
@@ -0,0 +1,712 @@
#include "libslic3r/IMEXHelpers.hpp"
#include <Eigen/Core>
#include <algorithm>
#include <cstddef>
#include <map>
#include <numeric>
#include <cassert>
#include <cctype>
#include <optional>
#include <set>
#include <sstream>
#include <string>
#include <unordered_set>
#include <boost/format.hpp>
#include <boost/log/trivial.hpp>
#include <vector>
#include <utility>
#include "Config.hpp"
#include "libslic3r.h"
#include "Point.hpp"
#include "BoundingBox.hpp"
#include "Polygon.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/I18N.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/PrintConfig.hpp"
// Mark string for localization and translate.
#define L(s) Slic3r::I18N::translate(s)
namespace Slic3r {
namespace {
// Strip every whitespace byte from a token in place.
//
// Not `::isspace` directly: it takes an int that must be representable as unsigned char
// (or equal EOF), and `char` is signed on all three targets, so any byte >= 0x80 is sign-
// extended to a negative int and the call is undefined. The strings parsed below come from
// printer profiles and 3MF metadata, neither of which is guaranteed ASCII, so the unsigned
// char cast is the same one the rest of the codebase already applies to std::toupper.
void strip_whitespace(std::string& token)
{
token.erase(std::remove_if(token.begin(), token.end(),
[](unsigned char c) { return std::isspace(c) != 0; }),
token.end());
}
} // namespace
ConfigOptionInts effective_physical_extruder_map(const ConfigOptionInts* explicit_pem, int nozzle_count)
{
// One entry per logical extruder, so a profile has authored a map only when its length
// matches. The single-element PrintConfig default does not, and is replaced rather than
// treated as a one-extruder machine.
if (explicit_pem && nozzle_count > 0 && (int) explicit_pem->values.size() == nozzle_count)
return *explicit_pem;
ConfigOptionInts derived;
derived.values.resize(std::max(nozzle_count, 1));
std::iota(derived.values.begin(), derived.values.end(), 0);
return derived;
}
ConfigOptionInts effective_physical_extruder_map(const PresetBundle& pb)
{
const ConfigOptionInts* explicit_pem = pb.project_config.option<ConfigOptionInts>("physical_extruder_map");
if (!explicit_pem || explicit_pem->values.size() < 2)
explicit_pem = pb.printers.get_edited_preset().config.option<ConfigOptionInts>("physical_extruder_map");
const auto* nozzles = pb.printers.get_edited_preset().config.option<ConfigOptionFloats>("nozzle_diameter");
return effective_physical_extruder_map(explicit_pem, nozzles ? (int) nozzles->values.size() : 0);
}
int imex_pem_tool_for(int filament_id, const std::string& parallel_mode, const ConfigOptionInts& pem)
{
const bool imex_parallel = !parallel_mode.empty() && parallel_mode != kImexPrimaryMode;
if (!imex_parallel || pem.values.empty())
return -1;
// Bounds-checked, not get_at() -- see the note in IMEXHelpers.hpp for why the two index
// spaces diverge. Emitting no tool qualifier is correct for a miss; pinning PA onto the
// primary's carriage is not.
if (filament_id < 0 || filament_id >= (int) pem.values.size())
return -1;
return pem.values[filament_id];
}
int imex_physical_heater_for(bool is_imex, const ConfigOptionInts& pem, int logical_id)
{
// Bounds-check rather than get_at(): get_at() CLAMPS to values.front(), which would
// silently retarget an out-of-range id at whatever heater sits in slot 0.
if (!is_imex || logical_id < 0 || logical_id >= (int) pem.values.size())
return logical_id;
return pem.values[logical_id];
}
bool imex_suppresses_bare_toolchange(const std::string& parallel_mode, unsigned int toolchange_count)
{
return toolchange_count <= 1
&& !parallel_mode.empty()
&& parallel_mode != kImexPrimaryMode;
}
std::string imex_multicolor_block_reason(const std::string& parallel_mode,
const std::string& active_tools_str,
int tools_per_gantry,
const std::vector<int>& used_filaments_0b,
const ConfigOptionInts& pem)
{
// Not an IMEX parallel print → nothing to validate here.
if (parallel_mode.empty() || parallel_mode == kImexPrimaryMode) return {};
// Single-color prints have no toolchanges to constrain.
if (used_filaments_0b.size() <= 1) return {};
// MMU lane sharing detection: two or more used filaments routed to the same
// physical head means a mid-print MMU swap on that head, which IMEX parallel
// modes can't slave to the secondary gantry.
if (!pem.values.empty()) {
std::unordered_set<int> seen_physicals;
for (int filament : used_filaments_0b) {
if (filament < 0 || filament >= (int)pem.values.size()) continue;
const int phys = pem.values[filament]; // bounds-checked above; get_at would clamp
if (!seen_physicals.insert(phys).second) {
return L("Multi-color prints in IDEX/IQEX parallel modes require each filament "
"to have its own dedicated physical extruder. Two or more of the active "
"filaments are routed to the same physical head via the printer's "
"physical extruder map (an MMU/AFC manifold), which the slaved gantry "
"cannot follow.");
}
}
}
// Determine which physical head is the primary in this mode.
const int primary_physical = imex_primary_tool_for_mode(active_tools_str);
if (primary_physical < 0) {
return L("The active IDEX/IQEX mode does not define a primary tool, so multi-color "
"printing cannot be scheduled. Open the printer settings IDEX/IQEX Modes "
"editor and assign a Primary role to one tool.");
}
// Walk the active tools once: collect the set of distinct gantries spanned by
// the mode and check for an explicit Span marker on the primary's gantry.
// Span declares "this tool is the multicolor partner of primary on the same
// gantry" — without it, two tools on primary's gantry mean two independent
// copies, not a within-gantry toolchange topology, which can't carry
// multicolor in a parallel mode.
const int tpg = std::max(1, tools_per_gantry);
const int primary_gantry = primary_physical / tpg;
bool span_on_primary_gantry = false;
std::set<int> active_gantries;
for (const auto& [phys, role] : parse_imex_active_tools(active_tools_str)) {
active_gantries.insert(phys / tpg);
if (phys / tpg == primary_gantry && role == ImexRole::Span)
span_on_primary_gantry = true;
}
// Single-gantry mode: the active tools all sit on one gantry, so no second
// gantry is being copied/mirrored to. The "Copy"/"Mirror" label is decorative
// — there's no actual parallel printing happening, just a regular multi-tool
// single-gantry print. Multi-color in this configuration is a misconfiguration
// (use Primary mode for that), and the user's mode_gcode would still emit
// parallel-print firmware setup that doesn't apply here.
if (active_gantries.size() < 2) {
return L("Multi-color in this mode isn't a parallel-print scenario — all of the "
"active tools sit on a single gantry, so there's no second gantry being "
"copied or mirrored to. Switch the plate to Primary mode for multi-color "
"printing on a single gantry, or define an IDEX/IQEX mode that includes "
"tools on a second gantry.");
}
// Dual-gantry mode but no Span tool on the primary's gantry — the slicer would
// emit toolchanges between filaments but the mode doesn't declare a within-gantry
// multicolor partner, so the swap can't carry. The user might intend independent
// copies (each gantry tool prints its own object) which is incompatible with
// mid-print multicolor in a parallel mode.
if (!span_on_primary_gantry) {
return L("Multi-color prints in IDEX/IQEX parallel modes require a Span tool on the "
"primary's gantry — without one, the mode doesn't declare a within-gantry "
"multicolor partner. Either reduce the print to a single filament, switch to "
"Primary mode, or open the printer settings IDEX/IQEX Modes editor and mark a "
"tool on the primary's gantry as Span.");
}
// Only the primary and its Span partners print the plate's colors; every other active head
// replays them. A color routed anywhere else -- a copying head, one the mode leaves inactive,
// or past the end of the map -- has no head of its own to print from.
std::set<int> color_heads{ primary_physical };
for (const auto& [phys, role] : parse_imex_active_tools(active_tools_str))
if (phys / tpg == primary_gantry && role == ImexRole::Span)
color_heads.insert(phys);
for (int filament : used_filaments_0b) {
const int phys = pem.values.empty() ? filament
: (filament >= 0 && filament < (int) pem.values.size()) ? pem.values[filament] : -1;
if (color_heads.count(phys))
continue;
std::string tools;
for (int head : color_heads)
tools += (tools.empty() ? "T" : "/T") + std::to_string(head);
// Filaments are numbered from 1 as the sidebar shows them, heads by their T index; the map
// decides which head a filament is on, so with MMU lanes the two numbers differ.
if (phys < 0)
return (boost::format(L("In this Span mode only %1% print colors, but filament %2% isn't routed to any "
"tool. Use filaments routed to %1%, or switch this plate to Primary mode.")) %
tools % (filament + 1)).str();
return (boost::format(L("In this Span mode only %1% print colors, but filament %2% is routed to T%3%. Use "
"filaments routed to %1%, or switch this plate to Primary mode.")) %
tools % (filament + 1) % phys).str();
}
return {};
}
int first_filament_for_physical_head(const ConfigOptionInts& pem, int physical)
{
const auto& v = pem.values;
if (v.empty())
return (physical == 0) ? 0 : -1; // degenerate: treat as identity on head 0
for (size_t i = 0; i < v.size(); ++i) {
if (v[i] == physical)
return static_cast<int>(i);
}
return -1;
}
bool has_mmu(const ConfigOptionInts& pem)
{
if (pem.values.size() < 2)
return false;
std::unordered_set<int> seen;
for (int pv : pem.values) {
if (!seen.insert(pv).second)
return true;
}
return false;
}
bool has_non_primary_mmu(const ConfigOptionInts& pem, int primary_physical)
{
if (pem.values.size() < 2)
return false;
std::unordered_set<int> seen;
for (int pv : pem.values) {
if (pv == primary_physical)
continue;
if (!seen.insert(pv).second)
return true;
}
return false;
}
// The def-side match is on type() rather than a dynamic_cast: ConfigOptionPercent and
// ConfigOptionFloatOrPercent both derive from ConfigOptionFloat, so a cast would accept a
// registration whose value cfg.option<ConfigOptionFloat>() had just refused, and silently return
// the default while a real value sat in the config. For the int, float and bool accessors here
// both halves therefore agree on the type. imex_cfg_enum() is the exception and goes the other
// way -- dynamic_cast on both halves -- because every ConfigOptionEnum<T> reports coEnum and a
// type() check cannot tell one enum type from another; see the comment on it in IMEXHelpers.hpp.
//
// Reaching the final return means the key is not registered at all -- a typo in the literal, which
// the compiler cannot catch. For the clearances that would be worse than the literal it replaced:
// zero suppresses every collision strip. assert() is compiled out under NDEBUG, so it catches this
// in a debug build only; the log line is what remains in Release, and the return is still a
// degraded value. This is a programming-error path, not a runtime-input one.
void imex_cfg_report_unregistered(const char* fn, const std::string& key)
{
BOOST_LOG_TRIVIAL(error) << fn << ": no registered default for " << key;
}
int imex_cfg_int(const ConfigBase& cfg, const std::string& key)
{
if (const auto* opt = cfg.option<ConfigOptionInt>(key))
return opt->value;
if (const ConfigOptionDef* def = print_config_def.get(key))
if (const ConfigOption* dv = def->default_value.get())
if (dv->type() == ConfigOptionInt::static_type())
return static_cast<const ConfigOptionInt*>(dv)->value;
assert(false && "imex_cfg_int: key not registered in print_config_def");
imex_cfg_report_unregistered("imex_cfg_int", key);
return 0;
}
bool imex_cfg_bool(const ConfigBase& cfg, const std::string& key)
{
if (const auto* opt = cfg.option<ConfigOptionBool>(key))
return opt->value;
if (const ConfigOptionDef* def = print_config_def.get(key))
if (const ConfigOption* dv = def->default_value.get())
if (dv->type() == ConfigOptionBool::static_type())
return static_cast<const ConfigOptionBool*>(dv)->value;
assert(false && "imex_cfg_bool: key not registered in print_config_def");
imex_cfg_report_unregistered("imex_cfg_bool", key);
return false;
}
double imex_cfg_float(const ConfigBase& cfg, const std::string& key)
{
if (const auto* opt = cfg.option<ConfigOptionFloat>(key))
return opt->value;
if (const ConfigOptionDef* def = print_config_def.get(key))
if (const ConfigOption* dv = def->default_value.get())
if (dv->type() == ConfigOptionFloat::static_type())
return static_cast<const ConfigOptionFloat*>(dv)->value;
assert(false && "imex_cfg_float: key not registered in print_config_def");
imex_cfg_report_unregistered("imex_cfg_float", key);
return 0.0;
}
char imex_role_letter(ImexRole role)
{
for (const ImexRoleDesc& d : kImexRoleTable)
if (d.role == role)
return d.letter;
// Only reachable if a role was added to the enum but not to kImexRoleTable. Serializing
// it as Copy keeps the string parseable rather than emitting a token no parser accepts.
return 'C';
}
ImexRole imex_role_from_suffix(const std::string& suffix)
{
if (suffix.size() == 1)
for (const ImexRoleDesc& d : kImexRoleTable)
if (d.letter == suffix[0])
return d.role;
return ImexRole::Copy;
}
std::vector<std::pair<int, ImexRole>> parse_imex_active_tools(const std::string& active_tools_for_mode)
{
std::vector<std::pair<int, ImexRole>> out;
if (active_tools_for_mode.empty()) return out;
std::istringstream ss(active_tools_for_mode);
std::string tok;
while (std::getline(ss, tok, ',')) {
strip_whitespace(tok);
if (tok.empty()) continue;
const auto colon = tok.find(':');
const std::string idx_str = (colon == std::string::npos) ? tok : tok.substr(0, colon);
int phys = -1;
try { phys = std::stoi(idx_str); } catch (...) { continue; }
if (phys < 0 || phys >= (int)MAXIMUM_EXTRUDER_NUMBER) continue;
// A bare token (no ':') is Copy here by design -- see the header note; the
// "bare == Primary" backwards-compat rule lives in imex_primary_tool_for_mode().
const ImexRole role = (colon == std::string::npos)
? ImexRole::Copy
: imex_role_from_suffix(tok.substr(colon + 1));
out.emplace_back(phys, role);
}
return out;
}
ImexModeKind imex_mode_kind(const std::string& active_tools_for_mode)
{
const int primary = imex_primary_tool_for_mode(active_tools_for_mode);
std::set<ImexRole> roles;
for (const auto& [phys, role] : parse_imex_active_tools(active_tools_for_mode))
if (phys != primary && role != ImexRole::Primary)
roles.insert(role);
if (roles.empty())
return ImexModeKind::Primary;
if (roles.count(ImexRole::Span))
return ImexModeKind::Custom;
return roles.count(ImexRole::Mirror) ? ImexModeKind::Mirror : ImexModeKind::Copy;
}
ImexGantryGrouping group_imex_active_tools_by_gantry(const std::string& active_tools_for_mode,
int tools_per_gantry)
{
ImexGantryGrouping out;
const int tpg = std::max(1, tools_per_gantry);
const int primary = imex_primary_tool_for_mode(active_tools_for_mode);
if (primary < 0)
return out;
out.primary_phys = primary;
out.primary_gantry = primary / tpg;
const int primary_col = primary % tpg;
// Bucket every active tool by its gantry index. Skip the primary entry itself
// since it carries no role-driven semantics for grouping (it's just the source).
std::map<int, std::vector<std::pair<int, ImexRole>>> by_gantry;
bool span_seen = false;
for (const auto& [phys, role] : parse_imex_active_tools(active_tools_for_mode)) {
if (phys < 0) continue;
const int g = phys / tpg;
by_gantry[g].emplace_back(phys, role);
if (g == out.primary_gantry && role == ImexRole::Span)
span_seen = true;
}
out.span_on_primary = span_seen;
for (auto& [g, tools] : by_gantry) {
ImexGantryGroup grp;
grp.gantry_index = g;
grp.tools = tools;
// Pick representative: column-paired to primary if active, else lowest phys.
const int paired_phys = g * tpg + primary_col;
auto pick = std::find_if(tools.begin(), tools.end(),
[&](const auto& pr) { return pr.first == paired_phys; });
if (pick == tools.end())
pick = std::min_element(tools.begin(), tools.end(),
[](const auto& a, const auto& b) { return a.first < b.first; });
grp.representative_phys = pick->first;
grp.representative_role = pick->second;
// Aggregate iff Span is on primary's gantry, this is a non-primary gantry,
// it has ≥2 tools, and all those tools share the same role (so the merged
// visualization is unambiguous). Mixed-role gantries fall back to per-tool.
const bool same_role = std::all_of(tools.begin(), tools.end(),
[&](const auto& pr) { return pr.second == grp.representative_role; });
grp.aggregate = span_seen
&& g != out.primary_gantry
&& tools.size() >= 2
&& same_role;
out.groups.push_back(std::move(grp));
}
return out;
}
int imex_primary_tool_for_mode(const std::string& active_tools_for_mode)
{
if (active_tools_for_mode.empty())
return -1;
std::istringstream ss(active_tools_for_mode);
std::string token;
int first_bare = -1;
while (std::getline(ss, token, ',')) {
strip_whitespace(token);
if (token.empty())
continue;
const auto colon = token.find(':');
const std::string idx_str = (colon == std::string::npos) ? token : token.substr(0, colon);
int idx = -1;
try {
idx = std::stoi(idx_str);
} catch (...) {
continue;
}
if (idx < 0)
continue;
if (colon == std::string::npos) {
// Backwards-compat: a bare index is Primary.
if (first_bare < 0)
first_bare = idx;
continue;
}
// Same suffix reading as parse_imex_active_tools(), so the two can never disagree
// about which letter means Primary.
if (imex_role_from_suffix(token.substr(colon + 1)) == ImexRole::Primary)
return idx;
}
return first_bare;
}
std::map<int,int> parse_imex_head_filament_map(const std::string& s)
{
std::map<int,int> result;
if (s.empty()) return result;
std::istringstream ss(s);
std::string token;
while (std::getline(ss, token, ',')) {
strip_whitespace(token);
if (token.empty()) continue;
auto colon = token.find(':');
if (colon == std::string::npos) continue;
try {
int phys = std::stoi(token.substr(0, colon));
int slot = std::stoi(token.substr(colon + 1));
// Same absolute sanity cap parse_imex_active_tools applies to its own physical
// index: neither can exceed the slicer-wide extruder ceiling. This only rejects
// nonsense; the bound that matters (slot must index a filament that exists) is
// in resolve_filament_for_head, which is the one that knows the count.
if (phys >= 0 && phys < (int) MAXIMUM_EXTRUDER_NUMBER
&& slot >= 1 && slot <= (int) MAXIMUM_EXTRUDER_NUMBER)
result[phys] = slot;
} catch (...) {}
}
return result;
}
int imex_primary_logical_from_objects(const std::vector<int>& used_slots_1b,
const ConfigOptionInts& pem,
int primary_physical)
{
if (pem.values.empty()) return -1;
const int pem_size = (int)pem.values.size();
for (int slot_1b : used_slots_1b) {
const int slot_0b = slot_1b - 1;
if (slot_0b >= 0 && slot_0b < pem_size && pem.values[slot_0b] == primary_physical)
return slot_0b;
}
return -1;
}
ImexRouting imex_resolve_routing(const ConfigBase& cfg,
const std::string& parallel_mode,
const std::vector<int>& used_slots_1b,
const ConfigOptionInts& pem)
{
ImexRouting out;
// An empty mode is a plate that never carried one; kImexPrimaryMode is the reserved
// "no parallel printing" sentinel. Neither resolves a row, so neither is looked up.
if (parallel_mode.empty() || parallel_mode == kImexPrimaryMode)
return out;
out.parallel = true;
// One lookup of the mode table, one parse of its roster, one search for the Primary
// marker. Every field below is derived from these, so no caller can re-derive one of
// them differently.
out.active_tools = find_imex_mode(cfg, parallel_mode).active_tools;
out.tools = parse_imex_active_tools(out.active_tools);
out.primary_phys = imex_primary_tool_for_mode(out.active_tools);
if (out.primary_phys >= 0)
out.primary_logical = imex_primary_logical_from_objects(used_slots_1b, pem, out.primary_phys);
// Bounds-checked exactly as imex_primary_logical_from_objects checks: get_at() would
// clamp an out-of-range slot to values.front(), which is how a head that nothing routes
// to can end up listed as a head something routes to.
out.routed_heads.reserve(used_slots_1b.size());
for (int slot_1b : used_slots_1b) {
const int slot_0b = slot_1b - 1;
if (slot_0b >= 0 && slot_0b < (int) pem.values.size())
out.routed_heads.push_back(pem.values[slot_0b]);
}
std::sort(out.routed_heads.begin(), out.routed_heads.end());
out.routed_heads.erase(std::unique(out.routed_heads.begin(), out.routed_heads.end()),
out.routed_heads.end());
// The pem emptiness test is not folded into primary_logical: an unpopulated map means
// the printer has declared no routing at all, which is not the same claim as "this
// plate's filaments go somewhere else", and only the latter is an error.
out.primary_unrouted = out.primary_phys >= 0 && !pem.values.empty() && out.primary_logical < 0;
return out;
}
std::vector<int> imex_secondary_logical_slots(const std::vector<int>& active_physicals,
int primary_physical,
const std::map<int,int>& plate_head_filament_map,
const ConfigOptionInts& pem)
{
std::vector<int> out;
std::unordered_set<int> seen;
for (int phys : active_physicals) {
if (phys == primary_physical) continue;
const int logical = resolve_filament_for_head(plate_head_filament_map, pem, phys);
if (logical < 0) continue;
if (seen.insert(logical).second)
out.push_back(logical);
}
return out;
}
int resolve_filament_for_head(const std::map<int,int>& plate_map,
const ConfigOptionInts& pem,
int physical)
{
auto it = plate_map.find(physical);
if (it != plate_map.end()) {
const int zero_based = it->second - 1;
// pem is indexed by filament slot here, so an override outside it names a filament
// this printer cannot route. Bounding here rather
// than at the parse site is deliberate: the parser is handed a raw string with no
// notion of how many filaments the project has, while every consumer of this
// function's result indexes a per-filament array. The picker only ever offers
// indices into pem (IMEXFilamentPickerPopover::build_row), so nothing the UI can
// author is rejected -- only a hand-edited or corrupt 3MF. Such an override falls
// through to the printer's own routing, i.e. it behaves as if it were absent,
// rather than resolving to a wrong filament that get_at() would silently clamp.
if (zero_based >= 0 && zero_based < (int) pem.values.size())
return zero_based;
}
return first_filament_for_physical_head(pem, physical);
}
ImexMirrorAxis imex_mirror_axis_for(int primary_phys, int target_phys, int tools_per_gantry)
{
const int tpg = std::max(1, tools_per_gantry);
return (target_phys / tpg) == (primary_phys / tpg) ? ImexMirrorAxis::X : ImexMirrorAxis::Y;
}
Transform3d imex_head_transform(int /*primary*/, int /*target*/, ImexRole role,
const Vec2d& gantry_offset,
const Vec2d& primary_zone_center,
ImexMirrorAxis mirror_axis)
{
switch (role) {
case ImexRole::Primary:
case ImexRole::Span:
// Span is the within-gantry multicolor partner of Primary: it prints the same
// objects in the same zone via mid-print toolchanges, so it has no zone of its own
// to be translated or reflected into. Identity is the only transform that keeps it
// on top of the primary. calc_imex_ghosts skips Span outright (no ghost is baked
// for it, since the primary's ghost already covers that zone), so today this case
// is unreachable — it is here so that a future caller which does reach it gets the
// right answer rather than the fall-through, and so -Wswitch stays quiet.
return Transform3d::Identity();
case ImexRole::Copy:
return Eigen::Translation3d(gantry_offset.x(), gantry_offset.y(), 0.0)
* Transform3d::Identity();
case ImexRole::Mirror: {
const double len2 = gantry_offset.squaredNorm();
if (len2 < 1e-12)
return Transform3d::Identity();
// True reflection about the zone-boundary plane between the two zones: perpendicular
// to `mirror_axis`, passing through the midpoint of the zone centers along it. The
// ghost lands at the mirrored position within the target zone — matching where the
// mirror tool actually prints — and drag reflects across that plane, so the mirrored
// axis inverts while the other translates 1:1.
// X: linear = diag(-1, 1, 1), translation = (2*center.x + off.x, off.y, 0)
// Y: linear = diag( 1,-1, 1), translation = (off.x, 2*center.y + off.y, 0)
// Both have det = -1: a real mirror image, not a 180° rotation (which would be
// diag(-1,-1,1), det = +1, and would print the primary's part merely turned around).
Transform3d out = Transform3d::Identity();
if (mirror_axis == ImexMirrorAxis::Y) {
out.linear() = Eigen::DiagonalMatrix<double, 3>(1.0, -1.0, 1.0);
out.translation() = Vec3d(gantry_offset.x(),
2.0 * primary_zone_center.y() + gantry_offset.y(),
0.0);
} else {
out.linear() = Eigen::DiagonalMatrix<double, 3>(-1.0, 1.0, 1.0);
out.translation() = Vec3d(2.0 * primary_zone_center.x() + gantry_offset.x(),
gantry_offset.y(),
0.0);
}
return out;
}
}
return Transform3d::Identity();
}
Vec2d compute_imex_slice_offset(bool firmware_managed,
const std::string& parallel_mode,
const std::optional<BoundingBoxf>& primary_zone_box)
{
if (!firmware_managed) return Vec2d::Zero();
if (parallel_mode.empty()) return Vec2d::Zero();
if (parallel_mode == kImexPrimaryMode) return Vec2d::Zero();
if (!primary_zone_box.has_value()) return Vec2d::Zero();
return primary_zone_box->center();
}
namespace {
// Builds the ImexMode for row `i`, padding whatever sibling array does not reach it.
// `names` is known non-null and `i` known in range; `tools` / `gcodes` may be either.
ImexMode imex_mode_at(const ConfigOptionStrings* names,
const ConfigOptionStrings* tools,
const ConfigOptionStrings* gcodes,
size_t i)
{
ImexMode m;
m.index = (int) i;
m.name = names->values[i];
const bool has_tools = tools != nullptr && i < tools->values.size();
const bool has_gcode = gcodes != nullptr && i < gcodes->values.size();
if (has_tools) m.active_tools = tools->values[i];
if (has_gcode) m.gcode = gcodes->values[i];
m.ragged = !has_tools || !has_gcode;
return m;
}
} // namespace
ImexMode find_imex_mode(const ConfigBase& cfg, const std::string& name)
{
const auto* names = cfg.option<ConfigOptionStrings>("imex_mode_names");
if (names == nullptr)
return {};
// The siblings are fetched once, not per row: a missing option is the same answer for
// every row, and it is padding rather than a reason to fail the lookup.
const auto* tools = cfg.option<ConfigOptionStrings>("imex_mode_active_tools");
const auto* gcodes = cfg.option<ConfigOptionStrings>("imex_mode_gcodes");
for (size_t i = 0; i < names->values.size(); ++i)
if (names->values[i] == name)
return imex_mode_at(names, tools, gcodes, i);
return {};
}
std::vector<ImexMode> imex_mode_table(const ConfigBase& cfg)
{
std::vector<ImexMode> table;
const auto* names = cfg.option<ConfigOptionStrings>("imex_mode_names");
if (names == nullptr)
return table;
const auto* tools = cfg.option<ConfigOptionStrings>("imex_mode_active_tools");
const auto* gcodes = cfg.option<ConfigOptionStrings>("imex_mode_gcodes");
table.reserve(names->values.size());
for (size_t i = 0; i < names->values.size(); ++i)
table.push_back(imex_mode_at(names, tools, gcodes, i));
return table;
}
std::vector<std::string> imex_plate_mode_choices(const ConfigBase& cfg)
{
std::vector<std::string> choices{ kImexPrimaryMode };
for (const ImexMode& m : imex_mode_table(cfg))
if (!m.name.empty() && std::find(choices.begin(), choices.end(), m.name) == choices.end())
choices.push_back(m.name);
return choices;
}
bool imex_hull_violates_zones(const std::vector<BoundingBoxf3>& zones, const Polygon& hull)
{
if (hull.points.empty())
return false;
for (const auto& box : zones) {
if (!intersection({box.polygon(true)}, {hull}).empty())
return true;
}
return false;
}
} // namespace Slic3r
+609
View File
@@ -0,0 +1,609 @@
#pragma once
#include <cassert>
#include <cstddef>
#include <map>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include <Eigen/Geometry>
#include "Config.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
namespace Slic3r {
class PresetBundle;
// Reserved sentinel name for the always-present, non-deletable Primary mode in
// `imex_mode_names`. Comparison with this constant indicates "no parallel printing
// in effect" — IMEX zone visualization, ghost rendering, secondary-tool PA / temp
// emission, and the 3MF metadata serialization all short-circuit when the active
// plate mode equals this. Treat any value NOT equal to this as a parallel mode.
inline constexpr const char* kImexPrimaryMode = "primary";
// =============================================================================
// The printer's IMEX mode table
// =============================================================================
// `imex_mode_names`, `imex_mode_active_tools` and `imex_mode_gcodes` are three
// independent ConfigOptionStrings coupled only by POSITION: entry i of each describes
// mode i. Nothing in the config system enforces equal length, and all three are on-disk
// format, so a hand-edited preset, a profile written before a key existed, or a 3MF from
// another build can arrive with a sibling array shorter than `imex_mode_names`.
//
// ImexMode is the in-memory view of one row. It is NOT a config type — nothing
// serializes it, and the three keys keep their names, their types and their on-disk
// representation exactly as before.
//
// Resolution rule. Applied by find_imex_mode() / imex_mode_table() and NOWHERE else;
// every consumer in the tree goes through one of the two:
// * `imex_mode_names` is the roster. A mode exists iff a row of that array carries its
// name, and the FIRST such row wins. (The modes editor makes a name unique when it is
// committed, so duplicates reach here from a hand-edited profile; first-match is what a
// std::find over the names array already did, and what the editor's own row order
// means.)
// * A sibling array too short to reach that row yields an EMPTY string for that field
// and sets `ragged`. It never yields an out-of-range read, and it never degrades to
// "no such mode": a missing script is a mode with no script, and the row's index
// still has to be reported to {imex_mode_index}. Callers that need tools already
// treat an empty tools string as "no tools" (imex_primary_tool_for_mode() returns -1,
// parse_imex_active_tools() returns {}), which is the behavior they had before.
// * `ragged` separates "the profile omitted this row" from "the author wrote an empty
// entry" — a distinction every open-coded call site used to lose.
//
// Reporting: a non-Primary mode that resolves to an empty tool roster produces G-code
// that is wrong rather than merely unconfigured (no per-head temperature transition, and
// a suppressed initial T<n>), so GCode::_do_export() warns and falls back to Primary,
// exactly as it already does for a name that resolves to no row at all.
struct ImexMode
{
// Position in `imex_mode_names`. -1 means no row carries the requested name — the
// not-found case, explicit rather than implied by an out-of-range index.
int index = -1;
// The row's entry in `imex_mode_names`. Empty when !found().
std::string name;
// `imex_mode_active_tools[index]`, or "" when that array is shorter than index + 1.
std::string active_tools;
// `imex_mode_gcodes[index]`, or "" when that array is shorter than index + 1.
std::string gcode;
// At least one sibling array was too short to cover `index`. Always false when
// !found().
bool ragged = false;
bool found() const { return this->index >= 0; }
explicit operator bool() const { return this->found(); }
};
// Resolves `name` against the printer's mode table; see the rule above. `cfg` is any
// config carrying the printer keys (a DynamicPrintConfig, a PrintConfig, ...). A config
// with no `imex_mode_names` yields a not-found ImexMode rather than throwing, so callers
// need no null check of their own.
ImexMode find_imex_mode(const ConfigBase& cfg, const std::string& name);
// The whole roster: one ImexMode per entry of `imex_mode_names`, in config order, padded
// by the same rule. Empty when `imex_mode_names` is absent. Use this where the caller
// wants every mode (the modes editor, the plate's mode menu) rather than one by name.
std::vector<ImexMode> imex_mode_table(const ConfigBase& cfg);
// The modes a plate can be set to, in the order its mode button cycles them: kImexPrimaryMode,
// then each name in the table once. A repeated name is listed at its first row only, the one
// find_imex_mode() resolves it to, and empty names and rows named kImexPrimaryMode are left out.
std::vector<std::string> imex_plate_mode_choices(const ConfigBase& cfg);
// =============================================================================
// PHYSICAL vs LOGICAL extruder indices — read this before adding a new IMEX call site
// =============================================================================
// IMEX has two distinct index spaces and they are NOT interchangeable on MMU/AFC
// printers (where multiple logical filament slots share one physical extruder):
//
// * PHYSICAL extruder index — identifies a hardware carriage / hotend.
// Source: `imex_mode_active_tools` ("0:P,1:C,2:M") and `printer_extruder_id`.
// Use for: user-facing labels (shown as "T0", "T1"...), per-firmware tool
// qualifiers (Klipper EXTRUDER=extruderN, RRF M572 D<N>, Marlin T<N>), and
// anything that addresses hardware.
//
// * LOGICAL filament slot index — identifies one filament in the project.
// Source: indexing into per-filament arrays — `filament_presets`,
// `nozzle_temperature_initial_layer`, bed_temp arrays per plate type, etc.
// Use for: looking up filament data by slot.
//
// On a single-extruder or pure-IDEX printer the two indices coincide. On any
// printer with `physical_extruder_map` size > 1 they diverge. Confusing them
// produces silent wrong-filament behavior — the slicer reads the wrong filament
// preset / temperature for a given carriage with no error or log line.
//
// Translate physical → logical via:
// resolve_filament_for_head(plate_head_filament_map, pem, physical_idx)
// (or the simpler `first_filament_for_physical_head` if no per-plate override).
//
// The reverse translation (logical → physical) is `imex_pem_tool_for`. Do NOT reach for
// `pem.get_at(logical_idx)`: get_at() CLAMPS an out-of-range index to values.front(), silently
// answering "physical head pem[0]" for a logical id that has no mapping at all. The map is one
// entry per nozzle while logical ids index filament slots, and nothing caps the slot count at
// the nozzle count, so out-of-range is reachable. Bounds-check and treat the miss as "no
// mapping" (-1).
//
// The same nozzle-vs-slot divergence bites in the other direction: anything derived from pem
// -- `resolve_filament_for_head`, `first_filament_for_physical_head` -- answers in NOZZLE index
// space, so bound it against the FILAMENT SLOT COUNT before using it as a filament id. Not
// against the option you are about to read: a variant-expanded option such as
// nozzle_temperature is as long as its columns, and is_extruder_used is padded to
// MAXIMUM_EXTRUDER_NUMBER, so both admit slots the project does not have.
//
// A miss is -1, and -1 is a correct tool QUALIFIER but not a skip-the-primary sentinel: no
// physical head equals it, so a test written as `head == primary` stops skipping anything. Do
// not substitute the mode's declared primary there. Which head prints a filament is answered by
// the filament and pem, never by a mode role: a primary-mode print may use any or all tools, one
// at a time, and pem is what says which. When pem cannot answer, the head is genuinely unknown -
// an omitted qualifier then applies to the active tool, which is the head printing that filament,
// so the honest options are to leave it unqualified or to emit nothing, not to name a carriage.
// Known gap, unreported: the two skip sites in GCode.cpp therefore skip nothing for a filament
// slot past the end of pem, so a plate with more slots than nozzles double-writes the primary's
// pressure advance and hands it another filament's transition temperature.
//
// -1 as a tool qualifier reaches GCodeWriter::set_pressure_advance, which omits the qualifier
// on Klipper, Marlin and BBL but substitutes the historical `D0` on RepRapFirmware
// (GCodeWriter.cpp, set_pressure_advance) -- deliberate, not a miss.
//
// Past bugs in this class:
// - GCode PA emission used the inline `pem.get_at(filament_id)` form at two
// sites (consolidated into `imex_pem_tool_for` in c2492ccc47).
// - Pre-slice warnings indexed `filament_presets` directly by physical idx,
// causing wrong-filament names on AFC/MMU layouts (fixed in fbc58d2a1d).
// - Ghost color resolution had its own inline pem lookup with a stale default
// (centralized in `effective_physical_extruder_map` in 6a3de6a28f).
//
// New call sites: if you index a per-filament array, you need a logical index.
// If you label a hardware carriage, use the physical index. When in doubt,
// route through one of the helpers below.
// =============================================================================
// physical_extruder_map has one entry per LOGICAL extruder -- the index space of
// nozzle_diameter -- with each value a physical extruder index. It is NOT derivable 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 two nozzles and {1,1,2,2,2}.
//
// A profile has authored a map only when its length matches nozzle_count; otherwise the identity
// is returned, which is the same default upstream applies (see Plater.cpp's extruder_map).
ConfigOptionInts effective_physical_extruder_map(const ConfigOptionInts* explicit_pem, int nozzle_count);
// GUI overload: resolves the effective pem from a live PresetBundle using the project_config
// → printer preset fallback. Use this instead of open-coding the lookup at ghost-color,
// tooltip, click-gate and cache-key call sites so they all agree with what the slicer sees.
ConfigOptionInts effective_physical_extruder_map(const PresetBundle& pb);
// Returns the physical extruder index to emit as a per-tool qualifier (PA / temperature)
// for the given filament slot, or -1 when the current IMEX state does not warrant a
// per-tool qualification: non-IMEX / primary mode, or the pem is empty / unpopulated.
// Used at tool-change and second-layer transition call sites where IMEX parallel modes
// route emission through the physical extruder, while ordinary tool changes emit bare
// firmware commands like any non-IMEX printer.
int imex_pem_tool_for(int filament_id, const std::string& parallel_mode, const ConfigOptionInts& pem);
// Translate a logical filament id into the physical heater that an M104/M109 `T` must
// name. Applies in every IMEX mode including Primary -- a heater command names hardware,
// so it does not depend on a parallel mode being active the way imex_pem_tool_for does.
//
// Gated on is_imex because physical_extruder_map carries two readings in this tree: the
// BBL paths index it by extruder id (GCode.cpp:3295, WipeTower.cpp:1353), the IMEX paths
// by filament id. Those coincide only when the filament and nozzle counts match, so an
// ungated mapping would impose the IMEX reading on profiles that mean the other one --
// fdm_bbl_3dp_002_common ships a non-identity [1,0] and is spared today only because
// single_extruder_multi_material suppresses the T qualifier entirely.
//
// Out-of-range ids pass through unchanged, the same rule GCodeProcessor's preheat rewrite
// uses (GCode/GCodeProcessor.cpp:1407), so a printer whose map is the registered
// single-entry default is unaffected.
int imex_physical_heater_for(bool is_imex, const ConfigOptionInts& pem, int logical_id);
// True when GCode::set_extruder should suppress its bare T<n> at the print-start
// initial-tool selection because the active IMEX parallel mode's setup macro
// (imex_mode_gcode) and machine_start_gcode already activate the primary tool
// (Klipper SET_PRINT_MODE / RRF M567 — both reference an active tool by definition),
// so any slicer-emitted T<n> at print-start is a duplicate.
//
// Mid-print toolchanges in parallel mode are intentionally NOT suppressed:
// - Single-color prints have no mid-print toolchanges anyway
// - Multi-color prints are blocked at slice-time when the active IMEX configuration
// can't physically support multi-color (see imex_multicolor_block_reason). The only
// not-blocked case is IQEX with 2+ tools active on the primary gantry, where mid-
// print T<n> is legitimately needed and the firmware handles the slaved gantry.
//
// Returns false for: non-IMEX printers (empty parallel_mode), Primary mode, and any
// toolchange after the first (toolchange_count > 1). Custom change_filament_gcode that
// contains its own T<n> is handled separately by GCode.cpp's custom_gcode_changes_tool().
bool imex_suppresses_bare_toolchange(const std::string& parallel_mode, unsigned int toolchange_count);
// Returns a user-facing block reason when multi-color printing is incompatible with
// the active IMEX parallel mode, or an empty string when the configuration is OK.
//
// Multi-color in a parallel mode requires the firmware to swap tools mid-print on the
// primary gantry while the slaved gantry follows automatically. That works only when
// every used filament has its own dedicated physical head (no MMU lane sharing), the
// active mode definition explicitly marks a Span tool on the primary's gantry (Span
// declares "this tool is the within-gantry multicolor partner of Primary"; it
// disambiguates the multicolor topology from independent same-gantry copies), AND every
// used filament routes to the primary or one of its Span tools, the only heads that print
// colors of their own.
//
// Catches:
// - IDEX (1 tool per gantry): primary's gantry can never carry a Span partner → blocked
// - IQEX 2-tool-active (e.g. T0 primary + T2 copy on different gantries): no Span on
// primary's gantry → blocked
// - MMU/AFC sharing among used filaments: multiple used filaments routed to same
// physical head → blocked (the slaved gantry can't follow MMU lane swaps)
// - IQEX 4-tool independent copies (T0:P,T1:C,T2:M,T3:M): no Span declared, T1 is an
// independent copy → blocked
// - IQEX paired-gantry multicolor (T0:P,T1:S,T2:M,T3:M): Span on T1 declares the
// multicolor partner, no MMU sharing → ALLOWED for colors on T0 and T1; a color
// routed to T2, T3 or past the end of the map → blocked
//
// Returns empty string for: non-IMEX (empty parallel_mode), Primary mode, single-color
// prints, or any configuration where multi-color is physically supportable.
//
// Inputs:
// parallel_mode — m_imex_parallel_mode at slice time
// active_tools_str — the imex_mode_active_tools entry for the active mode, e.g.
// "0:P,1:C,2:M,3:M"
// tools_per_gantry — imex_tools_per_gantry from printer config (>= 1)
// used_filaments_0b — 0-based logical filament indices used by the print
// pem — physical_extruder_map (logical idx → physical head)
std::string imex_multicolor_block_reason(const std::string& parallel_mode,
const std::string& active_tools_str,
int tools_per_gantry,
const std::vector<int>& used_filaments_0b,
const ConfigOptionInts& pem);
// Returns the lowest 0-based logical filament index L such that pem[L] == physical.
// Returns -1 if no filament routes to `physical`.
// Degenerate case: an empty pem returns 0 when `physical == 0` (identity-on-head-0
// fallback for printers that never configured a pem) and -1 otherwise.
// With identity pem, the caller sees logical == physical behavior because
// position L holds value L.
int first_filament_for_physical_head(const ConfigOptionInts& pem, int physical);
// Returns true if the printer has at least one physical head fed by >= 2 logical
// filaments (i.e., an MMU/AFC is present on some head). Drives the plater's
// click-handler branch: true -> open popover; false -> retain cycle-through.
bool has_mmu(const ConfigOptionInts& pem);
// Returns true if any physical head OTHER than `primary_physical` is fed by
// >= 2 logical filaments. Primary is the head whose filament is already
// controlled by the left sidebar's object->filament assignment, so an MMU
// there needs no popover row; only non-primary MMU heads do.
// Returns false when pem is empty or size < 2.
bool has_non_primary_mmu(const ConfigOptionInts& pem, int primary_physical);
// Parses one mode's entry from `imex_mode_active_tools` and returns the
// 0-based physical T-index carrying the `:P` (Primary) role marker.
// Accepts two forms:
// "0:P,1:C,2:C" (explicit role suffix)
// "0" (backwards-compat: a bare index == Primary)
// Returns -1 on empty/malformed input or if no primary is found.
// The caller is responsible for indexing into imex_mode_active_tools by mode.
int imex_primary_tool_for_mode(const std::string& active_tools_for_mode);
// Parses the "phys:slot,phys:slot" serialization of imex_head_filament_map.
// Keys are physical T-indices (0-based); values are 1-based filament slots.
// Returns empty map on empty/malformed input. This string arrives straight from 3MF
// metadata, so tokens outside [0, MAXIMUM_EXTRUDER_NUMBER) are dropped here as an absolute
// sanity cap; the bound against the *project's* filament count lives in
// resolve_filament_for_head, which is where that count is actually known.
std::map<int,int> parse_imex_head_filament_map(const std::string& s);
// Resolves which 0-based logical filament to use for a physical head.
// 1. If the plate map overrides `physical` with a slot that indexes an existing filament,
// returns (slot - 1) — 1-based → 0-based.
// 2. Else falls back to first_filament_for_physical_head(pem, physical).
// Returns -1 if neither source yields a valid filament.
//
// `pem` has one entry per logical filament slot, so its size is the slot count and the
// result is always a valid index into it (or -1). An override naming a slot at or past
// that size cannot be honored — every consumer indexes a per-filament array, and the
// ConfigOption get_at() several of them use clamps rather than failing, which would turn a
// corrupt 3MF's "1:9999" into a silently wrong filament. Such an override is ignored and
// the printer's own routing applies, exactly as if the override were absent.
int resolve_filament_for_head(const std::map<int,int>& plate_map,
const ConfigOptionInts& pem,
int physical);
// Returns the 0-based logical filament slot the IMEX *primary* tool prints with,
// based on what the plate's objects are actually assigned to. Walks `used_slots_1b`
// (1-based filament indices, e.g. from PartPlate::get_extruders) and returns the
// first one whose pem entry maps to `primary_physical`. Returns -1 if no object on
// the plate routes to the primary's physical extruder, or if pem is empty.
//
// On non-MMU/non-AFC printers (one logical per physical) this is unambiguous; on
// AFC layouts where multiple logicals route to one physical, the first match wins
// — sufficient for warning labels and is_extruder_used marking. Multi-color
// primaries on the AFC manifold may want all matches; that's a separate iteration.
int imex_primary_logical_from_objects(const std::vector<int>& used_slots_1b,
const ConfigOptionInts& pem,
int primary_physical);
// Returns the 0-based logical filament slots that IMEX *secondary* carriages
// will load during the print. Iterates `active_physicals` (the set returned by
// imex_mode_active_tools parsing — physical extruder indices), skips entries
// equal to `primary_physical` (the primary is owned by tool_ordering /
// per-object filament assignment, not enumerated here), and resolves each
// remaining physical via `resolve_filament_for_head` (per-plate override
// first, then first_filament_for_physical_head as fallback).
//
// Returned slots are deduplicated and -1 entries (no routing found) are
// dropped. Use this for is_extruder_used marking and pre-slice warnings'
// secondary lookup; the caller still owns whatever it does with the slots
// (mark a bool array, compare temps, etc.).
std::vector<int> imex_secondary_logical_slots(const std::vector<int>& active_physicals,
int primary_physical,
const std::map<int,int>& plate_head_filament_map,
const ConfigOptionInts& pem);
enum class ImexRole { Primary, Copy, Mirror, Span };
// The one table a new role has to be added to.
//
// `letter` is the suffix the role carries in the `imex_mode_active_tools` on-disk format
// ("0:P,1:C,2:M,3:S"). That string is written into printer profiles and into 3MF projects,
// so the letters are FORMAT: never renumber, rename or reorder them, only append. The table
// order is also the order the modes editor cycles its tiles through and lists its color
// legend in, which is why Primary comes first.
//
// Everything that used to open-code the letters or a parallel small-int encoding of the
// enum now goes through this table or through ImexRole itself, so adding a fifth role means
// editing the enum, this table, and the places that must genuinely make a new decision
// about it (the transform switch, the zone/marker classification, the editor's per-role
// color + label) — not a scattered set of int mappings that fail silently when missed.
struct ImexRoleDesc {
ImexRole role;
char letter;
};
inline constexpr ImexRoleDesc kImexRoleTable[] = {
{ ImexRole::Primary, 'P' },
{ ImexRole::Copy, 'C' },
{ ImexRole::Mirror, 'M' },
{ ImexRole::Span, 'S' },
};
// Reads an IMEX geometry key, falling back to the value registered for that option in
// print_config_def when the key is absent from `cfg`, rather than to a literal repeated at the
// call site. Only a partial or hand-built config reaches the fallback -- anything instantiated
// from the ConfigDef carries every key -- but that is exactly the case a literal gets wrong.
int imex_cfg_int(const ConfigBase& cfg, const std::string& key);
double imex_cfg_float(const ConfigBase& cfg, const std::string& key);
// Enum sibling. ConfigOptionEnum<T> derives from ConfigOptionSingle<T>, not from ConfigOptionInt,
// so it cannot go through imex_cfg_int(). Header-inline because it is a template.
bool imex_cfg_bool(const ConfigBase& cfg, const std::string& key);
// Logs an unregistered-key lookup. Out-of-line to keep boost.log out of this header.
void imex_cfg_report_unregistered(const char* fn, const std::string& key);
template<class T>
T imex_cfg_enum(const ConfigBase& cfg, const std::string& key)
{
// Matched by type, not by the coEnum tag. Every ConfigOptionEnum<T> reports coEnum, so the
// type() comparison that option<TYPE>() (Config.hpp:2656) and the int/float accessors rely on
// cannot tell one enum type from another: it would accept a ConfigOptionEnum<OtherEnum> and
// static_cast it to this T. ConfigOptionEnum<A> and ConfigOptionEnum<B> are unrelated
// siblings (both derive from ConfigOptionSingle<T>), so a dynamic_cast rejects the mismatch.
//
// A coEnum value has a second representation, ConfigOptionEnumGeneric, which derives from
// ConfigOptionInt rather than ConfigOptionSingle<T> - the enum analogue of the
// ConfigOptionPercent : ConfigOptionFloat inheritance that rules dynamic_cast out for
// imex_cfg_float. It is what a config not seeded from the static PrinterConfig holds, so it
// is read here too, keyed on the value map it carries: only T's own map yields a T.
const ConfigOptionDef* def = print_config_def.get(key);
if (const ConfigOption* opt = cfg.option(key)) {
if (const auto* e = dynamic_cast<const ConfigOptionEnum<T>*>(opt))
return e->value;
// The map the generic option was built from identifies the enum it holds: T's own map
// means T's own enumerators, so the stored int is a T. A key whose definition names a
// different enum carries a different map and falls through to the default below,
// exactly as the typed cast above would reject it.
if (const auto* g = dynamic_cast<const ConfigOptionEnumGeneric*>(opt))
if (g->keys_map == &ConfigOptionEnum<T>::get_enum_values())
return static_cast<T>(g->value);
}
if (def)
if (const ConfigOption* dv = def->default_value.get())
if (const auto* e = dynamic_cast<const ConfigOptionEnum<T>*>(dv))
return e->value;
// Same policy as imex_cfg_int/_float: an unregistered key is a typo the compiler cannot
// catch, so say so rather than return a silent zero that reads as a legitimate enumerator.
// The log call is out-of-line so this header, which much of libslic3r and the GUI includes,
// does not pull in boost.log for a path that only a typo reaches.
assert(false && "imex_cfg_enum: key not registered in print_config_def");
imex_cfg_report_unregistered("imex_cfg_enum", key);
return static_cast<T>(0);
}
// The on-disk suffix letter for `role`. Inverse of imex_role_from_suffix().
char imex_role_letter(ImexRole role);
// Reads a whole token suffix — everything after the ':' — back into a role. Anything that
// is not exactly one of the table's letters is Copy: that covers "C" itself, an empty
// suffix ("3:"), a multi-character suffix, and any letter a future build might write that
// this one does not know. Copy is the historical fallback for an unrecognised suffix and
// must stay so, or a project saved by a newer build changes meaning when reopened here.
ImexRole imex_role_from_suffix(const std::string& suffix);
// Parses `imex_mode_active_tools[mode]` into a list of (physical_head, role) pairs.
// Accepted token forms (comma-separated, whitespace-tolerant):
// "phys" — bare index, role defaults to Copy
// "phys:P" — Primary
// "phys:C" — Copy
// "phys:M" — Mirror
// "phys:S" — Span (multicolor partner of Primary on the same gantry; only
// meaningful on tools sharing primary's gantry, and only on a
// multi-gantry printer. Drives the multicolor-allow path and
// paired-gantry ghost/zone aggregation.)
// "phys:???" — unknown role suffix, treated as Copy
// Malformed tokens (unparseable int, negative phys) are skipped.
// NOTE: the bare-token → Copy default differs from `imex_primary_tool_for_mode`,
// which separately scans for a Primary; callers needing the primary index should
// use that helper. This parser is for call sites that already have the primary
// in hand and need the full head/role list (e.g. ghost factory/updater).
std::vector<std::pair<int, ImexRole>> parse_imex_active_tools(const std::string& active_tools_for_mode);
// What a mode does, as the plate's mode icon shows it. The heads beside the primary that
// imex_primary_tool_for_mode() finds decide: none is Primary, any Span head is Custom (the
// multicolor modes), any Mirror head is Mirror, and all heads copying is Copy. One Mirror head is
// enough because an IQEX mirror mode copies within the primary's gantry.
enum class ImexModeKind { Primary, Copy, Mirror, Custom };
inline constexpr size_t kImexModeKindCount = 4;
static_assert(size_t(ImexModeKind::Custom) + 1 == kImexModeKindCount);
ImexModeKind imex_mode_kind(const std::string& active_tools_for_mode);
// =============================================================================
// The one derivation of "what does this plate's mode do, and does its filament reach
// the primary" — shared by the hard block and the pre-slice warning
// =============================================================================
// Print::validate() refuses a plate whose filaments do not route to the mode's declared
// primary; Plater's collect_imex_warnings() names that same primary's filament in its
// bed-temperature and filament-type warnings. Both need the identical chain: resolve the
// mode row -> parse its tool roster -> find the declared primary -> ask whether any used
// filament slot routes there. Computed twice, the two drift, and the warning ends up
// describing a plate the block describes differently. Computed here, they cannot.
//
// Everything in ImexRouting is a pure function of the four inputs. Deliberately absent:
// the `is_imex` gate (Print::validate's, and only its, outer condition) and any bound on
// the tool roster against the project's filament count (the plater's, and only its, way of
// dropping tools it has no filament preset to name). Those are genuinely per-caller and
// each stays with the caller that owns it.
struct ImexRouting
{
// The mode is a parallel mode: non-empty and not kImexPrimaryMode. False leaves every
// other field at its default — no mode row is even looked up.
bool parallel = false;
// `imex_mode_active_tools` for this mode, or "" when the mode resolves to no row / a
// ragged table. See find_imex_mode().
std::string active_tools;
// The parsed roster, physical head + role, in the order the string lists them.
std::vector<std::pair<int, ImexRole>> tools;
// Declared primary PHYSICAL head, from imex_primary_tool_for_mode(). -1 when the mode
// resolves to no row, an empty roster, or a roster with no Primary marker.
int primary_phys = -1;
// The 0-based LOGICAL filament slot the primary prints with: the first entry of
// `used_slots_1b` whose pem entry maps to `primary_phys`. -1 when nothing on the plate
// routes there (which is exactly what `primary_unrouted` reports as a hard error, and
// what the plater's warning path falls back out of so it still has a filament to name).
int primary_logical = -1;
// Sorted, deduplicated PHYSICAL heads that `used_slots_1b` actually reach. Slots outside
// the pem are dropped rather than clamped: ConfigOption::get_at() clamps to values front,
// which would let an error message name the very head it just said nothing routes to.
std::vector<int> routed_heads;
// The mode declares a primary, the printer has a routing map, and no used filament
// reaches that primary. The precise condition Print::validate refuses the plate on.
bool primary_unrouted = false;
bool routes_to_primary() const { return this->primary_logical >= 0; }
};
// `cfg` is any config carrying the printer's mode-table keys — the Print's applied config
// in the slicer, the edited printer preset in the GUI. `used_slots_1b` is 1-based filament
// indices (Print::extruders() + 1, or PartPlate::get_extruders()). `pem` is the effective
// physical_extruder_map: already normalised on the Print's config by Print::apply(), and
// obtained from effective_physical_extruder_map(PresetBundle) in the GUI.
ImexRouting imex_resolve_routing(const ConfigBase& cfg,
const std::string& parallel_mode,
const std::vector<int>& used_slots_1b,
const ConfigOptionInts& pem);
// Per-gantry grouping derived from the active_tools string. The single source of
// truth for paired-gantry visualization aggregation: when the primary's gantry
// has at least one Span tool, every non-primary gantry's representative tool
// stands in for the whole gantry (one ghost, one zone strip). Gantries without
// the aggregation trigger keep per-tool semantics.
//
// `representative_phys` is the column-paired tool to primary on that gantry —
// i.e. `gantry_index * tools_per_gantry + (primary_phys % tools_per_gantry)` if
// active, else the lowest active phys on that gantry. Aggregation falls back
// when tools on the same non-primary gantry carry mixed roles (e.g. one Copy
// + one Mirror), since the user explicitly authored two distinct topologies.
struct ImexGantryGroup {
int gantry_index = -1;
int representative_phys = -1;
ImexRole representative_role = ImexRole::Copy;
bool aggregate = false; // collapse this gantry to one ghost/zone
std::vector<std::pair<int, ImexRole>> tools; // every active tool on this gantry
};
struct ImexGantryGrouping {
int primary_phys = -1;
int primary_gantry = -1;
bool span_on_primary = false; // any Span tool on primary's gantry
std::vector<ImexGantryGroup> groups; // every gantry with ≥1 active tool, sorted by index
};
// Groups parsed active tools by gantry (`phys / tools_per_gantry`). Aggregation
// triggers iff `span_on_primary` is true AND a non-primary gantry's tools all
// carry the same role; mixed-role non-primary gantries fall back to per-tool.
// Returns an empty grouping when active_tools_str is empty / has no primary.
ImexGantryGrouping group_imex_active_tools_by_gantry(const std::string& active_tools_for_mode,
int tools_per_gantry);
// World-space transform composed as `head_xf * primary_instance_world` to place a ghost
// copy of the primary into `target`'s frame under `role`.
// Which boundary a Mirror reflects across. Tools on the primary's own gantry sit beside
// it along X, so they mirror across the vertical boundary between their zones. Tools on a
// different gantry sit in front of / behind it along Y, so they mirror across the
// horizontal boundary between the gantry row strips — the part comes off that gantry as a
// Y-reflection of the tool directly behind it, not an X-reflection. Single-gantry printers
// only ever use X.
enum class ImexMirrorAxis { X, Y };
// The single source of truth for that choice. Gantry row is `phys / tools_per_gantry`, the
// same derivation PartPlate and GCodeViewer use to build their zone grids. Keep every caller
// on this helper: if the three of them ever disagree about the axis, the plate ghosts and the
// preview markers place the same tool in different spots for the same mode.
// `tools_per_gantry` is clamped to >= 1, so a zero/negative config divides safely. Note the
// clamp lands on "one tool per gantry", meaning every secondary is then cross-gantry and
// mirrors on Y — not "everything on one gantry". That is the honest reading of tpg = 1.
ImexMirrorAxis imex_mirror_axis_for(int primary_phys, int target_phys, int tools_per_gantry);
// `gantry_offset` = center_for(target) - center_for(primary) (XY, in mm).
// `primary_zone_center` = XY center of the primary head's zone in world coords. Only
// consulted for Mirror; Copy/Primary ignore it.
// `mirror_axis` = axis the Mirror reflection negates; get it from imex_mirror_axis_for().
// Deliberately NOT defaulted: a default would silently hand a forgetful caller the X
// reflection, which is wrong for every cross-gantry tool and would fail silently — the
// ghosts would just quietly go back to mirroring on the wrong axis. Ignored by Copy/Primary.
// Copy: pure translation by gantry_offset. Ghost tracks primary 1:1 during drag.
// Mirror: true reflection (det = -1, so chirality flips — a real mirror image) about the
// zone-boundary plane between primary and target, perpendicular to `mirror_axis`
// and passing through the midpoint of the two zone centers along that axis. The
// ghost origin lands at the mirrored position within the target zone, and primary
// drag reflects across that plane, so the mirrored axis moves opposite the primary
// while the other axis tracks 1:1.
// Zero-length gantry_offset degenerates to identity.
// Primary: identity.
// Span: identity, same as Primary. A Span tool shares the primary's gantry AND its zone,
// printing the same objects through mid-print toolchanges rather than a duplicate
// placed elsewhere, so there is nothing to translate or reflect. calc_imex_ghosts
// bakes no ghost for a Span head, so no caller reaches this today.
Transform3d imex_head_transform(int primary, int target, ImexRole role,
const Vec2d& gantry_offset,
const Vec2d& primary_zone_center,
ImexMirrorAxis mirror_axis);
// Slice-time XY shift for printers that delegate copy/mirror placement to firmware.
// When `imex_firmware_managed_zones` is on AND the active mode is non-primary, returns
// the primary zone's plate-local center so gcode emission can subtract it (yielding a
// centered slice the firmware can fan out). Returns Vec2d::Zero() in every other case:
// flag off, primary/empty mode, or no zone box (IMEX disabled, primary-only, etc.).
Vec2d compute_imex_slice_offset(bool firmware_managed,
const std::string& parallel_mode,
const std::optional<BoundingBoxf>& primary_zone_box);
// True if `hull` (scaled Clipper coords) overlaps any of `zones` (unscaled mm) —
// the one definition of "overlap" shared by the object and prime-tower placement
// checks. Area-based: Clipper yields no result for shapes sharing only an edge, so
// a hull flush against a zone boundary is NOT reported, it has to cross. That
// matches the long-standing object behavior. An empty hull never violates.
bool imex_hull_violates_zones(const std::vector<BoundingBoxf3>& zones, const Polygon& hull);
} // namespace Slic3r
+379
View File
@@ -0,0 +1,379 @@
#include "libslic3r/IMEXZones.hpp"
#include <algorithm>
#include <map>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "PrintConfig.hpp"
#include "BoundingBox.hpp"
#include "Config.hpp"
#include "libslic3r/IMEXHelpers.hpp"
namespace Slic3r {
ImexZoneLayout compute_imex_zone_layout(const DynamicPrintConfig& printer_cfg,
const std::string& plate_mode,
const std::string& process_mode,
const BoundingBoxf& bed_extents)
{
ImexZoneLayout out;
auto* is_imex_opt = printer_cfg.option<ConfigOptionBool>("is_imex");
if (!is_imex_opt || !is_imex_opt->value)
return out;
// Per-plate mode takes priority over the process preset.
std::string active_mode = plate_mode;
if (active_mode == kImexPrimaryMode && !process_mode.empty())
active_mode = process_mode;
if (active_mode == kImexPrimaryMode || active_mode.empty())
return out;
// Grid dimensions and tool layout from printer config
int n_cols = std::max(1, imex_cfg_int(printer_cfg, "imex_tools_per_gantry"));
int n_rows = std::max(1, imex_cfg_int(printer_cfg, "imex_gantry_count"));
// Which corner is T0? flip_x: col 0 is right(max-X); flip_y: row 0 is rear(max-Y)
const ImexToolLayout layout = imex_cfg_enum<ImexToolLayout>(printer_cfg, "imex_tool_layout");
bool flip_x = (layout == ImexToolLayout::FrontRight || layout == ImexToolLayout::RearRight);
bool flip_y = (layout == ImexToolLayout::RearLeft || layout == ImexToolLayout::RearRight);
// Convert tool index to physical (col=X-index, row=Y-index), col/row 0 = min-X/min-Y
auto tool_to_phys = [&](int idx) -> std::pair<int,int> {
int raw_col = idx % n_cols, raw_row = idx / n_cols;
return { flip_x ? (n_cols - 1 - raw_col) : raw_col,
flip_y ? (n_rows - 1 - raw_row) : raw_row };
};
int pri_col = 0, pri_row = 0;
if (n_cols == 1 && n_rows == 1)
return out; // nothing to dim with a single zone
// Look up secondary tool indices (active in mode, but NOT the primary tool).
// An unresolved mode, and a mode whose row the tools array does not reach, both come
// back as an empty string; the zone_roles guard below then returns an empty layout.
const std::string active_tools_str = find_imex_mode(printer_cfg, active_mode).active_tools;
// Which heads own a zone cell, and in what role. Roles carry through from
// parse_imex_active_tools() rather than being flattened into small ints, so the
// classification below is a comparison the compiler checks and a new role cannot slip
// through as an unhandled number.
//
// imex_primary_tool_for_mode handles the Primary slot (it owns the bare-legacy-token →
// Primary rule); parse_imex_active_tools fills in the Copy/Mirror secondaries. Mode
// strings use physical T-indices directly; filament routing is separate
// (imex_head_filament_map).
//
// Only Primary / Copy / Mirror land in this map. A Span head shares the primary's zone
// instead of owning one, so it deliberately gets no entry (and hence no zone center --
// see ImexZoneLayout::head_zone_centers), and an extra Primary entry beyond the first is
// ignored. Any further role must decide here whether it owns a cell.
std::map<int, ImexRole> zone_roles;
{
const int primary = imex_primary_tool_for_mode(active_tools_str);
if (primary >= 0 && primary < n_rows * n_cols)
zone_roles[primary] = ImexRole::Primary;
for (const auto& [phys_idx, role] : parse_imex_active_tools(active_tools_str)) {
if (phys_idx < 0 || phys_idx >= n_rows * n_cols) continue;
if (phys_idx == primary) continue;
// Exhaustive on purpose, with no default: a role added to the enum has to answer
// "does this own a zone cell?" here, and -Wswitch asks the question at compile
// time instead of letting the head silently vanish from the layout.
switch (role) {
case ImexRole::Copy:
case ImexRole::Mirror:
zone_roles[phys_idx] = role;
break;
case ImexRole::Primary: // an extra Primary beyond the first is ignored
case ImexRole::Span: // shares the primary's zone rather than owning one
break;
}
}
}
// If the mode name was found but has no tools (e.g. stale process-preset mode on a new
// printer that has no modes defined yet), there is nothing to compute.
if (zone_roles.empty())
return out;
// Identify the Primary tool from the mode definition
for (const auto& [idx, role] : zone_roles) {
if (role == ImexRole::Primary) { auto [c, r] = tool_to_phys(idx); pri_col = c; pri_row = r;
out.primary_head = idx; break; }
}
// No Primary landed inside the grid. Either the mode declares none at all, or its `:P`
// index is off-grid: the modes editor deliberately preserves tool assignments across
// imex_gantry_count changes, so a mode authored on a 4-tool IQEX with Primary on T3 keeps
// that assignment when the printer is reconfigured as a 2-tool IDEX, and the
// `primary < n_rows * n_cols` bound above then drops it from zone_roles.
//
// Every zone, strip, ghost center and slice offset below is derived from pri_col/pri_row,
// which would still be sitting at their (0,0) initializers — a position no active tool
// occupies. That yields a primary zone and a secondary zone that both claim the whole bed:
// imex_primary_zone() reports the plate clear while the blocking boxes report it full, and
// under imex_firmware_managed_zones compute_imex_slice_offset() shifts the slice by the bed
// center. Degrade to "this plate has no IMEX zones" instead — the same answer the earlier
// early returns give, and the one every consumer already handles.
if (out.primary_head < 0)
return ImexZoneLayout{};
// Per-gantry grouping drives Span-based aggregation. When the mode declares a
// Span partner on primary's gantry, non-primary gantries with multiple same-role
// tools collapse to one cell each (placed at primary's column so has_col_sep
// stays false and make_boxes expands the cell into a full-X row-strip).
// Mixed-role / single-tool / no-Span configurations keep per-tool cells.
const ImexGantryGrouping grouping =
group_imex_active_tools_by_gantry(active_tools_str, n_cols);
std::map<int, const ImexGantryGroup*> group_by_gantry;
for (const auto& grp : grouping.groups)
group_by_gantry[grp.gantry_index] = &grp;
// Separate copy and mirror secondary cells using physical coordinates
std::set<std::pair<int,int>> copy_cells, mirror_cells;
for (const auto& [idx, role] : zone_roles) {
if (role == ImexRole::Primary) continue; // primary handled separately
const int phys_gantry = idx / n_cols;
auto git = group_by_gantry.find(phys_gantry);
const ImexGantryGroup* grp = (git == group_by_gantry.end()) ? nullptr : git->second;
if (grp && grp->aggregate) {
// Only the representative contributes a cell; non-reps are folded into
// the same row-strip and skipped entirely.
if (idx != grp->representative_phys) continue;
auto [rep_c, r] = tool_to_phys(idx);
(void)rep_c; // intentionally discarded — aggregated cell pins to pri_col
if (role == ImexRole::Copy) copy_cells.insert({pri_col, r});
else if (role == ImexRole::Mirror) mirror_cells.insert({pri_col, r});
} else {
auto [c, r] = tool_to_phys(idx);
if (role == ImexRole::Copy) copy_cells.insert({c, r});
else if (role == ImexRole::Mirror) mirror_cells.insert({c, r});
}
}
// All active secondary cells combined (for separation axis computation)
std::set<std::pair<int,int>> all_secondary;
for (auto& p : copy_cells) all_secondary.insert(p);
for (auto& p : mirror_cells) all_secondary.insert(p);
double x_min = bed_extents.min(0), x_max = bed_extents.max(0);
double y_min = bed_extents.min(1), y_max = bed_extents.max(1);
// Zone sizing is based on ACTIVE tool count per axis, not total grid dimensions.
// Inactive tools (absent from zone_roles) donate their bed share to active neighbors.
// Sorted active col/row lists map physical index k → zone index k.
std::vector<int> active_cols_v, active_rows_v;
{
std::set<int> ac_set, ar_set;
for (const auto& [idx, role] : zone_roles) {
auto [c, r] = tool_to_phys(idx);
ac_set.insert(c); ar_set.insert(r);
}
active_cols_v.assign(ac_set.begin(), ac_set.end()); // sorted ascending
active_rows_v.assign(ar_set.begin(), ar_set.end());
}
int n_active_cols = std::max(1, (int)active_cols_v.size());
int n_active_rows = std::max(1, (int)active_rows_v.size());
std::map<int,int> col_to_zone, row_to_zone;
for (int k = 0; k < n_active_cols; ++k) col_to_zone[active_cols_v[k]] = k;
for (int k = 0; k < n_active_rows; ++k) row_to_zone[active_rows_v[k]] = k;
double zone_w = (x_max - x_min) / n_active_cols;
double zone_h = (y_max - y_min) / n_active_rows;
// Zone index of the primary tool
int pri_col_k = col_to_zone.count(pri_col) ? col_to_zone[pri_col] : 0;
int pri_row_k = row_to_zone.count(pri_row) ? row_to_zone[pri_row] : 0;
// Zone center per physical head — ghost placement consumes this so that
// ghosts land in their own secondary zone instead of stacking on primary.
// Every active tool (including primary) gets an entry; ghost offset math is
// simply center[head] - center[primary].
for (const auto& [idx, role] : zone_roles) {
auto [c, r] = tool_to_phys(idx);
int ck = col_to_zone.count(c) ? col_to_zone.at(c) : 0;
int rk = row_to_zone.count(r) ? row_to_zone.at(r) : 0;
out.head_zone_centers[idx] = Vec2d(
x_min + (ck + 0.5) * zone_w,
y_min + (rk + 0.5) * zone_h);
}
// Separation axes: row-sep = secondaries on a different gantry, col-sep = different column
bool has_row_sep = false, has_col_sep = false;
for (const auto& [sc, sr] : all_secondary) {
if (sr != pri_row) has_row_sep = true;
if (sc != pri_col) has_col_sep = true;
}
// Build expanded zone rects for a set of cells.
// When secondaries share only a row difference (same column as primary) → expand to full X width.
// When secondaries share only a column difference → expand to full Y height.
// When both axes differ → per-quadrant.
auto make_boxes = [&](const std::set<std::pair<int,int>>& cells) -> std::vector<BoundingBoxf> {
std::vector<BoundingBoxf> boxes;
if (cells.empty()) return boxes;
auto ck = [&](int c) { return col_to_zone.count(c) ? col_to_zone.at(c) : 0; };
auto rk = [&](int r) { return row_to_zone.count(r) ? row_to_zone.at(r) : 0; };
if (has_row_sep && !has_col_sep) {
std::set<int> rows; for (auto& [c,r] : cells) rows.insert(r);
for (int sr : rows) {
int k = rk(sr);
boxes.emplace_back(Vec2d(x_min, y_min + k*zone_h), Vec2d(x_max, y_min + (k+1)*zone_h));
}
} else if (has_col_sep && !has_row_sep) {
std::set<int> cols; for (auto& [c,r] : cells) cols.insert(c);
for (int sc : cols) {
int k = ck(sc);
boxes.emplace_back(Vec2d(x_min + k*zone_w, y_min), Vec2d(x_min + (k+1)*zone_w, y_max));
}
} else {
for (auto& [sc,sr] : cells) {
int ck_ = ck(sc), rk_ = rk(sr);
boxes.emplace_back(Vec2d(x_min + ck_*zone_w, y_min + rk_*zone_h),
Vec2d(x_min + (ck_+1)*zone_w, y_min + (rk_+1)*zone_h));
}
}
return boxes;
};
out.copy_zones = make_boxes(copy_cells);
out.mirror_zones = make_boxes(mirror_cells);
// --- Secondary zone blocking ---
// The expanded bounding boxes for all secondary (copy+mirror) zones. check_outside()
// uses these to prevent objects being placed outside the primary zone.
auto push_secondary_box = [&](const std::vector<BoundingBoxf>& boxes) {
for (const auto& b : boxes)
out.secondary_zone_boxes.emplace_back(Vec3d(b.min.x(), b.min.y(), -1.0),
Vec3d(b.max.x(), b.max.y(), 1e4));
};
push_secondary_box(out.copy_zones);
push_secondary_box(out.mirror_zones);
// --- Carriage collision danger strips ---
// Only add strips at boundaries of the PRIMARY zone — objects are only placed in the
// primary zone, so secondary-to-secondary boundaries have no relevance.
//
// Strip width is the literal nozzle clearance value on the primary side only:
// right X boundary: [bnd_x - nozzle_clearance_x, bnd_x]
// left X boundary: [bnd_x, bnd_x + nozzle_clearance_x]
// top Y boundary: [bnd_y - nozzle_clearance_y, bnd_y]
// bottom Y boundary:[bnd_y, bnd_y + nozzle_clearance_y]
//
// Strip length matches the primary zone extent (same expansion logic as make_boxes):
// !has_row_sep → full bed height; has_row_sep → primary row only
// !has_col_sep → full bed width; has_col_sep → primary column only
// Both strip blocks below are gated on `carriage_w > 0.0` / `carriage_h > 0.0`, so a zero
// clearance emits no collision strips at all while the preview still draws toolhead boxes.
// That is why these read through imex_cfg_float() rather than carrying a literal: the value
// for an absent key comes from the option's own registration, not from a number repeated here.
double carriage_w = imex_cfg_float(printer_cfg, "imex_nozzle_clearance_x");
double carriage_h = imex_cfg_float(printer_cfg, "imex_nozzle_clearance_y");
double margin = imex_cfg_float(printer_cfg, "imex_carriage_margin");
// Primary zone extent (the clear printable area):
// row-sep only → full bed width × primary row's Y band
// col-sep only → primary col's X band × full bed height
// both → primary quadrant
// Uses zone indices so inactive tools don't shrink the zone.
double pz_x0 = has_col_sep ? x_min + pri_col_k * zone_w : x_min;
double pz_x1 = has_col_sep ? x_min + (pri_col_k + 1) * zone_w : x_max;
double pz_y0 = has_row_sep ? y_min + pri_row_k * zone_h : y_min;
double pz_y1 = has_row_sep ? y_min + (pri_row_k + 1) * zone_h : y_max;
out.primary_zone_box = BoundingBoxf(Vec2d(pz_x0, pz_y0), Vec2d(pz_x1, pz_y1));
auto add_strip = [&](double sx0, double sx1, double sy0, double sy1) {
out.collision_zones.emplace_back(Vec3d(sx0, sy0, -1.0), Vec3d(sx1, sy1, 1e4));
};
auto add_margin_band = [&](double sx0, double sx1, double sy0, double sy1) {
out.margin_bands.emplace_back(Vec2d(sx0, sy0), Vec2d(sx1, sy1));
};
// Determine which directions have mirror secondaries adjacent to the primary.
// Only mirror tools can cause carriage collisions — they move toward each other.
// Copy tools always move in the same direction, so no collision strip is needed.
//
// Driven by `mirror_cells`, not by zone_roles: on an AGGREGATED gantry the cell is
// pinned to the primary's column and make_boxes then expands it into a full-width row
// strip, so the painted zone — not any one head's own cell — is what the primary's
// carriage can meet. Reading each head's own cell instead lost the strip whenever the
// representative's column differed from the primary's: at tpg=3, gantry_count=2, mode
// "0:P,1:S,4:M,5:M" the rear gantry paints a full-width mirror strip against the
// primary's rear boundary, yet neither T4 (col 1) nor T5 (col 2) is in the primary's
// column, so no strip and no margin band were emitted at all.
//
// Both axes require zone-adjacent AND same row/column on the OTHER axis: a mirror
// diagonally offset from primary (different row AND different column) can't collide
// with primary's carriage on either axis since the gantries don't overlap there.
// Without these checks, paired-gantry mc-mirror (`0:P,1:S,2:M,3:M`) would draw a
// spurious right-edge strip from T3 even though T3 lives on the other gantry's row.
//
// The adjacency tests (±1) are in ZONE indices so an inactive column or row between
// two active ones cannot hide the boundary they really share (see "inactive tools
// donate their bed share"), while the co-linearity test on the other axis stays in
// physical indices. The two forms agree for any cell that owns a zone — col_to_zone
// and row_to_zone are order-preserving bijections over exactly the active indices —
// so this is one rule written two ways, not two rules.
bool has_right_sec = false, has_left_sec = false;
bool has_top_sec = false, has_bottom_sec = false;
for (const auto& [sc, sr] : mirror_cells) {
int zc = col_to_zone.count(sc) ? col_to_zone.at(sc) : -1;
int zr = row_to_zone.count(sr) ? row_to_zone.at(sr) : -1;
if (zc < 0 || zr < 0) continue; // no zone of its own, so no boundary with primary
// X-boundary strips: mirror zone-adjacent in X, same physical row as primary.
if (zr == pri_row_k && zc == pri_col_k + 1) has_right_sec = true;
if (zr == pri_row_k && zc == pri_col_k - 1) has_left_sec = true;
// Y-boundary strips: mirror zone-adjacent in Y, same physical column as primary.
if (zr == pri_row_k + 1 && sc == pri_col) has_top_sec = true;
if (zr == pri_row_k - 1 && sc == pri_col) has_bottom_sec = true;
}
// X-axis boundaries (vertical strips, width = nozzle_clearance_x on primary side)
if (carriage_w > 0.0) {
if (has_right_sec) {
double bnd_x = x_min + (pri_col_k + 1) * zone_w;
double strip_inner = bnd_x - carriage_w;
add_strip(strip_inner, bnd_x, pz_y0, pz_y1);
if (margin > 0.0)
add_margin_band(strip_inner - margin, strip_inner, pz_y0, pz_y1);
}
if (has_left_sec) {
double bnd_x = x_min + pri_col_k * zone_w;
double strip_inner = bnd_x + carriage_w;
add_strip(bnd_x, strip_inner, pz_y0, pz_y1);
if (margin > 0.0)
add_margin_band(strip_inner, strip_inner + margin, pz_y0, pz_y1);
}
}
// Y-axis boundaries (horizontal strips, width = nozzle_clearance_y on primary side)
if (carriage_h > 0.0) {
if (has_top_sec) {
double bnd_y = y_min + (pri_row_k + 1) * zone_h;
double strip_inner = bnd_y - carriage_h;
add_strip(pz_x0, pz_x1, strip_inner, bnd_y);
if (margin > 0.0)
add_margin_band(pz_x0, pz_x1, strip_inner - margin, strip_inner);
}
if (has_bottom_sec) {
double bnd_y = y_min + pri_row_k * zone_h;
double strip_inner = bnd_y + carriage_h;
add_strip(pz_x0, pz_x1, bnd_y, strip_inner);
if (margin > 0.0)
add_margin_band(pz_x0, pz_x1, strip_inner, strip_inner + margin);
}
}
return out;
}
} // namespace Slic3r
+94
View File
@@ -0,0 +1,94 @@
#pragma once
#include <map>
#include <optional>
#include <string>
#include <vector>
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/PrintConfig.hpp"
namespace Slic3r {
// Where every IMEX bed zone, carriage collision strip and advisory margin band sits, in
// plate-local millimetres. Purely geometric: no rendering, no clipping against the bed
// outline, no GUI types. PartPlate turns the rectangles into GLModels (clipping each one
// to the bed polygon as it goes) and copies the boxes straight into the members that
// check_outside() consults, so this struct is the whole answer to "which boxes".
//
// Every list is empty and `primary_zone_box` unset when no parallel mode is in effect —
// IMEX off, Primary mode, a 1x1 tool grid, a mode whose tool roster is empty, or a mode
// whose Primary tool is not on the current grid (no `:P` marker, or a `:P` index left
// over from a wider grid). The whole layout hangs off the primary's cell, so without one
// there is nothing to divide and consumers must see the same "no zones" answer.
struct ImexZoneLayout
{
// Physical T-index of the active mode's Primary tool; -1 when the layout is empty.
int primary_head = -1;
// Physical head -> XY center of that head's zone. One entry per tool the mode makes
// active, Primary included. A Span tool has no entry: it shares the primary's zone
// rather than owning one. Ghost placement offsets by center[head] - center[primary].
//
// EXCEPTION, read before using the X component. These centers are built from the head's
// OWN grid cell, but an AGGREGATED representative's zone rectangle is pinned to the
// primary's column and then expanded to a full-width row strip. So for an aggregated head
// whose own column differs from the primary's, center[head].x names a column its painted
// zone does not have, and `center[head] - center[primary]` carries a bogus X offset.
// Reachable at imex_tools_per_gantry >= 3 when the primary-column tool on the aggregated
// gantry is inactive (e.g. tpg=3, gantry_count=2, mode "0:P,1:S,4:M,5:M").
// Both current consumers pin X to the primary's center for aggregated heads --
// PartPlate::calc_imex_ghosts() via bed_x_center, GCodeViewer via sec_center_of() -- and a
// new consumer must do the same until this is either pinned here or the aggregate flag is
// exposed on the layout.
std::map<int, Vec2d> head_zone_centers;
// The clear printable area — the primary tool's zone. Objects go here and nowhere else.
std::optional<BoundingBoxf> primary_zone_box;
// Copy / Mirror zone rectangles, already expanded along whichever axis carries no
// separation: secondaries differing from primary only by row give full-bed-width
// strips, only by column give full-bed-height strips, and differing on both axes give
// per-tool quadrants.
std::vector<BoundingBoxf> copy_zones;
std::vector<BoundingBoxf> mirror_zones;
// `copy_zones` followed by `mirror_zones`, as full-height boxes for the placement
// check. An object overlapping one of these is outside the primary zone.
std::vector<BoundingBoxf3> secondary_zone_boxes;
// Carriage danger strips lying just inside the primary zone's boundary, one per
// boundary that faces an adjacent Mirror ZONE -- an aggregated gantry contributes the
// one pinned zone it paints, not one per head -- in right / left / top / bottom order.
// Width is the literal nozzle clearance for that axis. Copy tools never contribute:
// they travel in the same direction as the primary and cannot close on it.
std::vector<BoundingBoxf3> collision_zones;
// Advisory (non-blocking) bands immediately inside each collision strip, present only
// when imex_carriage_margin > 0. Same boundary order as `collision_zones`.
std::vector<BoundingBoxf> margin_bands;
};
// Computes the IMEX zone layout for one plate.
//
// `printer_cfg` — the edited printer preset's config. Read for is_imex, the tool grid
// (imex_gantry_count / imex_tools_per_gantry / imex_tool_layout), the
// mode roster (imex_mode_names / imex_mode_active_tools) and the strip
// widths (imex_nozzle_clearance_x / _y, imex_carriage_margin). A key absent
// from the config falls back to the value registered for that option in
// print_config_def, via the imex_cfg_* accessors.
// `plate_mode` — the plate's own IMEX mode. Wins over the process preset unless it is
// `kImexPrimaryMode`.
// `process_mode` — the process preset's `imex_parallel_mode`, used only as the fallback
// when `plate_mode` is `kImexPrimaryMode`. Pass "" when unavailable.
// `bed_extents` — XY extents of the plate shape, in plate-local mm. Zones subdivide it.
//
// Zone sizing keys off the count of ACTIVE tools per axis rather than the grid dimensions,
// so a tool the mode leaves out donates its share of the bed to its active neighbours.
ImexZoneLayout compute_imex_zone_layout(const DynamicPrintConfig& printer_cfg,
const std::string& plate_mode,
const std::string& process_mode,
const BoundingBoxf& bed_extents);
} // namespace Slic3r
+8
View File
@@ -1473,6 +1473,9 @@ static std::vector<std::string> s_Preset_print_options{
"interlocking_boundary_avoidance",
"interlocking_beam_width",
"calib_flowrate_topinfill_special_order",
// IDEX/IQEX parallel print mode (per-print selection)
"imex_parallel_mode",
"imex_head_filament_map",
// Z Anti-Aliasing (ZAA)
"zaa_enabled",
"zaa_minimize_perimeter_height",
@@ -1591,6 +1594,11 @@ static std::vector<std::string> s_Preset_printer_options {
"z_offset",
"disable_m73", "preferred_orientation", "emit_machine_limits_to_gcode", "pellet_modded_printer", "support_multi_bed_types", "use_3mf", "default_bed_type", "bed_mesh_min","bed_mesh_max","bed_mesh_probe_distance", "adaptive_bed_mesh_margin", "enable_long_retraction_when_cut","long_retractions_when_cut","retraction_distances_when_cut",
"bed_temperature_formula", "nozzle_flush_dataset",
// IDEX/IQEX (independent X extruder) — printer capability and user-defined modes
"is_imex", "imex_firmware_managed_zones",
"imex_gantry_count", "imex_tools_per_gantry", "imex_tool_layout",
"imex_nozzle_clearance_x", "imex_nozzle_clearance_y", "imex_carriage_margin", "imex_viz_theme",
"imex_mode_names", "imex_mode_active_tools", "imex_mode_gcodes",
// Multi-nozzle count + pre-heat model printer options
"extruder_max_nozzle_count", "group_algo_with_time", "enable_pre_heating", "hotend_heating_rate", "hotend_cooling_rate",
"machine_hotend_change_time", "machine_prepare_compensation_time",
+6
View File
@@ -114,6 +114,12 @@ static std::vector<std::string> s_project_options {
"nozzle_volume_type",
"filament_map_mode",
"filament_map",
// physical_extruder_map intentionally NOT here: it's owned by the printer
// preset (s_Preset_printer_options). Listing it project-scoped caused
// project_config's default [0] to clobber the preset's authored value
// (e.g. AFC-shaped [0,1,1,1,1]) during full_fff_config() merge, and the
// clobbered value then rode into saved 3mfs and back into the edited
// preset on reload.
// Per-filament nozzle-volume choice; project-level like filament_map so the per-filament
// slot resolution survives preset switches.
"filament_volume_map",
+196 -3
View File
@@ -55,6 +55,8 @@
#include "TriangleMesh.hpp"
#include "Config.hpp"
#include "Exception.hpp"
#include "IMEXHelpers.hpp"
#include "IMEXZones.hpp"
#include "Print.hpp"
#include "BoundingBox.hpp"
#include "Brim.hpp"
@@ -519,6 +521,12 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
}
else if (opt_key == "z_hop_types") {
osteps.emplace_back(posDetectOverhangsForLift);
}
// The bed-zone colour scheme and the advisory margin bands are drawn from the printer
// config but never reach the slice: compute_imex_zone_layout keeps the margin out of
// primary_zone_box, which is the only part of the layout update_imex_slice_offset reads.
// imex_tool_layout does move that box, so it is left to the fallback below.
else if (opt_key == "imex_viz_theme" || opt_key == "imex_carriage_margin") {
} else {
// for legacy, if we can't handle this option let's invalidate all steps
//FIXME invalidate all steps of all objects as well?
@@ -1851,6 +1859,132 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
if (extruders.empty())
return { L("No extrusions under current settings.") };
// IDEX/IQEX: block multi-color in non-primary parallel modes when the active
// configuration can't physically support it (no within-gantry toolchange path
// or MMU lane sharing). See imex_multicolor_block_reason() for the full rule.
// Both IMEX checks below need the active mode's tools string and its declared primary,
// so imex_resolve_routing() derives them once for the whole block -- and is the same call
// the plater's pre-slice warning makes, so the two cannot describe the plate differently.
// Note this block is NOT gated on extruders.size() > 1: the primary
// routing check applies to a single-filament plate too, which is its most common case.
if (m_config.is_imex.value && !m_objects.empty()) {
const std::string& parallel_mode = m_objects.front()->config().imex_parallel_mode.value;
if (!parallel_mode.empty() && parallel_mode != kImexPrimaryMode) {
// Tool changes added from the layer slider switch heads mid-print as a painted color
// does, so they count here, as the plate's warning badge counts them.
const std::vector<unsigned int> imex_extruders = this->extruders(/*conside_custom_gcode=*/true);
std::vector<int> used_filaments_0b;
std::vector<int> used_slots_1b;
used_filaments_0b.reserve(imex_extruders.size());
used_slots_1b.reserve(imex_extruders.size());
for (unsigned int e : imex_extruders) {
used_filaments_0b.push_back((int)e);
used_slots_1b.push_back((int)e + 1);
}
// The active mode's tools string, its declared primary, and whether this plate's
// filaments reach that primary are all derived ONCE, here. The plater's pre-slice
// warning (collect_imex_warnings) makes the identical call, so the sentence it
// shows before slicing and the refusal below cannot describe the plate
// differently. An unresolved mode, and a mode the tools array is too short to
// cover, both yield an empty roster and no primary.
const ImexRouting routing = imex_resolve_routing(m_config, parallel_mode, used_slots_1b,
m_config.physical_extruder_map);
// A mixed filament is blended 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 are routed.
//
// Runs before BOTH rules below. Against the routing rule it would otherwise be
// reported as merely unrouted: mixed slots are kept at the tail of the filament
// arrays, past the physical filament count, so on a printer whose logical extruder
// count equals that count they fall outside physical_extruder_map and resolve to no
// head. Against the multi-color rule, a mixed slot plus any second filament trips
// the >1 gate and earns a lecture about gantry topology the user never configured.
// Being unsupported outright, this dominates both.
{
const auto& is_mixed = m_config.filament_is_mixed.values;
if (std::any_of(used_filaments_0b.begin(), used_filaments_0b.end(),
[&](int slot) { return slot >= 0 && slot < (int) is_mixed.size() && is_mixed[slot]; })) {
StringObjectException err;
// "Mixed filament" is the term the rest of the UI uses -- the button that
// creates one, and the sibling refusal for the wipe tower filament.
err.string = L("Mixed filaments are not supported in IDEX/IQEX parallel modes. "
"Switch this plate to Primary mode.");
err.object = m_objects.front();
return err;
}
}
// Multi-color has the more specific rule and its remedies are self-contained
// (an MMU manifold sharing one head cannot be fixed by switching mode), so it runs
// ahead of the routing block below, which would otherwise mask it with advice that
// leads to this error on the next slice. validate() returns on the first error.
if (used_filaments_0b.size() > 1) {
const std::string reason = imex_multicolor_block_reason(
parallel_mode,
routing.active_tools,
m_config.imex_tools_per_gantry.value,
used_filaments_0b,
m_config.physical_extruder_map);
if (!reason.empty())
return { reason };
}
// The IMEX Primary tool prints the sliced paths directly, so it can only use a
// filament the printer's physical_extruder_map routes to it. The ghost filament
// picker already enforces this for the secondary tools; the primary's filament
// comes from the ordinary object filament selector, which has no IMEX awareness,
// so nothing detected the mismatch. collect_imex_warnings() reads the SAME
// routing.primary_logical off the same derivation, and discards it into a display
// fallback so its warning still has a filament to name.
//
// Blocks rather than warns, matching the multi-color rule above -- but on intent,
// not on physics, and the distinction matters to anyone tempted to relax it. The
// emitter does not use the declared primary: it re-derives an effective one from
// the filament actually in use (GCode.cpp's initial_extruder_id -> pem lookups), so
// a plate whose only filament sits on a Span tool sharing the primary's gantry does
// produce coherent G-code. It is refused anyway. A parallel mode exists to run
// carriages in parallel; a single-colour plate riding one span lane is not that, and
// silently accepting it would make the mode's declared roster meaningless.
//
// Where the routed head is absent from the mode's active tools the plate is broken
// outright, not merely off-intent: the 1st->2nd layer temperature
// branch (GCode.cpp, mutually exclusive with the standard path) skips it too, so
// that head holds nozzle_temperature_initial_layer for the whole job. First-layer
// temperatures are unaffected -- _print_first_layer_extruder_temperatures is not
// IMEX-branched -- so this is a stuck-hot nozzle, not a cold one.
//
// `primary_unrouted` is the whole condition: a declared primary, a populated
// routing map, and nothing used reaching it. Blended slots would also read as
// unrouted -- they sit past the map by construction -- but they never get here,
// the mixed-filament rule above returns first.
if (routing.primary_unrouted) {
std::string routed_list;
for (int head : routing.routed_heads)
routed_list += (routed_list.empty() ? "T" : ", T") + std::to_string(head);
// Print::apply() normalises physical_extruder_map through
// effective_physical_extruder_map (PrintApply.cpp) before validate() runs, so
// an unauthored map arrives here as the identity and every slot is in range.
// What is left is a printer with more filaments than logical extruders, where
// the tail slots fall outside the map. Narrow, but without this the sentence
// ends in a dangling "on .".
if (routed_list.empty())
routed_list = L("no configured tool");
StringObjectException err;
err.string = Slic3r::format(
L("IDEX/IQEX mode \"%1%\" prints with T%2%, but this plate's filaments are on %3%. "
"Assign a filament loaded on T%2%, or switch this plate to Primary mode."),
parallel_mode, routing.primary_phys, routed_list);
// Gives the notification a "Jump to <object>" link, which selects the object
// and switches to Prepare -- directly enabling the first suggested remedy.
err.object = m_objects.front();
return err;
}
}
}
// Belt printer validation: incompatible features.
if (m_config.belt_printer.value) {
for (const PrintObject *object : m_objects) {
@@ -2969,6 +3103,10 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
name_tbb_thread_pool_threads_set_locale();
// IMEX firmware-managed zones: settle the emission-frame shift from the applied config
// before anything runs. Zero for every printer that is not IMEX + firmware-managed.
this->update_imex_slice_offset();
//compute the PrintObject with the same geometries
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": this=%1%, enter, use_cache=%2%, object size=%3%")%this%use_cache%m_objects.size();
if (m_objects.empty())
@@ -3688,7 +3826,16 @@ std::string Print::export_gcode(const std::string& path_template, GCodeProcessor
gcode = std::make_unique<GCode>();
//BBS: compute plate offset for gcode-generator
const Vec3d origin = this->get_plate_origin();
gcode->set_gcode_offset(origin(0), origin(1));
// IMEX firmware-managed zones: writer offset gets the IMEX shift so emitted gcode is
// centered at bed origin; processor offset stays at plate_origin so the gcode-preview
// visualizer renders the centered slice at the bed center rather than the prepare-view
// zone placement. Both arms reduce to set_gcode_offset() semantics when shift is zero.
// Derived here rather than read as pushed-in state, so an exporter reached without a
// preceding process() (or after a config change that only invalidated psGCodeExport)
// still emits in the frame the current config asks for.
this->update_imex_slice_offset();
const Vec2d imex_off = this->get_imex_slice_offset();
gcode->set_gcode_offset_with_imex_shift(origin(0), origin(1), imex_off.x(), imex_off.y());
gcode->do_export(this, path.c_str(), result, thumbnail_cb);
gcode->export_layer_filaments(result);
//BBS
@@ -4116,14 +4263,60 @@ Points Print::first_layer_wipe_tower_corners(bool check_wipe_tower_existance) co
return corners;
}
// IMEX firmware-managed zones: derive the slice-time XY shift from the config that has
// already been applied to this Print, so the engine never has to be told what it can work
// out. Deriving it here rather than taking a push from PartPlate is what makes a headless
// slice correct: the CLI builds a PartPlateList but drives no plater, so nothing ever ran
// the GUI-side handoff and the shift silently stayed at zero while the mode G-code told the
// firmware to fan copies out — parts in the wrong place, no diagnostic.
//
// Inputs, all reachable without a GUI:
// * m_full_print_config — the printer preset's IMEX keys, read from the full config so this
// sees the same values the preset holds whichever representation carries them.
// * the plate's `imex_parallel_mode`, read off the object config. That is the same source
// GCode.cpp and validate() resolve the active mode from (the plate's own config is
// merged into the full config before apply() by BackgroundSlicingProcess in the GUI and
// by CLI_Main in the CLI), so the shift can never disagree with the mode actually
// emitted. compute_imex_zone_layout() yields no zones for a mode the printer does not
// define, matching GCode.cpp's fallback to Primary for an unresolved mode name.
// * printable_area — the bed in PLATE-LOCAL mm. This must not be the plate's world-frame
// outline: translate_to_print_space() and the writer offset already subtract m_origin,
// so an offset carrying the plate origin would subtract it twice and put every plate
// but the first one a full plate stride out.
//
// A non-IMEX printer, or one with firmware-managed zones off, costs two bool reads and
// keeps m_imex_slice_offset at exactly Vec2d::Zero() — which is what keeps its G-code
// byte-identical.
void Print::update_imex_slice_offset()
{
m_imex_slice_offset = Vec2d::Zero();
if (!m_config.is_imex.value || !m_config.imex_firmware_managed_zones.value || m_objects.empty())
return;
// Already the resolved plate-or-process mode: the plate config overrides the process
// preset's key on the way in, exactly as PartPlate::get_imex_mode() prefers the plate.
const std::string& active_mode = m_objects.front()->config().imex_parallel_mode.value;
const ImexZoneLayout layout = compute_imex_zone_layout(m_full_print_config, active_mode,
std::string(),
get_extents(m_config.printable_area.values));
// Empty / Primary mode and an empty layout both come back as Vec2d::Zero() here.
m_imex_slice_offset = compute_imex_slice_offset(true, active_mode, layout.primary_zone_box);
}
//SoftFever
// "Print space" is the frame the emitted gcode uses. IMEX firmware-managed mode
// shifts that frame by `m_imex_slice_offset` (the primary zone center in plate-local
// coords) so the centered slice can be fanned out by firmware. Offset is zero in
// every other case → identical behavior for non-firmware-managed printers.
Vec2d Print::translate_to_print_space(const Vec2d &point) const {
//const BoundingBoxf bed_bbox(config().printable_area.values);
return Vec2d(point(0) - m_origin(0), point(1) - m_origin(1));
return Vec2d(point(0) - m_origin(0) - m_imex_slice_offset.x(),
point(1) - m_origin(1) - m_imex_slice_offset.y());
}
Vec2d Print::translate_to_print_space(const Point &point) const {
return Vec2d(unscaled(point.x()) - m_origin(0), unscaled(point.y()) - m_origin(1));
return Vec2d(unscaled(point.x()) - m_origin(0) - m_imex_slice_offset.x(),
unscaled(point.y()) - m_origin(1) - m_imex_slice_offset.y());
}
FilamentTempType Print::get_filament_temp_type(const std::string& filament_type)
+15
View File
@@ -1287,6 +1287,18 @@ public:
//BBS: plate's origin related functions
void set_plate_origin(Vec3d origin) { m_origin = origin; }
const Vec3d get_plate_origin() const { return m_origin; }
// IMEX firmware-managed zones: additional XY shift applied at gcode emission so the
// primary zone's contents land at bed origin (0,0). Non-zero only when
// `imex_firmware_managed_zones` is on AND the active mode is non-primary; Vec2d::Zero()
// in every other case → byte-identical gcode output for everyone else.
//
// Derived, never pushed in: update_imex_slice_offset() recomputes it from the applied
// config, so a headless slice gets the same shift a GUI slice does. It is a pure
// function of the config + printable_area, so calling it twice is a no-op; process()
// and export_gcode() both call it, which is what settles it ahead of every consumer
// (the writer offset in export_gcode(), and translate_to_print_space()).
void update_imex_slice_offset();
Vec2d get_imex_slice_offset() const { return m_imex_slice_offset; }
//BBS: export gcode from previous gcode file from 3mf
void set_gcode_file_ready();
void set_gcode_file_invalidated();
@@ -1539,6 +1551,9 @@ private:
//BBS: plate's origin
Vec3d m_origin {0, 0, 0};
// IMEX firmware-managed slice offset (plate-local primary-zone center, or zero).
// Cache of update_imex_slice_offset(); nothing outside that function writes it.
Vec2d m_imex_slice_offset { Vec2d::Zero() };
//BBS: modified_count
int m_modified_count {0};
//BBS
+30
View File
@@ -1,4 +1,5 @@
#include "ClipperUtils.hpp"
#include "IMEXHelpers.hpp"
#include "Geometry.hpp"
#include "CustomGCode.hpp"
#include "Config.hpp"
@@ -1385,6 +1386,35 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
}
}
// Fill in physical_extruder_map when the printer profile has not authored one. It has one
// entry per logical extruder, so it is sized from the nozzle count -- not from
// printer_extruder_id, which is indexed by variant slot.
//
// IMEX printers only. physical_extruder_map carries two readings in this tree (see
// IMEXHelpers.hpp): the IMEX paths need one entry per logical extruder, the inherited BBL
// paths read it through the clamping get_at(), which collapses every logical extruder to
// physical 0 while the profile default is the single-element {0}. Deriving the identity
// unconditionally would impose the IMEX reading on printers that mean the other one and
// change what they emit -- {first_tools}/{first_filaments}/{first_non_support_*} and
// {most_used_physical_extruder_id}/{curr_physical_extruder_id} in custom start G-code, plus
// the `; physical_extruder_map =` line in the G-code config block -- for every multi-nozzle
// profile that never authored a map. Non-IMEX printers therefore keep the config value
// exactly as it arrives, which is what upstream slices with.
const auto* is_imex_opt = new_full_config.option<ConfigOptionBool>("is_imex");
if (is_imex_opt && is_imex_opt->value) {
auto* pem = new_full_config.option<ConfigOptionInts>("physical_extruder_map", true);
if (pem) {
pem->values = effective_physical_extruder_map(pem, extruder_count).values;
// m_ori_full_print_config was snapshotted above, before this derivation, and the
// selector write-back path rebuilds m_full_print_config from that snapshot. Without
// mirroring the derived map into it, m_full_print_config keeps the unexpanded default
// while every later apply re-derives the expanded one, so an unchanged config diffs
// on physical_extruder_map forever and invalidates all steps on every re-apply.
if (auto* ori_pem = m_ori_full_print_config.option<ConfigOptionInts>("physical_extruder_map", true))
ori_pem->values = pem->values;
}
}
auto opt_filament_map = new_full_config.option<ConfigOptionInts>("filament_map");
std::vector<int> filament_maps = opt_filament_map ? opt_filament_map->values : std::vector<int>();
+173
View File
@@ -274,6 +274,7 @@ static t_config_enum_values s_keys_map_WipeTowerType {
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(WipeTowerType)
static t_config_enum_values s_keys_map_FuzzySkinMode {
{ "displacement", int(FuzzySkinMode::Displacement) },
{ "extrusion", int(FuzzySkinMode::Extrusion) },
@@ -720,6 +721,24 @@ static const t_config_enum_values s_keys_map_FilamentMapMode = {
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(FilamentMapMode)
// IMEX: keep the legacy hyphenated wire strings so existing presets / 3MFs
// deserialize unchanged after the coString -> coEnum migration.
static const t_config_enum_values s_keys_map_ImexToolLayout = {
{ "front-left", int(ImexToolLayout::FrontLeft) },
{ "front-right", int(ImexToolLayout::FrontRight) },
{ "rear-left", int(ImexToolLayout::RearLeft) },
{ "rear-right", int(ImexToolLayout::RearRight) }
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(ImexToolLayout)
static const t_config_enum_values s_keys_map_ImexVizTheme = {
{ "standard", int(ImexVizTheme::Standard) },
{ "deuteranopia", int(ImexVizTheme::Deuteranopia) },
{ "tritanopia", int(ImexVizTheme::Tritanopia) },
{ "high_contrast", int(ImexVizTheme::HighContrast) }
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(ImexVizTheme)
// PrimeVolumeMode. Serialized string keys must stay stable; they round-trip through .3mf.
static const t_config_enum_values s_keys_map_PrimeVolumeMode = {
{ "Default", pvmDefault },
@@ -6904,6 +6923,139 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(true));
// IDEX/IQEX (independent X extruder) — parallel printing support for IDEX/IQEX printers.
// Every key in this group is read at slice time from m_config, and the two per-plate process
// keys (imex_parallel_mode, imex_head_filament_map) round-trip through the 3MF's
// model_settings.config plate metadata, so none of them needs to appear in the exported
// g-code. They all carry non-nil defaults, so emitting them would add a line to every
// printer's dump. Kept out of the g-code config block (banned_keys).
def = this->add("is_imex", coBool);
def->label = L("IDEX/IQEX Printer");
def->tooltip = L("Enable parallel printing for printers with multiple independent carriages (IDEX, IQEX, and similar).");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
// Firmware-managed zones: slice the primary zone's contents at bed origin (0,0)
// so a center-origin printer firmware can apply its own copy/mirror offsets to
// each toolhead. The slicer's zone overlay, placement check, and mode signaling
// are unchanged; only the gcode emission frame shifts.
def = this->add("imex_firmware_managed_zones", coBool);
def->label = L("Firmware-managed zones");
def->tooltip = L("When enabled, the slicer emits g-code with the primary zone's "
"contents centered on bed origin (0,0). The printer firmware is "
"responsible for placing copies/mirrors at each physical zone "
"position. Enable only on printers whose firmware applies its own "
"zone offsets in copy/mirror mode (e.g. RepRapFirmware IDEX "
"duplication mode). Has no effect in Primary mode.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("imex_gantry_count", coInt);
def->label = L("Gantry Count");
def->tooltip = L("Number of independent Y-axis gantries. 1 for IDEX/single-rail IQEX. 2 for dual-gantry systems (divides bed into rows).");
def->min = 1;
def->max = 2;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionInt(1));
def = this->add("imex_tools_per_gantry", coInt);
def->label = L("Tools per Gantry");
def->tooltip = L("Number of independent toolheads per gantry along X. 2 for IDEX-style.");
def->min = 1;
def->max = 4;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionInt(2));
def = this->add("imex_tool_layout", coEnum);
def->label = L("Tool 0 Position");
def->tooltip = L("Physical position on the bed where tool T0 (index 0) is located. "
"Determines how tool indices map to bed zones. "
"For single-gantry setups (IDEX) only left/right matters; "
"for dual-gantry setups (IQEX) all four corners are selectable "
"(front = lower Y / near the operator, rear = higher Y / back of machine).");
def->mode = comAdvanced;
def->enum_keys_map = &ConfigOptionEnum<ImexToolLayout>::get_enum_values();
def->enum_values.push_back("front-left");
def->enum_values.push_back("front-right");
def->enum_values.push_back("rear-left");
def->enum_values.push_back("rear-right");
def->enum_labels.push_back(L("Front-left"));
def->enum_labels.push_back(L("Front-right"));
def->enum_labels.push_back(L("Rear-left"));
def->enum_labels.push_back(L("Rear-right"));
def->set_default_value(new ConfigOptionEnum<ImexToolLayout>(ImexToolLayout::FrontLeft));
def = this->add("imex_nozzle_clearance_x", coFloat);
def->label = L("Nozzle Clearance X");
def->tooltip = L("Distance (mm) from the nozzle to the collision-side carriage edge in X. Used to calculate the width of the collision exclusion strip at each X boundary.");
def->sidetext = L("mm");
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(30.0));
def = this->add("imex_nozzle_clearance_y", coFloat);
def->label = L("Nozzle Clearance Y");
def->tooltip = L("Distance (mm) from the nozzle to the collision-side carriage edge in Y. Used to calculate the width of the collision exclusion strip at each Y boundary.");
def->sidetext = L("mm");
def->min = 0;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(30.0));
def = this->add("imex_carriage_margin", coFloat);
def->label = L("Safety Margin");
def->tooltip = L("Non-blocking advisory clearance strip (mm) drawn inside the primary zone at each carriage boundary. Parts placed within this strip will still slice; it is a visual reminder to leave extra clearance near the zone edge.");
def->sidetext = L("mm");
def->min = 0;
def->max = 200;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0.0));
def = this->add("imex_viz_theme", coEnum);
def->label = L("Visualization Theme");
def->tooltip = L("Color theme for IDEX/IQEX bed zone visualization. Choose a colorblind-friendly theme if needed.");
def->mode = comAdvanced;
def->enum_keys_map = &ConfigOptionEnum<ImexVizTheme>::get_enum_values();
def->enum_values.push_back("standard");
def->enum_values.push_back("deuteranopia");
def->enum_values.push_back("tritanopia");
def->enum_values.push_back("high_contrast");
def->enum_labels.push_back(L("Standard"));
def->enum_labels.push_back(L("Deuteranopia / Protanopia (red-green)"));
def->enum_labels.push_back(L("Tritanopia (blue-yellow)"));
def->enum_labels.push_back(L("High Contrast"));
def->set_default_value(new ConfigOptionEnum<ImexVizTheme>(ImexVizTheme::Standard));
def = this->add("imex_parallel_mode", coString);
def->label = L("IDEX/IQEX Print Mode");
def->tooltip = L("Name of the active IDEX/IQEX parallel print mode, or \"primary\" for single-carriage printing. Requires IDEX/IQEX Printer enabled in the Printer preset (Printer \u2192 Multimaterial \u2192 IDEX/IQEX Configuration).");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionString("primary"));
def = this->add("imex_head_filament_map", coString);
def->label = L("IDEX/IQEX head filament map");
def->tooltip = L("Per-plate override mapping physical heads to filament slots "
"(1-based). Empty means use pem-inversion defaults.");
def->mode = comDevelop;
def->set_default_value(new ConfigOptionString(""));
def = this->add("imex_mode_names", coStrings);
def->label = L("IDEX/IQEX Mode Names");
def->tooltip = L("Display names for each user-defined IDEX/IQEX parallel print mode.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionStrings());
def = this->add("imex_mode_active_tools", coStrings);
def->label = L("IDEX/IQEX Mode Active Tools");
def->tooltip = L("Tool role assignments for each mode. Format: \"idx:P,idx:C,idx:M\" where P=Primary, C=Copy, M=Mirror (e.g. \"0:P,1:C,2:M,3:M\"). Managed by the IDEX/IQEX Modes editor in the Printer preset.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionStrings());
def = this->add("imex_mode_gcodes", coStrings);
def->label = L("IDEX/IQEX Mode G-codes");
def->tooltip = L("G-code or macro call to inject at print start for each mode.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionStrings());
def = this->add("manual_filament_change", coBool);
def->label = L("Manual Filament Change");
def->tooltip = L("Enable this option to omit the custom Change filament G-code only at the beginning of the print. "
@@ -9711,6 +9863,24 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
else if (opt_key == "wall_direction" && value == "auto") {
value = "ccw";
}
// Orca: the IDEX/IQEX parallel printing keys shipped to testers as ixex_* before the feature
// was renamed IMEX, and the two clearance keys were renamed again to say what they measure:
// nozzle to carriage edge on the collision side, not the carriage's full width. Without this
// an existing printer profile loses every one of these values silently.
// is_ixex is the master gate: without it every other key below migrates into a feature that
// stays switched off, which is worse than losing them all, because the settings then look
// configured. ixex_primary_col/_row are the only era-1 keys with no modern counterpart (the
// primary is a role in the mode's tools string now); they were never in an option list, so no
// saved file carries them, and the has() check at the end of this function drops them anyway.
else if (opt_key == "is_ixex") {
opt_key = "is_imex";
} else if (opt_key.compare(0, 5, "ixex_") == 0) {
opt_key = "imex_" + opt_key.substr(5);
if (opt_key == "imex_carriage_width_x")
opt_key = "imex_nozzle_clearance_x";
else if (opt_key == "imex_carriage_width_y")
opt_key = "imex_nozzle_clearance_y";
}
// Ignore the following obsolete configuration keys:
static std::set<std::string> ignore = {
@@ -13064,6 +13234,9 @@ OtherSlicingStatesConfigDef::OtherSlicingStatesConfigDef()
new_def("initial_no_support_extruder", coInt, "Initial no support extruder", "Zero-based index of the first extruder used for printing without support. Same as initial_no_support_tool.");
new_def("in_head_wrap_detect_zone", coBool, "In head wrap detect zone", "Indicates if the first layer overlaps with the head wrap zone.");
new_def("curr_bed_type", coString, "Current bed type", "Name of the currently selected bed plate type (e.g. 'Textured PEI Plate', 'Smooth High Temp Plate').");
new_def("imex_mode", coString, "IDEX/IQEX active mode", "Name of the active IDEX/IQEX parallel print mode for this plate (e.g. 'primary', 'mirror', 'copy'). Empty string if IDEX/IQEX is not enabled.");
new_def("imex_mode_index", coInt, "IDEX/IQEX active mode index", "Zero-based index of the active IDEX/IQEX parallel print mode within imex_mode_names.");
new_def("imex_mode_gcode", coString, "IDEX/IQEX active mode G-code", "The raw mode G-code template for the active IDEX/IQEX parallel print mode, after placeholder evaluation. Globals defined here flow into machine_start_gcode.");
}
PrintStatisticsConfigDef::PrintStatisticsConfigDef()
+40
View File
@@ -250,6 +250,26 @@ enum class WallDirection
Count,
};
// IMEX: physical bed corner where tool T0 sits. Determines how tool indices
// map to bed zones (front = lower Y near operator, rear = higher Y).
enum class ImexToolLayout {
FrontLeft,
FrontRight,
RearLeft,
RearRight,
Count,
};
// IMEX: color theme for the bed-zone visualization. Colorblind-friendly variants
// are provided for users with color-vision differences.
enum class ImexVizTheme {
Standard,
Deuteranopia,
Tritanopia,
HighContrast,
Count,
};
// Orca: print order of surface fill loops/fragments for center-based fill patterns
// (Concentric, Archimedean Chords, Octagram Spiral).
enum class SurfaceFillOrder {
@@ -1396,6 +1416,13 @@ PRINT_CONFIG_CLASS_DEFINE(
// Orca: internal use only
((ConfigOptionBool, calib_flowrate_topinfill_special_order)) // ORCA: special flag for flow rate calibration
// IDEX/IQEX parallel print mode (per-print selection, stores mode name or "primary")
((ConfigOptionString, imex_parallel_mode))
// Per-plate head→filament override for MMU-equipped printers.
// Serialized as compact "phys:slot,phys:slot" (1-based filament slots, matches UI).
// Empty string means "use first_filament_for_physical_head defaults everywhere".
((ConfigOptionString, imex_head_filament_map))
)
// This object is mapped to Perl as Slic3r::Config::PrintRegion.
@@ -1768,6 +1795,19 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionStrings, filament_start_gcode))
((ConfigOptionBool, single_extruder_multi_material))
((ConfigOptionBool, manual_filament_change))
// IDEX/IQEX (independent X extruder) — parallel printing support for IDEX/IQEX printers
((ConfigOptionBool, is_imex))
((ConfigOptionBool, imex_firmware_managed_zones))
((ConfigOptionInt, imex_gantry_count))
((ConfigOptionInt, imex_tools_per_gantry))
((ConfigOptionFloat, imex_nozzle_clearance_x))
((ConfigOptionFloat, imex_nozzle_clearance_y))
((ConfigOptionFloat, imex_carriage_margin))
((ConfigOptionEnum<ImexToolLayout>, imex_tool_layout))
((ConfigOptionEnum<ImexVizTheme>, imex_viz_theme))
((ConfigOptionStrings, imex_mode_names))
((ConfigOptionStrings, imex_mode_active_tools))
((ConfigOptionStrings, imex_mode_gcodes))
((ConfigOptionBool, single_extruder_multi_material_priming))
((ConfigOptionEnum<ToolChangeOrderingType>, toolchange_ordering))
((ConfigOptionString, toolchange_cyclic_order))
+14
View File
@@ -247,6 +247,20 @@ void smooth_height_pixels(std::vector<uint8_t> &pixels, int width, int height, f
}
} // namespace
bool height_texture_has_color(const TextureDisplacementLayer &layer)
{
if (layer.empty())
return false;
{
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
const auto it = g_decoded_texture_cache.entries.find(layer.image_data.get());
if (it != g_decoded_texture_cache.entries.end() && it->second.first.lock() == layer.image_data)
return it->second.second.has_color();
}
// Not decoded yet. Decoding caches the raw image, so this happens once per image.
return decode_height_texture(layer).has_color();
}
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer)
{
DecodedHeightTexture result;
+17 -5
View File
@@ -1,6 +1,7 @@
#ifndef slic3r_TextureDisplacement_hpp_
#define slic3r_TextureDisplacement_hpp_
#include <cmath>
#include <cstddef>
#include <Eigen/Core>
#include <cstdint>
@@ -387,6 +388,10 @@ struct TextureDisplacementOptions
// image (TextureDetail::flat_colors): a texture of flat colours prints in single filaments, a
// photograph or gradient in mixes. Off forces single filaments everywhere.
bool color_mix_enabled = true;
// The most mixes the palette may offer. Every mix a bake paints with becomes a mixed filament slot,
// so this is also the most slots one bake can add. The mixes themselves are picked from the
// texture's colours, those that improve the match the most coming first.
int color_mix_count = 8;
// Majority-filter passes over the assigned colours. See TextureColorRequest::despeckle_passes -
// this is the control for it, and 2 is enough to clear the salt-and-pepper an image with detail
// finer than the mesh leaves behind, without eating features that are genuinely a facet wide.
@@ -396,7 +401,7 @@ struct TextureDisplacementOptions
{
ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border,
pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k,
v2_relocate, color_mix_enabled, color_despeckle);
v2_relocate, color_mix_enabled, color_despeckle, color_mix_count);
}
};
@@ -480,6 +485,10 @@ struct DecodedHeightTexture
// DecodedHeightTexture if image_data is empty or is not a PNG at all.
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer);
// decode_height_texture(layer).has_color(), answered from the decode cache rather than from a copy of the
// texture - cheap enough to ask every frame. Smoothing does not change it, so the raw decode is what is read.
bool height_texture_has_color(const TextureDisplacementLayer &layer);
// Maps a linear RGB colour in [0, 1] to an index into the caller's palette, or -1 for "no colour".
//
// Deliberately a callback rather than a function here: matching a colour to a filament is a
@@ -494,12 +503,14 @@ using ColorQuantizeFn = std::function<int(const Vec3f &)>;
// interleaving, which the slicer does per print layer. Plain data, so it can be captured into a job.
struct PrintableColor
{
Vec3f rgb = Vec3f::Zero(); // what it looks like; for a mix, the perceptual average of the two
Vec3f rgb = Vec3f::Zero(); // what it looks like; for a mix, the colour its mixed filament slot shows
int a = 0; // filament index
int b = 0; // the second filament; == a for a pure entry
int num = 1; // a's share of the interleave, out of `den`
int den = 1;
bool is_mix() const { return a != b; }
// a's share in percent, the form a mixed filament slot is created from.
int a_percent() const { return int(std::lround(100.0 * double(num) / double(den))); }
};
// Everything needed to colour a mesh, captured on the main thread and handed to a job. An empty
@@ -762,9 +773,10 @@ struct TextureColorRequest
float min_color_region_mm2 = 0.5f;
// Filled per *base mesh* triangle (the bake is topology-preserving, so this indexes the returned
// mesh too): the quantize callback's index plus one, or 0 for "this triangle takes no colour from
// the texture". The +1 is not arbitrary - it lines up with EnforcerBlockerType, where 0 is NONE
// ("use the volume's own filament") and 1..16 are Extruder1..16, so the caller can hand these
// straight to a TriangleSelector without a second mapping table.
// the texture". The +1 lines up with EnforcerBlockerType, where 0 is NONE ("use the volume's own
// filament"): where every palette entry is a filament, these go straight to a TriangleSelector. A
// palette with mixes maps each index to the mix's filament slot first (see
// GLGizmoTextureDisplacement::palette_filaments()).
std::vector<uint8_t> *out_triangle = nullptr;
};
+4
View File
@@ -283,6 +283,10 @@ set(SLIC3R_GUI_SOURCES
GUI/FilamentMapDialog.hpp
GUI/IconManager.cpp
GUI/IconManager.hpp
GUI/IMEXFilamentPickerPopover.cpp
GUI/IMEXFilamentPickerPopover.hpp
GUI/IMEXModesCtrl.cpp
GUI/IMEXModesCtrl.hpp
GUI/IdleScheduler.cpp
GUI/IdleScheduler.hpp
GUI/ImageGrid.cpp
+29 -29
View File
@@ -1112,7 +1112,7 @@ wxWindow *CreateFilamentPresetDialog::create_dialog_buttons()
if (!m_can_not_find_vendor_checkbox->GetValue()) {
if (_L("Select Vendor") == vendor_str) {
MessageDialog dlg(this, _L("Vendor is not selected; please reselect vendor."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"),
wxYES | wxYES_DEFAULT | wxCENTRE);
wxOK | wxCENTRE);
dlg.ShowModal();
return;
} else {
@@ -1121,14 +1121,14 @@ wxWindow *CreateFilamentPresetDialog::create_dialog_buttons()
} else {
if (m_filament_custom_vendor_input->GetTextCtrl()->GetValue().empty()) {
MessageDialog dlg(this, _L("Custom vendor missing; please input custom vendor."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"),
wxYES | wxYES_DEFAULT | wxCENTRE);
wxOK | wxCENTRE);
dlg.ShowModal();
return;
} else {
vendor_name = into_u8(m_filament_custom_vendor_input->GetTextCtrl()->GetValue());
if (vendor_name == "Bambu" || vendor_name == "Generic") {
MessageDialog dlg(this, _L("\"Bambu\" or \"Generic\" cannot be used as a Vendor for custom filaments."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"),
wxYES | wxYES_DEFAULT | wxCENTRE);
wxOK | wxCENTRE);
dlg.ShowModal();
return;
}
@@ -1139,7 +1139,7 @@ wxWindow *CreateFilamentPresetDialog::create_dialog_buttons()
wxString type_str = m_filament_type_combobox->GetLabel();
std::string type_name;
if (_L("Select Type") == type_str) {
MessageDialog dlg(this, _L("Filament type is not selected, please reselect type."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxYES | wxYES_DEFAULT | wxCENTRE);
MessageDialog dlg(this, _L("Filament type is not selected, please reselect type."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxOK | wxCENTRE);
dlg.ShowModal();
return;
} else {
@@ -1150,7 +1150,7 @@ wxWindow *CreateFilamentPresetDialog::create_dialog_buttons()
std::string serial_name;
if (serial_str.empty()) {
MessageDialog dlg(this, _L("Filament serial missing; please input serial."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"),
wxYES | wxYES_DEFAULT | wxCENTRE);
wxOK | wxCENTRE);
dlg.ShowModal();
return;
} else {
@@ -1161,7 +1161,7 @@ wxWindow *CreateFilamentPresetDialog::create_dialog_buttons()
if (vendor_name.empty() || serial_name.empty()) {
MessageDialog dlg(this, _L("There may be disallowed characters in the vendor or serial input of the filament. Please delete and re-enter."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"),
wxYES | wxYES_DEFAULT | wxCENTRE);
wxOK | wxCENTRE);
dlg.ShowModal();
return;
}
@@ -1169,20 +1169,20 @@ wxWindow *CreateFilamentPresetDialog::create_dialog_buttons()
boost::algorithm::trim(serial_name);
if (vendor_name.empty() || serial_name.empty()) {
MessageDialog dlg(this, _L("All inputs in the custom vendor or serial are spaces. Please re-enter."),
wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxYES | wxYES_DEFAULT | wxCENTRE);
wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxOK | wxCENTRE);
dlg.ShowModal();
return;
}
if (m_can_not_find_vendor_checkbox->GetValue() && str_is_all_digit(vendor_name)) {
MessageDialog dlg(this, _L("The vendor cannot be a number; please re-enter."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"),
wxYES | wxYES_DEFAULT | wxCENTRE);
wxOK | wxCENTRE);
dlg.ShowModal();
return;
}
if (!is_check_box_selected()) {
MessageDialog dlg(this, _L("You have not selected a printer or preset yet. Please select at least one."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"),
wxYES | wxYES_DEFAULT | wxCENTRE);
wxOK | wxCENTRE);
dlg.ShowModal();
return;
}
@@ -1818,7 +1818,7 @@ wxBoxSizer *CreatePrinterPresetDialog::create_printer_item(wxWindow *parent)
m_select_model->SetLabelColor(*wxBLACK);
}
} else {
MessageDialog dlg(this, _L("The model was not found; please reselect vendor."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxYES | wxYES_DEFAULT | wxCENTRE);
MessageDialog dlg(this, _L("The model was not found; please reselect vendor."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxOK | wxCENTRE);
dlg.ShowModal();
}
@@ -2818,14 +2818,14 @@ wxWindow *CreatePrinterPresetDialog::create_page2_dialog_buttons(wxWindow *paren
// Confirm if the printer preset exists
if (!m_printer_preset) {
MessageDialog dlg(this, _L("You have not yet chosen which printer preset to create based on. Please choose the vendor and model of the printer"),
wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxYES | wxYES_DEFAULT | wxCENTRE);
wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxOK | wxCENTRE);
dlg.ShowModal();
return;
}
if (!save_printable_area_config(m_printer_preset)) {
MessageDialog dlg(this, _L("You have entered a disallowed character in the printable area section on the first page. Please use only numbers."),
wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxYES | wxYES_DEFAULT | wxCENTRE);
wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxOK | wxCENTRE);
dlg.ShowModal();
show_page1();
return;
@@ -2865,7 +2865,7 @@ wxWindow *CreatePrinterPresetDialog::create_page2_dialog_buttons(wxWindow *paren
}
}
if (selected_filament_presets.empty() && !filament_preset_is_exist) {
MessageDialog dlg(this, _L("You need to select at least one filament preset."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxYES | wxYES_DEFAULT | wxCENTRE);
MessageDialog dlg(this, _L("You need to select at least one filament preset."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxOK | wxCENTRE);
dlg.ShowModal();
return;
}
@@ -2880,7 +2880,7 @@ wxWindow *CreatePrinterPresetDialog::create_page2_dialog_buttons(wxWindow *paren
}
}
if (selected_process_presets.empty() && !process_preset_is_exist) {
MessageDialog dlg(this, _L("You need to select at least one process preset."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxYES | wxYES_DEFAULT | wxCENTRE);
MessageDialog dlg(this, _L("You need to select at least one process preset."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxOK | wxCENTRE);
dlg.ShowModal();
return;
}
@@ -3125,7 +3125,7 @@ void CreatePrinterPresetDialog::on_select_printer_model(wxCommandEvent &e)
wxArrayString printer_preset_model = printer_preset_sort_with_nozzle_diameter(m_printer_preset_vendor_selected, nozzle);
if (printer_preset_model.size() == 0) {
MessageDialog dlg(this, _L("Current vendor has no models, please reselect."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxYES | wxYES_DEFAULT | wxCENTRE);
MessageDialog dlg(this, _L("Current vendor has no models, please reselect."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxOK | wxCENTRE);
dlg.ShowModal();
return;
}
@@ -3386,7 +3386,7 @@ bool CreatePrinterPresetDialog::validate_input_valid()
std::string model_name = get_printer_model();
if ((vendor_name.empty() || model_name.empty())) {
MessageDialog dlg(this, _L("You have not selected the vendor and model or input the custom vendor and model."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"),
wxYES | wxYES_DEFAULT | wxCENTRE);
wxOK | wxCENTRE);
dlg.ShowModal();
return false;
}
@@ -3395,7 +3395,7 @@ bool CreatePrinterPresetDialog::validate_input_valid()
model_name = remove_special_key(model_name);
if (vendor_name.empty() || model_name.empty()) {
MessageDialog dlg(this, _L("There may be escape characters in the custom printer vendor or model. Please delete and re-enter."),
wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxYES | wxYES_DEFAULT | wxCENTRE);
wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxOK | wxCENTRE);
dlg.ShowModal();
return false;
}
@@ -3403,13 +3403,13 @@ bool CreatePrinterPresetDialog::validate_input_valid()
boost::algorithm::trim(model_name);
if (vendor_name.empty() || model_name.empty()) {
MessageDialog dlg(this, _L("All inputs in the custom printer vendor or model are spaces. Please re-enter."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"),
wxYES | wxYES_DEFAULT | wxCENTRE);
wxOK | wxCENTRE);
dlg.ShowModal();
return false;
}
if (check_printable_area() == false) {
MessageDialog dlg(this, _L("Please check bed printable shape and origin input."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxYES | wxYES_DEFAULT | wxCENTRE);
MessageDialog dlg(this, _L("Please check bed printable shape and origin input."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxOK | wxCENTRE);
dlg.ShowModal();
return false;
}
@@ -3417,7 +3417,7 @@ bool CreatePrinterPresetDialog::validate_input_valid()
wxString printer_name = m_select_printer->GetStringSelection();
if (printer_name.empty()) {
MessageDialog dlg(this, _L("You have not yet selected the printer to replace the nozzle for; please choose a printer."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"),
wxYES | wxYES_DEFAULT | wxCENTRE);
wxOK | wxCENTRE);
dlg.ShowModal();
return false;
}
@@ -3682,22 +3682,22 @@ void ExportConfigsDialog::show_export_result(const ExportCase &export_case)
MessageDialog *msg_dlg = nullptr;
switch (export_case) {
case ExportCase::INITIALIZE_FAIL:
msg_dlg = new MessageDialog(this, _L("initialize fail"), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxYES | wxYES_DEFAULT | wxCENTRE);
msg_dlg = new MessageDialog(this, _L("initialize fail"), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxOK | wxCENTRE);
break;
case ExportCase::ADD_FILE_FAIL:
msg_dlg = new MessageDialog(this, _L("add file fail"), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxYES | wxYES_DEFAULT | wxCENTRE);
msg_dlg = new MessageDialog(this, _L("add file fail"), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxOK | wxCENTRE);
break;
case ExportCase::ADD_BUNDLE_STRUCTURE_FAIL:
msg_dlg = new MessageDialog(this, _L("add bundle structure file fail"), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxYES | wxYES_DEFAULT | wxCENTRE);
msg_dlg = new MessageDialog(this, _L("add bundle structure file fail"), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxOK | wxCENTRE);
break;
case ExportCase::FINALIZE_FAIL:
msg_dlg = new MessageDialog(this, _L("finalize fail"), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxYES | wxYES_DEFAULT | wxCENTRE);
msg_dlg = new MessageDialog(this, _L("finalize fail"), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxOK | wxCENTRE);
break;
case ExportCase::OPEN_ZIP_WRITTEN_FILE:
msg_dlg = new MessageDialog(this, _L("open zip written fail"), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxYES | wxYES_DEFAULT | wxCENTRE);
msg_dlg = new MessageDialog(this, _L("open zip written fail"), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxOK | wxCENTRE);
break;
case ExportCase::EXPORT_SUCCESS:
msg_dlg = new MessageDialog(this, _L("Export successful"), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxYES | wxYES_DEFAULT | wxCENTRE);
msg_dlg = new MessageDialog(this, _L("Export successful"), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxOK | wxCENTRE);
break;
}
@@ -3768,7 +3768,7 @@ std::string ExportConfigsDialog::initial_file_name(const wxString &path, const s
MessageDialog dlg(this,
wxString::Format(_L("The file: %s\nmay have been opened by another program.\nPlease close it and try again."),
encode_path(printer_export_path.string().c_str())),
wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxYES | wxYES_DEFAULT | wxCENTRE);
wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"), wxOK | wxCENTRE);
dlg.ShowModal();
return "initial_failed";
}
@@ -4324,7 +4324,7 @@ wxWindow *ExportConfigsDialog::create_dialog_buttons(wxWindow* parent)
dlg_btns->GetOK()->Bind(wxEVT_BUTTON, [this](wxCommandEvent &e) {
if (!has_check_box_selected()) {
MessageDialog dlg(this, _L("Please select at least one printer or filament."), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"),
wxYES | wxYES_DEFAULT | wxCENTRE);
wxOK | wxCENTRE);
dlg.ShowModal();
return;
}
@@ -5042,7 +5042,7 @@ wxWindow *CreatePresetForPrinterDialog::create_dialog_buttons()
} else {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "filament choice not find filament preset and choice is:" << filament_preset_name;
MessageDialog dlg(this, _L("The filament choice not find filament preset, please reselect it"), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"),
wxYES | wxYES_DEFAULT | wxCENTRE);
wxOK | wxCENTRE);
dlg.ShowModal();
return;
}
+6 -6
View File
@@ -477,27 +477,27 @@ void ExportPresetBundleDialog::show_export_result(const ExportCase& export_case)
switch (export_case) {
case ExportCase::INITIALIZE_FAIL:
msg_dlg = new MessageDialog(this, _L("initialize fail"), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"),
wxYES | wxYES_DEFAULT | wxCENTRE);
wxOK | wxCENTRE);
break;
case ExportCase::ADD_FILE_FAIL:
msg_dlg = new MessageDialog(this, _L("add file fail"), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"),
wxYES | wxYES_DEFAULT | wxCENTRE);
wxOK | wxCENTRE);
break;
case ExportCase::ADD_BUNDLE_STRUCTURE_FAIL:
msg_dlg = new MessageDialog(this, _L("add bundle structure file fail"), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"),
wxYES | wxYES_DEFAULT | wxCENTRE);
wxOK | wxCENTRE);
break;
case ExportCase::FINALIZE_FAIL:
msg_dlg = new MessageDialog(this, _L("finalize fail"), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"),
wxYES | wxYES_DEFAULT | wxCENTRE);
wxOK | wxCENTRE);
break;
case ExportCase::OPEN_ZIP_WRITTEN_FILE:
msg_dlg = new MessageDialog(this, _L("open zip written fail"), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"),
wxYES | wxYES_DEFAULT | wxCENTRE);
wxOK | wxCENTRE);
break;
case ExportCase::EXPORT_SUCCESS:
msg_dlg = new MessageDialog(this, _L("Export successful"), wxString(SLIC3R_APP_FULL_NAME) + " - " + _L("Info"),
wxYES | wxYES_DEFAULT | wxCENTRE);
wxOK | wxCENTRE);
break;
}
+1 -1
View File
@@ -559,7 +559,7 @@ void Field::get_value_by_opt_type(wxString& str, const bool check_value/* = true
else if(m_opt_id == "filament_retraction_distances_when_cut" || opt_key_without_idx == "retraction_distances_when_cut"){
if (m_value.empty() || boost::any_cast<double>(m_value) != val) {
wxString msg_text = format_wxstr(_L("Value %s is out of range. The valid range is from %d to %d."), str, m_opt.min, m_opt.max);
WarningDialog dialog(m_parent, msg_text, _L("Parameter validation") + ": " + m_opt_id, wxYES);
WarningDialog dialog(m_parent, msg_text, _L("Parameter validation") + ": " + m_opt_id, wxOK);
if (dialog.ShowModal()) {
if (m_value.empty()) {
if (m_opt.min > val) val = m_opt.min;

Some files were not shown because too many files have changed in this diff Show More