CyGuy8 commited on
Commit
c8d77be
·
1 Parent(s): 8c02eb4

Refine footprint raster starts and lead-in UI

Browse files
Files changed (3) hide show
  1. app.py +15 -4
  2. tests/test_tiff_to_gcode.py +38 -10
  3. 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.Row():
3689
- gcode_lead_in_length = gr.Number(label="Lead In Length (mm)", value=5.0, minimum=0.1, step=0.1)
3690
- gcode_lead_in_clearance = gr.Number(label="Lead In Clearance (mm)", value=5.0, minimum=0.0, step=0.1)
3691
- gcode_lead_in_lines = gr.Number(label="Lead In Raster Lines", value=3, minimum=1, step=1)
 
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 gcode_path.read_text().splitlines()
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) == -1.0
700
- assert max(x_positions) == 4.0
701
- assert min(y_positions) == -1.0
702
- assert max(y_positions) == 3.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 gcode_path.read_text().splitlines()
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) >= 0.0
752
- assert max(x_positions) <= 3.0
753
- assert min(y_positions) == -1.0
754
- assert max(y_positions) == 3.0
 
 
 
 
 
 
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
- for col_number, col in enumerate(np.where(first_nonblank != -1)[0]):
 
 
 
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
- for row_number, row in enumerate(np.where(first_nonblank != -1)[0]):
 
 
 
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 = _woodpile_layer_segments(path_img, color_img, pixel_size, raster_axis)
 
 
 
 
 
 
 
 
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 layer_number > 0:
 
 
 
 
 
 
 
 
 
 
 
 
 
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,