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
This commit is contained in:
SoftFever
2026-10-10 20:34:56 +08:00
committed by GitHub
120 changed files with 16493 additions and 96 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
@@ -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

+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"
}
+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
+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))
+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
+465 -2
View File
@@ -4,10 +4,13 @@
#include "libslic3r/BuildVolume.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/LocalesUtils.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/IMEXHelpers.hpp"
#include "libslic3r/IMEXZones.hpp"
//BBS: add convex hull logic for toolpath check
#include "GUI_App.hpp"
@@ -25,6 +28,7 @@
#include "MsgDialog.hpp"
#include <boost/container_hash/hash.hpp>
#include "slic3r/GUI/MeshUtils.hpp"
#include <set>
#include <string>
#include "libvgcode/include/Types.hpp"
#include <vector>
@@ -89,6 +93,7 @@
#include <array>
#include <algorithm>
#include <limits>
#include <sstream>
#include <cmath>
#include <chrono>
#include <Eigen/Geometry>
@@ -320,7 +325,7 @@ void GCodeViewer::SequentialView::Marker::init(std::string filename)
} else {
m_model.init_from_file(filename);
}
m_model.set_color({ 1.0f, 1.0f, 1.0f, 0.5f });
m_model.set_color(default_color());
}
//BBS: GUI refactor: add canvas size from parameters
@@ -1094,9 +1099,13 @@ void GCodeViewer::SequentialView::GCodeWindow::stop_mapping_file()
}
void GCodeViewer::SequentialView::render_marker(const bool has_render_path, int canvas_width, int canvas_height, const libvgcode::EViewType& view_type)
{
if (has_render_path && m_show_marker)
if (has_render_path && m_show_marker) {
// marker.set_world_offset(current_offset);
marker.render(canvas_width, canvas_height, view_type);
// IDEX/IQEX secondary carriage markers
for (auto& sec : m_imex_secondary_markers)
sec.render(canvas_width, canvas_height, view_type);
}
}
void GCodeViewer::SequentialView::render_overlay(const bool has_render_path, float legend_height, const libvgcode::Viewer* viewer, uint32_t gcode_id, int canvas_width, int canvas_height, int right_margin, const libvgcode::EViewType& view_type)
@@ -1156,6 +1165,7 @@ void GCodeViewer::init(ConfigOptionMode mode, PresetBundle* preset_bundle)
}
}
m_marker_filename = filename;
m_sequential_view.marker.init(filename);
// initializes point sizes
@@ -1973,6 +1983,223 @@ void GCodeViewer::reset()
m_contained_in_bed = true;
}
// IDEX/IQEX: resolve where every secondary carriage marker sits, relative to the primary,
// for one (printer preset, active mode, plate bed) triple. This parses the mode string
// several times over and rebuilds the zone grid, and every one of its inputs is preset or
// plate state, so render_scene() runs it only when GCodeViewer::ImexMarkerKey changes and replays
// the returned plan on all the frames in between.
//
// `mode` is already resolved (the plate's mode beats the process preset). An empty
// `carriages` list means "no secondary markers for this configuration" — IMEX off in this
// mode, firmware-managed zones, or a mode whose roster has no zone-owning secondary.
GCodeViewer::ImexMarkerPlan GCodeViewer::resolve_imex_marker_plan(const DynamicPrintConfig& printer_cfg,
const std::string& mode,
const BoundingBoxf& bed_extents)
{
ImexMarkerPlan plan;
// Firmware-managed-zones 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 zone math uses. Computing secondaries here
// would place them off-bed. The firmware physically fans the centered
// toolpath out into the zones, so the centered preview with no
// secondaries is the honest representation.
if (imex_cfg_bool(printer_cfg, "imex_firmware_managed_zones"))
return plan;
if (mode.empty() || mode == kImexPrimaryMode)
return plan;
// Only the gantry grouping and the mirror axis are decided here; the zone grid itself comes
// from compute_imex_zone_layout() below, which reads the same keys through the same accessor.
const int tools_per_gantry = std::max(1, imex_cfg_int(printer_cfg, "imex_tools_per_gantry"));
plan.box_wx = (float)imex_cfg_float(printer_cfg, "imex_nozzle_clearance_x");
plan.box_wy = (float)imex_cfg_float(printer_cfg, "imex_nozzle_clearance_y");
// Parse "idx:P/C/M" via shared helpers — matches PartPlate::calc_imex_zones.
// The role travels as an ImexRole all the way to the marker placement
// below; there is no int encoding in between, so a role added to the enum
// cannot silently fall into the "not Mirror, therefore Copy" branch.
int pri_tool = -1;
std::vector<int> sec_tool_ids;
std::map<int, ImexRole> sec_tool_roles; // tool_id -> Copy or Mirror
std::set<int> sec_aggregated; // representatives standing in for a whole gantry
{
// Same lookup rule as PartPlate's zones and ghosts, and as the slicer:
// an unresolved mode, and a mode the tools array is too short to cover,
// both come back empty and leave the marker roster empty.
const std::string entry = find_imex_mode(printer_cfg, mode).active_tools;
pri_tool = imex_primary_tool_for_mode(entry);
// Aggregate per gantry via the same source of truth as PartPlate's
// zone/ghost aggregation: in Span modes only one tool prints per
// zone at a time, so a non-primary gantry collapses to one
// representative marker. Non-Span gantries keep per-tool markers.
const ImexGantryGrouping grouping =
group_imex_active_tools_by_gantry(entry, tools_per_gantry);
auto add_tool = [&](int phys_idx, ImexRole role, bool aggregated) {
if (phys_idx < 0 || phys_idx == pri_tool) return;
// Only Copy and Mirror print in a zone of their own, so only they
// get a marker. Exhaustive with no default so -Wswitch makes a new
// role answer "does this carriage get its own marker?" rather than
// defaulting into a Copy-shaped one.
switch (role) {
case ImexRole::Copy:
case ImexRole::Mirror:
break;
case ImexRole::Primary: // not a secondary carriage
case ImexRole::Span: // rides in the primary's zone
return;
}
sec_tool_roles[phys_idx] = role;
sec_tool_ids.push_back(phys_idx);
if (aggregated) sec_aggregated.insert(phys_idx);
};
// Out-of-grid tool indices (a stale mode string carried over from a
// printer with more tools) are handled inconsistently by the code we
// must agree with: compute_imex_zone_layout drops them, so they own no
// zone, while calc_imex_ghosts still bakes a ghost for them. No single
// policy matches both, so the marker roster is left exactly as it was
// and such a tool simply falls back to the first zone below. Clamping
// pri_tool instead is a trap: the -1 sentinel truncates to gantry 0 in
// imex_mirror_axis_for and flips the marker to the opposite mirror
// axis from the ghost. It is also why the layout's own `primary_head`
// cannot stand in for `pri_tool` here — the layout reports -1 for an
// off-grid primary, this roster has to keep the authored index.
for (const auto& grp : grouping.groups) {
// Markers: an aggregated non-primary gantry shows only its rep.
if (grp.aggregate && grp.gantry_index != grouping.primary_gantry)
add_tool(grp.representative_phys, grp.representative_role, true);
else
for (const auto& [phys_idx, role] : grp.tools)
add_tool(phys_idx, role, false);
}
}
const int sec_count = (int)sec_tool_ids.size();
if (sec_count == 0)
return plan;
// The zone grid is not re-derived here: this is the same call
// PartPlate::calc_imex_zones() makes, with the same printer config and the
// same bed extents, so the preview and the plate cannot place a tool
// differently for the same mode. The T0-corner flips, the active
// column/row sets and the zone pitch all live inside the library; the
// markers need nothing from it but `head_zone_centers`.
// `mode` is already resolved (per-plate mode beats the process preset),
// so it goes in as the plate mode with no process fallback.
const ImexZoneLayout zone_layout =
compute_imex_zone_layout(printer_cfg, mode, std::string(), bed_extents);
// Centre of zone (0,0), the fallback for any head the layout gave no zone —
// a mode string naming a tool outside this printer's grid. Zone indices are
// dense, so the lowest centre on each axis IS zone 0's. An empty layout
// means one undivided zone, whose centre is the bed's.
Vec2d zone0_center = bed_extents.center();
if (!zone_layout.head_zone_centers.empty()) {
zone0_center = zone_layout.head_zone_centers.begin()->second;
for (const auto& head_center : zone_layout.head_zone_centers) {
zone0_center.x() = std::min(zone0_center.x(), head_center.second.x());
zone0_center.y() = std::min(zone0_center.y(), head_center.second.y());
}
}
auto zone_center_of = [&](int tid) -> Vec2d {
auto it = zone_layout.head_zone_centers.find(tid);
return (it == zone_layout.head_zone_centers.end()) ? zone0_center : it->second;
};
const Vec2d pri_center = zone_center_of(pri_tool);
// An aggregated gantry's zone is a full-X row strip with no column of its
// own, so its marker tracks primary's X. calc_imex_ghosts() reaches the same
// zero X offset by putting both frames on the bed centreline; for a single
// marker, pinning the centre says it directly.
auto sec_center_of = [&](int tid) -> Vec2d {
Vec2d c = zone_center_of(tid);
if (sec_aggregated.count(tid))
c.x() = pri_center.x();
return c;
};
// Primary carriage box: seeded from whether primary holds the leftmost zone
// column, then turned by any secondary sitting to one side of it.
plan.pri_box_offset_x = (pri_center.x() <= zone0_center.x()) ? 0.0f : -plan.box_wx;
plan.pri_box_offset_y = -plan.box_wy;
for (int i = 0; i < sec_count; ++i) {
const Vec2d sc = sec_center_of(sec_tool_ids[i]);
if (sc.x() > pri_center.x()) { plan.pri_box_offset_x = 0.0f; }
else if (sc.x() < pri_center.x()) { plan.pri_box_offset_x = -plan.box_wx; }
if (sc.y() > pri_center.y()) { plan.pri_box_offset_y = 0.0f; }
else if (sc.y() < pri_center.y()) { plan.pri_box_offset_y = -plan.box_wy; }
}
plan.pri_head = pri_tool;
plan.carriages.reserve(sec_count);
for (int i = 0; i < sec_count; ++i) {
const int sec_tool = sec_tool_ids[i];
const Vec2d sec_center = sec_center_of(sec_tool);
const ImexRole sec_role = sec_tool_roles[sec_tool];
// A carriage stays inside its own zone; a Mirror reflects its
// zone-relative offset about that zone's centerline, on the axis of the
// boundary it shares with primary — the same rule the ghosts use:
// Copy → tracks primary on both axes.
// Mirror, same gantry → reflect X (zones sit side by side).
// Mirror, other gantry → reflect Y (zones sit front-to-back); X
// tracks primary, since the part off that
// gantry is a Y-reflection of the tool
// directly behind it.
// The axis comes from the shared helper, so the markers can never drift
// out of step with the plate ghosts.
const bool is_mirror = (sec_role == ImexRole::Mirror);
const bool cross_gantry = imex_mirror_axis_for(pri_tool, sec_tool,
tools_per_gantry) == ImexMirrorAxis::Y;
// Tracking an axis translates by the gap between the two zone centres.
// Mirroring it reflects primary about the midpoint of those centres —
// the zones being equal-sized, that midpoint is the boundary between
// them, so the carriage still lands inside its own zone. Both are a
// single term applied to the live primary position each frame:
// `term - pos` when mirrored, `pos + term` when tracked.
ImexMarkerPlan::Carriage carriage;
carriage.phys_head = sec_tool;
carriage.mirror_x = is_mirror && !cross_gantry;
carriage.mirror_y = is_mirror && cross_gantry;
carriage.x_term = carriage.mirror_x ? (float)(sec_center.x() + pri_center.x())
: (float)(sec_center.x() - pri_center.x());
carriage.y_term = carriage.mirror_y ? (float)(sec_center.y() + pri_center.y())
: (float)(sec_center.y() - pri_center.y());
// The box shows the side a toolhead could COLLIDE from, not merely which
// way its body hangs. Tools sharing a gantry share an X rail and can
// actually run into each other, and only in a same-gantry (X-axis) mirror
// do they converge — so that is the one case where the secondary's box
// flips to face the primary. Tools on different gantries cannot collide
// in X at all, so a cross-gantry mirror keeps the primary's facing, the
// same as a Copy. Do not "fix" this to follow carriage geometry: a box on
// the far side would point away from the only tool it can hit.
if (is_mirror && !cross_gantry) {
if (sec_center.x() > pri_center.x()) carriage.box_offset_x = -plan.box_wx;
else if (sec_center.x() < pri_center.x()) carriage.box_offset_x = 0.0f;
else carriage.box_offset_x = plan.pri_box_offset_x;
} else {
carriage.box_offset_x = plan.pri_box_offset_x;
}
// Y follows the same collision rule: face the gantry you could hit. A tool
// on ANOTHER row faces the primary's row; a tool on the primary's OWN row
// shares its gantry and can only hit the same other gantry, so it faces
// wherever the primary faces. The old `else` hardcoded -imex_box_wy, which
// happens to equal pri_box_offset_y on the rear-* layouts (primary's row
// sits above the others, so the loop above settles on -box_wy anyway)
// — hence a no-op there. On the front-* layouts the primary flips to 0.0f
// and the hardcoded value pointed the same-gantry secondary away from the
// only tools it could run into.
if (sec_center.y() > pri_center.y()) carriage.box_offset_y = -plan.box_wy;
else if (sec_center.y() < pri_center.y()) carriage.box_offset_y = 0.0f;
else carriage.box_offset_y = plan.pri_box_offset_y;
plan.carriages.push_back(carriage);
}
return plan;
}
//BBS: GUI refactor: add canvas width and height
void GCodeViewer::render_scene(int canvas_width, int canvas_height)
{
@@ -1990,7 +2217,242 @@ void GCodeViewer::render_scene(int canvas_width, int canvas_height)
const libvgcode::PathVertex& curr_vertex = m_viewer.get_current_vertex();
m_sequential_view.marker.set_world_position(libvgcode::convert(curr_vertex.position));
m_sequential_view.marker.set_z_offset(m_z_offset + 0.5f);
// IDEX/IQEX: compute all carriage positions; update secondary nozzle markers;
// carriage_box_draws is populated for toolhead footprint rendering after render_marker().
// Okabe-Ito colorblind-safe palette — excludes orange (#E69F00) and sky blue (#56B4E9)
// which are used by the bed zone fills, ensuring the markers contrast against the background.
// It identifies the carriages in the views that colour by anything but filament; in Filament
// view a carriage takes its own head's filament colour instead, so a marker, the ghost of
// what it prints and the toolpaths under it all agree on what is loaded there.
static const std::array<ColorRGBA, 4> s_carriage_colors = {{
{ 1.000f, 1.000f, 1.000f, 0.65f }, // primary — white
{ 0.941f, 0.894f, 0.259f, 0.65f }, // secondary 1 — yellow (#F0E442)
{ 0.835f, 0.369f, 0.000f, 0.65f }, // secondary 2 — vermilion (#D55E00)
{ 0.800f, 0.475f, 0.655f, 0.65f }, // secondary 3 — reddish purple (#CC79A7)
}};
struct CarriageDraw { Vec3f pos; ColorRGBA color; float box_offset_x = 0.0f; float box_offset_y = 0.0f; };
std::vector<CarriageDraw> carriage_box_draws;
float imex_box_wx = 0.0f, imex_box_wy = 0.0f;
{
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
bool imex_active = false;
if (preset_bundle && m_sequential_view.m_show_marker) {
const DynamicPrintConfig& printer_cfg = preset_bundle->printers.get_edited_preset().config;
auto* is_imex_opt = printer_cfg.opt<ConfigOptionBool>("is_imex");
if (is_imex_opt && is_imex_opt->value) {
const DynamicPrintConfig& process_cfg = preset_bundle->prints.get_edited_preset().config;
auto* mode_opt = process_cfg.opt<ConfigOptionString>("imex_parallel_mode");
std::string mode = mode_opt ? mode_opt->value : kImexPrimaryMode;
// Per-plate mode overrides the process preset.
PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list();
PartPlate* curr_plate = plate_list.get_curr_plate();
if (curr_plate) {
// Same resolution the plate itself uses, rather than a fourth copy of it.
const std::string plate_mode = curr_plate->get_effective_imex_mode();
if (plate_mode != kImexPrimaryMode)
mode = plate_mode;
}
// Bed X and Y bounds — read from the current plate's shape, which is
// in world/GL coordinates (same space as curr_vertex.position), and is
// the exact same source PartPlate::calc_imex_zones() hands to the library.
BoundingBoxf bed_extents;
{
const Pointfs& plate_shape = curr_plate ? curr_plate->get_shape() : Pointfs{};
if (!plate_shape.empty()) {
bed_extents = get_extents(plate_shape);
} else {
// Fallback: use toolpath bounding box extent
bed_extents = BoundingBoxf(Vec2d(m_paths_bounding_box.min.x(), m_paths_bounding_box.min.y()),
Vec2d(m_paths_bounding_box.max.x(), m_paths_bounding_box.max.y()));
}
}
// Cache guard. The marker plan below is resolved from preset/plate state only,
// so it is rebuilt when that state moves and reused otherwise — the sequential
// slider is sticky, and re-resolving the zone layout on every frame of the rest
// of the session showed up as a measurable per-frame cost. The comparison is
// deliberately made against the live config rather than against a freshly built
// key, so an unchanged frame allocates nothing at all.
static const std::vector<std::string> s_no_strings;
auto* names_opt = printer_cfg.opt<ConfigOptionStrings>("imex_mode_names");
auto* tools_opt = printer_cfg.opt<ConfigOptionStrings>("imex_mode_active_tools");
const std::vector<std::string>& mode_names = names_opt ? names_opt->values : s_no_strings;
const std::vector<std::string>& active_tools = tools_opt ? tools_opt->values : s_no_strings;
ImexMarkerKey::Scalars key;
key.plate_index = plate_list.get_curr_plate_index();
// Same accessor the plan is built from: a key holding a different fallback than
// the value actually laid out would compare equal across a real change.
key.gantry_count = imex_cfg_int(printer_cfg, "imex_gantry_count");
key.tools_per_gantry = imex_cfg_int(printer_cfg, "imex_tools_per_gantry");
key.tool_layout = (int)imex_cfg_enum<ImexToolLayout>(printer_cfg, "imex_tool_layout");
key.clearance_x = imex_cfg_float(printer_cfg, "imex_nozzle_clearance_x");
key.clearance_y = imex_cfg_float(printer_cfg, "imex_nozzle_clearance_y");
key.firmware_managed = imex_cfg_bool(printer_cfg, "imex_firmware_managed_zones");
key.bed_min_x = bed_extents.min.x();
key.bed_min_y = bed_extents.min.y();
key.bed_max_x = bed_extents.max.x();
key.bed_max_y = bed_extents.max.y();
if (!(m_imex_marker_key.s == key) || m_imex_marker_key.mode != mode ||
m_imex_marker_key.mode_names != mode_names ||
m_imex_marker_key.mode_active_tools != active_tools) {
m_imex_marker_key = ImexMarkerKey{ key, mode, mode_names, active_tools };
m_imex_marker_plan = resolve_imex_marker_plan(printer_cfg, mode, bed_extents);
// The carriage roster — and with it the marker count — can change with any
// of those inputs, so drop the markers and let the lazy init below rebuild
// them. Their colours are restamped every frame regardless.
m_sequential_view.m_imex_secondary_markers.clear();
}
const ImexMarkerPlan& plan = m_imex_marker_plan;
const int sec_count = (int)plan.carriages.size();
// Filament view colours the toolpaths by the filament printing them, so a
// carriage takes its own head's filament colour there and the palette
// everywhere else. get_imex_head_filament_color() answers UNPRINTABLE_COLOR
// for a head it cannot resolve, which is alpha 0.5 where a filament colour
// decodes opaque - that difference, not the black, is what distinguishes it,
// and a head with no filament keeps the palette rather than borrowing a
// colour that would read as a real assignment - the one point where a marker
// and the ghost of what it prints differ, the ghost having no palette to fall
// back to.
//
// The map and the pem are hoisted and each head resolved once, as
// PartPlate::update_imex_ghost_colors() does for the ghosts: both the marker
// and the toolhead box of a carriage want the same answer, and rebuilding it
// per draw parses the plate's map string and rebuilds the pem vector.
const bool color_by_filament = m_viewer.get_view_type() == libvgcode::EViewType::ColorPrint;
std::vector<ColorRGBA> carriage_colors(sec_count + 1);
if (sec_count > 0) {
const ConfigOptionInts pem = effective_physical_extruder_map(*preset_bundle);
const auto plate_map = curr_plate ? curr_plate->get_imex_head_filament_map()
: std::map<int, int>{};
std::map<int, ColorRGBA> head_colors;
auto color_for = [&](int phys_head, size_t palette_index) {
const ColorRGBA fallback = s_carriage_colors[palette_index % s_carriage_colors.size()];
if (!color_by_filament || curr_plate == nullptr || phys_head < 0)
return fallback;
auto hc = head_colors.find(phys_head);
if (hc == head_colors.end())
hc = head_colors.emplace(phys_head,
curr_plate->get_imex_head_filament_color(phys_head, pem, plate_map)).first;
if (hc->second == GLVolume::UNPRINTABLE_COLOR)
return fallback;
ColorRGBA c = hc->second;
c.a(fallback.a()); // as translucent as the palette makes them
return c;
};
carriage_colors[0] = color_for(plan.pri_head, 0);
for (int i = 0; i < sec_count; ++i)
carriage_colors[i + 1] = color_for(plan.carriages[i].phys_head, i + 1);
}
if (sec_count > 0) {
imex_active = true;
imex_box_wx = plan.box_wx;
imex_box_wy = plan.box_wy;
// Lazy init secondary nozzle markers
if ((int)m_sequential_view.m_imex_secondary_markers.size() != sec_count) {
m_sequential_view.m_imex_secondary_markers.resize(sec_count);
for (int i = 0; i < sec_count; ++i) {
m_sequential_view.m_imex_secondary_markers[i].init(m_marker_filename);
}
}
// Set every frame, not once at init: the view type and the filament palette
// both change without the marker list being rebuilt. The primary marker is
// one of the carriages here, so it is coloured alongside them - and restored
// below when no mode is active, since it is the same marker every non-IMEX
// preview draws.
m_sequential_view.marker.set_color(carriage_colors[0]);
for (int i = 0; i < sec_count; ++i)
m_sequential_view.m_imex_secondary_markers[i].set_color(carriage_colors[i + 1]);
const Vec3f prim_pos = libvgcode::convert(curr_vertex.position);
carriage_box_draws.push_back({ prim_pos, carriage_colors[0],
plan.pri_box_offset_x, plan.pri_box_offset_y });
for (int i = 0; i < sec_count; ++i) {
const ImexMarkerPlan::Carriage& carriage = plan.carriages[i];
// Tracking an axis translates the primary by the gap between the two
// zone centres; mirroring reflects it about the midpoint of those
// centres — the zones being equal-sized, that midpoint is the boundary
// between them, so the carriage still lands inside its own zone. Which
// axis does which, and both terms, come from resolve_imex_marker_plan().
const Vec3f sec_pos{ carriage.mirror_x ? carriage.x_term - prim_pos.x()
: prim_pos.x() + carriage.x_term,
carriage.mirror_y ? carriage.y_term - prim_pos.y()
: prim_pos.y() + carriage.y_term,
prim_pos.z() };
m_sequential_view.m_imex_secondary_markers[i].set_world_position(sec_pos);
m_sequential_view.m_imex_secondary_markers[i].set_z_offset(m_z_offset + 0.5f);
carriage_box_draws.push_back({
sec_pos, carriage_colors[i + 1],
carriage.box_offset_x, carriage.box_offset_y });
}
}
}
}
if (!imex_active) {
m_sequential_view.m_imex_secondary_markers.clear();
// The primary marker is the one every preview draws, so the carriage colour it may
// have been given has to come back off when no parallel mode is active.
m_sequential_view.marker.set_color(SequentialView::Marker::default_color());
}
}
m_sequential_view.render_marker(!m_no_render_path, canvas_width, sequential_view_height(canvas_height), m_viewer.get_view_type());
// IDEX/IQEX: render toolhead footprint boxes for each active carriage.
// Each box is imex_nozzle_clearance_x × imex_nozzle_clearance_y, sitting above the nozzle tip.
// Hidden when the user toggles off "Show IDEX/IQEX Toolhead Boxes" in the View menu — gives
// an unobstructed view of the toolpaths during sequential playback. Default-on; the absence
// of an app_config entry is also treated as on.
const bool show_toolhead_boxes = wxGetApp().app_config->get("show_imex_toolhead_boxes") != "false";
if (show_toolhead_boxes && !carriage_box_draws.empty() && imex_box_wx > 0.0f && imex_box_wy > 0.0f) {
// The mesh depends only on the two clearance dimensions, which a config edit can
// change without a G-code reload — so rebuild it when they move (or when it has not
// been built yet) rather than on every frame.
// Mesh origin: nozzle at x=0, centered in Y, Z starts at nozzle tip level.
// Per-carriage box_offset_x shifts the mesh left or right so the nozzle lands
// at the correct (collision-side) edge.
if (!m_imex_toolhead_box.is_initialized() ||
m_imex_box_mesh_wx != imex_box_wx || m_imex_box_mesh_wy != imex_box_wy) {
const float box_h = std::max(imex_box_wx, imex_box_wy);
indexed_triangle_set its = its_make_cube((double)imex_box_wx, (double)imex_box_wy, (double)box_h);
// No vertex pre-shifting — box_offset_x/y in the per-carriage transform
// positions the nozzle at the correct collision-side edge.
m_imex_toolhead_box.reset();
m_imex_toolhead_box.init_from(its);
m_imex_box_mesh_wx = imex_box_wx;
m_imex_box_mesh_wy = imex_box_wy;
}
GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light");
if (shader) {
glsafe(::glEnable(GL_BLEND));
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
shader->start_using();
shader->set_uniform("emission_factor", 0.0f);
const Camera& camera = wxGetApp().plater()->get_camera();
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
const Transform3d& view_matrix = camera.get_view_matrix();
for (const auto& draw : carriage_box_draws) {
const Transform3d model_matrix = Geometry::translation_transform(
(draw.pos + Vec3f(draw.box_offset_x, draw.box_offset_y, m_z_offset)).cast<double>());
shader->set_uniform("view_model_matrix", view_matrix * model_matrix);
const Matrix3d view_normal_matrix =
view_matrix.matrix().block(0, 0, 3, 3) *
model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
shader->set_uniform("view_normal_matrix", view_normal_matrix);
m_imex_toolhead_box.set_color(draw.color);
m_imex_toolhead_box.render();
}
shader->stop_using();
glsafe(::glDisable(GL_BLEND));
}
}
}
size_t GCodeViewer::shadow_casters_signature() const
@@ -3291,6 +3753,7 @@ void GCodeViewer::render_all_plates_stats(const std::vector<const GCodeProcessor
char buf[64];
::sprintf(buf, "%.2f", total_cost_all_plates);
imgui.text(buf);
}
ImGui::End();
ImGui::PopStyleColor(6);
+95 -1
View File
@@ -30,6 +30,7 @@
#include <set>
#include "slic3r/GUI/MeshUtils.hpp"
#include <string>
#include <tuple>
#include <unordered_set>
#include <utility>
#include <vector>
@@ -116,6 +117,9 @@ public:
bool is_visible() const { return m_visible; }
void set_visible(bool visible) { m_visible = visible; }
// What a marker looks like when no IDEX/IQEX carriage colour applies to it.
static ColorRGBA default_color() { return { 1.0f, 1.0f, 1.0f, 0.5f }; }
void set_color(const ColorRGBA& color) { m_model.set_color(color); }
void render(int canvas_width, int canvas_height, const libvgcode::EViewType& view_type);
void render_position_window(const libvgcode::Viewer* viewer, int canvas_width, int canvas_height, const libvgcode::EViewType& view_type);
@@ -164,6 +168,7 @@ public:
};
Marker marker;
std::vector<Marker> m_imex_secondary_markers; // one per active secondary carriage in IDEX/IQEX mode
GCodeWindow gcode_window;
float m_scale = 1.0;
bool m_show_marker = false;
@@ -228,6 +233,95 @@ private:
ConfigOptionMode m_user_mode;
bool m_fold = {false};
std::string m_marker_filename; // cached for lazy secondary marker init
// IDEX/IQEX: everything render_scene() needs to place the secondary carriage markers and the
// toolhead footprint boxes, resolved from the printer preset, the active mode and the
// plate bed. Resolving it walks the mode string through compute_imex_zone_layout()
// (several string parses plus a zone-grid rebuild), and none of its inputs change
// between frames, so it is resolved once per input change -- see ImexMarkerKey -- and
// replayed on every other frame.
struct ImexMarkerPlan
{
// One entry per secondary carriage that owns a zone, in marker order. Each axis
// either TRACKS the primary (pos + term) or MIRRORS it about the boundary the two
// zones share (term - pos); which of the two, and the term itself, depend only on
// the zone geometry, so both are resolved up front.
struct Carriage
{
int phys_head = -1; // physical head this carriage is, for its filament colour
bool mirror_x = false;
bool mirror_y = false;
float x_term = 0.0f;
float y_term = 0.0f;
float box_offset_x = 0.0f;
float box_offset_y = 0.0f;
};
std::vector<Carriage> carriages; // empty => no secondary carriages to draw
int pri_head = -1; // the primary's physical head, same purpose
float pri_box_offset_x = 0.0f;
float pri_box_offset_y = 0.0f;
float box_wx = 0.0f; // imex_nozzle_clearance_x / _y
float box_wy = 0.0f;
};
// Invalidation key for the plan above: every input the plan is derived from, and
// nothing that changes between frames. A stale plan would put the preview markers
// somewhere the plate's own zones and ghosts do not agree with, which is exactly the
// drift the shared layout call exists to prevent -- so this deliberately mirrors
// PartPlate::build_imex_cache_key(). The two keys are not field-for-field identical, and
// the differences are deliberate rather than incidental:
// - bed extents and plate_index are here and not there. Zone centres scale with the
// extents, and one preview serves every plate, so the preview must key what the plate
// gets for free -- it re-bakes on set_shape() and is keyed by being that plate.
// - mode_names / mode_active_tools hold the printer's WHOLE mode table; the plate resolves
// one active mode and keys that roster plus its primary head. Same information reached
// two ways, so a change to the active mode moves both keys.
// - imex_carriage_margin is there and not here: it only sizes the plate's advisory bands,
// which the preview never draws.
// imex_tool_layout is in both, which the plate's key gained for the reason this one has it:
// the T0-corner flip moves every zone rectangle while nothing else keyed changes.
struct ImexMarkerKey
{
// Scalar half. Built fresh on the stack each frame -- it allocates nothing -- and
// compared as a tuple.
struct Scalars
{
int plate_index = -1;
int gantry_count = 0;
int tools_per_gantry = 0;
int tool_layout = -1;
double clearance_x = 0.0;
double clearance_y = 0.0;
bool firmware_managed = false;
double bed_min_x = 0.0, bed_min_y = 0.0, bed_max_x = 0.0, bed_max_y = 0.0;
auto tied() const {
return std::tie(plate_index, gantry_count, tools_per_gantry, tool_layout,
clearance_x, clearance_y, firmware_managed,
bed_min_x, bed_min_y, bed_max_x, bed_max_y);
}
bool operator==(const Scalars& rhs) const { return tied() == rhs.tied(); }
};
Scalars s;
// Resolved active mode (the plate's mode beats the process preset), plus the printer
// preset's whole mode table. The name alone is not identity: two presets can carry
// the same mode name over different tool rosters, and editing a roster in place
// moves neither the name nor any scalar above. Compared by value, never copied
// unless something actually changed.
std::string mode;
std::vector<std::string> mode_names;
std::vector<std::string> mode_active_tools;
};
ImexMarkerKey m_imex_marker_key;
ImexMarkerPlan m_imex_marker_plan;
static ImexMarkerPlan resolve_imex_marker_plan(const DynamicPrintConfig& printer_cfg,
const std::string& mode,
const BoundingBoxf& bed_extents);
GLModel m_imex_toolhead_box; // shared box mesh for all carriage footprint overlays
float m_imex_box_mesh_wx{ 0.0f }; // clearance dimensions m_imex_toolhead_box was built for
float m_imex_box_mesh_wy{ 0.0f };
size_t m_extruders_count;
std::vector<float> m_filament_diameters;
@@ -306,7 +400,7 @@ public:
//BBS: add all plates filament statistics
void render_all_plates_stats(const std::vector<const GCodeProcessorResult*>& gcode_result_list, bool show = true) const;
//BBS: GUI refactor: add canvas width and height
// Shells, toolpaths and the sequential marker, drawn in 3D.
// Shells, toolpaths, the sequential markers and the IDEX/IQEX toolhead boxes, drawn in 3D.
void render_scene(int canvas_width, int canvas_height);
// Legend, sliders, the marker's position window and the G-code window, all ImGui.
void render_overlay(int canvas_width, int canvas_height, int right_margin);
+249 -3
View File
@@ -1,5 +1,6 @@
#include "libslic3r/libslic3r.h"
#include "libslic3r/Point.hpp"
#include <limits>
#include <string>
#include <sstream>
#include <iomanip>
@@ -2658,11 +2659,19 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init)
#endif
}
// Suppress the regular object-name/toolbar tooltip while hovering an IMEX
// ghost; the swatch overlay below stands in for it.
if (m_hover_ghost_head >= 0)
tooltip.clear();
set_tooltip(tooltip);
if (m_tooltip_enabled)
m_tooltip.render(m_mouse.position, *this);
// IMEX ghost hover overlay: layered on top of the normal tooltip pass.
_render_imex_ghost_tooltip();
wxGetApp().plater()->get_mouse3d_controller().render_settings_dialog(*this);
if (m_canvas_type != ECanvasType::CanvasAssembleView) {
@@ -5066,6 +5075,8 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
TransformationType trafo_type;
trafo_type.set_relative();
m_selection.translate(cur_pos - m_mouse.drag.start_position_3D, trafo_type);
// Ghost transforms refresh from _render_imex_ghosts via the live GLVolume
// lookup, so no explicit update call is needed here.
if (current_printer_technology() == ptFFF) {
if (fff_print()->config().print_sequence == PrintSequence::ByObject)
update_sequential_clearance();
@@ -5239,6 +5250,13 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
m_rectangle_selection.stop_dragging();
}
else if (evt.LeftUp() && !m_mouse.dragging && m_hover_ghost_head >= 0) {
// IMEX ghost click: dispatch to Plater (filament picker). The else-if chain
// already prevents deselect/plate-select from firing on the same event; we
// fall through to mouse_up_cleanup() below so mouse capture and drag state
// get reset like every other branch in this chain.
wxGetApp().plater()->on_imex_ghost_click(m_hover_ghost_head);
}
else if (evt.LeftUp() && !m_mouse.ignore_left_up && !m_mouse.dragging && m_hover_volume_idxs.empty() && m_hover_plate_idxs.empty() && !is_layers_editing_enabled()) {
// deselect and propagate event through callback
if (!evt.ShiftDown() && (!any_gizmo_active || !evt.CmdDown()) && m_picking_enabled)
@@ -5283,9 +5301,10 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
}
//BBS change plate selection
bool plate_icon_popup_shown = false;
if (!m_hover_plate_idxs.empty() && (m_canvas_type == CanvasView3D) && !m_mouse.dragging) {
int hover_idx = m_hover_plate_idxs.front();
wxGetApp().plater()->select_plate_by_hover_id(hover_idx, true);
plate_icon_popup_shown = (wxGetApp().plater()->select_plate_by_hover_id(hover_idx, true) == 1);
if (m_hover_volume_idxs.empty())
deselect_all();
render();
@@ -5305,10 +5324,10 @@ void GLCanvas3D::on_mouse(wxMouseEvent& evt)
if (!m_mouse.ignore_right_up && m_gizmos.get_current_type() == GLGizmosManager::EType::Undefined) {
//BBS post right click event
if (!m_hover_plate_idxs.empty()) {
if (!m_hover_plate_idxs.empty() && !plate_icon_popup_shown) {
post_event(RBtnPlateEvent(EVT_GLCANVAS_PLATE_RIGHT_CLICK, { logical_pos, m_hover_plate_idxs.front() }));
}
else {
else if (!plate_icon_popup_shown) {
// do not post the event if the user is panning the scene
// or if right click was done over the wipe tower
bool post_right_click_event = m_hover_volume_idxs.empty() || !m_volumes.volumes[get_first_hover_volume_idx()]->is_wipe_tower;
@@ -5618,6 +5637,10 @@ void GLCanvas3D::do_move(const std::string& snapshot_type)
reset_sequential_print_clearance();
// IMEX: selection commit may have moved/added/removed objects — ghost cache key
// doesn't encode per-instance transforms, so force a full rebuild on next render.
wxGetApp().plater()->get_partplate_list().invalidate_all_imex_ghosts();
m_dirty = true;
}
@@ -5728,6 +5751,9 @@ void GLCanvas3D::do_rotate(const std::string& snapshot_type)
post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_ROTATED));
}
// IMEX: rotate changes per-instance transforms without touching the ghost cache key.
wxGetApp().plater()->get_partplate_list().invalidate_all_imex_ghosts();
m_dirty = true;
}
@@ -5828,6 +5854,9 @@ void GLCanvas3D::do_scale(const std::string& snapshot_type)
post_event(SimpleEvent(EVT_GLCANVAS_INSTANCE_SCALED));
}
// IMEX: scale changes per-instance transforms without touching the ghost cache key.
wxGetApp().plater()->get_partplate_list().invalidate_all_imex_ghosts();
m_dirty = true;
}
@@ -5939,6 +5968,9 @@ void GLCanvas3D::do_mirror(const std::string& snapshot_type)
post_event(SimpleEvent(EVT_GLCANVAS_SCHEDULE_BACKGROUND_PROCESS));
// IMEX: mirror changes per-instance transforms without touching the ghost cache key.
wxGetApp().plater()->get_partplate_list().invalidate_all_imex_ghosts();
m_dirty = true;
}
@@ -7961,6 +7993,11 @@ void GLCanvas3D::_picking_pass()
_update_volumes_hover_state();
// IMEX ghost picking runs unconditionally after the normal pass: ghosts visually
// occlude the main volumes, so their tooltip/click handling must fire even when a
// regular volume hit also occurred underneath.
_picking_pass_imex_ghosts();
#if ENABLE_RAYCAST_PICKING_DEBUG
ImGuiWrapper& imgui = *wxGetApp().imgui();
imgui.begin(std::string("Hit result"), ImGuiWindowFlags_AlwaysAutoResize);
@@ -8061,6 +8098,180 @@ void GLCanvas3D::_picking_pass()
#endif // ENABLE_RAYCAST_PICKING_DEBUG
}
void GLCanvas3D::_picking_pass_imex_ghosts()
{
m_hover_ghost_head = -1;
m_hover_ghost_plate = -1;
if (!m_picking_enabled || m_mouse.dragging || m_mouse.position == Vec2d(DBL_MAX, DBL_MAX) || m_gizmos.is_dragging())
return;
// Build a world-space ray from the mouse: mouse_ray(pos) returns the near/far
// world-space points, so direction is b - a.
const Linef3 ray = mouse_ray(Point(static_cast<coord_t>(m_mouse.position.x()),
static_cast<coord_t>(m_mouse.position.y())));
const Vec3d ray_origin = ray.a;
const Vec3d ray_dir = ray.b - ray.a;
if (ray_dir.squaredNorm() == 0.0)
return;
// Standard slab ray-vs-AABB test. Accept a hit when the nearest plane entry is
// closer than the farthest plane exit and the exit is in front of the origin.
auto ray_hits_bbox = [](const Vec3d& o, const Vec3d& d, const BoundingBoxf3& bb) -> bool {
double tmin = -std::numeric_limits<double>::infinity();
double tmax = std::numeric_limits<double>::infinity();
for (int i = 0; i < 3; ++i) {
if (std::abs(d[i]) < 1e-12) {
if (o[i] < bb.min[i] || o[i] > bb.max[i])
return false;
}
else {
double t1 = (bb.min[i] - o[i]) / d[i];
double t2 = (bb.max[i] - o[i]) / d[i];
if (t1 > t2) std::swap(t1, t2);
tmin = std::max(tmin, t1);
tmax = std::min(tmax, t2);
if (tmin > tmax) return false;
}
}
return tmax >= 0.0;
};
// Match _render_imex_ghosts: only hit-test ghosts on the active plate. Picking
// through ghosts on background plates would hand the user a stale plate index
// for the head-filament popover and let them edit a plate they aren't looking at.
PartPlateList& ppl = wxGetApp().plater()->get_partplate_list();
PartPlate* active_plate = ppl.get_curr_plate();
if (active_plate) {
const auto& ghosts = active_plate->get_imex_ghost_volumes();
for (const auto& g : ghosts) {
if (!g || !g->is_active || !g->picking) continue;
const BoundingBoxf3 bbox = g->transformed_bounding_box();
if (ray_hits_bbox(ray_origin, ray_dir, bbox)) {
m_hover_ghost_head = PartPlate::imex_ghost_head_from_composite_id(g->composite_id.object_id);
m_hover_ghost_plate = ppl.get_curr_plate_index();
return; // first hit wins
}
}
}
}
void GLCanvas3D::_render_imex_ghosts(bool xray_pass)
{
// Draws through whichever shader the caller bound: the shaded pass leaves gouraud current,
// the X-Ray pass its own program. Both are fed per volume below; the uniforms a pass sets
// once for itself are the caller's (see _render_imex_ghosts_xray for the X-Ray pass's).
// GLVolume::render() only binds its mesh, so we must set the per-volume
// matrices AND uniform_color that GLVolumeCollection::render would normally
// set; otherwise ghosts pick up whatever the last main volume left behind.
GLShaderProgram* shader = wxGetApp().get_current_shader();
if (shader == nullptr)
return;
// Primary-volume live transform lookup: during a gizmo drag the GLVolume's
// instance_transformation is the source of truth (the ModelInstance matrix
// only catches up on mouse-up). Returning it from here makes ghosts track
// the drag every frame instead of snapping when the user releases.
auto primary_live_xf = [this](int obj_idx, int inst_idx) -> std::optional<Transform3d> {
for (const GLVolume* v : m_volumes.volumes) {
if (!v) continue;
if (v->composite_id.object_id == obj_idx &&
v->composite_id.instance_id == inst_idx)
return v->get_instance_transformation().get_matrix();
}
return std::nullopt;
};
const Camera& camera = wxGetApp().plater()->get_camera();
const Transform3d& view_matrix = camera.get_view_matrix();
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
// Ghosts live outside the build volume check; disable the partly-inside path.
shader->set_uniform("print_volume.type", -1);
shader->set_uniform("slope.actived", false);
// Only render ghosts for the active plate. Iterating every plate here causes
// ghosts from background plates to bleed through into the active scene
// (e.g. when entering paint mode), since the GL state is shared across the
// whole canvas. Per-plate ghost volumes still live on each PartPlate so they
// round-trip through 3MF saves; we just don't draw them when their plate
// isn't the one the user is currently looking at.
PartPlateList& ppl = wxGetApp().plater()->get_partplate_list();
PartPlate* active_plate = ppl.get_curr_plate();
if (active_plate) {
// Refresh per-frame so ghost positions reflect the primary's live drag state,
// and restamp ghost RGB from the live filament palette so color-only changes
// (palette edits, late-loading project colors) never leave stale ghosts.
active_plate->update_imex_ghost_transforms(primary_live_xf);
active_plate->update_imex_ghost_colors();
const auto& ghosts = active_plate->get_imex_ghost_volumes();
for (const auto& g : ghosts) {
if (!g || !g->is_active) continue;
const Transform3d model_matrix = g->world_matrix();
shader->set_uniform("volume_world_matrix", model_matrix);
shader->set_uniform("slope.volume_world_normal_matrix",
static_cast<Matrix3f>(model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose().cast<float>()));
shader->set_uniform("view_model_matrix", view_matrix * model_matrix);
const Matrix3d view_normal_matrix = view_matrix.matrix().block(0, 0, 3, 3)
* model_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
shader->set_uniform("view_normal_matrix", view_normal_matrix);
g->set_render_color();
// GLModel::render() pushes its own data.color into uniform_color,
// so we must stamp the ghost's color onto the model before render
// or it draws black. Pattern matches 3DScene.cpp:1099.
// X-Ray derives its own coverage from view angle and multiplies the colour's alpha
// into it, so a ghost carrying its translucency as well composites about three times
// fainter than the body it mirrors. Hand that pass an opaque colour and let its
// density be the only source of translucency.
ColorRGBA ghost_color = g->render_color;
if (xray_pass)
ghost_color.a(1.0f);
g->model.set_color(ghost_color);
g->render();
}
}
}
void GLCanvas3D::_render_imex_ghost_tooltip()
{
if (m_hover_ghost_head < 0)
return;
// Hover state can outlive the ghosts that produced it: _picking_pass_imex_ghosts
// only resets m_hover_ghost_head when it runs, and _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 but the stale head index survives — producing an orphan tooltip.
// Validate against live ghosts and self-heal before rendering.
const PartPlate* plate = wxGetApp().plater()->get_partplate_list().get_plate(m_hover_ghost_plate);
if (!plate || plate->get_imex_ghost_volumes().empty()) {
m_hover_ghost_head = -1;
m_hover_ghost_plate = -1;
return;
}
const auto t = wxGetApp().plater()->format_imex_ghost_tooltip(m_hover_ghost_head);
ImGuiWrapper& imgui = *wxGetApp().imgui();
const Vec2i32 mouse = m_mouse.position.cast<int>();
const float cursor_offset = imgui.scaled(1.6f); // ~16px @ 100%, scales with DPI
imgui.set_next_window_pos(float(mouse.x()) + cursor_offset, float(mouse.y()) + cursor_offset,
ImGuiCond_Always, 0.0f, 0.0f);
imgui.begin(std::string("##imex_ghost_tooltip"),
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoMouseInputs);
// Color swatch + label on the same row. imgui.scaled() keeps it readable on high-DPI.
const ImVec4 col(t.swatch.r(), t.swatch.g(), t.swatch.b(), t.swatch.a());
const float swatch_size = imgui.scaled(1.4f);
ImGui::ColorButton("##swatch", col,
ImGuiColorEditFlags_NoBorder | ImGuiColorEditFlags_NoTooltip,
ImVec2(swatch_size, swatch_size));
ImGui::SameLine();
imgui.text(t.label);
imgui.end();
}
void GLCanvas3D::_rectangular_selection_picking_pass()
{
m_gizmos.set_hover_id(-1);
@@ -9210,6 +9421,8 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with
if (_is_xray_view_active()) {
if (type == GLVolumeCollection::ERenderType::Opaque)
_render_xray_volumes();
else
_render_imex_ghosts_xray();
m_camera_clipping_plane = ClippingPlane::ClipsNothing();
return;
}
@@ -9337,6 +9550,9 @@ void GLCanvas3D::_render_objects(GLVolumeCollection::ERenderType type, bool with
}
},
partly_inside_enable, printable_heights);
// IMEX ghosts are transparent and share the same shader/camera state;
// render them right after the main transparent pass while the shader is still bound.
_render_imex_ghosts();
break;
}
}
@@ -9439,6 +9655,36 @@ void GLCanvas3D::_render_xray_volumes()
glsafe(::glDepthMask(GL_TRUE));
}
// The IDEX/IQEX ghosts are not in m_volumes, so the X-Ray pass that replaces both shaded passes
// does not reach them on its own. They go through the X-Ray shader here rather than their own,
// which is what makes a ghost read as one more see-through body.
void GLCanvas3D::_render_imex_ghosts_xray()
{
GLShaderProgram* shader = wxGetApp().get_shader("xray");
if (shader == nullptr)
return;
// The blend and depth state _render_xray_volumes() uses, plus its two-sided drawing: the
// shader shades back faces too, so a hollow ghost shows its far wall like a real body does.
glsafe(::glDepthMask(GL_FALSE));
glsafe(::glEnable(GL_BLEND));
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
glsafe(::glDisable(GL_CULL_FACE));
shader->start_using();
// _render_imex_ghosts() sets the per-volume matrices and the color, but the two uniforms
// the volume collection would have set for the whole pass are this pass's to supply, and
// the vertex shader discards everything outside z_range - an unset one hides every ghost.
shader->set_uniform("z_range", m_volumes.get_z_range());
shader->set_uniform("clipping_plane", m_volumes.get_clipping_plane());
_render_imex_ghosts(/*xray_pass=*/true);
shader->stop_using();
glsafe(::glEnable(GL_CULL_FACE));
glsafe(::glDisable(GL_BLEND));
glsafe(::glDepthMask(GL_TRUE));
}
void GLCanvas3D::_render_wireframe_overlay()
{
if (!wxGetApp().plater()->is_show_wireframe())
+13
View File
@@ -776,6 +776,9 @@ private:
//BBS:add plate related logic
mutable std::vector<int> m_hover_volume_idxs;
std::vector<int> m_hover_plate_idxs;
// IMEX ghost hover state (plate-owned transparent ghost volumes).
int m_hover_ghost_head { -1 }; // physical head index, -1 when not hovering a ghost
int m_hover_ghost_plate { -1 }; // plate index for the hovered ghost, -1 when none
//BBS if explosion_ratio is changed, need to update volume bounding box
mutable float m_explosion_ratio = 1.0;
mutable Vec3d m_rotation_center{ 0.0, 0.0, 0.0};
@@ -1313,6 +1316,8 @@ public:
int get_move_volume_id() const { return m_mouse.drag.move_volume_idx; }
int get_first_hover_volume_idx() const { return m_hover_volume_idxs.empty() ? -1 : m_hover_volume_idxs.front(); }
int get_hover_ghost_head() const { return m_hover_ghost_head; }
int get_hover_ghost_plate() const { return m_hover_ghost_plate; }
void set_selected_extruder(int extruder) { m_selected_extruder = extruder;}
class WipeTowerInfo {
@@ -1480,6 +1485,8 @@ private:
void _picking_pass();
void _rectangular_selection_picking_pass();
// IMEX ghost picking (ray vs. ghost bbox). Runs at the end of _picking_pass.
void _picking_pass_imex_ghosts();
bool _is_fxaa_enabled() const;
bool _is_realistic_view_enabled() const;
bool _is_ssao_enabled() const;
@@ -1521,6 +1528,12 @@ private:
void _render_cad_grid(const Transform3d& view_matrix, const Transform3d& projection_matrix);
//BBS: add outline drawing logic
void _render_objects(GLVolumeCollection::ERenderType type, bool with_outline = true);
// IMEX ghost volumes owned by PartPlate, drawn through the shader the caller bound:
// the shaded pass's transparent half, or the X-Ray pass below.
void _render_imex_ghosts(bool xray_pass = false);
void _render_imex_ghosts_xray();
// IMEX ghost hover tooltip: filament swatch + label drawn as an ImGui overlay.
void _render_imex_ghost_tooltip();
void _render_section_view_caps();
void _render_wireframe_overlay();
bool _is_xray_view_active() const;
@@ -0,0 +1,146 @@
#include "slic3r/GUI/IMEXFilamentPickerPopover.hpp"
#include <libslic3r/Config.hpp>
#include <wx/popupwin.h>
#include <utility>
#include <wx/sizer.h>
#include <vector>
#include <cstddef>
#include <wx/gdicmn.h>
#include <algorithm>
#include <wx/event.h>
#include <wx/stattext.h>
#include <wx/dcmemory.h>
#include <wx/image.h>
#include <wx/window.h>
#include <wx/string.h>
#include "libslic3r/IMEXHelpers.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "slic3r/GUI/BitmapComboBox.hpp"
#include "slic3r/GUI/PartPlate.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/wxExtensions.hpp"
namespace Slic3r { namespace GUI {
IMEXFilamentPickerPopover::IMEXFilamentPickerPopover(
wxWindow* parent, PartPlate* plate,
const ConfigOptionInts& pem, int physical_head,
CommitCallback on_commit)
: wxPopupTransientWindow(parent, wxBORDER_SIMPLE)
, m_plate(plate)
, m_pem(pem)
, m_physical_head(physical_head)
, m_on_commit(std::move(on_commit))
{
m_root_sizer = new wxBoxSizer(wxHORIZONTAL);
SetSizer(m_root_sizer);
build_row();
m_root_sizer->SetSizeHints(this);
SetClientSize(m_root_sizer->ComputeFittingClientSize(this));
Layout();
}
void IMEXFilamentPickerPopover::popup_at_cursor()
{
Position(wxGetMousePosition(), wxSize(0, 0));
Popup();
}
void IMEXFilamentPickerPopover::OnDismiss()
{
wxPopupTransientWindow::OnDismiss();
// No CallAfter needed: wxPopupTransientWindowBase::Destroy() already defers by appending to
// wxPendingDelete, and guards a second call with a wxCHECK rather than double-freeing.
Destroy();
}
void IMEXFilamentPickerPopover::build_row()
{
m_root_sizer->Clear(true);
m_root_sizer->AddSpacer(10);
auto* label = new wxStaticText(this, wxID_ANY,
wxString::Format("T%d", m_physical_head));
m_root_sizer->Add(label, 0, wxALIGN_CENTER_VERTICAL | wxTOP | wxBOTTOM, 6);
m_root_sizer->AddSpacer(10);
std::vector<int> lane_logicals;
for (size_t L = 0; L < m_pem.values.size(); ++L)
if (m_pem.values[L] == m_physical_head)
lane_logicals.push_back((int)L);
std::vector<wxBitmap*> bmps = get_extruder_color_icons(false /*thin_icon*/);
auto* choice = new BitmapComboBox(this, wxID_ANY, wxEmptyString,
wxDefaultPosition, wxSize(140, -1),
0, nullptr, wxCB_READONLY);
const int left_pad_px = 8;
auto pad_left = [&](const wxBitmap& src) -> wxBitmap {
if (!src.IsOk()) return src;
wxImage im(src.GetWidth() + left_pad_px, src.GetHeight());
im.InitAlpha();
unsigned char* a = im.GetAlpha();
std::fill(a, a + im.GetWidth() * im.GetHeight(), (unsigned char)0);
wxBitmap out(im);
wxMemoryDC dc(out);
dc.DrawBitmap(src, left_pad_px, 0, true);
dc.SelectObject(wxNullBitmap);
return out;
};
for (int L : lane_logicals) {
const wxString label = wxString::Format("filament %d", L + 1);
if (L >= 0 && L < (int)bmps.size() && bmps[L] && bmps[L]->IsOk())
choice->Append(label, pad_left(*bmps[L]));
else
choice->Append(label, wxNullBitmap);
}
const auto map = m_plate->get_imex_head_filament_map();
auto it = map.find(m_physical_head);
int selected_slot_1based = (it != map.end())
? it->second
: (lane_logicals.empty() ? -1 : lane_logicals.front() + 1);
for (int i = 0; i < (int)lane_logicals.size(); ++i) {
if (lane_logicals[i] + 1 == selected_slot_1based) {
choice->SetSelection(i);
break;
}
}
choice->Bind(wxEVT_COMBOBOX, [this, lane_logicals, choice](wxCommandEvent&) {
int sel = choice->GetSelection();
if (sel < 0 || sel >= (int)lane_logicals.size()) return;
on_filament_selected(lane_logicals[sel] + 1);
// DismissAndNotify(), not Dismiss(): Dismiss() is only PopHandlers()+Hide() and never
// reaches OnDismiss(), so destroying from OnDismiss() would miss this path entirely --
// which is the one users take on every successful pick.
DismissAndNotify();
});
m_root_sizer->Add(choice, 0, wxALIGN_CENTER_VERTICAL | wxALL, 6);
m_root_sizer->AddSpacer(4);
}
void IMEXFilamentPickerPopover::on_filament_selected(int slot_1_based)
{
auto map = m_plate->get_imex_head_filament_map();
// Erase when the user reselects the pem default for this head, so the 3mf
// doesn't carry no-op overrides (same invariant as PR3b).
const int default_logical = first_filament_for_physical_head(m_pem, m_physical_head);
if (default_logical >= 0 && slot_1_based == default_logical + 1)
map.erase(m_physical_head);
else
map[m_physical_head] = slot_1_based;
m_plate->set_imex_head_filament_map(map);
if (m_on_commit) m_on_commit();
}
}} // namespace Slic3r::GUI
@@ -0,0 +1,50 @@
#ifndef slic3r_IMEXFilamentPickerPopover_hpp_
#define slic3r_IMEXFilamentPickerPopover_hpp_
#include <libslic3r/Config.hpp>
#include <wx/popupwin.h>
#include <wx/panel.h>
#include <wx/sizer.h>
#include <functional>
#include "libslic3r/PrintConfig.hpp"
namespace Slic3r {
namespace GUI {
class PartPlate;
class IMEXFilamentPickerPopover : public wxPopupTransientWindow
{
public:
using CommitCallback = std::function<void()>;
IMEXFilamentPickerPopover(wxWindow* parent, PartPlate* plate,
const ConfigOptionInts& pem, int physical_head,
CommitCallback on_commit);
void popup_at_cursor();
private:
// Destroys the popover: the ghost-click handler creates one per click and keeps no
// reference, so every click would otherwise leak a live top-level window parented to the
// GL canvas. Only DismissAndNotify() reaches this -- plain Dismiss() just hides.
void OnDismiss() override;
void build_row();
void on_filament_selected(int slot_1_based);
PartPlate* m_plate;
// Owned by value: callers may pass a stack-local derived pem
// (e.g. effective_physical_extruder_map result) that won't outlive
// the popover's async lifetime.
ConfigOptionInts m_pem;
int m_physical_head;
CommitCallback m_on_commit;
wxSizer* m_root_sizer = nullptr;
};
}} // namespace Slic3r::GUI
#endif
+897
View File
@@ -0,0 +1,897 @@
#include "slic3r/GUI/IMEXModesCtrl.hpp"
#include <wx/anybutton.h>
#include <libslic3r/IMEXHelpers.hpp>
#include <cstddef>
#include <vector>
#include <tuple>
#include <libslic3r/Config.hpp>
#include <map>
#include <string>
#include <optional>
#include <functional>
#include <wx/app.h>
#include <wx/event.h>
#include <wx/colour.h>
#include <wx/font.h>
#include <wx/panel.h>
#include <wx/sizer.h>
#include <wx/gdicmn.h>
#include <wx/stattext.h>
#include <algorithm>
#include <boost/algorithm/string/predicate.hpp>
#include <utility>
#include <wx/string.h>
#include "slic3r/GUI/EditGCodeDialog.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/Widgets/Button.hpp"
#include "slic3r/GUI/Widgets/ComboBox.hpp"
#include "slic3r/GUI/Widgets/StateColor.hpp"
#include "slic3r/GUI/wxExtensions.hpp"
namespace Slic3r {
namespace GUI {
// Icon size for every button in this control -- the help button beside the legend and the
// edit / remove buttons on each row. One constant so the three cannot drift apart.
static constexpr int kImexIconPx = 20;
// Mode column width. The name field carries a drop-down arrow, which eats interior space, so
// this is wider than a bare text field needed. The header spacer below derives from it.
static constexpr int kNameColPx = 176;
IMEXModesCtrl::IMEXModesCtrl(wxWindow* parent, int n_cols, int n_rows, int layout)
: wxPanel(parent, wxID_ANY), m_n_cols(std::max(1, n_cols)), m_n_rows(std::max(1, n_rows)), m_layout(layout)
{
// Set explicitly: GTK otherwise gives child wxPanels a slightly lighter "widget bg",
// which shows as a light box around the chromeless ScalableButtons. The page's own
// color, as OG_CustomCtrl takes it, because the dark-mode walk only remaps palette
// colors: the app's window default is not one, so it never matched the page and
// froze in whichever theme the editor was last built in. Sub-panels inherit this color.
SetBackgroundColour(parent->GetBackgroundColour());
m_outer = new wxBoxSizer(wxVERTICAL);
m_rows_sizer = new wxBoxSizer(wxVERTICAL);
// Info panel (instructions + legend) and the column-header row. Both depend on the grid
// shape, so their CONTENTS are (re)built by rebuild_info_and_header(), which set_grid_size()
// calls again whenever the grid changes. The panels themselves are stable so m_outer's
// ordering never has to be rearranged.
m_info_panel = new wxPanel(this, wxID_ANY);
m_info_panel->SetBackgroundColour(GetBackgroundColour());
m_hdr_panel = new wxPanel(this, wxID_ANY);
m_hdr_panel->SetBackgroundColour(GetBackgroundColour());
rebuild_info_and_header();
// Add Mode is built by rebuild_info_and_header() into the info panel, above the rows:
// at the bottom it moved down the page every time a mode was added, so its position
// depended on how many modes you already had.
m_outer->Add(m_info_panel, 0, wxEXPAND | wxBOTTOM, FromDIP(4));
m_outer->Add(m_hdr_panel, 0, wxEXPAND | wxBOTTOM, FromDIP(2));
m_outer->Add(m_rows_sizer, 0, wxEXPAND);
m_outer->AddSpacer(FromDIP(4));
// SetSizer(), not SetSizerAndFit(): no row exists yet, so there is nothing to fit to --
// rows arrive from add_row() / load_from_config() / set_grid_size(). Deliberately NOT
// followed by m_outer->SetSizeHints(this) once they do: this panel owns a sizer, so
// wxWindow::GetBestSize() bypasses the best-size cache and re-runs m_outer->CalcMin()
// on every parent layout, and that is what the enclosing sizer already reserves space
// for via GetEffectiveMinSize(). An explicitly set min size takes PRIORITY over the best
// size in that call, so a size hint would pin the reserved height to the row count that
// happened to be on screen when it ran -- adding a mode after that would clip the bottom
// row, which is the very failure the hint is meant to avoid.
SetSizer(m_outer);
}
// A wxTextCtrl cannot paint its own border on GTK -- Orca sidesteps that in ::TextInput by
// drawing one on a StaticBox, but that has no multiline form and nothing in the tree uses it
// that way. A one-pixel panel behind the control gives the same visible edge for both the
// single-line name and the multiline G-code box, in the color the settings fields above use:
// the inputs here were landing on GTK's near-black default, invisible against the panel.
static wxPanel* framed_input(wxWindow* parent)
{
wxColour clr(0xDB, 0xDB, 0xDB);
if (wxGetApp().dark_mode())
clr = StateColor::darkModeColorFor(clr);
auto* frame = new wxPanel(parent, wxID_ANY);
frame->SetBackgroundColour(clr);
frame->SetSizer(new wxBoxSizer(wxVERTICAL));
return frame;
}
// The settings page's label color, set before the walk as Tab's labels are, so the theme
// remaps it with the page. Left on the system text color, a label follows the OS theme on MSW
// rather than Orca's, and on GTK a dark walk pins a color the light map cannot undo.
static wxStaticText* page_label(wxStaticText* lbl)
{
lbl->SetForegroundColour(wxColour("#363636"));
wxGetApp().UpdateDarkUI(lbl);
return lbl;
}
void IMEXModesCtrl::rebuild_info_and_header() {
// --- Info panel: help button + color legend ---
m_info_panel->DestroyChildren();
auto* info_sizer = new wxBoxSizer(wxVERTICAL);
// The overview rides on the "?" button at the head of the legend rather than sitting in
// the panel as body text: it is read once and then only gets in the way, while the legend
// beside it is the part worth keeping on screen. Per-role detail is on the swatches.
const wxString instructions =
_L("Each mode names the tool heads that take part and the role each one plays. "
"Click a tool button to cycle its role — hover a color below for what each role does.");
// Color legend — swatches sized to the body text height so they read as
// matched pairs with their labels regardless of system DPI / font scale,
// matching the on-hover ghost tooltip swatch's visual weight.
// One entry per role this printer offers, in kImexRoleTable order — the same order the
// tiles cycle in. A role added to that table appears here with no edit of its own.
auto* leg_sizer = new wxBoxSizer(wxHORIZONTAL);
const int swatch_side = m_info_panel->GetCharHeight();
// Action row: Add Mode, then the "?" carrying the overview. Width-matched to the mode
// column so the button sits over the field it creates.
auto* act_sizer = new wxBoxSizer(wxHORIZONTAL);
auto* add_btn = new Button(m_info_panel, _L("Add Mode"));
add_btn->SetStyle(ButtonStyle::Confirm, ButtonType::Window);
// Width-matched to the mode column. The height has to be a real number: Button takes its
// min size literally, so a -1 there collapses it to a sliver rather than meaning "auto".
add_btn->SetMinSize(wxSize(FromDIP(130), add_btn->GetBestSize().GetHeight()));
add_btn->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { add_row(); notify(); });
act_sizer->Add(add_btn, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6));
auto* help_btn = new ScalableButton(m_info_panel, wxID_ANY, "icon_qusetion", wxEmptyString,
wxDefaultSize, wxDefaultPosition,
wxBU_EXACTFIT | wxNO_BORDER, /*use_default_disabled_bitmap=*/true, kImexIconPx);
help_btn->SetToolTip(instructions);
act_sizer->Add(help_btn, 0, wxALIGN_CENTER_VERTICAL);
info_sizer->Add(act_sizer, 0, wxBOTTOM, FromDIP(6));
for (const ImexRoleDesc& d : kImexRoleTable) {
if (!role_offered(d.role)) continue;
const RoleStyle style = role_style(d.role);
auto* swatch = new wxPanel(m_info_panel, wxID_ANY, wxDefaultPosition, wxSize(swatch_side, swatch_side));
swatch->SetBackgroundColour(style.bg);
auto* leg_label = page_label(new wxStaticText(m_info_panel, wxID_ANY, style.label));
const wxString hint = role_hint(d.role);
swatch->SetToolTip(hint);
leg_label->SetToolTip(hint);
leg_sizer->Add(swatch, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4));
leg_sizer->Add(leg_label, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(16));
}
info_sizer->Add(leg_sizer, 0, wxBOTTOM, FromDIP(6));
m_info_panel->SetSizerAndFit(info_sizer);
// --- Column header row ---
// Spacers sized to align with the mode-row fields below. Name field: FromDIP(130) + FromDIP(6)
// gap; tool grid: n_cols*(FromDIP(24)+FromDIP(2))-FromDIP(2) + FromDIP(6) gap. grid_px tracks
// the live column count, so the G-code header stays aligned after a grid change.
m_hdr_panel->DestroyChildren();
auto* hdr_sizer = new wxBoxSizer(wxHORIZONTAL);
auto* hdr_name = page_label(new wxStaticText(m_hdr_panel, wxID_ANY, _L("Mode")));
auto* hdr_tools = page_label(new wxStaticText(m_hdr_panel, wxID_ANY, _L("Tools")));
auto* hdr_gcode = page_label(new wxStaticText(m_hdr_panel, wxID_ANY, _L("G-code")));
hdr_name->SetToolTip(_L("How the mode is labeled in the plate's IDEX/IQEX mode selector. "
"Required — a mode with no name cannot be selected."));
hdr_tools->SetToolTip(_L("Which tool heads take part in the mode and what role each one plays. "
"Click a tile to cycle its role."));
hdr_gcode->SetToolTip(_L("G-code run at print start to put the printer into this mode "
"(for example a Klipper SET_PRINT_MODE call). The slicer only emits "
"the Primary tool's paths; the firmware drives the others."));
const int grid_px = m_n_cols * FromDIP(26) - FromDIP(2); // tile 24 + 2px gap, less the trailing gap
const int name_col_px = FromDIP(kNameColPx + 6);
hdr_sizer->Add(hdr_name, 0, wxALIGN_CENTER_VERTICAL);
hdr_sizer->AddSpacer(std::max(0, name_col_px - hdr_name->GetBestSize().x));
hdr_sizer->Add(hdr_tools, 0, wxALIGN_CENTER_VERTICAL);
hdr_sizer->AddSpacer(std::max(0, grid_px + FromDIP(6) - hdr_tools->GetBestSize().x));
hdr_sizer->Add(hdr_gcode, 1, wxALIGN_CENTER_VERTICAL);
m_hdr_panel->SetSizerAndFit(hdr_sizer);
}
void IMEXModesCtrl::set_grid_size(int n_cols, int n_rows, int layout) {
n_cols = std::max(1, n_cols);
n_rows = std::max(1, n_rows);
if (layout < 0) layout = m_layout;
if (n_cols == m_n_cols && n_rows == m_n_rows && layout == m_layout) return;
auto [names, tools, gcodes] = get_mode_data();
clear_rows();
m_n_cols = n_cols;
m_n_rows = n_rows;
m_layout = layout;
// Span becomes available the moment a second gantry exists, and the header spacing tracks the
// column count -- so the legend and headers are rebuilt here rather than only at construction.
// Without this, adding a gantry gives the tiles a Span role the legend never explains until
// the preset is saved and the page reopened.
rebuild_info_and_header();
for (size_t i = 0; i < names.size(); ++i)
add_row(names[i], tools[i], gcodes[i], /*is_primary=*/(names[i] == kImexPrimaryMode));
Layout();
}
void IMEXModesCtrl::load_from_config(const DynamicPrintConfig& cfg) {
clear_rows();
// One ImexMode per row of imex_mode_names, with any sibling array too short for a row
// padded to an empty string -- exactly what the three per-index bounds checks this
// replaced were doing, now in one place shared with every other IMEX consumer.
const std::vector<ImexMode> table = imex_mode_table(cfg);
// Primary row is always first and non-deletable. Look for an existing
// "primary" entry in the config (present in configs saved after #8 was
// implemented); fall back to empty tool/gcode for older configs.
const ImexMode primary = find_imex_mode(cfg, kImexPrimaryMode);
add_row(kImexPrimaryMode, primary.active_tools, primary.gcode, /*is_primary=*/true);
for (const ImexMode& m : table) {
if (m.index == primary.index) continue; // already added above
add_row(m.name, m.active_tools, m.gcode);
}
refresh_reset_buttons();
Layout();
}
std::tuple<std::vector<std::string>, std::vector<std::string>, std::vector<std::string>>
IMEXModesCtrl::get_mode_data() const {
std::vector<std::string> names, tools, gcodes;
for (auto& r : m_rows) {
// into_u8(), not ToStdString(): everything downstream of here -- the preset, the 3MF
// metadata, the plate's stored mode name -- is UTF-8, while ToStdString() encodes
// through wxConvLibc, which on Windows is the ANSI codepage. The two agree only on a
// UTF-8 locale, so mixing them turns a non-ASCII mode name into mojibake (or an empty
// field, once from_u8() rejects it) on the round trip through the preset.
std::string nm = r.is_primary ? std::string(kImexPrimaryMode) : into_u8(r.name->GetTextCtrl()->GetValue());
if (nm.empty()) nm = unique_mode_name(names);
names.push_back(nm);
tools.push_back(active_tools_string(r));
gcodes.push_back(into_u8(r.gcode->GetValue()));
}
return {names, tools, gcodes};
}
bool IMEXModesCtrl::matches_config(const DynamicPrintConfig& cfg) const {
auto [names, tools, gcodes] = get_mode_data();
auto cfg_strings = [&cfg](const char* key) {
std::vector<std::string> v;
if (auto* o = cfg.option<ConfigOptionStrings>(key)) v = o->values;
return v;
};
return names == cfg_strings("imex_mode_names")
&& tools == cfg_strings("imex_mode_active_tools")
&& gcodes == cfg_strings("imex_mode_gcodes");
}
std::map<int, ImexRole> IMEXModesCtrl::roles_for_mode(const std::string& active_tools) {
std::map<int, ImexRole> roles;
for (const auto& [phys, role] : parse_imex_active_tools(active_tools))
roles[phys] = role;
// Promote the mode's primary, but never invent a tool that isn't in the
// slicer's own active-tool list: parse_imex_active_tools() drops negative and
// out-of-range indices, imex_primary_tool_for_mode() does not range-check the
// upper bound, and a junk index round-tripped back out on save would be a
// token the slicer ignores.
const int primary = imex_primary_tool_for_mode(active_tools);
if (primary >= 0) {
auto it = roles.find(primary);
if (it != roles.end())
it->second = ImexRole::Primary;
}
return roles;
}
std::string IMEXModesCtrl::unique_mode_name(const std::vector<std::string>& also_taken, const std::string& base) const {
auto is_taken = [&](const std::string& cand) {
if (std::find(also_taken.begin(), also_taken.end(), cand) != also_taken.end())
return true;
for (const auto& r : m_rows)
if (!r.is_primary && r.name && into_u8(r.name->GetTextCtrl()->GetValue()) == cand)
return true;
return false;
};
for (int n = 2; ; ++n) {
std::string cand = base + " " + std::to_string(n);
if (!is_taken(cand))
return cand;
}
}
bool IMEXModesCtrl::fix_row_name(Row& r) {
if (r.is_primary || !r.name)
return false;
const std::string name = into_u8(r.name->GetTextCtrl()->GetValue());
// Case-insensitive: a row named "Primary" works, but its menu entry reads the same as the
// built-in Primary's.
const bool reserved = boost::iequals(name, kImexPrimaryMode);
bool taken = false;
for (const Row& other : m_rows)
if (&other != &r && !other.is_primary && other.name && into_u8(other.name->GetTextCtrl()->GetValue()) == name)
taken = true;
if (!name.empty() && !reserved && !taken)
return false;
std::string base = name;
// "copy 2" taken becomes "copy 3", not "copy 2 2".
if (const size_t sp = base.find_last_of(' '); sp != std::string::npos && sp > 0 && sp + 1 < base.size() &&
base.find_first_not_of("0123456789", sp + 1) == std::string::npos)
base.erase(sp);
// ChangeValue(), not SetValue(): no nested wxEVT_TEXT.
r.name->GetTextCtrl()->ChangeValue(from_u8(name.empty() || reserved ? unique_mode_name({}) : unique_mode_name({}, base)));
return true;
}
IMEXModesCtrl::RoleStyle IMEXModesCtrl::role_style(std::optional<ImexRole> role) {
// Gray / "Inactive" is the no-role answer; every role gets an explicit case so a new
// one is a compile-time -Wswitch prompt rather than a tile that silently renders gray.
if (!role)
return { wxColour(90, 90, 90), *wxWHITE, "Inactive" };
switch (*role) {
case ImexRole::Primary: return { wxColour(50, 160, 50), *wxWHITE, "Primary" }; // green
case ImexRole::Copy: return { wxColour(60, 120, 210), *wxWHITE, "Copy" }; // blue
case ImexRole::Mirror: return { wxColour(210, 130, 20), *wxWHITE, "Mirror" }; // amber
case ImexRole::Span: return { wxColour(180, 180, 40), *wxWHITE, "Span" }; // yellow
}
return { wxColour(90, 90, 90), *wxWHITE, "Inactive" }; // keeps every compiler quiet
}
wxString IMEXModesCtrl::role_hint(ImexRole role) {
switch (role) {
case ImexRole::Primary:
return _L("Drives every sliced path. Always tool 0 — use Tool 0 Position above to choose "
"which corner of the bed it occupies. It cannot be moved from the grid.");
case ImexRole::Copy:
return _L("Follows the Primary at the firmware level, printing the same paths offset into "
"its own zone.");
case ImexRole::Mirror:
return _L("Follows the Primary at the firmware level, printing the same paths reflected "
"across the boundary between the two zones.");
case ImexRole::Span:
return _L("Marks a tool on the Primary's gantry as its multicolor partner. Required to "
"allow multi-color printing in paired-gantry modes.");
}
return wxEmptyString;
}
bool IMEXModesCtrl::role_offered(ImexRole role) const {
// Span declares a within-gantry multicolor partner of the Primary, which only means
// something when there is a second gantry running in parallel with it.
if (role == ImexRole::Span)
return m_n_rows >= 2;
return true;
}
bool IMEXModesCtrl::role_allowed_on_tile(ImexRole role, int tool_idx,
bool other_primary, int primary_gantry) const {
if (!role_offered(role))
return false;
// Exactly one tile drives the sliced paths.
if (role == ImexRole::Primary)
return !other_primary;
// Span only on a tile sharing the Primary's gantry — that is what the marker declares.
if (role == ImexRole::Span)
return primary_gantry >= 0 && (tool_idx / m_n_cols) == primary_gantry;
return true;
}
std::optional<ImexRole> IMEXModesCtrl::next_tile_role(std::optional<ImexRole> current,
const std::function<bool(ImexRole)>& allowed) {
const int n = (int)(sizeof(kImexRoleTable) / sizeof(kImexRoleTable[0]));
int start = 0; // no current role (Inactive) → start at the first role
if (current) {
for (int i = 0; i < n; ++i)
if (kImexRoleTable[i].role == *current) { start = i + 1; break; }
}
for (int i = start; i < n; ++i)
if (allowed(kImexRoleTable[i].role))
return kImexRoleTable[i].role;
return std::nullopt; // walked off the end → back to Inactive
}
// The tool grid says which topologies this printer can express, so a two-tool machine is not
// offered the four-carriage modes. Names a row already uses are dropped, leaving only what is
// still free. These are conventions, not keywords -- nothing in the slicer reads a mode's name
// except as the key a plate stores -- so the field stays typeable and this is only a shortcut.
std::vector<wxString> IMEXModesCtrl::suggested_mode_names(const ::ComboBox* skip) const {
std::vector<std::string> pool = { "copy", "mirror" };
// Four carriages to drive, however they are arranged.
if (m_n_cols * m_n_rows >= 4)
for (const char* n : { "iq-copy", "iq-mirror" })
pool.emplace_back(n);
// Multicolor needs a Span partner beside the primary, so two tools on a gantry, and a
// second gantry to copy the pair onto. imex_resolve_routing() refuses a multicolor mode
// with no Span on the primary's gantry, so offering one here that it would reject is
// worse than not offering it at all.
if (m_n_cols >= 2 && m_n_rows >= 2)
for (const char* n : { "mc-copy", "mc-mirror" })
pool.emplace_back(n);
std::vector<wxString> out;
for (const std::string& cand : pool) {
bool taken = false;
for (const Row& r : m_rows)
if (!r.is_primary && r.name && r.name != skip &&
into_u8(r.name->GetTextCtrl()->GetValue()) == cand) {
taken = true;
break;
}
if (!taken)
out.push_back(from_u8(cand));
}
return out;
}
void IMEXModesCtrl::apply_btn(wxButton* btn, int tool_idx, std::optional<ImexRole> role) {
// Always label as T{n} — the button color already encodes the role.
const RoleStyle style = role_style(role);
btn->SetLabel(wxString::Format("T%d", tool_idx));
btn->SetBackgroundColour(style.bg);
btn->SetForegroundColour(style.fg);
btn->Refresh();
}
IMEXModesCtrl::RowSnapshot IMEXModesCtrl::snapshot_row(const Row& r) const {
RowSnapshot s;
// UTF-8 throughout -- this snapshot is compared against the preset's own values.
s.name = r.is_primary ? std::string(kImexPrimaryMode) : into_u8(r.name->GetTextCtrl()->GetValue());
s.tools = active_tools_string(r);
s.gcode = into_u8(r.gcode->GetValue());
return s;
}
bool IMEXModesCtrl::row_differs_from_parent(int row_idx) const {
if (!m_parent_lookup) return false;
const DynamicPrintConfig* parent = m_parent_lookup();
if (!parent) return false;
auto* p_names = parent->option<ConfigOptionStrings>("imex_mode_names");
auto* p_tools = parent->option<ConfigOptionStrings>("imex_mode_active_tools");
auto* p_gcodes = parent->option<ConfigOptionStrings>("imex_mode_gcodes");
if (!p_names || row_idx < 0 || row_idx >= (int)p_names->values.size()) return false;
const Row& r = m_rows[row_idx];
const RowSnapshot s = snapshot_row(r);
if (!r.is_primary && s.name != p_names->values[row_idx]) return true;
if (p_tools && row_idx < (int)p_tools->values.size() && s.tools != p_tools->values[row_idx]) return true;
if (p_gcodes && row_idx < (int)p_gcodes->values.size() && s.gcode != p_gcodes->values[row_idx]) return true;
return false;
}
bool IMEXModesCtrl::row_has_parent_counterpart(int row_idx) const {
if (!m_parent_lookup) return false;
const DynamicPrintConfig* parent = m_parent_lookup();
if (!parent) return false;
auto* p_names = parent->option<ConfigOptionStrings>("imex_mode_names");
return p_names && row_idx >= 0 && row_idx < (int)p_names->values.size();
}
void IMEXModesCtrl::add_row(const std::string& name,
const std::string& active_tools,
const std::string& gcode,
bool is_primary)
{
Row r;
r.is_primary = is_primary;
r.panel = new wxPanel(this, wxID_ANY);
r.panel->SetBackgroundColour(GetBackgroundColour());
auto* sizer = new wxBoxSizer(wxHORIZONTAL);
// The mode column is vertical: name on top, and for a deletable row the remove button
// beneath it. Remove used to sit next to Edit in the right-hand column, one icon apart
// from a button people press often -- a destructive action does not belong there.
auto* name_col = new wxBoxSizer(wxVERTICAL);
if (is_primary) {
r.orig_name = kImexPrimaryMode;
r.name = nullptr;
auto* lbl = page_label(new wxStaticText(r.panel, wxID_ANY, _L("Primary"),
wxDefaultPosition, FromDIP(wxSize(kNameColPx, -1))));
wxFont f = lbl->GetFont();
f.SetWeight(wxFONTWEIGHT_BOLD);
lbl->SetFont(f);
name_col->Add(lbl, 0); // the combo below carries no frame, so no inset to match
} else {
// A nameless mode cannot be selected on a plate and used to be dropped on
// save together with its tool roles and G-code, so a name is pre-filled
// here and restored below if the field is left empty. That keeps the
// "Add Mode → assign tools → never typed a name" path from losing work
// without ever showing an error.
const std::string nm = name.empty() ? unique_mode_name({}) : name;
// The name as first shown, i.e. the one the plate's mode selector offered while this
// row looked like this. remove_row() reports it alongside the current name, because
// renaming is not routed through on_mode_removed and the two then diverge.
r.orig_name = nm;
// Editable: no wxCB_READONLY, so the inner text control stays live and any name can
// still be typed, which the mode table needs -- it is authored for whatever hardware
// the user has. ComboBox derives from TextInput and paints the same border the settings
// fields do, so it needs no frame panel of its own.
r.name = new ::ComboBox(r.panel, wxID_ANY, wxEmptyString, wxDefaultPosition,
FromDIP(wxSize(kNameColPx, -1)), 0, nullptr, 0);
for (const wxString& sug : suggested_mode_names())
r.name->Append(sug);
// Rebuilt on every click that could open the list: add_row() runs before this row joins
// m_rows and before the rows below it exist, so a list built once there filters against
// only part of the table and offers names that are already taken. By then every row is
// present -- and this row's own name is excluded from "taken" so it stays offered. It
// has to be BEFORE ComboBox opens it, not in wxEVT_COMBOBOX_DROPDOWN, which ComboBox
// sends after sizing and showing the popup from the old items: with every name taken
// that showed an empty popup. Bound handlers run ahead of ComboBox's own event-table
// entries, so this rebuilds first and, with nothing to offer, keeps it shut. Only clicks
// can open it: focus lives in the inner text control, so keys never reach the combo.
const auto on_open_click = [this, cb = r.name](wxMouseEvent& e) {
if (!cb->is_drop_down()) {
cb->Clear();
for (const wxString& sug : suggested_mode_names(cb))
cb->Append(sug);
if (cb->GetCount() == 0) {
cb->SetFocus(); // what ComboBox's own click would have done, short of the list
return;
}
}
e.Skip();
};
r.name->Bind(wxEVT_LEFT_DOWN, on_open_click);
r.name->Bind(wxEVT_LEFT_DCLICK, on_open_click);
// Constructed empty on purpose: ComboBox hands its value to TextInput as the LABEL --
// the small right-aligned slot a unit like "mm" lives in -- because a read-only combo
// hides the text control and shows that label instead. This one is editable, so the
// value belongs in the text control; left in the label it renders as a second, greyed
// copy of the name beside the hint.
r.name->GetTextCtrl()->ChangeValue(from_u8(nm));
r.name->GetTextCtrl()->SetHint(_L("Mode name (required)"));
r.name->SetToolTip(_L("Name of this parallel mode, as it appears in the plate's IDEX/IQEX mode "
"selector. Stored in the project by name, so renaming a mode that "
"plates already use makes them fall back to Primary. Cannot be empty "
"— a blank name is replaced with a generated one."));
r.name->GetTextCtrl()->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { notify(); });
// Picking a suggestion writes it into the text control, so everything downstream keeps
// reading one place for the name.
r.name->Bind(wxEVT_COMBOBOX, [this, cb = r.name](wxCommandEvent& e) {
// Do NOT call SetLabel() here. It is overridden, and on an editable combo it
// writes the TEXT CONTROL rather than the label (ComboBox.cpp, the IsShown()
// branch) -- clearing it would erase the name that was just picked. The label
// stays empty for this control's whole life anyway: it is constructed empty and
// every write routes through the same override.
// ChangeValue is still needed: SetSelection() early-returns when the index is
// unchanged, so re-picking the current item would otherwise leave typed-over text.
cb->GetTextCtrl()->ChangeValue(cb->GetDropDown().GetValue());
notify();
});
// Restore a name rather than let the row reach get_mode_data() unnamed.
// Row is located by panel pointer (stable across add/remove) so a
// kill-focus delivered while the rows are being torn down is a no-op.
r.name->GetTextCtrl()->Bind(wxEVT_SET_FOCUS, [this, panel = r.panel](wxFocusEvent& e) {
e.Skip();
for (auto& row_ref : m_rows)
if (row_ref.panel == panel && row_ref.name)
row_ref.focus_name = into_u8(row_ref.name->GetTextCtrl()->GetValue());
});
r.name->GetTextCtrl()->Bind(wxEVT_KILL_FOCUS, [this, panel = r.panel](wxFocusEvent& e) {
e.Skip();
if (m_clearing_rows) return; // focus-out emitted while the rows are being deleted
for (auto& row_ref : m_rows) {
if (row_ref.panel != panel) continue;
// Only an edit is checked. Tabbing through a hand-edited profile's duplicate
// leaves it alone rather than renaming the row plates resolve to.
const wxString value = row_ref.name ? row_ref.name->GetTextCtrl()->GetValue() : wxString();
if (!value.empty() && into_u8(value) == row_ref.focus_name)
return;
if (fix_row_name(row_ref))
notify();
return;
}
});
}
auto* grid_panel = new wxPanel(r.panel, wxID_ANY);
grid_panel->SetBackgroundColour(GetBackgroundColour());
auto* grid_sizer = new wxGridSizer(m_n_rows, m_n_cols, FromDIP(2), FromDIP(2));
auto tool_roles = roles_for_mode(active_tools);
if (tool_roles.empty())
tool_roles[0] = ImexRole::Primary; // default T0 → Primary when no assignment is stored
r.all_tool_roles = tool_roles; // preserve all assignments, including off-screen tools
wxPanel* this_panel = r.panel;
// Buttons rendered top=rear (high Y), bottom=front (low Y).
// Layout remapping: m_layout encodes which corner T0 is at physically.
// flip_x (layout 1,3): col 0 is on the right (max-X) instead of left
// flip_y (layout 2,3): row 0 is at the rear (max-Y) instead of front
// Display iterates: row from n_rows-1 down to 0 (rear→front = top→bottom).
// raw_row = flip_y ? (n_rows-1-row) : row
// raw_col = flip_x ? (n_cols-1-col) : col
//
// Render the whole grid: m_n_rows is the gantry count, so the valid tool indices are
// 0 .. m_n_rows*m_n_cols-1 and a window this tall only fits inside the grid at row 0.
// A Primary outside the grid is a data problem the zone layout already reports by
// producing no zones (compute_imex_zone_layout, IMEXZones.cpp).
bool flip_x = (m_layout == 1 || m_layout == 3);
bool flip_y = (m_layout == 2 || m_layout == 3);
for (int row = m_n_rows - 1; row >= 0; --row) {
for (int col = 0; col < m_n_cols; ++col) {
int raw_row = flip_y ? (m_n_rows - 1 - row) : row;
int raw_col = flip_x ? (m_n_cols - 1 - col) : col;
int tool_idx = raw_row * m_n_cols + raw_col;
std::optional<ImexRole> role; // nullopt == Inactive
auto it = tool_roles.find(tool_idx);
if (it != tool_roles.end()) role = it->second;
// wxBU_AUTODRAW, which wx ignores on a text button, is what GUI_App::UpdateDarkUI
// skips: without it every theme change ran the role colors through the palette
// map and left the labels gray. apply_btn() owns these colors in both themes.
auto* btn = new wxButton(grid_panel, wxID_ANY, wxEmptyString,
wxDefaultPosition, FromDIP(wxSize(24, 24)), wxBU_EXACTFIT | wxBU_AUTODRAW);
apply_btn(btn, tool_idx, role);
int btn_pos = (int)r.btns.size();
r.btns.push_back(btn);
r.btn_roles.push_back(role);
r.btn_tool_idx.push_back(tool_idx);
// Two tiles are read-only.
//
// The whole reserved `primary` row: it is the IMEX-off mode, so cycling roles there
// would be a no-op and confusing.
//
// And, on any row, the tile that currently HOLDS Primary. Primary is tool 0 and moves
// only via Tool 0 Position; letting a click demote it would leave the mode with no
// Primary at all, which parses to -1 and degrades the plate to an ordinary single-tool
// print with no zones — authored silently, in one click, with nothing in the editor
// showing what is wrong. The other tiles already never OFFER Primary while one is held
// (role_allowed_on_tile), so this closes the only remaining route to that state.
// A mode that reaches us WITHOUT a Primary (hand-edited preset, foreign 3MF) still has
// every tile live, so it can be repaired by promoting one.
if (is_primary || role == ImexRole::Primary) {
btn->Disable();
} else {
btn->Bind(wxEVT_BUTTON, [this, this_panel, btn_pos](wxCommandEvent&) {
// Find the row by panel pointer (stable across add/remove)
for (auto& row_ref : m_rows) {
if (row_ref.panel != this_panel) continue;
std::optional<ImexRole>& tile = row_ref.btn_roles[btn_pos];
const int tidx = row_ref.btn_tool_idx[btn_pos];
// Check if another button already holds the Primary role,
// and locate the primary's gantry row for Span eligibility.
bool other_primary = false;
int primary_gantry = -1;
for (int j = 0; j < (int)row_ref.btn_roles.size(); ++j) {
if (row_ref.btn_roles[j] == ImexRole::Primary) {
if (j != btn_pos) other_primary = true;
primary_gantry = row_ref.btn_tool_idx[j] / m_n_cols;
}
}
// Cycle Inactive → Primary → Copy → Mirror → Span → Inactive,
// skipping whatever this tile may not take right now.
tile = next_tile_role(tile, [&](ImexRole cand) {
return role_allowed_on_tile(cand, tidx, other_primary, primary_gantry);
});
// Keep all_tool_roles in sync so off-screen tools are preserved
if (tile)
row_ref.all_tool_roles[tidx] = *tile;
else
row_ref.all_tool_roles.erase(tidx);
apply_btn(row_ref.btns[btn_pos], row_ref.btn_tool_idx[btn_pos], tile);
notify();
break;
}
});
}
grid_sizer->Add(btn, 0);
}
}
grid_panel->SetSizerAndFit(grid_sizer);
// from_u8(): `gcode` arrives from the preset as UTF-8. Handing the raw std::string to
// wxString would decode it through the current locale's encoding instead.
auto* gcode_frame = framed_input(r.panel);
// wxBORDER_NONE matters off GTK: a wxTextCtrl's default border resolves to a themed or
// sunken edge on Windows and to NSBezelBorder on macOS, which would sit immediately inside
// the frame below and read as two borders.
r.gcode = new wxTextCtrl(gcode_frame, wxID_ANY, from_u8(gcode),
wxDefaultPosition, FromDIP(wxSize(220, 54)),
wxTE_MULTILINE | wxBORDER_NONE);
gcode_frame->GetSizer()->Add(r.gcode, 1, wxEXPAND | wxALL, 1);
// Same monospace face EditGCodeDialog gives its editor, so G-code reads the same
// wherever it is edited; and the same explicit-light-then-UpdateDarkUI treatment as the
// name field above, for the same reason.
r.gcode->SetFont(wxGetApp().code_font());
r.gcode->SetBackgroundColour(*wxWHITE);
r.gcode->SetForegroundColour(wxColour("#262E30")); // the palette's input text color
wxGetApp().UpdateDarkUI(r.gcode);
// Tooltip only, no SetHint(): wxTextEntry has no native placeholder for a
// multiline control, so wxWidgets emulates one by writing the hint into the
// control as gray text — indistinguishable from real G-code in this box.
r.gcode->SetToolTip(_L("G-code emitted once at the start of a print that uses this mode, before "
"the machine start G-code. This is where the printer is put into the "
"matching firmware mode — for example a Klipper SET_PRINT_MODE call or a "
"RepRapFirmware M567 — since the slicer only emits the Primary tool's "
"paths and the firmware drives the Copy / Mirror tools. Placeholders are "
"supported; use the edit button to browse them. Leave empty if the mode "
"needs no firmware setup."));
r.gcode->Bind(wxEVT_TEXT, [this](wxCommandEvent&) { notify(); });
if (!is_primary)
name_col->Add(r.name, 0);
sizer->Add(name_col, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6));
sizer->Add(grid_panel, 0, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(6));
sizer->Add(gcode_frame, 1, wxALIGN_CENTER_VERTICAL | wxRIGHT, FromDIP(4));
// Edit on top, per-row reset under it, in one column top-aligned with the G-code box.
// Remove is not here -- it sits under the name field, away from the button people press
// most. The column is top- rather than centre-justified so the edit icon keeps the same
// position whatever a row's height turns out to be.
auto* btn_col = new wxBoxSizer(wxVERTICAL);
wxTextCtrl* gcode_ctrl = r.gcode;
auto* ph_btn = new ScalableButton(r.panel, wxID_ANY, "edit", wxEmptyString,
wxDefaultSize, wxDefaultPosition,
wxBU_EXACTFIT | wxNO_BORDER, /*use_default_disabled_bitmap=*/true, kImexIconPx);
ph_btn->SetToolTip(_L("Edit G-code / browse placeholders"));
ph_btn->Bind(wxEVT_BUTTON, [this, gcode_ctrl](wxCommandEvent&) {
// EditGCodeDialog takes and returns UTF-8 (get_edited_gcode() is a ToUTF8()).
EditGCodeDialog dlg(this, "imex_mode_gcode", into_u8(gcode_ctrl->GetValue()));
if (dlg.ShowModal() == wxID_OK)
gcode_ctrl->SetValue(from_u8(dlg.get_edited_gcode()));
});
btn_col->Add(ph_btn, 0, wxBOTTOM, FromDIP(2));
if (!is_primary) {
auto* rm = new ScalableButton(r.panel, wxID_ANY, "delete", wxEmptyString,
wxDefaultSize, wxDefaultPosition,
wxBU_EXACTFIT | wxNO_BORDER, /*use_default_disabled_bitmap=*/true, kImexIconPx);
rm->SetToolTip(_L("Remove mode"));
rm->Bind(wxEVT_BUTTON, [this, this_panel](wxCommandEvent&) {
const std::vector<std::string> removed = remove_row(this_panel);
// Copied before notify(): on_change ends in on_value_change, which can send
// the tab through update() and hence load_from_config(), and that rebuilds
// every row. Reading the member afterwards would be reading through a
// handler whose owner has just been rebuilt underneath it.
auto removed_cb = on_mode_removed;
notify();
if (removed_cb && !removed.empty())
removed_cb(removed);
});
name_col->Add(rm, 0, wxALIGN_RIGHT | wxTOP, FromDIP(2));
}
// Reset only renders when this row has a counterpart in the saved preset (user-added rows
// beyond the saved mode count get no reset — remove covers "drop the row I just added").
wxPanel* this_panel_for_reset = r.panel;
if (row_has_parent_counterpart(static_cast<int>(m_rows.size()))) {
r.reset_btn = new ScalableButton(r.panel, wxID_ANY, "dot", wxEmptyString,
wxDefaultSize, wxDefaultPosition,
wxBU_EXACTFIT | wxNO_BORDER, /*use_default_disabled_bitmap=*/true, kImexIconPx);
r.reset_btn->SetToolTip(_L("Discard in-session edits to this mode (snap back to saved value)"));
r.reset_btn->Bind(wxEVT_BUTTON, [this, this_panel_for_reset](wxCommandEvent&) {
reset_row_to_parent(this_panel_for_reset);
});
btn_col->Add(r.reset_btn, 0, wxTOP, FromDIP(2));
}
sizer->Add(btn_col, 0, wxALIGN_TOP | wxRIGHT, FromDIP(4));
r.panel->SetSizerAndFit(sizer);
m_rows_sizer->Add(r.panel, 0, wxEXPAND | wxBOTTOM, FromDIP(4));
m_rows.push_back(std::move(r));
Layout();
}
std::vector<std::string> IMEXModesCtrl::remove_row(wxPanel* panel) {
for (size_t i = 0; i < m_rows.size(); ++i) {
if (m_rows[i].panel != panel) continue;
if (m_rows[i].is_primary) return {}; // primary row is non-deletable
// Both names a plate can be holding for this row: the one the row was built with and
// the one in the field now. They diverge as soon as the row is renamed, because a
// rename does not rebuild the row and is deliberately not reported as a removal (the
// name field notifies on every keystroke). Reporting only the current name is what
// let "rename, then delete" leave the plate on a mode that no longer exists.
std::vector<std::string> removed_names;
const std::string current = m_rows[i].name ? into_u8(m_rows[i].name->GetTextCtrl()->GetValue())
: std::string();
for (const std::string& n : {m_rows[i].orig_name, current})
if (!n.empty() && std::find(removed_names.begin(), removed_names.end(), n) == removed_names.end())
removed_names.push_back(n);
m_rows_sizer->Detach(panel);
m_rows.erase(m_rows.begin() + i);
// A name a surviving row still carries is not stale: the user can rename row A to
// row B's old name and then delete B. Plates holding it now resolve to A, so drop it
// rather than reset them.
auto still_in_use = [this](const std::string& n) {
return std::any_of(m_rows.begin(), m_rows.end(), [&n](const Row& r) {
return r.is_primary ? n == kImexPrimaryMode
: r.name && into_u8(r.name->GetTextCtrl()->GetValue()) == n;
});
};
removed_names.erase(std::remove_if(removed_names.begin(), removed_names.end(), still_in_use),
removed_names.end());
Layout();
// Defer widget destruction so any in-flight GTK events for
// panel's children (including the remove button we're inside) finish
// processing before the wxEvtHandlers are freed.
wxTheApp->CallAfter([panel]() { panel->Destroy(); });
return removed_names;
}
return {};
}
void IMEXModesCtrl::clear_rows() {
// Destroying a focused wxTextCtrl emits wxEVT_KILL_FOCUS, and the Name field's
// handler would then read m_rows while it is half-destroyed. Flag the teardown
// so that handler stays out of the way.
m_clearing_rows = true;
for (auto& r : m_rows) { m_rows_sizer->Detach(r.panel); r.panel->Destroy(); }
m_rows.clear();
m_clearing_rows = false;
}
std::string IMEXModesCtrl::active_tools_string(const Row& r) const {
// Serialize from all_tool_roles (not just visible buttons) so assignments
// for off-screen tools are preserved across grid size changes. Inactive tools are
// simply absent from the map, so there is nothing to filter out here.
//
// The letters come from imex_role_letter(), the same table parse_imex_active_tools()
// reads back — this used to be a second, independent int → letter switch, which is
// exactly where the editor and the slicer could come to disagree about a mode string.
std::string s;
for (const auto& [idx, role] : r.all_tool_roles) {
if (!s.empty()) s += ',';
s += std::to_string(idx) + ':' + imex_role_letter(role);
}
return s;
}
void IMEXModesCtrl::notify() {
refresh_reset_buttons();
if (on_change) on_change();
}
void IMEXModesCtrl::refresh_reset_buttons() {
for (size_t i = 0; i < m_rows.size(); ++i) {
Row& r = m_rows[i];
if (!r.reset_btn || !r.reset_btn->IsEnabled()) continue;
const bool dirty = row_differs_from_parent(static_cast<int>(i));
if (dirty == r.reset_dirty_cached) continue;
r.reset_dirty_cached = dirty;
r.reset_btn->SetBitmap_(dirty ? "undo" : "dot");
}
}
void IMEXModesCtrl::reset_row_to_parent(wxPanel* panel) {
if (!m_parent_lookup) return;
const DynamicPrintConfig* parent = m_parent_lookup();
if (!parent) return;
auto* p_names = parent->option<ConfigOptionStrings>("imex_mode_names");
auto* p_tools = parent->option<ConfigOptionStrings>("imex_mode_active_tools");
auto* p_gcodes = parent->option<ConfigOptionStrings>("imex_mode_gcodes");
for (size_t i = 0; i < m_rows.size(); ++i) {
Row& r = m_rows[i];
if (r.panel != panel) continue;
if (!p_names || i >= p_names->values.size()) return;
if (!r.is_primary && r.name) {
r.name->GetTextCtrl()->ChangeValue(from_u8(p_names->values[i]));
// The saved name may since have been given to another row, which keeps it.
fix_row_name(r);
}
if (p_gcodes && i < p_gcodes->values.size())
r.gcode->ChangeValue(from_u8(p_gcodes->values[i]));
if (p_tools && i < p_tools->values.size()) {
// Reapply tool roles from the parent's serialized form. all_tool_roles
// is the source of truth for round-tripping; rebuild it then re-paint
// the visible buttons.
r.all_tool_roles = roles_for_mode(p_tools->values[i]);
for (size_t j = 0; j < r.btns.size(); ++j) {
int tidx = r.btn_tool_idx[j];
auto it = r.all_tool_roles.find(tidx);
std::optional<ImexRole> role;
if (it != r.all_tool_roles.end()) role = it->second;
r.btn_roles[j] = role;
apply_btn(r.btns[j], tidx, role);
}
}
notify();
return;
}
}
}} // namespace Slic3r::GUI
+270
View File
@@ -0,0 +1,270 @@
#ifndef slic3r_IMEXModesCtrl_hpp_
#define slic3r_IMEXModesCtrl_hpp_
#include <libslic3r/PrintConfig.hpp>
#include <utility>
#include <wx/colour.h>
#include <wx/panel.h>
#include <wx/string.h>
#include <wx/sizer.h>
#include <wx/button.h>
#include <wx/textctrl.h>
#include <functional>
#include <map>
#include <optional>
#include <string>
#include <tuple>
#include <vector>
#include "libslic3r/IMEXHelpers.hpp"
class ScalableButton;
class ComboBox;
namespace Slic3r {
namespace GUI {
// ---------------------------------------------------------------------------
// IMEXModesCtrl — visual parallel-mode editor for the IDEX/IQEX printer tab
//
// A tool tile holds an `std::optional<ImexRole>`: the role the mode gives that tool, or
// nullopt for Inactive (gray — the tool does not take part in this mode). Inactive is the
// editor's own state and the only reason the tile is not a bare ImexRole; it is the absence
// of a role, so it is spelled as one rather than as an extra enumerator that libslic3r would
// then have to keep rejecting. Roles themselves are never re-encoded here — no int state
// table, no second copy of the on-disk letters.
//
// nullopt (gray) — not participating in this mode
// ImexRole::Primary (green) — the tool the slicer generates paths for
// ImexRole::Copy (blue) — firmware duplicates Primary at an offset
// ImexRole::Mirror (amber) — firmware mirrors Primary about an axis
// ImexRole::Span (yellow)— multicolor partner of Primary on the same gantry;
// declares paired-gantry multicolor topology and
// unlocks the multicolor block at slice time. Only
// offered on tools sharing primary's gantry, and
// only on multi-gantry printers (n_rows >= 2).
//
// Color and legend text are the editor's business and live in role_style(); the tile cycle
// order and the serialized letters come from kImexRoleTable in IMEXHelpers.hpp, so the
// editor cannot drift from the slicer's reading of a mode string.
//
// Active-tools string format: "idx:P,idx:C,idx:M,idx:S" (backwards-compat: plain "idx" = Primary)
// ---------------------------------------------------------------------------
class IMEXModesCtrl : public wxPanel {
public:
std::function<void()> on_change;
// Fired after on_change has written the shortened table back to the preset, with EVERY
// name the deleted row could be known by: the name the row was built with (what a plate
// set from the saved preset holds) and the name in the field at delete time (what a plate
// set after an in-session rename holds). Renaming is deliberately not routed through here
// -- the name field notifies on every keystroke -- so by the time the row is deleted the
// two can differ, and reporting only the current name leaves the plate pointing at a mode
// that no longer exists. Names a surviving row still carries are filtered out, so renaming
// one row to another's old name and then deleting cannot reset a plate that legitimately
// resolves elsewhere. Plates still set to any reported name are stale from this point on
// -- see the handler in TabPrinter::build_fff.
std::function<void(const std::vector<std::string>&)> on_mode_removed;
// Lazy lookup so the widget always sees the current parent (preset switches
// change the parent under us). Returns nullptr when the active preset has no
// parent (system preset itself), which disables per-row reset arrows.
using ParentConfigLookup = std::function<const DynamicPrintConfig*()>;
void set_parent_config_lookup(ParentConfigLookup f) { m_parent_lookup = std::move(f); }
// layout: 0=front-left, 1=front-right, 2=rear-left, 3=rear-right (T0 corner).
// The enum values intentionally line up 1:1 so this is a no-op cast — the helper
// is kept to make the conversion explicit at call sites.
static int parse_layout(ImexToolLayout layout) { return static_cast<int>(layout); }
IMEXModesCtrl(wxWindow* parent, int n_cols, int n_rows, int layout = 0);
void set_grid_size(int n_cols, int n_rows, int layout = -1);
void load_from_config(const DynamicPrintConfig& cfg);
// Every row on screen produces exactly one entry — a row is never skipped.
// Skipping empty-named rows used to drop the row's tool roles and G-code
// silently, and because matches_config() compares against this same filtered
// output the preset did not even go dirty to hint that something had gone
// missing; the row stayed on screen until the next load_from_config() quietly
// removed it. add_row() pre-fills a name and the Name field restores one when
// it is left empty, so the substitution below is the last-resort guard for a
// name that arrives empty from a hand-edited preset or 3MF.
std::tuple<std::vector<std::string>, std::vector<std::string>, std::vector<std::string>>
get_mode_data() const;
// True when the widget's current rows already mirror the three IMEX-mode option
// vectors in cfg. Used by TabPrinter::update_fff() to skip a destroy-and-rebuild
// pass when the only thing that just changed was the widget itself writing back
// to config — without this guard, every keystroke in the gcode textbox triggers
// a load_from_config() that detaches the textbox the user is typing in.
bool matches_config(const DynamicPrintConfig& cfg) const;
// Whether the printer is an IMEX printer. The grid sits in an options group that shows or hides
// everything in it by mode, so Show() keeps it hidden on other printers whatever the group asks.
void set_applicable(bool applicable)
{
m_applicable = applicable;
if (!applicable)
wxPanel::Show(false); // showing is left to the group, which knows the mode
}
bool Show(bool show = true) override { return wxPanel::Show(show && m_applicable); }
private:
bool m_applicable = true;
// map<physical tool idx, role> for one `imex_mode_active_tools` entry. A tool absent
// from the map is Inactive — the same convention the serialized string uses, so the
// round trip needs no separate "off" value.
//
// Deliberately NOT a parser of its own: the slicer decides what actually prints,
// so the editor has to read the string exactly the way the slicer does or the
// same mode means two different things in the two places. Roles come from
// parse_imex_active_tools(); the primary index comes from
// imex_primary_tool_for_mode(), which is where the "a bare index is Primary"
// backwards-compat rule lives. Both are needed: parse_imex_active_tools()
// defaults a bare token to Copy (deliberately — see the note above
// parse_imex_active_tools() in IMEXHelpers.hpp), so only the combination
// reproduces the slicer's reading of a legacy "0,1,2" string, which is
// "T0 primary, the rest copies".
//
// Indices are PHYSICAL tool indices (T0..TN-1). all_tool_roles preserves
// off-grid entries so the tile widget can round-trip indices that fall
// outside the currently visible rows × cols without losing data on save.
static std::map<int, ImexRole> roles_for_mode(const std::string& active_tools);
// `base` + " N" for the lowest N >= 2 not already used by a row or by `also_taken`
// (N == 1 is the fixed Primary row, or the name being made unique). Deterministic for a
// given set of rows, so get_mode_data() is stable across calls and matches_config() stays
// honest. Intentionally NOT translated: mode names are identifiers — objects
// store one in `imex_parallel_mode` and GCode.cpp matches it against
// `imex_mode_names` by string — so a locale-dependent name would break a project
// opened under a different language.
std::string unique_mode_name(const std::vector<std::string>& also_taken, const std::string& base = "Mode") const;
// Everything the editor draws for one role, in ONE place: a new role needs a color and
// a legend name here and nowhere else in this file. nullopt is Inactive (gray).
// `label` is deliberately untranslated — it names a role, and the legend sits next to
// the same letters the preset stores.
struct RoleStyle { wxColour bg; wxColour fg; const char* label; };
static RoleStyle role_style(std::optional<ImexRole> role);
// True when this printer offers `role` at all, regardless of which tile is clicked.
// Span needs a second gantry to be a partner across, so a single-gantry printer neither
// cycles to it nor lists it in the legend.
bool role_offered(ImexRole role) const;
// True when the tile for `tool_idx` may take `role` right now. Adds the positional rules
// on top of role_offered(): only one tile may hold Primary, and Span only on a tile
// sharing the Primary's gantry.
bool role_allowed_on_tile(ImexRole role, int tool_idx,
bool other_primary, int primary_gantry) const;
// Next state in the tile cycle: Inactive → kImexRoleTable order → Inactive, stepping
// over any role `allowed` rejects. Inactive is always reachable, so it terminates.
static std::optional<ImexRole> next_tile_role(std::optional<ImexRole> current,
const std::function<bool(ImexRole)>& allowed);
// Mode names this printer's tool grid can actually carry, minus those already taken.
std::vector<wxString> suggested_mode_names(const ::ComboBox* skip = nullptr) const;
void apply_btn(wxButton* btn, int tool_idx, std::optional<ImexRole> role);
struct Row {
wxPanel* panel;
::ComboBox* name; // nullptr for primary row (name is fixed)
wxTextCtrl* gcode;
std::vector<wxButton*> btns;
std::vector<std::optional<ImexRole>> btn_roles; // nullopt == Inactive
std::vector<int> btn_tool_idx;
std::map<int, ImexRole> all_tool_roles; // absent == Inactive
// Name the row was built with (UTF-8), i.e. the name that was in the preset — and so
// in the plate's mode selector — when the row appeared. Kept because the name field is
// free to drift from it without the row being rebuilt; remove_row() needs both to find
// the plates a delete strands. Rebuilding the rows (load_from_config / set_grid_size)
// re-seeds it, which is correct: config and rows agree again at that point.
std::string orig_name;
// The name when the field last took focus, so a commit that leaves it unchanged is not
// treated as an edit.
std::string focus_name;
bool is_primary {false};
ScalableButton* reset_btn {nullptr}; // nullptr when row has no parent counterpart
bool reset_dirty_cached {false};
};
// Snapshot of row content as it would be saved (matches get_mode_data() per-row).
struct RowSnapshot { std::string name, tools, gcode; };
RowSnapshot snapshot_row(const Row& r) const;
// True when row at `row_idx` differs from the parent preset's value at the same
// index. Returns false when there is no parent (system preset), when the row is
// beyond the parent's mode count (user added it — no defined "default"), or when
// the lookup callback isn't wired. The Primary row's name is sentinel-fixed so
// a name diff doesn't count for it; only tools/gcode do.
bool row_differs_from_parent(int row_idx) const;
// True when row index has a parent counterpart at all (i.e., the reset arrow
// should be present on the row at all, regardless of dirty state).
bool row_has_parent_counterpart(int row_idx) const;
void add_row(const std::string& name = "",
const std::string& active_tools = "",
const std::string& gcode = "",
bool is_primary = false);
// Returns every name the removed row was known by (UTF-8, deduplicated: its build-time
// name and its current name, which differ after an in-session rename), minus any name a
// surviving row still carries. The caller uses them to find the plates the delete
// stranded. Empty when nothing was removed, when the row is the non-deletable Primary,
// or when both names are blank -- a blank name is only reachable on a row that was never
// named, which no plate can be holding.
std::vector<std::string> remove_row(wxPanel* panel);
void clear_rows();
std::string active_tools_string(const Row& r) const;
// Replaces an edited row's name when it is empty, the reserved Primary name in any case, or
// one another row already carries: a plate stores the name and find_imex_mode() takes its
// first row, so the edited row yields. Returns whether it changed the name; the caller notifies.
bool fix_row_name(Row& r);
void notify();
// Update each row's reset bitmap to reflect current dirty state. Cached so we
// only swap the bitmap when state actually transitions — avoids flicker on
// every keystroke. Disabled buttons (no saved counterpart) keep their dot.
void refresh_reset_buttons();
// Reset row identified by `panel` (stable across add/remove) to the parent
// preset's value at the same index. Primary row's name stays sentinel-fixed —
// only tools and gcode are restored.
void reset_row_to_parent(wxPanel* panel);
// Rebuild the instruction text, color legend and column headers for the CURRENT grid.
// These depend on the grid shape -- the Span role is only offered on a multi-gantry printer,
// and the header spacing is derived from the column count -- so they cannot be built once in
// the constructor: adding a second gantry has to reveal Span without a save-and-reopen cycle.
void rebuild_info_and_header();
// One-line explanation per role, shown as a tooltip on that role's legend swatch and label.
// Keeps the per-role detail out of the instruction paragraph.
static wxString role_hint(ImexRole role);
wxBoxSizer* m_outer;
wxBoxSizer* m_rows_sizer;
wxPanel* m_info_panel = nullptr;
wxPanel* m_hdr_panel = nullptr;
std::vector<Row> m_rows;
int m_n_cols, m_n_rows;
int m_layout {0}; // 0=front-left, 1=front-right, 2=rear-left, 3=rear-right
bool m_clearing_rows {false}; // guards handlers against teardown-time events
ParentConfigLookup m_parent_lookup;
};
}} // namespace Slic3r::GUI
#endif // slic3r_IMEXModesCtrl_hpp_
+43
View File
@@ -5,6 +5,7 @@
#include "libslic3r/MTUtils.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/ModelArrange.hpp"
#include "libslic3r/IMEXArrange.hpp"
#include "slic3r/GUI/PartPlate.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
@@ -17,6 +18,7 @@
#include "libnest2d/common.hpp"
#include "libslic3r/Arrange.hpp"
#include <libslic3r/BoundingBox.hpp>
#include <utility>
#include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
@@ -536,6 +538,36 @@ void ArrangeJob::prepare_partplate() {
prepare_belt_regions(current_plate_index + 1);
}
// Snapshot each plate's IMEX zones on the main thread, so process() never has to touch
// PartPlate's IMEX cache; see the members' declaration for why that matters. The beds are the
// arranger's: arranging one plate packs into bed 0, and arranging all of them numbers the beds
// over the unlocked plates, as postprocess_bed_index_for_selected() maps them back.
void ArrangeJob::prepare_imex_zones()
{
m_imex = {};
PartPlateList& plate_list = m_plater->get_partplate_list();
std::vector<PartPlate*> plates;
if (only_on_partplate)
plates.push_back(plate_list.get_curr_plate());
else
for (int i = 0; i < plate_list.get_plate_count(); ++i)
if (!plate_list.get_plate(i)->is_locked())
plates.push_back(plate_list.get_plate(i));
for (PartPlate* plate : plates) {
std::optional<ImexArrangeZones> zones;
if (const std::optional<BoundingBoxf> pz = plate ? plate->imex_primary_zone() : std::nullopt) {
const Vec2d o = plate->get_origin().head<2>();
zones = ImexArrangeZones{BoundingBoxf(Vec2d(pz->min - o), Vec2d(pz->max - o)), {}};
for (const BoundingBoxf3& strip : plate->imex_collision_zones())
zones->collision_zones.emplace_back(Vec2d(strip.min.head<2>() - o), Vec2d(strip.max.head<2>() - o));
}
m_imex.beds.push_back(std::move(zones));
}
m_imex.adds_plates = !only_on_partplate;
m_imex.read_config(wxGetApp().preset_bundle->full_config());
}
//BBS: add partplate logic
void ArrangeJob::prepare()
{
@@ -564,6 +596,9 @@ void ArrangeJob::prepare()
prepare_partplate();
}
// After prepare_all(), which locks the plates it must leave alone.
prepare_imex_zones();
#if SAVE_ARRANGE_POLY
if (1)
@@ -645,6 +680,11 @@ void ArrangeJob::process(Ctl &ctl)
Points bedpts = get_shrink_bedpts(m_plater->config(),params);
// Keep the parts on IDEX/IQEX plates in their primary zones; see ImexArranger.
const ImexArranger imex(m_imex, params, bedpts);
bedpts = imex.bed_shape();
imex.add_keep_outs(m_unselected);
bool enable_wrapping = global_config.option<ConfigOptionBool>("enable_wrapping_detection")->value;
partplate_list.preprocess_exclude_areas(params.excluded_regions, enable_wrapping, 1, scale_(1));
@@ -669,7 +709,10 @@ void ArrangeJob::process(Ctl &ctl)
<<", bbox:"<<get_extents(item.poly).min.transpose()<<","<<get_extents(item.poly).max.transpose();
}
const ArrangePolygons before = imex.active() ? m_selected : ArrangePolygons();
arrangement::arrange(m_selected, m_unselected, bedpts, params);
if (!ctl.was_canceled())
imex.finish(m_selected, before, m_unselected, params);
// sort by item id
std::sort(m_selected.begin(), m_selected.end(), [](auto a, auto b) {return a.itemid < b.itemid; });
+19
View File
@@ -10,6 +10,8 @@
#include "Job.hpp"
#include "libslic3r/Arrange.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/IMEXArrange.hpp"
namespace Slic3r {
@@ -30,6 +32,22 @@ class ArrangeJob : public Job
std::map<int, ArrangePolygons> m_selected_groups; // groups of selected items for sequential printing
std::vector<int> m_uncompatible_plates; // plate indices with different printing sequence than global
// IMEX zone snapshot, taken on the main thread in prepare().
//
// process() runs on the worker thread (see Job::process). PartPlate::imex_primary_zone() is
// non-const and calls ensure_imex_zones(), which reads wxGetApp().preset_bundle -- main-thread
// GUI state -- on every call, and on a cache miss calls calc_imex_zones(), which destroys and
// rebuilds std::vector<GLModel> members. ~GLModel issues glDeleteBuffers/glDeleteVertexArrays,
// and no GL context is current on the worker, while the GUI thread may be painting those same
// vectors from GLCanvas3D::on_paint. (imex_collision_zones() is itself const and merely reads
// the member -- but it is only valid once imex_primary_zone() has warmed the cache, so it
// cannot be moved off the main thread on its own.) Snapshotting plain geometry here keeps
// every one of those touches on the main thread.
//
// The zones are stored already converted to plate-local coordinates, which is the space the
// arranger works in.
ImexArrangeInput m_imex;
arrangement::ArrangeParams params;
int current_plate_index = 0;
Polygon bed_poly;
@@ -51,6 +69,7 @@ class ArrangeJob : public Job
void prepare_partplate();
void prepare_wipe_tower();
void prepare_belt_regions(int num_plates);
void prepare_imex_zones();
ArrangePolygon prepare_arrange_polygon(void* instance);
+24
View File
@@ -3401,6 +3401,30 @@ void MainFrame::init_menubar_as_editor()
[this]() { return is_prepare_or_preview_tab(); },
[]() { return wxGetApp().show_plate_gridlines(); }, this);
// IDEX/IQEX: toggle the per-carriage toolhead representation that appears during
// G-code preview playback. Today this is the symbolic footprint box; if a future
// change adds true toolhead model support (cf. printer bed models) this same toggle
// governs that. Only relevant on Preview, and only meaningful when the active
// printer is IDEX/IQEX — gated to keep the menu uncluttered for everyone else.
append_menu_check_item(viewMenu, wxID_ANY, _L("Show IDEX/IQEX Toolhead"),
_L("Show the IDEX/IQEX toolhead representation around each active nozzle in the G-code preview."),
[this](wxCommandEvent&) {
const bool current = wxGetApp().app_config->get("show_imex_toolhead_boxes") != "false";
wxGetApp().app_config->set("show_imex_toolhead_boxes", current ? "false" : "true");
m_plater->get_current_canvas3D()->post_event(SimpleEvent(wxEVT_PAINT));
},
this,
[this]() {
if (m_tabpanel->GetSelectedPageName() != TAB_ID_PREVIEW) return false;
if (auto* pb = wxGetApp().preset_bundle) {
auto* o = pb->printers.get_edited_preset().config.option<ConfigOptionBool>("is_imex");
return o && o->value;
}
return false;
},
[]() { return wxGetApp().app_config->get("show_imex_toolhead_boxes") != "false"; },
this);
append_menu_item(
viewMenu, wxID_ANY, _L("Reset Window Layout"), _L("Reset to default window layout"),
[this](wxCommandEvent&) { m_plater->reset_window_layout(); }, "", this,
+20 -1
View File
@@ -214,6 +214,11 @@ wxPoint OG_CustomCtrl::get_pos(const Line& line, Field* field_in/* = nullptr*/)
if (line.widget) {
#ifndef DISABLE_BLINKING
h_pos += (line.has_undo_ui() ? 3 : 1) * blinking_button_width;
#else
// When blinking is disabled, widget lines still need to honour
// option_label_at_right the same way regular lines do below.
if (opt_group->option_label_at_right)
add_buttons_width(blinking_button_width);
#endif
for (auto child : line.widget_sizer->GetChildren())
@@ -740,6 +745,19 @@ void OG_CustomCtrl::CtrlLine::update_visibility(ConfigOptionMode mode)
return;
const std::vector<Option>& option_set = og_line.get_options();
// Widget-only lines (no options appended) are always visible unless explicitly hidden.
if (option_set.empty()) {
is_visible = og_line.toggle_visible;
if (og_line.near_label_widget_win)
og_line.near_label_widget_win->Show(is_visible);
if (og_line.widget_sizer)
og_line.widget_sizer->ShowItems(is_visible);
if (og_line.extra_widget_sizer)
og_line.extra_widget_sizer->ShowItems(is_visible);
correct_items_positions();
return;
}
const ConfigOptionMode& line_mode = option_set.front().opt.mode;
is_visible = og_line.toggle_visible && line_mode <= mode;
@@ -790,7 +808,8 @@ void OG_CustomCtrl::CtrlLine::render(wxDC& dc, wxCoord h_pos, wxCoord v_pos)
return;
}
Field* field = ctrl->opt_group->get_field(og_line.get_options().front().opt_id);
Field* field = og_line.get_options().empty() ? nullptr :
ctrl->opt_group->get_field(og_line.get_options().front().opt_id);
bool suppress_hyperlinks = false;
if (draw_just_act_buttons) {
File diff suppressed because it is too large Load Diff

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