Spaces:
Running
Running
Refine footprint raster starts and lead-in UI
Browse files- app.py +15 -4
- tests/test_tiff_to_gcode.py +38 -10
- tiff_to_gcode.py +79 -6
app.py
CHANGED
|
@@ -2641,6 +2641,10 @@ def update_nozzle_spacing_mode(layout_mode: str | None) -> tuple[dict[str, Any],
|
|
| 2641 |
return gr.update(visible=not custom_selected), gr.update(visible=custom_selected)
|
| 2642 |
|
| 2643 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2644 |
def _dropdown_update(records: list[dict], selected: str | None = None) -> dict[str, Any]:
|
| 2645 |
choices = [_shape_choice(record) for record in records]
|
| 2646 |
value = selected if selected in choices else (choices[0] if choices else None)
|
|
@@ -3685,10 +3689,11 @@ def build_dynamic_demo() -> gr.Blocks:
|
|
| 3685 |
allow_custom_value=False,
|
| 3686 |
)
|
| 3687 |
gcode_lead_in_enabled = gr.Checkbox(label="Lead In", value=False)
|
| 3688 |
-
with gr.
|
| 3689 |
-
|
| 3690 |
-
|
| 3691 |
-
|
|
|
|
| 3692 |
gcode_button = gr.Button("Generate G-Code", variant="primary")
|
| 3693 |
gcode_downloads = gr.File(label="Download G-Code Files", file_count="multiple", interactive=False, elem_classes=["gcode-download"])
|
| 3694 |
gcode_status = gr.Markdown("")
|
|
@@ -4022,6 +4027,12 @@ def build_dynamic_demo() -> gr.Blocks:
|
|
| 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])
|
| 4027 |
nozzle_layout_mode.change(
|
|
|
|
| 2641 |
return gr.update(visible=not custom_selected), gr.update(visible=custom_selected)
|
| 2642 |
|
| 2643 |
|
| 2644 |
+
def update_lead_in_options_visibility(enabled: bool | None) -> dict[str, Any]:
|
| 2645 |
+
return gr.update(visible=bool(enabled))
|
| 2646 |
+
|
| 2647 |
+
|
| 2648 |
def _dropdown_update(records: list[dict], selected: str | None = None) -> dict[str, Any]:
|
| 2649 |
choices = [_shape_choice(record) for record in records]
|
| 2650 |
value = selected if selected in choices else (choices[0] if choices else None)
|
|
|
|
| 3689 |
allow_custom_value=False,
|
| 3690 |
)
|
| 3691 |
gcode_lead_in_enabled = gr.Checkbox(label="Lead In", value=False)
|
| 3692 |
+
with gr.Group(visible=False) as gcode_lead_in_options_group:
|
| 3693 |
+
with gr.Row():
|
| 3694 |
+
gcode_lead_in_length = gr.Number(label="Lead In Length (mm)", value=5.0, minimum=0.1, step=0.1)
|
| 3695 |
+
gcode_lead_in_clearance = gr.Number(label="Lead In Clearance (mm)", value=5.0, minimum=0.0, step=0.1)
|
| 3696 |
+
gcode_lead_in_lines = gr.Number(label="Lead In Raster Lines", value=3, minimum=1, step=1)
|
| 3697 |
gcode_button = gr.Button("Generate G-Code", variant="primary")
|
| 3698 |
gcode_downloads = gr.File(label="Download G-Code Files", file_count="multiple", interactive=False, elem_classes=["gcode-download"])
|
| 3699 |
gcode_status = gr.Markdown("")
|
|
|
|
| 4027 |
inputs=[shape_records, gcode_text_source],
|
| 4028 |
outputs=[gcode_text],
|
| 4029 |
)
|
| 4030 |
+
gcode_lead_in_enabled.change(
|
| 4031 |
+
fn=update_lead_in_options_visibility,
|
| 4032 |
+
inputs=[gcode_lead_in_enabled],
|
| 4033 |
+
outputs=[gcode_lead_in_options_group],
|
| 4034 |
+
queue=False,
|
| 4035 |
+
)
|
| 4036 |
gcode_text_source.change(fn=load_selected_gcode_text, inputs=[shape_records, gcode_text_source], outputs=[gcode_text])
|
| 4037 |
refresh_gcode_text_button.click(fn=load_selected_gcode_text, inputs=[shape_records, gcode_text_source], outputs=[gcode_text])
|
| 4038 |
nozzle_layout_mode.change(
|
tests/test_tiff_to_gcode.py
CHANGED
|
@@ -673,15 +673,17 @@ def test_woodpile_raster_switches_print_axis_between_layers(tmp_path) -> None:
|
|
| 673 |
raster_pattern=RASTER_PATTERN_WOODPILE,
|
| 674 |
)
|
| 675 |
|
|
|
|
| 676 |
move_lines = [
|
| 677 |
line.strip()
|
| 678 |
-
for line in
|
| 679 |
if line.startswith(("G0", "G1"))
|
| 680 |
]
|
| 681 |
z_move_index = next(i for i, line in enumerate(move_lines) if " Z" in line)
|
| 682 |
first_layer_prints = [line for line in move_lines[:z_move_index] if line.startswith("G1") and "; Color 255" in line]
|
| 683 |
second_layer_prints = [line for line in move_lines[z_move_index + 1 :] if line.startswith("G1") and "; Color 255" in line]
|
| 684 |
|
|
|
|
| 685 |
assert any("X" in line and "Y0" in line for line in first_layer_prints)
|
| 686 |
assert any("X0" in line and "Y" in line for line in second_layer_prints)
|
| 687 |
|
|
@@ -696,10 +698,28 @@ def test_woodpile_raster_switches_print_axis_between_layers(tmp_path) -> None:
|
|
| 696 |
y += float(token[1:])
|
| 697 |
x_positions.append(x)
|
| 698 |
y_positions.append(y)
|
| 699 |
-
assert min(x_positions) ==
|
| 700 |
-
assert max(x_positions) ==
|
| 701 |
-
assert min(y_positions) == -1.
|
| 702 |
-
assert max(y_positions) ==
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 703 |
|
| 704 |
|
| 705 |
def test_y_direction_raster_prints_each_layer_along_y_axis(tmp_path) -> None:
|
|
@@ -724,9 +744,10 @@ def test_y_direction_raster_prints_each_layer_along_y_axis(tmp_path) -> None:
|
|
| 724 |
raster_pattern=RASTER_PATTERN_Y_DIRECTION,
|
| 725 |
)
|
| 726 |
|
|
|
|
| 727 |
move_lines = [
|
| 728 |
line.strip()
|
| 729 |
-
for line in
|
| 730 |
if line.startswith(("G0", "G1"))
|
| 731 |
]
|
| 732 |
print_lines = [
|
|
@@ -735,6 +756,7 @@ def test_y_direction_raster_prints_each_layer_along_y_axis(tmp_path) -> None:
|
|
| 735 |
if line.startswith("G1") and "; Color 255" in line
|
| 736 |
]
|
| 737 |
assert print_lines
|
|
|
|
| 738 |
assert all("X0" in line and "Y0" not in line for line in print_lines)
|
| 739 |
|
| 740 |
x = y = 0.0
|
|
@@ -748,10 +770,16 @@ def test_y_direction_raster_prints_each_layer_along_y_axis(tmp_path) -> None:
|
|
| 748 |
y += float(token[1:])
|
| 749 |
x_positions.append(x)
|
| 750 |
y_positions.append(y)
|
| 751 |
-
assert min(x_positions)
|
| 752 |
-
assert max(x_positions)
|
| 753 |
-
assert min(y_positions) ==
|
| 754 |
-
assert max(y_positions) ==
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 755 |
|
| 756 |
|
| 757 |
def test_contour_tracing_skips_inactive_nozzle_outline(tmp_path) -> None:
|
|
|
|
| 673 |
raster_pattern=RASTER_PATTERN_WOODPILE,
|
| 674 |
)
|
| 675 |
|
| 676 |
+
gcode_text = gcode_path.read_text()
|
| 677 |
move_lines = [
|
| 678 |
line.strip()
|
| 679 |
+
for line in gcode_text.splitlines()
|
| 680 |
if line.startswith(("G0", "G1"))
|
| 681 |
]
|
| 682 |
z_move_index = next(i for i, line in enumerate(move_lines) if " Z" in line)
|
| 683 |
first_layer_prints = [line for line in move_lines[:z_move_index] if line.startswith("G1") and "; Color 255" in line]
|
| 684 |
second_layer_prints = [line for line in move_lines[z_move_index + 1 :] if line.startswith("G1") and "; Color 255" in line]
|
| 685 |
|
| 686 |
+
assert move_lines[0] == "G0 X1.0 Y0.0 ; Color 0"
|
| 687 |
assert any("X" in line and "Y0" in line for line in first_layer_prints)
|
| 688 |
assert any("X0" in line and "Y" in line for line in second_layer_prints)
|
| 689 |
|
|
|
|
| 698 |
y += float(token[1:])
|
| 699 |
x_positions.append(x)
|
| 700 |
y_positions.append(y)
|
| 701 |
+
assert min(x_positions) == 0.0
|
| 702 |
+
assert max(x_positions) == 5.0
|
| 703 |
+
assert min(y_positions) == -1.5
|
| 704 |
+
assert max(y_positions) == 2.5
|
| 705 |
+
|
| 706 |
+
moves = _moves_with_colors(gcode_text)
|
| 707 |
+
first_layer_change = next(
|
| 708 |
+
move for move in moves if move["end"][2] > move["start"][2]
|
| 709 |
+
)
|
| 710 |
+
previous_xy = first_layer_change["start"][:2]
|
| 711 |
+
actual_restart_xy = first_layer_change["end"][:2]
|
| 712 |
+
old_restart_xy = (1.5, -1.5)
|
| 713 |
+
actual_restart_distance = (
|
| 714 |
+
(previous_xy[0] - actual_restart_xy[0]) ** 2
|
| 715 |
+
+ (previous_xy[1] - actual_restart_xy[1]) ** 2
|
| 716 |
+
)
|
| 717 |
+
old_restart_distance = (
|
| 718 |
+
(previous_xy[0] - old_restart_xy[0]) ** 2
|
| 719 |
+
+ (previous_xy[1] - old_restart_xy[1]) ** 2
|
| 720 |
+
)
|
| 721 |
+
assert actual_restart_xy != old_restart_xy
|
| 722 |
+
assert actual_restart_distance < old_restart_distance
|
| 723 |
|
| 724 |
|
| 725 |
def test_y_direction_raster_prints_each_layer_along_y_axis(tmp_path) -> None:
|
|
|
|
| 744 |
raster_pattern=RASTER_PATTERN_Y_DIRECTION,
|
| 745 |
)
|
| 746 |
|
| 747 |
+
gcode_text = gcode_path.read_text()
|
| 748 |
move_lines = [
|
| 749 |
line.strip()
|
| 750 |
+
for line in gcode_text.splitlines()
|
| 751 |
if line.startswith(("G0", "G1"))
|
| 752 |
]
|
| 753 |
print_lines = [
|
|
|
|
| 756 |
if line.startswith("G1") and "; Color 255" in line
|
| 757 |
]
|
| 758 |
assert print_lines
|
| 759 |
+
assert move_lines[0] == "G0 X0.0 Y1.0 ; Color 0"
|
| 760 |
assert all("X0" in line and "Y0" not in line for line in print_lines)
|
| 761 |
|
| 762 |
x = y = 0.0
|
|
|
|
| 770 |
y += float(token[1:])
|
| 771 |
x_positions.append(x)
|
| 772 |
y_positions.append(y)
|
| 773 |
+
assert min(x_positions) == 0.0
|
| 774 |
+
assert max(x_positions) == 2.0
|
| 775 |
+
assert min(y_positions) == 0.0
|
| 776 |
+
assert max(y_positions) == 4.0
|
| 777 |
+
|
| 778 |
+
moves = _moves_with_colors(gcode_text)
|
| 779 |
+
first_layer_change = next(
|
| 780 |
+
move for move in moves if move["end"][2] > move["start"][2]
|
| 781 |
+
)
|
| 782 |
+
assert first_layer_change["start"][:2] == first_layer_change["end"][:2]
|
| 783 |
|
| 784 |
|
| 785 |
def test_contour_tracing_skips_inactive_nozzle_outline(tmp_path) -> None:
|
tiff_to_gcode.py
CHANGED
|
@@ -829,6 +829,8 @@ def _woodpile_layer_segments(
|
|
| 829 |
color_img: np.ndarray,
|
| 830 |
pixel_size: float,
|
| 831 |
raster_axis: str,
|
|
|
|
|
|
|
| 832 |
) -> list[tuple[float, float, float, float, int]]:
|
| 833 |
mask = path_img > 0
|
| 834 |
segments: list[tuple[float, float, float, float, int]] = []
|
|
@@ -840,11 +842,14 @@ def _woodpile_layer_segments(
|
|
| 840 |
mask.shape[0] - 1 - np.flipud(mask).argmax(axis=0),
|
| 841 |
-1,
|
| 842 |
)
|
| 843 |
-
|
|
|
|
|
|
|
|
|
|
| 844 |
f_idx, l_idx = int(first_nonblank[col]), int(last_nonblank[col])
|
| 845 |
if f_idx == -1:
|
| 846 |
continue
|
| 847 |
-
forward = col_number % 2 == 0
|
| 848 |
row_values = list(range(f_idx, l_idx + 1)) if forward else list(range(l_idx, f_idx - 1, -1))
|
| 849 |
run_start = row_values[0]
|
| 850 |
prev_row = row_values[0]
|
|
@@ -893,11 +898,14 @@ def _woodpile_layer_segments(
|
|
| 893 |
mask.shape[1] - 1 - np.fliplr(mask).argmax(axis=1),
|
| 894 |
-1,
|
| 895 |
)
|
| 896 |
-
|
|
|
|
|
|
|
|
|
|
| 897 |
f_idx, l_idx = int(first_nonblank[row]), int(last_nonblank[row])
|
| 898 |
if f_idx == -1:
|
| 899 |
continue
|
| 900 |
-
forward = row_number % 2 == 0
|
| 901 |
col_values = list(range(f_idx, l_idx + 1)) if forward else list(range(l_idx, f_idx - 1, -1))
|
| 902 |
run_start = col_values[0]
|
| 903 |
prev_col = col_values[0]
|
|
@@ -945,6 +953,49 @@ def _raster_axis_for_pattern(pattern: str, layer_number: int) -> str:
|
|
| 945 |
return "X"
|
| 946 |
|
| 947 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 948 |
def _build_footprint_raster_gcode_list(
|
| 949 |
path_ref_list: list[np.ndarray],
|
| 950 |
color_ref_list: list[np.ndarray],
|
|
@@ -957,11 +1008,20 @@ def _build_footprint_raster_gcode_list(
|
|
| 957 |
gcode_list: list[dict] = []
|
| 958 |
current_x = 0.0
|
| 959 |
current_y = 0.0
|
|
|
|
| 960 |
contour_layers = contour_layers or []
|
| 961 |
|
| 962 |
for layer_number, (path_img, color_img) in enumerate(zip(path_ref_list, color_ref_list)):
|
| 963 |
raster_axis = _raster_axis_for_pattern(raster_pattern, layer_number)
|
| 964 |
-
segments =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 965 |
if not segments:
|
| 966 |
if layer_number > 0:
|
| 967 |
gcode_list.append({"X": 0.0, "Y": 0.0, "Z": layer_height, "Color": 0})
|
|
@@ -985,7 +1045,20 @@ def _build_footprint_raster_gcode_list(
|
|
| 985 |
continue
|
| 986 |
|
| 987 |
first_x, first_y = segments[0][0], segments[0][1]
|
| 988 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 989 |
current_x, current_y = _append_relative_move(
|
| 990 |
gcode_list,
|
| 991 |
current_x,
|
|
|
|
| 829 |
color_img: np.ndarray,
|
| 830 |
pixel_size: float,
|
| 831 |
raster_axis: str,
|
| 832 |
+
reverse_order: bool = False,
|
| 833 |
+
start_forward: bool = True,
|
| 834 |
) -> list[tuple[float, float, float, float, int]]:
|
| 835 |
mask = path_img > 0
|
| 836 |
segments: list[tuple[float, float, float, float, int]] = []
|
|
|
|
| 842 |
mask.shape[0] - 1 - np.flipud(mask).argmax(axis=0),
|
| 843 |
-1,
|
| 844 |
)
|
| 845 |
+
columns = list(np.where(first_nonblank != -1)[0])
|
| 846 |
+
if reverse_order:
|
| 847 |
+
columns.reverse()
|
| 848 |
+
for col_number, col in enumerate(columns):
|
| 849 |
f_idx, l_idx = int(first_nonblank[col]), int(last_nonblank[col])
|
| 850 |
if f_idx == -1:
|
| 851 |
continue
|
| 852 |
+
forward = (col_number % 2 == 0) == start_forward
|
| 853 |
row_values = list(range(f_idx, l_idx + 1)) if forward else list(range(l_idx, f_idx - 1, -1))
|
| 854 |
run_start = row_values[0]
|
| 855 |
prev_row = row_values[0]
|
|
|
|
| 898 |
mask.shape[1] - 1 - np.fliplr(mask).argmax(axis=1),
|
| 899 |
-1,
|
| 900 |
)
|
| 901 |
+
rows = list(np.where(first_nonblank != -1)[0])
|
| 902 |
+
if reverse_order:
|
| 903 |
+
rows.reverse()
|
| 904 |
+
for row_number, row in enumerate(rows):
|
| 905 |
f_idx, l_idx = int(first_nonblank[row]), int(last_nonblank[row])
|
| 906 |
if f_idx == -1:
|
| 907 |
continue
|
| 908 |
+
forward = (row_number % 2 == 0) == start_forward
|
| 909 |
col_values = list(range(f_idx, l_idx + 1)) if forward else list(range(l_idx, f_idx - 1, -1))
|
| 910 |
run_start = col_values[0]
|
| 911 |
prev_col = col_values[0]
|
|
|
|
| 953 |
return "X"
|
| 954 |
|
| 955 |
|
| 956 |
+
def _oriented_woodpile_layer_segments(
|
| 957 |
+
path_img: np.ndarray,
|
| 958 |
+
color_img: np.ndarray,
|
| 959 |
+
pixel_size: float,
|
| 960 |
+
raster_axis: str,
|
| 961 |
+
current_x: float,
|
| 962 |
+
current_y: float,
|
| 963 |
+
prefer_default: bool = False,
|
| 964 |
+
) -> list[tuple[float, float, float, float, int]]:
|
| 965 |
+
default_segments = _woodpile_layer_segments(
|
| 966 |
+
path_img,
|
| 967 |
+
color_img,
|
| 968 |
+
pixel_size,
|
| 969 |
+
raster_axis,
|
| 970 |
+
)
|
| 971 |
+
if prefer_default or not default_segments:
|
| 972 |
+
return default_segments
|
| 973 |
+
|
| 974 |
+
candidates: list[list[tuple[float, float, float, float, int]]] = [default_segments]
|
| 975 |
+
for reverse_order in (False, True):
|
| 976 |
+
for start_forward in (False, True):
|
| 977 |
+
segments = _woodpile_layer_segments(
|
| 978 |
+
path_img,
|
| 979 |
+
color_img,
|
| 980 |
+
pixel_size,
|
| 981 |
+
raster_axis,
|
| 982 |
+
reverse_order=reverse_order,
|
| 983 |
+
start_forward=start_forward,
|
| 984 |
+
)
|
| 985 |
+
if segments and segments not in candidates:
|
| 986 |
+
candidates.append(segments)
|
| 987 |
+
|
| 988 |
+
return min(
|
| 989 |
+
candidates,
|
| 990 |
+
key=lambda segments: _point_distance_sq(
|
| 991 |
+
current_x,
|
| 992 |
+
current_y,
|
| 993 |
+
segments[0][0],
|
| 994 |
+
segments[0][1],
|
| 995 |
+
),
|
| 996 |
+
)
|
| 997 |
+
|
| 998 |
+
|
| 999 |
def _build_footprint_raster_gcode_list(
|
| 1000 |
path_ref_list: list[np.ndarray],
|
| 1001 |
color_ref_list: list[np.ndarray],
|
|
|
|
| 1008 |
gcode_list: list[dict] = []
|
| 1009 |
current_x = 0.0
|
| 1010 |
current_y = 0.0
|
| 1011 |
+
raster_origin_initialized = False
|
| 1012 |
contour_layers = contour_layers or []
|
| 1013 |
|
| 1014 |
for layer_number, (path_img, color_img) in enumerate(zip(path_ref_list, color_ref_list)):
|
| 1015 |
raster_axis = _raster_axis_for_pattern(raster_pattern, layer_number)
|
| 1016 |
+
segments = _oriented_woodpile_layer_segments(
|
| 1017 |
+
path_img,
|
| 1018 |
+
color_img,
|
| 1019 |
+
pixel_size,
|
| 1020 |
+
raster_axis,
|
| 1021 |
+
current_x,
|
| 1022 |
+
current_y,
|
| 1023 |
+
prefer_default=not raster_origin_initialized,
|
| 1024 |
+
)
|
| 1025 |
if not segments:
|
| 1026 |
if layer_number > 0:
|
| 1027 |
gcode_list.append({"X": 0.0, "Y": 0.0, "Z": layer_height, "Color": 0})
|
|
|
|
| 1045 |
continue
|
| 1046 |
|
| 1047 |
first_x, first_y = segments[0][0], segments[0][1]
|
| 1048 |
+
if not raster_origin_initialized:
|
| 1049 |
+
if layer_number > 0:
|
| 1050 |
+
current_x, current_y = _append_relative_move(
|
| 1051 |
+
gcode_list,
|
| 1052 |
+
current_x,
|
| 1053 |
+
current_y,
|
| 1054 |
+
current_x,
|
| 1055 |
+
current_y,
|
| 1056 |
+
0,
|
| 1057 |
+
z_step=layer_height,
|
| 1058 |
+
)
|
| 1059 |
+
current_x, current_y = first_x, first_y
|
| 1060 |
+
raster_origin_initialized = True
|
| 1061 |
+
elif layer_number > 0:
|
| 1062 |
current_x, current_y = _append_relative_move(
|
| 1063 |
gcode_list,
|
| 1064 |
current_x,
|