MichaelRKessler Claude Fable 5 commited on
Commit
442e7e5
·
1 Parent(s): 38bc649

Add reference-stack motion option and global G-code toggles

Browse files

New options on the TIFF Slices to GCode tab:
- "Use Reference Stack for motion" (default on): every shape shares one
nozzle path derived from the combined Reference TIFF Stack, while the
valve still follows each shape, so parallel heads move in lockstep and
each dispenses only its own geometry. The shape slice is centred onto
the reference canvas to align with the motion path; layers beyond a
shape's height move without dispensing. Skips with a clear message if
no Reference TIFF Stack has been generated.
- "Use G1 for all moves" promoted from three per-shape checkboxes to one
global checkbox (default on) applied to every shape.

generate_snake_path_gcode now takes separate path (motion) and color
(valve) image sources; default behavior is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Files changed (2) hide show
  1. app.py +29 -0
  2. tiff_to_gcode.py +62 -17
app.py CHANGED
@@ -962,6 +962,9 @@ def run_all_tiff_to_gcode(
962
  pressure3: float,
963
  valve3: float,
964
  port3: float,
 
 
 
965
  layer_height: float = 0.8,
966
  pixel_size: float = 0.8,
967
  ) -> tuple[str | None, str | None, str | None, str]:
@@ -971,6 +974,12 @@ def run_all_tiff_to_gcode(
971
  (3, zip3, pressure3, valve3, port3),
972
  ]
973
 
 
 
 
 
 
 
974
  outputs: list[str | None] = [None, None, None]
975
  messages: list[str] = []
976
 
@@ -979,6 +988,13 @@ def run_all_tiff_to_gcode(
979
  messages.append(f"Shape {idx}: skipped (no TIFF ZIP available).")
980
  continue
981
 
 
 
 
 
 
 
 
982
  zip_name = Path(zip_path).stem
983
  default_shape_name = f"shape{idx}"
984
  shape_name = zip_name.replace("_tiff_slices", "") or default_shape_name
@@ -992,6 +1008,8 @@ def run_all_tiff_to_gcode(
992
  port=int(port),
993
  layer_height=float(layer_height),
994
  fil_width=float(pixel_size),
 
 
995
  )
996
  outputs[idx - 1] = str(gcode_path)
997
  messages.append(f"Shape {idx}: wrote `{gcode_path.name}`.")
@@ -1602,6 +1620,14 @@ def build_demo() -> gr.Blocks:
1602
  precision=0,
1603
  )
1604
 
 
 
 
 
 
 
 
 
1605
  gcode_button = gr.Button("Generate G-Code", variant="primary")
1606
 
1607
  with gr.Row():
@@ -1642,6 +1668,9 @@ def build_demo() -> gr.Blocks:
1642
  gcode_pressure_3,
1643
  gcode_valve_3,
1644
  gcode_port_3,
 
 
 
1645
  layer_height,
1646
  pixel_size,
1647
  ],
 
962
  pressure3: float,
963
  valve3: float,
964
  port3: float,
965
+ all_g1: bool = False,
966
+ use_reference_motion: bool = False,
967
+ ref_state: ViewerState | None = None,
968
  layer_height: float = 0.8,
969
  pixel_size: float = 0.8,
970
  ) -> tuple[str | None, str | None, str | None, str]:
 
974
  (3, zip3, pressure3, valve3, port3),
975
  ]
976
 
977
+ # When reference-driven motion is requested, every shape's nozzle path comes
978
+ # from the combined Reference TIFF Stack; the valve still follows each shape.
979
+ motion_tiffs: list[str] | None = None
980
+ if use_reference_motion:
981
+ motion_tiffs = (ref_state or {}).get("tiff_paths") or None
982
+
983
  outputs: list[str | None] = [None, None, None]
984
  messages: list[str] = []
985
 
 
988
  messages.append(f"Shape {idx}: skipped (no TIFF ZIP available).")
989
  continue
990
 
991
+ if use_reference_motion and not motion_tiffs:
992
+ messages.append(
993
+ f"Shape {idx}: skipped (Reference motion selected, but no Reference "
994
+ f"TIFF Stack has been generated on the first tab)."
995
+ )
996
+ continue
997
+
998
  zip_name = Path(zip_path).stem
999
  default_shape_name = f"shape{idx}"
1000
  shape_name = zip_name.replace("_tiff_slices", "") or default_shape_name
 
1008
  port=int(port),
1009
  layer_height=float(layer_height),
1010
  fil_width=float(pixel_size),
1011
+ all_g1=bool(all_g1),
1012
+ motion_tiffs=motion_tiffs,
1013
  )
1014
  outputs[idx - 1] = str(gcode_path)
1015
  messages.append(f"Shape {idx}: wrote `{gcode_path.name}`.")
 
1620
  precision=0,
1621
  )
1622
 
1623
+ gcode_use_ref_motion = gr.Checkbox(
1624
+ label="Use Reference Stack for motion (all shapes share one nozzle path; each dispenses only its own geometry). Generate the Reference TIFF Stack on the first tab first.",
1625
+ value=True,
1626
+ )
1627
+ gcode_all_g1 = gr.Checkbox(
1628
+ label="Use G1 for all moves (no G0 travel commands) — applies to all shapes",
1629
+ value=True,
1630
+ )
1631
  gcode_button = gr.Button("Generate G-Code", variant="primary")
1632
 
1633
  with gr.Row():
 
1668
  gcode_pressure_3,
1669
  gcode_valve_3,
1670
  gcode_port_3,
1671
+ gcode_all_g1,
1672
+ gcode_use_ref_motion,
1673
+ ref_state,
1674
  layer_height,
1675
  pixel_size,
1676
  ],
tiff_to_gcode.py CHANGED
@@ -163,6 +163,22 @@ def _load_grayscale(path: Path, invert: bool) -> np.ndarray:
163
  return array
164
 
165
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  def generate_snake_path_gcode(
167
  zip_path: str | Path,
168
  shape_name: str,
@@ -173,6 +189,8 @@ def generate_snake_path_gcode(
173
  fil_width: float = 0.8,
174
  invert: bool = True,
175
  increase_pressure_per_layer: float = 0.1,
 
 
176
  ) -> Path:
177
  zip_path = Path(zip_path)
178
  if not zip_path.exists():
@@ -185,19 +203,40 @@ def generate_snake_path_gcode(
185
  if not tiff_files:
186
  raise ValueError("No TIFF files found in the ZIP archive.")
187
 
188
- ref_list: list[np.ndarray] = []
189
- img_list: list[np.ndarray] = []
190
- for i, path in enumerate(tiff_files):
191
- img = _load_grayscale(path, invert=invert)
192
- ref_list.append(img.copy())
193
- if (i + 1) % 2 == 0:
194
- img = np.flipud(img)
195
- img_list.append(img)
196
-
197
  off_color = 0
198
  com_port = f"serialPort{port}"
199
  color_dict: dict[int, int] = {0: 100, 255: valve}
200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  setpress_lines = [_setpress_cmd(com_port, pressure, start=True)]
202
  pressure_on_lines = [_toggle_cmd(com_port, start=True)]
203
  pressure_off_lines = [_toggle_cmd(com_port, start=False)]
@@ -208,10 +247,9 @@ def generate_snake_path_gcode(
208
  use_flip_y = False
209
  direction = -1
210
 
211
- for layers in range(len(img_list)):
212
- current_image = img_list[layers]
213
- current_image_ref = ref_list[layers]
214
- last_image_ref = ref_list[layers - 1] if layers > 0 else None
215
  y_ref = current_image_ref.shape[0]
216
 
217
  def find_first_valid_y(row: np.ndarray | None, flip: bool = False) -> int | None:
@@ -265,9 +303,14 @@ def generate_snake_path_gcode(
265
  dist_sign_long = -dist_sign_long
266
  dist_sign_long = -dist_sign_long
267
 
268
- ref_for_path = current_image_ref.copy()
269
- if (layers + 1) % 2 == 0:
270
- ref_for_path = np.flipud(ref_for_path)
 
 
 
 
 
271
 
272
  if layers == 0:
273
  direction = -1
@@ -306,7 +349,9 @@ def generate_snake_path_gcode(
306
  f.write(_valve_cmd(color_dict[cur_color], 1))
307
  f.write(_valve_cmd(color_dict[prev_color], 0))
308
 
309
- move_type = "G1" if cur_color != off_color else "G0"
 
 
310
  if "Z" in move:
311
  line = (
312
  f"{move_type} X{move['X']} Y{move['Y']} Z{move['Z']} "
 
163
  return array
164
 
165
 
166
+ def _center_on_canvas(
167
+ img: np.ndarray, canvas_h: int, canvas_w: int, fill: int = 0
168
+ ) -> np.ndarray:
169
+ """Place `img` centred on a (canvas_h, canvas_w) canvas filled with `fill`.
170
+
171
+ Mirrors the centring used to build the reference stack, so a shape's slice
172
+ lines up pixel-for-pixel with the reference (motion) slice of the same layer.
173
+ """
174
+ h, w = img.shape[:2]
175
+ out = np.full((canvas_h, canvas_w), fill, dtype=img.dtype)
176
+ y_off = max(0, (canvas_h - h) // 2)
177
+ x_off = max(0, (canvas_w - w) // 2)
178
+ out[y_off : y_off + h, x_off : x_off + w] = img[: canvas_h, : canvas_w]
179
+ return out
180
+
181
+
182
  def generate_snake_path_gcode(
183
  zip_path: str | Path,
184
  shape_name: str,
 
189
  fil_width: float = 0.8,
190
  invert: bool = True,
191
  increase_pressure_per_layer: float = 0.1,
192
+ all_g1: bool = False,
193
+ motion_tiffs: list[str] | None = None,
194
  ) -> Path:
195
  zip_path = Path(zip_path)
196
  if not zip_path.exists():
 
203
  if not tiff_files:
204
  raise ValueError("No TIFF files found in the ZIP archive.")
205
 
 
 
 
 
 
 
 
 
 
206
  off_color = 0
207
  com_port = f"serialPort{port}"
208
  color_dict: dict[int, int] = {0: 100, 255: valve}
209
 
210
+ # Two non-flipped source image lists. The "path" images drive the nozzle
211
+ # motion (which rows are swept, the sweep extent, the inter-layer shifts);
212
+ # the "color" images decide the valve state (material) at each swept pixel.
213
+ # Normally both are this shape's own slices. When reference motion tiffs are
214
+ # supplied, motion comes from the combined reference stack while the valve is
215
+ # still driven by this shape's slices, centred onto the reference canvas — so
216
+ # parallel heads share one motion path but each dispenses only its geometry.
217
+ shape_imgs = [_load_grayscale(p, invert=invert) for p in tiff_files]
218
+
219
+ if motion_tiffs:
220
+ motion_paths = sorted(
221
+ (Path(p) for p in motion_tiffs), key=lambda p: _sort_key(p.name)
222
+ )
223
+ path_ref_list = [_load_grayscale(p, invert=invert) for p in motion_paths]
224
+ if not path_ref_list:
225
+ raise ValueError("No reference TIFF files provided for motion.")
226
+ color_ref_list: list[np.ndarray] = []
227
+ for li, motion_img in enumerate(path_ref_list):
228
+ h_c, w_c = motion_img.shape[:2]
229
+ if li < len(shape_imgs):
230
+ color_ref_list.append(
231
+ _center_on_canvas(shape_imgs[li], h_c, w_c, fill=off_color)
232
+ )
233
+ else:
234
+ # Reference is taller than this shape: move but dispense nothing.
235
+ color_ref_list.append(np.full((h_c, w_c), off_color, dtype=np.uint8))
236
+ else:
237
+ path_ref_list = [im.copy() for im in shape_imgs]
238
+ color_ref_list = [im.copy() for im in shape_imgs]
239
+
240
  setpress_lines = [_setpress_cmd(com_port, pressure, start=True)]
241
  pressure_on_lines = [_toggle_cmd(com_port, start=True)]
242
  pressure_off_lines = [_toggle_cmd(com_port, start=False)]
 
247
  use_flip_y = False
248
  direction = -1
249
 
250
+ for layers in range(len(path_ref_list)):
251
+ current_image_ref = path_ref_list[layers]
252
+ last_image_ref = path_ref_list[layers - 1] if layers > 0 else None
 
253
  y_ref = current_image_ref.shape[0]
254
 
255
  def find_first_valid_y(row: np.ndarray | None, flip: bool = False) -> int | None:
 
303
  dist_sign_long = -dist_sign_long
304
  dist_sign_long = -dist_sign_long
305
 
306
+ # Flip path and color together on even layers so they stay aligned.
307
+ even_layer = (layers + 1) % 2 == 0
308
+ ref_for_path = (
309
+ np.flipud(current_image_ref) if even_layer else current_image_ref.copy()
310
+ )
311
+ current_image = (
312
+ np.flipud(color_ref_list[layers]) if even_layer else color_ref_list[layers]
313
+ )
314
 
315
  if layers == 0:
316
  direction = -1
 
349
  f.write(_valve_cmd(color_dict[cur_color], 1))
350
  f.write(_valve_cmd(color_dict[prev_color], 0))
351
 
352
+ # When all_g1 is set, every move is emitted as G1 regardless of
353
+ # valve state; the valve commands still mark print vs travel.
354
+ move_type = "G1" if (all_g1 or cur_color != off_color) else "G0"
355
  if "Z" in move:
356
  line = (
357
  f"{move_type} X{move['X']} Y{move['Y']} Z{move['Z']} "