Compare commits

..
Author SHA1 Message Date
Hanif Koh d5165c3e75 Tell the Setup Wizard's Loading Page Only Once It Has Loaded
The wizard's loading page waits for HandleStudio to be called with
userguide_profile_load_finish and jumps to its target page on it; if the
call never arrives, a fallback timer in the page jumps after 180 s.

GuideFrame started the profile load from the first wxEVT_WEBVIEW_NAVIGATED
event and ran HandleStudio in the page as soon as the data was ready. That
event is the navigation's commit, before the page's scripts have run, and
with the wizard's derived cache the data is ready about 100 ms later.
Since the web view is created with an empty URL so the translated strings
can be injected before the first load, the first navigated event is the
commit of about:blank, so the data is ready before the real page has even
begun loading and the call reaches a page without HandleStudio. WebKit
drops the error and nothing retries, so on macOS the wizard often sits on
"Loading..." for three minutes before the printer list appears.

The loading page now reports in from its OnInit, the way the other wizard
pages request what they need, and the host sends the message once both
the data is ready and the page has reported; every instance of the page
reports again, so one that replaced an earlier one gets its own message.
2026-10-14 19:08:07 +08:00
HanifKoh 0a336941f4 Restore the Tagged User-Agent on Linux with WebKitGTK's Own Prefix (#16466)
Linux webviews lost the BBL-Slicer, dark|light and BBL-Language tags
with the stock WebKitGTK User-Agent, so the bundled pages stayed light
and dropped every message to the host. The tags come back on Linux, now
appended to the string WebKitGTK reports for itself instead of the Mac
one. Windows and macOS are untouched.
2026-10-14 19:03:53 +08:00
ea90f45127 Add Align/Distribute objects on the print plate (#13373)
* Add Align/Distribute objects on the print plate

Ports the Align/Distribute feature from BambuStudio 2.5 (AGPLv3,
same license as OrcaSlicer). Adds a new GLGizmoAlignment helper
that operates on the GLCanvas3D selection, twelve thin Plater
wrappers, and an Align/Distribute submenu attached to the same
five context menus BambuStudio attaches it to: object menu, text
part, BBL part, and the single/multi-volume context menus.

GLCanvas3D::do_move gains a defaulted force_volume_move parameter
so the helper can move multiple selected volumes within a single
object even when selection mode is Instance. GLGizmosManager gains
check_object_located_outside_plate, which switches the active plate
to the one containing the selection before align/distribute runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix build errors

---------
Co-authored-by: yw4z <ywsyildiz@gmail.com>
Co-authored-by: SoftFever <103989404+SoftFever@users.noreply.github.com>
2026-10-14 17:06:56 +08:00
HanifKoh c7b4e8ef84 build_linux.sh: Forward Arguments After -- to the CMake Configure Step (#16465)
Everything after `--` is appended, unchanged and last, to the cmake
configure line of the deps and OrcaSlicer builds, so a custom compiler,
compile flags or linker can be passed without adding an option to the
script for each one. The arguments are kept as an array, so a value with
spaces such as -DCMAKE_CXX_FLAGS="-O2 -march=native" survives, and they are
forwarded into the -g container. A stray argument without `--` aborts with
a message instead of reaching cmake as a path.

The existing DEPS_EXTRA_BUILD_ARGS and ORCA_EXTRA_BUILD_ARGS hooks are
unchanged and now mentioned in the usage text.
2026-10-14 15:59:41 +08:00
8f1103f064 Re-pull request regarding the printers of goofoo company (#16283)
* add new repositories and new push

* fix bug about setting_id

* fix bug and complete the test

* Fix Goofoo profiles so the bundle loads and passes profile checks

Repair the OrcaFilamentLibrary index entry that broke JSON parsing, regenerate the hand-written filament ids as Orca ids, and bump the library version. Point each printer's default filament and print profiles at presets that exist, normalize the two presets that needed it, rebuild both indexes, correct the ABS-GF/PA-GF/PA6-CF filament types, align the GP1/Velo+ preset names with the library's @ spacing, and fix the casing and resolution of the model cover images.

* Drop the copied Custom Klipper printer from the Goofoo bundle

Goofoo shipped Custom's MyKlipper machine model verbatim, so the Add Printer list offered "Generic Klipper Printer" twice and the printer type resolved its model_id by vendor order. The Goofoo machines now inherit the shared fdm_klipper_common directly, carrying over the per-variant layer heights the removed base supplied, matching how the other Klipper vendors wire theirs. Each machine's resolved settings are unchanged.

* Align the Goofoo preset names with the profile conventions

The machine models and variants already followed the <Model> / <Model> <nozzle> nozzle shape, but the processes put the model in the quality slot and used the bare vendor as the @ target (0.20mm GP1 @Goofoo). They are now named like every other vendor's: <lh>mm <Quality> @<Vendor> <Model> (0.20mm Standard @Goofoo GP1), with the machines' default_print_profile and the index updated to match. The four leaves also inherit the shared fdm_process_klipper_common directly instead of the copied Custom "0.20mm Standard @MyKlipper" base, which is removed; GP1 and TurboM gained the layer_height 0.2 that base supplied, so every preset's resolved settings are unchanged. Removed the orphaned Generic Klipper Printer cover and the unused Turbo-M texture, and renamed TurboM's bed texture to the -texture.svg form the other two models use.

* new Goofoo machine and clean profile

* Restore library filaments dropped by merges from main

* Give the Goofoo Velo+ Tool and 0.2 nozzle fitting processes

---------

Co-authored-by: SoftFever <103989404+SoftFever@users.noreply.github.com>
Co-authored-by: SoftFever <softfeverever@gmail.com>
2026-10-14 15:44:58 +08:00
Lizard 6e42a9b0ae Hot reload local preset bundles (#15424) 2026-10-14 14:18:56 +08:00
SoftFever e77ba5f672 Open .gcode files with their own settings, as .gcode.3mf files are (#16463) 2026-10-14 14:13:58 +08:00
97 changed files with 5297 additions and 114 deletions
+89
View File
@@ -0,0 +1,89 @@
{
"name": "Goofoo",
"version": "02.04.00.01",
"force_update": "0",
"description": "Goofoo configurations",
"machine_model_list": [
{
"name": "Goofoo GP1",
"sub_path": "machine/Goofoo GP1.json"
},
{
"name": "Goofoo TurboM",
"sub_path": "machine/Goofoo TurboM.json"
},
{
"name": "Goofoo Velo+",
"sub_path": "machine/Goofoo Velo+.json"
},
{
"name": "Goofoo Velo+ Tool",
"sub_path": "machine/Goofoo Velo+ Tool.json"
}
],
"process_list": [
{
"name": "fdm_process_common",
"sub_path": "process/fdm_process_common.json"
},
{
"name": "fdm_process_klipper_common",
"sub_path": "process/fdm_process_klipper_common.json"
},
{
"name": "0.20mm Standard @Goofoo GP1",
"sub_path": "process/0.20mm Standard @Goofoo GP1.json"
},
{
"name": "0.20mm Standard @Goofoo TurboM",
"sub_path": "process/0.20mm Standard @Goofoo TurboM.json"
},
{
"name": "0.20mm Standard @Goofoo Velo+",
"sub_path": "process/0.20mm Standard @Goofoo Velo+.json"
},
{
"name": "0.30mm Standard @Goofoo Velo+",
"sub_path": "process/0.30mm Standard @Goofoo Velo+.json"
},
{
"name": "0.10mm Standard @Goofoo Velo+",
"sub_path": "process/0.10mm Standard @Goofoo Velo+.json"
}
],
"filament_list": [],
"machine_list": [
{
"name": "fdm_machine_common",
"sub_path": "machine/fdm_machine_common.json"
},
{
"name": "fdm_klipper_common",
"sub_path": "machine/fdm_klipper_common.json"
},
{
"name": "Goofoo GP1 0.4 nozzle",
"sub_path": "machine/Goofoo GP1 0.4 nozzle.json"
},
{
"name": "Goofoo TurboM 0.4 nozzle",
"sub_path": "machine/Goofoo TurboM 0.4 nozzle.json"
},
{
"name": "Goofoo Velo+ 0.2 nozzle",
"sub_path": "machine/Goofoo Velo+ 0.2 nozzle.json"
},
{
"name": "Goofoo Velo+ 0.4 nozzle",
"sub_path": "machine/Goofoo Velo+ 0.4 nozzle.json"
},
{
"name": "Goofoo Velo+ 0.6 nozzle",
"sub_path": "machine/Goofoo Velo+ 0.6 nozzle.json"
},
{
"name": "Goofoo Velo+ Tool 0.4",
"sub_path": "machine/Goofoo Velo+ Tool 0.4.json"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256">
<g id="Layer_1">
<title>Layer 1</title>
<text fill="#0000ff" x="31.38885" y="400.22266" id="svg_1" font-size="24" font-family="Noto Sans JP" text-anchor="start" xml:space="preserve" stroke="#0000ff" transform="matrix(2.19884 0 0 1.66297 -63.896 -416.238)">GP1</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.
@@ -0,0 +1,7 @@
<svg width="600" height="480" xmlns="http://www.w3.org/2000/svg">
<g>
<title>Layer 1</title>
<text transform="matrix(2.3401 0 0 1.462 -92.4668 -168.496)" stroke="#000" stroke-width="3" xml:space="preserve" text-anchor="start" font-family="Noto Sans JP" font-size="24" id="svg_1" y="421.25588" x="52.33404" fill="#000000">TurboM</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="600" height="480" viewBox="0 0 600 480">
<g id="Layer_1">
<title>Layer 1</title>
<text transform="matrix(2.19884 0 0 1.66297 -203.896 -238.238)" stroke="#0000ff" xml:space="preserve" text-anchor="start" font-family="Noto Sans JP" font-size="24" id="svg_1" y="400.22266" x="120.07198" fill="#0000ff">VELO+</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.
@@ -0,0 +1,98 @@
{
"type": "machine",
"name": "Goofoo GP1 0.4 nozzle",
"inherits": "fdm_klipper_common",
"from": "system",
"setting_id": "x4GJ08PooNDQ8WJj",
"instantiation": "true",
"nozzle_diameter": [
"0.4"
],
"default_filament_profile": [
"Goofoo ABS @System"
],
"printer_model": "Goofoo GP1",
"default_print_profile": "0.20mm Standard @Goofoo GP1",
"printer_variant": "0.4",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\n;G92 E0",
"change_filament_gcode": "G5",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]\n;layer\n;{if layer_num == 2}\n;M106 P1 S255\n;{endif}\nSET_PRINT_STATS_INFO CURRENT_LAYER=[layer_num]\n",
"machine_end_gcode": "; End G-code\nEND_PRINT",
"machine_pause_gcode": "G5",
"machine_start_gcode": "M117 Printing...\nM107 ; Turn off the fan\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nM83 ; use relative distances for extrusion\nM104 S140\nM190 S[first_layer_bed_temperature] ; wait for bed temp\n;G32 ;Load bed mesh\nG28 ;home\nG29 bed leveling\nM104 S[first_layer_temperature] ; set extruder temp\nM140 S[first_layer_bed_temperature] ; set bed temp\nM109 S[first_layer_temperature] ; wait for extruder temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\nG0 Z2.0 F600;\nG0 X50 Y10 F8000;\nG92 E0.0 ; reset extruder distance position\nG0 Z0.4 F600;\nG1 X100.0 E10 F3000.0 ; intro line\nG92 E0.0 ; reset extruder distance position\nG1 X200.0 E15 F3000.0 ; intro line\nG92 E0.0 ; reset extruder distance position\nG0 Z0.8 F600;\nG1 X100.0 E15 F3000.0 ; intro line\nG1 Z0.4 F600 ;Wipe\nG0 Y12 F6000 ;Wipe\nG1 X100 F6000 ;Wipe\nG0 Y8 F12000 ;Wipe\nG1 X200 F6000 ;Wipe\nG1 X190 Y12 F6000 ;Wipe\nG1 X180 Y8 F6000 ;Wipe\nG1 X170 Y12 F6000 ;Wipe\nG1 X160 Y8 F6000 ;Wipe\nG1 X150 Y12 F6000 ;Wipe\n;G0 Z2.0 F600;\nG92 E0.0 ; reset extruder distance position\n;START_PRINT",
"purge_in_prime_tower": "0",
"time_lapse_gcode": ";TIMELAPSE_TAKE_FRAME",
"printable_height": "250",
"printable_area": [
"0x0",
"250x0",
"250x250",
"0x250"
],
"deretraction_speed": [
"0"
],
"extruder_clearance_height_to_lid": "140",
"extruder_clearance_height_to_rod": "36",
"extruder_clearance_radius": "65",
"machine_max_acceleration_e": [
"20000",
"5000"
],
"machine_max_acceleration_retracting": [
"15000",
"5000"
],
"machine_max_acceleration_z": [
"100",
"200"
],
"machine_max_jerk_e": [
"1",
"2.5"
],
"machine_max_jerk_x": [
"5",
"9"
],
"machine_max_jerk_y": [
"5",
"9"
],
"machine_max_speed_e": [
"25",
"25"
],
"machine_max_speed_x": [
"500",
"200"
],
"machine_max_speed_y": [
"500",
"200"
],
"machine_max_speed_z": [
"12",
"12"
],
"retract_before_wipe": [
"100%"
],
"retract_length_toolchange": [
"10"
],
"retraction_length": [
"0.5"
],
"retraction_minimum_travel": [
"2"
],
"retraction_speed": [
"50"
],
"thumbnails": "300x300/PNG",
"z_hop": [
"0"
],
"upward_compatible_machine": []
}
@@ -0,0 +1,11 @@
{
"type": "machine_model",
"name": "Goofoo GP1",
"nozzle_diameter": "0.4",
"bed_model": "Goofoo-GP1.stl",
"bed_texture": "Goofoo-GP1-texture.svg",
"family": "Goofoo",
"machine_tech": "FFF",
"model_id": "V003",
"default_materials": "Goofoo ABS @System"
}
@@ -0,0 +1,54 @@
{
"type": "machine",
"name": "Goofoo TurboM 0.4 nozzle",
"inherits": "fdm_klipper_common",
"from": "system",
"setting_id": "Wmdn5zTy1HH93Q5d",
"instantiation": "true",
"nozzle_diameter": [
"0.4"
],
"nozzle_type": "brass",
"printer_model": "Goofoo TurboM",
"printer_variant": "0.4",
"default_filament_profile": [
"Goofoo ABS @System"
],
"default_print_profile": "0.20mm Standard @Goofoo TurboM",
"printable_area": [
"0x0",
"230x0",
"230x200",
"0x200"
],
"extruder_clearance_height_to_lid": "350",
"extruder_clearance_height_to_rod": "60",
"extruder_clearance_radius": "50",
"machine_end_gcode": "M104 S0 ; turn off temperature\nM140 S0 ; turn off heatbed\nG92 E0.0 ; reset extruder distance position;\nG1 E-1 F2100 ; retract\nG28 X Y; home X Y axis\n;M84 ; disable motors\nM107 ; turn off fan\n;SET_VELOCITY_LIMIT ACCEL_TO_DECEL=4000",
"machine_max_jerk_z": [
"3",
"0.4"
],
"machine_start_gcode": "M107 ; Turn off the fan\nG21 ; set units to millimeters\nG90 ; use absolute coordinates\nM83 ; use relative distances for extrusion\n\n\nM140 S[first_layer_bed_temperature] ; set bed temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\n\nG28 ;home\n\nG1 X2 Y10 F3000;\n\nM104 S[first_layer_temperature] ; set extruder temp\nM140 S[first_layer_bed_temperature] ; set bed temp\nM109 S[first_layer_temperature] ; wait for extruder temp\nM190 S[first_layer_bed_temperature] ; wait for bed temp\n\nG1 Z0.28 F240\n\nG92 E0\n\nG1 Y140 E10 F1500 ; prime the nozzle\n\nG1 X2.3 F5000\nG92 E0\n\nG1 Y10 E10 F1200 ; prime the nozzle\n;G0 Z2.0 F600;\nG92 E0.0 ; reset extruder distance position;\nM82\n;SET_PRESSURE_ADVANCE ADVANCE=0.05\n;SET_VELOCITY_LIMIT ACCEL_TO_DECEL=10000\n",
"printable_height": "200",
"z_hop": [
"0"
],
"machine_max_speed_e": [
"25",
"25"
],
"machine_max_speed_x": [
"500",
"200"
],
"machine_max_speed_y": [
"500",
"200"
],
"machine_max_speed_z": [
"12",
"12"
],
"upward_compatible_machine": []
}
@@ -0,0 +1,11 @@
{
"type": "machine_model",
"name": "Goofoo TurboM",
"nozzle_diameter": "0.4",
"bed_model": "Goofoo-TurboM.stl",
"bed_texture": "Goofoo-TurboM-texture.svg",
"family": "Goofoo",
"machine_tech": "FFF",
"model_id": "V002",
"default_materials": "Goofoo ABS @System"
}
@@ -0,0 +1,106 @@
{
"type": "machine",
"name": "Goofoo Velo+ 0.2 nozzle",
"inherits": "fdm_klipper_common",
"from": "system",
"setting_id": "wSr6VNECZHekeNRi",
"instantiation": "true",
"nozzle_diameter": [
"0.2"
],
"nozzle_type": "hardened_steel",
"printer_model": "Goofoo Velo+",
"printer_variant": "0.2",
"default_filament_profile": [
"Goofoo ABS @System"
],
"default_print_profile": "0.10mm Standard @Goofoo Velo+",
"printable_area": [
"0x0",
"600x0",
"600x500",
"0x500"
],
"machine_end_gcode": "{if max_layer_z < printable_height}G1 Z{z_offset+min(max_layer_z+2, printable_height)} F600 ; Move print head up{endif}\nG1 X5 Y{print_bed_max[1]*0.8} F{travel_speed*60} ; present print\n{if max_layer_z < printable_height-10}G1 Z{z_offset+min(max_layer_z+70, printable_height-10)} F600 ; Move print head further up{endif}\n{if max_layer_z < max_print_height*0.6}G1 Z{printable_height*0.6} F600 ; Move print head further up{endif}\nM140 S0 ; turn off heatbed\nM104 S0 ; turn off temperature\nM107 ; turn off fan\nM84 X Y E ; disable motors",
"printable_height": "600",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0",
"change_filament_gcode": "PAUSE",
"deretraction_speed": [
"0"
],
"extruder_clearance_height_to_lid": "350",
"extruder_clearance_height_to_rod": "60",
"extruder_clearance_radius": "50",
"machine_max_acceleration_e": [
"20000",
"5000"
],
"machine_max_acceleration_retracting": [
"15000",
"5000"
],
"machine_max_acceleration_z": [
"100",
"200"
],
"machine_max_jerk_e": [
"1",
"2.5"
],
"machine_max_jerk_x": [
"5",
"9"
],
"machine_max_jerk_y": [
"5",
"9"
],
"machine_max_speed_e": [
"50",
"25"
],
"machine_max_speed_x": [
"730",
"200"
],
"machine_max_speed_y": [
"730",
"200"
],
"machine_max_speed_z": [
"15",
"12"
],
"machine_start_gcode": "G90 ; 绝对坐标模式\nM83 ; 挤出机相对模式\nM140 S[first_layer_bed_temperature] ; 设置热床温度\nM104 S140 ; 临时喷嘴温度=切片首层设定(防止归零渗料)\nG4 S10 ; 预热等待10秒\nG28 ; 归零所有轴\nG1 Z50 F240 ; Z轴抬升\nG1 X2 Y10 F3000 ; 移动至起始位置\nM104 S[nozzle_temperature_initial_layer] ; 正式设置喷嘴温度(与切片设置一致)\nM190 S[bed_temperature_initial_layer_single] ; 等待热床温度稳定\nM109 S[nozzle_temperature_initial_layer] ; 等待喷嘴温度稳定\nG1 Z0.28 F240 ; 喷嘴下降至打印高度\nG92 E0 ; 挤出机归零\nG1 Y140 E10 F1500 ; 画第一条 priming 线\nG1 X2.3 F5000 ; X轴微调\nG92 E0 ; 挤出机归零\nG1 Y10 E10 F1200 ; 画第二条 priming 线\nG92 E0 ; 最终挤出机归零",
"retract_before_wipe": [
"100%"
],
"retract_length_toolchange": [
"10"
],
"retraction_length": [
"0.5"
],
"retraction_minimum_travel": [
"2"
],
"retraction_speed": [
"50"
],
"thumbnails": "300x300/PNG",
"z_hop": [
"0"
],
"machine_max_acceleration_travel": [
"10000",
"10000"
],
"max_layer_height": [
"0.16"
],
"min_layer_height": [
"0.04"
],
"upward_compatible_machine": []
}
@@ -0,0 +1,100 @@
{
"type": "machine",
"name": "Goofoo Velo+ 0.4 nozzle",
"inherits": "fdm_klipper_common",
"from": "system",
"setting_id": "XzHz1vNOYg3Ofe8X",
"instantiation": "true",
"nozzle_diameter": [
"0.4"
],
"nozzle_type": "hardened_steel",
"printer_model": "Goofoo Velo+",
"printer_variant": "0.4",
"default_filament_profile": [
"Goofoo ABS @System"
],
"default_print_profile": "0.20mm Standard @Goofoo Velo+",
"printable_area": [
"0x0",
"600x0",
"600x500",
"0x500"
],
"machine_end_gcode": "{if max_layer_z < printable_height}G1 Z{z_offset+min(max_layer_z+2, printable_height)} F600 ; Move print head up{endif}\nG1 X5 Y{print_bed_max[1]*0.8} F{travel_speed*60} ; present print\n{if max_layer_z < printable_height-10}G1 Z{z_offset+min(max_layer_z+70, printable_height-10)} F600 ; Move print head further up{endif}\n{if max_layer_z < max_print_height*0.6}G1 Z{printable_height*0.6} F600 ; Move print head further up{endif}\nM140 S0 ; turn off heatbed\nM104 S0 ; turn off temperature\nM107 ; turn off fan\nM84 X Y E ; disable motors",
"printable_height": "600",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0",
"change_filament_gcode": "PAUSE",
"deretraction_speed": [
"0"
],
"extruder_clearance_height_to_lid": "350",
"extruder_clearance_height_to_rod": "60",
"extruder_clearance_radius": "50",
"machine_max_acceleration_e": [
"20000",
"5000"
],
"machine_max_acceleration_retracting": [
"15000",
"5000"
],
"machine_max_acceleration_z": [
"100",
"200"
],
"machine_max_jerk_e": [
"1",
"2.5"
],
"machine_max_jerk_x": [
"5",
"9"
],
"machine_max_jerk_y": [
"5",
"9"
],
"machine_max_speed_e": [
"50",
"25"
],
"machine_max_speed_x": [
"730",
"200"
],
"machine_max_speed_y": [
"730",
"200"
],
"machine_max_speed_z": [
"15",
"12"
],
"machine_start_gcode": "G90 ; 绝对坐标模式\nM83 ; 挤出机相对模式\nM140 S[first_layer_bed_temperature] ; 设置热床温度\nM104 S140 ; 临时喷嘴温度=切片首层设定(防止归零渗料)\nG4 S10 ; 预热等待10秒\nG28 ; 归零所有轴\nG1 Z50 F240 ; Z轴抬升\nG1 X2 Y10 F3000 ; 移动至起始位置\nM104 S[nozzle_temperature_initial_layer] ; 正式设置喷嘴温度(与切片设置一致)\nM190 S[bed_temperature_initial_layer_single] ; 等待热床温度稳定\nM109 S[nozzle_temperature_initial_layer] ; 等待喷嘴温度稳定\nG1 Z0.28 F240 ; 喷嘴下降至打印高度\nG92 E0 ; 挤出机归零\nG1 Y140 E10 F1500 ; 画第一条 priming 线\nG1 X2.3 F5000 ; X轴微调\nG92 E0 ; 挤出机归零\nG1 Y10 E10 F1200 ; 画第二条 priming 线\nG92 E0 ; 最终挤出机归零",
"retract_before_wipe": [
"100%"
],
"retract_length_toolchange": [
"10"
],
"retraction_length": [
"0.5"
],
"retraction_minimum_travel": [
"2"
],
"retraction_speed": [
"50"
],
"thumbnails": "300x300/PNG",
"z_hop": [
"0"
],
"machine_max_acceleration_travel": [
"10000",
"10000"
],
"upward_compatible_machine": []
}
@@ -0,0 +1,106 @@
{
"type": "machine",
"name": "Goofoo Velo+ 0.6 nozzle",
"inherits": "fdm_klipper_common",
"from": "system",
"setting_id": "Pauu7JRq7u2OD6kv",
"instantiation": "true",
"nozzle_diameter": [
"0.6"
],
"nozzle_type": "hardened_steel",
"printer_model": "Goofoo Velo+",
"printer_variant": "0.6",
"default_filament_profile": [
"Goofoo ABS @System"
],
"default_print_profile": "0.30mm Standard @Goofoo Velo+",
"printable_area": [
"0x0",
"600x0",
"600x500",
"0x500"
],
"machine_end_gcode": "{if max_layer_z < printable_height}G1 Z{z_offset+min(max_layer_z+2, printable_height)} F600 ; Move print head up{endif}\nG1 X5 Y{print_bed_max[1]*0.8} F{travel_speed*60} ; present print\n{if max_layer_z < printable_height-10}G1 Z{z_offset+min(max_layer_z+70, printable_height-10)} F600 ; Move print head further up{endif}\n{if max_layer_z < max_print_height*0.6}G1 Z{printable_height*0.6} F600 ; Move print head further up{endif}\nM140 S0 ; turn off heatbed\nM104 S0 ; turn off temperature\nM107 ; turn off fan\nM84 X Y E ; disable motors",
"printable_height": "600",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0",
"change_filament_gcode": "PAUSE",
"deretraction_speed": [
"0"
],
"extruder_clearance_height_to_lid": "350",
"extruder_clearance_height_to_rod": "60",
"extruder_clearance_radius": "50",
"machine_max_acceleration_e": [
"20000",
"5000"
],
"machine_max_acceleration_retracting": [
"15000",
"5000"
],
"machine_max_acceleration_z": [
"100",
"200"
],
"machine_max_jerk_e": [
"1",
"2.5"
],
"machine_max_jerk_x": [
"5",
"9"
],
"machine_max_jerk_y": [
"5",
"9"
],
"machine_max_speed_e": [
"50",
"25"
],
"machine_max_speed_x": [
"730",
"200"
],
"machine_max_speed_y": [
"730",
"200"
],
"machine_max_speed_z": [
"15",
"12"
],
"machine_start_gcode": "G90 ; 绝对坐标模式\nM83 ; 挤出机相对模式\nM140 S[first_layer_bed_temperature] ; 设置热床温度\nM104 S140 ; 临时喷嘴温度=切片首层设定(防止归零渗料)\nG4 S10 ; 预热等待10秒\nG28 ; 归零所有轴\nG1 Z50 F240 ; Z轴抬升\nG1 X2 Y10 F3000 ; 移动至起始位置\nM104 S[nozzle_temperature_initial_layer] ; 正式设置喷嘴温度(与切片设置一致)\nM190 S[bed_temperature_initial_layer_single] ; 等待热床温度稳定\nM109 S[nozzle_temperature_initial_layer] ; 等待喷嘴温度稳定\nG1 Z0.28 F240 ; 喷嘴下降至打印高度\nG92 E0 ; 挤出机归零\nG1 Y140 E10 F1500 ; 画第一条 priming 线\nG1 X2.3 F5000 ; X轴微调\nG92 E0 ; 挤出机归零\nG1 Y10 E10 F1200 ; 画第二条 priming 线\nG92 E0 ; 最终挤出机归零",
"retract_before_wipe": [
"100%"
],
"retract_length_toolchange": [
"10"
],
"retraction_length": [
"0.5"
],
"retraction_minimum_travel": [
"2"
],
"retraction_speed": [
"50"
],
"thumbnails": "300x300/PNG",
"z_hop": [
"0"
],
"machine_max_acceleration_travel": [
"10000",
"10000"
],
"max_layer_height": [
"0.4"
],
"min_layer_height": [
"0.12"
],
"upward_compatible_machine": []
}
@@ -0,0 +1,152 @@
{
"type": "machine",
"name": "Goofoo Velo+ Tool 0.4",
"inherits": "Goofoo Velo+ 0.4 nozzle",
"from": "system",
"setting_id": "ww39WeaF5nYks1em",
"instantiation": "true",
"nozzle_diameter": [
"0.4",
"0.4"
],
"printer_model": "Goofoo Velo+ Tool",
"change_filament_gcode": "T[next_extruder]\n;M104 S[nozzle_temperature_initial_layer] T[next_extruder]; 正式设置喷嘴温度(与切片设置一致)\n;M109 S[nozzle_temperature_initial_layer] T[next_extruder] ; 等待喷嘴温度稳定\n",
"default_nozzle_volume_type": [
"Standard",
"Standard"
],
"deretraction_speed": [
"0",
"0"
],
"extruder_colour": [
"#FCE94F",
"#FCE94F"
],
"extruder_offset": [
"0x0",
"0x0"
],
"extruder_printable_height": [
"0",
"0"
],
"extruder_type": [
"Direct Drive",
"Direct Drive"
],
"extruder_variant_list": [
"Direct Drive Standard",
"Direct Drive Standard"
],
"long_retractions_when_cut": [
"0",
"0"
],
"machine_end_gcode": "{if max_layer_z < printable_height}G1 Z{z_offset+min(max_layer_z+2, printable_height)} F600 ; Move print head up{endif}\nT0\nG1 X5 Y{print_bed_max[1]*0.8} F{travel_speed*60} ; present print\n{if max_layer_z < printable_height-10}G1 Z{z_offset+min(max_layer_z+70, printable_height-10)} F600 ; Move print head further up{endif}\n{if max_layer_z < max_print_height*0.6}G1 Z{printable_height*0.6} F600 ; Move print head further up{endif}\nM140 S0 ; turn off heatbed\nM104 S0 ; turn off temperature\nM107 ; turn off fan\nM84 X Y E ; disable motors",
"machine_start_gcode": "G90 ; 绝对坐标模式\nM83 ; 挤出机相对模式\nM140 S[first_layer_bed_temperature] ; 设置热床温度\nM104 S140 T0 ; 临时喷嘴温度=切片首层设定(防止归零渗料)\nM104 S140 T1\nG4 S10 ; 预热等待10秒\nG28 ; 归零所有轴\nG1 Z50 F240 ; Z轴抬升\nG1 X2 Y10 F3000 ; 移动至起始位置\nM104 S[nozzle_temperature_initial_layer] T0 ; 正式设置喷嘴温度(与切片设置一致)\nM104 S[nozzle_temperature_initial_layer] T1 ; 正式设置喷嘴温度(与切片设置一致)\nM190 S[bed_temperature_initial_layer_single] T0; 等待热床温度稳定\nM109 S[nozzle_temperature_initial_layer] T0 ; 等待喷嘴温度稳定\nG1 Z0.28 F240 ; 喷嘴下降至打印高度\nG92 E0 ; 挤出机归零\nG1 Y140 E10 F1500 ; 画第一条 priming 线\nG1 X2.3 F5000 ; X轴微调\nG92 E0 ; 挤出机归零\nG1 Y10 E10 F1200 ; 画第二条 priming 线\nG92 E0 ; 最终挤出机归零\nG1 Z20 F240 ; Z轴抬升\nG1 X280 Y10 F3000 ; 移动至起始位置\nT1\nM104 S[nozzle_temperature_initial_layer] T1 ; 正式设置喷嘴温度(与切片设置一致)\nM190 S[bed_temperature_initial_layer_single] T1; 等待热床温度稳定\nM109 S[nozzle_temperature_initial_layer] T1; 等待喷嘴温度稳定\nG4 S10 ; 预热等待10秒\nG1 Z0.28 F240 ; 喷嘴下降至打印高度\nG92 E0 ; 挤出机归零\nG1 Y140 E10 F1500 ; 画第一条 priming 线\nG1 X282.3 F5000 ; X轴微调\nG92 E0 ; 挤出机归零\nG1 Y10 E10 F1200 ; 画第二条 priming 线\nG92 E0 ; 最终挤出机归零",
"max_layer_height": [
"0.32",
"0.32"
],
"min_layer_height": [
"0.08",
"0.08"
],
"nozzle_flush_dataset": [
"nil",
"0"
],
"nozzle_type": [
"nil",
"hardened_steel"
],
"nozzle_volume": [
"nil",
"0"
],
"printable_area": [
"0x0",
"300x0",
"300x300",
"0x300"
],
"printer_extruder_id": [
"1",
"2"
],
"printer_extruder_variant": [
"Direct Drive Standard",
"Direct Drive Standard"
],
"purge_in_prime_tower": "0",
"retract_before_wipe": [
"100%",
"100%"
],
"retract_length_toolchange": [
"10",
"10"
],
"retract_lift_above": [
"0",
"0"
],
"retract_lift_below": [
"0",
"0"
],
"retract_lift_enforce": [
"All Surfaces",
"All Surfaces"
],
"retract_restart_extra": [
"0",
"0"
],
"retract_restart_extra_toolchange": [
"0",
"0"
],
"retract_when_changing_layer": [
"1",
"1"
],
"retraction_distances_when_cut": [
"18",
"18"
],
"retraction_length": [
"0.5",
"0.5"
],
"retraction_minimum_travel": [
"2",
"2"
],
"retraction_speed": [
"50",
"50"
],
"single_extruder_multi_material": "0",
"travel_slope": [
"3",
"3"
],
"wipe": [
"1",
"1"
],
"wipe_distance": [
"1",
"1"
],
"z_hop": [
"0",
"0"
],
"z_hop_types": [
"Slope Lift",
"Slope Lift"
]
}
@@ -0,0 +1,11 @@
{
"type": "machine_model",
"name": "Goofoo Velo+ Tool",
"nozzle_diameter": "0.4",
"bed_model": "Goofoo-Velo+.stl",
"bed_texture": "Goofoo-Velo+-texture.svg",
"family": "Goofoo",
"machine_tech": "FFF",
"model_id": "V006",
"default_materials": "Goofoo ABS @System"
}
@@ -0,0 +1,11 @@
{
"type": "machine_model",
"name": "Goofoo Velo+",
"nozzle_diameter": "0.2;0.4;0.6",
"bed_model": "Goofoo-Velo+.stl",
"bed_texture": "Goofoo-Velo+-texture.svg",
"family": "Goofoo",
"machine_tech": "FFF",
"model_id": "V001",
"default_materials": "Goofoo ABS @System"
}
@@ -0,0 +1,13 @@
{
"type": "machine",
"name": "fdm_klipper_common",
"inherits": "fdm_machine_common",
"from": "system",
"instantiation": "false",
"gcode_flavor": "klipper",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\nG92 E0.0\n;[layer_z]\n\n",
"machine_start_gcode": "G90 ; use absolute coordinates\nM83 ; use relative distances for extrusion\nM140 S{first_layer_bed_temperature[0]} ; set bed temp\nM104 S140 ; set temporary nozzle temp to prevent oozing\nG4 S10 ; wait 10s for preheating\nG28 ; home all axes\nG1 Z50 F240 ; raise Z axis\nG1 X2 Y10 F3000 ; move to start position\nM104 S{first_layer_temperature[0]} ; set final nozzle temp\nM190 S{first_layer_bed_temperature[0]} ; wait for bed temp\nM109 S{first_layer_temperature[0]} ; wait for nozzle temp\nG1 Z0.28 F240 ; lower nozzle to print height\nG92 E0 ; reset extrusion distance\nG1 Y140 E10 F1500 ; prime the nozzle\nG1 X2.3 F5000 ; slight X adjustment\nG92 E0 ; reset extrusion distance\nG1 Y10 E10 F1200 ; prime again\nG92 E0 ; final extrusion reset",
"machine_end_gcode": "{if max_layer_z < printable_height}G1 Z{z_offset+min(max_layer_z+2, printable_height)} F600 ; Move print head up{endif}\nG1 X5 Y{print_bed_max[1]*0.8} F{travel_speed*60} ; present print\n{if max_layer_z < printable_height-10}G1 Z{z_offset+min(max_layer_z+70, printable_height-10)} F600 ; Move print head further up{endif}\n{if max_layer_z < max_print_height*0.6}G1 Z{printable_height*0.6} F600 ; Move print head further up{endif}\nM140 S0 ; turn off heatbed\nM104 S0 ; turn off temperature\nM107 ; turn off fan\nM84 X Y E ; disable motors",
"change_filament_gcode": "PAUSE",
"machine_pause_gcode": "PAUSE"
}
@@ -0,0 +1,118 @@
{
"type": "machine",
"name": "fdm_machine_common",
"from": "system",
"instantiation": "false",
"printer_technology": "FFF",
"deretraction_speed": [
"40"
],
"extruder_colour": [
"#FCE94F"
],
"extruder_offset": [
"0x0"
],
"gcode_flavor": "marlin",
"machine_max_acceleration_e": [
"5000"
],
"machine_max_acceleration_extruding": [
"10000"
],
"machine_max_acceleration_retracting": [
"1000"
],
"machine_max_acceleration_x": [
"10000"
],
"machine_max_acceleration_y": [
"10000"
],
"machine_max_acceleration_z": [
"500"
],
"machine_max_speed_e": [
"60"
],
"machine_max_speed_x": [
"500"
],
"machine_max_speed_y": [
"500"
],
"machine_max_speed_z": [
"10"
],
"machine_max_jerk_e": [
"5"
],
"machine_max_jerk_x": [
"8"
],
"machine_max_jerk_y": [
"8"
],
"machine_max_jerk_z": [
"0.4"
],
"machine_min_extruding_rate": [
"0"
],
"machine_min_travel_rate": [
"0"
],
"max_layer_height": [
"0.32"
],
"min_layer_height": [
"0.08"
],
"printable_height": "250",
"extruder_clearance_radius": "65",
"extruder_clearance_height_to_rod": "36",
"extruder_clearance_height_to_lid": "140",
"nozzle_diameter": [
"0.4"
],
"printer_settings_id": "",
"printer_variant": "0.4",
"retraction_minimum_travel": [
"2"
],
"retract_before_wipe": [
"70%"
],
"retract_when_changing_layer": [
"1"
],
"retraction_length": [
"1"
],
"retract_length_toolchange": [
"1"
],
"z_hop": [
"0"
],
"retract_restart_extra": [
"0"
],
"retract_restart_extra_toolchange": [
"0"
],
"retraction_speed": [
"60"
],
"single_extruder_multi_material": "1",
"change_filament_gcode": "",
"wipe": [
"1"
],
"default_print_profile": "",
"machine_start_gcode": "G0 Z20 F9000\nG92 E0; G1 E-10 F1200\nG28\nM970 Q1 A10 B10 C130 K0\nM970 Q1 A10 B131 C250 K1\nM974 Q1 S1 P0\nM970 Q0 A10 B10 C130 H20 K0\nM970 Q0 A10 B131 C250 K1\nM974 Q0 S1 P0\nM220 S100 ;Reset Feedrate\nM221 S100 ;Reset Flowrate\nG29 ;Home\nG90;\nG92 E0 ;Reset Extruder \nG1 Z2.0 F3000 ;Move Z Axis up \nG1 X10.1 Y20 Z0.28 F5000.0 ;Move to start position\nM109 S205;\nG1 X10.1 Y200.0 Z0.28 F1500.0 E15 ;Draw the first line\nG1 X10.4 Y200.0 Z0.28 F5000.0 ;Move to side a little\nG1 X10.4 Y20 Z0.28 F1500.0 E30 ;Draw the second line\nG92 E0 ;Reset Extruder \nG1 X110 Y110 Z2.0 F3000 ;Move Z Axis up",
"machine_end_gcode": "M400 ; wait for buffer to clear\nG92 E0 ; zero the extruder\nG1 E-4.0 F3600; retract \nG91\nG1 Z3;\nM104 S0 ; turn off hotend\nM140 S0 ; turn off bed\nM106 S0 ; turn off fan\nG90 \nG0 X110 Y200 F3600 \nprint_end",
"layer_change_gcode": ";AFTER_LAYER_CHANGE\n;[layer_z]",
"before_layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nG92 E0\n",
"machine_pause_gcode": "M601"
}
@@ -0,0 +1,26 @@
{
"type": "process",
"name": "0.10mm Standard @Goofoo Velo+",
"inherits": "0.20mm Standard @Goofoo Velo+",
"from": "system",
"setting_id": "dJV4bTrLbOPzW2YB",
"instantiation": "true",
"compatible_printers": [
"Goofoo Velo+ 0.2 nozzle"
],
"layer_height": "0.1",
"initial_layer_print_height": "0.1",
"bottom_shell_layers": "6",
"top_shell_layers": "12",
"wall_loops": "4",
"line_width": "0.22",
"initial_layer_line_width": "0.25",
"inner_wall_line_width": "0.22",
"outer_wall_line_width": "0.22",
"sparse_infill_line_width": "0.22",
"internal_solid_infill_line_width": "0.22",
"top_surface_line_width": "0.22",
"support_line_width": "0.22",
"support_top_z_distance": "0.1",
"support_bottom_z_distance": "0.1"
}
@@ -0,0 +1,35 @@
{
"type": "process",
"name": "0.20mm Standard @Goofoo GP1",
"inherits": "fdm_process_klipper_common",
"from": "system",
"setting_id": "zrscQBUxp6L7BlQ9",
"instantiation": "true",
"compatible_printers": [
"Goofoo GP1 0.4 nozzle"
],
"layer_height": "0.2",
"accel_to_decel_enable": "0",
"bridge_acceleration": "100%",
"bridge_speed": "100",
"brim_type": "no_brim",
"gap_infill_speed": "250",
"initial_layer_acceleration": "1000",
"initial_layer_infill_speed": "100",
"initial_layer_speed": "70",
"inner_wall_speed": "250",
"internal_bridge_speed": "200%",
"internal_solid_infill_speed": "250",
"outer_wall_acceleration": "5000",
"outer_wall_speed": "100",
"overhang_4_4_speed": "0",
"reduce_crossing_wall": "1",
"skirt_distance": "6",
"skirt_height": "1",
"skirt_loops": "1",
"sparse_infill_pattern": "grid",
"sparse_infill_speed": "250",
"top_surface_acceleration": "5000",
"top_surface_speed": "200",
"travel_acceleration": "5000"
}
@@ -0,0 +1,67 @@
{
"type": "process",
"name": "0.20mm Standard @Goofoo TurboM",
"inherits": "fdm_process_klipper_common",
"from": "system",
"setting_id": "LVXxUtAcYGCY6irA",
"instantiation": "true",
"compatible_printers": [
"Goofoo TurboM 0.4 nozzle"
],
"layer_height": "0.2",
"bridge_acceleration": "2000",
"bridge_speed": "60",
"brim_type": "no_brim",
"default_acceleration": "10000",
"enable_support": "1",
"exclude_object": "0",
"gap_infill_speed": "200",
"infill_anchor": "600%",
"infill_combination": "1",
"infill_wall_overlap": "15%",
"initial_layer_acceleration": "1000",
"initial_layer_line_width": "0.45",
"initial_layer_speed": "60",
"inner_wall_acceleration": "3000",
"inner_wall_line_width": "0.42",
"inner_wall_speed": "150",
"internal_solid_infill_acceleration": "3000",
"internal_solid_infill_line_width": "0.42",
"internal_solid_infill_speed": "150",
"line_width": "0.42",
"min_skirt_length": "0",
"minimum_sparse_infill_area": "70",
"only_one_wall_top": "1",
"outer_wall_acceleration": "2500",
"outer_wall_line_width": "0.42",
"outer_wall_speed": "100",
"reduce_crossing_wall": "1",
"skirt_distance": "6",
"skirt_height": "1",
"skirt_loops": "4",
"small_perimeter_speed": "200",
"sparse_infill_acceleration": "3000",
"sparse_infill_line_width": "0.42",
"sparse_infill_pattern": "grid",
"sparse_infill_speed": "150",
"support_base_pattern": "rectilinear",
"support_interface_bottom_layers": "-1",
"support_interface_pattern": "rectilinear",
"support_interface_spacing": "0",
"support_interface_speed": "100",
"support_interface_top_layers": "3",
"support_line_width": "0.35",
"support_object_xy_distance": "0.21",
"support_speed": "100",
"thick_bridges": "1",
"top_shell_layers": "6",
"top_shell_thickness": "0",
"top_surface_acceleration": "2500",
"top_surface_line_width": "0.42",
"top_surface_speed": "150",
"travel_acceleration": "10000",
"travel_speed": "200",
"wall_generator": "classic",
"wall_loops": "2",
"wall_sequence": "outer wall/inner wall"
}
@@ -0,0 +1,70 @@
{
"type": "process",
"name": "0.20mm Standard @Goofoo Velo+",
"inherits": "fdm_process_klipper_common",
"from": "system",
"setting_id": "iWjynhCcvbF8MWev",
"instantiation": "true",
"compatible_printers": [
"Goofoo Velo+ 0.4 nozzle",
"Goofoo Velo+ Tool 0.4"
],
"layer_height": "0.2",
"initial_layer_print_height": "0.2",
"bottom_shell_layers": "3",
"top_shell_layers": "6",
"bridge_acceleration": "2000",
"bridge_speed": "60",
"brim_type": "no_brim",
"default_acceleration": "10000",
"enable_support": "1",
"exclude_object": "0",
"gap_infill_speed": "200",
"infill_anchor": "600%",
"infill_combination": "1",
"infill_wall_overlap": "15%",
"initial_layer_acceleration": "1000",
"initial_layer_line_width": "0.45",
"initial_layer_speed": "60",
"inner_wall_acceleration": "3000",
"inner_wall_line_width": "0.42",
"inner_wall_speed": "150",
"internal_solid_infill_acceleration": "3000",
"internal_solid_infill_line_width": "0.42",
"internal_solid_infill_speed": "150",
"line_width": "0.42",
"min_skirt_length": "0",
"minimum_sparse_infill_area": "70",
"only_one_wall_top": "1",
"outer_wall_acceleration": "2500",
"outer_wall_line_width": "0.42",
"outer_wall_speed": "100",
"reduce_crossing_wall": "1",
"skirt_distance": "6",
"skirt_height": "1",
"skirt_loops": "4",
"small_perimeter_speed": "200",
"sparse_infill_acceleration": "3000",
"sparse_infill_line_width": "0.42",
"sparse_infill_pattern": "grid",
"sparse_infill_speed": "150",
"support_base_pattern": "rectilinear",
"support_interface_bottom_layers": "-1",
"support_interface_pattern": "rectilinear",
"support_interface_spacing": "0",
"support_interface_speed": "100",
"support_interface_top_layers": "3",
"support_line_width": "0.35",
"support_object_xy_distance": "0.21",
"support_speed": "100",
"thick_bridges": "1",
"top_shell_thickness": "0",
"top_surface_acceleration": "2500",
"top_surface_line_width": "0.42",
"top_surface_speed": "150",
"travel_acceleration": "10000",
"travel_speed": "300",
"wall_generator": "classic",
"wall_loops": "2",
"wall_sequence": "outer wall/inner wall"
}
@@ -0,0 +1,13 @@
{
"type": "process",
"name": "0.30mm Standard @Goofoo Velo+",
"inherits": "fdm_process_klipper_common",
"from": "system",
"setting_id": "mi8ogYOYpqb2x60z",
"instantiation": "true",
"compatible_printers": [
"Goofoo Velo+ 0.6 nozzle"
],
"initial_layer_print_height": "0.3",
"layer_height": "0.3"
}
@@ -0,0 +1,91 @@
{
"type": "process",
"name": "fdm_process_common",
"from": "system",
"instantiation": "false",
"seam_position": "aligned",
"resolution": "0.012",
"elefant_foot_compensation": "0.2",
"precise_outer_wall": "1",
"ironing_type": "no ironing",
"ironing_speed": "30",
"ironing_flow": "10%",
"ironing_spacing": "0.15",
"reduce_crossing_wall": "1",
"max_travel_detour_distance": "0",
"detect_overhang_wall": "1",
"wall_loops": "3",
"top_surface_pattern": "monotonicline",
"top_shell_layers": "5",
"top_shell_thickness": "0.7",
"bottom_surface_pattern": "monotonic",
"bottom_shell_layers": "4",
"bottom_shell_thickness": "0.5",
"sparse_infill_density": "15%",
"sparse_infill_pattern": "crosshatch",
"infill_wall_overlap": "23%",
"infill_direction": "45",
"minimum_sparse_infill_area": "15",
"infill_combination": "0",
"ensure_vertical_shell_thickness": "1",
"initial_layer_speed": "50",
"initial_layer_infill_speed": "50",
"initial_layer_travel_speed": "100%",
"outer_wall_speed": "125",
"inner_wall_speed": "250",
"sparse_infill_speed": "250",
"internal_solid_infill_speed": "200",
"top_surface_speed": "150",
"gap_infill_speed": "150",
"support_speed": "125",
"support_interface_speed": "80",
"enable_overhang_speed": "1",
"overhang_1_4_speed": "0",
"overhang_2_4_speed": "50",
"overhang_3_4_speed": "25",
"overhang_4_4_speed": "15",
"bridge_speed": "50",
"travel_speed": "300",
"default_acceleration": "2500",
"outer_wall_acceleration": "500",
"inner_wall_acceleration": "500",
"bridge_acceleration": "500",
"travel_acceleration": "2500",
"default_jerk": "15",
"outer_wall_jerk": "10",
"inner_wall_jerk": "15",
"infill_jerk": "15",
"top_surface_jerk": "12",
"initial_layer_jerk": "12",
"travel_jerk": "20",
"enable_support": "0",
"support_type": "normal(auto)",
"support_threshold_angle": "30",
"support_on_build_plate_only": "0",
"raft_layers": "0",
"support_filament": "0",
"support_interface_filament": "0",
"support_top_z_distance": "0.2",
"support_base_pattern": "default",
"support_base_pattern_spacing": "2.5",
"support_interface_top_layers": "2",
"support_interface_bottom_layers": "2",
"support_interface_spacing": "0.5",
"support_object_xy_distance": "0.5",
"bridge_no_support": "0",
"independent_support_layer_height": "1",
"tree_support_branch_angle": "30",
"tree_support_wall_count": "0",
"skirt_loops": "2",
"skirt_distance": "3",
"skirt_height": "1",
"brim_width": "5",
"brim_object_gap": "0.1",
"enable_prime_tower": "0",
"prime_tower_width": "60",
"wipe_tower_no_sparse_layers": "0",
"print_sequence": "by layer",
"spiral_mode": "0",
"reduce_infill_retraction": "1",
"filename_format": "{input_filename_base}_{layer_height}mm_{filament_type[0]}_{printer_model}.gcode"
}
@@ -0,0 +1,23 @@
{
"type": "process",
"name": "fdm_process_klipper_common",
"inherits": "fdm_process_common",
"from": "system",
"instantiation": "false",
"default_acceleration": "5000",
"top_surface_acceleration": "3000",
"travel_acceleration": "7000",
"inner_wall_acceleration": "5000",
"outer_wall_acceleration": "3000",
"initial_layer_acceleration": "500",
"initial_layer_speed": "50",
"initial_layer_infill_speed": "105",
"outer_wall_speed": "120",
"inner_wall_speed": "200",
"internal_solid_infill_speed": "200",
"top_surface_speed": "100",
"gap_infill_speed": "100",
"sparse_infill_speed": "200",
"travel_speed": "350",
"exclude_object": "1"
}
+161 -1
View File
@@ -1,6 +1,6 @@
{
"name": "OrcaFilamentLibrary",
"version": "02.04.00.15",
"version": "02.04.00.16",
"force_update": "0",
"description": "Orca Filament Library",
"filament_list": [
@@ -1396,6 +1396,10 @@
"name": "addnorth ABS rABS",
"sub_path": "filament/addnorth/BBL/addnorth ABS rABS.json"
},
{
"name": "Goofoo ABS @base",
"sub_path": "filament/Goofoo/Goofoo ABS @base.json"
},
{
"name": "NIT ABS @System",
"sub_path": "filament/NIT/NIT ABS @System.json"
@@ -1480,6 +1484,10 @@
"name": "Elegoo ASA-CF @base",
"sub_path": "filament/Elegoo/Elegoo ASA-CF @base.json"
},
{
"name": "Goofoo ASA @base",
"sub_path": "filament/Goofoo/Goofoo ASA @base.json"
},
{
"name": "Overture ASA @BBL X1",
"sub_path": "filament/Overture/BBL/Overture ASA @BBL X1.json"
@@ -1636,10 +1644,22 @@
"name": "addnorth PVDF Adamant S1",
"sub_path": "filament/addnorth/BBL/addnorth PVDF Adamant S1.json"
},
{
"name": "Goofoo PA @base",
"sub_path": "filament/Goofoo/Goofoo PA @base.json"
},
{
"name": "addnorth PA-CF Adura X",
"sub_path": "filament/addnorth/BBL/addnorth PA-CF Adura X.json"
},
{
"name": "Goofoo PA-CF @base",
"sub_path": "filament/Goofoo/Goofoo PA-CF @base.json"
},
{
"name": "Goofoo PA-GF10 @base",
"sub_path": "filament/Goofoo/Goofoo PA-GF10 @base.json"
},
{
"name": "INSLOGIC PA6-66 @BBL P2S",
"sub_path": "filament/INSLOGIC/BBL/INSLOGIC PA6-66 @BBL P2S.json"
@@ -1712,6 +1732,10 @@
"name": "COEX PCTG PRIME @System",
"sub_path": "filament/COEX/COEX PCTG PRIME @System.json"
},
{
"name": "Goofoo PE @base",
"sub_path": "filament/Goofoo/Goofoo PE @base.json"
},
{
"name": "Generic PE-CF @System",
"sub_path": "filament/Generic PE-CF @System.json"
@@ -2176,6 +2200,10 @@
"name": "addnorth PETG rPETG Matte",
"sub_path": "filament/addnorth/BBL/addnorth PETG rPETG Matte.json"
},
{
"name": "Goofoo PETG @base",
"sub_path": "filament/Goofoo/Goofoo PETG @base.json"
},
{
"name": "Generic PETG HF @System",
"sub_path": "filament/Generic PETG HF @System.json"
@@ -2236,6 +2264,10 @@
"name": "eSUN PETG @System",
"sub_path": "filament/eSUN/eSUN PETG @System.json"
},
{
"name": "Goofoo PHA @base",
"sub_path": "filament/Goofoo/Goofoo PHA @base.json"
},
{
"name": "AliZ PLA @P1-X1",
"sub_path": "filament/AliZ/BBL/AliZ PLA @P1-X1.json"
@@ -3120,6 +3152,10 @@
"name": "addnorth PLA rPLA RE-ADD",
"sub_path": "filament/addnorth/BBL/addnorth PLA rPLA RE-ADD.json"
},
{
"name": "Goofoo PLA @base",
"sub_path": "filament/Goofoo/Goofoo PLA @base.json"
},
{
"name": "addnorth PLA X-PLA High Speed",
"sub_path": "filament/addnorth/BBL/addnorth PLA X-PLA High Speed.json"
@@ -3136,6 +3172,10 @@
"name": "addnorth PLA-CF Carbon Fiber",
"sub_path": "filament/addnorth/BBL/addnorth PLA-CF Carbon Fiber.json"
},
{
"name": "Goofoo PLA-CF @base",
"sub_path": "filament/Goofoo/Goofoo PLA-CF @base.json"
},
{
"name": "INSLOGIC PLA Pro @BBL P2S",
"sub_path": "filament/INSLOGIC/BBL/INSLOGIC PLA Pro @BBL P2S.json"
@@ -3820,6 +3860,10 @@
"name": "FILL3D PPCF @System",
"sub_path": "filament/FILL3D/FILL3D PPCF @System.json"
},
{
"name": "Goofoo PP @base",
"sub_path": "filament/Goofoo/Goofoo PP @base.json"
},
{
"name": "Generic PP-CF @System",
"sub_path": "filament/Generic PP-CF @System.json"
@@ -3840,6 +3884,10 @@
"name": "Bambu PVA @System",
"sub_path": "filament/Bambu/Bambu PVA @System.json"
},
{
"name": "Goofoo PVA @base",
"sub_path": "filament/Goofoo/Goofoo PVA @base.json"
},
{
"name": "FDplast SBS @System",
"sub_path": "filament/FDplast/FDplast SBS @System.json"
@@ -4112,6 +4160,14 @@
"name": "addnorth TPU Pro Matte 95A",
"sub_path": "filament/addnorth/BBL/addnorth TPU Pro Matte 95A.json"
},
{
"name": "Goofoo 90A TPU @base",
"sub_path": "filament/Goofoo/Goofoo 90A TPU @base.json"
},
{
"name": "Goofoo 95A TPU @base",
"sub_path": "filament/Goofoo/Goofoo 95A TPU @base.json"
},
{
"name": "Overture TPU @BBL A1",
"sub_path": "filament/Overture/BBL/Overture TPU @BBL A1.json"
@@ -4156,10 +4212,30 @@
"name": "Overture TPU @System",
"sub_path": "filament/Overture/Overture TPU @System.json"
},
{
"name": "Goofoo ABS @System",
"sub_path": "filament/Goofoo/Goofoo ABS @System.json"
},
{
"name": "Elegoo ASA-CF @System",
"sub_path": "filament/Elegoo/Elegoo ASA-CF @System.json"
},
{
"name": "Goofoo ASA @System",
"sub_path": "filament/Goofoo/Goofoo ASA @System.json"
},
{
"name": "Goofoo PA @System",
"sub_path": "filament/Goofoo/Goofoo PA @System.json"
},
{
"name": "Goofoo PA-CF @System",
"sub_path": "filament/Goofoo/Goofoo PA-CF @System.json"
},
{
"name": "Goofoo PA-GF10 @System",
"sub_path": "filament/Goofoo/Goofoo PA-GF10 @System.json"
},
{
"name": "Elegoo PC-FR @System",
"sub_path": "filament/Elegoo/Elegoo PC-FR @System.json"
@@ -4176,6 +4252,14 @@
"name": "COEX PCTG PRIME @BBL A1M 0.8 nozzle",
"sub_path": "filament/COEX/BBL/COEX PCTG PRIME @BBL A1M 0.8 nozzle.json"
},
{
"name": "Goofoo PE @System",
"sub_path": "filament/Goofoo/Goofoo PE @System.json"
},
{
"name": "Goofoo PE-CF @base",
"sub_path": "filament/Goofoo/Goofoo PE-CF @base.json"
},
{
"name": "AliZ PETG-CF @P1-X1",
"sub_path": "filament/AliZ/BBL/AliZ PETG-CF @P1-X1.json"
@@ -4236,10 +4320,26 @@
"name": "Fiberon PETG-ESD @BBL X1",
"sub_path": "filament/Polymaker/BBL/Fiberon PETG-ESD @BBL X1.json"
},
{
"name": "Goofoo PETG @System",
"sub_path": "filament/Goofoo/Goofoo PETG @System.json"
},
{
"name": "Goofoo PETG HF @base",
"sub_path": "filament/Goofoo/Goofoo PETG HF @base.json"
},
{
"name": "addnorth PETG-CF Rigid X",
"sub_path": "filament/addnorth/BBL/addnorth PETG-CF Rigid X.json"
},
{
"name": "Goofoo PETG-CF @base",
"sub_path": "filament/Goofoo/Goofoo PETG-CF @base.json"
},
{
"name": "Goofoo PHA @System",
"sub_path": "filament/Goofoo/Goofoo PHA @System.json"
},
{
"name": "COEX PLA @BBL A1M 0.2 nozzle",
"sub_path": "filament/COEX/BBL/COEX PLA @BBL A1M 0.2 nozzle.json"
@@ -4304,6 +4404,14 @@
"name": "Elegoo Rapid PLA+ @System",
"sub_path": "filament/Elegoo/Elegoo Rapid PLA+ @System.json"
},
{
"name": "Goofoo PLA @System",
"sub_path": "filament/Goofoo/Goofoo PLA @System.json"
},
{
"name": "Goofoo PLA-CF @System",
"sub_path": "filament/Goofoo/Goofoo PLA-CF @System.json"
},
{
"name": "Numakers PLA+ @BBL A1",
"sub_path": "filament/Numakers/BBL/Numakers PLA+ @BBL A1.json"
@@ -5188,9 +5296,61 @@
"name": "COEX PLA+Silk @System",
"sub_path": "filament/COEX/COEX PLA+Silk @System.json"
},
{
"name": "Goofoo PLA Silk @base",
"sub_path": "filament/Goofoo/Goofoo PLA Silk @base.json"
},
{
"name": "Goofoo PP @System",
"sub_path": "filament/Goofoo/Goofoo PP @System.json"
},
{
"name": "Goofoo PP-CF @base",
"sub_path": "filament/Goofoo/Goofoo PP-CF @base.json"
},
{
"name": "Goofoo PP-GF @base",
"sub_path": "filament/Goofoo/Goofoo PP-GF @base.json"
},
{
"name": "Goofoo PVA @System",
"sub_path": "filament/Goofoo/Goofoo PVA @System.json"
},
{
"name": "Elegoo Rapid TPU 95A @System",
"sub_path": "filament/Elegoo/Elegoo Rapid TPU 95A @System.json"
},
{
"name": "Goofoo 90A TPU @System",
"sub_path": "filament/Goofoo/Goofoo 90A TPU @System.json"
},
{
"name": "Goofoo 95A TPU @System",
"sub_path": "filament/Goofoo/Goofoo 95A TPU @System.json"
},
{
"name": "Goofoo PE-CF @System",
"sub_path": "filament/Goofoo/Goofoo PE-CF @System.json"
},
{
"name": "Goofoo PETG HF @System",
"sub_path": "filament/Goofoo/Goofoo PETG HF @System.json"
},
{
"name": "Goofoo PETG-CF @System",
"sub_path": "filament/Goofoo/Goofoo PETG-CF @System.json"
},
{
"name": "Goofoo PLA Silk @System",
"sub_path": "filament/Goofoo/Goofoo PLA Silk @System.json"
},
{
"name": "Goofoo PP-CF @System",
"sub_path": "filament/Goofoo/Goofoo PP-CF @System.json"
},
{
"name": "Goofoo PP-GF @System",
"sub_path": "filament/Goofoo/Goofoo PP-GF @System.json"
}
],
"process_list": [],
@@ -0,0 +1,9 @@
{
"type": "filament",
"name": "Goofoo 90A TPU @System",
"inherits": "Goofoo 90A TPU @base",
"from": "system",
"setting_id": "tjq76AQe9SCzZVo6",
"instantiation": "true",
"compatible_printers": []
}
@@ -0,0 +1,41 @@
{
"type": "filament",
"name": "Goofoo 90A TPU @base",
"inherits": "Generic TPU @System",
"from": "system",
"filament_id": "OFlVOXX3",
"instantiation": "false",
"fan_cooling_layer_time": [
"60"
],
"fan_min_speed": [
"35"
],
"filament_max_volumetric_speed": [
"2"
],
"hot_plate_temp": [
"40"
],
"hot_plate_temp_initial_layer": [
"40"
],
"nozzle_temperature": [
"220"
],
"nozzle_temperature_initial_layer": [
"220"
],
"reduce_fan_stop_start_freq": [
"0"
],
"slow_down_layer_time": [
"5"
],
"filament_vendor": [
"Goofoo"
],
"filament_type": [
"TPU"
]
}
@@ -0,0 +1,9 @@
{
"type": "filament",
"name": "Goofoo 95A TPU @System",
"inherits": "Goofoo 95A TPU @base",
"from": "system",
"setting_id": "2YTvrE1gu1yHHTNM",
"instantiation": "true",
"compatible_printers": []
}
@@ -0,0 +1,38 @@
{
"type": "filament",
"name": "Goofoo 95A TPU @base",
"inherits": "Generic TPU @System",
"from": "system",
"filament_id": "OFk50t30",
"instantiation": "false",
"fan_cooling_layer_time": [
"60"
],
"fan_min_speed": [
"35"
],
"hot_plate_temp": [
"40"
],
"hot_plate_temp_initial_layer": [
"40"
],
"nozzle_temperature": [
"220"
],
"nozzle_temperature_initial_layer": [
"220"
],
"reduce_fan_stop_start_freq": [
"0"
],
"slow_down_layer_time": [
"5"
],
"filament_vendor": [
"Goofoo"
],
"filament_type": [
"TPU"
]
}
@@ -0,0 +1,9 @@
{
"type": "filament",
"name": "Goofoo ABS @System",
"inherits": "Goofoo ABS @base",
"from": "system",
"setting_id": "UeINgPGH4eaSJinC",
"instantiation": "true",
"compatible_printers": []
}
@@ -0,0 +1,23 @@
{
"type": "filament",
"name": "Goofoo ABS @base",
"inherits": "Generic ABS @System",
"from": "system",
"filament_id": "OFd3PmW4",
"instantiation": "false",
"close_fan_the_first_x_layers": [
"1"
],
"hot_plate_temp": [
"110"
],
"hot_plate_temp_initial_layer": [
"110"
],
"filament_vendor": [
"Goofoo"
],
"filament_type": [
"ABS"
]
}
@@ -0,0 +1,9 @@
{
"type": "filament",
"name": "Goofoo ASA @System",
"inherits": "Goofoo ASA @base",
"from": "system",
"setting_id": "9ohqNl44uygTPfS9",
"instantiation": "true",
"compatible_printers": []
}
@@ -0,0 +1,20 @@
{
"type": "filament",
"name": "Goofoo ASA @base",
"inherits": "Generic ASA @System",
"from": "system",
"filament_id": "OFyncHIG",
"instantiation": "false",
"filament_flow_ratio": [
"0.93"
],
"hot_plate_temp_initial_layer": [
"100"
],
"filament_vendor": [
"Goofoo"
],
"filament_type": [
"ASA"
]
}
@@ -0,0 +1,9 @@
{
"type": "filament",
"name": "Goofoo PA @System",
"inherits": "Goofoo PA @base",
"from": "system",
"setting_id": "1ximJVgZZfGmGyzD",
"instantiation": "true",
"compatible_printers": []
}
@@ -0,0 +1,20 @@
{
"type": "filament",
"name": "Goofoo PA @base",
"inherits": "Generic PA @System",
"from": "system",
"filament_id": "OFc2x7Ly",
"instantiation": "false",
"filament_max_volumetric_speed": [
"12"
],
"reduce_fan_stop_start_freq": [
"1"
],
"filament_vendor": [
"Goofoo"
],
"filament_type": [
"PA"
]
}
@@ -0,0 +1,9 @@
{
"type": "filament",
"name": "Goofoo PA-CF @System",
"inherits": "Goofoo PA-CF @base",
"from": "system",
"setting_id": "US4zznMNti8tLUHG",
"instantiation": "true",
"compatible_printers": []
}
@@ -0,0 +1,17 @@
{
"type": "filament",
"name": "Goofoo PA-CF @base",
"inherits": "Generic PA-CF @System",
"from": "system",
"filament_id": "OF7skcRj",
"instantiation": "false",
"filament_max_volumetric_speed": [
"12"
],
"filament_vendor": [
"Goofoo"
],
"filament_type": [
"PA-CF"
]
}
@@ -0,0 +1,9 @@
{
"type": "filament",
"name": "Goofoo PA-GF10 @System",
"inherits": "Goofoo PA-GF10 @base",
"from": "system",
"setting_id": "Lea3hf2lQG0M9eTC",
"instantiation": "true",
"compatible_printers": []
}
@@ -0,0 +1,23 @@
{
"type": "filament",
"name": "Goofoo PA-GF10 @base",
"inherits": "Generic PA-CF @System",
"from": "system",
"filament_id": "OFtjqw2h",
"instantiation": "false",
"nozzle_temperature": [
"230"
],
"nozzle_temperature_initial_layer": [
"230"
],
"reduce_fan_stop_start_freq": [
"1"
],
"filament_vendor": [
"Goofoo"
],
"filament_type": [
"PA-GF"
]
}
@@ -0,0 +1,9 @@
{
"type": "filament",
"name": "Goofoo PE @System",
"inherits": "Goofoo PE @base",
"from": "system",
"setting_id": "muNxvgr0vdavMaOJ",
"instantiation": "true",
"compatible_printers": []
}
@@ -0,0 +1,14 @@
{
"type": "filament",
"name": "Goofoo PE @base",
"inherits": "Generic PE @System",
"from": "system",
"filament_id": "OFAln723",
"instantiation": "false",
"filament_vendor": [
"Goofoo"
],
"filament_type": [
"PE"
]
}
@@ -0,0 +1,9 @@
{
"type": "filament",
"name": "Goofoo PE-CF @System",
"inherits": "Goofoo PE-CF @base",
"from": "system",
"setting_id": "58fMbg6AClvalw5o",
"instantiation": "true",
"compatible_printers": []
}
@@ -0,0 +1,14 @@
{
"type": "filament",
"name": "Goofoo PE-CF @base",
"inherits": "Generic PE-CF @System",
"from": "system",
"filament_id": "OF4Ll1Sk",
"instantiation": "false",
"filament_vendor": [
"Goofoo"
],
"filament_type": [
"PE-CF"
]
}
@@ -0,0 +1,9 @@
{
"type": "filament",
"name": "Goofoo PETG @System",
"inherits": "Goofoo PETG @base",
"from": "system",
"setting_id": "a9OKgJeoJgzfZiAB",
"instantiation": "true",
"compatible_printers": []
}
@@ -0,0 +1,26 @@
{
"type": "filament",
"name": "Goofoo PETG @base",
"inherits": "Generic PETG @System",
"from": "system",
"filament_id": "OFHdIbiu",
"instantiation": "false",
"hot_plate_temp": [
"80"
],
"hot_plate_temp_initial_layer": [
"80"
],
"nozzle_temperature": [
"240"
],
"nozzle_temperature_initial_layer": [
"240"
],
"filament_vendor": [
"Goofoo"
],
"filament_type": [
"PETG"
]
}
@@ -0,0 +1,9 @@
{
"type": "filament",
"name": "Goofoo PETG HF @System",
"inherits": "Goofoo PETG HF @base",
"from": "system",
"setting_id": "eMjhTRf5qPeeqSgk",
"instantiation": "true",
"compatible_printers": []
}
@@ -0,0 +1,14 @@
{
"type": "filament",
"name": "Goofoo PETG HF @base",
"inherits": "Generic PETG HF @System",
"from": "system",
"filament_id": "OF5bjxyx",
"instantiation": "false",
"filament_vendor": [
"Goofoo"
],
"filament_type": [
"PETG HF"
]
}
@@ -0,0 +1,9 @@
{
"type": "filament",
"name": "Goofoo PETG-CF @System",
"inherits": "Goofoo PETG-CF @base",
"from": "system",
"setting_id": "ULNFo6zjgjc5MEIa",
"instantiation": "true",
"compatible_printers": []
}
@@ -0,0 +1,14 @@
{
"type": "filament",
"name": "Goofoo PETG-CF @base",
"inherits": "Generic PETG-CF @System",
"from": "system",
"filament_id": "OFo6Ff38",
"instantiation": "false",
"filament_vendor": [
"Goofoo"
],
"filament_type": [
"PETG-CF"
]
}
@@ -0,0 +1,9 @@
{
"type": "filament",
"name": "Goofoo PHA @System",
"inherits": "Goofoo PHA @base",
"from": "system",
"setting_id": "cfdeMyqld72Lqh38",
"instantiation": "true",
"compatible_printers": []
}
@@ -0,0 +1,14 @@
{
"type": "filament",
"name": "Goofoo PHA @base",
"inherits": "Generic PHA @System",
"from": "system",
"filament_id": "OFicmNhN",
"instantiation": "false",
"filament_vendor": [
"Goofoo"
],
"filament_type": [
"PHA"
]
}
@@ -0,0 +1,9 @@
{
"type": "filament",
"name": "Goofoo PLA @System",
"inherits": "Goofoo PLA @base",
"from": "system",
"setting_id": "UFWRE1b3706Cu1Iq",
"instantiation": "true",
"compatible_printers": []
}
@@ -0,0 +1,26 @@
{
"type": "filament",
"name": "Goofoo PLA @base",
"inherits": "Generic PLA @System",
"from": "system",
"filament_id": "OFoq0Cmt",
"instantiation": "false",
"hot_plate_temp": [
"40"
],
"hot_plate_temp_initial_layer": [
"40"
],
"nozzle_temperature": [
"230"
],
"nozzle_temperature_initial_layer": [
"230"
],
"filament_vendor": [
"Goofoo"
],
"filament_type": [
"PLA"
]
}
@@ -0,0 +1,9 @@
{
"type": "filament",
"name": "Goofoo PLA Silk @System",
"inherits": "Goofoo PLA Silk @base",
"from": "system",
"setting_id": "2pW2NEoY4hFCkNun",
"instantiation": "true",
"compatible_printers": []
}
@@ -0,0 +1,14 @@
{
"type": "filament",
"name": "Goofoo PLA Silk @base",
"inherits": "Generic PLA Silk @System",
"from": "system",
"filament_id": "OFpta2gc",
"instantiation": "false",
"filament_vendor": [
"Goofoo"
],
"filament_type": [
"PLA Silk"
]
}
@@ -0,0 +1,9 @@
{
"type": "filament",
"name": "Goofoo PLA-CF @System",
"inherits": "Goofoo PLA-CF @base",
"from": "system",
"setting_id": "R0UMDUPY9ryat8gQ",
"instantiation": "true",
"compatible_printers": []
}
@@ -0,0 +1,29 @@
{
"type": "filament",
"name": "Goofoo PLA-CF @base",
"inherits": "Generic PLA-CF @System",
"from": "system",
"filament_id": "OFMNABGJ",
"instantiation": "false",
"filament_flow_ratio": [
"0.98"
],
"hot_plate_temp": [
"40"
],
"hot_plate_temp_initial_layer": [
"40"
],
"nozzle_temperature": [
"240"
],
"nozzle_temperature_initial_layer": [
"240"
],
"filament_vendor": [
"Goofoo"
],
"filament_type": [
"PLA-CF"
]
}
@@ -0,0 +1,9 @@
{
"type": "filament",
"name": "Goofoo PP @System",
"inherits": "Goofoo PP @base",
"from": "system",
"setting_id": "TaB8JUtjJWgRcNrv",
"instantiation": "true",
"compatible_printers": []
}
@@ -0,0 +1,14 @@
{
"type": "filament",
"name": "Goofoo PP @base",
"inherits": "Generic PP @System",
"from": "system",
"filament_id": "OFlqlUSx",
"instantiation": "false",
"filament_vendor": [
"Goofoo"
],
"filament_type": [
"PP"
]
}
@@ -0,0 +1,9 @@
{
"type": "filament",
"name": "Goofoo PP-CF @System",
"inherits": "Goofoo PP-CF @base",
"from": "system",
"setting_id": "uHocbocT6flJhlBz",
"instantiation": "true",
"compatible_printers": []
}
@@ -0,0 +1,14 @@
{
"type": "filament",
"name": "Goofoo PP-CF @base",
"inherits": "Generic PP-CF @System",
"from": "system",
"filament_id": "OFUUiuIV",
"instantiation": "false",
"filament_vendor": [
"Goofoo"
],
"filament_type": [
"PP-CF"
]
}
@@ -0,0 +1,9 @@
{
"type": "filament",
"name": "Goofoo PP-GF @System",
"inherits": "Goofoo PP-GF @base",
"from": "system",
"setting_id": "mns8gICV0aNwJ02L",
"instantiation": "true",
"compatible_printers": []
}
@@ -0,0 +1,14 @@
{
"type": "filament",
"name": "Goofoo PP-GF @base",
"inherits": "Generic PP-GF @System",
"from": "system",
"filament_id": "OFaOhzl8",
"instantiation": "false",
"filament_vendor": [
"Goofoo"
],
"filament_type": [
"PP-GF"
]
}
@@ -0,0 +1,9 @@
{
"type": "filament",
"name": "Goofoo PVA @System",
"inherits": "Goofoo PVA @base",
"from": "system",
"setting_id": "tUXbv31nYkXJ87OT",
"instantiation": "true",
"compatible_printers": []
}
@@ -0,0 +1,14 @@
{
"type": "filament",
"name": "Goofoo PVA @base",
"inherits": "Generic PVA @System",
"from": "system",
"filament_id": "OF86rkt6",
"instantiation": "false",
"filament_vendor": [
"Goofoo"
],
"filament_type": [
"PVA"
]
}
+5
View File
@@ -10,6 +10,11 @@ function OnInit()
// Orca: fallback timeout in case the C++ -> JS signal fails (e.g., WebKit issues).
// Jump to the target page after 3 minutes so slow computers don't get stuck on a partially loaded page.
setTimeout("JumpToTarget()",180*1000);
var tSend={};
tSend['sequence_id']=Math.round(new Date() / 1000);
tSend['command']="userguide_loading_page_ready";
SendWXMessage(JSON.stringify(tSend));
}
function HandleStudio( pVal )
+410
View File
@@ -0,0 +1,410 @@
# /// script
# requires-python = ">=3.12"
#
# [tool.orcaslicer.plugin]
# id = "reload-local-bundle-demo"
# name = "Reload Local Bundle Demo"
# description = "Manages local filament, process, and printer profiles with live bundle reload."
# author = "OrcaSlicer contributors"
# version = "0.1.0"
# ///
"""A small interactive consumer of ``orca.host.preset.bundles.reload_local``."""
import json
import os
import tempfile
import orca
BUNDLE_ID = "reload-local-bundle-demo"
STATE_FILE = ".reload-local-bundle-demo-state.json"
DEMO_PRESETS = (
{
"id": "filament",
"name": "Local Bundle Demo Filament",
"kind": "Filament",
"type": "filament",
"directory": "filament",
"collection": "filaments",
"setting_key": "filament_settings_id",
},
{
"id": "process",
"name": "Local Bundle Demo Process",
"kind": "Process",
"type": "process",
"directory": "process",
"collection": "prints",
"setting_key": "print_settings_id",
},
{
"id": "printer",
"name": "Local Bundle Demo Printer",
"kind": "Printer",
"type": "machine",
"directory": "machine",
"collection": "printers",
"setting_key": "printer_settings_id",
},
)
def _data_dir():
parts = os.path.abspath(__file__).replace("\\", "/").split("/")
if "orca_plugins" not in parts:
raise RuntimeError("Install this file as a local OrcaSlicer plugin")
return "/".join(parts[: parts.index("orca_plugins")])
def _folder_from_preset_file(path):
parts = os.path.normpath(path or "").replace("\\", "/").split("/")
try:
return parts[parts.index("user") + 1]
except (ValueError, IndexError):
return None
def _bundle_dir(bundle):
for collection in (bundle.filaments, bundle.prints, bundle.printers):
for index in range(collection.size()):
folder = _folder_from_preset_file(collection.preset(index).file)
if folder:
return os.path.join(_data_dir(), "user", folder, "_local", BUNDLE_ID)
return os.path.join(_data_dir(), "user", "default", "_local", BUNDLE_ID)
def _source_path(bundle, item):
return os.path.join(_bundle_dir(bundle), item["directory"], item["name"] + ".json")
def _state_path(bundle):
return os.path.join(_bundle_dir(bundle), STATE_FILE)
def _write_json_atomic(path, value):
os.makedirs(os.path.dirname(path), exist_ok=True)
descriptor, temporary = tempfile.mkstemp(
prefix="." + os.path.basename(path) + ".", suffix=".tmp", dir=os.path.dirname(path)
)
try:
with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
json.dump(value, stream, ensure_ascii=False, indent=2)
stream.write("\n")
os.replace(temporary, path)
except Exception:
try:
os.unlink(temporary)
except OSError:
pass
raise
def _remove_file(path):
try:
os.unlink(path)
except FileNotFoundError:
pass
def _read_state(bundle):
try:
with open(_state_path(bundle), "r", encoding="utf-8") as stream:
state = json.load(stream)
except FileNotFoundError:
return {"profiles": {}}
except (OSError, ValueError) as error:
raise RuntimeError("The demo state file cannot be read: %s" % error)
if not isinstance(state, dict) or not isinstance(state.get("profiles"), dict):
raise RuntimeError("The demo state file has an invalid shape")
return state
def _profile_state(state, item):
profiles = state["profiles"]
profile = profiles.get(item["id"])
if profile is None:
profile = {}
profiles[item["id"]] = profile
if not isinstance(profile, dict):
raise RuntimeError("The demo state for %s has an invalid shape" % item["name"])
profile["enabled"] = profile.get("enabled") is True
profile["deleted"] = profile.get("deleted") is True
if profile["deleted"]:
profile["enabled"] = False
profile["parent_name"] = str(profile.get("parent_name") or "")
return profile
def _collection(bundle, item):
return getattr(bundle, item["collection"])
def _target_name(item):
return "_local/%s/%s" % (BUNDLE_ID, item["name"])
def _parent_name(bundle, item):
collection = _collection(bundle, item)
own_names = {
_target_name(candidate)
for candidate in DEMO_PRESETS
if candidate["collection"] == item["collection"]
}
candidates = [collection.selected_preset()]
candidates.extend(collection.preset(index) for index in range(collection.size()))
for preset in candidates:
if preset.name not in own_names:
return preset.name
raise RuntimeError("A parent %s preset is required for this demo" % item["kind"].lower())
def _parent_is_live(bundle, item, name):
return bool(name) and name not in {_target_name(item)} and _collection(bundle, item).find_preset(name) is not None
def _profile(item, parent_name):
return {
"type": item["type"],
"name": item["name"],
"from": "User",
"instantiation": "true",
"version": "1.0.0",
"inherits": parent_name,
item["setting_key"]: item["name"],
}
def _reconcile(bundle, state):
for item in DEMO_PRESETS:
profile = _profile_state(state, item)
if profile["enabled"] and not profile["deleted"] and not _parent_is_live(bundle, item, profile["parent_name"]):
profile["parent_name"] = _parent_name(bundle, item)
for item in DEMO_PRESETS:
profile = _profile_state(state, item)
path = _source_path(bundle, item)
if profile["enabled"] and not profile["deleted"]:
_write_json_atomic(path, _profile(item, profile["parent_name"]))
else:
_remove_file(path)
_write_json_atomic(
os.path.join(_bundle_dir(bundle), "bundle_metadata.json"),
{"id": BUNDLE_ID, "name": "Reload Local Bundle Demo", "version": "0.1.0"},
)
_write_json_atomic(_state_path(bundle), state)
orca.host.preset.bundles.reload_local(BUNDLE_ID)
def _state_payload(bundle, state, message="", error=""):
items = []
for item in DEMO_PRESETS:
profile = _profile_state(state, item)
items.append(
{
"id": item["id"],
"name": item["name"],
"kind": item["kind"],
"enabled": profile["enabled"],
"deleted": profile["deleted"],
"loaded": _collection(bundle, item).find_preset(_target_name(item)) is not None,
}
)
return {
"command": "state",
"items": items,
"bundles": orca.host.preset.bundles.list_ids(),
"message": message,
"error": error,
}
PAGE = r"""<!doctype html>
<html><head><meta charset="utf-8"><style>
:root { color-scheme: light dark; font: 14px system-ui, sans-serif; }
body { margin: 0; padding: 16px; background: Canvas; color: CanvasText; }
.profiles { display: grid; gap: 9px; }
.profile { border: 1px solid rgba(127, 127, 127, .4); border-radius: 8px; padding: 11px; }
.top, .bottom { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.top { margin-bottom: 10px; }
.name { font-weight: 650; }
.kind, .message { color: GrayText; font-size: 12px; }
.status { border-radius: 999px; padding: 3px 8px; font-size: 12px; white-space: nowrap; }
.loaded { background: #0b8f5a; color: white; }
.disabled, .deleted { background: rgba(127, 127, 127, .25); }
.missing { background: #be4b0a; color: white; }
.toggle { display: inline-flex; align-items: center; gap: 7px; cursor: pointer; }
.toggle input { position: absolute; opacity: 0; pointer-events: none; }
.switch { width: 34px; height: 20px; border-radius: 999px; background: rgba(127, 127, 127, .45); position: relative; }
.switch::after { content: ""; position: absolute; top: 3px; left: 3px; width: 14px; height: 14px; border-radius: 50%; background: white; transition: transform .15s ease; }
.toggle input:checked + .switch { background: #008f7a; }
.toggle input:checked + .switch::after { transform: translateX(14px); }
.toggle input:disabled + .switch, button:disabled { opacity: .5; cursor: wait; }
button { border: 0; border-radius: 6px; padding: 6px 10px; font: inherit; cursor: pointer; background: #b64036; color: white; }
button.secondary { background: rgba(127, 127, 127, .25); color: CanvasText; }
.actions { display: flex; justify-content: flex-end; margin-top: 14px; }
.message { min-height: 18px; margin-top: 10px; }
.error { color: #d13c32; }
.help { margin: 0 0 12px; color: GrayText; font-size: 12px; line-height: 1.4; }
.help summary { color: CanvasText; cursor: pointer; font-weight: 650; }
.help p { margin: 7px 0 0; }
h2 { margin: 16px 0 8px; font-size: 13px; }
.bundles { display: grid; gap: 6px; }
.bundle { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.bundle code { overflow-wrap: anywhere; }
</style></head><body>
<details class="help"><summary>How it works</summary><p>Each row controls one profile in this local bundle. A switch adds or removes its source file; Delete removes the demo record and Restore returns it disabled. Every action reloads this bundle across filament, process, and machine. Local bundles lists every bundle id that orca.host.preset.bundles.list_ids() reports; Reload calls orca.host.preset.bundles.reload_local() for that id.</p></details>
<div class="profiles" id="profiles"></div>
<h2>Local bundles</h2>
<div class="bundles" id="bundles"></div>
<div class="actions"><button class="secondary" id="close">Close</button></div>
<div class="message" id="message"></div>
<script>
'use strict';
const $ = id => document.getElementById(id);
let items = [];
let bundles = [];
let busy = false;
function esc(value) {
return String(value == null ? '' : value).replace(/[&<>"']/g, c =>
({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
function status(item) {
if (item.deleted) return ['deleted', 'Deleted'];
if (item.loaded) return ['loaded', 'Loaded'];
if (item.enabled) return ['missing', 'Missing after reload'];
return ['disabled', 'Not in Orca'];
}
function setBusy(value) {
document.querySelectorAll('[data-toggle], [data-action]').forEach(control => control.disabled = value);
}
function render(message, error) {
$('profiles').innerHTML = items.map(item => {
const state = status(item);
const deleted = item.deleted ? ' disabled' : '';
const action = item.deleted
? '<button class="secondary" data-action="restore" data-id="' + esc(item.id) + '">Restore</button>'
: '<button data-action="delete" data-id="' + esc(item.id) + '">Delete</button>';
return '<section class="profile"><div class="top"><div><div class="name">' + esc(item.name) +
'</div><div class="kind">' + esc(item.kind) + '</div></div><span class="status ' + state[0] +
'">' + state[1] + '</span></div><div class="bottom"><label class="toggle"><input type="checkbox" data-toggle="' +
esc(item.id) + '"' + (item.enabled ? ' checked' : '') + deleted + '><span class="switch"></span>Available in Orca</label>' +
action + '</div></section>';
}).join('');
$('bundles').innerHTML = bundles.length ? bundles.map(id =>
'<div class="bundle"><code>' + esc(id) + '</code><button class="secondary" data-action="reload_bundle" data-bundle="' +
esc(id) + '">Reload</button></div>').join('') : '<div class="kind">No local bundles found.</div>';
$('message').textContent = error || message || '';
$('message').className = error ? 'message error' : 'message';
if (busy) setBusy(true);
}
function post(message) {
busy = true;
setBusy(true);
$('message').textContent = 'Reloading local bundle…';
$('message').className = 'message';
orca.postMessage(message);
}
$('close').addEventListener('click', () => orca.close());
$('profiles').addEventListener('change', event => {
if (!busy && event.target.matches('[data-toggle]')) {
post({command: 'set_enabled', id: event.target.dataset.toggle, enabled: event.target.checked});
}
});
$('profiles').addEventListener('click', event => {
const button = event.target.closest('[data-action]');
if (!button || busy) return;
if (button.dataset.action === 'delete' && !confirm('Delete this demo profile from the local bundle?')) return;
post({command: button.dataset.action, id: button.dataset.id});
});
$('bundles').addEventListener('click', event => {
const button = event.target.closest('[data-action="reload_bundle"]');
if (button && !busy) post({command: 'reload_bundle', bundle_id: button.dataset.bundle});
});
orca.onMessage(message => {
if (!message || message.command !== 'state') return;
items = message.items || [];
bundles = message.bundles || [];
busy = false;
render(message.message, message.error);
});
orca.postMessage({command: 'state'});
</script></body></html>"""
class ReloadLocalBundleDemo(orca.script.ScriptPluginCapabilityBase):
window = None
def get_name(self):
return "Reload local bundle demo"
def execute(self):
try:
if self.window is not None and self.window.is_open():
self._post_state()
return orca.ExecutionResult.success("Reload local bundle demo refreshed.")
self.window = orca.host.ui.create_window(
title="Reload local bundle demo", html=PAGE, width=500, height=420,
on_message=self.on_message, on_close=self.on_close,
)
return orca.ExecutionResult.success("Reload local bundle demo opened.")
except Exception as error:
message = "Reload local bundle demo failed: %s" % error
orca.host.ui.message(message, title="Reload local bundle demo", buttons="ok", icon="error")
return orca.ExecutionResult.failure(orca.PluginResult.RecoverableError, message)
def on_close(self):
self.window = None
def _post_state(self, message="", error=""):
try:
bundle = orca.host.preset_bundle()
payload = _state_payload(bundle, _read_state(bundle), message, error)
except Exception as state_error:
payload = {"command": "state", "items": [], "message": message, "error": str(state_error)}
if self.window is not None and self.window.is_open():
self.window.post(payload)
def on_message(self, message):
try:
message = message or {}
bundle = orca.host.preset_bundle()
state = _read_state(bundle)
if message.get("command") == "state":
self._post_state()
return
if message.get("command") == "reload_bundle":
bundle_id = message.get("bundle_id")
if not isinstance(bundle_id, str) or not bundle_id:
raise RuntimeError("A bundle id is required")
orca.host.preset.bundles.reload_local(bundle_id)
self._post_state(message="Reloaded local bundle %s." % bundle_id)
return
item = next((item for item in DEMO_PRESETS if item["id"] == message.get("id")), None)
if item is None:
raise RuntimeError("Unknown demo preset")
profile = _profile_state(state, item)
command = message.get("command")
if command == "set_enabled":
enabled = message.get("enabled")
if not isinstance(enabled, bool):
raise RuntimeError("Preset availability must be a boolean")
if profile["deleted"]:
raise RuntimeError("Restore the deleted demo preset before enabling it")
profile["enabled"] = enabled
elif command == "delete":
profile.update({"enabled": False, "deleted": True, "parent_name": ""})
elif command == "restore":
profile.update({"enabled": False, "deleted": False, "parent_name": ""})
else:
raise RuntimeError("Unknown demo action")
_reconcile(bundle, state)
self._post_state(message="The change was applied. Check the corresponding preset selector in Orca.")
except Exception as error:
self._post_state(error=str(error))
@orca.plugin
class ReloadLocalBundleDemoPackage(orca.base):
def register_capabilities(self):
orca.register_capability(ReloadLocalBundleDemo)
+16 -5
View File
@@ -1822,7 +1822,7 @@ PresetCollection::UserPresetLoad PresetCollection::resolve_user_preset(
const boost::filesystem::path &file, const std::string &canonical_name,
const PresetOrigin &load_origin, ForwardCompatibilitySubstitutionRule substitution_rule,
const std::string &extruder_id_name, const std::string &extruder_variant_name,
std::set<std::string> *key_set1, std::set<std::string> *key_set2) const
std::set<std::string> *key_set1, std::set<std::string> *key_set2, bool read_only) const
{
UserPresetLoad out;
out.preset = Preset(m_type, canonical_name, false);
@@ -1856,7 +1856,10 @@ PresetCollection::UserPresetLoad PresetCollection::resolve_user_preset(
std::string version_str = key_values[BBL_JSON_KEY_VERSION];
boost::optional<Semver> version = Semver::parse(version_str);
if (!version) return out;
if (!version) {
out.errors.push_back((boost::format("invalid version %1% in config %2%") % version_str % preset.file).str());
return out;
}
preset.version = *version;
if (key_values.find(BBL_JSON_KEY_FILAMENT_ID) != key_values.end())
@@ -1878,6 +1881,12 @@ PresetCollection::UserPresetLoad PresetCollection::resolve_user_preset(
inherit_preset = this->find_preset2(inherits_value);
Preset::normalize_inherits(config, inherit_preset);
}
// Local hot reload only supports FFF; no SLA default printer is installed.
if (read_only && load_origin.kind == PresetOrigin::Kind::LocalBundle && m_type == Preset::TYPE_PRINTER &&
Preset::printer_technology(config) != ptFFF) {
out.errors.push_back((boost::format("local bundle printer %1% is not FFF") % preset.file).str());
return out;
}
const Preset& default_preset = this->default_preset_for(config);
if (inherit_preset) {
preset.config = inherit_preset->config;
@@ -2042,7 +2051,7 @@ void PresetCollection::load_presets(
resolve_then_commit<CNumericLocalesSetter>(files.size(),
[&](size_t i) {
return this->resolve_user_preset(files[i].path, files[i].canonical_name, resolved_origin, substitution_rule,
extruder_id_name, extruder_variant_name, key_set1, key_set2);
extruder_id_name, extruder_variant_name, key_set1, key_set2, read_only);
},
[&](size_t i, UserPresetLoad &&loaded) {
// Resolve read the files without the lock, so another instance may have saved
@@ -2062,7 +2071,7 @@ void PresetCollection::load_presets(
if (! (on_disk == loaded.on_disk)) {
CNumericLocalesSetter locales_setter;
loaded = this->resolve_user_preset(files[i].path, files[i].canonical_name, resolved_origin, substitution_rule,
extruder_id_name, extruder_variant_name, key_set1, key_set2);
extruder_id_name, extruder_variant_name, key_set1, key_set2, read_only);
}
}
const bool leave_files = read_only || (! lock_path.empty() && ! instance_lock.locked());
@@ -3135,7 +3144,9 @@ std::pair<Preset*, bool> PresetCollection::load_external_preset(
}
// Insert a new profile.
//BBS: add project embedded preset logic
bool from_project = boost::algorithm::iends_with(name, ".3mf");
// A G-code's settings are shown like a project's, so a preset made from them lives with the project
// rather than in the user's library.
bool from_project = boost::algorithm::iends_with(name, ".3mf") || is_gcode_file(name);
if (m_type == Preset::TYPE_PRINT)
cfg.option<ConfigOptionString >("print_settings_id", true)->value = new_name;
else if (m_type == Preset::TYPE_FILAMENT)
+2 -1
View File
@@ -991,7 +991,8 @@ private:
UserPresetLoad resolve_user_preset(const boost::filesystem::path &file, const std::string &canonical_name,
const PresetOrigin &load_origin, ForwardCompatibilitySubstitutionRule substitution_rule,
const std::string &extruder_id_name, const std::string &extruder_variant_name,
std::set<std::string> *key_set1, std::set<std::string> *key_set2) const;
std::set<std::string> *key_set1, std::set<std::string> *key_set2,
bool read_only) const;
// Install one resolved preset. The collection, its alias maps, the error count
// and the preset files on disk are touched here and only here.
+435 -30
View File
@@ -1,3 +1,4 @@
#include <array>
#include <atomic>
#include <boost/filesystem/path.hpp>
#include <boost/filesystem/operations.hpp>
@@ -56,8 +57,10 @@
#include <algorithm>
#include <cstdlib>
#include <mutex>
#include <numeric>
#include <set>
#include <shared_mutex>
#include <fstream>
#include <string>
#include <tuple>
@@ -1383,6 +1386,27 @@ std::string PresetBundle::get_hotend_model_for_printer_model(std::string model_n
return out;
}
static void load_bundle_presets(PresetBundle& bundle, const std::string& bundle_dir, BundleMetadata& metadata,
const PresetOrigin& origin, PresetsConfigSubstitutions& substitutions,
ForwardCompatibilitySubstitutionRule substitution_rule, bool read_only = false)
{
struct Collection {
PresetCollection& presets;
const char* directory;
std::vector<std::string>& names;
};
metadata.print_presets.clear();
metadata.filament_presets.clear();
metadata.printer_presets.clear();
for (Collection collection : {
Collection{ bundle.prints, PRESET_PRINT_NAME, metadata.print_presets },
Collection{ bundle.filaments, PRESET_FILAMENT_NAME, metadata.filament_presets },
Collection{ bundle.printers, PRESET_PRINTER_NAME, metadata.printer_presets } })
collection.presets.load_presets(bundle_dir, collection.directory, substitutions, substitution_rule,
[&](Preset& preset) { collection.names.push_back(preset.name); }, origin, read_only);
}
PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, ForwardCompatibilitySubstitutionRule substitution_rule, bool read_only)
{
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " entry and user is: " << user;
@@ -1431,19 +1455,8 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For
BundleMetadata metadata;
if (!load_bundle_metadata(metadata_file, metadata)) continue;
metadata.print_presets.clear();
metadata.filament_presets.clear();
metadata.printer_presets.clear();
this->prints.load_presets(bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, [&](Preset& preset) {
metadata.print_presets.push_back(preset.name);
}, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id), read_only);
this->filaments.load_presets(bundle_dir, PRESET_FILAMENT_NAME, substitutions, substitution_rule, [&](Preset& preset) {
metadata.filament_presets.push_back(preset.name);
}, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id), read_only);
this->printers.load_presets(bundle_dir, PRESET_PRINTER_NAME, substitutions, substitution_rule, [&](Preset& preset) {
metadata.printer_presets.push_back(preset.name);
}, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id), read_only);
load_bundle_presets(*this, bundle_dir, metadata, PresetOrigin(PresetOrigin::Kind::LocalBundle, metadata.id),
substitutions, substitution_rule, read_only);
metadata.bundle_type = BundleType::Local;
metadata.path = metadata_file.string();
@@ -1466,20 +1479,10 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For
BundleMetadata metadata;
if (!load_bundle_metadata(metadata_file, metadata)) continue;
metadata.print_presets.clear();
metadata.filament_presets.clear();
metadata.printer_presets.clear();
metadata.is_subscribed = true;
this->prints.load_presets(bundle_dir, PRESET_PRINT_NAME, substitutions, substitution_rule, [&](Preset& preset) {
metadata.print_presets.push_back(preset.name);
}, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id), read_only);
this->filaments.load_presets(bundle_dir, PRESET_FILAMENT_NAME, substitutions, substitution_rule, [&](Preset& preset) {
metadata.filament_presets.push_back(preset.name);
}, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id), read_only);
this->printers.load_presets(bundle_dir, PRESET_PRINTER_NAME, substitutions, substitution_rule, [&](Preset& preset) {
metadata.printer_presets.push_back(preset.name);
}, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id), read_only);
load_bundle_presets(*this, bundle_dir, metadata, PresetOrigin(PresetOrigin::Kind::SubscribedBundle, metadata.id),
substitutions, substitution_rule, read_only);
metadata.bundle_type = BundleType::Subscribed;
metadata.path = metadata_file.string();
@@ -1534,6 +1537,335 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For
return PresetsConfigSubstitutions();
}
static bool is_single_path_component(const std::string& value)
{
const fs::path path(value);
return !value.empty() && value != "." && value != ".." && value.find_first_of("/\\") == std::string::npos &&
!path.has_root_name() && !path.has_root_directory();
}
static bool check_local_bundle(PresetBundleMetadata& bundles, const fs::path& user_dir, const std::string& bundle_id,
BundleMetadata& metadata, std::string& error)
{
if (!is_single_path_component(bundle_id)) {
error = "Local bundle id must be a single path component";
return false;
}
const fs::path bundle_dir = user_dir / PRESET_LOCAL_DIR / bundle_id;
if (!fs::is_directory(bundle_dir)) {
error = "Local bundle directory does not exist";
return false;
}
if (fs::exists(user_dir / PRESET_SUBSCRIBED_DIR / bundle_id)) {
error = "A subscribed bundle already uses this id";
return false;
}
{
std::shared_lock<std::shared_mutex> lock(bundles.RWMtx);
const auto it = bundles.m_bundles.find(bundle_id);
if (it != bundles.m_bundles.end() && (it->second.bundle_type == BundleType::Subscribed || it->second.is_subscribed)) {
error = "A subscribed bundle already uses this id";
return false;
}
}
if (!metadata.load_from_json((bundle_dir / PRESET_BUNDLE_METADATA).string()) || metadata.id != bundle_id) {
error = "Local bundle metadata is missing, malformed, or has a different id";
return false;
}
return true;
}
std::vector<std::string> PresetBundle::list_local_bundle_ids(const std::string& preset_folder)
{
std::vector<std::string> ids;
const std::string user = preset_folder.empty() ? DEFAULT_USER_FOLDER_NAME : preset_folder;
if (!is_single_path_component(user))
return ids;
const fs::path user_dir = fs::path(data_dir()) / PRESET_USER_DIR / user;
const fs::path local_dir = user_dir / PRESET_LOCAL_DIR;
try {
if (!fs::is_directory(local_dir))
return ids;
for (const auto& entry : fs::directory_iterator(local_dir)) {
const std::string bundle_id = entry.path().filename().string();
try {
BundleMetadata metadata;
std::string error;
if (check_local_bundle(bundles, user_dir, bundle_id, metadata, error))
ids.push_back(bundle_id);
} catch (const std::exception& exception) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": skipping " << bundle_id << ": " << exception.what();
}
}
} catch (const std::exception& exception) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": " << exception.what();
}
std::sort(ids.begin(), ids.end());
return ids;
}
bool PresetBundle::reload_local_bundle(const std::string& preset_folder, const std::string& bundle_id, std::string* error)
{
auto fail = [error](const std::string& message) {
if (error != nullptr)
*error = message;
return false;
};
if (!is_single_path_component(bundle_id))
return fail("Local bundle id must be a single path component");
const std::string user = preset_folder.empty() ? DEFAULT_USER_FOLDER_NAME : preset_folder;
if (!is_single_path_component(user))
return fail("Preset folder must be a single path component");
try {
const fs::path user_dir = fs::path(data_dir()) / PRESET_USER_DIR / user;
const fs::path bundle_dir = user_dir / PRESET_LOCAL_DIR / bundle_id;
const fs::path metadata_file = bundle_dir / PRESET_BUNDLE_METADATA;
BundleMetadata metadata;
std::string check_error;
if (!check_local_bundle(bundles, user_dir, bundle_id, metadata, check_error))
return fail(check_error);
// Keep background preset sync from changing the collections between copying and committing them.
std::scoped_lock collection_lock(prints.m_mutex, filaments.m_mutex, printers.m_mutex);
const std::string target_prefix = std::string(PRESET_LOCAL_DIR) + "/" + bundle_id + "/";
const auto is_target = [&](const Preset& preset) {
return preset.bundle_id == bundle_id && boost::starts_with(preset.name, target_prefix);
};
struct Selection {
std::string name;
Preset base{Preset::TYPE_INVALID, std::string()};
Preset edited{Preset::TYPE_INVALID, std::string()};
std::vector<std::string> dirty_options;
};
auto snapshot = [&](const PresetCollection& collection, Selection& selection) {
selection.name = collection.get_selected_preset_name();
if (selection.name.empty())
return false;
for (const Preset& preset : collection.get_presets()) {
if (preset.name == selection.name) {
selection.base = preset;
selection.edited = collection.get_edited_preset();
selection.dirty_options = PresetCollection::dirty_options(&selection.edited, &selection.base);
return true;
}
}
return false;
};
Selection print_selection, filament_selection, printer_selection;
if (!snapshot(prints, print_selection) || !snapshot(filaments, filament_selection) || !snapshot(printers, printer_selection))
return fail("Preset selections are not initialized");
PresetBundle staged;
// Collection copies keep vendor pointers into the live vendor map, which is not replaced by this reload.
staged.prints = prints;
staged.filaments = filaments;
staged.printers = printers;
staged.filament_presets = filament_presets;
staged.project_config.apply_only(project_config, {"flush_volumes_matrix", "flush_volumes_vector", "flush_multiplier"});
auto remove_target = [&](PresetCollection& collection) {
auto begin = collection.m_presets.begin() + collection.m_num_default_presets;
collection.m_presets.erase(std::remove_if(begin, collection.m_presets.end(), is_target), collection.m_presets.end());
collection.sort_presets();
};
remove_target(staged.prints);
remove_target(staged.filaments);
remove_target(staged.printers);
const std::array<int, 3> errors_before = { staged.prints.error_count(), staged.filaments.error_count(), staged.printers.error_count() };
PresetsConfigSubstitutions substitutions;
load_bundle_presets(staged, bundle_dir.string(), metadata, PresetOrigin(PresetOrigin::Kind::LocalBundle, bundle_id),
substitutions, ForwardCompatibilitySubstitutionRule::Enable, true);
if (staged.prints.error_count() != errors_before[0] || staged.filaments.error_count() != errors_before[1] || staged.printers.error_count() != errors_before[2])
return fail("Local bundle contains an invalid preset");
auto dirty_target_was_removed = [&](const Selection& selection, const PresetCollection& collection) {
if (selection.dirty_options.empty() || !is_target(selection.base))
return false;
for (const Preset& preset : collection.get_presets())
if (preset.name == selection.name && is_target(preset))
return false;
return true;
};
if (dirty_target_was_removed(print_selection, staged.prints) ||
dirty_target_was_removed(filament_selection, staged.filaments) ||
dirty_target_was_removed(printer_selection, staged.printers))
return fail("A modified selected preset was removed or renamed");
auto external_child_name = [&](const PresetCollection& collection) {
auto depends_on_target = [&](const Preset& preset) {
if (is_target(preset))
return false;
const Preset* parent = &preset;
std::unordered_set<const Preset*> visited;
while ((parent = collection.get_preset_parent(*parent)) != nullptr && visited.insert(parent).second)
if (is_target(*parent))
return true;
for (const char* key : {"compatible_prints", "compatible_printers"}) {
const auto* names = preset.config.option<ConfigOptionStrings>(key);
if (names == nullptr)
continue;
const PresetCollection& referenced = std::string(key) == "compatible_printers" ? printers : prints;
for (const std::string& name : names->values) {
const Preset* dependency = referenced.find_preset(name, false);
if (dependency != nullptr && is_target(*dependency))
return true;
}
}
return false;
};
for (const Preset& preset : collection.get_presets())
if (depends_on_target(preset))
return preset.name;
if (depends_on_target(collection.get_edited_preset()))
return collection.get_edited_preset().name;
return std::string();
};
for (const PresetCollection* collection : std::array<const PresetCollection*, 3>{&prints, &filaments, &printers}) {
const std::string child_name = external_child_name(*collection);
if (!child_name.empty())
return fail("Preset outside this bundle depends on it: " + child_name);
}
auto restore_selection = [](PresetCollection& collection, const Selection& selection) {
if (collection.find_preset(selection.name, false, true) == nullptr) {
collection.select_preset(collection.first_visible_idx());
return false;
}
collection.select_preset_by_name(selection.name, true);
if (collection.get_selected_preset_name() != selection.name)
return false;
Preset& edited = collection.get_edited_preset();
for (const std::string& key : selection.dirty_options) {
if (const ConfigOption* option = selection.edited.config.option(key))
edited.config.set_key_value(key, option->clone());
else
edited.config.erase(key);
}
collection.update_dirty();
return true;
};
staged.prints.update_map_alias_to_profile_name();
staged.filaments.update_map_alias_to_profile_name();
staged.printers.update_map_alias_to_profile_name();
const bool printer_preserved = restore_selection(staged.printers, printer_selection);
const bool print_preserved = restore_selection(staged.prints, print_selection);
const bool filament_preserved = restore_selection(staged.filaments, filament_selection);
const auto selection_mode = [printer_preserved](bool selection_preserved) {
if (!selection_preserved)
return PresetSelectCompatibleType::Always;
return printer_preserved ? PresetSelectCompatibleType::Never : PresetSelectCompatibleType::OnlyIfWasCompatible;
};
if (staged.printers.get_edited_preset().printer_technology() != ptFFF)
return fail("Local bundle reload requires an FFF printer selection");
staged.update_compatible(selection_mode(print_preserved), selection_mode(filament_preserved));
for (std::string& name : staged.filament_presets)
if (boost::starts_with(name, target_prefix) && staged.filaments.find_preset(name, false) == nullptr)
name = staged.filaments.first_compatible().name;
staged.update_multi_material_filament_presets();
struct Collection
{
PresetCollection& live;
PresetCollection& candidate;
size_t original_size;
};
std::array<Collection, 3> collections{{{prints, staged.prints, prints.size()},
{filaments, staged.filaments, filaments.size()},
{printers, staged.printers, printers.size()}}};
for (size_t i = 0; i < collections.size(); ++i) {
const auto& collection = collections[i];
const auto dirty_options = collection.live.current_dirty_options();
if (dirty_options.empty())
continue;
if (collection.live.get_selected_preset_name() != collection.candidate.get_selected_preset_name())
return fail("Reload would discard a modified selected preset");
for (const std::string& key : dirty_options) {
const ConfigOption* before = collection.live.get_edited_preset().config.option(key);
const ConfigOption* after = collection.candidate.get_edited_preset().config.option(key);
if ((before == nullptr) != (after == nullptr) || (before != nullptr && *before != *after))
return fail("Reload would discard a modified preset option: " + key);
}
}
if (calibrate_printer != nullptr && staged.printers.find_preset(calibrate_printer->name, false, true) != nullptr)
staged.set_calibrate_printer(calibrate_printer->name);
std::array<std::pair<ConfigOptionFloats*, ConfigOptionFloats*>, 3> flush_options;
size_t i = 0;
for (const char* key : {"flush_volumes_matrix", "flush_volumes_vector", "flush_multiplier"})
flush_options[i++] = {project_config.option<ConfigOptionFloats>(key), staged.project_config.option<ConfigOptionFloats>(key)};
metadata.bundle_type = BundleType::Local;
metadata.is_subscribed = false;
metadata.update_available = false;
metadata.path = metadata_file.string();
fs::path local_dir = user_dir / PRESET_LOCAL_DIR;
std::unique_lock<std::shared_mutex> lock(bundles.RWMtx);
const auto existing = bundles.m_bundles.find(bundle_id);
if (existing != bundles.m_bundles.end() &&
(existing->second.bundle_type == BundleType::Subscribed || existing->second.is_subscribed))
return fail("A subscribed bundle already uses this id");
std::set<const Preset*> calibration_filaments;
const Preset* calibration_printer = nullptr;
try {
// Growing a deque preserves existing objects. Roll back appended copies if preparation fails.
for (size_t i = 0; i < collections.size(); ++i) {
auto& collection = collections[i];
while (collection.live.size() < collection.candidate.size())
collection.live.m_presets.emplace_back(collection.candidate.m_presets[collection.live.size()]);
}
auto calibration_preset = [](const Preset* preset, PresetCollection& live, PresetCollection& candidate) -> const Preset* {
if (preset == nullptr)
return nullptr;
if (preset == &candidate.get_edited_preset())
return &live.get_edited_preset();
const auto it = candidate.find_preset_internal(preset->name);
return &live.m_presets[it - candidate.m_presets.begin()];
};
calibration_printer = calibration_preset(staged.calibrate_printer, printers, staged.printers);
for (const Preset* preset : staged.calibrate_filaments)
calibration_filaments.insert(calibration_preset(preset, filaments, staged.filaments));
// Insertion is the last allocating operation. Replacing metadata only moves its members.
bundles.m_bundles.insert_or_assign(bundle_id, std::move(metadata));
} catch (...) {
for (size_t i = 0; i < collections.size(); ++i)
while (collections[i].live.size() > collections[i].original_size)
collections[i].live.m_presets.pop_back();
throw;
}
auto install_target = [](PresetCollection& live, PresetCollection& candidate) noexcept {
// Keep stored Preset references alive when the collection's layout is unchanged.
// Adding/removing presets retains the collection's usual sorted-position semantics.
for (size_t i = 0; i < candidate.size(); ++i)
live.m_presets[i] = std::move(candidate.m_presets[i]);
while (live.size() > candidate.size())
live.m_presets.pop_back();
live.m_map_alias_to_profile_name.swap(candidate.m_map_alias_to_profile_name);
// Move assignment does not allocate (including Semver); swapping Presets would construct temporary containers.
live.m_edited_preset = std::move(candidate.m_edited_preset);
live.m_saved_preset = std::move(candidate.m_saved_preset);
live.m_idx_selected = candidate.m_idx_selected;
};
for (size_t i = 0; i < collections.size(); ++i)
install_target(collections[i].live, collections[i].candidate);
filament_presets.swap(staged.filament_presets);
for (const auto& options : flush_options)
options.first->values.swap(options.second->values);
calibrate_printer = calibration_printer;
calibrate_filaments.swap(calibration_filaments);
dir_user_presets_local.swap(local_dir);
return true;
} catch (const std::exception& exception) {
return fail(std::string("Failed to reload local bundle: ") + exception.what());
}
}
PresetsConfigSubstitutions PresetBundle::load_user_presets(AppConfig & config,
std::map<std::string, std::map<std::string, std::string>> &my_presets,
ForwardCompatibilitySubstitutionRule substitution_rule)
@@ -3898,6 +4230,8 @@ void PresetBundle::load_selections(AppConfig &config, const PresetPreferences& p
// BBS: change directories by design
void PresetBundle::export_selections(AppConfig &config)
{
if (m_selection_export_blockers > 0)
return;
assert(this->printers.get_edited_preset().printer_technology() != ptFFF || filament_presets.size() >= 1);
//assert(this->printers.get_edited_preset().printer_technology() != ptFFF || filament_presets.size() > 1 || filaments.get_selected_preset_name() == filament_presets.front());
config.clear_section("presets");
@@ -5524,12 +5858,10 @@ DynamicPrintConfig PresetBundle::full_sla_config() const
ConfigSubstitutions PresetBundle::load_config_file(const std::string &path, ForwardCompatibilitySubstitutionRule compatibility_rule)
{
if (is_gcode_file(path)) {
DynamicPrintConfig config;
//BBS: add config related logs
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, gcodefile %1%, compatibility_rule %2%")%path %compatibility_rule;
config.apply(FullPrintConfig::defaults());
ConfigSubstitutions config_substitutions = config.load_from_gcode_file(path, compatibility_rule);
Preset::normalize(config);
ConfigSubstitutions config_substitutions;
DynamicPrintConfig config = config_from_gcode(path, compatibility_rule, config_substitutions);
load_config_file_config(path, true, std::move(config));
return config_substitutions;
}
@@ -5537,10 +5869,83 @@ ConfigSubstitutions PresetBundle::load_config_file(const std::string &path, Forw
//BBS: add config related logs
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" can not load config file %1% not from gcode")%path ;
throw Slic3r::RuntimeError((boost::format(L("Unknown configuration file: %1%")) % path).str());
return ConfigSubstitutions{};
}
DynamicPrintConfig PresetBundle::config_from_gcode(const std::string &path, ForwardCompatibilitySubstitutionRule compatibility_rule,
ConfigSubstitutions &substitutions) const
{
DynamicPrintConfig config;
config.apply(FullPrintConfig::defaults());
// The config block leaves a few project settings out (filament_colour_type, prime_volume_mode,
// flush_multiplier_fast); those keep this bundle's values rather than falling back to the defaults.
config.apply(this->project_config);
const DynamicPrintConfig base = config;
substitutions = config.load_from_gcode_file(path, compatibility_rule);
// An empty list of choices sets nothing: G-code written by a tool rather than from presets leaves per-extruder
// lists such as nozzle_volume_type empty, and the settings tabs expect a choice per extruder. An empty text
// list, such as filament_multi_colour, is a value of its own and stays.
for (const std::string &key : config.keys())
if (const ConfigOption *opt = config.option(key); opt->type() == coEnums && static_cast<const ConfigOptionVectorBase *>(opt)->size() == 0)
if (const ConfigOption *below = base.option(key); below != nullptr && static_cast<const ConfigOptionVectorBase *>(below)->size() > 0)
config.set_key_value(key, below->clone());
Preset::normalize(config);
// The block holds flush_volumes_matrix already multiplied by the multiplier the slice used, so a
// multiplier of 1 keeps the purge volumes the file was sliced with. G-code from before 2.3.2 held the
// plain matrix and loses a multiplier other than 1.
for (const char *key : { "flush_multiplier", "flush_multiplier_fast" })
if (auto *multiplier = config.option<ConfigOptionFloats>(key))
std::fill(multiplier->values.begin(), multiplier->values.end(), 1.);
return config;
}
std::optional<PresetBundle::Selection> PresetBundle::selection() const
{
auto capture = [](const PresetCollection &presets, Selection::Collection &out) {
if (std::any_of(presets.m_presets.begin(), presets.m_presets.end(), [](const Preset &preset) { return preset.is_project_embedded; }))
return false;
out.selected = presets.get_selected_preset_name();
out.edited = presets.get_edited_preset().config;
out.saved = presets.m_saved_preset.config;
out.visible.reserve(presets.m_presets.size());
for (const Preset &preset : presets.m_presets)
out.visible.push_back(preset.is_visible);
return true;
};
Selection selection;
if (!capture(this->prints, selection.prints) || !capture(this->filaments, selection.filaments) || !capture(this->printers, selection.printers))
return std::nullopt;
selection.filament_presets = this->filament_presets;
selection.project_config = this->project_config;
selection.extruder_ams_counts = this->extruder_ams_counts;
return selection;
}
void PresetBundle::restore_selection(const Selection &selection)
{
auto restore = [](PresetCollection &presets, const Selection::Collection &saved) {
presets.reset_project_embedded_presets();
// Before selecting: select_preset_by_name() passes over a hidden preset.
assert(presets.m_presets.size() == saved.visible.size());
for (size_t i = 0; i < std::min(presets.m_presets.size(), saved.visible.size()); ++i)
presets.m_presets[i].is_visible = saved.visible[i];
presets.select_preset_by_name(saved.selected, true);
presets.m_saved_preset.config = saved.saved;
presets.get_edited_preset().config = saved.edited;
presets.update_dirty();
};
restore(this->printers, selection.printers);
restore(this->prints, selection.prints);
restore(this->filaments, selection.filaments);
this->update_compatible(PresetSelectCompatibleType::Never);
this->update_multi_material_filament_presets();
// Last, as update_multi_material_filament_presets() resizes filament_presets and the flush data in project_config.
this->filament_presets = selection.filament_presets;
this->project_config = selection.project_config;
this->extruder_ams_counts = selection.extruder_ams_counts;
}
//some filament presets split from one to sperate ones
//following map recording these filament presets
+48
View File
@@ -302,6 +302,9 @@ public:
// BBS Load user presets
PresetsConfigSubstitutions load_user_presets(std::string user, ForwardCompatibilitySubstitutionRule rule, bool read_only = false);
PresetsConfigSubstitutions load_user_presets(AppConfig &config, std::map<std::string, std::map<std::string, std::string>>& my_presets, ForwardCompatibilitySubstitutionRule rule);
bool reload_local_bundle(const std::string& preset_folder, const std::string& bundle_id, std::string* error = nullptr);
std::vector<std::string> list_local_bundle_ids(const std::string& preset_folder);
// Orca: Import subscribed bundle presets (load and save to disk in one operation), handles one bundle at a time
PresetsConfigSubstitutions update_subscribed_presets(AppConfig& config,
const std::map<std::string, std::map<std::string, std::string>>& bundle_presets,
@@ -397,6 +400,28 @@ public:
std::vector<Preset*> get_current_project_embedded_presets();
void reset_project_embedded_presets();
// What importing a file's settings replaces, so that an import which fails part-way, or whose
// file is then rejected, can be undone without copying every preset.
struct Selection
{
struct Collection
{
std::string selected;
DynamicPrintConfig edited;
// The baseline saved_is_dirty() compares against, which feeds the project dirty state.
DynamicPrintConfig saved;
// By index: an import only inserts project-embedded presets, which a restore drops first.
std::vector<bool> visible;
};
Collection prints, filaments, printers;
std::vector<std::string> filament_presets;
DynamicPrintConfig project_config;
std::vector<std::map<int, int>> extruder_ams_counts;
};
// Nothing while project-embedded presets exist: a restore drops them, which would lose a loaded project's presets.
std::optional<Selection> selection() const;
void restore_selection(const Selection &selection);
//BBS: find printer model
std::string get_texture_for_printer_model(std::string model_name);
std::string get_stl_model_for_printer_model(std::string model_name);
@@ -405,6 +430,21 @@ public:
// Export selections (current print, current filaments, current printer) into config.ini
void export_selections(AppConfig &config);
// While one lives, export_selections() writes nothing, so a selection only shown for a while (the
// settings of a G-code being previewed) is not remembered as the user's. It also covers the
// exports the GUI makes on its own while refreshing.
class SelectionExportBlocker
{
public:
explicit SelectionExportBlocker(PresetBundle &bundle) : m_bundle(bundle) { ++m_bundle.m_selection_export_blockers; }
~SelectionExportBlocker() { --m_bundle.m_selection_export_blockers; }
SelectionExportBlocker(const SelectionExportBlocker &) = delete;
SelectionExportBlocker &operator=(const SelectionExportBlocker &) = delete;
private:
PresetBundle &m_bundle;
};
// n is the total slot count, and growth appends at the raw tail - which is where the mixed
// slots live. A caller adding physical filaments has to add num_mixed_filaments() on top and
// then move the new slots ahead of the mixed tail, as Sidebar::add_custom_filament does.
@@ -546,6 +586,11 @@ public:
// If the file is loaded successfully, its print / filament / printer profiles will be activated.
ConfigSubstitutions load_config_file(const std::string &path, ForwardCompatibilitySubstitutionRule compatibility_rule);
// The settings an OrcaSlicer G-code was sliced with, from its config block, ready for load_config_model().
// Throws when the file carries no such block (another slicer, or the block was left out).
DynamicPrintConfig config_from_gcode(const std::string &path, ForwardCompatibilitySubstitutionRule compatibility_rule,
ConfigSubstitutions &substitutions) const;
// Load a config bundle file, into presets and store the loaded presets into separate files
// of the local configuration directory.
// Load settings into the provided settings instance.
@@ -896,6 +941,9 @@ private:
std::string vendor_to_validate = "";
int m_errors = 0;
// The live SelectionExportBlockers.
int m_selection_export_blockers = 0;
// Helper function: save preset to bundle directory with common logic
bool save_preset_to_bundle_dir(Preset& preset, PresetCollection* collection,
const std::string& bundle_id, const std::string& type_subdir,
+2
View File
@@ -174,6 +174,8 @@ set(SLIC3R_GUI_SOURCES
GUI/Gizmos/GLGizmoAssembly.hpp
GUI/Gizmos/GLGizmoBase.cpp
GUI/Gizmos/GLGizmoBase.hpp
GUI/Gizmos/GLGizmoAlignment.cpp
GUI/Gizmos/GLGizmoAlignment.hpp
GUI/Gizmos/GLGizmoBrimEars.cpp
GUI/Gizmos/GLGizmoBrimEars.hpp
GUI/Gizmos/GLGizmoCut.cpp
+3 -1
View File
@@ -1797,7 +1797,9 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
}
else {*/
//BBS: add only gcode mode
if (m_only_gcode_in_preview) {
// When the file's own settings were loaded into the presets, the bed already is the file's, with the
// model and texture of a user or custom printer too, which the lookup below cannot resolve.
if (m_only_gcode_in_preview && !wxGetApp().plater()->gcode_settings_loaded()) {
Pointfs printable_area;
//BBS: add bed exclude area
Pointfs bed_exclude_area = Pointfs();
+11 -11
View File
@@ -5520,7 +5520,7 @@ void GLCanvas3D::set_tooltip(const std::string& tooltip)
m_tooltip.set_text(tooltip);
}
void GLCanvas3D::do_move(const std::string& snapshot_type)
void GLCanvas3D::do_move(const std::string& snapshot_type, bool force_volume_move)
{
if (m_model == nullptr)
return;
@@ -5552,10 +5552,18 @@ void GLCanvas3D::do_move(const std::string& snapshot_type)
// Move instances/volumes
ModelObject* model_object = m_model->objects[object_idx];
if (model_object == nullptr)
if (model_object == nullptr)
continue;
if (selection_mode == Selection::Instance) {
if (force_volume_move || selection_mode == Selection::Volume) {
auto cur_mv = model_object->volumes[volume_idx];
if (cur_mv->get_transformation() != v->get_volume_transformation()) {
cur_mv->set_transformation(v->get_volume_transformation());
// BBS: backup
Slic3r::save_object_mesh(*model_object);
}
}
else if (selection_mode == Selection::Instance) {
if (m_canvas_type == GLCanvas3D::ECanvasType::CanvasAssembleView) {
if ((model_object->instances[instance_idx]->get_assemble_offset() - v->get_instance_offset()).norm() > 1e-2) {
model_object->instances[instance_idx]->set_assemble_transformation(v->get_instance_transformation());
@@ -5564,14 +5572,6 @@ void GLCanvas3D::do_move(const std::string& snapshot_type)
model_object->instances[instance_idx]->set_transformation(v->get_instance_transformation());
}
}
else if (selection_mode == Selection::Volume) {
auto cur_mv = model_object->volumes[volume_idx];
if (cur_mv->get_transformation() != v->get_volume_transformation()) {
cur_mv->set_transformation(v->get_volume_transformation());
// BBS: backup
Slic3r::save_object_mesh(*model_object);
}
}
object_moved = true;
model_object->invalidate_bounding_box();
+1 -1
View File
@@ -1305,7 +1305,7 @@ public:
void set_tooltip(const std::string& tooltip);
// the following methods add a snapshot to the undo/redo stack, unless the given string is empty
void do_move(const std::string& snapshot_type);
void do_move(const std::string& snapshot_type, bool force_volume_move = false);
void do_rotate(const std::string& snapshot_type);
void do_scale(const std::string& snapshot_type);
void do_center();
+251 -3
View File
@@ -16,6 +16,7 @@
#include "Selection.hpp"
//BBS: add partplate related logic
#include "PartPlate.hpp"
#include "Gizmos/GLGizmoAlignment.hpp"
#include "Gizmos/GLGizmoEmboss.hpp"
#include "Gizmos/GLGizmoSVG.hpp"
@@ -1672,6 +1673,8 @@ void MenuFactory::create_extra_object_menu()
append_menu_item_merge_parts_to_single_part(&m_object_menu);
// Object Center
append_menu_item_center(&m_object_menu);
// Object Align/Distribute
append_menu_item_align_distribute(&m_object_menu);
// Object Drop
append_menu_item_drop(&m_object_menu);
// Object Split
@@ -1779,6 +1782,7 @@ void MenuFactory::create_text_part_menu()
append_menu_item_fix_through_cgal(menu);
append_menu_item_simplify(menu);
append_menu_item_center(menu);
append_menu_item_align_distribute(menu);
append_menu_items_mirror(menu);
menu->AppendSeparator();
append_menu_item_per_object_process(menu);
@@ -1811,6 +1815,7 @@ void MenuFactory::create_bbl_part_menu()
append_menu_item_simplify(menu);
append_menu_item_smooth_mesh(menu);
append_menu_item_center(menu);
append_menu_item_align_distribute(menu);
append_menu_item_drop(menu);
append_menu_items_mirror(menu);
wxMenu* split_menu = new wxMenu();
@@ -2136,6 +2141,7 @@ wxMenu* MenuFactory::multi_selection_menu()
index++;
}
append_menu_item_center(menu);
append_menu_item_align_distribute(menu);
append_menu_item_drop(menu);
append_menu_item_fix_through_cgal(menu);
//append_menu_item_simplify(menu);
@@ -2144,13 +2150,13 @@ wxMenu* MenuFactory::multi_selection_menu()
append_menu_item_set_printable(menu);
menu->AppendSeparator();
append_menu_item_set_auto_drop(menu);
menu->AppendSeparator();
append_menu_item_per_object_process(menu);
menu->AppendSeparator();
append_menu_items_convert_unit(menu);
append_menu_item_replace_all_with_stl(menu);
//BBS
@@ -2161,6 +2167,7 @@ wxMenu* MenuFactory::multi_selection_menu()
}
else {
append_menu_item_center(menu);
append_menu_item_align_distribute(menu);
append_menu_item_drop(menu);
append_menu_item_fix_through_cgal(menu);
//append_menu_item_simplify(menu);
@@ -2325,6 +2332,247 @@ void MenuFactory::append_menu_item_center(wxMenu* menu)
}, m_parent);
}
void MenuFactory::append_menu_item_align_distribute(wxMenu *menu)
{
wxMenu* align_distribute_menu = new wxMenu();
if (!align_distribute_menu)
return;
append_menu_item(
align_distribute_menu, wxID_ANY, _L("Distribute left-right") + " (X)", "",
[](wxCommandEvent &) {
auto canvas3d = plater()->get_view3D_canvas3D();
canvas3d->get_gizmos_manager().check_object_located_outside_plate(true);
plater()->distribute_selection_x();
},
"", nullptr,
[]() {
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
return false;
auto canvas3d = plater()->get_view3D_canvas3D();
GLGizmoAlignment alignment_helper(*canvas3d);
return alignment_helper.can_distribute(GLGizmoAlignment::AlignType::DISTRIBUTE_X);
},
m_parent);
append_menu_item(
align_distribute_menu, wxID_ANY, _L("Distribute front-back") + " (Y)", "",
[](wxCommandEvent &) {
auto canvas3d = plater()->get_view3D_canvas3D();
canvas3d->get_gizmos_manager().check_object_located_outside_plate(true);
plater()->distribute_selection_y();
},
"", nullptr,
[]() {
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
return false;
auto canvas3d = plater()->get_view3D_canvas3D();
GLGizmoAlignment alignment_helper(*canvas3d);
return alignment_helper.can_distribute(GLGizmoAlignment::AlignType::DISTRIBUTE_Y);
},
m_parent);
append_menu_item(
align_distribute_menu, wxID_ANY, _L("Distribute top-bottom") + " (Z)", "",
[](wxCommandEvent &) {
auto canvas3d = plater()->get_view3D_canvas3D();
canvas3d->get_gizmos_manager().check_object_located_outside_plate(true);
plater()->distribute_selection_z();
},
"", nullptr,
[]() {
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
return false;
auto canvas3d = plater()->get_view3D_canvas3D();
GLGizmoAlignment alignment_helper(*canvas3d);
return alignment_helper.can_distribute(GLGizmoAlignment::AlignType::DISTRIBUTE_Z);
},
m_parent);
align_distribute_menu->AppendSeparator();
append_menu_item(
align_distribute_menu, wxID_ANY, _L("Align left") + " (-X)", "",
[](wxCommandEvent &) {
auto canvas3d = plater()->get_view3D_canvas3D();
canvas3d->get_gizmos_manager().check_object_located_outside_plate(true);
plater()->align_selection_x_min();
},
"", nullptr,
[]() {
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
return false;
auto canvas3d = plater()->get_view3D_canvas3D();
GLGizmoAlignment alignment_helper(*canvas3d);
return alignment_helper.can_align(GLGizmoAlignment::AlignType::X_MIN);
},
m_parent);
append_menu_item(
align_distribute_menu, wxID_ANY, _L("Align left-right center") + " (X)", "",
[](wxCommandEvent &) {
auto canvas3d = plater()->get_view3D_canvas3D();
canvas3d->get_gizmos_manager().check_object_located_outside_plate(true);
plater()->align_selection_x_center();
},
"", nullptr,
[]() {
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
return false;
auto canvas3d = plater()->get_view3D_canvas3D();
GLGizmoAlignment alignment_helper(*canvas3d);
return alignment_helper.can_align(GLGizmoAlignment::AlignType::CENTER_X);
},
m_parent);
append_menu_item(
align_distribute_menu, wxID_ANY, _L("Align right") + " (+X)", "",
[](wxCommandEvent &) {
auto canvas3d = plater()->get_view3D_canvas3D();
canvas3d->get_gizmos_manager().check_object_located_outside_plate(true);
plater()->align_selection_x_max();
},
"", nullptr,
[]() {
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
return false;
auto canvas3d = plater()->get_view3D_canvas3D();
GLGizmoAlignment alignment_helper(*canvas3d);
return alignment_helper.can_align(GLGizmoAlignment::AlignType::X_MAX);
},
m_parent);
align_distribute_menu->AppendSeparator();
append_menu_item(
align_distribute_menu, wxID_ANY, _L("Align front") + " (-Y)", "",
[](wxCommandEvent &) {
auto canvas3d = plater()->get_view3D_canvas3D();
canvas3d->get_gizmos_manager().check_object_located_outside_plate(true);
plater()->align_selection_y_min();
},
"", nullptr,
[]() {
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
return false;
auto canvas3d = plater()->get_view3D_canvas3D();
GLGizmoAlignment alignment_helper(*canvas3d);
return alignment_helper.can_align(GLGizmoAlignment::AlignType::Y_MIN);
},
m_parent);
append_menu_item(
align_distribute_menu, wxID_ANY, _L("Align front-back center") + " (Y)", "",
[](wxCommandEvent &) {
auto canvas3d = plater()->get_view3D_canvas3D();
canvas3d->get_gizmos_manager().check_object_located_outside_plate(true);
plater()->align_selection_y_center();
},
"", nullptr,
[]() {
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
return false;
auto canvas3d = plater()->get_view3D_canvas3D();
GLGizmoAlignment alignment_helper(*canvas3d);
return alignment_helper.can_align(GLGizmoAlignment::AlignType::CENTER_Y);
},
m_parent);
append_menu_item(
align_distribute_menu, wxID_ANY, _L("Align back") + " (+Y)", "",
[](wxCommandEvent &) {
auto canvas3d = plater()->get_view3D_canvas3D();
canvas3d->get_gizmos_manager().check_object_located_outside_plate(true);
plater()->align_selection_y_max();
},
"", nullptr,
[]() {
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
return false;
auto canvas3d = plater()->get_view3D_canvas3D();
GLGizmoAlignment alignment_helper(*canvas3d);
return alignment_helper.can_align(GLGizmoAlignment::AlignType::Y_MAX);
},
m_parent);
align_distribute_menu->AppendSeparator();
append_menu_item(
align_distribute_menu, wxID_ANY, _L("Align bottom") + " (-Z)", "",
[](wxCommandEvent &) {
auto canvas3d = plater()->get_view3D_canvas3D();
canvas3d->get_gizmos_manager().check_object_located_outside_plate(true);
plater()->align_selection_z_min();
},
"", nullptr,
[]() {
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
return false;
auto canvas3d = plater()->get_view3D_canvas3D();
GLGizmoAlignment alignment_helper(*canvas3d);
return alignment_helper.can_align(GLGizmoAlignment::AlignType::Z_MIN);
},
m_parent);
append_menu_item(
align_distribute_menu, wxID_ANY, _L("Align top-bottom center") + " (Z)", "",
[](wxCommandEvent &) {
auto canvas3d = plater()->get_view3D_canvas3D();
canvas3d->get_gizmos_manager().check_object_located_outside_plate(true);
plater()->align_selection_z_center();
},
"", nullptr,
[]() {
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
return false;
auto canvas3d = plater()->get_view3D_canvas3D();
GLGizmoAlignment alignment_helper(*canvas3d);
return alignment_helper.can_align(GLGizmoAlignment::AlignType::CENTER_Z);
},
m_parent);
append_menu_item(
align_distribute_menu, wxID_ANY, _L("Align top") + " (+Z)", "",
[](wxCommandEvent &) {
auto canvas3d = plater()->get_view3D_canvas3D();
canvas3d->get_gizmos_manager().check_object_located_outside_plate(true);
plater()->align_selection_z_max();
},
"", nullptr,
[]() {
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
return false;
auto canvas3d = plater()->get_view3D_canvas3D();
GLGizmoAlignment alignment_helper(*canvas3d);
return alignment_helper.can_align(GLGizmoAlignment::AlignType::Z_MAX);
},
m_parent);
append_submenu(menu, align_distribute_menu, wxID_ANY, _L("Align/Distribute"), _L("Align and distribute objects"), "",
[]() {
if (plater()->canvas3D()->get_canvas_type() != GLCanvas3D::ECanvasType::CanvasView3D)
return false;
auto canvas3d = plater()->get_view3D_canvas3D();
GLGizmoAlignment alignment_helper(*canvas3d);
bool has_any_align = alignment_helper.can_align(GLGizmoAlignment::AlignType::CENTER_X) ||
alignment_helper.can_align(GLGizmoAlignment::AlignType::CENTER_Y) ||
alignment_helper.can_align(GLGizmoAlignment::AlignType::CENTER_Z) ||
alignment_helper.can_align(GLGizmoAlignment::AlignType::X_MAX) ||
alignment_helper.can_align(GLGizmoAlignment::AlignType::X_MIN) ||
alignment_helper.can_align(GLGizmoAlignment::AlignType::Y_MAX) ||
alignment_helper.can_align(GLGizmoAlignment::AlignType::Y_MIN) ||
alignment_helper.can_align(GLGizmoAlignment::AlignType::Z_MAX) ||
alignment_helper.can_align(GLGizmoAlignment::AlignType::Z_MIN);
bool has_any_distribute = alignment_helper.can_distribute(GLGizmoAlignment::AlignType::DISTRIBUTE_X) ||
alignment_helper.can_distribute(GLGizmoAlignment::AlignType::DISTRIBUTE_Y) ||
alignment_helper.can_distribute(GLGizmoAlignment::AlignType::DISTRIBUTE_Z);
return has_any_align || has_any_distribute;
}, m_parent);
}
void MenuFactory::append_menu_item_drop(wxMenu* menu)
{
append_menu_item(menu, wxID_ANY, _L("Drop") , "",
+1
View File
@@ -175,6 +175,7 @@ private:
void append_menu_item_merge_to_single_object(wxMenu* menu);
void append_menu_item_merge_parts_to_single_part(wxMenu *menu);
void append_menu_items_mirror(wxMenu *menu);
void append_menu_item_align_distribute(wxMenu *menu);
void append_menu_item_precise_seam_submenu(wxMenu* menu);
void append_menu_item_invalidate_cut_info(wxMenu *menu);
void append_menu_item_edit_text(wxMenu *menu);
+694
View File
@@ -0,0 +1,694 @@
#include "GLGizmoAlignment.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "libslic3r/Model.hpp"
namespace Slic3r {
namespace GUI {
GLGizmoAlignment::GLGizmoAlignment(GLCanvas3D& canvas) : m_canvas(canvas)
{
}
bool GLGizmoAlignment::align_objects(AlignType type, bool align_parent)
{
if (!validate_selection_for_align()) {
return false;
}
switch (type) {
case AlignType::CENTER_X: return align_to_center(type, align_parent);
case AlignType::CENTER_Y: return align_to_center(type, align_parent);
case AlignType::CENTER_Z: return align_to_center(type, align_parent);
case AlignType::Y_MAX: return align_to_y_max(align_parent);
case AlignType::Y_MIN: return align_to_y_min(align_parent);
case AlignType::X_MAX: return align_to_x_max(align_parent);
case AlignType::X_MIN: return align_to_x_min(align_parent);
case AlignType::Z_MAX: return align_to_z_max(align_parent);
case AlignType::Z_MIN: return align_to_z_min(align_parent);
default:
return false;
}
}
bool GLGizmoAlignment::distribute_objects(AlignType type)
{
if (!can_distribute(type)) {
return false;
}
switch (type) {
case AlignType::DISTRIBUTE_X:
return distribute_x();
case AlignType::DISTRIBUTE_Y:
return distribute_y();
case AlignType::DISTRIBUTE_Z:
return distribute_z();
default:
return false;
}
}
bool GLGizmoAlignment::align_to_y_max(bool align_parent)
{
return align_objects_generic(
[](const ObjectInfo& obj) { return obj.bbox.max.y(); },
[](Vec3d& displacement, double target, double current_max) {
displacement.y() = target - current_max;
}, "Align Y Max", align_parent
);
}
bool GLGizmoAlignment::align_to_y_min(bool align_parent)
{
return align_objects_generic(
[](const ObjectInfo& obj) { return obj.bbox.min.y(); },
[](Vec3d& displacement, double target, double current_min) {
displacement.y() = target - current_min;
}, "Align Y Min", align_parent
);
}
bool GLGizmoAlignment::align_to_x_max(bool align_parent)
{
return align_objects_generic(
[](const ObjectInfo& obj) { return obj.bbox.max.x(); },
[](Vec3d& displacement, double target, double current_max) {
displacement.x() = target - current_max;
}, "Align X Max", align_parent
);
}
bool GLGizmoAlignment::align_to_x_min(bool align_parent)
{
return align_objects_generic(
[](const ObjectInfo& obj) { return obj.bbox.min.x(); },
[](Vec3d& displacement, double target, double current_min) {
displacement.x() = target - current_min;
}, "Align X Min", align_parent
);
}
bool GLGizmoAlignment::align_to_center(AlignType type,bool align_parent)
{
const Selection& selection = get_selection();
std::string type_str = type == AlignType::CENTER_X ? "X" : (type == AlignType::CENTER_Y ? "Y" : "Z");
std::string operation_name = type == AlignType::CENTER_X ? "X Center" : (type == AlignType::CENTER_Y ? "Y Center" : "Z Center");
double selection_center = 0.f;
if (align_parent) {
selection_center = get_current_coord(operation_name, m_parent_box);
// Handle multiple volume selection
if (is_part_align_parent()) {
if (!take_snapshot("Align to parent center " + type_str)) {
return false;
}
get_selection().setup_cache();
auto selection_bbox = selection.get_bounding_box();
double current_coord = get_current_coord(operation_name, selection_bbox);
Vec3d displacement = generate_displacement(operation_name, selection_center - current_coord);
// Move each selected volume to align to the center of their collective bounding box
for (unsigned int idx : selection.get_volume_idxs()) {
const GLVolume *volume = selection.get_volume(idx);
if (volume) {
get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement);
}
}
finish_operation("Align to parent center " + type_str);
return true;
}
BoundingBoxf3 big_bb;
auto objects = get_selected_objects_info(big_bb);
if (objects.empty()) return false;
// Handle multiple objects or parts selection
BoundingBoxf3 selection_bbox = selection.get_bounding_box();
if (!take_snapshot("Align to parent center " + type_str)) {
return false;
}
get_selection().setup_cache();
double current_coord = get_current_coord(operation_name, big_bb);
Vec3d displacement = generate_displacement(operation_name, selection_center - current_coord);
// Multiple objects: use selection bounding box for alignment
for (const auto &obj : objects) {
apply_transformation(obj.object_idx, obj.instance_idx, displacement);
}
finish_operation("Align to parent center " + type_str);
return true;
}
// Handle single object selection// Handle multiple volume selection
if (selection.is_single_full_object() ||
selection.is_multiple_volume() || selection.is_multiple_modifier() || selection.is_single_volume() || selection.is_single_modifier()) {
if (!take_snapshot("Align Parts Center " + type_str)){
return false;
}
get_selection().setup_cache();
BoundingBoxf3 selection_bbox = selection.get_bounding_box();
selection_center = get_current_coord(operation_name, selection_bbox);
// Move each selected volume to align to the center of their collective bounding box
for (unsigned int idx : selection.get_volume_idxs()) {
const GLVolume* volume = selection.get_volume(idx);
if (volume) {
auto temp_box = volume->transformed_convex_hull_bounding_box();
double current = get_current_coord(operation_name, temp_box);
double offset = selection_center - current;
Vec3d displacement = generate_displacement(operation_name, offset);
get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement);
}
}
finish_operation("Align Parts Center" + type_str, true);
return true;
}
BoundingBoxf3 big_bb;
auto objects = get_selected_objects_info(big_bb);
if (objects.empty()) return false;
// Handle multiple objects or parts selection
if (selection.is_multiple_full_object()) {
BoundingBoxf3 selection_bbox = selection.get_bounding_box();
double selection_center = get_current_coord(operation_name, selection_bbox);
if (!take_snapshot("Align Objects Center" + type_str)) {
return false;
}
get_selection().setup_cache();
// Multiple objects: use selection bounding box for alignment
for (const auto& obj : objects) {
BoundingBoxf3 temp_bbox = obj.bbox;
double current_coord = get_current_coord(operation_name, temp_bbox);
Vec3d displacement = generate_displacement(operation_name, selection_center - current_coord);
apply_transformation(obj.object_idx, obj.instance_idx, displacement);
}
finish_operation("Align Objects Center" + type_str);
return true;
}
return false;
}
bool GLGizmoAlignment::align_to_z_max(bool align_parent)
{
return align_objects_generic(
[](const ObjectInfo& obj) { return obj.bbox.max.z(); },
[](Vec3d& displacement, double target, double current_max) {
displacement.z() = target - current_max;
}, "Align Z Max", align_parent
);
}
bool GLGizmoAlignment::align_to_z_min(bool align_parent)
{
return align_objects_generic(
[](const ObjectInfo& obj) { return obj.bbox.min.z(); },
[](Vec3d& displacement, double target, double current_min) {
displacement.z() = target - current_min;
}, "Align Z Min", align_parent
);
}
bool GLGizmoAlignment::distribute_y()
{
return distribute_objects_generic(
[](const ObjectInfo& obj) { return obj.center.y(); },
1,
"Distribute Y"
);
}
bool GLGizmoAlignment::distribute_x()
{
return distribute_objects_generic(
[](const ObjectInfo& obj) { return obj.center.x(); },
0,
"Distribute X"
);
}
bool GLGizmoAlignment::distribute_z()
{
return distribute_objects_generic(
[](const ObjectInfo& obj) { return obj.center.z(); },
2,
"Distribute Z"
);
}
template<typename GetCoordFunc, typename SetCoordFunc>
bool GLGizmoAlignment::align_objects_generic(GetCoordFunc get_coord, SetCoordFunc set_coord, const std::string &operation_name ,bool align_parent)
{
const Selection& selection = get_selection();
if (align_parent) {
// Find reference coordinate
double reference_coord = get_current_coord(operation_name, m_parent_box);
// Handle parts selection differently
if (is_part_align_parent()) {
if (!take_snapshot("align to parent node " + operation_name)) {
return false;
}
get_selection().setup_cache();
// Get all selected volumes
std::vector<const GLVolume *> volumes;
for (unsigned int idx : selection.get_volume_idxs()) {
const GLVolume *volume = selection.get_volume(idx);
if (volume) { volumes.push_back(volume); }
}
if (volumes.empty())
return false;
BoundingBoxf3 selection_bbox = selection.get_bounding_box();
double current_coord = get_current_coord(operation_name, selection_bbox);
Vec3d displacement = generate_displacement(operation_name, reference_coord - current_coord);
for (const GLVolume *volume : volumes) {
if (displacement.norm() > 1e-6) {
get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement);
}
}
finish_operation("align to parent node " + operation_name);
return true;
}
// Handle objects selection (original logic)
BoundingBoxf3 big_bb;
auto objects = get_selected_objects_info(big_bb);
if (objects.empty()) return false;
if (!take_snapshot("align to parent node " + operation_name)) { return false; }
get_selection().setup_cache();
double current_coord = get_current_coord(operation_name, big_bb);
Vec3d displacement = generate_displacement(operation_name, reference_coord - current_coord);
for (const auto &obj : objects) {
if (displacement.norm() > 1e-6) {
apply_transformation(obj.object_idx, obj.instance_idx, displacement);
}
}
finish_operation("align to parent node " + operation_name);
return true;
}
// Handle parts selection differently
if (selection.is_single_full_object() || selection.is_multiple_volume() || selection.is_multiple_modifier()) {
if (!take_snapshot(operation_name)) {
return false;
}
get_selection().setup_cache();
// Get all selected volumes
std::vector<const GLVolume*> volumes;
for (unsigned int idx : selection.get_volume_idxs()) {
const GLVolume* volume = selection.get_volume(idx);
if (volume) {
volumes.push_back(volume);
}
}
if (volumes.empty()) return false;
// Find reference coordinate
double reference_coord = 0.0;
bool first = true;
for (const GLVolume* volume : volumes) {
BoundingBoxf3 bbox = volume->transformed_convex_hull_bounding_box();
double coord = 0.0;
if (operation_name.find("X Max") != std::string::npos) {
coord = bbox.max.x();
} else if (operation_name.find("X Min") != std::string::npos) {
coord = bbox.min.x();
} else if (operation_name.find("Y Max") != std::string::npos) {
coord = bbox.max.y();
} else if (operation_name.find("Y Min") != std::string::npos) {
coord = bbox.min.y();
} else if (operation_name.find("Z Max") != std::string::npos) {
coord = bbox.max.z();
} else if (operation_name.find("Z Min") != std::string::npos) {
coord = bbox.min.z();
}
if (first) {
reference_coord = coord;
first = false;
} else {
if (operation_name.find("Max") != std::string::npos) {
reference_coord = std::max(reference_coord, coord);
} else if (operation_name.find("Min") != std::string::npos) {
reference_coord = std::min(reference_coord, coord);
}
}
}
for (const GLVolume* volume : volumes) {
BoundingBoxf3 bbox = volume->transformed_convex_hull_bounding_box();
double current_coord = 0.0;
if (operation_name.find("X Max") != std::string::npos) {
current_coord = bbox.max.x();
} else if (operation_name.find("X Min") != std::string::npos) {
current_coord = bbox.min.x();
} else if (operation_name.find("Y Max") != std::string::npos) {
current_coord = bbox.max.y();
} else if (operation_name.find("Y Min") != std::string::npos) {
current_coord = bbox.min.y();
} else if (operation_name.find("Z Max") != std::string::npos) {
current_coord = bbox.max.z();
} else if (operation_name.find("Z Min") != std::string::npos) {
current_coord = bbox.min.z();
}
Vec3d displacement = Vec3d::Zero();
if (operation_name.find("X") != std::string::npos) {
displacement.x() = reference_coord - current_coord;
} else if (operation_name.find("Y") != std::string::npos) {
displacement.y() = reference_coord - current_coord;
} else if (operation_name.find("Z") != std::string::npos) {
displacement.z() = reference_coord - current_coord;
}
if (displacement.norm() > 1e-6) {
get_selection().translate(volume->object_idx(), volume->instance_idx(), volume->volume_idx(), displacement);
}
}
finish_operation(operation_name,true);
return true;
}
// Handle objects selection (original logic)
BoundingBoxf3 big_bb;
auto objects = get_selected_objects_info(big_bb);
if (objects.empty()) return false;
if (!take_snapshot(operation_name)) {
return false;
}
get_selection().setup_cache();
double reference_coord = get_coord(objects[0]);
for (const auto& obj : objects) {
double coord = get_coord(obj);
if (operation_name.find("Max") != std::string::npos) {
reference_coord = std::max(reference_coord, coord);
} else if (operation_name.find("Min") != std::string::npos) {
reference_coord = std::min(reference_coord, coord);
}
}
for (const auto& obj : objects) {
double current_coord = get_coord(obj);
Vec3d displacement = Vec3d::Zero();
set_coord(displacement, reference_coord, current_coord);
if (displacement.norm() > 1e-6) {
apply_transformation(obj.object_idx, obj.instance_idx, displacement);
}
}
finish_operation(operation_name);
return true;
}
template<typename GetCoordFunc>
bool GLGizmoAlignment::distribute_objects_generic(GetCoordFunc get_coord, int axis, const std::string& operation_name)
{
const Selection& selection = get_selection();
if (selection.is_single_full_object() || selection.is_multiple_volume() || selection.is_multiple_modifier()) {
std::vector<const GLVolume*> volumes;
for (unsigned int idx : selection.get_volume_idxs()) {
const GLVolume* v = selection.get_volume(idx);
if (v) volumes.push_back(v);
}
if (volumes.size() < 3) return false;
if (!take_snapshot(operation_name)) {
return false;
}
get_selection().setup_cache();
struct VolEntry { const GLVolume* v; double coord; };
std::vector<VolEntry> entries;
entries.reserve(volumes.size());
for (const GLVolume* v : volumes) {
BoundingBoxf3 bbox = v->transformed_convex_hull_bounding_box();
Vec3d c = bbox.center();
double coord = (axis == 0 ? c.x() : axis == 1 ? c.y() : c.z());
entries.push_back({v, coord});
}
std::sort(entries.begin(), entries.end(), [](const VolEntry& a, const VolEntry& b){ return a.coord < b.coord; });
BoundingBoxf3 selection_bbox = selection.get_bounding_box();
BoundingBoxf3 left_bbox = entries.front().v->transformed_convex_hull_bounding_box();
BoundingBoxf3 right_bbox = entries.back().v->transformed_convex_hull_bounding_box();
auto axis_extent = [&](const BoundingBoxf3 &bb) { return (axis == 0 ? (bb.max.x() - bb.min.x()) : axis == 1 ? (bb.max.y() - bb.min.y()) : (bb.max.z() - bb.min.z())); };
double left_width = axis_extent(left_bbox);
double right_width = axis_extent(right_bbox);
double bbox_min = (axis == 0 ? selection_bbox.min.x() : axis == 1 ? selection_bbox.min.y() : selection_bbox.min.z());
double bbox_max = (axis == 0 ? selection_bbox.max.x() : axis == 1 ? selection_bbox.max.y() : selection_bbox.max.z());
double min_center = bbox_min + left_width * 0.5;
double max_center = bbox_max - right_width * 0.5;
double total_distance = max_center - min_center;
double interval = (entries.size() > 1) ? total_distance / (entries.size() - 1) : 0;
for (size_t i = 1; i < entries.size() - 1; ++i) {
double target_coord = min_center + i * interval;
double current_coord = entries[i].coord;
double offset = target_coord - current_coord;
if (std::abs(offset) > 1e-6) {
Vec3d displacement = Vec3d::Zero();
displacement[axis] = offset;
const GLVolume *v = entries[i].v;
get_selection().translate(v->object_idx(), v->instance_idx(), v->volume_idx(), displacement);
}
}
finish_operation(operation_name,true);
return true;
}
BoundingBoxf3 big_bb;
auto objects = get_selected_objects_info(big_bb);
if (objects.size() < 3) return false;
if (!take_snapshot(operation_name)) {
return false;
}
get_selection().setup_cache();
std::sort(objects.begin(), objects.end(),
[&get_coord](const ObjectInfo& a, const ObjectInfo& b) {
return get_coord(a) < get_coord(b);
});
double min_coord = get_coord(objects.front());
double max_coord = get_coord(objects.back());
double total_distance = max_coord - min_coord;
double interval = (objects.size() > 1) ? total_distance / (objects.size() - 1) : 0;
for (size_t i = 1; i < objects.size() - 1; ++i) {
double target_coord = min_coord + i * interval;
double current_coord = get_coord(objects[i]);
double offset = target_coord - current_coord;
if (std::abs(offset) > 1e-6) {
Vec3d displacement = Vec3d::Zero();
displacement[axis] = offset;
apply_transformation(objects[i].object_idx, objects[i].instance_idx, displacement);
}
}
finish_operation(operation_name);
return true;
}
bool GLGizmoAlignment::can_align(AlignType type) const
{
const Selection& selection = get_selection();
bool is_single_object = selection.is_single_full_object();
bool is_multiple_objects = selection.is_multiple_full_object();
bool is_single_part = selection.is_single_volume() || selection.is_single_modifier();
bool is_multiple_parts = selection.is_multiple_volume() || selection.is_multiple_modifier();
return validate_selection_for_align();
}
bool GLGizmoAlignment::can_distribute(AlignType type) const
{
const Selection& selection = get_selection();
if (selection.is_single_full_object()) {
size_t count = selection.get_volume_idxs().size();
if (count >= 3)
return true;
}
if (selection.is_single_volume() ||
selection.is_single_modifier()) {
return false;
}
if (selection.is_multiple_volume() || selection.is_multiple_modifier()) {
size_t count = selection.get_volume_idxs().size();
if (count < 3) return false;
return (type == AlignType::DISTRIBUTE_X ||
type == AlignType::DISTRIBUTE_Y ||
type == AlignType::DISTRIBUTE_Z);
}
if (selection.is_multiple_full_object()) {
BoundingBoxf3 big_bb;
auto objects = get_selected_objects_info(big_bb);
if (objects.size() < 3) return false;
return (type == AlignType::DISTRIBUTE_X || type == AlignType::DISTRIBUTE_Y || type == AlignType::DISTRIBUTE_Z);
}
return validate_selection_for_distribute();
}
double GLGizmoAlignment::get_current_coord(std::string operation_name, BoundingBoxf3 &bbox)
{
double current_coord = 0.0;
if (operation_name.find("X Max") != std::string::npos) {
current_coord = bbox.max.x();
} else if (operation_name.find("X Min") != std::string::npos) {
current_coord = bbox.min.x();
} else if (operation_name.find("Y Max") != std::string::npos) {
current_coord = bbox.max.y();
} else if (operation_name.find("Y Min") != std::string::npos) {
current_coord = bbox.min.y();
} else if (operation_name.find("Z Max") != std::string::npos) {
current_coord = bbox.max.z();
} else if (operation_name.find("Z Min") != std::string::npos) {
current_coord = bbox.min.z();
} else if (operation_name.find("X Center") != std::string::npos) {
current_coord = bbox.center().x();
} else if (operation_name.find("Y Center") != std::string::npos) {
current_coord = bbox.center().y();
} else if (operation_name.find("Z Center") != std::string::npos) {
current_coord = bbox.center().z();
}
return current_coord;
}
Vec3d GLGizmoAlignment::generate_displacement(std::string operation_name, double offset) {
Vec3d displacement = Vec3d::Zero();
if (operation_name.find("X") != std::string::npos) {
displacement.x() = offset;
} else if (operation_name.find("Y") != std::string::npos) {
displacement.y() = offset;
} else if (operation_name.find("Z") != std::string::npos) {
displacement.z() = offset;
}
return displacement;
}
bool GLGizmoAlignment::is_part_align_parent()
{
Selection &selection = get_selection();
if (selection.is_multiple_volume() || selection.is_multiple_modifier() || selection.is_single_volume() || selection.is_single_modifier()) {
return true;
}
return false;
}
std::vector<GLGizmoAlignment::ObjectInfo> GLGizmoAlignment::get_selected_objects_info(BoundingBoxf3 &big_bb) const
{
std::vector<ObjectInfo> objects;
Selection& selection = get_selection();
if (selection.is_empty()) return objects;
const auto& content = selection.get_content();
for (const auto& obj_it : content) {
for (const auto& inst_idx : obj_it.second) {
int obj_idx = obj_it.first;
if (obj_idx >= 0 && obj_idx < selection.get_model()->objects.size()) {
ModelObject* object = selection.get_model()->objects[obj_idx];
if (inst_idx >= 0 && inst_idx < object->instances.size()) {
BoundingBoxf3 bbox = object->instance_bounding_box(inst_idx);
big_bb.merge(bbox);
objects.emplace_back(obj_idx, inst_idx, bbox);
}
}
}
}
return objects;
}
void GLGizmoAlignment::set_parent_box(const BoundingBoxf3 &bb) {
m_parent_box = bb;
}
Selection& GLGizmoAlignment::get_selection() const
{
return m_canvas.get_selection();
}
bool GLGizmoAlignment::take_snapshot(const std::string& name)
{
wxGetApp().plater()->take_snapshot(name);
return true;
}
void GLGizmoAlignment::apply_transformation(int obj_idx, int inst_idx, const Vec3d& displacement)
{
get_selection().translate(obj_idx, inst_idx, displacement);
}
void GLGizmoAlignment::finish_operation(const std::string &operation_name, bool force_volume_move)
{
get_selection().notify_instance_update(-1, -1);
m_canvas.do_move(operation_name, force_volume_move);
}
bool GLGizmoAlignment::validate_selection_for_align() const
{
const Selection& selection = get_selection();
if (selection.is_single_full_object() ||
selection.is_single_volume() ||
selection.is_single_modifier()) {
return true;
}
return selection.get_volume_idxs().size() >= 2;
}
bool GLGizmoAlignment::validate_selection_for_distribute() const
{
const Selection& selection = get_selection();
if (selection.is_single_full_object() || selection.is_multiple_full_object()) {
std::set<int> object_indices;
for (int volume_idx : selection.get_volume_idxs()) {
const GLVolume* volume = selection.get_volume(volume_idx);
if (volume) {
object_indices.insert(volume->object_idx());
}
}
return object_indices.size() >= 3;
}
return selection.get_volume_idxs().size() >= 3;
}
} // namespace GUI
} // namespace Slic3r
@@ -0,0 +1,95 @@
#ifndef slic3r_GLGizmoAlignment_hpp_
#define slic3r_GLGizmoAlignment_hpp_
#include "libslic3r/Point.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "slic3r/GUI/Selection.hpp"
#include <vector>
#include <functional>
namespace Slic3r {
namespace GUI {
class GLCanvas3D;
class GLGizmoAlignment
{
public:
enum class AlignType {
NONE = -1,
CENTER_X,
CENTER_Y,
CENTER_Z,
Y_MAX,
Y_MIN,
X_MAX,
X_MIN,
Z_MAX,
Z_MIN,
DISTRIBUTE_X,
DISTRIBUTE_Y,
DISTRIBUTE_Z
};
struct ObjectInfo {
int object_idx;
int instance_idx;
BoundingBoxf3 bbox;
Vec3d center;
ObjectInfo(int obj_idx, int inst_idx, const BoundingBoxf3& bb)
: object_idx(obj_idx), instance_idx(inst_idx), bbox(bb), center(bb.center()) {}
};
explicit GLGizmoAlignment(GLCanvas3D& canvas);
~GLGizmoAlignment() = default;
bool align_objects(AlignType type,bool align_parent = false);
bool distribute_objects(AlignType type);
bool align_to_center(AlignType type,bool align_parent = false);
bool align_to_y_max(bool align_parent = false);
bool align_to_y_min(bool align_parent = false);
bool align_to_x_max(bool align_parent = false);
bool align_to_x_min(bool align_parent = false);
bool align_to_z_max(bool align_parent = false);
bool align_to_z_min(bool align_parent = false);
bool distribute_x();
bool distribute_y();
bool distribute_z();
bool can_align(AlignType type) const;
bool can_distribute(AlignType type) const;
std::vector<ObjectInfo> get_selected_objects_info(BoundingBoxf3 &big_bb) const;
double get_current_coord(std::string operation_name, BoundingBoxf3 &bbox);
Vec3d generate_displacement(std::string operation_name, double offset);
bool is_part_align_parent();
void set_parent_box(const BoundingBoxf3 &bb);
private:
GLCanvas3D& m_canvas;
template<typename GetCoordFunc, typename SetCoordFunc>
bool align_objects_generic(GetCoordFunc get_coord, SetCoordFunc set_coord, const std::string &operation_name, bool align_parent = false);
template<typename GetCoordFunc>
bool distribute_objects_generic(GetCoordFunc get_coord, int axis,
const std::string& operation_name);
Selection& get_selection() const;
bool take_snapshot(const std::string& name);
void apply_transformation(int obj_idx, int inst_idx, const Vec3d& displacement);
void finish_operation(const std::string &operation_name, bool force_volume_move = false);
bool validate_selection_for_align() const;
bool validate_selection_for_distribute() const;
private:
BoundingBoxf3 m_parent_box;
};
} // namespace GUI
} // namespace Slic3r
#endif // slic3r_GLGizmoAlignment_hpp_
+31
View File
@@ -470,6 +470,37 @@ bool GLGizmosManager::check_gizmos_closed_except(EType type) const
return true;
}
// Switches the active plate to the one containing the first selected object,
// so that subsequent operations (align/distribute) act on a plate the user
// can actually see.
void GLGizmosManager::check_object_located_outside_plate(bool change_plate)
{
PartPlateList& plate_list = wxGetApp().plater()->get_partplate_list();
auto curr_plate_index = plate_list.get_curr_plate_index();
Selection& selection = m_parent.get_selection();
auto idxs = selection.get_volume_idxs();
if (idxs.empty())
return;
const GLVolume* v = selection.get_volume(*idxs.begin());
int object_idx = v->object_idx();
const Model* model = m_parent.get_model();
if (object_idx < 0 || object_idx >= (int) model->objects.size())
return;
ModelObject* model_object = model->objects[object_idx];
for (size_t i = 0; i < plate_list.get_plate_count(); ++i) {
auto plate = plate_list.get_plate(i);
for (auto object : plate->get_objects_on_this_plate()) {
if (model_object == object) {
if (change_plate && curr_plate_index != (int) i)
plate_list.select_plate(i);
return;
}
}
}
}
void GLGizmosManager::set_hover_id(int id)
{
// Measure and assembly handles hover by itself
@@ -261,6 +261,7 @@ public:
void reset_all_states();
bool open_gizmo(EType type);
bool check_gizmos_closed_except(EType) const;
void check_object_located_outside_plate(bool change_plate = true);
void set_hover_id(int id);
+236 -57
View File
@@ -245,6 +245,7 @@
#include "Widgets/MultiNozzleSync.hpp" // NozzleOption, tryPopUpMultiNozzleDialog, setExtruderNozzleCount
#include "DeviceCore/DevNozzleSystem.h" // DevNozzle, GetExtNozzles / GetRackNozzles
#include "ProjectDirtyStateManager.hpp"
#include "Gizmos/GLGizmoAlignment.hpp"
#include "Gizmos/GLGizmoSimplify.hpp" // create suggestion notification
#include "Gizmos/GLGizmoSVG.hpp" // Drop SVG file
#include "Gizmos/GizmoObjectManipulation.hpp"
@@ -6013,7 +6014,8 @@ void Sidebar::add_custom_filament(wxColour new_col, const std::string& preset_na
bool Sidebar::is_new_project_in_gcode3mf()
{
if (p->plater->using_exported_file()) {
// A .gcode previewed with its own settings shows them here too, and is closed the same way.
if (p->plater->using_exported_file() || p->plater->gcode_settings_loaded()) {
auto filename = p->plater->get_preview_only_filename();
auto text = wxString::Format(_L("After completing your operation, %s project will be closed and create a new project."), filename);
MessageDialog dlg(wxGetApp().plater(), text, _L("Warning"),
@@ -7367,6 +7369,9 @@ struct Plater::priv
void enable_sidebar(bool enabled);
void collapse_sidebar(bool collapse);
void update_sidebar(bool force_update = false);
// The sidebar shows the selected presets, which a G-code previewed without its own settings has nothing to do
// with, so it stays hidden then. With the file's settings loaded it shows them, as for a .gcode.3mf.
bool gcode_preview_without_settings() const { return q->m_only_gcode && !q->m_gcode_settings_loaded; }
void reset_window_layout();
Sidebar::DockingState get_sidebar_docking_state();
void add_dock_pane(wxWindow* window, const std::string& name, const wxString& caption, const std::string& dock,
@@ -7480,6 +7485,18 @@ struct Plater::priv
void delete_all_objects_from_model();
void reset(bool apply_presets_change = false, bool reload_presets = true);
void center_selection();
void distribute_selection_x();
void distribute_selection_y();
void distribute_selection_z();
void align_selection_x_min();
void align_selection_x_center();
void align_selection_x_max();
void align_selection_y_min();
void align_selection_y_center();
void align_selection_y_max();
void align_selection_z_min();
void align_selection_z_center();
void align_selection_z_max();
void drop_selection();
void mirror(Axis axis);
void split_object(bool auto_drop = true);
@@ -8592,7 +8609,7 @@ void Plater::priv::select_next_view_3D()
void Plater::priv::enable_sidebar(bool enabled)
{
if (q->m_only_gcode)
if (gcode_preview_without_settings())
enabled = false;
sidebar_layout.is_enabled = enabled;
@@ -8601,7 +8618,7 @@ void Plater::priv::enable_sidebar(bool enabled)
void Plater::priv::collapse_sidebar(bool collapse)
{
if (q->m_only_gcode)
if (gcode_preview_without_settings())
return;
sidebar_layout.is_collapsed = collapse;
@@ -8864,6 +8881,64 @@ static void log_substitutions(const PresetsConfigSubstitutions& substitutions, c
log_substitutions(preset.substitutions, source + " (preset " + preset.preset_name + ")");
}
// After the settings of a project or a G-code were loaded into the presets: size the per-filament project
// lists to the loaded filament count, and drop state only a connected printer may set.
static void sync_loaded_project_filament_config(DynamicConfig& proj_cfg)
{
ConfigOptionStrings* filament_color = proj_cfg.opt<ConfigOptionStrings>("filament_colour");
if (filament_color) {
size_t filament_count = filament_color->size();
// Sync filament map
ConfigOptionInts* filament_map = proj_cfg.opt<ConfigOptionInts>("filament_map", true);
if (filament_map->size() != filament_count) {
filament_map->values.resize(filament_count, 1);
}
ConfigOptionInts* filament_nozzle_map = proj_cfg.opt<ConfigOptionInts>("filament_nozzle_map", true);
if (filament_nozzle_map->size() != filament_count) {
filament_nozzle_map->values.resize(filament_count, 0);
}
ConfigOptionInts* filament_volume_map = proj_cfg.opt<ConfigOptionInts>("filament_volume_map", true);
if (filament_volume_map->size() != filament_count) {
filament_volume_map->values.resize(filament_count, static_cast<int>(NozzleVolumeType::nvtStandard));
}
// Sync filament multi colour
ConfigOptionStrings* filament_multi_color = proj_cfg.opt<ConfigOptionStrings>("filament_multi_colour", true);
if (filament_multi_color->size() != filament_count) {
filament_multi_color->values.resize(filament_count);
}
// If there is no multi-color data or color is not match, use single color as default value
for (size_t i = 0; i < filament_count; i++) {
std::vector<std::string> colors = Slic3r::split_string(filament_multi_color->values[i], ' ');
if (i >= filament_multi_color->values.size() || colors.empty() || colors[0] != filament_color->values[i] ) {
filament_multi_color->values[i] = filament_color->values[i];
}
}
// Sync filament colour type
ConfigOptionStrings* filament_color_type = proj_cfg.opt<ConfigOptionStrings>("filament_colour_type", true);
if (filament_color_type && filament_color_type->size() != filament_count) {
filament_color_type->values.resize(filament_count);
for (size_t i = 0; i < filament_count; i++) {
if (i >= filament_color_type->values.size() || filament_color_type->values[i].empty()) {
filament_color_type->values[i] = "1";
}
}
}
}
// Filament Track Switch state is derived live from the connected
// printer; a loaded project must not carry a stale installed/active
// flag, so clear both and let device sync re-derive them.
if (auto* has_switcher = proj_cfg.opt<ConfigOptionBool>("has_filament_switcher"))
has_switcher->value = false;
if (auto* dynamic_map = proj_cfg.opt<ConfigOptionBool>("enable_filament_dynamic_map"))
dynamic_map->value = false;
}
// BBS: backup & restore
std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_files,
LoadStrategy strategy,
@@ -9680,58 +9755,7 @@ std::vector<size_t> Plater::priv::load_files(const std::vector<fs::path>& input_
if (file_wipe_tower_y)
*wipe_tower_y = *file_wipe_tower_y;
ConfigOptionStrings* filament_color = proj_cfg.opt<ConfigOptionStrings>("filament_colour");
if (filament_color) {
size_t filament_count = filament_color->size();
// Sync filament map
ConfigOptionInts* filament_map = proj_cfg.opt<ConfigOptionInts>("filament_map", true);
if (filament_map->size() != filament_count) {
filament_map->values.resize(filament_count, 1);
}
ConfigOptionInts* filament_nozzle_map = proj_cfg.opt<ConfigOptionInts>("filament_nozzle_map", true);
if (filament_nozzle_map->size() != filament_count) {
filament_nozzle_map->values.resize(filament_count, 0);
}
ConfigOptionInts* filament_volume_map = proj_cfg.opt<ConfigOptionInts>("filament_volume_map", true);
if (filament_volume_map->size() != filament_count) {
filament_volume_map->values.resize(filament_count, static_cast<int>(NozzleVolumeType::nvtStandard));
}
// Sync filament multi colour
ConfigOptionStrings* filament_multi_color = proj_cfg.opt<ConfigOptionStrings>("filament_multi_colour", true);
if (filament_multi_color->size() != filament_count) {
filament_multi_color->values.resize(filament_count);
}
// If there is no multi-color data or color is not match, use single color as default value
for (size_t i = 0; i < filament_count; i++) {
std::vector<std::string> colors = Slic3r::split_string(filament_multi_color->values[i], ' ');
if (i >= filament_multi_color->values.size() || colors.empty() || colors[0] != filament_color->values[i] ) {
filament_multi_color->values[i] = filament_color->values[i];
}
}
// Sync filament colour type
ConfigOptionStrings* filament_color_type = proj_cfg.opt<ConfigOptionStrings>("filament_colour_type", true);
if (filament_color_type && filament_color_type->size() != filament_count) {
filament_color_type->values.resize(filament_count);
for (size_t i = 0; i < filament_count; i++) {
if (i >= filament_color_type->values.size() || filament_color_type->values[i].empty()) {
filament_color_type->values[i] = "1";
}
}
}
}
// Filament Track Switch state is derived live from the connected
// printer; a loaded project must not carry a stale installed/active
// flag, so clear both and let device sync re-derive them.
if (auto* has_switcher = proj_cfg.opt<ConfigOptionBool>("has_filament_switcher"))
has_switcher->value = false;
if (auto* dynamic_map = proj_cfg.opt<ConfigOptionBool>("enable_filament_dynamic_map"))
dynamic_map->value = false;
sync_loaded_project_filament_config(proj_cfg);
}
// Update filament combobox after loading config
if (published_config.published) {
@@ -10902,6 +10926,78 @@ void Plater::priv::center_selection()
view3D->center_selected();
}
void Plater::priv::distribute_selection_x()
{
GLGizmoAlignment alignment_helper(*view3D->get_canvas3d());
alignment_helper.distribute_objects(GLGizmoAlignment::AlignType::DISTRIBUTE_X);
}
void Plater::priv::distribute_selection_y()
{
GLGizmoAlignment alignment_helper(*view3D->get_canvas3d());
alignment_helper.distribute_objects(GLGizmoAlignment::AlignType::DISTRIBUTE_Y);
}
void Plater::priv::distribute_selection_z()
{
GLGizmoAlignment alignment_helper(*view3D->get_canvas3d());
alignment_helper.distribute_objects(GLGizmoAlignment::AlignType::DISTRIBUTE_Z);
}
void Plater::priv::align_selection_x_min()
{
GLGizmoAlignment alignment_helper(*view3D->get_canvas3d());
alignment_helper.align_objects(GLGizmoAlignment::AlignType::X_MIN);
}
void Plater::priv::align_selection_x_center()
{
GLGizmoAlignment alignment_helper(*view3D->get_canvas3d());
alignment_helper.align_objects(GLGizmoAlignment::AlignType::CENTER_X);
}
void Plater::priv::align_selection_x_max()
{
GLGizmoAlignment alignment_helper(*view3D->get_canvas3d());
alignment_helper.align_objects(GLGizmoAlignment::AlignType::X_MAX);
}
void Plater::priv::align_selection_y_min()
{
GLGizmoAlignment alignment_helper(*view3D->get_canvas3d());
alignment_helper.align_objects(GLGizmoAlignment::AlignType::Y_MIN);
}
void Plater::priv::align_selection_y_center()
{
GLGizmoAlignment alignment_helper(*view3D->get_canvas3d());
alignment_helper.align_objects(GLGizmoAlignment::AlignType::CENTER_Y);
}
void Plater::priv::align_selection_y_max()
{
GLGizmoAlignment alignment_helper(*view3D->get_canvas3d());
alignment_helper.align_objects(GLGizmoAlignment::AlignType::Y_MAX);
}
void Plater::priv::align_selection_z_min()
{
GLGizmoAlignment alignment_helper(*view3D->get_canvas3d());
alignment_helper.align_objects(GLGizmoAlignment::AlignType::Z_MIN);
}
void Plater::priv::align_selection_z_center()
{
GLGizmoAlignment alignment_helper(*view3D->get_canvas3d());
alignment_helper.align_objects(GLGizmoAlignment::AlignType::CENTER_Z);
}
void Plater::priv::align_selection_z_max()
{
GLGizmoAlignment alignment_helper(*view3D->get_canvas3d());
alignment_helper.align_objects(GLGizmoAlignment::AlignType::Z_MAX);
}
void Plater::priv::drop_selection()
{
view3D->drop_selected();
@@ -12560,7 +12656,7 @@ void Plater::priv::set_current_panel(wxPanel* panel, bool no_slice)
// Add sidebar and toolbar collapse logic
if (panel == view3D || panel == preview) {
this->enable_sidebar(!q->only_gcode_mode());
this->enable_sidebar(true);
}
if (panel == preview) {
if (q->only_gcode_mode()) {
@@ -18067,11 +18163,80 @@ void Plater::load_gcode(const wxString& filename)
m_last_loaded_gcode = filename;
// An OrcaSlicer G-code carries the settings it was sliced with. They replace the selected presets as a
// .gcode.3mf's do, so unsaved preset changes get the save-or-discard choice of opening a project rather
// than the new project's choice to keep them.
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
const std::string path = into_u8(filename);
std::optional<DynamicPrintConfig> file_config;
ConfigSubstitutions substitutions;
{
wxBusyCursor wait;
try {
file_config = preset_bundle->config_from_gcode(path, ForwardCompatibilitySubstitutionRule::Enable, substitutions);
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ": no settings to load from " << path << ": " << ex.what();
}
}
// BSS: create a new project when load_gcode, force close previous one
if (new_project(false, true) != wxID_YES)
if (file_config) {
auto check = [](bool yes_or_no) {
return yes_or_no || wxGetApp().check_and_save_current_preset_changes(_L("Load project"), _L("Some presets are modified."));
};
if (close_with_confirm(check) == wxID_CANCEL || new_project(true, true) != wxID_YES)
return;
} else if (new_project(false, true) != wxID_YES)
return;
m_only_gcode = true;
m_gcode_settings_loaded = false;
// A previewed G-code's settings are not the selection the app should remember.
PresetBundle::SelectionExportBlocker no_selection_export(*preset_bundle);
// What loading the file's settings replaces, to put back if that load fails or the file is rejected.
std::optional<PresetBundle::Selection> previous;
if (file_config) {
previous = preset_bundle->selection();
if (!previous)
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": project presets are still loaded, not loading the settings of " << path;
}
auto unload_file_settings = [this, preset_bundle, &previous]() {
if (!previous)
return;
preset_bundle->restore_selection(*previous);
wxGetApp().load_current_presets(false, false);
// Switching back to the previous printer re-derives some project settings (TabPrinter::on_preset_loaded),
// and only the printer refresh shows them in the sidebar's flow selectors.
preset_bundle->project_config = std::move(previous->project_config);
sidebar().update_presets(Preset::TYPE_PRINTER);
reset_project_dirty_initial_presets();
update_project_dirty_from_presets();
m_gcode_settings_loaded = false;
// The rejections after the switch to the preview have already shown the sidebar with the file's settings.
enable_sidebar(false);
previous.reset();
};
if (previous) {
try {
preset_bundle->load_config_model(path, std::move(*file_config));
m_gcode_settings_loaded = true;
} catch (const std::exception& ex) {
unload_file_settings();
get_notification_manager()->push_notification(NotificationType::CustomNotification,
NotificationManager::NotificationLevel::WarningNotificationLevel,
GUI::format(_L("Failed loading file \"%1%\". An invalid configuration was found."), path) +
"\n" + ex.what());
}
}
if (m_gcode_settings_loaded) {
wxGetApp().load_current_presets(false, false);
sync_loaded_project_filament_config(preset_bundle->project_config);
sidebar().update_presets(Preset::TYPE_FILAMENT);
reset_project_dirty_initial_presets();
update_project_dirty_from_presets();
if (!substitutions.empty())
show_substitutions_info(substitutions, path);
}
// cleanup view before to start loading/processing
//BBS: update gcode to current partplate's
@@ -18099,6 +18264,7 @@ void Plater::load_gcode(const wxString& filename)
}
catch (const std::exception& ex)
{
unload_file_settings();
show_error(this, ex.what());
return;
}
@@ -18146,6 +18312,7 @@ void Plater::load_gcode(const wxString& filename)
p->preview->get_canvas3d()->zoom_to_plate(0);
if (p->preview->get_canvas3d()->get_gcode_layers_zs().empty()) {
unload_file_settings();
MessageDialog(this, format_wxstr(_L("The selected file:\n%1%\ndoes not contain valid G-code."), filename),
wxString(GCODEVIEWER_APP_NAME) + " - " + _L("An Error has occurred while loading the G-code file."), wxCLOSE | wxICON_WARNING | wxCENTRE).ShowModal();
set_project_filename(_L(DEFAULT_PROJECT_NAME));
@@ -22144,6 +22311,18 @@ void Plater::suppress_background_process(const bool stop_background_process)
BackgroundSlicingProcess& Plater::background_process() { return p->background_process; }
void Plater::center_selection() { p->center_selection(); }
void Plater::distribute_selection_x() { p->distribute_selection_x(); }
void Plater::distribute_selection_y() { p->distribute_selection_y(); }
void Plater::distribute_selection_z() { p->distribute_selection_z(); }
void Plater::align_selection_x_min() { p->align_selection_x_min(); }
void Plater::align_selection_x_center() { p->align_selection_x_center(); }
void Plater::align_selection_x_max() { p->align_selection_x_max(); }
void Plater::align_selection_y_min() { p->align_selection_y_min(); }
void Plater::align_selection_y_center() { p->align_selection_y_center(); }
void Plater::align_selection_y_max() { p->align_selection_y_max(); }
void Plater::align_selection_z_min() { p->align_selection_z_min(); }
void Plater::align_selection_z_center() { p->align_selection_z_center(); }
void Plater::align_selection_z_max() { p->align_selection_z_max(); }
void Plater::drop_selection() { p->drop_selection(); }
void Plater::mirror(Axis axis) { p->mirror(axis); }
void Plater::split_object(bool auto_drop) { p->split_object(auto_drop); }
+17
View File
@@ -418,6 +418,9 @@ public:
bool is_gcode_3mf() { return m_exported_file; }
bool only_gcode_mode() { return m_only_gcode; }
void set_only_gcode(bool only_gcode) { m_only_gcode = only_gcode; }
// The previewed G-code's own settings were loaded into the presets, as a .gcode.3mf's are: the bed already is the
// one it was sliced for, and the sidebar shows them.
bool gcode_settings_loaded() const { return m_only_gcode && m_gcode_settings_loaded; }
//BBS: add only gcode mode
bool using_exported_file() { return m_exported_file; }
@@ -759,6 +762,18 @@ public:
//BBS: add clone logic
void clone_selection();
void center_selection();
void distribute_selection_x();
void distribute_selection_y();
void distribute_selection_z();
void align_selection_x_min();
void align_selection_x_center();
void align_selection_x_max();
void align_selection_y_min();
void align_selection_y_center();
void align_selection_y_max();
void align_selection_z_min();
void align_selection_z_center();
void align_selection_z_max();
void drop_selection();
void search(bool plater_is_active, Preset::Type type, wxWindow *tag, TextInput *etag, wxWindow *stag);
void mirror(Axis axis);
@@ -1082,6 +1097,8 @@ private:
wxString m_last_loaded_gcode;
//BBS: add only gcode mode
bool m_only_gcode { false };//just for .gcode file not for .gcode.3mf
// Set by load_gcode() for the file it previews, so it means nothing once m_only_gcode is cleared.
bool m_gcode_settings_loaded { false };
bool m_exported_file { false };
bool skip_thumbnail_invalid { false };
bool m_loading_project { false };
+15 -1
View File
@@ -450,6 +450,16 @@ void GuideFrame::on_profile_loaded()
SaveProfileData();
const std::string strAll = m_ProfileJson.dump(-1, ' ', false, json::error_handler_t::ignore);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", finished, json contents:\n" << strAll;
m_profile_ready = true;
notify_profile_loaded();
}
// The data is often ready before the loading page has run its scripts, and a
// call into a page without HandleStudio is lost, so the message waits for both.
void GuideFrame::notify_profile_loaded()
{
if (!m_profile_ready || !m_page_ready)
return;
json res;
res["command"] = "userguide_profile_load_finish";
res["sequence_id"] = "10001";
@@ -545,7 +555,11 @@ void GuideFrame::OnScriptMessage(wxWebViewEvent &evt)
wxString strCmd = j["command"];
BOOST_LOG_TRIVIAL(trace) << "GuideFrame::OnScriptMessage;Command:" << strCmd;
if (strCmd == "close_page") {
if (strCmd == "userguide_loading_page_ready") {
m_page_ready = true;
notify_profile_loaded();
}
else if (strCmd == "close_page") {
this->EndModal(wxID_CANCEL);
}
if (strCmd == "user_clause") {
+5
View File
@@ -102,6 +102,7 @@ public:
int LoadProfileFamily(std::string strVendor, std::string strFilePath);
void init_guide_paths();
void on_profile_loaded();
void notify_profile_loaded();
bool BuildProfileJson(const PresetBundle& bundle, bool require_all_resource_vendors);
bool BuildProfileDataFromPresetBundle();
bool BuildProfileDataFromVendors();
@@ -142,6 +143,10 @@ private:
//First Load
bool bFirstComplete{false};
// The loading page gets userguide_profile_load_finish once both are true;
// every instance of it reports in, so the flag is never reset.
bool m_profile_ready{false};
bool m_page_ready{false};
// Set once in the destructor. Read through `this` by the loading thread
// (joined before `this` dies) and captured as the shared_ptr by CallAfter
// lambdas so they don't touch `this` after the object is freed.
+28 -2
View File
@@ -56,7 +56,12 @@ namespace fs = boost::filesystem;
#define WEBKIT_API
struct WebKitWebView;
struct WebKitJavascriptResult;
struct WebKitSettings;
extern "C" {
WEBKIT_API WebKitSettings *
webkit_settings_new (void);
WEBKIT_API const gchar *
webkit_settings_get_user_agent (WebKitSettings *settings);
WEBKIT_API void
webkit_web_view_run_javascript (WebKitWebView *web_view,
const gchar *script,
@@ -284,6 +289,23 @@ static WebViewRef *webview_ref(wxWebView *webView)
return webView ? static_cast<WebViewRef *>(webView->GetRefData()) : nullptr;
}
#ifdef __linux__
// WebKitGTK's own User-Agent with the tags the bundled pages (globalapi.js) and Bambu's cloud read
// appended. wx's GTK backend cannot report the stock string, so it comes from a throwaway settings object.
static wxString linux_user_agent()
{
static const wxString stock = [] {
WebKitSettings *settings = webkit_settings_new();
wxString ua = wxString::FromUTF8(webkit_settings_get_user_agent(settings));
g_object_unref(settings);
return ua;
}();
return wxString::Format("%s BBL-Slicer/v%s (%s) BBL-Language/%s", stock, Slic3r::GUI::wxGetApp().get_bbl_client_version(),
Slic3r::GUI::wxGetApp().dark_mode() ? "dark" : "light",
Slic3r::GUI::wxGetApp().current_language_code().BeforeFirst('_'));
}
#endif
wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url)
{
#if wxUSE_WEBVIEW_EDGE
@@ -336,7 +358,9 @@ wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url)
s_schemes_registered = true;
}
webView->Create(parent, wxID_ANY, url2, wxDefaultPosition, wxDefaultSize, wxBORDER_NONE);
#ifndef __linux__
#ifdef __linux__
webView->SetUserAgent(linux_user_agent());
#else
webView->SetUserAgent(wxString::Format("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) BBL-Slicer/v%s (%s) BBL-Language/%s",
Slic3r::GUI::wxGetApp().get_bbl_client_version(), Slic3r::GUI::wxGetApp().dark_mode() ? "dark" : "light", language_code.mb_str()));
#endif
@@ -472,7 +496,9 @@ void WebView::RecreateAll()
wxString language_code = Slic3r::GUI::wxGetApp().current_language_code().BeforeFirst('_');
language_code = language_code.ToStdString();
for (auto webView : g_webviews) {
#ifndef __linux__
#ifdef __linux__
webView->SetUserAgent(linux_user_agent());
#else
webView->SetUserAgent(wxString::Format("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) BBL-Slicer/v%s (%s) BBL-Language/%s",
Slic3r::GUI::wxGetApp().get_bbl_client_version(), dark ? "dark" : "light", language_code.mb_str()));
#endif
+68 -1
View File
@@ -1,16 +1,27 @@
#include "PluginHostBindings.hpp"
#include "PluginHostUi.hpp"
#include "slic3r/plugin/PluginBindingUtils.hpp"
#include <pybind11/pytypes.h>
#include <pybind11/cast.h>
#include <pybind11/pybind11.h>
#include "slic3r/plugin/PluginBindingUtils.hpp"
#include <cstddef>
#include <libslic3r/AppConfig.hpp>
#include <libslic3r/Preset.hpp>
#include <libslic3r/PresetBundle.hpp>
#include <memory>
#include <slic3r/GUI/GUI.hpp>
#include <slic3r/GUI/GUI_App.hpp>
#include <slic3r/GUI/MainFrame.hpp>
#include <slic3r/GUI/Plater.hpp>
#include <slic3r/GUI/Tab.hpp>
#include <pybind11/stl.h>
#include <stdexcept>
#include <string>
#include <vector>
@@ -37,10 +48,66 @@ PresetCollection& printer_presets(PresetBundle& bundle)
return static_cast<PresetCollection&>(bundle.printers);
}
void reload_local_bundle(const std::string& bundle_id)
{
bool reloaded = false;
std::string error;
PluginHostUi::run_on_ui_thread([&] {
GUI::GUI_App& app = GUI::wxGetApp();
if (app.is_closing())
throw std::runtime_error("OrcaSlicer is closing");
if (app.preset_bundle == nullptr)
throw std::runtime_error("Preset bundle is not available");
if (app.app_config == nullptr)
throw std::runtime_error("Application configuration is not available");
if (app.mainframe == nullptr)
throw std::runtime_error("OrcaSlicer main window is not available");
reloaded = app.preset_bundle->reload_local_bundle(app.app_config->get("preset_folder"), bundle_id, &error);
if (!reloaded)
return;
app.mainframe->update_side_preset_ui();
for (GUI::Tab* tab : app.tabs_list) {
tab->reload_config();
tab->update_changed_ui();
}
if (GUI::Plater* plater = app.plater())
plater->sidebar().update_all_preset_comboboxes();
});
if (!reloaded)
throw std::runtime_error(error.empty() ? "Local bundle was not reloaded" : error);
}
std::vector<std::string> list_bundle_ids()
{
std::vector<std::string> ids;
PluginHostUi::run_on_ui_thread([&] {
GUI::GUI_App& app = GUI::wxGetApp();
if (app.is_closing())
throw std::runtime_error("OrcaSlicer is closing");
if (app.preset_bundle == nullptr)
throw std::runtime_error("Preset bundle is not available");
if (app.app_config == nullptr)
throw std::runtime_error("Application configuration is not available");
ids = app.preset_bundle->list_local_bundle_ids(app.app_config->get("preset_folder"));
});
return ids;
}
} // namespace
void host_bindings::register_presets(py::module_& host)
{
auto preset = host.def_submodule("preset", "Preset operations");
auto bundles = preset.def_submodule("bundles", "Local preset bundle operations");
bundles.def("reload_local", &reload_local_bundle, py::arg("bundle_id"),
"Reload one local preset bundle from the active user's _local directory.");
bundles.def("list_ids", &list_bundle_ids,
"IDs of local preset bundles in the active user's _local directory that reload_local() accepts, "
"including bundles added after startup.");
py::enum_<Preset::Type>(host, "PresetType")
.value("Invalid", Preset::TYPE_INVALID)
.value("Print", Preset::TYPE_PRINT)
+5
View File
@@ -646,6 +646,11 @@ void plater_notification(GUI::NotificationManager::NotificationLevel notificatio
} // namespace
void PluginHostUi::run_on_ui_thread(std::function<void()> fn)
{
run_on_ui_blocking(std::move(fn));
}
void PluginHostUi::RegisterBindings(pybind11::module_& host)
{
auto ui = host.def_submodule(
+3
View File
@@ -2,6 +2,7 @@
#include <pybind11/pybind11.h>
#include <functional>
#include <string>
namespace Slic3r {
@@ -19,6 +20,8 @@ class PluginHostUi
public:
static void RegisterBindings(pybind11::module_& host);
static void run_on_ui_thread(std::function<void()> fn);
// Lifecycle hook: close and tear down every UI window owned by a plugin. PluginManager invokes
// this after plugin teardown and also for bulk unload during application shutdown.
static void close_windows_for_plugin(const std::string& plugin_key);
+44
View File
@@ -3,17 +3,25 @@
#include <catch2/catch_message.hpp>
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/ModelArrange.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/Preset.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "test_helpers.hpp"
#include "test_utils.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/Point.hpp"
#include <fstream>
#include <ios>
#include <string>
#include <vector>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
using namespace Slic3r;
@@ -41,3 +49,39 @@ TEST_CASE("Klipper object labels name each copy without the characters Klipper c
CHECK(gcode.find("EXCLUDE_OBJECT_START NAME=" + instance_label + "\n") != std::string::npos);
}
}
TEST_CASE("An exported G-code's settings load back as the settings it was sliced with", "[GCode]")
{
// Two filaments, so the config block carries the flush matrix already multiplied by flush_multiplier.
const DynamicPrintConfig config = Test::multifilament_config(2, {
{ "flush_multiplier", "0.5" },
{ "printable_area", "0x0,250x0,250x210,0x210" },
{ "printable_height", "180" },
});
Print print;
Model model;
Test::init_print(std::vector<TriangleMesh>{ Test::cube(20.) }, print, model, config, nullptr, false);
ScopedTemporaryFile file(".gcode");
{
std::ofstream os(file.string(), std::ios::binary);
os << Test::gcode(print);
REQUIRE(os.good());
}
PresetBundle bundle;
// The config block leaves prime_volume_mode out, so the receiver's choice has to survive the load.
bundle.project_config.option<ConfigOptionEnum<PrimeVolumeMode>>("prime_volume_mode")->value = PrimeVolumeMode::pvmFast;
ConfigSubstitutions substitutions;
bundle.load_config_model(file.string(), bundle.config_from_gcode(file.string(), ForwardCompatibilitySubstitutionRule::Enable, substitutions));
const DynamicPrintConfig &printer = bundle.printers.get_edited_preset().config;
CHECK(printer.opt_serialize("printable_area") == config.opt_serialize("printable_area"));
CHECK_THAT(printer.opt_float("printable_height"), Catch::Matchers::WithinAbs(180., 1e-6));
// The purge between the two filaments is what the slice used: 280 at a 0.5 multiplier.
const std::vector<double> &matrix = bundle.project_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values;
const std::vector<double> &multiplier = bundle.project_config.option<ConfigOptionFloats>("flush_multiplier")->values;
REQUIRE(matrix.size() == 4);
REQUIRE(multiplier.size() == 1);
CHECK_THAT(matrix[1] * multiplier[0], Catch::Matchers::WithinAbs(140., 1e-6));
CHECK(bundle.project_config.option<ConfigOptionEnum<PrimeVolumeMode>>("prime_volume_mode")->value == PrimeVolumeMode::pvmFast);
}
@@ -10,10 +10,17 @@
#include <boost/nowide/fstream.hpp>
#include <cstddef>
#include <fstream>
#include <functional>
#include <future>
#include <mutex>
#include <new>
#include <nlohmann/json.hpp>
#include <shared_mutex>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include <catch2/catch_message.hpp>
#include <catch2/generators/catch_generators.hpp>
#include "libslic3r/PresetBundle.hpp"
@@ -38,6 +45,7 @@
#include "libslic3r/Config.hpp"
#include <map>
#include <memory>
#include <optional>
#include <regex>
#include "libslic3r/libslic3r.h"
#include "libslic3r/PublishSettings.hpp"
@@ -147,6 +155,124 @@ struct RenameTestCollection : public PresetCollection
using PresetCollection::update_map_system_profile_renamed;
};
struct ScopedPresetDataDir
{
ScopedTemporaryDir tmp{"orca-local-bundle-reload"};
std::string previous{data_dir()};
ScopedPresetDataDir() { set_data_dir(tmp.path().string()); }
~ScopedPresetDataDir() { set_data_dir(previous); }
fs::path bundle_dir(const std::string& id) const
{
return tmp.path() / PRESET_USER_DIR / DEFAULT_USER_FOLDER_NAME / PRESET_LOCAL_DIR / id;
}
};
void write_bundle_metadata(const fs::path& bundle_dir, const std::string& id)
{
fs::create_directories(bundle_dir);
std::ofstream((bundle_dir / PRESET_BUNDLE_METADATA).string()) << "{\"id\":\"" << id << "\"}";
}
void write_print_preset_with_layer_height(const DynamicPrintConfig& default_config, const fs::path& file,
const std::string& name, double layer_height)
{
DynamicPrintConfig config(default_config);
config.option<ConfigOptionString>("print_settings_id", true)->value = name;
config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS, true)->value.clear();
config.option<ConfigOptionFloat>("layer_height", true)->value = layer_height;
fs::create_directories(file.parent_path());
config.save_to_json(file.string(), name, "User", "1.0.0");
}
double layer_height(const Preset& preset)
{
return preset.config.option<ConfigOptionFloat>("layer_height")->value;
}
// The first copy snapshots the dirty edit; cloning that snapshot replays the edit after source loading.
// This exercises late failures and concurrent metadata updates without a production test hook.
struct ReloadEdit : ConfigOptionString
{
std::function<void()> on_restore;
bool copied = false;
explicit ReloadEdit(std::function<void()> on_restore) : ConfigOptionString("dirty start gcode"), on_restore(std::move(on_restore)) {}
ConfigOption* clone() const override
{
if (copied)
on_restore();
auto* copy = new ReloadEdit(*this);
copy->copied = true;
return copy;
}
};
void check_reload_state(const PresetBundle& bundle, const PresetBundle& before)
{
const PresetCollection* actual[] = {&bundle.prints, &bundle.filaments, &bundle.printers};
const PresetCollection* expected[] = {&before.prints, &before.filaments, &before.printers};
for (size_t i = 0; i < 3; ++i) {
CAPTURE(i);
CHECK(actual[i]->get_selected_preset_name() == expected[i]->get_selected_preset_name());
CHECK(actual[i]->get_edited_preset().config == expected[i]->get_edited_preset().config);
CHECK(actual[i]->get_edited_preset().is_dirty == expected[i]->get_edited_preset().is_dirty);
REQUIRE(actual[i]->size() == expected[i]->size());
for (size_t j = 0; j < actual[i]->size(); ++j) {
const Preset& preset = actual[i]->get_presets()[j];
const Preset& old = expected[i]->get_presets()[j];
CHECK(preset.name == old.name);
CHECK(preset.config == old.config);
CHECK(preset.bundle_id == old.bundle_id);
CHECK(preset.is_compatible == old.is_compatible);
CHECK(preset.is_visible == old.is_visible);
CHECK(preset.is_dirty == old.is_dirty);
CHECK(actual[i]->get_preset_name_by_alias(preset.alias) == expected[i]->get_preset_name_by_alias(old.alias));
}
}
CHECK(bundle.filament_presets == before.filament_presets);
CHECK(bundle.project_config == before.project_config);
}
struct LocalReloadFixture
{
ScopedPresetDataDir data_dir;
PresetBundle bundle;
const std::string id = "reload-target";
const fs::path directory = data_dir.bundle_dir(id);
const std::string prefix = std::string(PRESET_LOCAL_DIR) + "/" + id + "/";
std::string error;
LocalReloadFixture()
{
write_bundle_metadata(directory, id);
write_print_preset_with_layer_height(bundle.prints.default_preset().config, directory / PRESET_PRINT_NAME / "Print.json", "Print",
0.20);
write_preset_with_inherits(bundle.filaments.default_preset().config, directory / PRESET_FILAMENT_NAME / "Filament.json", "Filament",
"");
write_preset_with_inherits(bundle.printers.default_preset().config, directory / PRESET_PRINTER_NAME / "Printer.json", "Printer", "");
REQUIRE(bundle.reload_local_bundle("", id, &error));
bundle.prints.select_preset_by_name(prefix + "Print", true);
bundle.filaments.select_preset_by_name(prefix + "Filament", true);
bundle.printers.select_preset_by_name(prefix + "Printer", true);
bundle.filament_presets = {prefix + "Filament"};
bundle.update_compatible(PresetSelectCompatibleType::Never);
}
void change_sources()
{
write_print_preset_with_layer_height(bundle.prints.default_preset().config, directory / PRESET_PRINT_NAME / "Print.json", "Print",
0.30);
write_preset_with_inherits(bundle.filaments.default_preset().config, directory / PRESET_FILAMENT_NAME / "Added Filament.json",
"Added Filament", "");
write_preset_with_inherits(bundle.printers.default_preset().config, directory / PRESET_PRINTER_NAME / "Added Printer.json",
"Added Printer", "");
std::ofstream((directory / PRESET_BUNDLE_METADATA).string()) << "{\"id\":\"" << id << "\",\"version\":\"2.0.0\"}";
}
};
} // namespace
TEST_CASE("Preset identity is canonicalized from load path", "[Preset][Identity]")
@@ -266,6 +392,431 @@ TEST_CASE("Printer extruder count tolerates missing nozzle diameter", "[Preset][
CHECK(bundle.get_printer_extruder_count() == 2);
}
TEST_CASE("Reloading one local bundle preserves unrelated and modified presets", "[Preset][Bundle]")
{
using Catch::Matchers::WithinAbs;
ScopedPresetDataDir data_dir;
PresetBundle bundle;
const std::string id = "reload-target";
const fs::path bundle_dir = data_dir.bundle_dir(id);
const fs::path process_file = bundle_dir / PRESET_PRINT_NAME / "Managed Print.json";
const std::string target_name = std::string(PRESET_LOCAL_DIR) + "/" + id + "/Managed Print";
std::string error;
write_bundle_metadata(bundle_dir, id);
write_print_preset_with_layer_height(bundle.prints.default_preset().config, process_file, "Managed Print", 0.20);
write_preset_with_inherits(bundle.filaments.default_preset().config,
bundle_dir / PRESET_FILAMENT_NAME / "Managed Filament.json", "Managed Filament", "");
write_preset_with_inherits(bundle.printers.default_preset().config,
bundle_dir / PRESET_PRINTER_NAME / "Managed Printer.json", "Managed Printer", "");
REQUIRE(bundle.reload_local_bundle("", id, &error));
const Preset* target = bundle.prints.find_preset(target_name, false, true);
REQUIRE(target != nullptr);
CHECK(target->bundle_id == id);
CHECK_FALSE(target->is_user());
REQUIRE(bundle.filaments.find_preset(std::string(PRESET_LOCAL_DIR) + "/" + id + "/Managed Filament") != nullptr);
REQUIRE(bundle.printers.find_preset(std::string(PRESET_LOCAL_DIR) + "/" + id + "/Managed Printer") != nullptr);
CHECK(bundle.prints.get_preset_name_by_alias("Managed Print") == target_name);
CHECK(bundle.filaments.get_preset_name_by_alias("Managed Filament") ==
std::string(PRESET_LOCAL_DIR) + "/" + id + "/Managed Filament");
CHECK(bundle.printers.get_preset_name_by_alias("Managed Printer") ==
std::string(PRESET_LOCAL_DIR) + "/" + id + "/Managed Printer");
const std::string unrelated_name = add_inmemory_preset(bundle.prints, "Unrelated Process").name;
bundle.prints.select_preset_by_name(unrelated_name, true);
write_print_preset_with_layer_height(bundle.prints.default_preset().config, process_file, "Managed Print", 0.25);
REQUIRE(bundle.reload_local_bundle("", id, &error));
CHECK(bundle.prints.get_selected_preset_name() == unrelated_name);
target = bundle.prints.find_preset(target_name, false, true);
REQUIRE(target != nullptr);
CHECK_THAT(layer_height(*target), WithinAbs(0.25, 1e-6));
bundle.prints.select_preset_by_name(target_name, true);
bundle.prints.get_edited_preset().config.option<ConfigOptionFloat>("layer_height", true)->value = 0.33;
REQUIRE(bundle.prints.update_dirty());
write_print_preset_with_layer_height(bundle.prints.default_preset().config, process_file, "Managed Print", 0.28);
REQUIRE(bundle.reload_local_bundle("", id, &error));
CHECK(bundle.prints.get_selected_preset_name() == target_name);
CHECK_THAT(layer_height(bundle.prints.get_selected_preset()), WithinAbs(0.28, 1e-6));
CHECK_THAT(layer_height(bundle.prints.get_edited_preset()), WithinAbs(0.33, 1e-6));
CHECK(bundle.prints.current_is_dirty());
std::ofstream(process_file.string()) << "not json";
CHECK_FALSE(bundle.reload_local_bundle("", id, &error));
CHECK(fs::exists(process_file));
CHECK_THAT(layer_height(bundle.prints.get_selected_preset()), WithinAbs(0.28, 1e-6));
CHECK_THAT(layer_height(bundle.prints.get_edited_preset()), WithinAbs(0.33, 1e-6));
bundle.prints.discard_current_changes();
REQUIRE(fs::remove_all(process_file.parent_path()) > 0);
REQUIRE(bundle.reload_local_bundle("", id, &error));
CHECK(bundle.prints.find_preset(target_name, false, true) == nullptr);
CHECK(bundle.prints.get_selected_preset_name() != target_name);
CHECK(bundle.filaments.find_preset(std::string(PRESET_LOCAL_DIR) + "/" + id + "/Managed Filament") != nullptr);
CHECK(bundle.printers.find_preset(std::string(PRESET_LOCAL_DIR) + "/" + id + "/Managed Printer") != nullptr);
const std::string filament_name = std::string(PRESET_LOCAL_DIR) + "/" + id + "/Managed Filament";
const std::string printer_name = std::string(PRESET_LOCAL_DIR) + "/" + id + "/Managed Printer";
bundle.filaments.select_preset_by_name(filament_name, true);
bundle.printers.select_preset_by_name(printer_name, true);
bundle.filament_presets = { filament_name };
REQUIRE(fs::remove_all(bundle_dir / PRESET_FILAMENT_NAME) > 0);
REQUIRE(fs::remove_all(bundle_dir / PRESET_PRINTER_NAME) > 0);
REQUIRE(bundle.reload_local_bundle("", id, &error));
CHECK(bundle.filaments.get_selected_preset_name() != filament_name);
CHECK(bundle.printers.get_selected_preset_name() != printer_name);
REQUIRE(bundle.filament_presets.size() == 1);
CHECK(bundle.filament_presets.front() != filament_name);
CHECK(bundle.filaments.find_preset(bundle.filament_presets.front(), false) != nullptr);
}
TEST_CASE("Reloading a local bundle resolves a loaded parent preset", "[Preset][Bundle]")
{
ScopedPresetDataDir data_dir;
PresetBundle bundle;
const std::string id = "inherited-target";
const fs::path bundle_dir = data_dir.bundle_dir(id);
const std::string parent_name = "Generic ABS @System";
const std::string target_name = std::string(PRESET_LOCAL_DIR) + "/" + id + "/Managed Filament";
std::string error;
add_inmemory_preset(bundle.filaments, parent_name);
write_bundle_metadata(bundle_dir, id);
write_preset_with_inherits(bundle.filaments.default_preset().config,
bundle_dir / PRESET_FILAMENT_NAME / "Managed Filament.json", "Managed Filament", parent_name);
REQUIRE(bundle.reload_local_bundle("", id, &error));
const Preset* target = bundle.filaments.find_preset(target_name, false, true);
REQUIRE(target != nullptr);
CHECK(target->inherits() == parent_name);
}
TEST_CASE("Reloading a local bundle rejects unsafe source and inheritance changes", "[Preset][Bundle]")
{
ScopedPresetDataDir data_dir;
PresetBundle bundle;
const std::string id = "guarded-target";
const fs::path bundle_dir = data_dir.bundle_dir(id);
const std::string target_name = std::string(PRESET_LOCAL_DIR) + "/" + id + "/Managed Print";
std::string error;
write_bundle_metadata(bundle_dir, id);
write_print_preset_with_layer_height(bundle.prints.default_preset().config,
bundle_dir / PRESET_PRINT_NAME / "Managed Print.json", "Managed Print", 0.20);
REQUIRE(bundle.reload_local_bundle("", id, &error));
add_inmemory_preset(bundle.prints, "External Child", target_name);
CHECK_FALSE(bundle.reload_local_bundle("", id, &error));
CHECK(error.find("External Child") != std::string::npos);
CHECK(bundle.prints.find_preset(target_name, false, true) != nullptr);
for (auto it = bundle.prints.begin(); it != bundle.prints.end(); ++it) {
if (it->name == "External Child") {
bundle.prints.erase(it);
break;
}
}
fs::remove_all(bundle_dir);
CHECK_FALSE(bundle.reload_local_bundle("", id, &error));
CHECK(bundle.prints.find_preset(target_name, false, true) != nullptr);
CHECK_FALSE(bundle.reload_local_bundle("", "../outside", &error));
CHECK(error == "Local bundle id must be a single path component");
CHECK_FALSE(bundle.reload_local_bundle("../outside", id, &error));
CHECK(error == "Preset folder must be a single path component");
#ifdef _WIN32
CHECK_FALSE(bundle.reload_local_bundle("", "C:", &error));
CHECK(error == "Local bundle id must be a single path component");
#endif
}
TEST_CASE("A failure while restoring dirty edits leaves the whole local bundle unchanged", "[Preset][Bundle][Regression]")
{
LocalReloadFixture f;
bool restored = false;
f.bundle.printers.get_edited_preset().config.set_key_value("machine_start_gcode", new ReloadEdit([&] {
restored = true;
throw std::bad_alloc();
}));
f.bundle.printers.update_dirty();
const PresetBundle before(f.bundle);
const BundleMetadata metadata = f.bundle.bundles.m_bundles.at(f.id);
const fs::path local_dir = f.bundle.dir_user_presets_local;
f.change_sources();
CHECK_FALSE(f.bundle.reload_local_bundle("", f.id, &f.error));
REQUIRE(restored);
CHECK_THAT(f.error, Catch::Matchers::ContainsSubstring("Failed to reload"));
check_reload_state(f.bundle, before);
CHECK(f.bundle.dir_user_presets_local == local_dir);
const auto& after = f.bundle.bundles.m_bundles.at(f.id);
CHECK(after.version == metadata.version);
CHECK(after.path == metadata.path);
CHECK(after.bundle_type == metadata.bundle_type);
CHECK(after.print_presets == metadata.print_presets);
CHECK(after.filament_presets == metadata.filament_presets);
CHECK(after.printer_presets == metadata.printer_presets);
CHECK(fs::exists(f.directory / PRESET_FILAMENT_NAME / "Added Filament.json"));
// The rejected staging state does not poison the next reload or leave locks held.
f.bundle.printers.discard_current_changes();
REQUIRE(f.bundle.reload_local_bundle("", f.id, &f.error));
CHECK_THAT(layer_height(f.bundle.prints.get_selected_preset()), Catch::Matchers::WithinAbs(0.30, 1e-6));
}
TEST_CASE("A subscribed bundle registered during staging wins over the local reload", "[Preset][Bundle][Regression]")
{
const bool replace_local_entry = GENERATE(false, true);
LocalReloadFixture f;
if (!replace_local_entry)
f.bundle.bundles.m_bundles.erase(f.id);
bool registered = false;
f.bundle.printers.get_edited_preset().config.set_key_value("machine_start_gcode", new ReloadEdit([&] {
// Finish a background metadata write after the initial local-source
// check, before live commit.
std::async(std::launch::async, [&] {
BundleMetadata subscribed;
subscribed.id = f.id;
subscribed.bundle_type = BundleType::Subscribed;
subscribed.is_subscribed = true;
subscribed.version = "9.0.0";
std::unique_lock<std::shared_mutex> lock(f.bundle.bundles.RWMtx);
f.bundle.bundles.m_bundles.insert_or_assign(f.id,
std::move(subscribed));
}).get();
registered = true;
}));
f.bundle.printers.update_dirty();
const PresetBundle before(f.bundle);
f.change_sources();
CHECK_FALSE(f.bundle.reload_local_bundle("", f.id, &f.error));
REQUIRE(registered);
CHECK_THAT(f.error, Catch::Matchers::ContainsSubstring("subscribed"));
check_reload_state(f.bundle, before);
const auto& metadata = f.bundle.bundles.m_bundles.at(f.id);
CHECK(metadata.bundle_type == BundleType::Subscribed);
CHECK(metadata.is_subscribed);
CHECK(metadata.version == "9.0.0");
}
TEST_CASE("External compatible process references prevent local bundle replacement and removal", "[Preset][Bundle][Regression]")
{
const bool edited_only = GENERATE(false, true);
const bool remove = GENERATE(false, true);
LocalReloadFixture f;
const std::string external_name = "External Filament";
add_inmemory_preset(f.bundle.filaments, external_name);
f.bundle.filaments.select_preset_by_name(external_name, true);
Preset& dependent = edited_only ? f.bundle.filaments.get_edited_preset() : *f.bundle.filaments.find_preset(external_name, false, true);
dependent.config.set_key_value("compatible_prints", new ConfigOptionStrings({f.prefix + "Print"}));
if (!edited_only)
f.bundle.filaments.select_preset_by_name(external_name, true);
f.bundle.filaments.update_dirty();
f.bundle.filament_presets = {external_name};
f.bundle.update_compatible(PresetSelectCompatibleType::Never);
REQUIRE(f.bundle.filaments.get_edited_preset().is_compatible);
const PresetBundle before(f.bundle);
if (remove)
REQUIRE(fs::remove(f.directory / PRESET_PRINT_NAME / "Print.json"));
else
f.change_sources();
CHECK_FALSE(f.bundle.reload_local_bundle("", f.id, &f.error));
CHECK_THAT(f.error, Catch::Matchers::ContainsSubstring(external_name));
check_reload_state(f.bundle, before);
CHECK(f.bundle.bundles.m_bundles.at(f.id).print_presets == std::vector<std::string>{f.prefix + "Print"});
}
TEST_CASE("External compatible printer references prevent local bundle replacement and removal", "[Preset][Bundle][Regression]")
{
const bool filament = GENERATE(false, true);
const bool edited_only = GENERATE(false, true);
const bool remove = GENERATE(false, true);
LocalReloadFixture f;
PresetCollection& collection = filament ? f.bundle.filaments : f.bundle.prints;
const std::string external_name = filament ? "External Filament" : "External Print";
add_inmemory_preset(collection, external_name);
collection.select_preset_by_name(external_name, true);
Preset& dependent = edited_only ? collection.get_edited_preset() : *collection.find_preset(external_name, false, true);
dependent.config.set_key_value("compatible_printers", new ConfigOptionStrings({f.prefix + "Printer"}));
if (!edited_only)
collection.select_preset_by_name(external_name, true);
collection.update_dirty();
if (filament)
f.bundle.filament_presets = {external_name};
f.bundle.update_compatible(PresetSelectCompatibleType::Never);
REQUIRE(collection.get_edited_preset().is_compatible);
const PresetBundle before(f.bundle);
if (remove)
REQUIRE(fs::remove(f.directory / PRESET_PRINTER_NAME / "Printer.json"));
else
f.change_sources();
CHECK_FALSE(f.bundle.reload_local_bundle("", f.id, &f.error));
CHECK_THAT(f.error, Catch::Matchers::ContainsSubstring(external_name));
check_reload_state(f.bundle, before);
}
TEST_CASE("Local reload retains internal compatibility references and live vendor and calibration pointers", "[Preset][Bundle][Regression]")
{
LocalReloadFixture f;
VendorProfile& vendor = f.bundle.vendors["TestVendor"];
vendor.id = "TestVendor";
Preset& external = add_inmemory_preset(f.bundle.filaments, "Vendor Filament");
external.is_system = true;
external.vendor = &vendor;
f.bundle.set_calibrate_printer(f.prefix + "Printer");
DynamicPrintConfig config(f.bundle.filaments.default_preset().config);
config.set_key_value("compatible_prints", new ConfigOptionStrings({f.prefix + "Print"}));
config.set_key_value("compatible_printers", new ConfigOptionStrings({f.prefix + "Printer"}));
write_preset_with_inherits(config, f.directory / PRESET_FILAMENT_NAME / "Filament.json", "Filament", "");
f.change_sources();
REQUIRE(f.bundle.reload_local_bundle("", f.id, &f.error));
CHECK(f.bundle.filaments.get_selected_preset_name() == f.prefix + "Filament");
CHECK(f.bundle.filaments.get_edited_preset().is_compatible);
CHECK(f.bundle.vendors.at("TestVendor").id == vendor.id);
CHECK(f.bundle.filaments.find_preset("Vendor Filament")->vendor == &vendor);
CHECK(f.bundle.calibrate_printer == f.bundle.printers.find_preset(f.prefix + "Printer"));
CHECK(f.bundle.calibrate_filaments.count(f.bundle.filaments.find_preset("Vendor Filament")) == 1);
CHECK(f.bundle.calibrate_filaments.count(f.bundle.filaments.find_preset(f.prefix + "Filament", false, true)) == 1);
}
TEST_CASE("Local reload preserves dirty filament and printer options over new source values", "[Preset][Bundle][Regression]")
{
LocalReloadFixture f;
f.bundle.filaments.get_edited_preset().config.option<ConfigOptionFloats>("filament_flow_ratio")->values = {1.13};
f.bundle.printers.get_edited_preset().config.option<ConfigOptionFloats>("nozzle_diameter")->values = {0.6};
f.bundle.filaments.update_dirty();
f.bundle.printers.update_dirty();
DynamicPrintConfig filament_config(f.bundle.filaments.get_selected_preset().config);
filament_config.option<ConfigOptionFloats>("filament_flow_ratio")->values = {0.98};
write_preset_with_inherits(filament_config, f.directory / PRESET_FILAMENT_NAME / "Filament.json", "Filament", "");
DynamicPrintConfig printer_config(f.bundle.printers.get_selected_preset().config);
printer_config.option<ConfigOptionFloats>("nozzle_diameter")->values = {0.8};
write_preset_with_inherits(printer_config, f.directory / PRESET_PRINTER_NAME / "Printer.json", "Printer", "");
REQUIRE(f.bundle.reload_local_bundle("", f.id, &f.error));
check_double_vector(f.bundle.filaments.get_selected_preset().config.option<ConfigOptionFloats>("filament_flow_ratio")->values, {0.98});
check_double_vector(f.bundle.filaments.get_edited_preset().config.option<ConfigOptionFloats>("filament_flow_ratio")->values, {1.13});
check_double_vector(f.bundle.printers.get_selected_preset().config.option<ConfigOptionFloats>("nozzle_diameter")->values, {0.8});
check_double_vector(f.bundle.printers.get_edited_preset().config.option<ConfigOptionFloats>("nozzle_diameter")->values, {0.6});
CHECK(f.bundle.filaments.current_is_dirty());
CHECK(f.bundle.printers.current_is_dirty());
}
TEST_CASE("Local reload rejects removal or rename of a dirty selected preset in every collection", "[Preset][Bundle][Regression]")
{
const auto type = GENERATE(Preset::TYPE_PRINT, Preset::TYPE_FILAMENT, Preset::TYPE_PRINTER);
const bool rename = GENERATE(false, true);
LocalReloadFixture f;
PresetCollection& collection = type == Preset::TYPE_PRINT ? f.bundle.prints :
type == Preset::TYPE_FILAMENT ? f.bundle.filaments :
f.bundle.printers;
const char* directory = type == Preset::TYPE_PRINT ? PRESET_PRINT_NAME :
type == Preset::TYPE_FILAMENT ? PRESET_FILAMENT_NAME :
PRESET_PRINTER_NAME;
const std::string name = type == Preset::TYPE_PRINT ? "Print" : type == Preset::TYPE_FILAMENT ? "Filament" : "Printer";
const char* key = type == Preset::TYPE_PRINT ? "layer_height" :
type == Preset::TYPE_FILAMENT ? "filament_flow_ratio" :
"nozzle_diameter";
collection.get_edited_preset().config.set_deserialize_strict(key, "0.37");
collection.update_dirty();
REQUIRE(collection.current_is_dirty());
const PresetBundle before(f.bundle);
REQUIRE(fs::remove(f.directory / directory / (name + ".json")));
if (rename)
write_preset_with_inherits(collection.default_preset().config, f.directory / directory / "Renamed.json", "Renamed", "");
CHECK_FALSE(f.bundle.reload_local_bundle("", f.id, &f.error));
CHECK_THAT(f.error, Catch::Matchers::ContainsSubstring("modified selected preset"));
check_reload_state(f.bundle, before);
}
TEST_CASE("Local reload reconciles filament slots and purge volumes while retaining unrelated project options",
"[Preset][Bundle][Regression]")
{
LocalReloadFixture f;
DynamicPrintConfig printer_config(f.bundle.printers.get_selected_preset().config);
printer_config.option<ConfigOptionFloats>("nozzle_diameter")->values = {0.4, 0.4};
write_preset_with_inherits(printer_config, f.directory / PRESET_PRINTER_NAME / "Printer.json", "Printer", "");
f.bundle.project_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values = {0.};
f.bundle.project_config.option<ConfigOptionFloats>("flush_volumes_vector")->values = {111., 222.};
f.bundle.project_config.option<ConfigOptionFloats>("flush_multiplier")->values = {0.75};
f.bundle.project_config.option<ConfigOptionStrings>("filament_colour")->values = {"#123456"};
const ConfigOption* colour = f.bundle.project_config.option("filament_colour");
REQUIRE(f.bundle.reload_local_bundle("", f.id, &f.error));
CHECK(f.bundle.filament_presets == std::vector<std::string>{f.prefix + "Filament", f.prefix + "Filament"});
check_double_vector(f.bundle.project_config.option<ConfigOptionFloats>("flush_multiplier")->values, {0.75, 1.});
check_double_vector(f.bundle.project_config.option<ConfigOptionFloats>("flush_volumes_vector")->values, {111., 222., 111., 222.});
check_double_vector(f.bundle.project_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values,
{0., 333., 333., 0., 0., 333., 333., 0.});
CHECK(f.bundle.project_config.option("filament_colour") == colour);
CHECK(f.bundle.project_config.option<ConfigOptionStrings>("filament_colour")->values == std::vector<std::string>{"#123456"});
}
TEST_CASE("Local reload rejects a non-FFF printer selection without changing live presets", "[Preset][Bundle][Regression]")
{
const bool from_source = GENERATE(false, true);
LocalReloadFixture f;
DynamicPrintConfig printer_config(f.bundle.printers.get_selected_preset().config);
printer_config.set_key_value("printer_technology", new ConfigOptionEnum<PrinterTechnology>(ptSLA));
if (from_source)
write_preset_with_inherits(printer_config, f.directory / PRESET_PRINTER_NAME / "Printer.json", "Printer", "");
else
f.bundle.printers.load_preset("", "Non-FFF Printer", printer_config, true);
const PresetBundle before(f.bundle);
CHECK_FALSE(f.bundle.reload_local_bundle("", f.id, &f.error));
CHECK_THAT(f.error, Catch::Matchers::ContainsSubstring(from_source ? "invalid preset" : "FFF printer selection"));
check_reload_state(f.bundle, before);
CHECK(f.bundle.bundles.m_bundles.at(f.id).printer_presets == std::vector<std::string>{f.prefix + "Printer"});
}
TEST_CASE("Listing local bundles returns only ids that reload accepts", "[Preset][Bundle]")
{
ScopedPresetDataDir data_dir;
PresetBundle bundle;
const fs::path local_dir = data_dir.bundle_dir("unused").parent_path();
std::string error;
CHECK(bundle.list_local_bundle_ids("").empty());
CHECK_FALSE(fs::exists(local_dir));
CHECK(bundle.list_local_bundle_ids("../outside").empty());
write_bundle_metadata(data_dir.bundle_dir("valid-b"), "valid-b");
write_print_preset_with_layer_height(bundle.prints.default_preset().config,
data_dir.bundle_dir("valid-b") / PRESET_PRINT_NAME / "Listed Print.json",
"Listed Print", 0.20);
write_bundle_metadata(data_dir.bundle_dir("valid-a"), "valid-a");
fs::create_directories(data_dir.bundle_dir("no-metadata"));
write_bundle_metadata(data_dir.bundle_dir("wrong-id"), "other-id");
write_bundle_metadata(data_dir.bundle_dir("subscribed-folder"), "subscribed-folder");
fs::create_directories(data_dir.tmp.path() / PRESET_USER_DIR / DEFAULT_USER_FOLDER_NAME / PRESET_SUBSCRIBED_DIR /
"subscribed-folder");
write_bundle_metadata(data_dir.bundle_dir("subscribed-registry"), "subscribed-registry");
BundleMetadata subscribed;
subscribed.id = "subscribed-registry";
subscribed.bundle_type = BundleType::Subscribed;
bundle.bundles.m_bundles.emplace(subscribed.id, subscribed);
std::ofstream((local_dir / "stray-file.json").string()) << "{}";
const size_t print_count = bundle.prints.size();
const std::vector<std::string> expected{"valid-a", "valid-b"};
CHECK(bundle.list_local_bundle_ids("") == expected);
CHECK(bundle.prints.size() == print_count);
CHECK_FALSE(fs::exists(data_dir.bundle_dir("no-metadata") / PRESET_BUNDLE_METADATA));
for (const std::string& rejected : {"no-metadata", "wrong-id", "subscribed-folder", "subscribed-registry"})
CHECK_FALSE(bundle.reload_local_bundle("", rejected, &error));
REQUIRE(bundle.reload_local_bundle("", "valid-b", &error));
CHECK(bundle.prints.find_preset(std::string(PRESET_LOCAL_DIR) + "/valid-b/Listed Print", false, true) != nullptr);
}
TEST_CASE("Selected printer uses its default or saved bed type", "[Preset][Bundle]")
{
PresetBundle bundle;
@@ -5957,6 +6508,176 @@ TEST_CASE("Config import confines zip entries, preset names and bundle ids to th
}
}
namespace {
void change_json_value(const fs::path& path, const std::string& key, const std::string& value)
{
nlohmann::json document;
{
std::ifstream input(path.string());
input >> document;
}
document[key] = value;
std::ofstream(path.string()) << document.dump(2);
}
} // namespace
TEST_CASE("Shrinking an edited vector survives local reload", "[Preset][Bundle][Regression]")
{
LocalReloadFixture f;
DynamicPrintConfig config(f.bundle.printers.get_selected_preset().config);
config.option<ConfigOptionFloats>("nozzle_diameter")->values = {0.4, 0.4};
write_preset_with_inherits(config, f.directory / PRESET_PRINTER_NAME / "Printer.json", "Printer", "");
REQUIRE(f.bundle.reload_local_bundle("", f.id, &f.error));
f.bundle.printers.get_edited_preset().config.option<ConfigOptionFloats>("nozzle_diameter")->values = {0.4};
f.bundle.printers.update_dirty();
REQUIRE(f.bundle.printers.current_is_dirty());
REQUIRE(f.bundle.reload_local_bundle("", f.id, &f.error));
CHECK(f.bundle.printers.get_edited_preset().config.option<ConfigOptionFloats>("nozzle_diameter")->values.size() == 1);
CHECK(f.bundle.printers.current_is_dirty());
}
TEST_CASE("Removing a selected printer preserves external dirty process edits", "[Preset][Bundle][Regression]")
{
LocalReloadFixture f;
DynamicPrintConfig printer_config(f.bundle.printers.get_selected_preset().config);
printer_config.option<ConfigOptionFloats>("nozzle_diameter")->values = {0.6};
write_preset_with_inherits(printer_config, f.directory / PRESET_PRINTER_NAME / "Printer.json", "Printer", "");
REQUIRE(f.bundle.reload_local_bundle("", f.id, &f.error));
Preset& external = add_inmemory_preset(f.bundle.prints, "External Process");
external.config.set_key_value("compatible_printers_condition", new ConfigOptionString("nozzle_diameter[0] == 0.6"));
f.bundle.prints.select_preset_by_name("External Process", true);
f.bundle.update_compatible(PresetSelectCompatibleType::Never);
REQUIRE(f.bundle.prints.get_edited_preset().is_compatible);
f.bundle.prints.get_edited_preset().config.option<ConfigOptionFloat>("layer_height")->value = 0.27;
f.bundle.prints.update_dirty();
REQUIRE(f.bundle.prints.current_is_dirty());
REQUIRE(fs::remove(f.directory / PRESET_PRINTER_NAME / "Printer.json"));
const PresetBundle before(f.bundle);
const bool reloaded = f.bundle.reload_local_bundle("", f.id, &f.error);
CAPTURE(f.error);
CHECK_FALSE(reloaded);
check_reload_state(f.bundle, before);
CHECK(f.bundle.prints.get_selected_preset_name() == "External Process");
CHECK_THAT(layer_height(f.bundle.prints.get_edited_preset()), Catch::Matchers::WithinAbs(0.27, 1e-6));
CHECK(f.bundle.prints.current_is_dirty());
}
TEST_CASE("A preset with an invalid version does not silently disappear on reload", "[Preset][Bundle][Regression]")
{
const auto type = GENERATE(Preset::TYPE_PRINT, Preset::TYPE_FILAMENT, Preset::TYPE_PRINTER);
LocalReloadFixture f;
const char* directory = type == Preset::TYPE_PRINT ? PRESET_PRINT_NAME :
type == Preset::TYPE_FILAMENT ? PRESET_FILAMENT_NAME :
PRESET_PRINTER_NAME;
const char* name = type == Preset::TYPE_PRINT ? "Print.json" : type == Preset::TYPE_FILAMENT ? "Filament.json" : "Printer.json";
change_json_value(f.directory / directory / name, "version", "broken-version");
const PresetBundle before(f.bundle);
const bool reloaded = f.bundle.reload_local_bundle("", f.id, &f.error);
CAPTURE(f.error);
CHECK_FALSE(reloaded);
check_reload_state(f.bundle, before);
CHECK(f.bundle.prints.find_preset(f.prefix + "Print", false, true) != nullptr);
}
TEST_CASE("Hiding a selected source preset does not replay its edits onto another preset", "[Preset][Bundle][Regression]")
{
LocalReloadFixture f;
add_inmemory_preset(f.bundle.prints, "External Process");
f.bundle.prints.get_edited_preset().config.option<ConfigOptionFloat>("layer_height")->value = 0.27;
f.bundle.prints.update_dirty();
REQUIRE(f.bundle.prints.current_is_dirty());
change_json_value(f.directory / PRESET_PRINT_NAME / "Print.json", "instantiation", "false");
const PresetBundle before(f.bundle);
const bool reloaded = f.bundle.reload_local_bundle("", f.id, &f.error);
CAPTURE(f.error);
CHECK_FALSE(reloaded);
check_reload_state(f.bundle, before);
CHECK(f.bundle.prints.get_selected_preset_name() == f.prefix + "Print");
CHECK_THAT(layer_height(f.bundle.prints.get_edited_preset()), Catch::Matchers::WithinAbs(0.27, 1e-6));
CHECK(f.bundle.prints.current_is_dirty());
}
TEST_CASE("Background subscription flags are retained at the reload commit", "[Preset][Bundle][Regression]")
{
LocalReloadFixture f;
bool registered = false;
f.bundle.printers.get_edited_preset().config.set_key_value("machine_start_gcode", new ReloadEdit([&] {
std::async(std::launch::async, [&] {
std::unique_lock<std::shared_mutex> lock(f.bundle.bundles.RWMtx);
auto& metadata = f.bundle.bundles.m_bundles.at(f.id);
metadata.is_subscribed = true;
metadata.update_available = true;
}).get();
registered = true;
}));
f.bundle.printers.update_dirty();
const PresetBundle before(f.bundle);
const bool reloaded = f.bundle.reload_local_bundle("", f.id, &f.error);
REQUIRE(registered);
CAPTURE(f.error);
CHECK_FALSE(reloaded);
check_reload_state(f.bundle, before);
CHECK(f.bundle.bundles.m_bundles.at(f.id).is_subscribed);
CHECK(f.bundle.bundles.m_bundles.at(f.id).update_available);
}
TEST_CASE("An unrelated stored preset remains at the address exposed to consumers", "[Preset][Bundle][Regression]")
{
LocalReloadFixture f;
const std::string name = "External Process";
const Preset* external = &add_inmemory_preset(f.bundle.prints, name);
const Preset* selected = &f.bundle.prints.get_selected_preset();
const Preset* edited = &f.bundle.prints.get_edited_preset();
REQUIRE(f.bundle.prints.get_selected_preset_name() != name);
write_print_preset_with_layer_height(f.bundle.prints.default_preset().config, f.directory / PRESET_PRINT_NAME / "Print.json", "Print",
0.30);
REQUIRE(f.bundle.reload_local_bundle("", f.id, &f.error));
REQUIRE(f.bundle.prints.find_preset(name, false, true) == external);
REQUIRE(&f.bundle.prints.get_selected_preset() == selected);
REQUIRE(&f.bundle.prints.get_edited_preset() == edited);
CHECK(external->name == name);
CHECK_THAT(layer_height(*selected), Catch::Matchers::WithinAbs(0.30, 1e-6));
CHECK_THAT(layer_height(*edited), Catch::Matchers::WithinAbs(0.30, 1e-6));
}
TEST_CASE("Calibration compatibility reflects the reloaded printer settings", "[Preset][Bundle][Regression]")
{
LocalReloadFixture f;
Preset& external = add_inmemory_preset(f.bundle.filaments, "Calibration Filament");
external.config.set_key_value("compatible_printers_condition", new ConfigOptionString("nozzle_diameter[0] == 0.4"));
f.bundle.set_calibrate_printer(f.prefix + "Printer");
REQUIRE(f.bundle.calibrate_filaments.count(&external) == 1);
DynamicPrintConfig config(f.bundle.printers.get_selected_preset().config);
config.option<ConfigOptionFloats>("nozzle_diameter")->values = {0.6};
write_preset_with_inherits(config, f.directory / PRESET_PRINTER_NAME / "Printer.json", "Printer", "");
REQUIRE(f.bundle.reload_local_bundle("", f.id, &f.error));
const Preset* filament = f.bundle.filaments.find_preset("Calibration Filament", false, true);
REQUIRE(filament != nullptr);
REQUIRE_FALSE(is_compatible_with_printer(f.bundle.filaments.get_preset_with_vendor_profile(*filament),
f.bundle.printers.get_preset_with_vendor_profile(*f.bundle.calibrate_printer)));
CHECK(f.bundle.calibrate_filaments.count(filament) == 0);
}
TEST_CASE("Local reload rolls back prepared collection growth on a late allocation failure", "[Preset][Bundle][Regression]")
{
LocalReloadFixture f;
const size_t original_print_count = f.bundle.prints.size();
bool failed = false;
Preset& external = add_inmemory_preset(f.bundle.printers, "zzzz External Printer");
external.config.set_key_value("machine_start_gcode", new ReloadEdit([&] {
if (f.bundle.prints.size() > original_print_count) {
failed = true;
throw std::bad_alloc();
}
}));
const PresetBundle before(f.bundle);
f.change_sources();
write_print_preset_with_layer_height(f.bundle.prints.default_preset().config, f.directory / PRESET_PRINT_NAME / "Added Print.json",
"Added Print", 0.25);
CHECK_FALSE(f.bundle.reload_local_bundle("", f.id, &f.error));
REQUIRE(failed);
check_reload_state(f.bundle, before);
CHECK(f.bundle.bundles.m_bundles.at(f.id).bundle_type == BundleType::Local);
}
namespace {
const std::string kTextureBytes("\x89PNG\x00tex", 8);
@@ -7492,3 +8213,158 @@ TEST_CASE("Migrating the selected user filament keeps a printer list edit pendin
// What marks the project as having unsaved preset changes.
CHECK(filaments.saved_is_dirty() == expected_dirty);
}
namespace {
// The settings a file sliced on another machine carries: print, printer and filament presets this
// bundle does not have installed.
DynamicPrintConfig sender_config(size_t filament_count)
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.opt<ConfigOptionFloats>("filament_diameter")->values.assign(filament_count, 1.75);
config.opt<ConfigOptionStrings>("filament_colour")->values.assign(filament_count, "#FF0000");
Preset::normalize(config);
config.opt_string("print_settings_id", true) = "Sender Process";
config.opt_string("printer_settings_id", true) = "Sender Printer";
std::vector<std::string> &filaments = config.option<ConfigOptionStrings>("filament_settings_id", true)->values;
for (size_t i = 0; i < filament_count; ++i)
filaments.push_back("Sender Filament " + std::to_string(i + 1));
return config;
}
size_t project_preset_count(const PresetBundle &bundle)
{
size_t count = 0;
for (const PresetCollection *presets : std::initializer_list<const PresetCollection *>{ &bundle.prints, &bundle.filaments, &bundle.printers })
count += std::count_if(presets->begin(), presets->end(), [](const Preset &preset) { return preset.is_project_embedded; });
return count;
}
size_t files_under(const fs::path &dir)
{
size_t count = 0;
for (const fs::directory_entry &entry : fs::recursive_directory_iterator(dir))
count += fs::is_regular_file(entry.path());
return count;
}
} // namespace
TEST_CASE("Presets a G-code names but the receiver lacks load as temporary project presets", "[Preset][Bundle]")
{
// A G-code's settings select presets the way a .gcode.3mf's do, without adding to the user's library.
const std::string source = GENERATE("part.gcode", "part.gcode.3mf");
CAPTURE(source);
ScopedTemporaryDir dir;
ScopedDataDir data(dir.path());
PresetBundle bundle;
bundle.update_user_presets_directory(DEFAULT_USER_FOLDER_NAME);
bundle.load_config_model(source, sender_config(1));
for (const PresetCollection *presets : std::initializer_list<const PresetCollection *>{ &bundle.prints, &bundle.filaments, &bundle.printers }) {
CAPTURE(presets->get_selected_preset_name());
CHECK(presets->get_selected_preset().is_project_embedded);
}
CHECK(files_under(dir.path()) == 0);
bundle.reset_project_embedded_presets();
CHECK(project_preset_count(bundle) == 0);
}
TEST_CASE("Restoring a selection undoes an import that failed part-way", "[Preset][Bundle]")
{
ScopedTemporaryDir dir;
ScopedDataDir data(dir.path());
PresetBundle bundle;
bundle.update_user_presets_directory(DEFAULT_USER_FOLDER_NAME);
add_inmemory_preset(bundle.prints, "My Process");
add_inmemory_preset(bundle.prints, "Sender Process");
// The file names an installed but hidden process, which the import makes visible.
bundle.prints.find_preset("Sender Process", false, true)->is_visible = false;
REQUIRE(bundle.prints.select_preset_by_name("My Process", true));
// An unsaved edit that the saved baseline already holds, as new_project leaves it.
bundle.prints.get_edited_preset().config.opt_float("layer_height") = 0.12;
bundle.prints.update_dirty();
bundle.prints.update_saved_preset_from_current_preset();
REQUIRE(bundle.prints.get_edited_preset().is_dirty);
REQUIRE_FALSE(bundle.prints.saved_is_dirty());
const std::string printer_before = bundle.printers.get_selected_preset_name();
const std::vector<std::string> filaments_before = bundle.filament_presets;
const DynamicPrintConfig project_before = bundle.project_config;
const std::optional<PresetBundle::Selection> previous = bundle.selection();
REQUIRE(previous);
// Two filaments, one with a per-filament list the filament split cannot read: the load throws after
// the print and printer presets are selected.
DynamicPrintConfig config = sender_config(2);
config.option<ConfigOptionStrings>("filament_dev_ams_drying_ams_limitations", true)->values.clear();
REQUIRE_THROWS_AS(bundle.load_config_model("part.gcode", std::move(config)), ConfigurationError);
REQUIRE(bundle.prints.get_selected_preset_name() == "Sender Process");
bundle.restore_selection(*previous);
CHECK(bundle.prints.get_selected_preset_name() == "My Process");
CHECK_THAT(bundle.prints.get_edited_preset().config.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.12, 1e-9));
CHECK(bundle.prints.get_edited_preset().is_dirty);
CHECK_FALSE(bundle.prints.saved_is_dirty());
CHECK_FALSE(bundle.prints.find_preset("Sender Process", false, true)->is_visible);
CHECK(bundle.printers.get_selected_preset_name() == printer_before);
CHECK_FALSE(bundle.printers.saved_is_dirty());
CHECK(bundle.filament_presets == filaments_before);
CHECK(bundle.project_config == project_before);
CHECK(project_preset_count(bundle) == 0);
}
TEST_CASE("A selection is not taken while project presets are loaded", "[Preset][Bundle]")
{
// Restoring drops every project-embedded preset, which would lose those of the project loaded now.
ScopedTemporaryDir dir;
ScopedDataDir data(dir.path());
PresetBundle bundle;
bundle.update_user_presets_directory(DEFAULT_USER_FOLDER_NAME);
bundle.load_config_model("project.3mf", sender_config(1));
REQUIRE(project_preset_count(bundle) > 0);
CHECK_FALSE(bundle.selection());
}
TEST_CASE("Selections reach the app config only while no export blocker lives", "[Preset][Bundle]")
{
PresetBundle bundle;
AppConfig app_config;
{
PresetBundle::SelectionExportBlocker blocker(bundle);
bundle.export_selections(app_config);
CHECK(app_config.get("presets", PRESET_PRINTER_NAME).empty());
}
bundle.export_selections(app_config);
CHECK(app_config.get("presets", PRESET_PRINTER_NAME) == bundle.printers.get_selected_preset_name());
}
TEST_CASE("An empty choice list in a G-code's settings keeps the value it had before the file was read", "[Preset][Bundle]")
{
// A G-code written by a tool rather than from presets can leave nozzle_volume_type empty, and the settings
// tabs index it per extruder. A blank text list is a value of its own and stays blank.
const DynamicPrintConfig block = DynamicPrintConfig::full_print_config();
ScopedTemporaryFile file(".gcode");
{
std::ofstream os(file.string());
os << "; generated by OrcaSlicer\n; CONFIG_BLOCK_START\n";
for (const std::string &key : block.keys())
if (key != "nozzle_volume_type" && key != "filament_multi_colour")
os << "; " << key << " = " << block.opt_serialize(key) << '\n';
os << "; nozzle_volume_type = \n; filament_multi_colour = \n; CONFIG_BLOCK_END\n";
REQUIRE(os.good());
}
PresetBundle bundle;
bundle.project_config.set_deserialize_strict("nozzle_volume_type", "High Flow");
bundle.project_config.option<ConfigOptionStrings>("filament_multi_colour", true)->values = { "#FF0000 #0000FF" };
ConfigSubstitutions substitutions;
const DynamicPrintConfig config = bundle.config_from_gcode(file.string(), ForwardCompatibilitySubstitutionRule::Enable, substitutions);
CHECK(config.opt_serialize("nozzle_volume_type") == "High Flow");
CHECK(config.option<ConfigOptionStrings>("filament_multi_colour")->values.empty());
}
@@ -56,6 +56,14 @@ TEST_CASE("Plugin host API exposes host-owned bundle and preset surface to Pytho
REQUIRE(has_attr(host, "Model"));
REQUIRE(has_attr(host, "ModelObject"));
REQUIRE(has_attr(host, "Plater"));
REQUIRE(has_attr(host, "preset"));
py::object preset = host.attr("preset");
REQUIRE(has_attr(preset, "bundles"));
py::object bundles = preset.attr("bundles");
CHECK(has_attr(bundles, "list_ids"));
CHECK(has_attr(bundles, "reload_local"));
CHECK_FALSE(has_attr(host, "reload_local_bundle"));
CHECK_FALSE(has_attr(host, "list_bundle_ids"));
py::object preset_bundle_type = host.attr("PresetBundle");
CHECK(has_attr(preset_bundle_type, "prints"));
@@ -132,6 +140,7 @@ TEST_CASE("Plugin host API exposes host-owned bundle and preset surface to Pytho
TEST_CASE("Plugin host API reports unavailable GUI objects before Orca app initialization", "[PluginHost][Python]")
{
py::object host = import_orca_module().attr("host");
py::object bundles = host.attr("preset").attr("bundles");
for (const char* function_name : { "preset_bundle", "plater", "model", "app_info", "selected_printer" }) {
CAPTURE(function_name);
@@ -143,6 +152,22 @@ TEST_CASE("Plugin host API reports unavailable GUI objects before Orca app initi
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
}
}
try {
bundles.attr("reload_local")("test-bundle");
FAIL("host.preset.bundles.reload_local unexpectedly succeeded without a wx application");
} catch (const py::error_already_set& error) {
CHECK(error.matches(PyExc_RuntimeError));
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
}
try {
bundles.attr("list_ids")();
FAIL("host.preset.bundles.list_ids unexpectedly succeeded without a wx application");
} catch (const py::error_already_set& error) {
CHECK(error.matches(PyExc_RuntimeError));
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
}
}
TEST_CASE("Plugin host API reports no GL details before the 3D view exists", "[PluginHost][Python]")