Spaces:
Running
Running
Add Y-direction raster option
Browse files- README.md +1 -1
- tests/test_tiff_to_gcode.py +56 -1
- 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**: `
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 = "
|
|
|
|
| 15 |
RASTER_PATTERN_WOODPILE = "Woodpile raster"
|
| 16 |
-
RASTER_PATTERN_CHOICES = (
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 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
|
| 427 |
-
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] = []
|