Compare commits

..
Author SHA1 Message Date
Ian Chua 7ed4095b20 fix: clang-tidy 2026-10-15 16:06:50 +08:00
Ian Chua ee1cc1b227 Merge branch 'feat/ams-parsing' into feat/generic-extruders-and-ams 2026-10-15 16:05:00 +08:00
Ian Chua 46096416cd Merge branch 'main' into feat/generic-n-extruders 2026-10-15 16:04:30 +08:00
Ian Chua 74fe2b7996 Merge branch 'main' into feat/generic-n-extruders 2026-10-15 14:45:26 +08:00
Ian Chua 38f649fc00 Merge branch 'main' into feat/ams-parsing 2026-10-15 14:42:59 +08:00
peachismomo 23f6771dcb fix: parse AFC lane data and non-bambu ams tray indices correctly 2026-10-11 20:11:47 +08:00
Ian Chua aebc0a2979 fix: change ams type when parsing AFC mmu 2026-10-09 18:27:14 +08:00
Ian Chua 6f0ec60d21 feat: update happy hare parsing 2026-10-08 21:26:00 +08:00
Ian Chua 8e43a2ac13 fix: center external spool 2026-10-08 21:19:00 +08:00
Ian Chua 6b366ad8a9 fix: sync_ams_list should drop placeholder filaments 2026-10-08 20:42:11 +08:00
Ian Chua 7d8261668e fix: use extruder count for bbl and m_nozzle_book for non bbl 2026-10-08 20:29:47 +08:00
Ian Chua 00127beffa fix: use multi-nozzle AMS layout for non-Bambu printers at any extruder count 2026-10-08 19:26:43 +08:00
Lam Wei Lun 927eb9922c Map slot id directly 2026-10-08 18:37:09 +08:00
Ian Chua 3d62769664 feat: expand filament parsing in MoonrakerPrinterAgent 2026-10-08 17:46:01 +08:00
Ian Chua e5e30ee0e8 fix: harden generic N-extruder device model and status/AMS UI, add N-toolhead ExtruderImage sizing, tests, and HLSD 2026-10-08 17:39:44 +08:00
Ian Chua 7515fb480f fix: remove dead code 2026-10-08 15:56:20 +08:00
Ian Chua acc1d6fd21 Merge branch 'main' into feat/generic-n-extruders 2026-10-08 14:49:50 +08:00
Ian Chua 6ca0fc5ef1 fix: clang-tidy 2026-10-07 19:33:58 +08:00
Ian Chua bcf7d078cd Merge branch 'main' into feat/generic-n-extruders 2026-10-07 19:25:52 +08:00
Ian Chua 15fc96fca7 Merge branch 'main' into feat/generic-n-extruders 2026-10-07 19:25:35 +08:00
Ian Chua 1fffda0533 chore: add tests and HLSD 2026-10-07 19:05:06 +08:00
Ian Chua 33e26a127e fix: warn when toolhead count differs from the selected printer preset's nozzle_diameter count 2026-10-07 18:40:41 +08:00
Ian Chua 6dd0cb9eba fix: harden generic N-extruder device paths 2026-10-07 18:30:49 +08:00
Ian Chua 9bf0fc2a8e test: reset virtual trays in the generic AMS tray index map case 2026-10-06 20:53:46 +08:00
Ian Chua 5bf6db95b3 fix: clang-tidy 2026-10-06 19:56:53 +08:00
Ian Chua 0feb87b417 feat: generic N extruders, multiple AMS and AMS slots 2026-10-06 19:22:10 +08:00
90 changed files with 4972 additions and 34809 deletions
+199
View File
@@ -0,0 +1,199 @@
# Multi-extruder and multi-AMS device control
`DeviceManager`/`MachineObject` model the toolheads, AMS units, and filament slots of a connected
printer and drive the device-control UI from that model. The model represents the counts as
device-reported runtime values rather than compile-time caps, so a printer with an arbitrary number
of toolheads, several AMS units, and a non-uniform number of slots per unit is handled by the same
code paths. The printer agent is responsible for translating its printer's state into the shared
JSON dialect this model consumes; the agent boundary is described in
[printer-agent.md](printer-agent.md), and this document describes the dialect and the model.
## Model layout
`MachineObject` owns the device subsystems that this document covers:
- `DevExtderSystem` — the toolheads (extruders) and their per-toolhead filament/temperature state.
- `DevFilaSystem` — the AMS units (`DevAms`) and their trays (`DevAmsTray`).
- `DevNozzleSystem` — the nozzles and, on rack printers, `DevNozzleRack`.
- `DevFilaSwitch` — a filament switch that can feed several AMS units to more than one extruder.
Each subsystem exposes its own count — `DevExtderSystem::GetTotalExtderCount()`,
`DevFilaSystem::GetAmsCount()`, `DevAms::GetSlotCount()` — and the rest of the application derives
its layout from those values rather than from fixed assumptions.
## Wire dialect
The dialect is the Bambu-shaped JSON that `DeviceManager` and `MachineObject` already consume. An
agent whose printer speaks something else is expected to translate into it; the agent is not free
to invent a new shape. Two representations coexist for extruders, and one for AMS/slots.
### Extruders
The generic representation is `print.device.extruder`, parsed by
`ExtderSystemParser::ParseV2_0` (`DevExtruderSystem.cpp`). It has a packed `state` integer and an
`info` array with one object per toolhead:
```text
device.extruder.state packed integer
bits 0..3 total extruder count
bits 4..7 current extruder id
bits 8..11 target extruder id
bits 12..14 switch state
bits 15..18 currently loading extruder id
bit 19 busy for loading
device.extruder.info[] one entry per toolhead
id extruder id
filam_bak array of backup-group bits
info bit 1 has filament, bit 2 buffer has filament, bit 3 nozzle present
temp bits 0..15 current temperature, bits 16..31 target temperature
spre slot_id bits 0..7, ams_id bits 8..15 (previous)
snow same packing (current)
star same packing (target)
stat bit 0..15 AMS status, bits 16..31 RFID status
hnow current nozzle id
```
The legacy single/dual representation is the pair of scalar fields `nozzle_temper` /
`nozzle_target_temper`, plus the `print.ams.tray_tar` / `tray_now` tray pointer, parsed by
`ExtderSystemParser::ParseV1_0`. That parser is a fallback and only fills the main extruder when the
reported toolhead count is one.
Physical extruder ids are fixed (`MAIN_EXTRUDER_ID = 0`, `DEPUTY_EXTRUDER_ID = 1`). Bambu printers
present the two toolheads as a left/right pair and invert the physical-to-logical mapping; generic
printers use the toolhead id itself as the logical id. `DevNozzle::GetLogicExtruderId()` /
`GetExtruderId()` centralise that distinction.
### AMS units and slots
AMS units arrive as `print.ams.ams[]` and are parsed by `DevFilaSystemParser::ParseV1_0`. Each unit
carries an `id` and an `info` bitfield:
```text
info bits 0..3 type: 0 dummy, 1 AMS, 2 AMS-Lite, 3 N3F, 4 N3S, 5 AMS-Lite-mixed (N9)
info bits 8..11 bound extruder id (0xE marks a switch-bound unit)
info bits 24..27 switch inlet when the unit is switch-bound
```
`DevAmsType` (`DevDefs.h`) enumerates the resulting unit kinds: `EXT_SPOOL`, `AMS`, `AMS_LITE`,
`N3F`, `N3S`, and the `AMS_LITE_MIXED` N9 variant. `AMS_LITE_MIXED` reports itself as `AMS_LITE` to
generic callers; code that needs the distinction uses `IsAmsLiteMixed()`.
A unit whose bound-extruder nibble is `0xE` is switch-bound: it is kept only when a `DevFilaSwitch`
is installed, in which case its extruder id is pinned to the main extruder and its binding set
becomes `{0, 1}`; otherwise the unit is dropped from the AMS list.
Each unit owns a map of `DevAmsTray` keyed by the tray id from the payload. The slot count is a
property of the unit kind and the reported trays:
- `AMS`, `AMS_LITE`, and `N3F` report four slots on Bambu printers, matching the firmware contract;
- `N3S` reports one slot;
- `EXT_SPOOL` and any non-Bambu unit report the number of trays actually present.
The tray ids are the payload's slot indices; they are expected to be unique, 0-based, and contiguous
within a unit, because the generic tray-index mapping below is cumulative.
Bulk/extended spools are carried by `print.vir_slot`, which `DeviceManager` parses into
`MachineObject::vt_slot`. Two ids are fixed: `VIRTUAL_TRAY_MAIN_ID = 255` (extruder 0) and
`VIRTUAL_TRAY_DEPUTY_ID = 254` (extruder 1); the id convention is `vt_id = 255 - extruder_id`, so
further extruders use 253, 252, … . The `vir_slot` parser recognises the 255/254 pair; the agent
filament descriptor path `DevFilaSystemParser::ParseAgentFilament` is the entry that stores a fuller
descending set in `vt_slot`.
## Mapping
Several subsystems relate AMS units, trays, and extruders, and more than one place derives a tray
index. The Bambu mapping path (`DevMappingUtil`) computes `ams_id * 4 + slot_id` and knows only the
Bambu unit kinds; `DevFilaSystem` provides the device model's own mapping below. The two agree for
Bambu units and diverge for a non-Bambu unit whose slot count is not four.
### AMS id to extruder id
`DevFilaSystem::GetExtruderIdByAmsId()` resolves an AMS id to the extruder it feeds:
- an AMS unit returns its bound extruder (`DevAms::GetExtruderId()`);
- a virtual tray returns `VIRTUAL_TRAY_MAIN_ID - vt_id`;
- Bambu's virtual AMS ids map to the main/deputy extruder.
A unit may be bound to more than one extruder when a `DevFilaSwitch` feeds it; the binding set
(`DevAms::m_binded_extruder_set`) and switch inlet are read by switch-aware code while the
single-extruder id stays pinned for legacy callers.
### Tray index
`DevFilaSystem::GetTrayIndexMap()` maps a global tray index to an `(ams_id, slot_id)` pair and is
used for backup-slot translation, calibration addressing, and the mapping popups.
- Bambu AMS: `ams_id * 4 + slot_id`; N3S: the AMS id; AMS-Lite-mixed (N9): `24 + slot_id`.
- Generic (non-Bambu): a running lane index — the count of trays in preceding units plus the slot
id. This depends on each unit's tray ids being 0-based and contiguous.
### Virtual-slot recognition
`devPrinterUtil::IsVirtualSlot()` decides whether an id denotes a virtual tray. On Bambu printers it
is the fixed pair `{255, 254}`. On generic printers it is the descending range
`[255 - count + 1, 255]`, where `count` is the selected printer preset's extruder count. The preset
is expected to match the connected device; when the two disagree the virtual range is wrong, which
is why a mismatch is reported rather than silently accepted.
## Device-control UI
The device-control widgets follow the same device-reported counts.
- `AMSModel` (`AMSItem.hpp`) mirrors `DevAmsType` numerically: `EXT_AMS`, `GENERIC_AMS`, `AMS_LITE`,
`N3F_AMS`, `N3S_AMS`. A plain `AMS` maps to `GENERIC_AMS`.
- On non-Bambu printers `use_generic_ams_layout()` routes `GENERIC_AMS` units through a variable-lane
rendering that draws one lane per reported tray; Bambu keeps its fixed four-slot rendering.
- `AMSControl` picks its layout by toolhead count, not vendor: `CreateAmsSingleNozzle` for one
toolhead, `CreateAmsDoubleNozzle` for two, and `CreateAmsMultiNozzle` (one preview and one AMS
simplebook per toolhead) for three or more. A two-toolhead generic printer therefore keeps the
legacy left/right presentation; that is deliberate.
- The status panel's `ExtruderImage` renders one per-toolhead state per nozzle and is resized to
`GetTotalExtderCount()`. Non-Bambu printers choose the active toolhead through
`m_generic_nozzle_selector` (shown when more than one toolhead exists); Bambu keeps its left/right
`m_nozzle_btn_panel`. Nozzle temperature controls are created on demand per toolhead.
## Constraints
- **Single dialect.** The agent translates its protocol into this JSON; the model does not learn
vendor protocols. Extending the model with a new unit kind or field means extending the dialect.
- **Agent-produced counts.** Toolhead, AMS, and slot counts come from the device payload. Nothing
caps them at two toolheads or four slots except the Bambu contract described above.
- **Extruder dialect invariants.** `device.extruder.state`'s count field must equal the length of
`info[]`, and `info[]` must be ordered by extruder id with `id` equal to the array index: the
parser places toolheads by array position while `GetExtderById()` indexes by id.
- **One filament road per toolhead.** Each toolhead renders a single AMS filament road rather than an
independent per-toolhead road state. A generic scaffold that allowed the latter was removed as
unused; the one-extruder-per-toolhead road is the intended model.
- **Preset/device agreement.** The generic layout and virtual-slot range derive their count from the
selected printer preset. A printer preset must therefore declare the same number of
`nozzle_diameter` entries as the device has toolheads. A mismatch is surfaced only as a log warning
(`StatusPanel::check_extruder_count_mismatch()`), deliberately log-only, because the sidebar and
extruder logic would otherwise silently disagree.
- **Bambu compatibility.** Bambu-specific presentation (left/right pair, fixed 4-slot units, fixed
virtual ids, physical/logical inversion) is preserved behind the vendor check. Generic paths do
not alter it.
- **Tray-index invariant.** The generic tray-index map is cumulative, so payload tray ids must be
unique, 0-based, and contiguous per unit. A malformed id is a producer error.
- **Protocol markers.** The dialect has no neutral equivalent for some Bambu state, so agents that
lack it send empty `print.cfg`, `print.fun`, `print.aux`, and `print.stat`. Their presence selects
the new-protocol parse path; they are compatibility markers, not feature switches.
## Main implementation locations
- [`DevExtruderSystem`](../../src/slic3r/GUI/DeviceCore/DevExtruderSystem.h) — toolhead model and the
`device.extruder` parser
- [`DevFilaSystem`](../../src/slic3r/GUI/DeviceCore/DevFilaSystem.h) — AMS/tray model, the AMS parser,
and the mapping helpers
- [`DevDefs.h`](../../src/slic3r/GUI/DeviceCore/DevDefs.h) — `DevAmsType`, extruder ids, and virtual
tray ids
- [`DeviceManager.cpp`](../../src/slic3r/GUI/DeviceManager.cpp) — message dispatch, `vir_slot`
parsing, and device state assembly
- [`AMSControl.cpp`](../../src/slic3r/GUI/Widgets/AMSControl.cpp) — per-toolhead AMS panes
(`CreateAmsMultiNozzle`) and the generic layout
- [`AMSItem.hpp`](../../src/slic3r/GUI/Widgets/AMSItem.hpp) — `AMSModel` and the variable-lane AMS
rendering
- [`StatusPanel.cpp`](../../src/slic3r/GUI/StatusPanel.cpp) — `ExtruderImage`, the toolhead selector,
and per-toolhead temperature controls
- [`MoonrakerPrinterAgent.cpp`](../../src/slic3r/Utils/MoonrakerPrinterAgent.cpp) — producer example
(AMS payload and the `device.extruder` encoding)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

@@ -592,10 +592,6 @@
"name": "SUNLU PETG @base",
"sub_path": "filament/SUNLU/SUNLU PETG @base.json"
},
{
"name": "School House Spools PETG @base",
"sub_path": "filament/SchoolHouseSpools/School House Spools PETG @base.json"
},
{
"name": "eSUN PETG @base",
"sub_path": "filament/eSUN/eSUN PETG @base.json"
@@ -1100,10 +1096,6 @@
"name": "SUNLU Wood PLA @base",
"sub_path": "filament/SUNLU/SUNLU Wood PLA @base.json"
},
{
"name": "School House Spools PLA @base",
"sub_path": "filament/SchoolHouseSpools/School House Spools PLA @base.json"
},
{
"name": "Valment PLA @base",
"sub_path": "filament/Valment/Valment PLA @base.json"
@@ -2268,10 +2260,6 @@
"name": "SUNLU PETG @System",
"sub_path": "filament/SUNLU/SUNLU PETG @System.json"
},
{
"name": "School House Spools PETG @System",
"sub_path": "filament/SchoolHouseSpools/School House Spools PETG @System.json"
},
{
"name": "eSUN PETG @System",
"sub_path": "filament/eSUN/eSUN PETG @System.json"
@@ -3824,10 +3812,6 @@
"name": "SUNLU Wood PLA @System",
"sub_path": "filament/SUNLU/SUNLU Wood PLA @System.json"
},
{
"name": "School House Spools PLA @System",
"sub_path": "filament/SchoolHouseSpools/School House Spools PLA @System.json"
},
{
"name": "Valment PLA @System",
"sub_path": "filament/Valment/Valment PLA @System.json"
@@ -1,18 +0,0 @@
{
"type": "filament",
"name": "School House Spools PETG @System",
"inherits": "School House Spools PETG @base",
"from": "system",
"setting_id": "RSu2orfhR8ktT36h",
"instantiation": "true",
"filament_max_volumetric_speed": [
"13"
],
"filament_long_retractions_when_cut": [
"1"
],
"filament_retraction_distances_when_cut": [
"18"
],
"compatible_printers": []
}
@@ -1,80 +0,0 @@
{
"type": "filament",
"name": "School House Spools PETG @base",
"inherits": "fdm_filament_pet",
"from": "system",
"filament_id": "OFnaCGQ9",
"instantiation": "false",
"cool_plate_temp": [
"0"
],
"cool_plate_temp_initial_layer": [
"0"
],
"eng_plate_temp": [
"70"
],
"eng_plate_temp_initial_layer": [
"70"
],
"fan_cooling_layer_time": [
"30"
],
"fan_max_speed": [
"40"
],
"fan_min_speed": [
"10"
],
"filament_cost": [
"16.00"
],
"filament_density": [
"1.25"
],
"filament_flow_ratio": [
"0.95"
],
"filament_max_volumetric_speed": [
"8"
],
"filament_multitool_ramming_flow": [
"8"
],
"filament_vendor": [
"School House Spools"
],
"hot_plate_temp": [
"70"
],
"hot_plate_temp_initial_layer": [
"70"
],
"nozzle_temperature": [
"252"
],
"nozzle_temperature_initial_layer": [
"252"
],
"nozzle_temperature_range_high": [
"270"
],
"nozzle_temperature_range_low": [
"230"
],
"overhang_fan_speed": [
"90"
],
"overhang_fan_threshold": [
"10%"
],
"slow_down_layer_time": [
"12"
],
"textured_plate_temp": [
"70"
],
"textured_plate_temp_initial_layer": [
"70"
]
}
@@ -1,18 +0,0 @@
{
"type": "filament",
"name": "School House Spools PLA @System",
"inherits": "School House Spools PLA @base",
"from": "system",
"setting_id": "3rHJk6SVAvFzyHI0",
"instantiation": "true",
"filament_max_volumetric_speed": [
"21"
],
"filament_long_retractions_when_cut": [
"1"
],
"filament_retraction_distances_when_cut": [
"18"
],
"compatible_printers": []
}
@@ -1,35 +0,0 @@
{
"type": "filament",
"name": "School House Spools PLA @base",
"inherits": "fdm_filament_pla",
"from": "system",
"filament_id": "OFHzwmT9",
"instantiation": "false",
"filament_cost": [
"17.00"
],
"filament_density": [
"1.26"
],
"filament_flow_ratio": [
"0.98"
],
"filament_max_volumetric_speed": [
"12"
],
"filament_vendor": [
"School House Spools"
],
"filament_scarf_seam_type": [
"none"
],
"filament_scarf_height": [
"10%"
],
"filament_scarf_gap": [
"0%"
],
"filament_scarf_length": [
"10"
]
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "RatRig",
"version": "02.04.00.07",
"version": "02.04.00.06",
"force_update": "0",
"description": "RatRig configurations",
"machine_model_list": [
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -107,7 +107,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -107,7 +107,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -107,7 +107,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -106,7 +106,7 @@
"z_hop": [
"0.2"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single] TOTAL_LAYER_COUNT={total_layer_count} X0={first_layer_print_min[0]} Y0={first_layer_print_min[1]} X1={first_layer_print_max[0]} Y1={first_layer_print_max[1]}",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n_ON_LAYER_CHANGE LAYER={layer_num + 1}",
"change_filament_gcode": "M600",
@@ -131,7 +131,7 @@
"bed_exclude_area": [
"0x0"
],
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] EXTRUDER_OTHER_LAYER_TEMP=[nozzle_temperature] BED_TEMP=[bed_temperature_initial_layer_single]",
"machine_start_gcode": "START_PRINT EXTRUDER_TEMP=[nozzle_temperature_initial_layer] BED_TEMP=[bed_temperature_initial_layer_single]",
"machine_end_gcode": "END_PRINT",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n",
@@ -36,11 +36,6 @@ uniform int palette_b[64];
uniform float prefer_pure_de; // PREFER_PURE_DE: how much better than a single filament a mix must be
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
uniform bool has_color_tex;
// False means the texture is a single placement, not a repeat. The sampler's wrap mode can express
// that for the *height* - a zero border reads as no displacement - but not for colour, because a
// zero border is indistinguishable from the image genuinely being black there. So the uv is tested
// instead, which is what sample_layer_color() does on the CPU side.
uniform bool tile_enabled;
uniform bool volume_mirrored;
uniform mat4 view_model_matrix;
@@ -50,14 +45,6 @@ uniform sampler2D height_tex;
uniform vec2 height_tex_texel;
uniform float depth_mm;
uniform float tiling_scale;
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - the square tile
// every layer had before the two axes could differ - which is also what layer_tile_mm() does on the
// CPU side, so an unset uniform behaves exactly as this shader always did.
uniform float tiling_scale_v;
// Mirror the texture along one axis of the tile. Both default to false, which GLSL guarantees for an
// uninitialised bool uniform, so a shader whose caller has not set them behaves exactly as before.
uniform bool mirror_u;
uniform bool mirror_v;
// Height map width / height. Scales the v axis so a non-square image keeps its proportions
// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform().
uniform float tex_aspect;
@@ -67,12 +54,6 @@ uniform bool invert;
uniform float midlevel; // the height that means "don't move"; needed by the parallax step
uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor)
uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin
// The placement's rotation and its inverse, so this shader turns a world position into the texture
// frame exactly as texture_displacement_bake_frame() does. Both are passed rather than one
// transposed here, because GLSL 110 has no transpose(). Identity leaves the old world-aligned
// behaviour, which is what an unset uniform gives.
uniform mat3 tex_rot; // world -> texture frame
uniform mat3 tex_rot_inv; // texture frame -> world
uniform bool use_vertex_uv;
// 2x3 affine (lin = (m00, m01, m10, m11), tr = (m02, m12)) applied to the dragged island's uv; see the
// 140 variant. Identity when nothing is dragged.
@@ -181,29 +162,13 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
}
}
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
vec2 tile_mm()
{
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
}
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
vec2 mirror_sign()
{
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
}
vec2 project_uv(vec3 p, vec3 n)
{
vec2 planar = projection_raw(p, n);
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
// the same division, in the tile's own frame.
r *= mirror_sign() / tile_mm();
// After the rotation, so the rotation stays a rotation rather than becoming a shear.
r.y *= tex_aspect;
return r + uv_offset;
@@ -272,12 +237,9 @@ void main()
// World millimetres throughout, like the bake - see the 140 variant.
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor); // the frame the texture is projected in, as the bake does
vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does
if (volume_mirrored)
triangle_normal = -triangle_normal;
// The same normal in that frame. triangle_normal itself stays in world, because the lighting at
// the end of this shader needs it there.
vec3 tex_normal = tex_rot * triangle_normal;
// Where the colour is read from. Both branches below already compute the uv this fragment's
// *height* came from - including the parallax-marched one on the triplanar path - and the colour
@@ -308,14 +270,14 @@ void main()
} else if (weight > 0.0) {
vec3 t, b;
vec2 units_per_mm;
projection_axes(tex_pos, tex_normal, t, b, units_per_mm);
projection_axes(tex_pos, triangle_normal, t, b, units_per_mm);
// Parallax occlusion mapping: march the view ray through the height shell and shade at the
// first point where it drops below the displaced surface (see header).
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
vec3 view_dir = normalize(tex_rot * eye_model_pos - tex_pos);
float v_dot_n = dot(view_dir, tex_normal);
vec2 uv = project_uv(tex_pos, tex_normal);
vec3 view_dir = normalize(eye_model_pos - tex_pos);
float v_dot_n = dot(view_dir, triangle_normal);
vec2 uv = project_uv(tex_pos, triangle_normal);
// The shell the displaced surface lives inside, as signed heights along the normal. Taken from
// both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel,
@@ -327,18 +289,18 @@ void main()
// How far, in mm, sweeping the ray across the shell slides the sample point sideways. Below half
// a texel there is no parallax to find and the march would be pure cost - which is the common
// case of looking straight down at a surface.
float sweep = length(view_dir - tex_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
if (v_dot_n > 0.05 && sweep > 0.5 * min(tile_mm().x, tile_mm().y) * height_tex_texel.x) {
float sweep = length(view_dir - triangle_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
if (v_dot_n > 0.05 && sweep > 0.5 * tiling_scale * height_tex_texel.x) {
// A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the
// undisplaced surface. Start at the top of the shell, where the ray is outside the surface
// by construction, and step inward; the crossing is what this pixel actually sees.
float s = h_hi / v_dot_n;
float ds = (h_hi - h_lo) / (v_dot_n * float(PARALLAX_STEPS));
vec2 prev_uv = project_uv(tex_pos + view_dir * s, tex_normal);
vec2 prev_uv = project_uv(tex_pos + view_dir * s, triangle_normal);
float prev_gap = h_hi - amp * (H_AT(prev_uv) - midlevel); // >= 0 by construction
for (int i = 0; i < PARALLAX_STEPS; ++i) {
s -= ds;
vec2 cur_uv = project_uv(tex_pos + view_dir * s, tex_normal);
vec2 cur_uv = project_uv(tex_pos + view_dir * s, triangle_normal);
float gap = s * v_dot_n - amp * (H_AT(cur_uv) - midlevel);
if (gap <= 0.0) {
// Crossed between the last two samples - interpolating the hit is what stops it
@@ -360,23 +322,18 @@ void main()
float hU = texture2D(height_tex, uv + vec2(0.0, height_tex_texel.y)).r;
vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y));
vec2 tile = tile_mm();
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0);
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
// One uv unit is tile.x mm along u but tile.y / tex_aspect mm along v, so each component of
// the uv-space gradient carries its own factor before being rotated back into t/b.
// Each component carries its own extent and its own mirror sign, both of which project_uv()
// applied on the way in; without the sign a mirrored layer would light as though it were not.
vec2 ms = mirror_sign();
vec2 g = vec2(dh_duv.x * ms.x / tile.x, dh_duv.y * tex_aspect * ms.y / tile.y);
// One uv unit is tiling_scale mm along u but tiling_scale / tex_aspect mm along v, so the v
// component of the gradient carries the extra factor before being rotated back into t/b.
vec2 g = vec2(dh_duv.x, dh_duv.y * tex_aspect);
// ...and back out of raw-projection units into millimetres along t / b; see the 140 variant.
vec2 slope = amplitude * vec2(g.x * cs + g.y * sn, -g.x * sn + g.y * cs) * units_per_mm;
// t and b came out of the texture frame, so the gradient is in it too and has to come back
// to world before it perturbs the normal the lighting uses.
vec3 gradient = tex_rot_inv * (slope.x * t + slope.y * b);
vec3 gradient = slope.x * t + slope.y * b;
gradient -= triangle_normal * dot(triangle_normal, gradient);
triangle_normal = normalize(triangle_normal - gradient);
}
@@ -396,9 +353,7 @@ void main()
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
// the same material under the same light, and the relief this preview exists to show is unaffected.
vec3 albedo = uniform_color.rgb;
bool uv_covered = tile_enabled ||
(all(greaterThanEqual(color_uv, vec2(0.0))) && all(lessThan(color_uv, vec2(1.0))));
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0 && uv_covered)
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
albedo = palette_rgb[nearest_palette_entry(texture2D(color_tex, color_uv).rgb)];
gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
}
@@ -16,16 +16,7 @@ uniform bool volume_mirrored;
uniform int mode;
uniform float checker_freq;
uniform float tiling_scale;
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - see
// layer_tile_mm() on the CPU side, so an unset uniform behaves exactly as this shader always did.
uniform float tiling_scale_v;
// Mirror along one tile axis; false by default, so an unset uniform is the old behaviour.
uniform bool mirror_u;
uniform bool mirror_v;
uniform vec3 tex_anchor; // the volume's origin in world space
// The placement's rotation, so this overlay projects the same frame the bake does. Identity, which
// is what an unset uniform gives, leaves the old world-aligned behaviour.
uniform mat3 tex_rot; // world -> texture frame
uniform float rotation_rad;
uniform vec2 uv_offset;
uniform bool use_vertex_uv;
@@ -88,30 +79,13 @@ vec2 projection_raw(vec3 p, vec3 n)
return vec2(box_face_sign(n.z) * p.x, -p.y);
}
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
vec2 tile_mm()
{
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
}
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
vec2 mirror_sign()
{
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
}
vec2 project_uv(vec3 p, vec3 n)
{
vec2 planar = projection_raw(p, n);
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
// the same division, in the tile's own frame.
r *= mirror_sign() / tile_mm();
r.y *= tex_aspect; // after the rotation, so the rotation stays a rotation rather than a shear
return r + uv_offset;
}
@@ -131,7 +105,7 @@ void main()
// World space anchored at the volume's origin, like the bake and the shaded preview.
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor);
vec3 tex_pos = world_pos.xyz - tex_anchor;
if (volume_mirrored)
triangle_normal = -triangle_normal;
@@ -139,7 +113,7 @@ void main()
if (mode == 1) {
base = heatmap(distortion);
} else {
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, tex_rot * triangle_normal);
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, triangle_normal);
vec2 c = floor(uv * checker_freq);
float check = mod(c.x + c.y, 2.0);
base = (check < 0.5) ? vec3(0.22, 0.23, 0.26) : vec3(0.82, 0.83, 0.86);
@@ -95,11 +95,6 @@ uniform int palette_b[64];
uniform float prefer_pure_de; // PREFER_PURE_DE: how much better than a single filament a mix must be
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
uniform bool has_color_tex;
// False means the texture is a single placement, not a repeat. The sampler's wrap mode can express
// that for the *height* - a zero border reads as no displacement - but not for colour, because a
// zero border is indistinguishable from the image genuinely being black there. So the uv is tested
// instead, which is what sample_layer_color() does on the CPU side.
uniform bool tile_enabled;
uniform bool volume_mirrored;
uniform mat4 view_model_matrix;
@@ -109,14 +104,6 @@ uniform sampler2D height_tex;
uniform vec2 height_tex_texel; // (1/width, 1/height) of height_tex
uniform float depth_mm;
uniform float tiling_scale;
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - the square tile
// every layer had before the two axes could differ - which is also what layer_tile_mm() does on the
// CPU side, so an unset uniform behaves exactly as this shader always did.
uniform float tiling_scale_v;
// Mirror the texture along one axis of the tile. Both default to false, which GLSL guarantees for an
// uninitialised bool uniform, so a shader whose caller has not set them behaves exactly as before.
uniform bool mirror_u;
uniform bool mirror_v;
// Height map width / height. Scales the v axis so a non-square image keeps its proportions
// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform().
uniform float tex_aspect;
@@ -126,12 +113,6 @@ uniform bool invert;
uniform float midlevel; // the height that means "don't move"; needed by the parallax step
uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor)
uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin
// The placement's rotation and its inverse, so this shader turns a world position into the texture
// frame exactly as texture_displacement_bake_frame() does. Both are passed rather than one
// transposed here, because GLSL 110 has no transpose(). Identity leaves the old world-aligned
// behaviour, which is what an unset uniform gives.
uniform mat3 tex_rot; // world -> texture frame
uniform mat3 tex_rot_inv; // texture frame -> world
uniform bool use_vertex_uv; // true: sample at vertex_uv with a derived tangent frame (LSCM)
// A 2x3 affine (columns packed as lin = (m00, m01, m10, m11), tr = (m02, m12)) applied to the uv of
// the island currently being dragged in the UV editor (island_active > 0.5). Identity when nothing is
@@ -247,29 +228,13 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
}
}
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
vec2 tile_mm()
{
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
}
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
vec2 mirror_sign()
{
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
}
vec2 project_uv(vec3 p, vec3 n)
{
vec2 planar = projection_raw(p, n);
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
// the same division, in the tile's own frame.
r *= mirror_sign() / tile_mm();
// After the rotation, so the rotation stays a rotation rather than becoming a shear.
r.y *= tex_aspect;
return r + uv_offset;
@@ -340,12 +305,9 @@ void main()
// printed part whatever the instance's scale or rotation, so the preview has to project from the
// world position and perturb the world normal.
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor); // the frame the texture is projected in, as the bake does
vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does
if (volume_mirrored)
triangle_normal = -triangle_normal;
// The same normal in that frame. triangle_normal itself stays in world, because the lighting at
// the end of this shader needs it there.
vec3 tex_normal = tex_rot * triangle_normal;
// Where the colour is read from. Both branches below already compute the uv this fragment's
// *height* came from - including the parallax-marched one on the triplanar path - and the colour
@@ -389,14 +351,14 @@ void main()
// closed-form uv here, unlike the LSCM case.
vec3 t, b;
vec2 units_per_mm;
projection_axes(tex_pos, tex_normal, t, b, units_per_mm);
projection_axes(tex_pos, triangle_normal, t, b, units_per_mm);
// Parallax occlusion mapping: march the view ray through the height shell and shade at the
// first point where it drops below the displaced surface (see header).
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
vec3 view_dir = normalize(tex_rot * eye_model_pos - tex_pos);
float v_dot_n = dot(view_dir, tex_normal);
vec2 uv = project_uv(tex_pos, tex_normal);
vec3 view_dir = normalize(eye_model_pos - tex_pos);
float v_dot_n = dot(view_dir, triangle_normal);
vec2 uv = project_uv(tex_pos, triangle_normal);
// The shell the displaced surface lives inside, as signed heights along the normal. Taken from
// both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel,
@@ -408,18 +370,18 @@ void main()
// How far, in mm, sweeping the ray across the shell slides the sample point sideways. Below half
// a texel there is no parallax to find and the march would be pure cost - which is the common
// case of looking straight down at a surface.
float sweep = length(view_dir - tex_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
if (v_dot_n > 0.05 && sweep > 0.5 * min(tile_mm().x, tile_mm().y) * height_tex_texel.x) {
float sweep = length(view_dir - triangle_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
if (v_dot_n > 0.05 && sweep > 0.5 * tiling_scale * height_tex_texel.x) {
// A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the
// undisplaced surface. Start at the top of the shell, where the ray is outside the surface
// by construction, and step inward; the crossing is what this pixel actually sees.
float s = h_hi / v_dot_n;
float ds = (h_hi - h_lo) / (v_dot_n * float(PARALLAX_STEPS));
vec2 prev_uv = project_uv(tex_pos + view_dir * s, tex_normal);
vec2 prev_uv = project_uv(tex_pos + view_dir * s, triangle_normal);
float prev_gap = h_hi - amp * (H_AT(prev_uv) - midlevel); // >= 0 by construction
for (int i = 0; i < PARALLAX_STEPS; ++i) {
s -= ds;
vec2 cur_uv = project_uv(tex_pos + view_dir * s, tex_normal);
vec2 cur_uv = project_uv(tex_pos + view_dir * s, triangle_normal);
float gap = s * v_dot_n - amp * (H_AT(cur_uv) - midlevel);
if (gap <= 0.0) {
// Crossed between the last two samples - interpolating the hit is what stops it
@@ -443,28 +405,23 @@ void main()
// Central difference, per uv unit (not per texel).
vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y));
// uv -> mm is the reciprocal of the tile extents, applied per component below, which is what
// turns the uv-space gradient into a real surface slope.
vec2 tile = tile_mm();
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
// uv -> mm is 1/tiling_scale for the triplanar projection, so this turns the uv-space
// gradient into a real surface slope.
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0);
// uv was rotated by project_uv() while t/b are the unrotated model axes, so rotate the
// gradient back into the axes' frame.
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
// One uv unit is tile.x mm along u but tile.y / tex_aspect mm along v, so each component of
// the uv-space gradient carries its own factor before being rotated back into t/b.
// Each component carries its own extent and its own mirror sign, both of which project_uv()
// applied on the way in; without the sign a mirrored layer would light as though it were not.
vec2 ms = mirror_sign();
vec2 g = vec2(dh_duv.x * ms.x / tile.x, dh_duv.y * tex_aspect * ms.y / tile.y);
// One uv unit is tiling_scale mm along u but tiling_scale / tex_aspect mm along v, so the v
// component of the gradient carries the extra factor before being rotated back into t/b.
vec2 g = vec2(dh_duv.x, dh_duv.y * tex_aspect);
// ...and back out of raw-projection units into millimetres of travel along t / b, which is a
// no-op except for the spherical longitude (see projection_axes()). After the inverse rotation,
// because units_per_mm is expressed in the t/b frame rather than in uv.
vec2 slope = amplitude * vec2(g.x * cs + g.y * sn, -g.x * sn + g.y * cs) * units_per_mm;
// t and b came out of the texture frame, so the gradient is in it too and has to come back
// to world before it perturbs the normal the lighting uses.
vec3 gradient = tex_rot_inv * (slope.x * t + slope.y * b);
vec3 gradient = slope.x * t + slope.y * b;
gradient -= triangle_normal * dot(triangle_normal, gradient);
triangle_normal = normalize(triangle_normal - gradient);
}
@@ -484,9 +441,7 @@ void main()
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
// the same material under the same light, and the relief this preview exists to show is unaffected.
vec3 albedo = uniform_color.rgb;
bool uv_covered = tile_enabled ||
(all(greaterThanEqual(color_uv, vec2(0.0))) && all(lessThan(color_uv, vec2(1.0))));
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0 && uv_covered)
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
albedo = palette_rgb[nearest_palette_entry(texture(color_tex, color_uv).rgb)];
out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
}
@@ -25,16 +25,7 @@ uniform bool volume_mirrored;
uniform int mode; // 0 checker, 1 distortion
uniform float checker_freq; // checker squares per uv unit (one uv unit == one texture tile)
uniform float tiling_scale;
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - see
// layer_tile_mm() on the CPU side, so an unset uniform behaves exactly as this shader always did.
uniform float tiling_scale_v;
// Mirror along one tile axis; false by default, so an unset uniform is the old behaviour.
uniform bool mirror_u;
uniform bool mirror_v;
uniform vec3 tex_anchor; // the volume's origin in world space
// The placement's rotation, so this overlay projects the same frame the bake does. Identity, which
// is what an unset uniform gives, leaves the old world-aligned behaviour.
uniform mat3 tex_rot; // world -> texture frame
uniform float rotation_rad;
uniform vec2 uv_offset;
uniform bool use_vertex_uv;
@@ -99,30 +90,13 @@ vec2 projection_raw(vec3 p, vec3 n)
return vec2(box_face_sign(n.z) * p.x, -p.y);
}
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
vec2 tile_mm()
{
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
}
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
vec2 mirror_sign()
{
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
}
vec2 project_uv(vec3 p, vec3 n)
{
vec2 planar = projection_raw(p, n);
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
// the same division, in the tile's own frame.
r *= mirror_sign() / tile_mm();
r.y *= tex_aspect; // after the rotation, so the rotation stays a rotation rather than a shear
return r + uv_offset;
}
@@ -143,7 +117,7 @@ void main()
// World space anchored at the volume's origin, like the bake and the shaded preview.
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor);
vec3 tex_pos = world_pos.xyz - tex_anchor;
if (volume_mirrored)
triangle_normal = -triangle_normal;
@@ -151,7 +125,7 @@ void main()
if (mode == 1) {
base = heatmap(distortion);
} else {
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, tex_rot * triangle_normal);
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, triangle_normal);
vec2 c = floor(uv * checker_freq);
float check = mod(c.x + c.y, 2.0);
// Two distinct greys, plus a faint tint on one set so orientation is readable at a glance.
+16 -8
View File
@@ -4607,6 +4607,16 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
maps.erase(j);
}
}
}
if (is_placeholder) {
// Orca: an empty AMS slot is a placeholder, not a filament - never synthesize a
// preset for it. Kept here only to keep index alignment with ams_infos; the sync
// drops it so no filament slot is created or retained for an empty tray.
ams_filament_presets.push_back("");
ams_filament_colors.push_back("");
ams_filament_color_types.push_back("");
ams_multi_color_filment.push_back({});
} else if (use_map) {
ams_filament_presets.push_back("Generic PLA");//for unknow matieral
auto default_unknown_color = "#CECECE";
ams_filament_colors.push_back(default_unknown_color);
@@ -4615,12 +4625,6 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
filament_multi_color.push_back(default_unknown_color);
}
ams_multi_color_filment.push_back(filament_multi_color);
} else if (is_placeholder) {
// Orca: push placeholders to keep index alignment with ams_infos
ams_filament_presets.push_back("");
ams_filament_colors.push_back("");
ams_filament_color_types.push_back("");
ams_multi_color_filment.push_back({});
}
continue;
}
@@ -4984,6 +4988,10 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
result_multi_colors.push_back(
i < ams_multi_color_filment.size() ? ams_multi_color_filment[i]
: std::vector<std::string>{ams_filament_colors[i]});
} else if (i < ams_infos.size() && ams_infos[i].is_placeholder) {
// Orca: an empty AMS slot is a placeholder, not a filament, so drop it instead of
// keeping or filling a slot for it. The device AMS panel still shows the empty tray.
continue;
} else if (i < exist_presets.size()) {
// Empty tray or beyond tray count: keep existing filament
result_colors.push_back(exist_colors[i]);
@@ -5008,8 +5016,8 @@ unsigned int PresetBundle::sync_ams_list(std::vector<std::pair<DynamicPrintConfi
filament_color_type->values = result_color_types;
this->filament_presets = result_presets;
ams_multi_color_filment = result_multi_colors;
filament_map->values.resize(total, 1);
filament_volume_map->values.resize(total, static_cast<int>(NozzleVolumeType::nvtStandard));
filament_map->values.resize(result_presets.size(), 1);
filament_volume_map->values.resize(result_presets.size(), static_cast<int>(NozzleVolumeType::nvtStandard));
} else {
// BBL: existing wholesale replace
filament_color->values = ams_filament_colors;
+5 -4
View File
@@ -858,10 +858,11 @@ void PrintObject::contour_z()
BOOST_LOG_TRIVIAL(debug) << "Contouring in parallel - start";
TriangleMesh mesh = this->m_model_object->raw_mesh();
// A PrintObject groups the instances that share their whole transformation but the XY shift: an
// instance rotated, scaled or mirrored differently is a PrintObject of its own. So the first
// instance stands for all of them.
const ModelInstance *inst = m_instances.front().model_instance;
if (m_model_object->instances.size() != 1) {
throw RuntimeError("ContourZ: unexpected number of instances");
}
ModelInstance *inst = m_model_object->instances.front();
Point center_offset = this->center_offset();
Geometry::Transformation trans = inst->get_transformation();
+26 -95
View File
@@ -1,7 +1,6 @@
#include "TextureDisplacement.hpp"
#include <Eigen/Core>
#include <Eigen/SVD>
#include <Eigen/Geometry>
#include <algorithm>
#include <array>
@@ -723,10 +722,7 @@ V2Resolution recommend_v2_resolution(const indexed_triangle_set
const DecodedHeightTexture &tex = decode_height_texture(layer);
if (tex.width <= 0)
continue;
// Texels are square in both axes (the aspect correction is what keeps them so), so the finer
// of the two tile extents sets the finest texel the refinement has to resolve.
const Vec2f tile = layer_tile_mm(layer);
const float texel = std::min(tile.x(), tile.y()) / float(tex.width);
const float texel = layer.tiling_scale / float(tex.width);
if (out.texel_mm <= 0.f || texel < out.texel_mm) {
out.texel_mm = texel;
out.pixels_per_edge = analyze_texture_detail(layer).pixels_per_edge;
@@ -2168,7 +2164,7 @@ namespace {
// of them was most of the per-sample arithmetic.
struct UVTransform
{
float inv_u, inv_v, cs, sn, aspect;
float scale, cs, sn, aspect;
Vec2f offset;
// `normalized_uv` drops the two steps that only mean something for a coordinate measured on the
@@ -2176,18 +2172,7 @@ struct UVTransform
// rotation and offset. See apply_uv_transform(); TextureProjectionMethod::ModelUV is what needs it.
UVTransform(const TextureDisplacementLayer &layer, float aspect_, bool normalized_uv = false)
{
// The mirror folds into the reciprocal: it is a negation of the same coordinate, so there is
// nothing to do per sample beyond carrying the sign here. A normalized uv has no millimetre
// tiling to divide by, but a mirror is still a mirror, so the sign applies either way.
const Vec2f sign = layer_mirror_sign(layer);
if (normalized_uv) {
inv_u = sign.x();
inv_v = sign.y();
} else {
const Vec2f tile = layer_tile_mm(layer);
inv_u = sign.x() / tile.x();
inv_v = sign.y() / tile.y();
}
scale = (normalized_uv || layer.tiling_scale <= 1e-6f) ? 1.f : (1.f / layer.tiling_scale);
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
cs = std::cos(rad);
sn = std::sin(rad);
@@ -2196,11 +2181,9 @@ struct UVTransform
}
Vec2f operator()(const Vec2f &planar) const
{
// See apply_uv_transform() for why both the tile extents and the aspect correction follow
// the rotation rather than preceding it.
Vec2f rotated(planar.x() * cs - planar.y() * sn, planar.x() * sn + planar.y() * cs);
rotated.x() *= inv_u;
rotated.y() *= inv_v;
const Vec2f scaled = planar * scale;
Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs);
// See apply_uv_transform() for why the aspect correction follows the rotation.
if (aspect > 0.f && aspect != 1.f)
rotated.y() *= aspect;
return rotated + offset;
@@ -2220,43 +2203,26 @@ inline Vec3f triplanar_weights(const Vec3f &normal)
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect, bool normalized_uv)
{
// A uv that is already normalized has no millimetre scale to divide by and no aspect to correct -
// see the header. UVTransform does the same, and the two must stay in step.
const float scale = (normalized_uv || layer.tiling_scale <= 1e-6f) ? 1.f : (1.f / layer.tiling_scale);
const Vec2f scaled = planar * scale;
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
const float cs = std::cos(rad);
const float sn = std::sin(rad);
Vec2f rotated(planar.x() * cs - planar.y() * sn, planar.x() * sn + planar.y() * cs);
// The tile extents. Dividing here rather than before the rotation is what makes a non-square
// tile a rotated *rectangle* on the surface: scaling the two axes of an already-rotated
// coordinate by different amounts would be a shear, so the rotation has to come first and the
// extents divide in the tile's own frame. With a square tile the two orders agree exactly,
// R being linear - which is why this is not a change in behaviour for any existing layer.
//
// The mirror rides along as a sign on the same division. It is in the tile's own frame too, so
// it mirrors along the tile's axes rather than the world's, and because the offset is what
// positions the tile, mirroring about the tile's middle is a matter of the offset the gizmo
// writes alongside it, not of anything here.
// A uv that is already normalized has no millimetre scale to divide by - see the header - but it
// can still be mirrored, so the sign is applied in both cases and only the extents are skipped.
// UVTransform does the same, and the two must stay in step.
const Vec2f sign = layer_mirror_sign(layer);
if (normalized_uv) {
rotated.x() *= sign.x();
rotated.y() *= sign.y();
} else {
const Vec2f tile = layer_tile_mm(layer);
rotated.x() *= sign.x() / tile.x();
rotated.y() *= sign.y() / tile.y();
}
Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs);
// Non-square textures. Without this the [0,1] square of uv covers the whole image whatever its
// proportions, so a 2:1 image is squeezed into one tile and every feature in it comes out half
// as wide as it should be. The tile is `layer_tile_mm().y() * height / width` mm along v, which
// is exactly what keeps texels square - so dividing v by that extent is the same as multiplying
// it by width / height. A square texture has aspect 1 and is untouched, which is why this
// changes nothing for the shipped library.
// proportions, so a 2:1 image is squeezed into a square tile and every feature in it comes out
// half as wide as it should be. `tiling_scale` is the tile's size along u; the tile is
// `tiling_scale * height / width` mm along v, which is exactly what keeps texels square - so
// dividing v by that extent is the same as multiplying it by width / height. A square texture has
// aspect 1 and is untouched, which is why this changes nothing for the shipped library.
//
// Applied after the rotation, like the extents, and for the same reason: scaling one axis of an
// already-rotated coordinate before turning it would be a shear.
// Applied after the rotation, not before: scaling one axis of an already-rotated coordinate is a
// shear, and doing it the other way round would make "Rotation" skew the pattern instead of
// turning it.
if (!normalized_uv && aspect > 0.f && aspect != 1.f)
rotated.y() *= aspect;
@@ -3600,36 +3566,13 @@ void texture_displacement_patch_frame(const indexed_triangle_set &patch, const s
(an.y() <= an.x() && an.y() <= an.z()) ? Vec3f::UnitY() : Vec3f::UnitZ();
}
Matrix3d texture_displacement_frame_rotation(const Transform3d &volume_to_world)
{
// Separated out by polar decomposition rather than by assuming the linear part is a clean
// rotation times a scale: a sheared or mirrored placement is legal here. A reflection is left
// behind with the scale on purpose, so a mirrored instance still makes the bake frame's
// determinant negative and the bake still knows to flip the normals it derives.
const Matrix3d linear = volume_to_world.linear();
Eigen::JacobiSVD<Matrix3d> svd(linear, Eigen::ComputeFullU | Eigen::ComputeFullV);
Matrix3d rotation = svd.matrixU() * svd.matrixV().transpose();
if (rotation.determinant() < 0.0) {
Matrix3d u = svd.matrixU();
u.col(2) *= -1.0;
rotation = u * svd.matrixV().transpose();
}
return rotation.transpose(); // world -> texture frame
}
Transform3d texture_displacement_bake_frame(const Transform3d &volume_to_world)
{
// The model's own orientation, world scale, and the origin moved to where the volume's origin
// sits. So the texture rides with the model however it is moved *and* however it is turned,
// while a tile is still `tiling_scale` printed millimetres whatever the instance's scale, and a
// single untiled stamp starts on the model rather than at the plate's corner.
//
// Orientation used to come from the world too, which meant turning a part on the plate slid the
// decoration across its surface. Turning a part is a print-time decision - clearance, supports,
// warping - and it should not be a design one.
Transform3d unrotate = Transform3d::Identity();
unrotate.linear() = texture_displacement_frame_rotation(volume_to_world);
return unrotate * Eigen::Translation3d(-volume_to_world.translation()) * volume_to_world;
// World orientation and scale, but the origin moved to where the volume's own origin sits: the
// texture then rides with the model when it is moved about the plate (and a single, untiled stamp
// starts on the model rather than at the plate's corner), while a tile is still `tiling_scale`
// printed millimetres whatever the instance's scale.
return Eigen::Translation3d(-volume_to_world.translation()) * volume_to_world;
}
Transform3d texture_displacement_volume_to_world(const ModelVolume &volume)
@@ -5840,13 +5783,7 @@ std::string texture_displacement_layers_to_json(const std::vector<TextureDisplac
j["depth_mm"] = l.depth_mm;
j["tiling_scale"] = l.tiling_scale;
// The v extent and the mirroring. Written unconditionally, including the <= 0 "same as u"
// sentinel, so a project says plainly that the tile was square rather than leaving a reader
// to infer it from a missing key.
j["tiling_scale_v"] = l.tiling_scale_v;
j["mirror_u"] = l.mirror_u;
j["mirror_v"] = l.mirror_v;
j["rotation_deg"] = l.rotation_deg;
j["rotation_deg"] = l.rotation_deg;
j["offset"] = vec2_to_json(l.offset);
j["invert"] = l.invert;
j["midlevel"] = l.midlevel;
@@ -5954,12 +5891,6 @@ bool texture_displacement_layers_from_json(const std::string
read(j, "depth_mm", l.depth_mm);
read(j, "tiling_scale", l.tiling_scale);
// Absent in a project written before the tile could be rectangular or mirrored, and
// then each keeps its default: -1 for the v extent, which layer_tile_mm() reads as
// "square", and false for the mirrors. So an older project loads as what it was.
read(j, "tiling_scale_v", l.tiling_scale_v);
read(j, "mirror_u", l.mirror_u);
read(j, "mirror_v", l.mirror_v);
read(j, "rotation_deg", l.rotation_deg);
if (const auto o = j.find("offset"); o != j.end())
l.offset = vec2_from_json(*o, l.offset);
+8 -47
View File
@@ -225,17 +225,7 @@ struct TextureDisplacementLayer
std::shared_ptr<std::vector<unsigned char>> image_data;
float depth_mm = 0.4f; // maximum displacement along the surface normal, in mm
float tiling_scale = 12.5f; // size of one texture tile along u, in mm
// Size of one tile along v, in mm. Anything <= 0 means "whatever tiling_scale is", which is both
// the default and what a layer created before the tile could be non-square meant - so an older
// undo snapshot still comes back as the square tile it was.
float tiling_scale_v = -1.f;
// Mirror the texture along one axis of the tile. Set by dragging a width or height handle back
// through the tile's middle, which is how a side handle behaves in an image editor: the extent
// cannot go negative, so the sign lives here instead. Both default to off, which is what every
// layer written before this meant.
bool mirror_u = false;
bool mirror_v = false;
float tiling_scale = 12.5f; // size of one texture tile, in mm
float rotation_deg = 0.f;
Vec2f offset = Vec2f::Zero();
bool invert = false;
@@ -372,8 +362,7 @@ struct TextureDisplacementLayer
static_cast<int>(tile_method), static_cast<int>(projection_method), lscm_seam_angle_deg, islands,
static_cast<int>(blend_mode), midlevel, island_padding_mm, lscm_seam_edges, view_project_right,
view_project_up, smoothing, edge_smoothing, edge_smoothing_amount, auto_connect_islands, island_groups,
lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled, tiling_scale_v,
mirror_u, mirror_v);
lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled);
}
template<class Archive> void load(Archive &ar)
{
@@ -385,7 +374,7 @@ struct TextureDisplacementLayer
tile_method_int, projection_method_int, lscm_seam_angle_deg, islands, blend_mode_int, midlevel,
island_padding_mm, lscm_seam_edges, view_project_right, view_project_up, smoothing, edge_smoothing,
edge_smoothing_amount, auto_connect_islands, island_groups, lscm_uv_overrides, view_project_projective,
view_project_matrix, color_enabled, tiling_scale_v, mirror_u, mirror_v);
view_project_matrix, color_enabled);
image_data = blob.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(blob.begin(), blob.end());
tile_method = static_cast<TextureTileMethod>(tile_method_int);
projection_method = static_cast<TextureProjectionMethod>(projection_method_int);
@@ -394,25 +383,6 @@ struct TextureDisplacementLayer
};
// The layer's tile extents in mm, u first. The v extent falls back to the u one whenever
// tiling_scale_v is unset, so "unset means square" is written down once and every consumer - the
// bake, the preview shaders, the UV pane and the placement gizmo - agrees on what a layer with no
// v extent means.
inline Vec2f layer_tile_mm(const TextureDisplacementLayer &layer)
{
const float su = (layer.tiling_scale > 1e-6f) ? layer.tiling_scale : 1.f;
return Vec2f(su, (layer.tiling_scale_v > 1e-6f) ? layer.tiling_scale_v : su);
}
// +1 or -1 per axis, for the layer's mirroring. A separate factor from the extents rather than a
// signed extent: the extents are a size the panel shows and the refinement measures texels against,
// and letting one of them go negative would mean every one of those places had to remember to take
// the magnitude.
inline Vec2f layer_mirror_sign(const TextureDisplacementLayer &layer)
{
return Vec2f(layer.mirror_u ? -1.f : 1.f, layer.mirror_v ? -1.f : 1.f);
}
// Settings that apply to the whole layer stack rather than to one layer, held per ModelVolume next
// to texture_displacement_layers and consumed by build_texture_displacement().
struct TextureDisplacementOptions
@@ -698,19 +668,14 @@ Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
// projection method, without going through project_texture_displacement_uv()'s own dispatch
// (which only knows how to compute the *analytic* methods from a single vertex + normal).
// `aspect` is the height map's width / height. It scales the v axis so a non-square image is not
// squeezed into a square tile: the tile is `layer_tile_mm().y() * height / width` mm along v, which
// keeps texels square. 1 (the default) is the square case and leaves the coordinate exactly as it
// always was.
//
// The tile extents divide *after* the rotation, not before. With one extent that made no difference
// (R is linear, so R(p / s) == R(p) / s), but with two it is what keeps a non-square tile a rotated
// rectangle on the surface instead of a sheared parallelogram. The layer's mirroring is a sign on
// that same division.
// squeezed into a square tile: `tiling_scale` is the tile's size along u, and the tile is
// `tiling_scale * height / width` mm along v, which keeps texels square. 1 (the default) is the
// square case and leaves the coordinate exactly as it always was.
//
// `normalized_uv` says the coordinate is already a uv rather than a millimetre measurement on the
// model - which is what TextureProjectionMethod::ModelUV hands in. Then neither the tile extents (mm
// model - which is what TextureProjectionMethod::ModelUV hands in. Then neither the tiling scale (mm
// per tile is meaningless for it) nor the aspect correction (the artist's layout already accounts for
// the image's proportions) applies, and only the rotation, the mirroring and the offset are left.
// the image's proportions) applies, and only the rotation and offset are left.
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect = 1.f,
bool normalized_uv = false);
@@ -1082,10 +1047,6 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
Transform3d texture_displacement_volume_to_world(const ModelVolume &volume);
// The frame the bake and the previews project the texture in: `volume_to_world` with its translation
// removed, i.e. world orientation and scale about the volume's own origin. See build_texture_displacement().
// The rotation that carries a world direction into the texture's frame, i.e. the inverse of the
// placement's own rotation. Exposed because the preview shaders rebuild the frame themselves from a
// world position and have to turn it the same way the bake does, or the two disagree.
Matrix3d texture_displacement_frame_rotation(const Transform3d &volume_to_world);
Transform3d texture_displacement_bake_frame(const Transform3d &volume_to_world);
// Area-weighted vertex normals of `its` - the directions the bake both projects and displaces along.
+5 -2
View File
@@ -163,8 +163,8 @@ public:
~devPrinterUtil() = delete;
public:
static bool IsVirtualSlot(int ams_id) { return (ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID);}
static bool IsVirtualSlot(const std::string& ams_id) { return (ams_id == VIRTUAL_AMS_MAIN_ID_STR || ams_id == VIRTUAL_AMS_DEPUTY_ID_STR); }
static bool IsVirtualSlot(int ams_id);
static bool IsVirtualSlot(const std::string& ams_id);
};
namespace GUI
@@ -198,3 +198,6 @@ template<> struct std::hash<NozzleDef>
return h1 ^ (h2 + 0x9e3779b9 + (h1 << 6) + (h1 >> 2));
};
};
// key(extruder_id) -> { key1(nozzle type info), val1( number of the nozzle type)}
using ExtruderNozzleInfos = std::unordered_map<int, std::unordered_map<NozzleDef, int>>;
+114 -22
View File
@@ -8,6 +8,7 @@
#include <cstdlib>
#include <chrono>
#include <nlohmann/json.hpp>
#include <wx/app.h>
#include <wx/colour.h>
#include <string>
#include <wx/string.h>
@@ -33,6 +34,36 @@
using namespace nlohmann;
namespace Slic3r {
bool devPrinterUtil::IsVirtualSlot(int ams_id)
{
// When the app or preset bundle is unavailable we cannot know the vendor's
// extruder count, so fall back deterministically to the fixed Bambu pair
// {255, 254} rather than guessing from an unknown generic layout.
if (wxTheApp == nullptr || GUI::wxGetApp().preset_bundle == nullptr || GUI::wxGetApp().preset_bundle->is_bbl_vendor())
return ams_id == VIRTUAL_TRAY_MAIN_ID || ams_id == VIRTUAL_TRAY_DEPUTY_ID;
const int extruder_count = GUI::wxGetApp().preset_bundle->get_printer_extruder_count();
return ams_id >= VIRTUAL_TRAY_MAIN_ID - extruder_count + 1 && ams_id <= VIRTUAL_TRAY_MAIN_ID;
}
bool devPrinterUtil::IsVirtualSlot(const std::string& ams_id)
{
// Strict: only a fully numeric string is a valid slot id. std::stoi alone
// would accept trailing garbage (e.g. "255abc" -> 255).
if (ams_id.empty())
return false;
for (char c : ams_id) {
if (c < '0' || c > '9')
return false;
}
try {
return IsVirtualSlot(std::stoi(ams_id));
} catch (...) {
return false;
}
}
static int _hex_digit_to_int(const char c) { return (c >= '0' && c <= '9') ? c - '0' : (c >= 'A' && c <= 'F') ? c - 'A' + 10 : (c >= 'a' && c <= 'f') ? c - 'a' + 10 : -1; }
wxColour DevAmsTray::decode_color(const std::string &color)
@@ -200,18 +231,22 @@ wxString DevAms::GetDisplayName() const
int DevAms::GetSlotCount() const
{
// GetAmsType() maps AMS_LITE_MIXED -> AMS_LITE, so N9 reports 4 slots like AMS-Lite.
auto ams_type = GetAmsType();
if (ams_type == AMS || ams_type == AMS_LITE || ams_type == N3F)
{
return 4;
}
else if (ams_type == N3S)
{
// N3S is a 1-slot Bambu contract, so report 1 regardless of whether the
// preset bundle is momentarily unavailable (m_trays.size() can be 0).
if (GetAmsType() == N3S)
return 1;
if (wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
GUI::wxGetApp().preset_bundle->is_bbl_vendor()) {
// GetAmsType() maps AMS_LITE_MIXED -> AMS_LITE, so N9 reports 4 slots like AMS-Lite.
auto ams_type = GetAmsType();
if (ams_type == AMS || ams_type == AMS_LITE || ams_type == N3F)
{
return 4;
}
}
return 1;
return static_cast<int>(m_trays.size());
}
DevAmsTray* DevAms::GetTray(const std::string& tray_id) const
@@ -302,13 +337,21 @@ int DevFilaSystem::GetExtruderIdByAmsId(const std::string& ams_id) const
{
return it->second->GetExtruderId();
}
else if (stoi(ams_id) == VIRTUAL_TRAY_MAIN_ID)
{
return MAIN_EXTRUDER_ID;
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor();
if (!is_bbl_vendor && GetOwner()) {
for (const auto& tray : GetOwner()->vt_slot) {
if (tray.id == ams_id)
return VIRTUAL_TRAY_MAIN_ID - std::stoi(ams_id);
}
}
else if (stoi(ams_id) == VIRTUAL_TRAY_DEPUTY_ID)
{
if (is_bbl_vendor && ams_id == VIRTUAL_AMS_MAIN_ID_STR)
return MAIN_EXTRUDER_ID;
if (is_bbl_vendor && ams_id == VIRTUAL_AMS_DEPUTY_ID_STR)
return DEPUTY_EXTRUDER_ID;
if (!is_bbl_vendor && devPrinterUtil::IsVirtualSlot(ams_id)) {
return VIRTUAL_TRAY_MAIN_ID - std::stoi(ams_id);
}
assert(false && __FUNCTION__);
@@ -325,9 +368,22 @@ std::string DevFilaSystem::GetNozzleFlowStringByAmsId(const std::string& ams_id)
std::map<int, DevAmsSlotId> DevFilaSystem::GetTrayIndexMap()
{
std::map<int, DevAmsSlotId> tray_id_map;
tray_id_map[VIRTUAL_TRAY_MAIN_ID] = DevAmsSlotId{VIRTUAL_TRAY_MAIN_ID, 0};
tray_id_map[VIRTUAL_TRAY_DEPUTY_ID] = DevAmsSlotId{VIRTUAL_TRAY_DEPUTY_ID, 0};
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor();
if (is_bbl_vendor) {
tray_id_map[VIRTUAL_TRAY_MAIN_ID] = DevAmsSlotId{VIRTUAL_TRAY_MAIN_ID, 0};
tray_id_map[VIRTUAL_TRAY_DEPUTY_ID] = DevAmsSlotId{VIRTUAL_TRAY_DEPUTY_ID, 0};
} else if (GetOwner()) {
for (const auto& tray : GetOwner()->vt_slot) {
try {
const int tray_id = std::stoi(tray.id);
tray_id_map[tray_id] = DevAmsSlotId{tray_id, 0};
} catch (...) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " invalid virtual tray id: " << tray.id;
}
}
}
int generic_tray_index = 0;
for (auto& [ams_id, ams_item] : GetAmsList()) {
for (auto &[slot_id, slot_item] : ams_item->GetTrays()) {
if (ams_item && slot_item) {
@@ -335,9 +391,11 @@ std::map<int, DevAmsSlotId> DevFilaSystem::GetTrayIndexMap()
int ams_id_int = stoi(ams_id);
int slot_id_int = stoi(slot_id);
int tray_index = -1;
if (ams_item->GetAmsType() == DevAms::N3S) {
if (!is_bbl_vendor) {
tray_index = generic_tray_index + slot_id_int;
} else if (ams_item->GetAmsType() == DevAms::N3S) {
tray_index = ams_id_int;
} else if(ams_item->GetAmsType() == DevAms::AMS_LITE && ams_item->IsAmsLiteMixed()) {
} else if (ams_item->GetAmsType() == DevAms::AMS_LITE && ams_item->IsAmsLiteMixed()) {
tray_index = 24 + slot_id_int;
} else {
tray_index = (ams_id_int * 4 + slot_id_int);
@@ -348,6 +406,8 @@ std::map<int, DevAmsSlotId> DevFilaSystem::GetTrayIndexMap()
}
}
}
if (!is_bbl_vendor)
generic_tray_index += static_cast<int>(ams_item->GetTrays().size());
}
return tray_id_map;
@@ -456,6 +516,13 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
ams_id_set.insert(it->first);
}
// Bambu trays index their exist bit as ams_id*4+tray_id; a generic (non-Bambu) unit
// reports its own lane count, so its trays carry a cumulative lane index instead.
// Same index DevFilaSystem::GetTrayIndexMap() builds and the agent sets bits with.
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
GUI::wxGetApp().preset_bundle->is_bbl_vendor();
int generic_tray_base = 0;
for (auto it = j_ams.begin(); it != j_ams.end(); it++)
{
if (!it->contains("id")) continue;
@@ -795,7 +862,18 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
if (type_id < 4)
{
curr_tray->is_exists = (obj->tray_exist_bits & (1 << (ams_id_int * 4 + tray_id_int))) != 0 ? true : false;
if (is_bbl_vendor)
{
curr_tray->is_exists = (obj->tray_exist_bits & (1 << (ams_id_int * 4 + tray_id_int))) != 0 ? true : false;
}
else
{
// A generic unit is not four slots wide, so its trays carry a
// cumulative lane index across units instead of ams_id*4+tray_id.
const int tray_bit = generic_tray_base + tray_id_int;
curr_tray->is_exists = tray_bit >= 0 && tray_bit < 32 &&
(obj->tray_exist_bits & (1UL << tray_bit)) != 0;
}
}
else if (type_id == DevAms::AMS_LITE_MIXED)
{
@@ -851,6 +929,9 @@ void DevFilaSystemParser::ParseV1_0(const json& jj, MachineObject* obj, DevFilaS
}
}
}
if (!is_bbl_vendor && it->contains("tray") && (*it)["tray"].is_array()) {
generic_tray_base += static_cast<int>((*it)["tray"].size());
}
}
// remove not in amsList
for (auto it = ams_id_set.begin(); it != ams_id_set.end(); it++)
@@ -913,6 +994,8 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob
}
ams->m_exist = true;
ams->m_binded_extruder_set = {ext_id};
ams->m_binded_switcher_pos.reset();
ams->m_current_temperature = u.value("temperature", (float) INVALID_AMS_TEMPERATURE);
ams->m_humidity_percent = u.value("humidity_percent", -1);
ams->m_left_dry_time = u.value("dry_time_min", 0);
@@ -995,16 +1078,22 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob
}
// --- external / direct spools -> obj->vt_slot ---
// extruder 0 -> main virtual slot, extruder >0 -> deputy.
// Keep one virtual slot per extruder. The legacy two-extruder mapping uses
// 255 for extruder 0 and 254 for extruder 1; continue that sequence for
// additional extruders (253, 252, ...).
if (obj && data.contains("external") && data["external"].is_array())
{
std::map<int, DevAmsTray> external_slots;
obj->vt_slot.clear();
for (const auto& e : data["external"])
{
if (!e.is_object())
continue;
const int ext = e.value("extruder", MAIN_EXTRUDER_ID);
const int vt_id = (ext == MAIN_EXTRUDER_ID) ? VIRTUAL_TRAY_MAIN_ID : VIRTUAL_TRAY_DEPUTY_ID;
if (ext < MAIN_EXTRUDER_ID || ext >= VIRTUAL_TRAY_MAIN_ID)
continue;
const int vt_id = VIRTUAL_TRAY_MAIN_ID - ext;
DevAmsTray tray(std::to_string(vt_id));
tray.is_exists = e.value("loaded", false);
tray.m_fila_type = e.value("material", std::string());
@@ -1013,8 +1102,11 @@ void DevFilaSystemParser::ParseAgentFilament(const json& data, MachineObject* ob
tray.nozzle_temp_min = std::to_string(e.value("nozzle_temp_min", 0));
tray.nozzle_temp_max = std::to_string(e.value("nozzle_temp_max", 0));
tray.remain = e.value("remain_percent", -1);
obj->vt_slot.push_back(tray);
external_slots.insert_or_assign(ext, std::move(tray));
}
for (auto& entry : external_slots)
obj->vt_slot.push_back(std::move(entry.second));
}
}
+2 -2
View File
@@ -348,7 +348,7 @@ public:
DevAmsTray* GetAmsTray(const std::string& ams_id, const std::string& tray_id) const;
void CollectAmsColors(std::vector<wxColour>& ams_colors) const;
// Map a linear tray index -> {ams_id, slot_id}. Includes the two virtual (external-spool) trays,
// Map a linear tray index -> {ams_id, slot_id}. Includes virtual external-spool trays,
// N3S single-slot units, and the A2L/N9 AMS-Lite-mixed layout (trays 24-27).
std::map<int, DevAmsSlotId> GetTrayIndexMap();
@@ -433,4 +433,4 @@ struct DevFilamentDryingPreset
float filament_dev_ams_drying_heat_distortion_temperature = 0.0f;
};
}// namespace Slic3r
}// namespace Slic3r
+37 -17
View File
@@ -13,6 +13,7 @@
#include "slic3r/GUI/DeviceCore/DevDefs.h"
#include "libslic3r/PrintConfig.hpp"
#include <boost/log/trivial.hpp>
#include <wx/app.h>
#include <wx/string.h>
#include <string>
#include "libslic3r/CommonDefs.hpp"
@@ -27,6 +28,7 @@
#include <unordered_map>
#include <optional>
#include <cmath>
#include "slic3r/GUI/GUI_App.hpp"
using json = nlohmann::json;
@@ -176,34 +178,52 @@ DevFirmwareVersionInfo DevNozzle::GetFirmwareInfo() const
int DevNozzle::GetLogicExtruderId() const
{
int total_ext_count = GetTotalExtruderCount();
if (total_ext_count == 1) {
return LOGIC_UNIQUE_EXTRUDER_ID;
} else if (total_ext_count == 2) {
if (AtLeftExtruder()) {
return LOGIC_L_EXTRUDER_ID;
} else if (AtRightExtruder()) {
return LOGIC_R_EXTRUDER_ID;
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
GUI::wxGetApp().preset_bundle->is_bbl_vendor();
if (is_bbl_vendor) {
if (total_ext_count == 1) {
return LOGIC_UNIQUE_EXTRUDER_ID;
} else if (total_ext_count == 2) {
if (AtLeftExtruder()) {
return LOGIC_L_EXTRUDER_ID;
} else if (AtRightExtruder()) {
return LOGIC_R_EXTRUDER_ID;
}
}
assert(0);
return LOGIC_UNIQUE_EXTRUDER_ID;
}
assert(0);
return LOGIC_UNIQUE_EXTRUDER_ID;
// For some reason, BBL's logical extruder ID is inverted:
// physical extruder id = 0 (MAIN_EXTRUDER_ID) vs logical extruder id = 1 (LOGIC_R_EXTRUDER_ID)
// Likely because logical id reads from left to right (left = 0, right = 1)
// For generic N extruders, this inversion does not apply.
if (IsOnRack()) return INVALID_EXTRUDER_ID;
return m_nozzle_id;
}
int DevNozzle::GetExtruderId() const
{
int total_ext_count = GetTotalExtruderCount();
if (total_ext_count == 1) {
return MAIN_EXTRUDER_ID;
} else if (total_ext_count == 2) {
if (AtRightExtruder()) {
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr &&
GUI::wxGetApp().preset_bundle->is_bbl_vendor();
if (is_bbl_vendor) {
int total_ext_count = GetTotalExtruderCount();
if (total_ext_count == 1) {
return MAIN_EXTRUDER_ID;
} else if (AtLeftExtruder()) {
return DEPUTY_EXTRUDER_ID;
} else if (total_ext_count == 2) {
if (AtRightExtruder()) {
return MAIN_EXTRUDER_ID;
} else if (AtLeftExtruder()) {
return DEPUTY_EXTRUDER_ID;
}
}
return MAIN_EXTRUDER_ID;
}
return MAIN_EXTRUDER_ID;
return m_nozzle_id;
}
bool DevNozzle::AtLeftExtruder() const
+63 -46
View File
@@ -65,6 +65,7 @@
#include <unordered_set>
#include <utility>
#include <vector>
#include <wx/app.h>
#include <wx/colour.h>
#include <tuple>
#include <wx/app.h>
@@ -806,8 +807,12 @@ bool MachineObject::is_extrusion_cali_finished()
DevAmsTray *MachineObject::get_curr_tray()
{
const std::string& cur_ams_id = m_extder_system->GetCurrentAmsId();
if (cur_ams_id.compare(std::to_string(VIRTUAL_TRAY_MAIN_ID)) == 0) {
return &vt_slot[0];
if (devPrinterUtil::IsVirtualSlot(cur_ams_id)) {
for (auto& tray : vt_slot) {
if (tray.id == cur_ams_id)
return &tray;
}
return nullptr;
}
DevAms* curr_ams = get_curr_Ams();
@@ -1273,19 +1278,24 @@ int MachineObject::get_bed_temperature_limit()
bool MachineObject::is_filament_installed()
{
if (m_extder_system->GetTotalExtderCount() > 0) {
// right//or single
auto ext = m_extder_system->m_extders[MAIN_EXTRUDER_ID];
if (ext.m_ext_has_filament) {
// if (m_extder_system->GetTotalExtderCount() > 0) {
// // right//or single
// auto ext = m_extder_system->m_extders[MAIN_EXTRUDER_ID];
// if (ext.m_ext_has_filament) {
// return true;
// }
// }
// /*left*/
// if (m_extder_system->GetTotalExtderCount() > 1) {
// auto ext = m_extder_system->m_extders[DEPUTY_EXTRUDER_ID];
// if (ext.m_ext_has_filament) {
// return true;
// }
// }
for (auto& ext : m_extder_system->m_extders) {
if (ext.m_ext_has_filament)
return true;
}
}
/*left*/
if (m_extder_system->GetTotalExtderCount() > 1) {
auto ext = m_extder_system->m_extders[DEPUTY_EXTRUDER_ID];
if (ext.m_ext_has_filament) {
return true;
}
}
return false;
}
@@ -2627,13 +2637,10 @@ void MachineObject::reset()
json empty_j;
print_json.diff2all_base_reset(empty_j);
for (auto i = 0; i < vt_slot.size(); i++) {
vt_slot[i].reset();
if (i == 1) {
vt_slot.erase(vt_slot.begin() + 1);
}
}
for (auto& tray : vt_slot)
tray.reset();
if (vt_slot.size() > 1)
vt_slot.erase(vt_slot.begin() + 1, vt_slot.end());
// why: reset reuses MachineObject, so release its lazy subtask
// before dropping the pointer to prevent reconnect leaks.
if (subtask_) {
@@ -4114,25 +4121,28 @@ int MachineObject::parse_json(std::string tunnel, std::string payload, bool key_
if (jj.contains("vir_slot") && jj["vir_slot"].is_array()) {
for (auto it = jj["vir_slot"].begin(); it != jj["vir_slot"].end(); it++) {
auto vslot = parse_vt_tray(it.value().get<json>());
try {
const auto tray_json = it.value().get<json>();
if (!tray_json.is_object() || !tray_json.contains("id") || !tray_json["id"].is_string())
continue;
if (vslot.id == std::to_string(VIRTUAL_TRAY_MAIN_ID)) {
auto it = std::next(vt_slot.begin(), 0);
if (it != vt_slot.end()) {
vt_slot[0] = vslot;
}
else {
vt_slot.push_back(vslot);
const auto tray_id_str = tray_json["id"].get<std::string>();
int tray_id = 0;
const auto result = std::from_chars(tray_id_str.data(), tray_id_str.data() + tray_id_str.size(), tray_id);
if (result.ec != std::errc{} || result.ptr != tray_id_str.data() + tray_id_str.size()
|| tray_id < 0 || tray_id > VIRTUAL_TRAY_MAIN_ID)
continue;
auto vslot = parse_vt_tray(tray_json);
const auto index = static_cast<std::vector<DevAmsTray>::size_type>(VIRTUAL_TRAY_MAIN_ID - tray_id);
while (vt_slot.size() <= index) {
const int placeholder_id = VIRTUAL_TRAY_MAIN_ID - static_cast<int>(vt_slot.size());
vt_slot.emplace_back(std::to_string(placeholder_id));
}
vt_slot[index] = vslot;
}
else if (vslot.id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID)) {
auto it = std::next(vt_slot.begin(), 1);
if (it != vt_slot.end()) {
vt_slot[1] = vslot;
}
else {
vt_slot.push_back(vslot);
}
catch (...) {
continue;
}
}
@@ -5270,16 +5280,17 @@ DevAmsTray MachineObject::parse_vt_tray(json vtray)
bool MachineObject::contains_tray(const std::string &ams_id, const std::string &tray_id) const
{
if (ams_id != VIRTUAL_AMS_MAIN_ID_STR && ams_id != VIRTUAL_AMS_DEPUTY_ID_STR) {
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor();
if (is_bbl_vendor && ams_id != VIRTUAL_AMS_MAIN_ID_STR && ams_id != VIRTUAL_AMS_DEPUTY_ID_STR)
return m_fila_system->GetAmsTray(ams_id, tray_id) != nullptr;
} else {
if (!is_bbl_vendor || ams_id == VIRTUAL_AMS_MAIN_ID_STR || ams_id == VIRTUAL_AMS_DEPUTY_ID_STR) {
for (const auto& tray : vt_slot) {
if (tray.id == ams_id) { return true; }
if (tray.id == ams_id) return true;
}
}
return false;
return !is_bbl_vendor && m_fila_system->GetAmsTray(ams_id, tray_id) != nullptr;
}
DevAmsTray MachineObject::get_tray(const std::string &ams_id, const std::string &tray_id) const
@@ -5289,13 +5300,19 @@ DevAmsTray MachineObject::get_tray(const std::string &ams_id, const std::string
return DevAmsTray(tray_id);
}
if (ams_id != VIRTUAL_AMS_MAIN_ID_STR && ams_id != VIRTUAL_AMS_DEPUTY_ID_STR) {
const bool is_bbl_vendor = wxTheApp != nullptr && GUI::wxGetApp().preset_bundle != nullptr && GUI::wxGetApp().preset_bundle->is_bbl_vendor();
if (is_bbl_vendor && ams_id != VIRTUAL_AMS_MAIN_ID_STR && ams_id != VIRTUAL_AMS_DEPUTY_ID_STR) {
auto tray = m_fila_system->GetAmsTray(ams_id, tray_id);
if (tray) { return *tray;};
if (tray) return *tray;
}
else {
else if (!is_bbl_vendor || ams_id == VIRTUAL_AMS_MAIN_ID_STR || ams_id == VIRTUAL_AMS_DEPUTY_ID_STR) {
for (const auto &tray : vt_slot) {
if (tray.id == ams_id) { return tray; }
if (tray.id == ams_id) return tray;
}
if (!is_bbl_vendor) {
auto tray = m_fila_system->GetAmsTray(ams_id, tray_id);
if (tray) return *tray;
}
}
File diff suppressed because it is too large Load Diff
@@ -508,120 +508,25 @@ private:
// is where the live adjustment is applied. Called whenever the panel moves or reverts it.
void invalidate_adjust_textures();
// "Adjust Texture" mode: instead of painting, the active layer's placement is a control lying
// on the model. The gizmo is one tile of the texture in the paint patch's own tangent plane:
// where it sits is the layer's offset, how it is turned is rotation_deg, how big it is is the
// tile extents - so each handle is one of the values it edits rather than a stand-in for it.
// Anchored to the centroid/average-normal of the active layer's paint patch (see
// libslic3r::compute_layer_paint_anchor()), so nothing is drawn until something is painted.
// "Adjust Texture" mode: instead of painting, dragging an on-canvas handle changes the active
// layer's offset. The handle is a flat panel lying in the paint patch's own tangent plane
// (a "pan" - drag anywhere on it for free 2D movement), plus two arrows along the patch's
// own U/V axes that constrain the drag to just that one axis for precise nudging. Anchored to
// the centroid/average-normal of the active layer's current paint patch (see
// libslic3r::compute_layer_paint_anchor()), so nothing is drawn if it has nothing painted yet.
//
// Drawn through ImGui's foreground draw list rather than the "flat" shader, which is what gives
// it antialiased sub-pixel strokes, rounded joins, a contrast halo and the numeric readout. The
// cost is that render_adjust_texture_gizmo() has to be called from inside the gizmo's ImGui
// frame (see on_render_input_window()), not from on_render() with the rest of the GL drawing.
// Move is the whole interior, free in both axes. AxisMove is the four arrows, each holding a
// move to one of the tile's axes. EdgeScale is the four edge bars, one tile extent each, and
// CornerScale the four corner grips, which take both extents at once.
//
// The arrows and the bars share the same four rays out of the tile, so they are kept apart by
// distance rather than direction: the bar hugs its edge, then a clear gap, then the arrow well
// outside it. adjust_pick() leans on that - see the order it tests them in.
enum class AdjustHandle { None, Move, AxisMove, EdgeScale, CornerScale, Rotate };
// The gizmo's geometry for one frame, in screen pixels. Worked out once and shared by the
// drawing and the hit test, so the two cannot disagree about where a handle is. Everything
// except the frame itself is a fixed number of pixels, which is what keeps the control equally
// usable on a 5 mm part and a 500 mm one - the old handle scaled with the volume's bounding box
// and so was unusable at both ends.
struct AdjustFrame
{
bool valid = false;
Vec2d center = Vec2d::Zero();
// +u+v, +u-v, -u-v, -u+v. A scale grip sits on each.
std::array<Vec2d, 4> corner{};
// +u, -u, +v, -v: the middle of each edge, and the two ends of the bar that sits just
// outside it. The bar's ends are kept rather than recomputed so the drawing and the hit test
// cannot end up with it in two different places.
std::array<Vec2d, 4> edge_mid{};
std::array<Vec2d, 4> bar_a{};
std::array<Vec2d, 4> bar_b{};
// The axis arrow further out on the same ray: where its shaft starts and where its chevron
// ends. Kept, like the bar's ends, so the drawing and the hit test cannot disagree.
std::array<Vec2d, 4> arm_base{};
std::array<Vec2d, 4> arm_tip{};
std::array<Vec2d, 4> arm_dir{}; // unit, pointing outward from the centre
// A ring is placed off every corner, but only one is drawn and only that one can be picked:
// four marks for a single operation is clutter, and whichever corner the cursor is near is
// the one worth offering. `rotate_shown` says which, and see update_rotate_corner() for why
// it does not simply track the nearest.
std::array<Vec2d, 4> rotate_at{};
int rotate_shown = 0;
// The tile projects to less than ADJUST_MIN_TILE_PX across, so the frame was drawn at that
// floor instead. It is dashed to say so, and the readout still gives the true millimetres.
bool clamped = false;
// What the frame was built from, in mesh-local mm: the tile's own (rotated) edge directions
// and its extents. The drag arithmetic reuses these rather than recomputing them, so it
// cannot end up working against a different tile than the one on screen.
Vec3f e_u = Vec3f::UnitX(), e_v = Vec3f::UnitY();
Vec2f tile_mm = Vec2f::Ones(); // (u, v) as stored on the layer
Vec2f half_planar = Vec2f::Ones(); // half extents in planar mm, v already aspect-corrected
Vec2f center_planar = Vec2f::Zero();
};
// NOTE: the drag direction/sign below is this session's best-effort reasoning about which way
// the texture should appear to move as the handle is dragged - it could not be visually
// confirmed while writing it (no way to render/see pixels in this environment), so it may
// need a one-line sign flip once actually tested.
bool update_adjust_anchor(); // recomputes m_adjust_anchor_pos/normal; false if nothing painted
// The selected instance's volume-to-world transform, which the bake frame is derived from.
bool adjust_volume_to_world(Transform3d &out) const;
// The anchor, carried from mesh-local into the bake frame. Converted on every use rather than
// stored converted: the object can be moved, rotated or scaled while this mode is open, and a
// stored copy would quietly describe the shape it used to be. The expensive part - finding the
// paint patch's centroid - stays cached in the local members.
bool adjust_anchor_in_frame(Vec3f &pos, Vec3f &normal) const;
// Drops every scrap of live gizmo state. Called from each of the several ways out of the mode -
// the panel's toggle, collapsing the card, switching to seam marking, closing the gizmo - so a
// stale hover highlight or a half-finished drag cannot survive into the next time it is opened.
void end_adjust_texture_mode();
bool on_mouse_adjust_texture(const wxMouseEvent &mouse_event);
void render_adjust_texture_gizmo();
// This frame's gizmo geometry, or an invalid frame when there is nothing to draw.
AdjustFrame adjust_frame() const;
// Which handle is under `mouse`, with `index` set to the arm or corner it belongs to. Tested
// outermost first; see the hit-area board of the design for the order and the radii.
AdjustHandle adjust_pick(const Vec2d &mouse, const AdjustFrame &f, int &index) const;
// Chooses which corner carries the rotate ring. The nearest to the cursor, but it only moves once
// the newcomer is clearly nearer, because without that margin the ring flits between two corners
// every time the cursor wanders along the line equidistant from both. Never moves during a drag.
void update_rotate_corner(const AdjustFrame &f, const Vec2d &mouse);
// Where the tile's centre sits, in planar mm relative to the anchor, for a layer's placement.
// Not the patch centroid: the gizmo *is* the texture's placement, so it has to travel as the
// offset changes - pinning it to the centroid is what made the old handle look broken, the
// texture sliding while the handle stayed put.
static Vec2f adjust_handle_planar(const TextureDisplacementLayer &layer, float aspect);
// The exact inverse: the offset that puts the tile's centre at `planar`. Rotating or resizing
// the tile changes where a given offset places it, so every drag recomputes the offset from the
// centre it wants to keep - which is what makes rotate and scale act *in place* rather than
// swinging the tile around the anchor.
static Vec2f adjust_offset_for_planar(const Vec2f &planar, float rot_deg, const Vec2f &tile_mm,
const Vec2f &mirror_sign, float aspect);
// The mesh-local-to-world transform of the selected instance, the (unrotated) tangent basis and
// the lifted anchor the whole gizmo is built on. False when there is nothing to build on - which
// is the single place that decides the gizmo is not drawable, so the drawing, the hit test and
// the drag cannot disagree about it.
bool adjust_basis(Transform3d &trafo, Vec3f &u_axis, Vec3f &v_axis, Vec3f &base) const;
// Screen-space quad of a tile with the given placement. Used for the pre-drag ghost, which is
// drawn from the captured start placement rather than from the layer, the layer having already
// been moved by then.
bool adjust_quad_for(const Vec2f &center_planar, float rot_deg, const Vec2f &tile_mm,
std::array<Vec2d, 4> &out) const;
// Where the cursor meets the drag plane, in planar mm relative to the anchor. The plane is
// deliberately the *fixed* anchor plane rather than the tile's current position: measuring a
// drag against a plane the drag itself moves is a feedback loop.
bool adjust_planar_under(const Vec2d &mouse, Vec2f &planar) const;
// Draws a small '+'/'-' next to the mouse over the 3D view while painting/selecting, so it is
// obvious whether the next stroke adds paint (default) or erases it (Shift). Uses ImGui's
// foreground draw list, so it must be called from inside the gizmo's ImGui frame.
void render_paint_cursor_hint();
// Bake-frame tangent-plane basis at m_adjust_anchor_normal: box_projection_axes(), so dragging
// Mesh-local tangent-plane basis at m_adjust_anchor_normal: box_projection_axes(), so dragging
// on-canvas maps consistently onto offset whichever face the anchor is on.
void adjust_tangent_basis(Vec3f &u_axis, Vec3f &v_axis) const;
@@ -1213,47 +1118,23 @@ private:
// See the "Adjust Texture" block of private methods above.
bool m_adjust_texture_mode = false;
bool m_adjust_anchor_valid = false;
// Mesh-local, as compute_layer_paint_anchor() produces them. Everything else about the gizmo
// works in the bake frame - world orientation and scale, origin at the volume's own, which is
// where the texture is projected (texture_displacement_bake_frame()) - so these go through
// adjust_anchor_in_frame() at the point of use. A tile is so many *printed* millimetres whatever
// the instance's scale, which is why measuring the gizmo in mesh-local millimetres made its tile
// differ from the rendered one by exactly that scale.
Vec3f m_adjust_anchor_pos = Vec3f::Zero();
Vec3f m_adjust_anchor_normal = Vec3f::UnitZ();
Vec3f m_adjust_anchor_pos = Vec3f::Zero(); // mesh-local
Vec3f m_adjust_anchor_normal = Vec3f::UnitZ(); // mesh-local
// What the cursor is over, and what is being dragged. `index` picks the arm or the corner
// (0..3, in AdjustFrame's order); it is -1 for Move and Rotate, which have only one of each.
// Only ever one of each is set, so two handles can never both look live.
// Which corner currently carries the rotate ring. Persists between frames, which is what gives
// update_rotate_corner() something to apply its margin against.
int m_adjust_rotate_corner = 0;
AdjustHandle m_adjust_hover_kind = AdjustHandle::None;
int m_adjust_hover_index = -1;
AdjustHandle m_adjust_drag_kind = AdjustHandle::None;
int m_adjust_drag_index = -1;
// Pan: free drag anywhere on the flat panel, moves offset along both axes. AxisU/AxisV: drag
// the corresponding arrow, moves offset along only that one axis.
enum class AdjustHandle { None, Pan, AxisU, AxisV };
AdjustHandle m_adjust_drag_handle = AdjustHandle::None;
Vec2f m_adjust_drag_start_offset = Vec2f::Zero();
// Anchor-relative planar position (see project_planar()) of the point under the mouse at the
// moment the current drag started; every subsequent frame's delta is measured against this,
// rather than accumulated frame-to-frame, to avoid drift.
Vec2f m_adjust_drag_start_planar = Vec2f::Zero();
// Everything the live drag is measured against, captured once on mouse-down. Every frame's
// delta is taken from these rather than accumulated frame to frame, so a long drag cannot
// drift away from the cursor.
Vec2f m_adjust_drag_start_center = Vec2f::Zero(); // tile centre, planar mm
Vec2f m_adjust_drag_start_grab = Vec2f::Zero(); // the grabbed point, planar mm
// How far the grab landed from the handle's own position, in planar mm. A scale drag reads the
// extents straight off the cursor, so without subtracting this the tile would jump by however
// far inside the generous hit radius the click happened to fall.
Vec2f m_adjust_drag_grab_bias = Vec2f::Zero();
float m_adjust_drag_start_rot = 0.f;
Vec2f m_adjust_drag_start_tile = Vec2f::Ones(); // (u, v) extents as stored
float m_adjust_drag_start_angle = 0.f; // planar angle of the grab, radians
// The mirroring the layer had when the drag began. A width or height handle dragged back through
// the tile's middle toggles its axis against this, rather than against whatever the last frame
// left behind, so dragging back and forth across the middle cannot ratchet the flag.
bool m_adjust_drag_start_mirror_u = false;
bool m_adjust_drag_start_mirror_v = false;
// The live value to put in the readout pill, built by the drag and drawn by the renderer in the
// same frame. Empty when no drag is running, which is also when no pill is drawn.
std::string m_adjust_readout;
// Lazily-built unit quad (the pan panel) and unit line-with-arrowhead (reused, rotated, for
// both the U and V axis arrows), transformed into place at render time.
GLModel m_adjust_panel_glmodel;
GLModel m_adjust_arrow_glmodel;
std::map<std::string, wxString> m_desc;
-12
View File
@@ -816,18 +816,6 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
bool MainFrame::handle_global_shortcut(const KeyChord& chord)
{
// ImGui draws its text fields (the text tool's box, a sketch value field) on the GL canvas, so
// while one of them is typed into the focused wx window is that canvas or an in-canvas frame:
// neither is a wxControl, so focus_keeps_space() cannot see it and CHAR_HOOK takes the chord
// here first -- Space opened the Speed Dial, Ctrl+P the Preferences, and the field never saw
// the key. ImGui raises WantCaptureKeyboard while one of its items owns the keyboard, so leave
// the key to it then. A focused wxControl (text field, list, web view) is untouched: those
// keep their own keys, and ImGui's flag can outlive the focus moving to one of them.
wxWindow* const focus = wxWindow::FindFocus();
ImGuiWrapper* const imgui = wxGetApp().imgui();
if (focus != nullptr && imgui != nullptr && imgui->want_keyboard() && dynamic_cast<wxControl*>(focus) == nullptr)
return false;
const std::optional<Shortcut> shortcut = wxGetApp().shortcuts().lookup(ShortcutContext::Global, chord);
if (!shortcut.has_value())
return false;
+16 -1
View File
@@ -6071,6 +6071,8 @@ void Sidebar::on_bed_type_change(BedType bed_type)
* - Map key encoding:
* - Virtual trays: 0x10000 + vt_tray.id (first/main extruder), or just vt_tray.id (secondary)
* - AMS trays: 0x10000 + (ams_id * 4 + slot_id) (main extruder), or (ams_id * 4 + slot_id) (secondary)
* - Generic (non-Bambu) units are not four slots wide: their trays index by a cumulative lane
* count across units instead of ams_id * 4 + slot_id
* - The 0x10000 flag indicates the main/right extruder
* - Map value: DynamicPrintConfig with filament properties (id, type, color, etc.)
*
@@ -6136,13 +6138,26 @@ std::map<int, DynamicPrintConfig> Sidebar::build_filament_ams_list(MachineObject
return std::string();
};
const bool is_bbl_vendor = wxTheApp != nullptr && wxGetApp().preset_bundle != nullptr && wxGetApp().preset_bundle->is_bbl_vendor();
auto list = obj->GetFilaSystem()->GetAmsList();
for (auto ams : list) {
int ams_id = std::stoi(ams.first);
int extruder = ams.second->GetExtruderId() ? 0 : 0x10000; // Main (first) extruder at right
// Bambu units are four slots wide; a generic unit reports its own lane count and its trays
// carry a cumulative lane index. The AMS map is keyed by string id, so count preceding units
// numerically to keep this the same index the parser and the agent use.
int generic_tray_base = 0;
if (!is_bbl_vendor) {
for (const auto& other : list) {
if (std::stoi(other.first) < ams_id)
generic_tray_base += static_cast<int>(other.second->GetTrays().size());
}
}
for (auto tray : ams.second->GetTrays()) {
int slot_id = std::stoi(tray.first);
filament_ams_list.emplace(extruder + (ams_id * 4 + slot_id),
const int tray_index = is_bbl_vendor ? ams_id * 4 + slot_id : generic_tray_base + slot_id;
filament_ams_list.emplace(extruder + tray_index,
build_tray_config(*tray.second, get_ams_name(ams_id, slot_id), std::to_string(ams_id), std::to_string(slot_id)));
}
}
File diff suppressed because it is too large Load Diff
+36 -4
View File
@@ -1,6 +1,8 @@
#ifndef slic3r_StatusPanel_hpp_
#define slic3r_StatusPanel_hpp_
#include <vector>
#include "libslic3r/ProjectTask.hpp"
#include "DeviceManager.hpp"
#include "MonitorPage.hpp"
@@ -66,6 +68,7 @@
#include "StagedBuild.hpp"
class StepIndicator;
class wxChoice;
#define COMMAND_TIMEOUT 5
@@ -140,28 +143,36 @@ class ExtruderImage : public wxWindow
ScalableBitmap *m_left_extruder_active_empty;
ScalableBitmap *m_left_extruder_unactive_filled;
ScalableBitmap *m_left_extruder_unactive_empty;
ScalableBitmap *m_right_extruder_active_filled;
ScalableBitmap *m_right_extruder_active_empty;
ScalableBitmap *m_right_extruder_unactive_filled;
ScalableBitmap *m_right_extruder_unactive_empty;
ScalableBitmap *m_extruder_single_nozzle_empty_load;
ScalableBitmap *m_extruder_single_nozzle_empty_unload;
ScalableBitmap *m_extruder_single_nozzle_filled_load;
ScalableBitmap *m_extruder_single_nozzle_filled_unload;
ExtruderState m_left_ext_state = {ExtruderState::EMPTY_LOAD};
ExtruderState m_right_ext_state = {ExtruderState::EMPTY_LOAD};
ExtruderState m_single_ext_state = {ExtruderState::EMPTY_LOAD};
std::vector<ExtruderState> m_multi_extruder_states;
bool m_generic_nozzle_display{false};
public:
void update(int nozzle_num, int nozzle_id);
void update(ExtruderState single_state);
void update(ExtruderState right_state, ExtruderState left_state);
void update(ExtruderState state, int idx);
void msw_rescale();
void updateGenericSize();
void setExtruderCount(int nozzle_num);
void setGenericNozzleDisplay(bool enabled);
void setExtruderUsed(std::string loc);
void setExtruderUsed(int nozzle_idx);
void paintEvent(wxPaintEvent &evt);
void render(wxDC &dc);
@@ -495,6 +506,8 @@ protected:
std::vector<ExtruderImage *> m_extruderImage;
SwitchBoard * m_nozzle_btn_panel;
wxChoice* m_generic_nozzle_selector{nullptr};
int m_generic_nozzle_selector_count{0};
wxStaticText * m_text_tasklist_caption;
@@ -507,10 +520,23 @@ protected:
wxBoxSizer * m_misc_ctrl_sizer;
StaticBox* m_fan_panel;
StaticLine * m_line_nozzle;
TempInput* m_tempCtrl_nozzle;
wxWindowID m_nozzle_temp_control_id{wxID_ANY};
wxWindow* m_temp_nozzle_parent{nullptr};
wxBoxSizer* m_temp_nozzle_sizer{nullptr};
size_t m_temp_nozzle_active_count{0};
TempInput* m_tempCtrl_nozzle{nullptr};
int m_temp_nozzle_timeout{ 0 };
TempInput* m_tempCtrl_nozzle_deputy;
TempInput* m_tempCtrl_nozzle_deputy{nullptr};
int m_temp_nozzle_deputy_timeout{ 0 };
std::vector<TempInput*> m_tempCtrl_nozzles;
std::vector<int> m_temp_nozzle_timeouts;
// Last (dev_id, device toolhead count, preset nozzle count) we warned about, to avoid log spam.
std::string m_last_mismatch_dev_id;
int m_last_mismatch_device_count{-1};
int m_last_mismatch_preset_count{-1};
TempInput * m_tempCtrl_bed;
int m_temp_bed_timeout {0};
TempInput * m_tempCtrl_chamber;
@@ -550,6 +576,7 @@ protected:
/* AMS control box <-> live nozzle-rack panel toggle (rack printers only) */
SwitchBoard* m_ams_rack_switch{ nullptr };
MultiSwitchButton* m_ams_nozzle_switch{ nullptr };
AMSControl* m_ams_control;
StaticBox* m_ams_control_box;
@@ -624,6 +651,9 @@ public:
wxBoxSizer *create_temp_axis_group(wxWindow *parent);
wxBoxSizer *create_temp_control(wxWindow *parent);
TempInput* create_nozzle_temp_control(wxWindow *parent, wxWindowID id);
void set_temp_input_colors(TempInput* temp_ctrl);
void ensure_nozzle_temp_controls(size_t count);
wxBoxSizer *create_misc_control(wxWindow *parent);
wxBoxSizer *create_axis_control(wxWindow *parent);
wxPanel *create_bed_control(wxWindow *parent);
@@ -654,6 +684,7 @@ private:
friend class MonitorPanel;
void wire_controls();
bool load_thumbnail_from_url(const wxString &url, MachineObject *obj);
void sync_nozzle_temp_controls(size_t count);
protected:
std::shared_ptr<SliceInfoPopup> m_slice_info_popup;
@@ -801,6 +832,7 @@ protected:
void update_cloud_subtask(MachineObject *obj);
void update_sdcard_subtask(MachineObject *obj);
void update_temp_ctrl(MachineObject *obj);
void check_extruder_count_mismatch(MachineObject* obj);
void update_misc_ctrl(MachineObject *obj);
void update_ams(MachineObject* obj);
void update_rack(MachineObject* obj);
+5 -18
View File
@@ -297,12 +297,9 @@ void UVEditorCanvas::set_island_fill_colors(std::vector<ColorRGBA> colors)
Refresh();
}
void UVEditorCanvas::set_uv_transform(float tiling_scale, float tiling_scale_v, const Vec2f &mirror_sign,
float rotation_deg, bool tile_enabled, bool tile_mirrored)
void UVEditorCanvas::set_uv_transform(float tiling_scale, float rotation_deg, bool tile_enabled, bool tile_mirrored)
{
m_tiling_scale = (std::abs(tiling_scale) > 1e-6f) ? tiling_scale : 1.f;
m_tiling_scale_v = (tiling_scale_v > 1e-6f) ? tiling_scale_v : m_tiling_scale;
m_mirror_sign = mirror_sign;
m_rotation_deg = rotation_deg;
m_tile_enabled = tile_enabled;
m_tile_mirrored = tile_mirrored;
@@ -664,18 +661,11 @@ Vec2f UVEditorCanvas::island_centroid(int island) const
Vec2f UVEditorCanvas::uv_delta_to_unwrap(const Vec2f &delta_uv) const
{
// apply_uv_transform() maps unwrap -> uv as uv = S^-1 R(unwrap) + offset, with S the tile
// extents, so the inverse of a *delta* (the translation drops out) is unwrap = R^-1(S delta_uv).
// The extents therefore multiply *before* the inverse rotation, which is the mirror image of the
// forward order; with a square tile the two are interchangeable, with a rectangular one they are
// not.
// apply_uv_transform() maps unwrap -> uv as uv = R(unwrap / tiling) + offset, so the inverse of
// a *delta* (the translation drops out) is unwrap = tiling * R^-1(delta_uv).
const float rad = m_rotation_deg * float(M_PI) / 180.f;
const float cs = std::cos(rad), sn = std::sin(rad);
// The mirror is its own sign on the same axis, and a negation is its own inverse, so it comes
// back out here exactly as it went in.
const Vec2f scaled(delta_uv.x() * m_tiling_scale * m_mirror_sign.x(),
delta_uv.y() * m_tiling_scale_v * m_mirror_sign.y());
return Vec2f(scaled.x() * cs + scaled.y() * sn, -scaled.x() * sn + scaled.y() * cs);
return Vec2f(delta_uv.x() * cs + delta_uv.y() * sn, -delta_uv.x() * sn + delta_uv.y() * cs) * m_tiling_scale;
}
Vec2f UVEditorCanvas::snap_correction(int island) const
@@ -2190,10 +2180,7 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY)
void UVEditorPanel::apply_state(const UVEditorCanvas::PaneState &s)
{
const wxString name = s.has_layer ? s.layer_name : _L("No layer mapped with Unwrap");
// A square tile reads as one number; only a genuinely rectangular one is worth two.
const wxString tile = !s.has_layer ? wxString() :
std::abs(s.tile_mm_v - s.tile_mm) < 0.005f ? wxString::Format(_L("%.1f mm tile"), s.tile_mm) :
wxString::Format(_L("%.1f x %.1f mm tile"), s.tile_mm, s.tile_mm_v);
const wxString tile = s.has_layer ? wxString::Format(_L("%.1f mm tile"), s.tile_mm) : wxString();
const bool relayout = m_layer_name->GetLabel() != name || m_tile->GetLabel() != tile;
m_layer_name->SetLabel(name);
m_tile->SetLabel(tile);
+4 -10
View File
@@ -91,10 +91,7 @@ public:
// space, back into the unwrap's mm space - which is where a TextureIsland's offset actually
// lives (see apply_uv_transform()). The tile settings come along because the background has to
// repeat exactly the way the height sampler does, or the pane would stop showing what gets baked.
// `tiling_scale_v` is the tile's extent along v; pass <= 0 for a square tile, matching
// layer_tile_mm().
void set_uv_transform(float tiling_scale, float tiling_scale_v, const Vec2f &mirror_sign,
float rotation_deg, bool tile_enabled, bool tile_mirrored);
void set_uv_transform(float tiling_scale, float rotation_deg, bool tile_enabled, bool tile_mirrored);
// Same 8-bit grayscale pixels build_texture_displacement()'s height sampling uses, shown
// beneath the wireframe (expanded to RGBA on upload) so the unwrap can be checked against the
@@ -132,8 +129,7 @@ public:
{
bool has_layer = false; // an active layer mapped with Unwrap
wxString layer_name;
float tile_mm = 0.f; // tile extent along u
float tile_mm_v = 0.f; // along v; equal to tile_mm for a square tile
float tile_mm = 0.f;
std::vector<unsigned char> thumbnail_rgb; // thumbnail_px square, 3 bytes per pixel; empty for none
int thumbnail_px = 0;
float seam_angle_deg = 40.f;
@@ -296,10 +292,8 @@ private:
GLModel m_grid_glmodel;
float m_grid_step = 0.f; // the UV step m_grid_glmodel was built for; 0 = not built
float m_tiling_scale = 1.f;
float m_tiling_scale_v = 1.f; // already resolved: never the "unset" sentinel, see set_uv_transform()
Vec2f m_mirror_sign = Vec2f::Ones(); // +1 or -1 per axis, see layer_mirror_sign()
float m_rotation_deg = 0.f;
float m_tiling_scale = 1.f;
float m_rotation_deg = 0.f;
bool m_tile_enabled = true;
bool m_tile_mirrored = false;
bool m_snap_enabled = false;
+502 -21
View File
@@ -244,7 +244,7 @@ AMSControl::AMSControl(wxWindow *parent, wxWindowID id, const wxPoint &pos, cons
m_sizer_ams_body->Add(m_sizer_ams_area_right, wxALIGN_CENTER, 0);
m_sizer_body->Add(m_sizer_ams_items, 0, wxALIGN_CENTER, 0);
m_sizer_body->Add(0, 0, 1, wxEXPAND | wxTOP, FromDIP(10));
m_sizer_ams_gap = m_sizer_body->Add(0, 0, 1, wxEXPAND | wxTOP, FromDIP(10));
m_sizer_body->Add(m_sizer_ams_body, 0, wxALIGN_CENTER, 0);
m_sizer_body->Add(m_sizer_down_road, 0, wxALIGN_CENTER, 0);
m_sizer_body->Add(m_sizer_ams_option, 0, wxEXPAND, 0);
@@ -371,7 +371,7 @@ bool AMSControl::IsAmsInRightPanel(std::string ams_id) {
return false;
}
}
else{
else if (m_total_ext_count == 1){
for (auto id : m_item_ids[MAIN_EXTRUDER_ID]){
if (id == ams_id){
return true;
@@ -379,6 +379,10 @@ bool AMSControl::IsAmsInRightPanel(std::string ams_id) {
}
return false;
}
else {
// Generic N Nozzles
}
return false;
}
void AMSControl::AmsSelectedSwitch(wxCommandEvent& event) {
@@ -519,7 +523,7 @@ void AMSControl::msw_rescale()
m_button_ams_setting_press.msw_rescale();
m_button_ams_setting->SetBitmap(m_button_ams_setting_normal.bmp());
m_extruder->msw_rescale();
if (m_extruder) m_extruder->msw_rescale();
if (m_button_extruder_feed) m_button_extruder_feed->Rescale(); // ORCA
if (m_button_extruder_back) m_button_extruder_back->Rescale(); // ORCA
@@ -603,6 +607,8 @@ void AMSControl::CreateAms()
void AMSControl::ClearAms() {
m_sizer_ams_items->Clear(false);
m_simplebook_ams_right->DeleteAllPages();
m_simplebook_ams_left->DeleteAllPages();
m_simplebook_ams_right->DestroyChildren();
@@ -613,7 +619,7 @@ void AMSControl::ClearAms() {
m_simplebook_ams_left->Refresh();
for (auto it : m_ams_preview_list) {
delete it.second;
if (it.second) it.second->Destroy();
}
m_ams_preview_list.clear();
m_ext_image_list.clear();
@@ -624,12 +630,181 @@ void AMSControl::ClearAms() {
m_ams_item_list.clear();
m_sizer_prv_right->Clear();
m_sizer_prv_left->Clear();
m_item_ids = { {}, {} };
if (m_nozzle_book) {
m_nozzle_book->Destroy();
m_nozzle_book = nullptr;
} else {
for (auto &pane : m_nozzle_panes) {
if (pane.preview_panel) pane.preview_panel->Destroy();
if (pane.ams_book) pane.ams_book->Destroy();
if (pane.page) pane.page->Destroy();
}
}
m_item_ids.assign(std::max(m_total_ext_count, 2), {});
m_nozzle_panes.clear();
m_active_nozzle_id = 0;
m_current_ams.clear();
m_current_show_ams_left.clear();
m_current_show_ams_right.clear();
pair_id.clear();
}
void AMSControl::restore_legacy_ams_layout()
{
m_sizer_ams_items->Clear(false);
m_sizer_ams_items->SetOrientation(wxHORIZONTAL);
m_sizer_body->GetItem(m_sizer_ams_items)->SetFlag(wxALIGN_CENTER);
m_sizer_ams_items->Add(m_panel_prv_left, 0, wxLEFT | wxRIGHT, FromDIP(5));
m_sizer_ams_items->Add(m_panel_prv_right, 0, wxLEFT | wxRIGHT, FromDIP(5));
m_panel_prv_left->Show();
m_panel_prv_right->Show();
m_simplebook_ams_left->Show();
m_simplebook_ams_right->Show();
m_panel_down_road->Show();
m_sizer_body->Show(m_sizer_ams_body, true);
m_sizer_option_mid->Show(m_extruder, true);
m_down_road->SetSingleSideLayout(false);
m_sizer_ams_gap->SetProportion(1);
m_sizer_ams_gap->SetBorder(FromDIP(10));
m_down_road->SetNozzleCount(m_total_ext_count);
}
bool AMSControl::use_multi_nozzle_layout() const
{
const bool is_bbl = wxGetApp().preset_bundle && wxGetApp().preset_bundle->is_bbl_vendor();
return is_bbl ? m_total_ext_count > 2 : m_nozzle_book != nullptr;
}
void AMSControl::CreateAmsMultiNozzle(const std::string &series_name, const std::string &printer_type) {
const size_t pane_count = static_cast<size_t>(std::max(m_total_ext_count, 0));
m_nozzle_panes.resize(pane_count);
m_active_nozzle_id = 0;
m_sizer_ams_items->Clear(false);
m_sizer_ams_items->SetOrientation(wxVERTICAL);
m_sizer_body->GetItem(m_sizer_ams_items)->SetFlag(wxALIGN_CENTER);
m_sizer_body->Show(m_sizer_ams_body, false);
m_sizer_ams_gap->SetProportion(0);
m_sizer_ams_gap->SetBorder(0);
m_panel_prv_left->Hide();
m_panel_prv_right->Hide();
m_simplebook_ams_left->Hide();
m_simplebook_ams_right->Hide();
m_panel_down_road->Show();
m_sizer_option_mid->Show(m_extruder, true);
m_switcher->Hide();
m_down_road->SetNozzleCount(1);
m_down_road->SetSingleSideLayout(true, AMSPanelPos::LEFT_PANEL);
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_SINGLE);
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
m_extruder->updateNozzleNum(1, series_name);
m_nozzle_book = new wxSimplebook(m_amswin, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0);
m_nozzle_book->SetMinSize(wxSize(FromDIP(264), -1));
m_nozzle_book->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_BLOCK_BK_COLOUR));
m_sizer_ams_items->Add(m_nozzle_book, 0, wxALIGN_CENTER);
for (size_t nozzle_id = 0; nozzle_id < pane_count; ++nozzle_id) {
auto &pane = m_nozzle_panes[nozzle_id];
// A generic page is one H2C side. Keep the left-side geometry as the
// canonical single-nozzle presentation; the road and AMS item code
// still use the same side-specific paths as the legacy layout.
pane.panel_pos = AMSPanelPos::LEFT_PANEL;
pane.page = new wxPanel(m_nozzle_book, wxID_ANY);
pane.page->SetMinSize(wxSize(FromDIP(264), -1));
pane.page->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_BLOCK_BK_COLOUR));
pane.page_sizer = new wxBoxSizer(wxVERTICAL);
pane.page->SetSizer(pane.page_sizer);
pane.preview_panel = new wxScrolledWindow(pane.page, wxID_ANY);
pane.preview_panel->SetScrollRate(10, 0);
pane.preview_panel->SetSize(AMS_ITEMS_PANEL_SIZE);
pane.preview_panel->SetMinSize(AMS_ITEMS_PANEL_SIZE);
pane.preview_panel->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_BLOCK_BK_COLOUR));
pane.preview_sizer = new wxBoxSizer(wxHORIZONTAL);
pane.preview_panel->SetSizer(pane.preview_sizer);
// Generic lane cards grow to their calculated width for 1-8 lanes. Keep the
// book at the legacy minimum, but allow the page to expand to that width.
pane.ams_book = new wxSimplebook(pane.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, 0);
pane.ams_book->SetMinSize(wxSize(FromDIP(264), -1));
pane.ams_book->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_BLOCK_BK_COLOUR));
// wxALIGN_CENTER does not centre horizontally inside a HORIZONTAL sizer, so a book wider
// than pane.ams_area's own minimum would sit at ams_area's left edge and overflow to the
// right, shifting the whole card column off the shared down-road connector axis. A pair of
// stretch spacers keeps the book centred on the pane (and thus on the connector) at any
// book width; ams_area is expanded below so the spacers have room to work with.
pane.ams_area = new wxBoxSizer(wxHORIZONTAL);
pane.ams_area->AddStretchSpacer(1);
pane.ams_area->Add(pane.ams_book, 0, wxALIGN_CENTER, 0);
pane.ams_area->AddStretchSpacer(1);
pane.page_sizer->Add(pane.preview_panel, 0, wxALIGN_CENTER | wxTOP, FromDIP(5));
pane.page_sizer->Add(pane.ams_area, 0, wxEXPAND | wxTOP, FromDIP(10));
m_nozzle_book->AddPage(pane.page, wxEmptyString, nozzle_id == 0);
}
auto add_info = [this, series_name, printer_type, pane_count](AMSinfo info) {
if (pane_count == 0) return;
info.nozzle_id = std::clamp(info.nozzle_id, 0, static_cast<int>(pane_count) - 1);
AddAmsPreview(info, info.ams_type);
if (info.ams_type == AMSModel::EXT_AMS && info.cans.size() == 1)
AddAms({info}, series_name, printer_type, m_nozzle_panes[info.nozzle_id].panel_pos);
else
AddAms(info);
};
for (const auto &info : m_ams_info)
add_info(info);
for (auto &info : m_ext_info) {
const int virtual_tray_id = std::atoi(info.ams_id.c_str());
if (virtual_tray_id <= VIRTUAL_TRAY_MAIN_ID &&
virtual_tray_id >= VIRTUAL_TRAY_MAIN_ID - static_cast<int>(pane_count) + 1)
info.nozzle_id = VIRTUAL_TRAY_MAIN_ID - virtual_tray_id;
add_info(info);
}
for (auto &pane : m_nozzle_panes) {
if (pane.ams_book && pane.ams_book->GetPageCount() > 0)
pane.ams_book->SetSelection(0);
if (pane.preview_sizer) pane.preview_sizer->Layout();
if (pane.preview_panel) pane.preview_panel->FitInside();
if (pane.ams_book) pane.ams_book->Layout();
if (pane.page) pane.page->Layout();
if (!pane.current_ams.empty()) {
auto preview = m_ams_preview_list.find(pane.current_ams);
if (preview != m_ams_preview_list.end() && preview->second)
preview->second->OnSelected();
}
}
if (m_nozzle_book && pane_count > 0) {
m_nozzle_book->SetSelection(0);
m_current_ams = m_nozzle_panes[0].current_ams;
if (!m_current_ams.empty())
SwitchAms(m_current_ams);
else {
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
}
}
m_amswin->Layout();
m_amswin->Fit();
// Generic N Nozzles
}
void AMSControl::CreateAmsDoubleNozzle(const std::string &series_name, const std::string &printer_type)
{
restore_legacy_ams_layout();
std::vector<AMSinfo> single_info_left;
std::vector<AMSinfo> single_info_right;
@@ -751,6 +926,8 @@ void AMSControl::CreateAmsDoubleNozzle(const std::string &series_name, const std
void AMSControl::CreateAmsSingleNozzle(const std::string &series_name, const std::string &printer_type)
{
restore_legacy_ams_layout();
std::vector<int>m_item_nums{0,0};
std::vector<AMSinfo> single_info;
@@ -979,6 +1156,17 @@ void AMSControl::UpdateAms(const std::string &series_name,
if (m_ams_info[i].routes_to_main_extruder() != ams_info[i].routes_to_main_extruder()) {
fresh = true;
}
if (m_ams_info[i].nozzle_id != ams_info[i].nozzle_id) {
fresh = true;
}
}
for (int i = 0; i < static_cast<int>(m_ext_info.size()); ++i) {
if (m_ext_info[i].ams_id != ext_info[i].ams_id ||
m_ext_info[i].nozzle_id != ext_info[i].nozzle_id ||
m_ext_info[i].ext_type != ext_info[i].ext_type) {
fresh = true;
}
}
}
else{
@@ -993,11 +1181,16 @@ void AMSControl::UpdateAms(const std::string &series_name,
m_dev_id = dev_id;
if (fresh){
ClearAms();
if (m_total_ext_count >= 2){
CreateAmsDoubleNozzle(series_name, printer_type);
}else{
CreateAmsSingleNozzle(series_name, printer_type);
bool is_bbl = wxGetApp().preset_bundle && wxGetApp().preset_bundle->is_bbl_vendor();
if (is_bbl) {
if (m_total_ext_count == 1)
CreateAmsSingleNozzle(series_name, printer_type);
else if (m_total_ext_count == 2)
CreateAmsDoubleNozzle(series_name, printer_type);
}
else
CreateAmsMultiNozzle(series_name, printer_type);
SetSize(wxSize(FromDIP(578), -1));
SetMinSize(wxSize(FromDIP(578), -1));
Layout();
@@ -1070,7 +1263,7 @@ void AMSControl::UpdateAms(const std::string &series_name,
}
/*update ams extruder*/
if (m_extruder->updateNozzleNum(m_total_ext_count, series_name))
if (m_total_ext_count <= 2 && m_extruder->updateNozzleNum(m_total_ext_count, series_name))
{
m_amswin->Layout();
}
@@ -1106,7 +1299,15 @@ void AMSControl::AddAmsPreview(AMSinfo info, AMSModel type)
{
AMSPreview *ams_prv = nullptr;
if (info.routes_to_main_extruder())
// Match AddAms: the generic multi-nozzle layout's preview panel is the visible one for
// non-Bambu vendors at any extruder count, not only above two.
if (use_multi_nozzle_layout()) {
const int nozzle_id = std::clamp(info.nozzle_id, 0, m_total_ext_count - 1);
auto &pane = m_nozzle_panes[nozzle_id];
ams_prv = new AMSPreview(pane.preview_panel, wxID_ANY, info, type);
pane.preview_sizer->Add(ams_prv, 0, wxALIGN_CENTER | wxLEFT, FromDIP(6));
}
else if (info.routes_to_main_extruder())
{
ams_prv = new AMSPreview(m_panel_prv_right, wxID_ANY, info, type);
m_sizer_prv_right->Add(ams_prv, 0, wxALIGN_CENTER | wxLEFT, FromDIP(6));
@@ -1129,6 +1330,54 @@ void AMSControl::AddAmsPreview(AMSinfo info, AMSModel type)
void AMSControl::createAms(wxSimplebook* parent, int& idx, AMSinfo info, AMSPanelPos pos) {
auto ams_item = new AmsItem(parent, info, info.ams_type, pos);
parent->InsertPage(idx, ams_item, wxEmptyString, true);
// Generic AMS cards may be wider than the legacy 264 DIP page. Propagate
// that calculated width through the page and simplebook so 5-8 lanes are
// laid out fully instead of being clipped by the old minimum width.
if (info.ams_type == AMSModel::GENERIC_AMS) {
const int required_width = ams_item->GetMinSize().x;
if (required_width > 0) {
const int page_width = std::max(parent->GetMinSize().x, required_width);
parent->SetMinSize(wxSize(page_width, -1));
if (auto *page = parent->GetParent()) {
page->SetMinSize(wxSize(std::max(page->GetMinSize().x, page_width), -1));
if (auto *nozzle_book = page->GetParent())
nozzle_book->SetMinSize(wxSize(std::max(nozzle_book->GetMinSize().x, page_width), -1));
}
}
// Above eight lanes, AmsItem is a fixed-minimum viewport over a wider
// lane strip. Expand the viewport through its page's sizers rather
// than propagating the strip's virtual width into the card/book.
// Reset back to the centered fixed layout for eight or fewer lanes so a
// later small unit is not left in expand mode.
const bool generic_lane_layout = !wxGetApp().preset_bundle || !wxGetApp().preset_bundle->is_bbl_vendor();
if (generic_lane_layout) {
if (auto *page = parent->GetParent()) {
auto pane = std::find_if(m_nozzle_panes.begin(), m_nozzle_panes.end(),
[page](const NozzleAmsPane &candidate) { return candidate.page == page; });
if (pane != m_nozzle_panes.end() && m_nozzle_book) {
const bool expand_viewport = info.cans.size() > 8;
if (auto *book_item = pane->ams_area->GetItem(pane->ams_book)) {
book_item->SetProportion(expand_viewport ? 1 : 0);
book_item->SetFlag(expand_viewport ? wxEXPAND : wxALIGN_CENTER);
}
// Keep the centring spacers (items 0 and 2) elastic only in the centred layout;
// in the >8-lane viewport they must collapse so the book can fill ams_area.
if (auto *lead = pane->ams_area->GetItem(static_cast<size_t>(0)))
lead->SetProportion(expand_viewport ? 0 : 1);
if (auto *trail = pane->ams_area->GetItem(static_cast<size_t>(2)))
trail->SetProportion(expand_viewport ? 0 : 1);
if (auto *area_item = pane->page_sizer->GetItem(pane->ams_area))
area_item->SetFlag(wxEXPAND | wxTOP);
if (auto *nozzle_book_item = m_sizer_ams_items->GetItem(m_nozzle_book))
nozzle_book_item->SetFlag(expand_viewport ? wxEXPAND : wxALIGN_CENTER);
if (auto *items_item = m_sizer_body->GetItem(m_sizer_ams_items))
items_item->SetFlag(expand_viewport ? wxEXPAND : wxALIGN_CENTER);
m_amswin->Layout();
}
}
}
}
ams_item->set_selection(idx);
idx++;
@@ -1153,7 +1402,10 @@ AMSRoadShowMode AMSControl::findFirstMode(AMSPanelPos pos) {
if (item->second->get_ams_model() == AMSModel::EXT_AMS && item->second->get_ext_type() == AMSModelOriginType::LITE_EXT) return AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE;
return AMSRoadShowMode::AMS_ROAD_MODE_FOUR;
}
else{
else if (item->second->get_can_count() > GENERIC_AMS_SLOT_NUM) {
return AMSRoadShowMode::AMS_ROAD_MODE_GENERIC;
}
else {
for (auto ids : pair_id){
if (ids.first == ams_id || ids.second == ams_id){
return AMSRoadShowMode::AMS_ROAD_MODE_DOUBLE;
@@ -1172,8 +1424,13 @@ void AMSControl::createAmsPanel(wxSimplebook *parent, int &idx, std::vector<AMSi
wxPanel* book_panel = new wxPanel(parent);
wxBoxSizer* book_sizer = new wxBoxSizer(wxHORIZONTAL);
book_panel->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_LIB_BK_COLOUR));
book_panel->SetSize(AMS_PANEL_SIZE);
book_panel->SetMinSize(AMS_PANEL_SIZE);
// Size the page to the AMS book it becomes, not to the legacy fixed width: an AMS card is
// added directly as the book page (so it always spans the book), while a single external
// spool is wrapped here. Matching the book keeps the Ext card centred on the same axis as
// the AMS cards and the shared down-road connector anchored below them.
const wxSize book_panel_size(std::max(parent->GetMinSize().x, AMS_PANEL_SIZE.x), AMS_PANEL_SIZE.y);
book_panel->SetSize(book_panel_size);
book_panel->SetMinSize(book_panel_size);
AmsItem* ams1 = nullptr, * ams2 = nullptr;
ams1 = new AmsItem(book_panel, infos[0], infos[0].ams_type, pos);
@@ -1200,6 +1457,14 @@ void AMSControl::createAmsPanel(wxSimplebook *parent, int &idx, std::vector<AMSi
//book_sizer->Add(ams1, 0, wxALIGN_CENTER_HORIZONTAL, 0);
book_sizer->Add(ams1, 0, wxLEFT, (book_panel->GetSize().x - ams1->GetSize().x) / 2);
}
else if (pos == AMSPanelPos::SINGLE_PANEL) {
// Centre the single external-spool card explicitly, using the same fixed-margin
// technique as the Lite-EXT branch above. Two stretch spacers centred the card
// against the wrapper's own width rather than the AMS book, which left the card
// (and the connector drawn below it) off the book/connector axis.
book_sizer->Add(ams1, 0, wxLEFT,
std::max(0, (book_panel_size.x - ams1->GetSize().x) / 2));
}
else{
auto ext_image = new AMSExtImage(book_panel, pos, m_total_ext_count, false);
book_sizer->Add(ams1, 0, wxLEFT, FromDIP(30));
@@ -1228,7 +1493,20 @@ void AMSControl::createAmsPanel(wxSimplebook *parent, int &idx, std::vector<AMSi
void AMSControl::AddAms(AMSinfo info, AMSPanelPos pos)
{
if (m_total_ext_count > 1){
// The generic multi-nozzle layout (non-Bambu vendors) owns the only visible book; a unit must
// land there for any extruder count. Keying off m_total_ext_count instead sent 1- and 2-extruder
// generic printers into the legacy simplebooks that CreateAmsMultiNozzle hides.
if (use_multi_nozzle_layout()) {
const int nozzle_id = std::clamp(info.nozzle_id, 0, m_total_ext_count - 1);
auto &pane = m_nozzle_panes[nozzle_id];
pane.item_ids.push_back(info.ams_id);
m_item_ids[nozzle_id].push_back(info.ams_id);
if (pane.current_ams.empty()) pane.current_ams = info.ams_id;
createAms(pane.ams_book, pane.page_index, info, AMSPanelPos::SINGLE_PANEL);
pane.ams_book->Fit();
pane.ams_book->Layout();
}
else if (m_total_ext_count == 2){
if (info.routes_to_main_extruder()){
createAms(m_simplebook_ams_right, m_right_page_index, info, AMSPanelPos::RIGHT_PANEL);
}
@@ -1239,6 +1517,9 @@ void AMSControl::AddAms(AMSinfo info, AMSPanelPos pos)
else if (m_total_ext_count == 1){
createAms(m_simplebook_ams_left, m_left_page_index, info, AMSPanelPos::LEFT_PANEL);
}
else {
// Generic N Nozzles
}
m_simplebook_ams_left->Layout();
m_simplebook_ams_right->Layout();
m_simplebook_ams_left->Refresh();
@@ -1269,7 +1550,19 @@ void AMSControl::AddAms(std::vector<AMSinfo> single_info, const std::string &ser
if (single_info.size() <= 0){
return;
}
if (m_total_ext_count == 2) {
if (use_multi_nozzle_layout()) {
const int nozzle_id = std::clamp(single_info.front().nozzle_id, 0, m_total_ext_count - 1);
auto &pane = m_nozzle_panes[nozzle_id];
for (const auto &info : single_info) {
pane.item_ids.push_back(info.ams_id);
m_item_ids[nozzle_id].push_back(info.ams_id);
}
if (pane.current_ams.empty()) pane.current_ams = single_info.front().ams_id;
createAmsPanel(pane.ams_book, pane.page_index, single_info, series_name, printer_type, AMSPanelPos::SINGLE_PANEL, m_total_ext_count);
pane.ams_book->Fit();
pane.ams_book->Layout();
}
else if (m_total_ext_count == 2) {
if (single_info[0].routes_to_main_extruder()) {
createAmsPanel(m_simplebook_ams_right, m_right_page_index, single_info, series_name, printer_type, AMSPanelPos::RIGHT_PANEL, m_total_ext_count);
}
@@ -1285,6 +1578,9 @@ void AMSControl::AddAms(std::vector<AMSinfo> single_info, const std::string &ser
createAmsPanel(m_simplebook_ams_left, m_left_page_index, single_info, series_name, printer_type, AMSPanelPos::LEFT_PANEL, m_total_ext_count);
}
}
else {
// Generic N Nozzles
}
m_simplebook_ams_left->Layout();
m_simplebook_ams_right->Layout();
@@ -1322,6 +1618,21 @@ void AMSControl::AddAms(std::vector<AMSinfo> single_info, const std::string &ser
void AMSControl::AddAmsPreview(std::vector<AMSinfo>single_info, AMSPanelPos pos) {
if (single_info.size() <= 0) return;
if (use_multi_nozzle_layout()) {
const int nozzle_id = std::clamp(single_info.front().nozzle_id, 0, m_total_ext_count - 1);
auto &pane = m_nozzle_panes[nozzle_id];
for (const auto &info : single_info) {
auto *ams_prv = new AMSPreview(pane.preview_panel, wxID_ANY, info, info.ams_type);
pane.preview_sizer->Add(ams_prv, 0, wxALIGN_CENTER | wxLEFT, FromDIP(6));
ams_prv->Bind(wxEVT_LEFT_DOWN, [this, ams_prv](wxMouseEvent &e) {
SwitchAms(ams_prv->get_ams_id());
e.Skip();
});
m_ams_preview_list[info.ams_id] = ams_prv;
}
return;
}
AMSPreview* ams_prv = nullptr;
AMSPreview* ams_prv2 = nullptr;
if (pos == AMSPanelPos::RIGHT_PANEL){
@@ -1360,8 +1671,97 @@ void AMSControl::AddAmsPreview(std::vector<AMSinfo>single_info, AMSPanelPos pos)
}
}
void AMSControl::SelectNozzle(int nozzle_id)
{
if (nozzle_id < 0 || nozzle_id >= static_cast<int>(m_nozzle_panes.size()))
return;
if (m_active_nozzle_id == nozzle_id &&
(!m_nozzle_book || m_nozzle_book->GetSelection() == nozzle_id))
return;
m_active_nozzle_id = nozzle_id;
if (m_nozzle_book && m_nozzle_book->GetSelection() != nozzle_id)
m_nozzle_book->SetSelection(nozzle_id);
m_current_ams = m_nozzle_panes[nozzle_id].current_ams;
if (!m_current_ams.empty()) {
SwitchAms(m_current_ams);
return;
}
if (m_down_road) {
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
}
if (m_extruder)
m_extruder->OnAmsLoading(false, 0);
post_event(SimpleEvent(EVT_AMS_SWITCH));
}
void AMSControl::SwitchAms(std::string ams_id)
{
if (use_multi_nozzle_layout()) {
NozzleAmsPane *pane = nullptr;
int nozzle_id = -1;
for (int index = 0; index < static_cast<int>(m_nozzle_panes.size()); ++index) {
auto &candidate = m_nozzle_panes[index];
if (std::find(candidate.item_ids.begin(), candidate.item_ids.end(), ams_id) != candidate.item_ids.end()) {
pane = &candidate;
nozzle_id = index;
break;
}
}
if (!pane) return;
m_active_nozzle_id = nozzle_id;
if (m_nozzle_book && m_nozzle_book->GetSelection() != nozzle_id)
m_nozzle_book->SetSelection(nozzle_id);
pane->current_ams = ams_id;
m_current_ams = ams_id;
m_current_select = ams_id;
for (const auto &id : pane->item_ids) {
auto preview = m_ams_preview_list.find(id);
if (preview == m_ams_preview_list.end() || !preview->second) continue;
if (id == ams_id) preview->second->OnSelected();
else preview->second->UnSelected();
}
auto item = m_ams_item_list.find(ams_id);
if (item != m_ams_item_list.end() && item->second) {
if (pane->ams_book)
pane->ams_book->SetSelection(item->second->get_selection());
AMSRoadShowMode road_mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE;
if (item->second->get_can_count() == GENERIC_AMS_SLOT_NUM)
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_FOUR;
else if (item->second->get_can_count() > GENERIC_AMS_SLOT_NUM)
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_GENERIC;
else if (item->second->get_ams_model() == AMSModel::N3S_AMS)
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE_N3S;
else if (item->second->get_ams_model() == AMSModel::AMS_LITE ||
(item->second->get_ams_model() == AMSModel::EXT_AMS &&
item->second->get_ext_type() == AMSModelOriginType::LITE_EXT))
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE;
if (m_down_road) {
if (pane->panel_pos == AMSPanelPos::LEFT_PANEL) {
m_down_road->UpdateLeft(1, road_mode);
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
} else {
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
m_down_road->UpdateRight(1, road_mode);
}
m_down_road->UpdatePassRoad(pane->panel_pos, -1, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE);
}
if (m_extruder)
m_extruder->OnAmsLoading(false, 0);
}
post_event(SimpleEvent(EVT_AMS_SWITCH));
return;
}
if(ams_id == m_current_show_ams_left || ams_id == m_current_show_ams_right){return;}
bool is_in_right = IsAmsInRightPanel(ams_id);
@@ -1432,13 +1832,17 @@ void AMSControl::SwitchAms(std::string ams_id)
else {
AMSRoadShowMode mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE;
if (item->get_ams_model() == AMSModel::N3S_AMS)
if (item->get_can_count() > GENERIC_AMS_SLOT_NUM)
mode = AMSRoadShowMode::AMS_ROAD_MODE_GENERIC;
else if (item->get_ams_model() == AMSModel::N3S_AMS)
mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE_N3S;
for (auto it : pair_id) {
if (it.first == ams_id || it.second == ams_id) {
mode = AMSRoadShowMode::AMS_ROAD_MODE_DOUBLE;
break;
if (item->get_can_count() <= GENERIC_AMS_SLOT_NUM) {
for (auto it : pair_id) {
if (it.first == ams_id || it.second == ams_id) {
mode = AMSRoadShowMode::AMS_ROAD_MODE_DOUBLE;
break;
}
}
}
pos == AMSPanelPos::LEFT_PANEL ? m_down_road->UpdateLeft(m_total_ext_count, mode)
@@ -1500,6 +1904,20 @@ void AMSControl::SetExtruder(bool on_off, int nozzle_id, std::string ams_id, std
AmsItem *item = nullptr;
if (m_ams_item_list.find(ams_id) != m_ams_item_list.end()) { item = m_ams_item_list[ams_id]; }
if (use_multi_nozzle_layout()) {
if (nozzle_id < 0 || nozzle_id >= static_cast<int>(m_nozzle_panes.size()))
return;
if (!m_extruder)
return;
if (on_off && item)
m_extruder->OnAmsLoading(true, 0, item->GetTagColr(slot_id));
else
m_extruder->OnAmsLoading(false, 0);
return;
}
if (on_off && item) {
auto col = item->GetTagColr(slot_id);
m_extruder->OnAmsLoading(true, nozzle_id, col);
@@ -1624,6 +2042,69 @@ void AMSControl::SetAmsStep(std::string ams_id, std::string canid, AMSPassRoadTy
else{
return;
}
if (use_multi_nozzle_layout()) {
const int nozzle_id = std::clamp(ams->get_nozzle_id(), 0, m_total_ext_count - 1);
if (nozzle_id >= static_cast<int>(m_nozzle_panes.size()))
return;
auto &pane = m_nozzle_panes[nozzle_id];
AMSRoadShowMode road_mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE;
if (ams->get_can_count() == GENERIC_AMS_SLOT_NUM)
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_FOUR;
else if (ams->get_can_count() > GENERIC_AMS_SLOT_NUM)
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_GENERIC;
else if (ams->get_ams_model() == AMSModel::N3S_AMS)
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_SINGLE_N3S;
else if (ams->get_ams_model() == AMSModel::AMS_LITE ||
(ams->get_ams_model() == AMSModel::EXT_AMS && ams->get_ext_type() == AMSModelOriginType::LITE_EXT))
road_mode = AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE;
if (can_index >= 0 && can_index < static_cast<int>(info.cans.size()) && m_down_road)
m_down_road->SetPassRoadColour(pane.panel_pos == AMSPanelPos::LEFT_PANEL,
info.cans[can_index].material_colour);
if (m_down_road) {
if (pane.panel_pos == AMSPanelPos::LEFT_PANEL) {
m_down_road->UpdateLeft(1, road_mode);
m_down_road->UpdateRight(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
} else {
m_down_road->UpdateLeft(1, AMSRoadShowMode::AMS_ROAD_MODE_NONE);
m_down_road->UpdateRight(1, road_mode);
}
}
int road_length = 0;
if (step == AMSPassRoadSTEP::AMS_ROAD_STEP_NONE) {
ams->SetAmsStep(ams_id, canid, type, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE);
if (m_down_road)
m_down_road->UpdatePassRoad(pane.panel_pos, -1, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE);
if (m_extruder)
m_extruder->OnAmsLoading(false, 0);
}
else if (step == AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP1) {
ams->SetAmsStep(ams_id, canid, type, AMSPassRoadSTEP::AMS_ROAD_STEP_1);
if (m_down_road)
m_down_road->UpdatePassRoad(pane.panel_pos, road_length, AMSPassRoadSTEP::AMS_ROAD_STEP_1);
if (m_extruder)
m_extruder->OnAmsLoading(false, 0);
}
else if (step == AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP2) {
ams->SetAmsStep(ams_id, canid, type, AMSPassRoadSTEP::AMS_ROAD_STEP_2);
if (m_down_road)
m_down_road->UpdatePassRoad(pane.panel_pos, road_length, AMSPassRoadSTEP::AMS_ROAD_STEP_2);
if (m_extruder)
m_extruder->OnAmsLoading(true, 0, ams->GetTagColr(canid));
}
else if (step == AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP3) {
ams->SetAmsStep(ams_id, canid, type, AMSPassRoadSTEP::AMS_ROAD_STEP_3);
if (m_down_road)
m_down_road->UpdatePassRoad(pane.panel_pos, road_length, AMSPassRoadSTEP::AMS_ROAD_STEP_3);
if (m_extruder)
m_extruder->OnAmsLoading(true, 0, ams->GetTagColr(canid));
}
return;
}
if (can_index >= 0 && can_index < info.cans.size())
{
m_down_road->SetPassRoadColour(left, info.cans[can_index].material_colour);
+27
View File
@@ -47,6 +47,20 @@ public:
void on_retry();
protected:
struct NozzleAmsPane
{
wxPanel* page{nullptr};
wxBoxSizer* page_sizer{nullptr};
wxScrolledWindow* preview_panel{nullptr};
wxBoxSizer* preview_sizer{nullptr};
wxSimplebook* ams_book{nullptr};
wxBoxSizer* ams_area{nullptr};
AMSPanelPos panel_pos{AMSPanelPos::LEFT_PANEL};
std::vector<std::string> item_ids;
std::string current_ams;
int page_index{0};
};
std::string m_current_ams;
std::string m_current_slot_left;
std::string m_current_slot_right;
@@ -63,6 +77,9 @@ protected:
std::string m_dev_id;
std::vector<std::vector<std::string>> m_item_ids{ {}, {} };
std::vector<NozzleAmsPane> m_nozzle_panes;
wxSimplebook* m_nozzle_book{nullptr};
int m_active_nozzle_id{0};
std::vector<std::pair<std::string, std::string>> pair_id;
int m_total_ext_count = 1;
@@ -103,6 +120,7 @@ protected:
wxSimplebook *m_simplebook_ams_left{nullptr};
wxSimplebook *m_simplebook_ams_right{ nullptr };
wxSimplebook *m_simplebook_bottom{nullptr};
wxSizerItem *m_sizer_ams_gap{nullptr};
wxPanel *m_panel_down_road{ nullptr };
int m_left_page_index = 0;
int m_right_page_index = 0;
@@ -156,6 +174,7 @@ public:
void createAms(wxSimplebook* parent, int& idx, AMSinfo info, AMSPanelPos pos);
void createAmsPanel(wxSimplebook *parent, int &idx, std::vector<AMSinfo> infos, const std::string &series_name, const std::string &printer_type, AMSPanelPos pos, int total_ext_num);
AMSRoadShowMode findFirstMode(AMSPanelPos pos);
void restore_legacy_ams_layout();
AMSModel m_ams_model{AMSModel::EXT_AMS};
AMSModel m_ext_model{AMSModel::EXT_AMS};
@@ -183,6 +202,7 @@ public:
void UpdatePassRoad(std::string ams_id, AMSPassRoadType type, AMSPassRoadSTEP step);
void CreateAms();
void CreateAmsMultiNozzle(const std::string& series_name, const std::string& printer_type);
void CreateAmsDoubleNozzle(const std::string &series_name, const std::string& printer_type);
void CreateAmsSingleNozzle(const std::string &series_name, const std::string &printer_type);
void ClearAms();
@@ -210,6 +230,8 @@ public:
void SetExtruder(bool on_off, int nozzle_id, std::string ams_id, std::string slot_id);
void SetAmsStep(std::string ams_id, std::string canid, AMSPassRoadType type, AMSPassRoadSTEP step);
void SwitchAms(std::string ams_id);
void SelectNozzle(int nozzle_id);
int GetActiveNozzleId() const { return m_active_nozzle_id; }
void msw_rescale();
void on_filament_load(wxCommandEvent &event);
@@ -234,6 +256,11 @@ public:
private:
std::string get_filament_id(const std::string& ams_id, const std::string& can_id);
// True when AMS units are laid out on the generic multi-nozzle book. Bambu
// printers keep the legacy count-based single/double-nozzle layout at any
// extruder count; other vendors use the book created for them.
bool use_multi_nozzle_layout() const;
public:
std::string m_current_select;
};
+317 -54
View File
@@ -42,6 +42,7 @@
#include <boost/log/trivial.hpp>
#include <wx/timer.h>
#include <wx/sizer.h>
#include <wx/scrolwin.h>
#include "CalibUtils.hpp"
#include "slic3r/GUI/wxExtensions.hpp"
@@ -76,6 +77,48 @@ namespace Slic3r { namespace GUI {
//#define AMS_SINGLE_CAN_SIZE wxSize(FromDIP(78), 144)
#define AMS_CANS_WINDOW_SIZE wxSize(FromDIP(264), FromDIP(174))
#define AMS_SINGLE_CAN_SIZE wxSize(FromDIP(78), FromDIP(174))
int generic_ams_layout_width(wxWindow *window, int lane_count)
{
const int base_width = window->FromDIP(264);
const int lane_width = window->FromDIP(52);
const int lane_gap = window->FromDIP(4);
if (lane_count <= 4)
return base_width;
// Lane centers are distributed as width * (index + 1) / (count + 1).
// Keep at least one card width plus the requested gap between centers.
return std::max(base_width, (lane_count + 1) * (lane_width + lane_gap));
}
std::vector<int> generic_ams_lane_centers(int lane_count, int width)
{
std::vector<int> centers;
if (lane_count <= 0)
return centers;
centers.reserve(lane_count);
for (int lane = 0; lane < lane_count; ++lane)
centers.push_back(width * (lane + 1) / (lane_count + 1));
return centers;
}
int generic_ams_preview_width(wxWindow *window, int lane_count)
{
const int base_width = window->FromDIP(52);
const int lane_width = window->FromDIP(9);
const int lane_gap = window->FromDIP(4);
if (lane_count <= 0)
return base_width;
return std::max(base_width, lane_count * lane_width + (lane_count + 1) * lane_gap);
}
bool use_generic_ams_layout(AMSModel model)
{
return model == AMSModel::GENERIC_AMS &&
(!wxGetApp().preset_bundle || !wxGetApp().preset_bundle->is_bbl_vendor());
}
bool AMSinfo::parse_ams_info(MachineObject *obj, DevAms *ams, bool remain_flag, bool humidity_flag)
{
if (!ams) return false;
@@ -170,10 +213,14 @@ void AMSinfo::parse_ext_info(MachineObject* obj, DevAmsTray tray) {
info.can_id = std::to_string(0);
this->cans.clear();
if (tray.id == std::to_string(VIRTUAL_TRAY_MAIN_ID))
try {
const int virtual_tray_id = std::stoi(tray.id);
if (virtual_tray_id >= 0 && virtual_tray_id <= VIRTUAL_TRAY_MAIN_ID)
this->nozzle_id = VIRTUAL_TRAY_MAIN_ID - virtual_tray_id;
}
catch (...) {
this->nozzle_id = 0;
else if (tray.id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID))
this->nozzle_id = 1;
}
if (tray.is_tray_info_ready()) {
info.ctype = tray.ctype;
@@ -856,7 +903,11 @@ AMSextruder::AMSextruder(wxWindow *parent, wxWindowID id, int nozzle_num, const
void AMSextruder::TurnOff()
{
m_left_extruder->TurnOff();
if (m_left_extruder) m_left_extruder->TurnOff();
if (m_right_extruder) m_right_extruder->TurnOff();
for (auto *extruder : m_nozzle_extruders) {
if (extruder) extruder->TurnOff();
}
}
void AMSextruder::create(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, int nozzle_num)
@@ -882,7 +933,7 @@ void AMSextruder::OnAmsLoading(bool load, int nozzle_id, wxColour col /*= AMS_CO
m_left_extruder->OnAmsLoading(load, col);
if (load) m_current_colur_deputy = col;
}
else if (m_nozzle_num > 1){
else if (m_nozzle_num == 2){
if (nozzle_id == MAIN_EXTRUDER_ID) {
m_right_extruder->OnAmsLoading(load, col);
if (m_current_colur != col){
@@ -896,6 +947,15 @@ void AMSextruder::OnAmsLoading(bool load, int nozzle_id, wxColour col /*= AMS_CO
}
}
}
else if (m_nozzle_num > 2) {
if (nozzle_id >= 0 && nozzle_id < static_cast<int>(m_nozzle_extruders.size())) {
m_nozzle_extruders[nozzle_id]->OnAmsLoading(load, col);
if (load) m_current_colur = col;
}
}
else {
// Generic N Nozzles
}
}
/*return true if something is updated*/
@@ -905,12 +965,26 @@ bool AMSextruder::updateNozzleNum(int nozzle_num, const std::string& series_name
m_series_name = series_name;
m_nozzle_num = nozzle_num;
this->DestroyChildren();
m_left_extruder = nullptr;
m_right_extruder = nullptr;
m_nozzle_extruders.clear();
m_right_extruder = new AMSextruderImage(this, wxID_ANY, "right_nozzle", AMS_EXTRUDER_DOUBLE_NOZZLE_BITMAP_SIZE);
m_bitmap_sizer = new wxBoxSizer(wxHORIZONTAL);
if (m_nozzle_num == 1) {
if (MachineObject::is_series_n(m_series_name)) {
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_n", AMS_EXTRUDER_SINGLE_NOZZLE_N_SIZE);
} else if (MachineObject::is_series_x(m_series_name) || MachineObject::is_series_p(m_series_name)) {
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_xp", AMS_EXTRUDER_SINGLE_NOZZLE_XP_SIZE);
} else {
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_xp", AMS_EXTRUDER_SINGLE_NOZZLE_XP_SIZE);
}
wxBoxSizer* m_bitmap_sizer = new wxBoxSizer(wxHORIZONTAL);
if (m_nozzle_num >= 2)
m_left_extruder->setShowState(true);
m_bitmap_sizer->Add(m_left_extruder, 0, wxALIGN_LEFT | wxALIGN_TOP, 0);
}
else if (m_nozzle_num == 2)
{
m_right_extruder = new AMSextruderImage(this, wxID_ANY, "right_nozzle", AMS_EXTRUDER_DOUBLE_NOZZLE_BITMAP_SIZE);
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "left_nozzle", AMS_EXTRUDER_DOUBLE_NOZZLE_BITMAP_SIZE);
m_left_extruder->setShowState(true);
m_right_extruder->setShowState(true);
@@ -918,25 +992,17 @@ bool AMSextruder::updateNozzleNum(int nozzle_num, const std::string& series_name
m_bitmap_sizer->Add(m_right_extruder, 0, wxLEFT | wxALIGN_TOP, FromDIP(2));
m_bitmap_sizer->AddSpacer(2);
}
else if (m_nozzle_num > 2) {
for (int nozzle_id = 0; nozzle_id < m_nozzle_num; ++nozzle_id) {
auto *extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_n", AMS_EXTRUDER_SINGLE_NOZZLE_N_SIZE);
extruder->setShowState(true);
m_nozzle_extruders.push_back(extruder);
m_bitmap_sizer->Add(extruder, 0, wxALIGN_LEFT | wxALIGN_TOP, 0);
}
}
else
{
if (MachineObject::is_series_n(m_series_name))
{
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_n", AMS_EXTRUDER_SINGLE_NOZZLE_N_SIZE);
}
else if(MachineObject::is_series_x(m_series_name) || MachineObject::is_series_p(m_series_name))
{
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_xp", AMS_EXTRUDER_SINGLE_NOZZLE_XP_SIZE);
}
else
{
m_left_extruder = new AMSextruderImage(this, wxID_ANY, "single_nozzle_xp", AMS_EXTRUDER_SINGLE_NOZZLE_XP_SIZE);
}
m_left_extruder->setShowState(true);
m_right_extruder->setShowState(false);
m_bitmap_sizer->Add(m_left_extruder, 0, wxALIGN_LEFT | wxALIGN_TOP, 0);
m_bitmap_sizer->Add(m_right_extruder, 0, wxLEFT | wxALIGN_TOP, FromDIP(3));
// Generic N Nozzles
}
SetSizer(m_bitmap_sizer);
@@ -951,6 +1017,9 @@ void AMSextruder::msw_rescale()
//m_amsSextruder->msw_rescale();
if (m_left_extruder) m_left_extruder->msw_rescale();
if (m_right_extruder) m_right_extruder->msw_rescale();
for (auto *extruder : m_nozzle_extruders) {
if (extruder) extruder->msw_rescale();
}
Layout();
Update();
Refresh();
@@ -2123,7 +2192,10 @@ AMSRoadUpPart::AMSRoadUpPart(wxWindow* parent, wxWindowID id, AMSinfo info, AMSM
m_ams_model = model;
if (m_ams_model == AMSModel::GENERIC_AMS){
create(parent, id, pos, wxSize(FromDIP(264), FromDIP(34)));
const int width = use_generic_ams_layout(m_ams_model)
? generic_ams_layout_width(this, static_cast<int>(m_amsinfo.cans.size()))
: FromDIP(264);
create(parent, id, pos, wxSize(width, FromDIP(34)));
}
else{
create(parent, id, pos, wxSize(FromDIP(78), FromDIP(34)));
@@ -2235,6 +2307,38 @@ void AMSRoadUpPart::doRender(wxDC& dc)
dc.SetPen(wxPen(AMS_CONTROL_GRAY500, 2, wxPENSTYLE_SOLID));
dc.SetBrush(wxBrush(*wxTRANSPARENT_BRUSH));
if (use_generic_ams_layout(m_ams_model)) {
const int lane_count = static_cast<int>(m_amsinfo.cans.size());
if (lane_count <= 0)
return;
const auto lane_centers = generic_ams_lane_centers(lane_count, size.x);
const int height = FromDIP(21);
for (const int lane_center : lane_centers)
dc.DrawLine(lane_center, 0, lane_center, height);
if (lane_centers.size() > 1)
dc.DrawLine(lane_centers.front(), height, lane_centers.back(), height);
dc.DrawLine(size.x / 2, height, size.x / 2, size.y);
if (m_load_step != AMSPassRoadSTEP::AMS_ROAD_STEP_NONE &&
m_load_slot_index >= 0 && m_load_slot_index < lane_count) {
const int lane_center = lane_centers[m_load_slot_index];
dc.SetPen(wxPen(_get_diff_clr(this, m_amsinfo.cans[m_load_slot_index].material_colour), 4, wxPENSTYLE_SOLID));
dc.DrawLine(lane_center, 0, lane_center, height);
dc.DrawLine(std::min(lane_center, size.x / 2), height,
std::max(lane_center, size.x / 2), height);
if (m_load_step == AMSPassRoadSTEP::AMS_ROAD_STEP_2 ||
m_load_step == AMSPassRoadSTEP::AMS_ROAD_STEP_3)
dc.DrawLine(size.x / 2, height, size.x / 2, size.y);
}
dc.SetPen(*wxTRANSPARENT_PEN);
dc.SetBrush(wxBrush(wxColour(194, 194, 194)));
dc.DrawRectangle(size.x / 2 - FromDIP(14), height - FromDIP(5), FromDIP(28), FromDIP(10));
return;
}
if ((m_ams_model == N3S_AMS || m_ams_model == EXT_AMS) && m_amsinfo.cans.size() != 4){
dc.DrawLine(((float)size.x / 2), (0), ((float)size.x / 2), (size.y));
if (m_load_step == AMSPassRoadSTEP::AMS_ROAD_STEP_2 || m_load_step == AMSPassRoadSTEP::AMS_ROAD_STEP_3){
@@ -2360,6 +2464,25 @@ void AMSRoadDownPart::UpdateRight(int nozzle_num, AMSRoadShowMode mode)
Refresh();
}
void AMSRoadDownPart::SetNozzleCount(int nozzle_num)
{
const int new_nozzle_num = std::max(1, nozzle_num);
if (m_nozzle_num == new_nozzle_num) return;
m_nozzle_num = new_nozzle_num;
Refresh();
}
void AMSRoadDownPart::SetSingleSideLayout(bool enabled, AMSPanelPos pos)
{
if (m_single_side_layout == enabled && m_single_side_pos == pos)
return;
m_single_side_layout = enabled;
m_single_side_pos = pos;
Refresh();
}
void AMSRoadDownPart::OnVamsLoading(bool load, wxColour col /*= AMS_CONTROL_GRAY500*/)
{
/*m_vams_loading = load;
@@ -2433,8 +2556,14 @@ void AMSRoadDownPart::doRender(wxDC& dc)
/*if (m_road_color.Alpha() == 0) { dc.SetPen(wxPen(*wxWHITE, m_passroad_width, wxPENSTYLE_SOLID)); }
else { dc.SetPen(wxPen(m_road_color, m_passroad_width, wxPENSTYLE_SOLID)); }*/
dc.SetPen(wxPen(AMS_CONTROL_GRAY500, 2, wxPENSTYLE_SOLID));
auto xpos = left_nozzle_pos.x;
if (m_left_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_NONE || m_right_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_NONE){
if (m_single_side_layout) {
dc.DrawLine(size.x / 2, 0, size.x / 2, size.y);
}
else if (
(m_left_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_NONE ||
m_right_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_NONE)){
auto length = 50;
if (m_left_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE || m_right_rode_mode == AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE)
length = -13;
@@ -2465,6 +2594,9 @@ void AMSRoadDownPart::doRender(wxDC& dc)
case AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE:
dc.DrawLine(left_nozzle_pos.x, 0, left_nozzle_pos.x, size.y);
break;
case AMSRoadShowMode::AMS_ROAD_MODE_GENERIC:
// Generic N-lane geometry is intentionally left unimplemented.
break;
default:
break;
}
@@ -2492,23 +2624,41 @@ void AMSRoadDownPart::doRender(wxDC& dc)
dc.DrawLine(left_nozzle_pos.x, (size.y / 2), left_nozzle_pos.x + FromDIP(145), (size.y / 2));
dc.DrawLine(left_nozzle_pos.x + FromDIP(145), 0, left_nozzle_pos.x + FromDIP(145), (size.y / 2));
break;
case AMSRoadShowMode::AMS_ROAD_MODE_GENERIC:
// Generic N-lane geometry is intentionally left unimplemented.
break;
default:
break;
}
}
if (m_single_side_layout) {
if (m_pass_road_left_step == AMSPassRoadSTEP::AMS_ROAD_STEP_2 ||
m_pass_road_left_step == AMSPassRoadSTEP::AMS_ROAD_STEP_3) {
dc.SetPen(wxPen(_get_diff_clr(this, m_road_color[1]), 4, wxPENSTYLE_SOLID));
dc.DrawLine(size.x / 2, 0, size.x / 2, size.y);
}
return;
}
if (m_right_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_AMS_LITE){
if (m_nozzle_num == 2) {
if (m_nozzle_num == 1) {
if (m_right_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_NONE && m_left_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_NONE) {
dc.DrawLine((left_nozzle_pos.x), (size.y / 2), (right_nozzle_pos.x), (size.y / 2));
}
const auto &nozzle_pos = m_single_side_layout && m_single_side_pos == AMSPanelPos::RIGHT_PANEL
? right_nozzle_pos
: left_nozzle_pos;
dc.DrawLine(nozzle_pos.x, (size.y / 2), nozzle_pos.x, (size.y));
}
else if (m_nozzle_num == 2) {
/*dc.DrawLine(FromDIP(left_nozzle_pos.x), FromDIP(size.y / 2), FromDIP(left_nozzle_pos.x), FromDIP(size.y));
dc.DrawLine(FromDIP(right_nozzle_pos.x), FromDIP(size.y / 2), FromDIP(right_nozzle_pos.x), FromDIP(size.y));*/
dc.DrawLine((left_nozzle_pos.x), (size.y / 2), (left_nozzle_pos.x), (size.y));
dc.DrawLine((right_nozzle_pos.x), (size.y / 2), (right_nozzle_pos.x), (size.y));
}
else {
if (m_right_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_NONE && m_left_rode_mode != AMSRoadShowMode::AMS_ROAD_MODE_NONE) {
dc.DrawLine((left_nozzle_pos.x), (size.y / 2), (right_nozzle_pos.x), (size.y / 2));
}
dc.DrawLine((left_nozzle_pos.x), (size.y / 2), (left_nozzle_pos.x), (size.y));
// Generic N Nozzles
}
}
@@ -2533,10 +2683,16 @@ void AMSRoadDownPart::doRender(wxDC& dc)
dc.DrawLine(xpos, size.y / 2, xpos, size.y);*/
int x = left_nozzle_pos.x;
int len = m_right_road_length;
if (m_nozzle_num == 2) {
if (m_nozzle_num == 1) {
// Keep the single-nozzle path anchored at the left nozzle.
}
else if (m_nozzle_num == 2) {
x = right_nozzle_pos.x;
len = len - 14;
}
else {
// Generic N Nozzles
}
dc.DrawLine(((x)), (size.y / 2), x + FromDIP(len), (size.y / 2));
dc.DrawLine(x + FromDIP(len), (0), x + FromDIP(len), (size.y / 2));
dc.DrawLine((x), (size.y / 2), (x), (size.y));
@@ -2544,10 +2700,16 @@ void AMSRoadDownPart::doRender(wxDC& dc)
else{
int x = left_nozzle_pos.x;
int len = m_right_road_length;
if (m_nozzle_num == 2) {
if (m_nozzle_num == 1) {
// Keep the single-nozzle path anchored at the left nozzle.
}
else if (m_nozzle_num == 2) {
x = right_nozzle_pos.x;
len = len - 14;
}
else {
// Generic N Nozzles
}
dc.DrawLine(((x)), (size.y / 2), x + FromDIP(len), (size.y / 2));
dc.DrawLine(x + FromDIP(len), (0), x + FromDIP(len), (size.y / 2));
dc.DrawLine((x), (size.y / 2), (x), (size.y));
@@ -2571,7 +2733,7 @@ void AMSRoadDownPart::doRender(wxDC& dc)
}
void AMSRoadDownPart::UpdatePassRoad(AMSPanelPos pos, int len, AMSPassRoadSTEP step) {
if (m_nozzle_num >= 2){
if (m_nozzle_num == 2){
if (pos == AMSPanelPos::LEFT_PANEL){
if (m_left_road_length == len && m_pass_road_left_step == step){ return; }
m_left_road_length = len;;
@@ -2583,7 +2745,7 @@ void AMSRoadDownPart::UpdatePassRoad(AMSPanelPos pos, int len, AMSPassRoadSTEP s
m_pass_road_right_step = step;
}
}
else{
else if (m_nozzle_num == 1){
if (pos == AMSPanelPos::LEFT_PANEL) {
if (m_left_road_length == len && m_pass_road_left_step == step && m_right_road_length == -1) { return; }
m_left_road_length = len;
@@ -2597,6 +2759,9 @@ void AMSRoadDownPart::UpdatePassRoad(AMSPanelPos pos, int len, AMSPassRoadSTEP s
m_pass_road_right_step = step;
}
}
else {
// Generic N Nozzles
}
Refresh();
}
@@ -2615,7 +2780,10 @@ AMSPreview::AMSPreview(wxWindow* parent, wxWindowID id, AMSinfo amsinfo, AMSMode
{
wxWindow::Create(parent, id, pos);
if (itemType == AMSModel::GENERIC_AMS || itemType == AMSModel::AMS_LITE || itemType == AMSModel::N3F_AMS) {
create(parent, id, pos, AMS_PREV_FOUR_SIZE);
if (itemType == AMSModel::GENERIC_AMS && use_generic_ams_layout(itemType))
create(parent, id, pos, wxSize(generic_ams_preview_width(this, static_cast<int>(amsinfo.cans.size())), AMS_PREV_FOUR_SIZE.y));
else
create(parent, id, pos, AMS_PREV_FOUR_SIZE);
}
else {
create(parent, id, pos, AMS_PREV_SINGLE_SIZE);
@@ -2650,7 +2818,13 @@ void AMSPreview::UpdateInfo(AMSinfo amsinfo)
m_amsinfo = amsinfo;
m_ams_item_type = amsinfo.ams_type;
if (m_ams_item_type == AMSModel::GENERIC_AMS || m_ams_item_type == AMSModel::AMS_LITE || m_ams_item_type == AMSModel::N3F_AMS) {
if (m_ams_item_type == AMSModel::GENERIC_AMS && use_generic_ams_layout(m_ams_item_type)) {
const wxSize generic_size(generic_ams_preview_width(this, static_cast<int>(m_amsinfo.cans.size())), AMS_PREV_FOUR_SIZE.y);
SetSize(generic_size);
SetMinSize(generic_size);
SetMaxSize(generic_size);
}
else if (m_ams_item_type == AMSModel::GENERIC_AMS || m_ams_item_type == AMSModel::AMS_LITE || m_ams_item_type == AMSModel::N3F_AMS) {
SetMinSize(AMS_PREV_FOUR_SIZE);
SetMaxSize(AMS_PREV_FOUR_SIZE);
} else {
@@ -2758,8 +2932,41 @@ void AMSPreview::doRender(wxDC &dc)
dc.DrawRoundedRectangle(0, 0, size.x, size.y, FromDIP(3));
auto left = 0;
//four slot
if (m_ams_item_type != AMSModel::EXT_AMS && m_ams_item_type != AMSModel::N3S_AMS){
// Generic AMS previews use the same normalized lane centers as the card row and connector.
if (m_ams_item_type == AMSModel::GENERIC_AMS && use_generic_ams_layout(m_ams_item_type)) {
const int lane_count = static_cast<int>(m_amsinfo.cans.size());
const auto lane_centers = generic_ams_lane_centers(lane_count, size.x);
const int cube_width = FromDIP(9);
const int cube_height = FromDIP(14);
for (int lane = 0; lane < lane_count; ++lane) {
const Caninfo &can = m_amsinfo.cans[lane];
const int cube_left = lane_centers[lane] - cube_width / 2;
const int cube_top = (size.y - cube_height) / 2;
wxRect rect(cube_left, cube_top, cube_width, cube_height);
if (can.material_colour.Alpha() == 0) {
if (wxGetApp().dark_mode())
dc.DrawBitmap(m_ts_bitmap_cube_dark.bmp(), cube_left - FromDIP(1), (size.y - m_ts_bitmap_cube_dark.GetBmpHeight()) / 2);
else
dc.DrawBitmap(m_ts_bitmap_cube.bmp(), cube_left - FromDIP(1), (size.y - m_ts_bitmap_cube.GetBmpHeight()) / 2);
}
else if (can.material_state == AMSCanType::AMS_CAN_TYPE_EMPTY) {
dc.SetPen(wxPen(wxColor(0, 0, 0)));
dc.DrawLine(rect.GetRight() - FromDIP(1), rect.GetTop() + FromDIP(1),
rect.GetLeft() + FromDIP(1), rect.GetBottom() - FromDIP(1));
}
else {
wxColour color = can.material_colour;
change_the_opacity(color);
dc.SetPen(wxPen(*wxTRANSPARENT_PEN));
dc.SetBrush(wxBrush(color));
dc.DrawRoundedRectangle(rect, 2);
}
}
}
// four slot
else if (m_ams_item_type != AMSModel::EXT_AMS && m_ams_item_type != AMSModel::N3S_AMS){
left = FromDIP(8);
for (std::vector<Caninfo>::iterator iter = m_amsinfo.cans.begin(); iter != m_amsinfo.cans.end(); iter++) {
@@ -3158,10 +3365,19 @@ AmsItem::AmsItem(wxWindow *parent,AMSinfo info, AMSModel model, AMSPanelPos pos
m_panel_pos = pos;
if (m_ams_model == AMSModel::GENERIC_AMS){
wxWindow::Create(parent, wxID_ANY, wxDefaultPosition, AMS_CANS_WINDOW_SIZE);
SetSize(AMS_CANS_WINDOW_SIZE);
SetMinSize(AMS_CANS_WINDOW_SIZE);
SetMaxSize(AMS_CANS_WINDOW_SIZE);
const int lane_count = static_cast<int>(m_info.cans.size());
const int width = use_generic_ams_layout(m_ams_model) && lane_count <= 8
? generic_ams_layout_width(this, lane_count)
: FromDIP(264);
const wxSize generic_size(width, FromDIP(174));
wxWindow::Create(parent, wxID_ANY, wxDefaultPosition, generic_size);
SetSize(generic_size);
SetMinSize(generic_size);
// For large lane counts the card is a viewport, not the lane strip. Let
// its parent allocate the available width; the scrolled child retains
// the wider virtual content size.
if (!(use_generic_ams_layout(m_ams_model) && lane_count > 8))
SetMaxSize(generic_size);
}
else{
wxWindow::Create(parent, wxID_ANY, wxDefaultPosition, AMS_SINGLE_CAN_SIZE);
@@ -3187,12 +3403,43 @@ void AmsItem::create(wxWindow *parent)
if (m_ams_model != AMSModel::AMS_LITE) {
sizer_can = new wxBoxSizer(wxHORIZONTAL);
sizer_item = new wxBoxSizer(wxVERTICAL);
auto it = m_info.cans.begin();
for (; it != m_info.cans.end(); it++) {
AddCan(*it, m_can_count, m_info.cans.size(), sizer_can);
const bool generic_layout = use_generic_ams_layout(m_ams_model);
const int lane_count = static_cast<int>(m_info.cans.size());
const int content_width = generic_layout ? generic_ams_layout_width(this, lane_count) : 0;
const bool scroll_lanes = generic_layout && lane_count > 8;
const auto lane_centers = generic_layout
? generic_ams_lane_centers(lane_count, content_width)
: std::vector<int>();
m_can_parent = this;
if (scroll_lanes) {
m_can_scroll = new wxScrolledWindow(this, wxID_ANY);
m_can_scroll->SetScrollRate(FromDIP(10), 0);
m_can_scroll->SetMinSize(wxSize(FromDIP(264), FromDIP(143)));
m_can_scroll->SetBackgroundColour(StateColor::darkModeColorFor(AMS_CONTROL_DEF_LIB_BK_COLOUR));
m_can_scroll_sizer = new wxBoxSizer(wxVERTICAL);
m_can_scroll->SetSizer(m_can_scroll_sizer);
m_can_parent = m_can_scroll;
}
int previous_right = 0;
for (int can_index = 0; can_index < lane_count; ++can_index) {
if (generic_layout) {
const int left = lane_centers[can_index] - FromDIP(26);
sizer_can->AddSpacer(std::max(0, left - previous_right));
AddCan(m_info.cans[can_index], m_can_count, m_info.cans.size(), sizer_can);
previous_right = left + FromDIP(52);
}
else {
AddCan(m_info.cans[can_index], m_can_count, m_info.cans.size(), sizer_can);
}
m_can_count++;
}
it = m_info.cans.begin();
if (generic_layout)
sizer_can->AddSpacer(std::max(0, content_width - previous_right));
if (generic_layout)
sizer_can->SetMinSize(wxSize(content_width, -1));
//auto road_panel = new wxWindow(this, wxID_ANY);
//auto road_panel = new wxPanel(this, wxID_ANY);
//road_panel->SetSize(AMS_CAN_ROAD_SIZE);
@@ -3207,11 +3454,18 @@ void AmsItem::create(wxWindow *parent)
sizer_item->Add(m_ext_image, 0, wxALIGN_CENTER, 0);
}
}
m_panel_road = new AMSRoadUpPart(this, wxID_ANY, m_info, m_ams_model);
m_panel_road = new AMSRoadUpPart(m_can_parent, wxID_ANY, m_info, m_ams_model);
sizer_item->Add(sizer_can, 0, wxALIGN_CENTER_HORIZONTAL, 0);
//sizer_item->Add(m_panel_road, 0, wxALIGN_CENTER_HORIZONTAL, 0);
sizer_item->Add(m_panel_road, 1, wxEXPAND);
if (scroll_lanes) {
m_can_scroll_sizer->Add(sizer_can, 0, wxALIGN_LEFT, 0);
m_can_scroll_sizer->Add(m_panel_road, 0, wxALIGN_LEFT, 0);
sizer_item->Add(m_can_scroll, 1, wxEXPAND);
}
else {
sizer_item->Add(sizer_can, 0, wxALIGN_CENTER_HORIZONTAL, 0);
//sizer_item->Add(m_panel_road, 0, wxALIGN_CENTER_HORIZONTAL, 0);
sizer_item->Add(m_panel_road, 1, wxEXPAND);
}
SetSizer(sizer_item);
}
@@ -3235,13 +3489,17 @@ void AmsItem::create(wxWindow *parent)
Layout();
Fit();
if (m_can_scroll) {
m_can_scroll->Layout();
m_can_scroll->FitInside();
}
Thaw();
//Refresh();
}
void AmsItem::AddCan(Caninfo caninfo, int canindex, int maxcan, wxBoxSizer* sizer)
{
auto amscan = new wxWindow(this, wxID_ANY);
auto amscan = new wxWindow(m_can_parent ? m_can_parent : this, wxID_ANY);
amscan->SetSize(wxSize(FromDIP(52), FromDIP(109)));
amscan->SetMinSize(wxSize(FromDIP(52), FromDIP(109)));
@@ -3322,7 +3580,8 @@ void AmsItem::AddCan(Caninfo caninfo, int canindex, int maxcan, wxBoxSizer* size
amscan->Layout();
amscan->Fit();
sizer->Add(amscan, 0, wxUP | wxLEFT | wxRIGHT, FromDIP(5));
const int margin = use_generic_ams_layout(m_ams_model) ? 0 : FromDIP(5);
sizer->Add(amscan, 0, wxUP | wxLEFT | wxRIGHT, margin);
m_can_lib_list[caninfo.can_id] = m_panel_lib;
//m_can_road_list[caninfo.can_id] = m_panel_road;
@@ -3447,6 +3706,10 @@ void AmsItem::UpdateInfo(AMSinfo info)
}
Layout();
if (m_can_scroll) {
m_can_scroll->Layout();
m_can_scroll->FitInside();
}
}
void AmsItem::SetDefSelectCan()
+10
View File
@@ -30,6 +30,7 @@
#include <wx/string.h>
#include <wx/timer.h>
#include <wx/sizer.h>
#include <wx/scrolwin.h>
#include "slic3r/GUI/DeviceCore/DevFilaSwitch.h" // Orca: DevFilaSwitch::SwitchPos for inlet-aware AMS placement
@@ -96,6 +97,7 @@ enum class AMSRoadShowMode : int {
AMS_ROAD_MODE_SINGLE,
AMS_ROAD_MODE_SINGLE_N3S,
AMS_ROAD_MODE_AMS_LITE,
AMS_ROAD_MODE_GENERIC,
AMS_ROAD_MODE_NONE
};
@@ -480,6 +482,7 @@ public:
//AMSextruderImage *m_amsSextruder{nullptr};
AMSextruderImage* m_left_extruder = nullptr;
AMSextruderImage* m_right_extruder = nullptr;
std::vector<AMSextruderImage*> m_nozzle_extruders;
AMSextruder(wxWindow *parent, wxWindowID id, int nozzle_num, const wxPoint &pos = wxDefaultPosition, const wxSize &size = wxDefaultSize);
~AMSextruder();
@@ -681,6 +684,8 @@ public:
// void Update(AMSRoadDownPartMode nozzle, AMSRoadShowMode left_mode, AMSRoadShowMode right_mode, int left_len, int right_len);
void UpdateLeft(int nozzle_num, AMSRoadShowMode mode);
void UpdateRight(int nozzle_num, AMSRoadShowMode mode);
void SetNozzleCount(int nozzle_num);
void SetSingleSideLayout(bool enabled, AMSPanelPos pos = AMSPanelPos::LEFT_PANEL);
void OnVamsLoading(bool load, wxColour col = AMS_CONTROL_GRAY500);
void SetPassRoadColour(bool left, wxColour col);
@@ -698,6 +703,8 @@ private:
AMSRoadShowMode m_left_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR};
AMSRoadShowMode m_right_rode_mode = {AMSRoadShowMode::AMS_ROAD_MODE_FOUR};
bool m_selected = {false};
bool m_single_side_layout = {false};
AMSPanelPos m_single_side_pos = {AMSPanelPos::LEFT_PANEL};
int m_left_road_length = {-1};
int m_right_road_length = {-1};
@@ -864,6 +871,9 @@ private:
std::map<std::string, AMSLib*> m_can_lib_list;
//std::map<std::string, AMSRoad*> m_can_road_list;
wxScrolledWindow* m_can_scroll = { nullptr };
wxBoxSizer* m_can_scroll_sizer = { nullptr };
wxWindow* m_can_parent = { nullptr };
AMSRoadUpPart* m_panel_road = { nullptr };
std::map<std::string, AMSrefresh*> m_can_refresh_list;
AMSHumidity* m_humidity = { nullptr };
+18 -3
View File
@@ -420,6 +420,14 @@ ExtruderBadge::ExtruderBadge(wxWindow* parent) : wxPanel(parent)
void ExtruderBadge::SetExtruderInfo(int extruder_id, const std::string& diameter, const NozzleVolumeType& volume_type)
{
// The badge renders exactly two extruders (left/right). A generic non-BBL printer with
// N > 2 can report an out-of-range id here (see MultiNozzleStatusTable::UpdateRackInfo);
// reject it rather than writing past the two-element lists.
if (extruder_id < 0 || extruder_id >= static_cast<int>(m_diameter_list.size())) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": out-of-range extruder_id " << extruder_id;
return;
}
m_diameter_list[extruder_id] = diameter;
m_volume_type_list[extruder_id] = volume_type;
@@ -867,14 +875,21 @@ void MultiNozzleStatusTable::UpdateRackInfo(std::weak_ptr<DevNozzleRack> rack)
bool has_right = false;
for (auto& elem : nozzles_in_extruder) {
auto& nozzle = elem.second;
int extruder_id = nozzle.AtLeftExtruder() ? 0 : 1;
if (nozzle.AtRightExtruder())
has_right = true;
int extruder_id{};
if (wxGetApp().preset_bundle && wxGetApp().preset_bundle->is_bbl_vendor()) {
extruder_id = nozzle.AtLeftExtruder() ? 0 : 1;
if (nozzle.AtRightExtruder())
has_right = true;
}
else
extruder_id = nozzle.GetExtruderId();
NozzleVolumeType volume_type = DevNozzle::ToNozzleVolumeType(nozzle.m_nozzle_flow);
m_badge->SetExtruderInfo(extruder_id, format_diameter_to_str(nozzle.GetNozzleDiameter()), volume_type);
}
// TODO: Update for N extruders
m_badge->SetExtruderValid(has_right);
}
}
+3
View File
@@ -184,6 +184,9 @@ void TempInput::SetFinish()
wxCommandEvent event(wxCUSTOMEVT_SET_TEMP_FINISH);
event.SetInt(temp_type);
event.SetString(wxString::Format("%d", m_input_type));
// N-extruder temp controls all share TEMP_OF_NORMAL_TYPE, so the string payload above can't
// tell them apart; carry widget identity so the listener can find which one fired.
event.SetEventObject(this);
wxPostEvent(this->GetParent(), event);
}
+400 -89
View File
@@ -38,6 +38,7 @@
#include <boost/log/trivial.hpp>
#include <algorithm>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <cctype>
#include <ios>
@@ -61,6 +62,42 @@ namespace websocket = beast::websocket;
namespace net = boost::asio;
using tcp = net::ip::tcp;
// Decimal value of a string that is nothing but digits, or -1 when it is empty or not a number.
int decimal_number(const std::string& digits)
{
if (digits.empty() || !std::all_of(digits.begin(), digits.end(), [](unsigned char c) { return std::isdigit(c) != 0; }))
return -1;
try {
return std::stoi(digits);
} catch (...) {
return -1;
}
}
// Lane database keys address a lane either as "T<N>" (0-based tool index) or as "lane<N>"
// (1-based, the AFC/Happy Hare style). Returns -1 when the key carries no usable lane number.
int lane_index_from_key(const std::string& lane_key)
{
if (lane_key.size() > 1 && (lane_key[0] == 'T' || lane_key[0] == 't'))
return decimal_number(lane_key.substr(1));
if (lane_key.size() > 4 && lane_key.compare(0, 4, "lane") == 0) {
const int lane_number = decimal_number(lane_key.substr(4));
return lane_number > 0 ? lane_number - 1 : -1;
}
return -1;
}
// AFC names toolheads "extruder", "extruder1", ... and derives the toolhead index from the name
// itself (AFC_lane.lane_extruder_index): no suffix is 0, otherwise the suffix is the index.
int extruder_index_from_name(const std::string& toolhead)
{
if (toolhead == "extruder")
return 0;
if (toolhead.size() > 8 && toolhead.compare(0, 8, "extruder") == 0)
return decimal_number(toolhead.substr(8));
return -1;
}
struct WsEndpoint
{
std::string host;
@@ -443,19 +480,7 @@ int MoonrakerPrinterAgent::set_queue_on_main_fn(QueueOnMainFn fn)
void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index, const std::vector<AmsTrayData>& trays)
{
// Look up MachineObject via DeviceManager
auto* dev_manager = GUI::wxGetApp().getDeviceManager();
if (!dev_manager) {
return;
}
MachineObject* obj = dev_manager->get_my_machine(device_info.dev_id);
if (!obj) {
return;
}
// Build BBL-format JSON for DevFilaSystemParser::ParseV1_0
nlohmann::json ams_json = nlohmann::json::object();
nlohmann::json ams_array = nlohmann::json::array();
// Calculate ams_exist_bits and tray_exist_bits
@@ -463,11 +488,9 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index,
unsigned long tray_exist_bits = 0;
for (int ams_id = 0; ams_id < ams_count; ++ams_id) {
ams_exist_bits |= (1 << ams_id);
nlohmann::json ams_unit = nlohmann::json::object();
ams_unit["id"] = std::to_string(ams_id);
ams_unit["info"] = "0002"; // treat as AMS_LITE
if (ams_id < 32) {
ams_exist_bits |= (1UL << ams_id);
}
nlohmann::json tray_array = nlohmann::json::array();
int max_slot_in_this_ams = std::min(3, max_lane_index - ams_id * 4);
@@ -483,43 +506,135 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index,
}
}
nlohmann::json tray_json = nlohmann::json::object();
tray_json["id"] = std::to_string(slot_id);
tray_json["tag_uid"] = "0000000000000000";
if (tray && tray->has_filament) {
tray_exist_bits |= (1 << slot_index);
tray_json["tray_info_idx"] = tray->tray_info_idx;
tray_json["tray_type"] = tray->tray_type;
tray_json["tray_color"] = normalize_color_value(tray->tray_color);
// Add temperature data if provided
if (tray->bed_temp > 0) {
tray_json["bed_temp"] = std::to_string(tray->bed_temp);
}
if (tray->nozzle_temp > 0) {
tray_json["nozzle_temp_max"] = std::to_string(tray->nozzle_temp);
}
} else {
tray_json["tray_info_idx"] = "";
tray_json["tray_type"] = "";
tray_json["tray_color"] = "00000000";
tray_json["tray_slot_placeholder"] = "1";
if (tray && tray->has_filament && slot_index < 32) {
tray_exist_bits |= (1UL << slot_index);
}
tray_array.push_back(tray_json);
static const AmsTrayData empty_tray;
tray_array.push_back(make_ams_tray_json(std::to_string(slot_id), tray ? *tray : empty_tray));
}
nlohmann::json ams_unit = nlohmann::json::object();
ams_unit["id"] = std::to_string(ams_id);
// Generic unit: type 1 (AMS) maps to AMSModel::GENERIC_AMS, which drives the
// variable-lane rendering. This is not a real Bambu AMS-Lite.
ams_unit["info"] = "0001";
ams_unit["tray"] = tray_array;
ams_array.push_back(ams_unit);
}
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload: Parsed " << trays.size() << " trays";
submit_ams_payload(ams_array, ams_exist_bits, tray_exist_bits);
}
void MoonrakerPrinterAgent::build_ams_payload_grouped(const std::vector<AmsTrayData>& trays)
{
// One AMS box per extruder, best effort. Lane-data changers (AFC, recent Happy Hare) report an
// extruder per lane but no physical box layout, so a box here is a logical feed group, not a
// discovered unit. Lanes without an extruder fall back to the main extruder.
std::map<int, std::vector<const AmsTrayData*>> boxes;
for (const auto& tray : trays) {
boxes[tray.extruder_id].push_back(&tray);
}
nlohmann::json ams_array = nlohmann::json::array();
unsigned long ams_exist_bits = 0;
unsigned long tray_exist_bits = 0;
int ams_id = 0;
// Cumulative lane index across all boxes, in box then slot order. A lane maps to exactly one
// extruder, so this is the lane's identity; the parser derives the same index for generic units.
int lane_index = 0;
for (auto& [extruder_id, lanes] : boxes) {
if (ams_id < 32) {
ams_exist_bits |= (1UL << ams_id);
}
// Keep a deterministic slot order within the box.
std::stable_sort(lanes.begin(), lanes.end(),
[](const AmsTrayData* a, const AmsTrayData* b) { return a->slot_index < b->slot_index; });
nlohmann::json tray_array = nlohmann::json::array();
for (size_t slot = 0; slot < lanes.size(); ++slot) {
const AmsTrayData& tray = *lanes[slot];
// A logical feed group is not a four-slot Bambu AMS: the bit is the lane's cumulative
// index, not ams_id*4+slot. DevFilaSystemParser::ParseV1_0 reads the same index.
const int bit = lane_index++;
if (tray.has_filament && bit < 32) {
tray_exist_bits |= (1UL << bit);
}
tray_array.push_back(make_ams_tray_json(std::to_string(slot), tray));
}
nlohmann::json ams_unit = nlohmann::json::object();
ams_unit["id"] = std::to_string(ams_id);
// info: bits 0-3 = unit type (1 = AMS, i.e. AMSModel::GENERIC_AMS), bits 8-11 = bound
// extruder id. A logical feed group is a generic unit, not a Bambu AMS-Lite.
const int info_val = 1 | ((extruder_id & 0x0F) << 8);
const char* hex_digits = "0123456789ABCDEF";
std::string info = "0000";
info[3] = hex_digits[info_val & 0x0F];
info[2] = hex_digits[(info_val >> 4) & 0x0F];
info[1] = hex_digits[(info_val >> 8) & 0x0F];
info[0] = hex_digits[(info_val >> 12) & 0x0F];
ams_unit["info"] = info;
ams_unit["tray"] = tray_array;
ams_array.push_back(ams_unit);
++ams_id;
}
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload_grouped: Parsed " << trays.size()
<< " trays in " << boxes.size() << " extruder box(es)";
submit_ams_payload(ams_array, ams_exist_bits, tray_exist_bits);
}
nlohmann::json MoonrakerPrinterAgent::make_ams_tray_json(const std::string& tray_id, const AmsTrayData& tray)
{
nlohmann::json tray_json = nlohmann::json::object();
tray_json["id"] = tray_id;
tray_json["tag_uid"] = "0000000000000000";
if (tray.has_filament) {
tray_json["tray_info_idx"] = tray.tray_info_idx;
tray_json["tray_type"] = tray.tray_type;
tray_json["tray_color"] = normalize_color_value(tray.tray_color);
// Add temperature data if provided
if (tray.bed_temp > 0) {
tray_json["bed_temp"] = std::to_string(tray.bed_temp);
}
if (tray.nozzle_temp > 0) {
tray_json["nozzle_temp_max"] = std::to_string(tray.nozzle_temp);
}
} else {
tray_json["tray_info_idx"] = "";
tray_json["tray_type"] = "";
tray_json["tray_color"] = "00000000";
tray_json["tray_slot_placeholder"] = "1";
}
return tray_json;
}
void MoonrakerPrinterAgent::submit_ams_payload(const nlohmann::json& ams_array, unsigned long ams_exist_bits, unsigned long tray_exist_bits)
{
// Look up MachineObject via DeviceManager
auto* dev_manager = GUI::wxGetApp().getDeviceManager();
if (!dev_manager) {
return;
}
MachineObject* obj = dev_manager->get_my_machine(device_info.dev_id);
if (!obj) {
return;
}
// Format as hex strings (matching BBL protocol)
std::ostringstream ams_exist_ss;
ams_exist_ss << std::hex << std::uppercase << ams_exist_bits;
std::ostringstream tray_exist_ss;
tray_exist_ss << std::hex << std::uppercase << tray_exist_bits;
nlohmann::json ams_json = nlohmann::json::object();
ams_json["ams"] = ams_array;
ams_json["ams_exist_bits"] = ams_exist_ss.str();
ams_json["tray_exist_bits"] = tray_exist_ss.str();
@@ -530,7 +645,6 @@ void MoonrakerPrinterAgent::build_ams_payload(int ams_count, int max_lane_index,
// Call the parser to populate DevFilaSystem
DevFilaSystemParser::ParseV1_0(print_json, obj, obj->GetFilaSystem().get(), false);
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::build_ams_payload: Parsed " << trays.size() << " trays";
// Set printer_type so update_sync_status() can match it against the preset's printer type.
// Without this, the comparison fails and all sync badges are cleared.
@@ -570,17 +684,15 @@ bool MoonrakerPrinterAgent::fetch_filament_info(std::string dev_id, FilamentSync
if (fetch_moonraker_filament_data(trays, max_lane_index)) {
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected Moonraker filament system with "
<< (max_lane_index + 1) << " lanes";
int ams_count = (max_lane_index + 4) / 4;
build_ams_payload(ams_count, max_lane_index, trays);
build_ams_payload_grouped(trays);
return true;
}
// Attempt Happy Hare first (more widely adopted, supports more filament changers)
// Fall back to the Happy Hare `mmu` object for HH builds that do not publish lane_data
if (fetch_hh_filament_info(trays, max_lane_index)) {
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_filament_info: Detected Happy Hare MMU with "
<< (max_lane_index + 1) << " gates";
int ams_count = (max_lane_index + 4) / 4;
build_ams_payload(ams_count, max_lane_index, trays);
build_ams_payload_grouped(trays);
return true;
}
@@ -686,6 +798,30 @@ int MoonrakerPrinterAgent::safe_json_int(const nlohmann::json& obj, const char*
return 0;
}
int MoonrakerPrinterAgent::safe_json_int_flexible(const nlohmann::json& obj, const char* key, int default_value)
{
auto it = obj.find(key);
if (it == obj.end())
return default_value;
if (it->is_number_integer())
return it->get<int>();
if (it->is_string()) {
const std::string s = it->get<std::string>();
if (s.empty())
return default_value;
try {
std::size_t consumed = 0;
const int value = std::stoi(s, &consumed);
if (consumed != s.size()) // reject partial parses such as "2oops"
return default_value;
return value;
} catch (...) {
return default_value;
}
}
return default_value;
}
std::string MoonrakerPrinterAgent::safe_array_string(const nlohmann::json& arr, int idx)
{
if (arr.is_array() && idx >= 0 && idx < static_cast<int>(arr.size()) && arr[idx].is_string())
@@ -789,28 +925,39 @@ bool MoonrakerPrinterAgent::fetch_moonraker_filament_data(std::vector<AmsTrayDat
trays.clear();
max_lane_index = 0;
// The lane_data namespace carries no extruder field; read the lane -> toolhead binding from the
// AFC printer objects so lanes on different toolheads end up in different AMS boxes.
std::map<std::string, int> lane_extruder;
fetch_afc_lane_extruders(lane_extruder);
for (const auto& [lane_key, lane_obj] : value.items()) {
if (!lane_obj.is_object()) {
continue;
}
// Extract lane index from the "lane" field (tool number, 0-based)
std::string lane_str = safe_json_string(lane_obj, "lane");
int lane_index = -1;
if (!lane_str.empty()) {
try {
lane_index = std::stoi(lane_str);
} catch (...) {
lane_index = -1;
}
}
// Lane tool number (0-based). AFC reports it as a string in the documented sample but it
// can also be a number; accept either and skip lanes without a usable index. Databases
// written without a "lane" field fall back to the key, which names the lane either as
// "T<N>" (0-based) or as "lane<N>" (1-based).
int lane_index = safe_json_int_flexible(lane_obj, "lane", -1);
if (lane_index < 0) {
continue;
lane_index = lane_index_from_key(lane_key);
if (lane_index < 0) {
continue;
}
}
AmsTrayData tray;
tray.slot_index = lane_index;
// AFC exposes the lane's extruder/toolhead as "extruder_index"; tolerate "extruder_id" as
// an alias. lane_data has neither, so fall back to the AFC lane objects and only then to
// the main extruder (best effort).
tray.extruder_id = safe_json_int_flexible(lane_obj, "extruder_index",
safe_json_int_flexible(lane_obj, "extruder_id", -1));
if (tray.extruder_id < 0) {
const auto it = lane_extruder.find(lane_key);
tray.extruder_id = it != lane_extruder.end() ? it->second : 0;
}
tray.tray_color = safe_json_string(lane_obj, "color");
tray.tray_type = safe_json_string(lane_obj, "material");
tray.bed_temp = safe_json_int(lane_obj, "bed_temp");
@@ -915,50 +1062,119 @@ bool MoonrakerPrinterAgent::fetch_hh_filament_info(std::vector<AmsTrayData>& tra
return false;
}
// Parse gate data
// Parse gate data. Every gate becomes a lane so empty/unknown gates surface
// as empty AMS slots rather than vanishing. HH is single-extruder, so all
// lanes belong to feed group 0.
trays.clear();
max_lane_index = 0;
for (int gate_idx = 0; gate_idx < num_gates; ++gate_idx) {
// Check gate_status: -1 = unknown, 0 = empty, 1 or 2 = available
int status = safe_array_int(gate_status, gate_idx);
if (status <= 0) {
continue; // Skip unknown or empty gates
}
// Extract gate data
std::string material = safe_array_string(gate_material, gate_idx);
std::string color = safe_array_string(gate_color, gate_idx);
int nozzle_temp = safe_array_int(gate_temperature, gate_idx);
// Skip if no material type (empty gate)
if (material.empty()) {
continue;
}
AmsTrayData tray;
tray.slot_index = gate_idx;
tray.tray_type = material;
tray.tray_color = color;
tray.nozzle_temp = nozzle_temp;
tray.bed_temp = 0; // HH doesn't provide bed temp in gate arrays
tray.has_filament = true;
tray.extruder_id = 0;
auto* bundle = GUI::wxGetApp().preset_bundle;
tray.tray_info_idx = bundle
? bundle->filaments.filament_id_by_type(tray.tray_type)
: map_filament_type_to_generic_id(tray.tray_type);
if (status > 0 && !material.empty()) {
tray.tray_type = material;
tray.tray_color = color;
tray.nozzle_temp = nozzle_temp;
tray.bed_temp = 0; // HH doesn't provide bed temp in gate arrays
tray.has_filament = true;
auto* bundle = GUI::wxGetApp().preset_bundle;
tray.tray_info_idx = bundle
? bundle->filaments.filament_id_by_type(tray.tray_type)
: map_filament_type_to_generic_id(tray.tray_type);
} else {
tray.has_filament = false;
}
max_lane_index = std::max(max_lane_index, gate_idx);
trays.push_back(tray);
}
if (trays.empty()) {
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent::fetch_hh_filament_info: No valid HH gates found";
max_lane_index = num_gates - 1;
return true;
}
// AFC writes its filament data into the lane_data database namespace, and the records of the
// fixture/agent write path carry no extruder field, so the lane -> toolhead binding has to be read
// from the printer objects: each AFC_lane <name>.extruder names the toolhead its lane feeds, and
// the toolhead's name carries its own index. Best effort: on failure every lane keeps the fallback
// of the main extruder.
bool MoonrakerPrinterAgent::fetch_afc_lane_extruders(std::map<std::string, int>& lane_extruder)
{
auto query_objects = [this](const std::string& query_string, nlohmann::json& out) {
auto http = Http::get(join_url(device_info.base_url, "/printer/objects/query?" + query_string));
if (!device_info.api_key.empty()) {
http.header("X-Api-Key", device_info.api_key);
}
std::string body;
unsigned status_code = 0;
http.timeout_connect(5)
.timeout_max(10)
.on_complete([&](std::string response, unsigned status) {
body = std::move(response);
status_code = status;
})
.perform_sync();
if (status_code != 200) {
return false;
}
out = nlohmann::json::parse(body, nullptr, false, true);
return !out.is_discarded();
};
nlohmann::json afc_json;
if (!query_objects("AFC", afc_json) || !afc_json.contains("result") ||
!afc_json["result"].contains("status") || !afc_json["result"]["status"].contains("AFC")) {
return false;
}
return true;
const auto& afc = afc_json["result"]["status"]["AFC"];
const auto& lanes = afc.contains("lanes") ? afc["lanes"] : nlohmann::json::array();
if (!lanes.is_array() || lanes.empty()) {
return false;
}
// Query every lane object in one request; the object names are "AFC_lane <lane name>".
std::string query_string;
for (const auto& lane : lanes) {
if (!lane.is_string()) {
return false;
}
if (!query_string.empty()) {
query_string += "&";
}
query_string += "AFC_lane%20" + lane.get<std::string>();
}
nlohmann::json lanes_json;
if (!query_objects(query_string, lanes_json) || !lanes_json.contains("result") ||
!lanes_json["result"].contains("status")) {
return false;
}
const auto& status = lanes_json["result"]["status"];
for (const auto& lane : lanes) {
const std::string name = lane.get<std::string>();
const std::string key = "AFC_lane " + name;
if (!status.contains(key) || !status[key].is_object() || !status[key].contains("extruder") ||
!status[key]["extruder"].is_string()) {
continue;
}
const std::string toolhead = status[key]["extruder"].get<std::string>();
const int toolhead_index = extruder_index_from_name(toolhead);
if (toolhead_index >= 0) {
lane_extruder[name] = toolhead_index;
}
}
return !lane_extruder.empty();
}
int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::string& json_str)
@@ -1059,6 +1275,25 @@ int MoonrakerPrinterAgent::handle_request(const std::string& dev_id, const std::
}
}
// Tool selection is printer-specific G-code. Moonraker forwards the
// conventional T<index> command to Klipper, where the printer can
// implement logical selection or a physical toolhead attach macro.
if (cmd == "select_extruder") {
if (!json["print"].contains("extruder_index") || !json["print"]["extruder_index"].is_number_integer()) {
BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: select_extruder missing integer extruder_index, full json: " << json_str;
return BAMBU_NETWORK_ERR_INVALID_RESULT;
}
const int extruder_idx = json["print"]["extruder_index"].get<int>();
if (extruder_idx < 0) {
BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: select_extruder received negative extruder_index: " << extruder_idx;
return BAMBU_NETWORK_ERR_INVALID_RESULT;
}
send_gcode_async(dev_id, "T" + std::to_string(extruder_idx));
return BAMBU_NETWORK_SUCCESS;
}
if (cmd == "home") {
send_gcode_async(dev_id, "G28");
return BAMBU_NETWORK_SUCCESS;
@@ -1155,7 +1390,7 @@ bool MoonrakerPrinterAgent::query_printer_status(const std::string& base_url,
nlohmann::json& status,
std::string& error) const
{
std::string url = join_url(base_url, "/printer/objects/query?print_stats&virtual_sdcard&extruder&heater_bed&fan");
std::string url = join_url(base_url, "/printer/objects/query?print_stats&virtual_sdcard&toolhead&extruder&heater_bed&fan");
std::string response_body;
bool success = false;
@@ -1563,11 +1798,12 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba
}
for (const auto& name : this->available_objects) {
if (name == "extruder" || name.rfind("extruder", 0) == 0) {
const bool is_main_extruder = name == "extruder";
const bool is_additional_extruder =
name.rfind("extruder", 0) == 0 && name.size() > 8 &&
std::all_of(name.begin() + 8, name.end(), [](unsigned char c) { return std::isdigit(c) != 0; });
if (is_main_extruder || is_additional_extruder) {
subscribe_objects.insert(name);
if (name == "extruder") {
break;
}
}
}
} else {
@@ -1863,6 +2099,81 @@ nlohmann::json MoonrakerPrinterAgent::build_print_payload_locked() const
}
}
// MachineObject::parse_new_info() only parses device.extruder when the four
// legacy new-protocol fields are present. Moonraker has no equivalents, so
// empty values act as compatibility markers without enabling any features.
std::map<int, const nlohmann::json*> extruder_status;
for (const auto& item : status_cache.items()) {
int extruder_id = -1;
if (item.key() == "extruder") {
extruder_id = 0;
} else if (item.key().rfind("extruder", 0) == 0 && item.key().size() > 8 &&
std::all_of(item.key().begin() + 8, item.key().end(), [](unsigned char c) { return std::isdigit(c) != 0; })) {
try {
extruder_id = std::stoi(item.key().substr(8));
} catch (...) {
extruder_id = -1;
}
}
if (extruder_id >= 0 && item.value().is_object()) {
extruder_status.emplace(extruder_id, &item.value());
}
}
if (!extruder_status.empty()) {
auto& device_extruder = payload["print"]["device"]["extruder"];
int current_extruder_id = 0;
if (status_cache.contains("toolhead") && status_cache["toolhead"].is_object() &&
status_cache["toolhead"].contains("extruder") && status_cache["toolhead"]["extruder"].is_string()) {
const std::string active_extruder = status_cache["toolhead"]["extruder"].get<std::string>();
if (active_extruder != "extruder" && active_extruder.rfind("extruder", 0) == 0 && active_extruder.size() > 8 &&
std::all_of(active_extruder.begin() + 8, active_extruder.end(), [](unsigned char c) { return std::isdigit(c) != 0; })) {
try {
const int parsed_id = std::stoi(active_extruder.substr(8));
if (parsed_id >= 0 && parsed_id < 16 && extruder_status.count(parsed_id) != 0) {
current_extruder_id = parsed_id;
}
} catch (...) {
// Keep the main extruder as the fallback for malformed status data.
}
}
}
const int extruder_count = std::min<int>(static_cast<int>(extruder_status.size()), 0xF);
device_extruder["state"] = extruder_count | (current_extruder_id << 4);
device_extruder["info"] = nlohmann::json::array();
auto encoded_temperature = [](const nlohmann::json& status, const char* key) {
if (!status.contains(key) || !status[key].is_number()) {
return 0;
}
const double temperature = status[key].get<double>();
return std::clamp(static_cast<int>(temperature + 0.5), 0, 65535);
};
for (const auto& [extruder_id, status] : extruder_status) {
const int current_temperature = encoded_temperature(*status, "temperature");
const int target_temperature = encoded_temperature(*status, "target");
device_extruder["info"].push_back({
{"id", extruder_id},
{"filam_bak", nlohmann::json::array()},
{"info", 1 << 3}, // The configured Moonraker extruder has a toolhead.
{"temp", current_temperature | (target_temperature << 16)},
{"spre", 0},
{"snow", 0},
{"star", 0},
{"stat", 0},
{"hnow", 0},
});
}
payload["print"]["cfg"] = "";
payload["print"]["fun"] = "";
payload["print"]["aux"] = "";
payload["print"]["stat"] = "";
}
if (status_cache.contains("heater_bed") && status_cache["heater_bed"].is_object()) {
const auto& bed = status_cache["heater_bed"];
if (bed.contains("temperature") && bed["temperature"].is_number()) {
@@ -7,6 +7,7 @@
#include <functional>
#include <cstdint>
#include <atomic>
#include <map>
#include <memory>
#include <mutex>
#include <set>
@@ -88,6 +89,7 @@ protected:
// Tray data for AMS payload building
struct AmsTrayData {
int slot_index = 0; // 0-based slot index
int extruder_id = 0; // Extruder this lane feeds (AFC extruder_index); 0 when unknown
bool has_filament = false;
std::string tray_type; // Material type (e.g., "PLA", "ASA")
std::string tray_color; // Raw color (#RRGGBB, 0xRRGGBB, or RRGGBBAA)
@@ -99,6 +101,10 @@ protected:
// Build ams JSON and call parser
void build_ams_payload(int ams_count, int max_lane_index, const std::vector<AmsTrayData>& trays);
// Build ams JSON with one box per extruder (best effort, for lane_data-based filament
// changers such as AFC) and call the parser. Boxes are ordered by extruder id.
void build_ams_payload_grouped(const std::vector<AmsTrayData>& trays);
// Methods that derived classes may need to override or access
virtual bool init_device_info(const std::string& dev_id, const std::string& dev_ip, const std::string& username, const std::string& password, bool use_ssl, const std::string& port);
virtual bool fetch_device_info(const std::string& base_url, const std::string& api_key, MoonrakerDeviceInfo& info, std::string& error) const;
@@ -163,10 +169,22 @@ private:
// System-specific filament fetch methods
bool fetch_hh_filament_info(std::vector<AmsTrayData>& trays, int& max_lane_index);
bool fetch_moonraker_filament_data(std::vector<AmsTrayData>& trays, int& max_lane_index);
// Lane name -> toolhead index, read from the AFC printer objects (the lane_data database has no
// extruder field). Returns false when AFC is not present or the objects cannot be read.
bool fetch_afc_lane_extruders(std::map<std::string, int>& lane_extruder);
// Build one BBL tray JSON object (placeholder when the tray has no filament).
static nlohmann::json make_ams_tray_json(const std::string& tray_id, const AmsTrayData& tray);
// Wrap the AMS boxes into the payload ParseV1_0 understands and apply the pull-mode
// readiness state (printer_type, push counters, storage, module versions).
void submit_ams_payload(const nlohmann::json& ams_array, unsigned long ams_exist_bits, unsigned long tray_exist_bits);
// JSON helper methods
static std::string safe_json_string(const nlohmann::json& obj, const char* key);
static int safe_json_int(const nlohmann::json& obj, const char* key);
// Accepts either a JSON number or a numeric string (AFC reports some fields as strings).
static int safe_json_int_flexible(const nlohmann::json& obj, const char* key, int default_value);
static std::string safe_array_string(const nlohmann::json& arr, int idx);
static int safe_array_int(const nlohmann::json& arr, int idx);
static std::string normalize_color_value(const std::string& color);
-125
View File
@@ -30,7 +30,6 @@
#include "libslic3r/Line.hpp"
#include "libslic3r/ExtrusionEntity.hpp"
#include "libslic3r/BoundingBox.hpp"
#include <functional>
#include <map>
#include <set>
#include <string>
@@ -597,127 +596,3 @@ TEST_CASE("Body centering survives islands merging and splitting between layers"
}
}
}
TEST_CASE("Z contouring slices an object that has several instances", "[PrintObject]")
{
Print print;
Model model;
place_two_cube_instances_apart(10., { { "zaa_enabled", "1" } }, print, model);
REQUIRE(print.objects().size() == 1);
REQUIRE(print.objects().front()->instances().size() == 2);
CHECK_NOTHROW(print.process());
CHECK(print.objects().front()->is_step_done(posContouring));
}
TEST_CASE("Z contouring slices instances of one object that are turned differently", "[PrintObject]")
{
// An instance turned about Z is sliced as a PrintObject of its own, with the same ModelObject behind it.
Print print;
Model model;
place_two_cube_instances_apart(10., { { "zaa_enabled", "1" } }, print, model);
model.objects.front()->instances.back()->set_rotation(Z, PI / 4.);
print.apply(model, DynamicPrintConfig(print.full_print_config()));
REQUIRE(print.objects().size() == 2);
CHECK_NOTHROW(print.process());
for (const PrintObject *object : print.objects())
CHECK(object->is_step_done(posContouring));
}
// A box tilted about X, so that its top slopes along Y and Z contouring has a surface to follow, with
// one instance for each Z rotation given, laid out in a row.
static void slice_tilted_box_instances(const std::vector<double> &z_rotations, Print &print, Model &model)
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({ { "zaa_enabled", "1" } });
TriangleMesh box = make_cube(20, 20, 10);
box.rotate_x(float(PI / 12.));
ModelObject *object = model.add_object();
object->add_volume(std::move(box));
for (size_t idx = 0; idx < z_rotations.size(); ++idx) {
ModelInstance *instance = object->add_instance();
instance->set_offset(Vec3d(50. + 50. * double(idx), 100., 0.));
instance->set_rotation(Z, z_rotations[idx]);
}
object->ensure_on_bed();
print.auto_assign_extruders(object);
print.apply(model, config);
print.validate();
print.set_status_silent();
print.process();
}
// The Z that contouring gave each point of the object's extrusions, by layer and by XY in the
// PrintObject's own frame, which does not depend on where its instances stand on the bed.
static std::vector<std::map<std::pair<coord_t, coord_t>, coord_t>> contoured_heights(const PrintObject &object)
{
std::vector<std::map<std::pair<coord_t, coord_t>, coord_t>> heights(object.layer_count());
for (size_t layer_idx = 0; layer_idx < object.layer_count(); ++layer_idx) {
auto &layer_heights = heights[layer_idx];
std::function<void(const ExtrusionEntity &)> visit = [&](const ExtrusionEntity &entity) {
if (const auto *path = dynamic_cast<const ExtrusionPath *>(&entity)) {
if (path->z_contoured)
for (const Point3 &pt : path->polyline.points)
layer_heights[{ pt.x(), pt.y() }] = pt.z();
} else if (const auto *multipath = dynamic_cast<const ExtrusionMultiPath *>(&entity)) {
for (const ExtrusionPath &path : multipath->paths)
visit(path);
} else if (const auto *loop = dynamic_cast<const ExtrusionLoop *>(&entity)) {
for (const ExtrusionPath &path : loop->paths)
visit(path);
} else if (const auto *collection = dynamic_cast<const ExtrusionEntityCollection *>(&entity)) {
for (const ExtrusionEntity *child : collection->entities)
visit(*child);
}
};
for (const LayerRegion *region : object.get_layer(int(layer_idx))->regions()) {
visit(region->perimeters);
visit(region->fills);
}
}
return heights;
}
TEST_CASE("Z contouring follows the surface of each instance as it is turned", "[PrintObject]")
{
// Two instances of a box with a sloped top, the second turned a quarter turn about Z: its top slopes
// along X instead of Y. Each is contoured against the box turned as that instance is, so it has to
// match the same box placed alone with that turn.
Print print;
Model model;
slice_tilted_box_instances({ 0., PI / 2. }, print, model);
REQUIRE(print.objects().size() == 2);
for (const PrintObject *object : print.objects()) {
const double z_rotation = object->instances().front().model_instance->get_rotation().z();
Print reference;
Model reference_model;
slice_tilted_box_instances({ z_rotation }, reference, reference_model);
REQUIRE(reference.objects().size() == 1);
const auto heights = contoured_heights(*object);
const auto reference_heights = contoured_heights(*reference.objects().front());
REQUIRE(heights.size() == reference_heights.size());
size_t matched = 0, lifted = 0;
for (size_t layer_idx = 0; layer_idx < heights.size(); ++layer_idx) {
const coord_t print_z = scaled(object->get_layer(int(layer_idx))->print_z);
for (const auto &[xy, z] : heights[layer_idx]) {
const auto it = reference_heights[layer_idx].find(xy);
if (it == reference_heights[layer_idx].end())
continue;
INFO("layer " << layer_idx << ", rotation " << z_rotation);
CHECK(std::abs(z - it->second) <= scaled(0.001));
++matched;
if (std::abs(z - print_z) > scaled(0.01))
++lifted;
}
}
INFO("rotation " << z_rotation);
// Enough points in common to mean something, and contouring did move some of them.
CHECK(matched > 100);
CHECK(lifted > 0);
}
}
+1
View File
@@ -5,6 +5,7 @@ add_executable(${_TEST_NAME}_tests
test_config_value_formatter.cpp
test_creality_cfs_match.cpp
test_dev_mapping.cpp
test_dev_extder_system.cpp
test_device_access.cpp
test_moonraker_discovery.cpp
test_network_discovery.cpp
@@ -0,0 +1,161 @@
// Match the include environment that libslic3r_gui TUs get from pchheader.hpp: Windows.h with
// WIN32_LEAN_AND_MEAN/NOMINMAX must come first so rpcndr.h's `byte` is processed before <cstddef>
// makes std::byte a competing candidate (otherwise the Windows COM headers pulled in via
// DeviceManager.hpp error with an ambiguous `byte`). wx/timer.h must precede DeviceManager.hpp,
// which includes DeviceErrorDialog.hpp (uses wxTimerEvent) before its own wx/timer.h include.
#include "catch2/catch_test_macros.hpp"
#include "catch2/catch_message.hpp"
#include <slic3r/GUI/DeviceCore/DevDefs.h>
#include <string>
#include <utility>
#include <vector>
#ifdef WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#endif
#include <catch2/catch_all.hpp>
#include <wx/timer.h>
#include "slic3r/GUI/DeviceManager.hpp"
#include "slic3r/GUI/DeviceCore/DevExtruderSystem.h"
#include <nlohmann/json.hpp>
using json = nlohmann::json;
using namespace Slic3r;
TEST_CASE("Extruder state decodes a generic toolhead set", "[DevExtderSystem]")
{
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
// device.extruder.state packing: count 0..3, current 4..7, target 8..11,
// switch 12..14, loading 15..18, busy 19.
const int extruder_count = 4;
const int current_extruder = 2;
const int switch_state = ES_BUSY;
const int loading_extruder = 1;
const int state = extruder_count | (current_extruder << 4) | (3 << 8) |
(switch_state << 12) | (loading_extruder << 15) | (1 << 19);
json extruder = { {"state", state}, {"info", json::array()} };
for (int id = 0; id < extruder_count; ++id) {
// info: bit 1 has filament, bit 3 nozzle present.
// temp: current 0..15, target 16..31. spre/snow/star: slot 0..7, ams 8..15.
extruder["info"].push_back({
{"id", id},
{"filam_bak", json::array({id})},
{"info", (id % 2 == 0 ? 2 : 0) | 8},
{"temp", (200 + id) | ((250 + id) << 16)},
{"spre", 10 << 8},
{"snow", id | ((20 + id) << 8)},
{"star", 30 << 8},
{"stat", 0},
{"hnow", id},
});
}
ExtderSystemParser::ParseV2_0(extruder, obj.GetExtderSystem());
REQUIRE(obj.GetExtderSystem()->GetTotalExtderCount() == extruder_count);
CHECK(obj.GetExtderSystem()->GetCurrentExtderId() == current_extruder);
CHECK(static_cast<int>(obj.GetExtderSystem()->GetSwitchState()) == switch_state);
CHECK(obj.GetExtderSystem()->GetLoadingExtderId() == loading_extruder);
CHECK(obj.GetExtderSystem()->IsBusyLoading());
for (int id = 0; id < extruder_count; ++id) {
const auto ext = obj.GetExtderSystem()->GetExtderById(id);
REQUIRE(ext.has_value());
INFO("extruder " << id);
CHECK(ext->GetExtId() == id);
CHECK(ext->GetCurrentTemp() == 200 + id);
CHECK(ext->GetTargetTemp() == 250 + id);
CHECK(ext->HasFilamentInExt() == (id % 2 == 0));
CHECK(ext->GetFilamBackup() == std::vector<int>{id});
CHECK(ext->GetSlotPre().ams_id == "10");
CHECK(ext->GetSlotNow().ams_id == std::to_string(20 + id));
CHECK(ext->GetSlotNow().slot_id == std::to_string(id));
CHECK(ext->GetSlotTarget().ams_id == "30");
CHECK(ext->GetNozzleId() == id);
}
}
TEST_CASE("Legacy single-toolhead parser fills only the main extruder", "[DevExtderSystem]")
{
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
REQUIRE(obj.GetExtderSystem()->GetTotalExtderCount() == 1);
// ParseV1_0 is the legacy single/dual fallback. It returns unless the system reports exactly
// one toolhead, so it only ever writes MAIN_EXTRUDER_ID's fields.
const json legacy = {
{"nozzle_temper", 210},
{"nozzle_target_temper", 245},
{"ams", {{"tray_now", "2"}, {"tray_tar", "3"}}}
};
ExtderSystemParser::ParseV1_0(legacy, obj.GetExtderSystem());
const auto ext = obj.GetExtderSystem()->GetExtderById(MAIN_EXTRUDER_ID);
REQUIRE(ext.has_value());
CHECK(ext->GetCurrentTemp() == 210);
CHECK(ext->GetTargetTemp() == 245);
// Below 0x80 the tray pointer encodes ams_id = value / 4, slot_id = value % 4.
CHECK(ext->GetSlotNow().ams_id == "0");
CHECK(ext->GetSlotNow().slot_id == "2");
CHECK(ext->GetSlotTarget().ams_id == "0");
CHECK(ext->GetSlotTarget().slot_id == "3");
CHECK(obj.GetExtderSystem()->IsBusyLoading());
CHECK(obj.GetExtderSystem()->GetLoadingExtderId() == MAIN_EXTRUDER_ID);
}
// ParseV2_0 does not enforce the documented extruder dialect invariants: the count in `state` and
// the id ordering of `info[]` are trusted, not checked. This test pins the resulting behavior.
TEST_CASE("Malformed extruder payload is stored by info array position", "[DevExtderSystem]")
{
const auto extder_info = [](int id) {
// Every key ParseV2_0 reads must be present.
return json{ {"id", id}, {"filam_bak", json::array()}, {"info", 0}, {"temp", 0},
{"spre", 0}, {"snow", 0}, {"star", 0}, {"stat", 0}, {"hnow", 0} };
};
SECTION("state count disagrees with info length") {
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
// `state` reports 2 toolheads but info[] carries 3.
json info = json::array();
for (int id = 0; id < 3; ++id)
info.push_back(extder_info(id));
ExtderSystemParser::ParseV2_0(json{ {"state", 2}, {"info", info} }, obj.GetExtderSystem());
// The info array wins: three toolheads are stored. m_total_extder_count still holds 2, so
// GetTotalExtderCount() would trip its debug assert; only the stored vector is inspected.
CHECK(obj.GetExtderSystem()->GetTotalExtderSize() == 3);
for (int id = 0; id < 3; ++id) {
const auto ext = obj.GetExtderSystem()->GetExtderById(id);
REQUIRE(ext.has_value());
CHECK(ext->GetExtId() == id);
}
}
SECTION("info id differs from the array index") {
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
// ids 1 and 2 sit at array positions 0 and 1.
json info = json::array({extder_info(1), extder_info(2)});
ExtderSystemParser::ParseV2_0(json{ {"state", 2}, {"info", info} }, obj.GetExtderSystem());
// Toolheads are stored by array position; the payload id is kept but is not the lookup key.
const auto first = obj.GetExtderSystem()->GetExtderById(0);
REQUIRE(first.has_value());
CHECK(first->GetExtId() == 1);
const auto second = obj.GetExtderSystem()->GetExtderById(1);
REQUIRE(second.has_value());
CHECK(second->GetExtId() == 2);
}
}
+161 -1
View File
@@ -13,10 +13,14 @@
#include <Windows.h>
#endif
#include <set>
#include <string>
#include <utility>
#include <vector>
#include <catch2/catch_test_macros.hpp>
#include "slic3r/GUI/DeviceCore/DevDefs.h"
#include "libslic3r/ProjectTask.hpp"
#include <vector>
#include <catch2/catch_message.hpp>
#include <catch2/catch_all.hpp>
@@ -65,6 +69,162 @@ TEST_CASE("AMS tray placeholder state follows the latest status", "[DevFilaSyste
CHECK_FALSE(tray->is_slot_placeholder);
}
TEST_CASE("Agent filament slots preserve every extruder and AMS binding", "[DevFilaSystem]")
{
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
const json filament = json::parse(R"({
"units": [
{ "id": "0", "extruder": 0, "slots": [
{ "index": 0, "loaded": true, "material": "PLA", "color": "FF5733FF" },
{ "index": 1, "loaded": true, "material": "PLA", "color": "33C1FFFF" },
{ "index": 2, "loaded": true, "material": "PLA", "color": "8D33FFFF" },
{ "index": 3, "loaded": false, "material": "", "color": "00000000" }
] },
{ "id": "1", "extruder": 1, "slots": [
{ "index": 0, "loaded": true, "material": "ASA", "color": "33C1FFFF" },
{ "index": 1, "loaded": true, "material": "ASA", "color": "8D33FFFF" },
{ "index": 2, "loaded": true, "material": "ASA", "color": "FF5733FF" },
{ "index": 3, "loaded": false, "material": "", "color": "00000000" }
] },
{ "id": "2", "extruder": 2, "slots": [
{ "index": 0, "loaded": true, "material": "ABS", "color": "8D33FFFF" },
{ "index": 1, "loaded": true, "material": "ABS", "color": "FF5733FF" },
{ "index": 2, "loaded": true, "material": "ABS", "color": "33C1FFFF" },
{ "index": 3, "loaded": false, "material": "", "color": "00000000" }
] },
{ "id": "3", "extruder": 3, "slots": [
{ "index": 0, "loaded": true, "material": "PETG", "color": "FF5733FF" },
{ "index": 1, "loaded": true, "material": "PETG", "color": "33C1FFFF" },
{ "index": 2, "loaded": true, "material": "PETG", "color": "8D33FFFF" },
{ "index": 3, "loaded": false, "material": "", "color": "00000000" }
] }
],
"external": [
{ "extruder": 3, "loaded": true, "material": "PETG", "preset_id": "P3" },
{ "extruder": 0, "loaded": true, "material": "PLA", "preset_id": "P0" },
{ "extruder": 2, "loaded": false, "material": "ABS", "preset_id": "P2" },
{ "extruder": 1, "loaded": true, "material": "ASA", "preset_id": "P1" }
]
})");
DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get());
REQUIRE(obj.vt_slot.size() == 4);
CHECK(obj.vt_slot[0].id == "255");
CHECK(obj.vt_slot[1].id == "254");
CHECK(obj.vt_slot[2].id == "253");
CHECK(obj.vt_slot[3].id == "252");
CHECK(obj.vt_slot[0].setting_id == "P0");
CHECK(obj.vt_slot[1].setting_id == "P1");
CHECK(obj.vt_slot[2].setting_id == "P2");
CHECK(obj.vt_slot[3].setting_id == "P3");
REQUIRE(obj.GetFilaSystem()->GetAmsCount() == 4);
for (int extruder = 0; extruder < 4; ++extruder) {
const auto& ams = obj.GetFilaSystem()->GetAmsList().at(std::to_string(extruder));
CHECK(ams->GetExtruderId() == extruder);
CHECK(ams->GetBindedExtruderSet() == std::set<int>{extruder});
REQUIRE(ams->GetTrays().size() == 4);
CHECK(ams->GetTrays().at("0")->color != ams->GetTrays().at("1")->color);
CHECK(ams->GetTrays().at("1")->color != ams->GetTrays().at("2")->color);
CHECK_FALSE(ams->GetTrays().at("3")->is_exists);
}
CHECK(obj.contains_tray("253", "0"));
CHECK(obj.get_tray("253", "0").setting_id == "P2");
CHECK(obj.GetFilaSystem()->GetExtruderIdByAmsId("253") == 2);
const auto tray_index_map = obj.GetFilaSystem()->GetTrayIndexMap();
REQUIRE(tray_index_map.count(252) == 1);
CHECK(tray_index_map.at(252).first == 252);
CHECK(tray_index_map.at(252).second == 0);
}
TEST_CASE("Generic AMS tray index map uses cumulative lane counts", "[DevMapping]")
{
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
// A well-formed generic payload pairs "units" with "external". An empty external array resets
// vt_slot, clearing the default virtual tray (255) MachineObject's constructor seeds; otherwise
// that phantom external tray lands in GetTrayIndexMap and inflates the map size.
json filament = { {"units", json::array()}, {"external", json::array()} };
for (int ams_id = 0; ams_id < 2; ++ams_id) {
json unit = {
{"id", std::to_string(ams_id)},
{"extruder", ams_id},
{"slots", json::array()}
};
for (int slot_id = 0; slot_id < 5; ++slot_id)
unit["slots"].push_back({{"index", slot_id}, {"loaded", false}});
filament["units"].push_back(std::move(unit));
}
DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get());
const auto tray_index_map = obj.GetFilaSystem()->GetTrayIndexMap();
REQUIRE(tray_index_map.size() == 10);
CHECK(tray_index_map.at(0).first == 0);
CHECK(tray_index_map.at(0).second == 0);
CHECK(tray_index_map.at(4).first == 0);
CHECK(tray_index_map.at(4).second == 4);
CHECK(tray_index_map.at(5).first == 1);
CHECK(tray_index_map.at(5).second == 0);
CHECK(tray_index_map.at(9).first == 1);
CHECK(tray_index_map.at(9).second == 4);
}
TEST_CASE("Generic tray index map handles unequal per-unit slot counts", "[DevMapping]")
{
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
// An "external" array must accompany "units" so vt_slot is reset (see the cumulative test above).
json filament = { {"units", json::array()}, {"external", json::array()} };
const std::vector<int> slot_counts = {4, 12};
for (int ams_id = 0; ams_id < static_cast<int>(slot_counts.size()); ++ams_id) {
json unit = { {"id", std::to_string(ams_id)}, {"extruder", ams_id}, {"slots", json::array()} };
for (int slot_id = 0; slot_id < slot_counts[ams_id]; ++slot_id)
unit["slots"].push_back({{"index", slot_id}, {"loaded", false}});
filament["units"].push_back(std::move(unit));
}
DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get());
const auto tray_index_map = obj.GetFilaSystem()->GetTrayIndexMap();
REQUIRE(tray_index_map.size() == 16);
// Unit 0 occupies global lanes 0..3; unit 1 continues at 4..15 because the counter advances
// by the reported tray count, not by a fixed four-slot stride.
CHECK(tray_index_map.at(0).first == 0);
CHECK(tray_index_map.at(0).second == 0);
CHECK(tray_index_map.at(3).first == 0);
CHECK(tray_index_map.at(3).second == 3);
CHECK(tray_index_map.at(4).first == 1);
CHECK(tray_index_map.at(4).second == 0);
CHECK(tray_index_map.at(15).first == 1);
CHECK(tray_index_map.at(15).second == 11);
}
TEST_CASE("A generic AMS unit reports its reported tray count", "[DevFilaSystem]")
{
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");
json filament = { {"units", json::array()}, {"external", json::array()} };
const std::vector<int> slot_counts = {8, 1};
for (int ams_id = 0; ams_id < static_cast<int>(slot_counts.size()); ++ams_id) {
json unit = { {"id", std::to_string(ams_id)}, {"extruder", ams_id}, {"slots", json::array()} };
for (int slot_id = 0; slot_id < slot_counts[ams_id]; ++slot_id)
unit["slots"].push_back({{"index", slot_id}, {"loaded", false}});
filament["units"].push_back(std::move(unit));
}
DevFilaSystemParser::ParseAgentFilament(filament, &obj, obj.GetFilaSystem().get());
// Non-Bambu units are not capped at the four-slot Bambu contract; the reported tray count wins.
const auto& ams_list = obj.GetFilaSystem()->GetAmsList();
REQUIRE(ams_list.at("0")->GetSlotCount() == 8);
REQUIRE(ams_list.at("1")->GetSlotCount() == 1);
}
TEST_CASE("Switch-bound AMS trays map to the left extruder", "[DevMapping]")
{
MachineObject obj(nullptr, nullptr, "test", "test_dev", "127.0.0.1");