Spaces:
Running
Running
Refine G-code startup and preview refresh
Browse files- app.py +44 -10
- tests/test_tiff_to_gcode.py +41 -2
- tiff_to_gcode.py +12 -4
app.py
CHANGED
|
@@ -2928,6 +2928,22 @@ def shift_selected_slice(records: list[dict] | None, selected: str | None, index
|
|
| 2928 |
return gr.update(value=next_index), label, preview
|
| 2929 |
|
| 2930 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2931 |
def generate_dynamic_stacks(
|
| 2932 |
records: list[dict] | None,
|
| 2933 |
settings_table: Any,
|
|
@@ -2989,20 +3005,12 @@ def generate_dynamic_stacks(
|
|
| 2989 |
messages.append("Reference TIFF Stack: updated automatically.")
|
| 2990 |
else:
|
| 2991 |
messages.append("Reference TIFF Stack: skipped (no generated shape slices available).")
|
| 2992 |
-
|
| 2993 |
-
label, preview = _render_selected_slice(first_state, 0)
|
| 2994 |
-
slider = gr.update(
|
| 2995 |
-
minimum=0,
|
| 2996 |
-
maximum=max(0, len(first_state.get("tiff_paths", [])) - 1),
|
| 2997 |
-
value=0,
|
| 2998 |
-
step=1,
|
| 2999 |
-
interactive=len(first_state.get("tiff_paths", [])) > 1,
|
| 3000 |
-
)
|
| 3001 |
return (
|
| 3002 |
records,
|
| 3003 |
[record.get("zip_path") for record in records if record.get("zip_path")],
|
| 3004 |
"\n".join(messages),
|
| 3005 |
-
|
| 3006 |
slider,
|
| 3007 |
label,
|
| 3008 |
preview,
|
|
@@ -3089,6 +3097,10 @@ def split_selected_shape_for_grid(
|
|
| 3089 |
records,
|
| 3090 |
_shape_settings_rows(records),
|
| 3091 |
_spacing_table_update(records, existing_spacing, use_individual_spacing),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3092 |
[],
|
| 3093 |
[],
|
| 3094 |
_gcode_dropdown_update(records),
|
|
@@ -3123,6 +3135,10 @@ def split_selected_shape_for_grid(
|
|
| 3123 |
records,
|
| 3124 |
_shape_settings_rows(records),
|
| 3125 |
_spacing_table_update(records, existing_spacing, use_individual_spacing),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3126 |
[record.get("zip_path") for record in records if record.get("zip_path")],
|
| 3127 |
[record.get("gcode_path") for record in records if record.get("gcode_path")],
|
| 3128 |
_gcode_dropdown_update(records),
|
|
@@ -3168,6 +3184,8 @@ def split_selected_shape_for_grid(
|
|
| 3168 |
})
|
| 3169 |
split_records.append(piece_record)
|
| 3170 |
next_records = _reindex_shape_records([*records[:pos], *split_records, *records[pos + 1:]])
|
|
|
|
|
|
|
| 3171 |
slider, label, preview = preview_selected_split_piece(pieces, None)
|
| 3172 |
status = (
|
| 3173 |
f"Split Shape {source.get('idx', pos + 1)} into {len(pieces)} print-ready stacks "
|
|
@@ -3180,6 +3198,10 @@ def split_selected_shape_for_grid(
|
|
| 3180 |
next_records,
|
| 3181 |
_shape_settings_rows(next_records),
|
| 3182 |
_spacing_table_update(next_records, existing_spacing, use_individual_spacing),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3183 |
[record.get("zip_path") for record in next_records if record.get("zip_path")],
|
| 3184 |
[record.get("gcode_path") for record in next_records if record.get("gcode_path")],
|
| 3185 |
_gcode_dropdown_update(next_records),
|
|
@@ -3219,6 +3241,7 @@ def generate_dynamic_gcode(
|
|
| 3219 |
all_g1: bool,
|
| 3220 |
use_reference_motion: bool,
|
| 3221 |
raster_pattern: str | None,
|
|
|
|
| 3222 |
lead_in_enabled: bool,
|
| 3223 |
lead_in_length: float,
|
| 3224 |
lead_in_clearance: float,
|
|
@@ -3252,6 +3275,7 @@ def generate_dynamic_gcode(
|
|
| 3252 |
port=int(record.get("port", 1)),
|
| 3253 |
layer_height=float(layer_height),
|
| 3254 |
fil_width=float(pixel_size),
|
|
|
|
| 3255 |
all_g1=bool(all_g1),
|
| 3256 |
motion_tiffs=motion_tiffs,
|
| 3257 |
raster_pattern=raster_pattern,
|
|
@@ -3653,6 +3677,7 @@ def build_dynamic_demo() -> gr.Blocks:
|
|
| 3653 |
value=True,
|
| 3654 |
)
|
| 3655 |
gcode_all_g1 = gr.Checkbox(label="Move at one constant speed (no fast travel moves)", value=True)
|
|
|
|
| 3656 |
gcode_raster_pattern = gr.Dropdown(
|
| 3657 |
label="Raster Pattern",
|
| 3658 |
choices=list(RASTER_PATTERN_CHOICES),
|
|
@@ -3942,6 +3967,10 @@ def build_dynamic_demo() -> gr.Blocks:
|
|
| 3942 |
shape_records,
|
| 3943 |
shape_settings,
|
| 3944 |
nozzle_spacing_table,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3945 |
tiff_downloads,
|
| 3946 |
gcode_downloads,
|
| 3947 |
gcode_text_source,
|
|
@@ -3978,6 +4007,7 @@ def build_dynamic_demo() -> gr.Blocks:
|
|
| 3978 |
gcode_all_g1,
|
| 3979 |
gcode_use_ref_motion,
|
| 3980 |
gcode_raster_pattern,
|
|
|
|
| 3981 |
gcode_lead_in_enabled,
|
| 3982 |
gcode_lead_in_length,
|
| 3983 |
gcode_lead_in_clearance,
|
|
@@ -3987,6 +4017,10 @@ def build_dynamic_demo() -> gr.Blocks:
|
|
| 3987 |
pixel_size,
|
| 3988 |
],
|
| 3989 |
outputs=[shape_records, gcode_downloads, gcode_status, gcode_text_source, gcode_source],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3990 |
)
|
| 3991 |
gcode_text_source.change(fn=load_selected_gcode_text, inputs=[shape_records, gcode_text_source], outputs=[gcode_text])
|
| 3992 |
refresh_gcode_text_button.click(fn=load_selected_gcode_text, inputs=[shape_records, gcode_text_source], outputs=[gcode_text])
|
|
|
|
| 2928 |
return gr.update(value=next_index), label, preview
|
| 2929 |
|
| 2930 |
|
| 2931 |
+
def _tiff_preview_update(records: list[dict], selected: str | None = None) -> tuple[dict[str, Any], dict[str, Any], str, Image.Image | None]:
|
| 2932 |
+
dropdown = _dropdown_update(records, selected)
|
| 2933 |
+
selected_value = dropdown.get("value") if isinstance(dropdown, dict) else selected
|
| 2934 |
+
pos = _selected_record_index(records, selected_value)
|
| 2935 |
+
state = (records[pos].get("tiff_state") if pos >= 0 else None) or _empty_state()
|
| 2936 |
+
label, preview = _render_selected_slice(state, 0)
|
| 2937 |
+
slider = gr.update(
|
| 2938 |
+
minimum=0,
|
| 2939 |
+
maximum=max(0, len(state.get("tiff_paths", [])) - 1),
|
| 2940 |
+
value=0,
|
| 2941 |
+
step=1,
|
| 2942 |
+
interactive=len(state.get("tiff_paths", [])) > 1,
|
| 2943 |
+
)
|
| 2944 |
+
return dropdown, slider, label, preview
|
| 2945 |
+
|
| 2946 |
+
|
| 2947 |
def generate_dynamic_stacks(
|
| 2948 |
records: list[dict] | None,
|
| 2949 |
settings_table: Any,
|
|
|
|
| 3005 |
messages.append("Reference TIFF Stack: updated automatically.")
|
| 3006 |
else:
|
| 3007 |
messages.append("Reference TIFF Stack: skipped (no generated shape slices available).")
|
| 3008 |
+
selected_update, slider, label, preview = _tiff_preview_update(records)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3009 |
return (
|
| 3010 |
records,
|
| 3011 |
[record.get("zip_path") for record in records if record.get("zip_path")],
|
| 3012 |
"\n".join(messages),
|
| 3013 |
+
selected_update,
|
| 3014 |
slider,
|
| 3015 |
label,
|
| 3016 |
preview,
|
|
|
|
| 3097 |
records,
|
| 3098 |
_shape_settings_rows(records),
|
| 3099 |
_spacing_table_update(records, existing_spacing, use_individual_spacing),
|
| 3100 |
+
_dropdown_update(records),
|
| 3101 |
+
_reset_slider(),
|
| 3102 |
+
"No slice stack loaded yet.",
|
| 3103 |
+
None,
|
| 3104 |
[],
|
| 3105 |
[],
|
| 3106 |
_gcode_dropdown_update(records),
|
|
|
|
| 3135 |
records,
|
| 3136 |
_shape_settings_rows(records),
|
| 3137 |
_spacing_table_update(records, existing_spacing, use_individual_spacing),
|
| 3138 |
+
_dropdown_update(records, selected),
|
| 3139 |
+
_reset_slider(),
|
| 3140 |
+
"No slice stack loaded yet.",
|
| 3141 |
+
None,
|
| 3142 |
[record.get("zip_path") for record in records if record.get("zip_path")],
|
| 3143 |
[record.get("gcode_path") for record in records if record.get("gcode_path")],
|
| 3144 |
_gcode_dropdown_update(records),
|
|
|
|
| 3184 |
})
|
| 3185 |
split_records.append(piece_record)
|
| 3186 |
next_records = _reindex_shape_records([*records[:pos], *split_records, *records[pos + 1:]])
|
| 3187 |
+
split_selected = _shape_choice(next_records[pos]) if pos < len(next_records) else None
|
| 3188 |
+
selected_update, main_slider, main_label, main_preview = _tiff_preview_update(next_records, split_selected)
|
| 3189 |
slider, label, preview = preview_selected_split_piece(pieces, None)
|
| 3190 |
status = (
|
| 3191 |
f"Split Shape {source.get('idx', pos + 1)} into {len(pieces)} print-ready stacks "
|
|
|
|
| 3198 |
next_records,
|
| 3199 |
_shape_settings_rows(next_records),
|
| 3200 |
_spacing_table_update(next_records, existing_spacing, use_individual_spacing),
|
| 3201 |
+
selected_update,
|
| 3202 |
+
main_slider,
|
| 3203 |
+
main_label,
|
| 3204 |
+
main_preview,
|
| 3205 |
[record.get("zip_path") for record in next_records if record.get("zip_path")],
|
| 3206 |
[record.get("gcode_path") for record in next_records if record.get("gcode_path")],
|
| 3207 |
_gcode_dropdown_update(next_records),
|
|
|
|
| 3241 |
all_g1: bool,
|
| 3242 |
use_reference_motion: bool,
|
| 3243 |
raster_pattern: str | None,
|
| 3244 |
+
pressure_ramp_enabled: bool,
|
| 3245 |
lead_in_enabled: bool,
|
| 3246 |
lead_in_length: float,
|
| 3247 |
lead_in_clearance: float,
|
|
|
|
| 3275 |
port=int(record.get("port", 1)),
|
| 3276 |
layer_height=float(layer_height),
|
| 3277 |
fil_width=float(pixel_size),
|
| 3278 |
+
pressure_ramp_enabled=bool(pressure_ramp_enabled),
|
| 3279 |
all_g1=bool(all_g1),
|
| 3280 |
motion_tiffs=motion_tiffs,
|
| 3281 |
raster_pattern=raster_pattern,
|
|
|
|
| 3677 |
value=True,
|
| 3678 |
)
|
| 3679 |
gcode_all_g1 = gr.Checkbox(label="Move at one constant speed (no fast travel moves)", value=True)
|
| 3680 |
+
gcode_pressure_ramp_enabled = gr.Checkbox(label="Increase Pressure Each Layer", value=True)
|
| 3681 |
gcode_raster_pattern = gr.Dropdown(
|
| 3682 |
label="Raster Pattern",
|
| 3683 |
choices=list(RASTER_PATTERN_CHOICES),
|
|
|
|
| 3967 |
shape_records,
|
| 3968 |
shape_settings,
|
| 3969 |
nozzle_spacing_table,
|
| 3970 |
+
selected_shape,
|
| 3971 |
+
slice_slider,
|
| 3972 |
+
slice_label,
|
| 3973 |
+
slice_preview,
|
| 3974 |
tiff_downloads,
|
| 3975 |
gcode_downloads,
|
| 3976 |
gcode_text_source,
|
|
|
|
| 4007 |
gcode_all_g1,
|
| 4008 |
gcode_use_ref_motion,
|
| 4009 |
gcode_raster_pattern,
|
| 4010 |
+
gcode_pressure_ramp_enabled,
|
| 4011 |
gcode_lead_in_enabled,
|
| 4012 |
gcode_lead_in_length,
|
| 4013 |
gcode_lead_in_clearance,
|
|
|
|
| 4017 |
pixel_size,
|
| 4018 |
],
|
| 4019 |
outputs=[shape_records, gcode_downloads, gcode_status, gcode_text_source, gcode_source],
|
| 4020 |
+
).then(
|
| 4021 |
+
fn=load_selected_gcode_text,
|
| 4022 |
+
inputs=[shape_records, gcode_text_source],
|
| 4023 |
+
outputs=[gcode_text],
|
| 4024 |
)
|
| 4025 |
gcode_text_source.change(fn=load_selected_gcode_text, inputs=[shape_records, gcode_text_source], outputs=[gcode_text])
|
| 4026 |
refresh_gcode_text_button.click(fn=load_selected_gcode_text, inputs=[shape_records, gcode_text_source], outputs=[gcode_text])
|
tests/test_tiff_to_gcode.py
CHANGED
|
@@ -69,6 +69,10 @@ def _moves_with_colors(gcode_text: str) -> list[dict]:
|
|
| 69 |
return moves
|
| 70 |
|
| 71 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
def test_trace_mask_contours_uses_tiff_pixel_border_edges() -> None:
|
| 73 |
contours = _trace_mask_contours(
|
| 74 |
np.array(
|
|
@@ -535,8 +539,8 @@ def test_gcode_header_writes_presets_before_initial_aux_commands(tmp_path) -> No
|
|
| 535 |
|
| 536 |
assert lines[0] == "G91"
|
| 537 |
assert lines[1] == "{aux_command}WAGO_ValveCommands(7, 0)"
|
| 538 |
-
assert lines[2]
|
| 539 |
-
assert lines[3]
|
| 540 |
assert lines[4].startswith("{aux_command}WAGO_ValveCommands(")
|
| 541 |
assert lines[5].startswith("{aux_command}WAGO_ValveCommands(")
|
| 542 |
|
|
@@ -585,6 +589,41 @@ def test_gcode_lead_in_runs_once_before_first_layer(tmp_path) -> None:
|
|
| 585 |
assert not any(move["start"][0] < -3.0 or move["end"][0] < -3.0 for move in moves[first_z_index:])
|
| 586 |
|
| 587 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
def test_gcode_uses_g1_for_print_and_g0_for_travel(tmp_path) -> None:
|
| 589 |
tiff_path = tmp_path / "slice_0000.tif"
|
| 590 |
Image.new("L", (1, 1), 0).save(tiff_path)
|
|
|
|
| 69 |
return moves
|
| 70 |
|
| 71 |
|
| 72 |
+
def _pressure_set_count(gcode_text: str) -> int:
|
| 73 |
+
return gcode_text.count("\\x30\\x38\\x50\\x53") + gcode_text.count("setpress(")
|
| 74 |
+
|
| 75 |
+
|
| 76 |
def test_trace_mask_contours_uses_tiff_pixel_border_edges() -> None:
|
| 77 |
contours = _trace_mask_contours(
|
| 78 |
np.array(
|
|
|
|
| 539 |
|
| 540 |
assert lines[0] == "G91"
|
| 541 |
assert lines[1] == "{aux_command}WAGO_ValveCommands(7, 0)"
|
| 542 |
+
assert lines[2] == "serialPort3.write(eval(setpress(25)))"
|
| 543 |
+
assert lines[3] == "serialPort3.write(eval(togglepress()))"
|
| 544 |
assert lines[4].startswith("{aux_command}WAGO_ValveCommands(")
|
| 545 |
assert lines[5].startswith("{aux_command}WAGO_ValveCommands(")
|
| 546 |
|
|
|
|
| 589 |
assert not any(move["start"][0] < -3.0 or move["end"][0] < -3.0 for move in moves[first_z_index:])
|
| 590 |
|
| 591 |
|
| 592 |
+
def test_gcode_pressure_ramp_can_be_disabled(tmp_path) -> None:
|
| 593 |
+
tiff_paths = []
|
| 594 |
+
for index in range(2):
|
| 595 |
+
tiff_path = tmp_path / f"slice_{index:04d}.tif"
|
| 596 |
+
Image.new("L", (1, 1), 0).save(tiff_path)
|
| 597 |
+
tiff_paths.append(tiff_path)
|
| 598 |
+
|
| 599 |
+
zip_path = tmp_path / "slices.zip"
|
| 600 |
+
with zipfile.ZipFile(zip_path, mode="w") as archive:
|
| 601 |
+
for tiff_path in tiff_paths:
|
| 602 |
+
archive.write(tiff_path, arcname=tiff_path.name)
|
| 603 |
+
|
| 604 |
+
ramped_path = generate_snake_path_gcode(
|
| 605 |
+
zip_path,
|
| 606 |
+
shape_name="pressure_ramped",
|
| 607 |
+
pressure=25,
|
| 608 |
+
valve=7,
|
| 609 |
+
port=3,
|
| 610 |
+
layer_height=1.0,
|
| 611 |
+
pressure_ramp_enabled=True,
|
| 612 |
+
)
|
| 613 |
+
fixed_path = generate_snake_path_gcode(
|
| 614 |
+
zip_path,
|
| 615 |
+
shape_name="pressure_fixed",
|
| 616 |
+
pressure=25,
|
| 617 |
+
valve=7,
|
| 618 |
+
port=3,
|
| 619 |
+
layer_height=1.0,
|
| 620 |
+
pressure_ramp_enabled=False,
|
| 621 |
+
)
|
| 622 |
+
|
| 623 |
+
assert _pressure_set_count(ramped_path.read_text()) > 1
|
| 624 |
+
assert _pressure_set_count(fixed_path.read_text()) == 1
|
| 625 |
+
|
| 626 |
+
|
| 627 |
def test_gcode_uses_g1_for_print_and_g0_for_travel(tmp_path) -> None:
|
| 628 |
tiff_path = tmp_path / "slice_0000.tif"
|
| 629 |
Image.new("L", (1, 1), 0).save(tiff_path)
|
tiff_to_gcode.py
CHANGED
|
@@ -66,12 +66,16 @@ def _togglepress() -> str:
|
|
| 66 |
|
| 67 |
|
| 68 |
def _setpress_cmd(port: str, pressure: float, start: bool) -> str:
|
| 69 |
-
|
|
|
|
|
|
|
| 70 |
return f"\n\r{insert}{port}.write({_setpress(pressure)})"
|
| 71 |
|
| 72 |
|
| 73 |
def _toggle_cmd(port: str, start: bool) -> str:
|
| 74 |
-
|
|
|
|
|
|
|
| 75 |
return f"\n\r{insert}{port}.write({_togglepress()})"
|
| 76 |
|
| 77 |
|
|
@@ -1050,6 +1054,7 @@ def generate_snake_path_gcode(
|
|
| 1050 |
fil_width: float = 0.8,
|
| 1051 |
invert: bool = True,
|
| 1052 |
increase_pressure_per_layer: float = 0.1,
|
|
|
|
| 1053 |
all_g1: bool = False,
|
| 1054 |
motion_tiffs: list[str] | None = None,
|
| 1055 |
raster_pattern: str | None = RASTER_PATTERN_SAME_DIRECTION,
|
|
@@ -1301,8 +1306,11 @@ def generate_snake_path_gcode(
|
|
| 1301 |
f"{move_type} X{move['X']} Y{move['Y']} Z{move['Z']} "
|
| 1302 |
f"; Color {move['Color']}"
|
| 1303 |
)
|
| 1304 |
-
|
| 1305 |
-
|
|
|
|
|
|
|
|
|
|
| 1306 |
else:
|
| 1307 |
line = (
|
| 1308 |
f"{move_type} X{move['X']} Y{move['Y']} ; Color {move['Color']}"
|
|
|
|
| 66 |
|
| 67 |
|
| 68 |
def _setpress_cmd(port: str, pressure: float, start: bool) -> str:
|
| 69 |
+
if start:
|
| 70 |
+
return f"\n\r{port}.write(eval(setpress({pressure:g})))"
|
| 71 |
+
insert = ""
|
| 72 |
return f"\n\r{insert}{port}.write({_setpress(pressure)})"
|
| 73 |
|
| 74 |
|
| 75 |
def _toggle_cmd(port: str, start: bool) -> str:
|
| 76 |
+
if start:
|
| 77 |
+
return f"\n\r{port}.write(eval(togglepress()))"
|
| 78 |
+
insert = ""
|
| 79 |
return f"\n\r{insert}{port}.write({_togglepress()})"
|
| 80 |
|
| 81 |
|
|
|
|
| 1054 |
fil_width: float = 0.8,
|
| 1055 |
invert: bool = True,
|
| 1056 |
increase_pressure_per_layer: float = 0.1,
|
| 1057 |
+
pressure_ramp_enabled: bool = True,
|
| 1058 |
all_g1: bool = False,
|
| 1059 |
motion_tiffs: list[str] | None = None,
|
| 1060 |
raster_pattern: str | None = RASTER_PATTERN_SAME_DIRECTION,
|
|
|
|
| 1306 |
f"{move_type} X{move['X']} Y{move['Y']} Z{move['Z']} "
|
| 1307 |
f"; Color {move['Color']}"
|
| 1308 |
)
|
| 1309 |
+
if pressure_ramp_enabled:
|
| 1310 |
+
pressure_cur += increase_pressure_per_layer
|
| 1311 |
+
pressure_next = _setpress_cmd(com_port, pressure_cur, start=False)
|
| 1312 |
+
else:
|
| 1313 |
+
pressure_next = None
|
| 1314 |
else:
|
| 1315 |
line = (
|
| 1316 |
f"{move_type} X{move['X']} Y{move['Y']} ; Color {move['Color']}"
|