CyGuy8 commited on
Commit
4f71a6a
·
1 Parent(s): aff1067

Add Y-direction raster option

Browse files
Files changed (3) hide show
  1. README.md +1 -1
  2. tests/test_tiff_to_gcode.py +56 -1
  3. tiff_to_gcode.py +23 -6
README.md CHANGED
@@ -82,7 +82,7 @@ When you click **Generate Reference TIFF Stack**, the app combines available TIF
82
  - Pressure increases by `0.1` psi per layer by default.
83
  - **Use G1 for all moves**: when enabled, every movement line is emitted as `G1` (no `G0` rapid travel); the WAGO valve still marks where material is dispensed. Applies to all shapes.
84
  - **Use Reference Stack for motion**: when enabled, every shape's snake-path *motion* is taken from the combined Reference TIFF Stack while each shape's *valve/dispensing* comes from its own slices — so parallel print heads share one synchronized nozzle path and each deposits only its own geometry. Requires generating the Reference TIFF Stack on the first tab first; shapes are skipped with a message if it is missing.
85
- - **Raster Pattern**: `Same-direction raster` keeps the existing back-and-forth raster direction on every layer. `Woodpile raster` alternates the raster axis by layer, switching between X-direction and Y-direction sweeps.
86
 
87
  ### Print vs Travel Classification
88
 
 
82
  - Pressure increases by `0.1` psi per layer by default.
83
  - **Use G1 for all moves**: when enabled, every movement line is emitted as `G1` (no `G0` rapid travel); the WAGO valve still marks where material is dispensed. Applies to all shapes.
84
  - **Use Reference Stack for motion**: when enabled, every shape's snake-path *motion* is taken from the combined Reference TIFF Stack while each shape's *valve/dispensing* comes from its own slices — so parallel print heads share one synchronized nozzle path and each deposits only its own geometry. Requires generating the Reference TIFF Stack on the first tab first; shapes are skipped with a message if it is missing.
85
+ - **Raster Pattern**: `X-direction raster` keeps the existing X-direction back-and-forth raster on every layer. `Y-direction raster` rasters every layer in Y. `Woodpile raster` alternates the raster axis by layer, switching between X-direction and Y-direction sweeps.
86
 
87
  ### Print vs Travel Classification
88
 
tests/test_tiff_to_gcode.py CHANGED
@@ -4,7 +4,11 @@ import zipfile
4
 
5
  from PIL import Image
6
 
7
- from tiff_to_gcode import RASTER_PATTERN_WOODPILE, generate_snake_path_gcode
 
 
 
 
8
 
9
 
10
  def test_gcode_header_writes_presets_before_initial_aux_commands(tmp_path) -> None:
@@ -111,3 +115,54 @@ def test_woodpile_raster_switches_print_axis_between_layers(tmp_path) -> None:
111
  assert max(x_positions) <= 3.0
112
  assert min(y_positions) >= 0.0
113
  assert max(y_positions) <= 2.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
 
5
  from PIL import Image
6
 
7
+ from tiff_to_gcode import (
8
+ RASTER_PATTERN_WOODPILE,
9
+ RASTER_PATTERN_Y_DIRECTION,
10
+ generate_snake_path_gcode,
11
+ )
12
 
13
 
14
  def test_gcode_header_writes_presets_before_initial_aux_commands(tmp_path) -> None:
 
115
  assert max(x_positions) <= 3.0
116
  assert min(y_positions) >= 0.0
117
  assert max(y_positions) <= 2.0
118
+
119
+
120
+ def test_y_direction_raster_prints_each_layer_along_y_axis(tmp_path) -> None:
121
+ tiff_paths = []
122
+ for index in range(2):
123
+ tiff_path = tmp_path / f"slice_{index:04d}.tif"
124
+ Image.new("L", (3, 2), 0).save(tiff_path)
125
+ tiff_paths.append(tiff_path)
126
+
127
+ zip_path = tmp_path / "slices.zip"
128
+ with zipfile.ZipFile(zip_path, mode="w") as archive:
129
+ for tiff_path in tiff_paths:
130
+ archive.write(tiff_path, arcname=tiff_path.name)
131
+
132
+ gcode_path = generate_snake_path_gcode(
133
+ zip_path,
134
+ shape_name="y_direction",
135
+ pressure=25,
136
+ valve=7,
137
+ port=3,
138
+ raster_pattern=RASTER_PATTERN_Y_DIRECTION,
139
+ )
140
+
141
+ move_lines = [
142
+ line.strip()
143
+ for line in gcode_path.read_text().splitlines()
144
+ if line.startswith(("G0", "G1"))
145
+ ]
146
+ print_lines = [
147
+ line
148
+ for line in move_lines
149
+ if line.startswith("G1") and "; Color 255" in line
150
+ ]
151
+ assert print_lines
152
+ assert all("X0" in line and "Y0" not in line for line in print_lines)
153
+
154
+ x = y = 0.0
155
+ x_positions = [x]
156
+ y_positions = [y]
157
+ for line in move_lines:
158
+ for token in line.split():
159
+ if token.startswith("X"):
160
+ x += float(token[1:])
161
+ if token.startswith("Y"):
162
+ y += float(token[1:])
163
+ x_positions.append(x)
164
+ y_positions.append(y)
165
+ assert min(x_positions) >= 0.0
166
+ assert max(x_positions) <= 3.0
167
+ assert min(y_positions) >= 0.0
168
+ assert max(y_positions) <= 2.0
tiff_to_gcode.py CHANGED
@@ -11,14 +11,21 @@ import numpy as np
11
  from PIL import Image
12
 
13
 
14
- RASTER_PATTERN_SAME_DIRECTION = "Same-direction raster"
 
15
  RASTER_PATTERN_WOODPILE = "Woodpile raster"
16
- RASTER_PATTERN_CHOICES = (RASTER_PATTERN_SAME_DIRECTION, RASTER_PATTERN_WOODPILE)
 
 
 
 
17
 
18
 
19
  def _normalize_raster_pattern(pattern: str | None) -> str:
20
  if pattern == RASTER_PATTERN_WOODPILE:
21
  return RASTER_PATTERN_WOODPILE
 
 
22
  return RASTER_PATTERN_SAME_DIRECTION
23
 
24
 
@@ -299,18 +306,27 @@ def _woodpile_layer_segments(
299
  return segments
300
 
301
 
302
- def _build_woodpile_gcode_list(
 
 
 
 
 
 
 
 
303
  path_ref_list: list[np.ndarray],
304
  color_ref_list: list[np.ndarray],
305
  pixel_size: float,
306
  layer_height: float,
 
307
  ) -> list[dict]:
308
  gcode_list: list[dict] = []
309
  current_x = 0.0
310
  current_y = 0.0
311
 
312
  for layer_number, (path_img, color_img) in enumerate(zip(path_ref_list, color_ref_list)):
313
- raster_axis = "X" if layer_number % 2 == 0 else "Y"
314
  segments = _woodpile_layer_segments(path_img, color_img, pixel_size, raster_axis)
315
  if not segments:
316
  if layer_number > 0:
@@ -423,12 +439,13 @@ def generate_snake_path_gcode(
423
  pressure_on_lines = [_toggle_cmd(com_port, start=True)]
424
  pressure_off_lines = [_toggle_cmd(com_port, start=False)]
425
 
426
- if raster_pattern == RASTER_PATTERN_WOODPILE:
427
- gcode_list = _build_woodpile_gcode_list(
428
  path_ref_list,
429
  color_ref_list,
430
  fil_width,
431
  layer_height,
 
432
  )
433
  else:
434
  gcode_list: list[dict] = []
 
11
  from PIL import Image
12
 
13
 
14
+ RASTER_PATTERN_SAME_DIRECTION = "X-direction raster"
15
+ RASTER_PATTERN_Y_DIRECTION = "Y-direction raster"
16
  RASTER_PATTERN_WOODPILE = "Woodpile raster"
17
+ RASTER_PATTERN_CHOICES = (
18
+ RASTER_PATTERN_SAME_DIRECTION,
19
+ RASTER_PATTERN_Y_DIRECTION,
20
+ RASTER_PATTERN_WOODPILE,
21
+ )
22
 
23
 
24
  def _normalize_raster_pattern(pattern: str | None) -> str:
25
  if pattern == RASTER_PATTERN_WOODPILE:
26
  return RASTER_PATTERN_WOODPILE
27
+ if pattern == RASTER_PATTERN_Y_DIRECTION:
28
+ return RASTER_PATTERN_Y_DIRECTION
29
  return RASTER_PATTERN_SAME_DIRECTION
30
 
31
 
 
306
  return segments
307
 
308
 
309
+ def _raster_axis_for_pattern(pattern: str, layer_number: int) -> str:
310
+ if pattern == RASTER_PATTERN_Y_DIRECTION:
311
+ return "Y"
312
+ if pattern == RASTER_PATTERN_WOODPILE and layer_number % 2 == 1:
313
+ return "Y"
314
+ return "X"
315
+
316
+
317
+ def _build_footprint_raster_gcode_list(
318
  path_ref_list: list[np.ndarray],
319
  color_ref_list: list[np.ndarray],
320
  pixel_size: float,
321
  layer_height: float,
322
+ raster_pattern: str,
323
  ) -> list[dict]:
324
  gcode_list: list[dict] = []
325
  current_x = 0.0
326
  current_y = 0.0
327
 
328
  for layer_number, (path_img, color_img) in enumerate(zip(path_ref_list, color_ref_list)):
329
+ raster_axis = _raster_axis_for_pattern(raster_pattern, layer_number)
330
  segments = _woodpile_layer_segments(path_img, color_img, pixel_size, raster_axis)
331
  if not segments:
332
  if layer_number > 0:
 
439
  pressure_on_lines = [_toggle_cmd(com_port, start=True)]
440
  pressure_off_lines = [_toggle_cmd(com_port, start=False)]
441
 
442
+ if raster_pattern in (RASTER_PATTERN_Y_DIRECTION, RASTER_PATTERN_WOODPILE):
443
+ gcode_list = _build_footprint_raster_gcode_list(
444
  path_ref_list,
445
  color_ref_list,
446
  fil_width,
447
  layer_height,
448
+ raster_pattern,
449
  )
450
  else:
451
  gcode_list: list[dict] = []