MichaelRKessler commited on
Commit
e237055
·
1 Parent(s): 00e3783

fixed theG1 and G0 at the beginning of the G-Code

Browse files

Every Color 255 line now starts with G0, every Color 0 line starts with G1.

Files changed (2) hide show
  1. app.py +195 -5
  2. tiff_to_gcode.py +332 -0
app.py CHANGED
@@ -7,6 +7,7 @@ import gradio as gr
7
  from PIL import Image
8
 
9
  from stl_slicer import SliceStack, load_mesh, slice_stl_to_tiffs
 
10
 
11
 
12
  ViewerState = dict[str, Any]
@@ -33,6 +34,24 @@ APP_CSS = """
33
  margin-bottom: 0.4rem !important;
34
  }
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  .model3D button[aria-label="Undo"] {
37
  color: var(--block-label-text-color) !important;
38
  cursor: pointer !important;
@@ -240,6 +259,55 @@ def jump_to_slice(state: ViewerState, index: float) -> tuple[str, Image.Image |
240
  return _render_selected_slice(state, int(index))
241
 
242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  def shift_slice(state: ViewerState, index: float, delta: int) -> tuple[int, str, Image.Image | None]:
244
  tiff_paths = state.get("tiff_paths", [])
245
  if not tiff_paths:
@@ -327,7 +395,7 @@ def build_demo() -> gr.Blocks:
327
  step=1,
328
  interactive=False,
329
  )
330
- download_zip = gr.File(label=f"Download TIFF ZIP {i + 1}")
331
  state = gr.State(_empty_state())
332
 
333
  slice_labels.append(slice_label)
@@ -394,10 +462,132 @@ def build_demo() -> gr.Blocks:
394
  )
395
 
396
  with gr.Tab("TIFF Slices to GCode"):
397
- gr.Textbox(
398
- label="Status",
399
- value="More to come later.",
400
- interactive=False,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
  )
402
 
403
  return demo
 
7
  from PIL import Image
8
 
9
  from stl_slicer import SliceStack, load_mesh, slice_stl_to_tiffs
10
+ from tiff_to_gcode import generate_snake_path_gcode
11
 
12
 
13
  ViewerState = dict[str, Any]
 
34
  margin-bottom: 0.4rem !important;
35
  }
36
 
37
+ .gcode-shape-card {
38
+ border: 1px solid var(--border-color-primary);
39
+ border-radius: 0.5rem;
40
+ padding: 0.5rem !important;
41
+ min-height: 220px;
42
+ }
43
+
44
+ .gcode-shape-card .prose {
45
+ margin-bottom: 0.25rem !important;
46
+ }
47
+
48
+ .gcode-param-label {
49
+ font-size: 0.8rem;
50
+ font-weight: 600;
51
+ line-height: 1.15;
52
+ margin-bottom: 0.2rem !important;
53
+ }
54
+
55
  .model3D button[aria-label="Undo"] {
56
  color: var(--block-label-text-color) !important;
57
  cursor: pointer !important;
 
259
  return _render_selected_slice(state, int(index))
260
 
261
 
262
+ def run_all_tiff_to_gcode(
263
+ zip1: str | None,
264
+ zip2: str | None,
265
+ zip3: str | None,
266
+ pressure1: float,
267
+ valve1: float,
268
+ port1: float,
269
+ pressure2: float,
270
+ valve2: float,
271
+ port2: float,
272
+ pressure3: float,
273
+ valve3: float,
274
+ port3: float,
275
+ ) -> tuple[str | None, str | None, str | None, str]:
276
+ specs = [
277
+ (1, zip1, pressure1, valve1, port1),
278
+ (2, zip2, pressure2, valve2, port2),
279
+ (3, zip3, pressure3, valve3, port3),
280
+ ]
281
+
282
+ outputs: list[str | None] = [None, None, None]
283
+ messages: list[str] = []
284
+
285
+ for idx, zip_path, pressure, valve, port in specs:
286
+ if not zip_path:
287
+ messages.append(f"Shape {idx}: skipped (no TIFF ZIP available).")
288
+ continue
289
+
290
+ zip_name = Path(zip_path).stem
291
+ default_shape_name = f"shape{idx}"
292
+ shape_name = zip_name.replace("_tiff_slices", "") or default_shape_name
293
+
294
+ try:
295
+ gcode_path = generate_snake_path_gcode(
296
+ zip_path=zip_path,
297
+ shape_name=shape_name,
298
+ pressure=float(pressure),
299
+ valve=int(valve),
300
+ port=int(port),
301
+ )
302
+ outputs[idx - 1] = str(gcode_path)
303
+ messages.append(f"Shape {idx}: wrote `{gcode_path.name}`.")
304
+ except Exception as exc: # surface errors in the UI
305
+ outputs[idx - 1] = None
306
+ messages.append(f"Shape {idx}: failed ({exc}).")
307
+
308
+ return outputs[0], outputs[1], outputs[2], "\n".join(messages)
309
+
310
+
311
  def shift_slice(state: ViewerState, index: float, delta: int) -> tuple[int, str, Image.Image | None]:
312
  tiff_paths = state.get("tiff_paths", [])
313
  if not tiff_paths:
 
395
  step=1,
396
  interactive=False,
397
  )
398
+ download_zip = gr.File(label=f"Download TIFF ZIP {i + 1}", interactive=False)
399
  state = gr.State(_empty_state())
400
 
401
  slice_labels.append(slice_label)
 
462
  )
463
 
464
  with gr.Tab("TIFF Slices to GCode"):
465
+ gr.Markdown(
466
+ """
467
+ # TIFF Slices to GCode
468
+ Uses TIFF ZIP outputs from the first tab. Set pressure, valve,
469
+ and port for each shape, then generate G-code files in one run.
470
+ """
471
+ )
472
+
473
+ with gr.Row():
474
+ with gr.Column(min_width=250):
475
+ with gr.Group(elem_classes=["gcode-shape-card"]):
476
+ gr.Markdown("### Shape 1")
477
+ with gr.Row():
478
+ with gr.Column(min_width=70):
479
+ gr.Markdown("Pressure (psi)", elem_classes=["gcode-param-label"])
480
+ gcode_pressure_1 = gr.Number(
481
+ show_label=False,
482
+ value=25.0,
483
+ minimum=0.0,
484
+ step=0.5,
485
+ )
486
+ with gr.Column(min_width=70):
487
+ gr.Markdown("Valve", elem_classes=["gcode-param-label"])
488
+ gcode_valve_1 = gr.Number(
489
+ show_label=False,
490
+ value=4,
491
+ minimum=0,
492
+ step=1,
493
+ precision=0,
494
+ )
495
+ with gr.Column(min_width=70):
496
+ gr.Markdown("Port", elem_classes=["gcode-param-label"])
497
+ gcode_port_1 = gr.Number(
498
+ show_label=False,
499
+ value=1,
500
+ minimum=1,
501
+ step=1,
502
+ precision=0,
503
+ )
504
+ with gr.Column(min_width=250):
505
+ with gr.Group(elem_classes=["gcode-shape-card"]):
506
+ gr.Markdown("### Shape 2")
507
+ with gr.Row():
508
+ with gr.Column(min_width=70):
509
+ gr.Markdown("Pressure (psi)", elem_classes=["gcode-param-label"])
510
+ gcode_pressure_2 = gr.Number(
511
+ show_label=False,
512
+ value=25.0,
513
+ minimum=0.0,
514
+ step=0.5,
515
+ )
516
+ with gr.Column(min_width=70):
517
+ gr.Markdown("Valve", elem_classes=["gcode-param-label"])
518
+ gcode_valve_2 = gr.Number(
519
+ show_label=False,
520
+ value=4,
521
+ minimum=0,
522
+ step=1,
523
+ precision=0,
524
+ )
525
+ with gr.Column(min_width=70):
526
+ gr.Markdown("Port", elem_classes=["gcode-param-label"])
527
+ gcode_port_2 = gr.Number(
528
+ show_label=False,
529
+ value=1,
530
+ minimum=1,
531
+ step=1,
532
+ precision=0,
533
+ )
534
+ with gr.Column(min_width=250):
535
+ with gr.Group(elem_classes=["gcode-shape-card"]):
536
+ gr.Markdown("### Shape 3")
537
+ with gr.Row():
538
+ with gr.Column(min_width=70):
539
+ gr.Markdown("Pressure (psi)", elem_classes=["gcode-param-label"])
540
+ gcode_pressure_3 = gr.Number(
541
+ show_label=False,
542
+ value=25.0,
543
+ minimum=0.0,
544
+ step=0.5,
545
+ )
546
+ with gr.Column(min_width=70):
547
+ gr.Markdown("Valve", elem_classes=["gcode-param-label"])
548
+ gcode_valve_3 = gr.Number(
549
+ show_label=False,
550
+ value=4,
551
+ minimum=0,
552
+ step=1,
553
+ precision=0,
554
+ )
555
+ with gr.Column(min_width=70):
556
+ gr.Markdown("Port", elem_classes=["gcode-param-label"])
557
+ gcode_port_3 = gr.Number(
558
+ show_label=False,
559
+ value=1,
560
+ minimum=1,
561
+ step=1,
562
+ precision=0,
563
+ )
564
+
565
+ gcode_button = gr.Button("Generate G-Code", variant="primary")
566
+
567
+ with gr.Row():
568
+ gcode_file_1 = gr.File(label="Download G-Code Shape 1")
569
+ gcode_file_2 = gr.File(label="Download G-Code Shape 2")
570
+ gcode_file_3 = gr.File(label="Download G-Code Shape 3")
571
+
572
+ gcode_status = gr.Markdown("")
573
+
574
+ gcode_button.click(
575
+ fn=run_all_tiff_to_gcode,
576
+ inputs=[
577
+ download_zips[0],
578
+ download_zips[1],
579
+ download_zips[2],
580
+ gcode_pressure_1,
581
+ gcode_valve_1,
582
+ gcode_port_1,
583
+ gcode_pressure_2,
584
+ gcode_valve_2,
585
+ gcode_port_2,
586
+ gcode_pressure_3,
587
+ gcode_valve_3,
588
+ gcode_port_3,
589
+ ],
590
+ outputs=[gcode_file_1, gcode_file_2, gcode_file_3, gcode_status],
591
  )
592
 
593
  return demo
tiff_to_gcode.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import tempfile
5
+ import zipfile
6
+ from codecs import encode
7
+ from pathlib import Path
8
+ from textwrap import wrap
9
+
10
+ import numpy as np
11
+ from PIL import Image
12
+
13
+
14
+ def _setpress(pressure: float) -> str:
15
+ pressure_str = str(int(pressure * 10)).zfill(4)
16
+ command_bytes = bytes("08PS " + pressure_str, "utf-8")
17
+ hex_command = encode(command_bytes, "hex").decode("utf-8")
18
+ format_command = "\\x" + "\\x".join(
19
+ hex_command[i : i + 2] for i in range(0, len(hex_command), 2)
20
+ )
21
+
22
+ hex_pairs = wrap(hex_command, 2)
23
+ decimal_sum = sum(int(pair, 16) for pair in hex_pairs)
24
+ checksum_bin = bin(decimal_sum % 256)[2:].zfill(8)
25
+ inverted = int("".join("1" if c == "0" else "0" for c in checksum_bin), 2) + 1
26
+ checksum_hex = hex(inverted)[2:].upper()
27
+ format_checksum = "\\x" + "\\x".join(
28
+ checksum_hex[i : i + 2] for i in range(0, len(checksum_hex), 2)
29
+ )
30
+
31
+ return "b'" + "\\x05\\x02" + format_command + format_checksum + "\\x03" + "'"
32
+
33
+
34
+ def _togglepress() -> str:
35
+ return "b'\\x05\\x02\\x30\\x34\\x44\\x49\\x20\\x20\\x43\\x46\\x03'"
36
+
37
+
38
+ def _setpress_cmd(port: str, pressure: float, start: bool) -> str:
39
+ insert = "{preset}" if start else ""
40
+ return f"\n\r{insert}{port}.write({_setpress(pressure)})"
41
+
42
+
43
+ def _toggle_cmd(port: str, start: bool) -> str:
44
+ insert = "{preset}" if start else ""
45
+ return f"\n\r{insert}{port}.write({_togglepress()})"
46
+
47
+
48
+ def _valve_cmd(valve: int, command: int) -> str:
49
+ return f"\n{{aux_command}}WAGO_ValveCommands({valve}, {command})\n"
50
+
51
+
52
+ def _gcode_layer(
53
+ path_img: np.ndarray,
54
+ color_img: np.ndarray,
55
+ output_list: list[dict],
56
+ pixel_size: float,
57
+ direction: int,
58
+ layer_number: int,
59
+ ) -> int:
60
+ mask = path_img > 0
61
+ first_nonblack = np.where(mask.any(axis=1), mask.argmax(axis=1), -1)
62
+ last_nonblack = np.where(
63
+ mask.any(axis=1),
64
+ mask.shape[1] - 1 - np.fliplr(mask).argmax(axis=1),
65
+ -1,
66
+ )
67
+
68
+ stored_gcode: list[dict] = []
69
+ nonblank_rows = np.where(first_nonblack != -1)[0]
70
+
71
+ for idx, i in enumerate(nonblank_rows):
72
+ f_idx, l_idx = int(first_nonblack[i]), int(last_nonblack[i])
73
+ if f_idx == -1:
74
+ continue
75
+
76
+ if direction < 0:
77
+ rng = range(f_idx, l_idx + 1)
78
+ else:
79
+ rng = range(l_idx, f_idx - 1, -1)
80
+ direction *= -1
81
+
82
+ prev_color = None
83
+ color_len = 0
84
+ buffer = direction
85
+ stored_gcode.append({"X": buffer * pixel_size, "Y": 0, "Color": 0})
86
+
87
+ for j in rng:
88
+ this_color = int(color_img[i, j])
89
+ if prev_color is None:
90
+ prev_color = this_color
91
+ color_len = 1
92
+ elif this_color == prev_color:
93
+ color_len += 1
94
+ else:
95
+ stored_gcode.append(
96
+ {
97
+ "X": direction * color_len * pixel_size,
98
+ "Y": 0,
99
+ "Color": prev_color,
100
+ }
101
+ )
102
+ color_len = 1
103
+ prev_color = this_color
104
+
105
+ if color_len > 0:
106
+ stored_gcode.append(
107
+ {
108
+ "X": direction * color_len * pixel_size,
109
+ "Y": 0,
110
+ "Color": prev_color,
111
+ }
112
+ )
113
+
114
+ stored_gcode.append({"X": buffer * pixel_size, "Y": 0, "Color": 0})
115
+
116
+ curr_x = l_idx if direction > 0 else f_idx
117
+ curr_x += buffer
118
+
119
+ if idx + 1 < len(nonblank_rows):
120
+ next_i = int(nonblank_rows[idx + 1])
121
+ y_travel_dist = next_i - int(i)
122
+ nf, nl = int(first_nonblack[next_i]), int(last_nonblack[next_i])
123
+ if nf == -1:
124
+ continue
125
+ next_start = nf if direction < 0 else nl
126
+ travel_x = (next_start + buffer) - curr_x
127
+ y_dir = -1 if layer_number % 2 == 1 else 1
128
+ stored_gcode.append(
129
+ {
130
+ "X": travel_x * pixel_size,
131
+ "Y": y_travel_dist * pixel_size * y_dir,
132
+ "Color": 0,
133
+ }
134
+ )
135
+
136
+ output_list.extend(stored_gcode)
137
+ return direction
138
+
139
+
140
+ def _sort_key(filename: str) -> int:
141
+ digits = "".join(filter(str.isdigit, filename))
142
+ return int(digits) if digits else 2**31
143
+
144
+
145
+ def _extract_zip_tiffs(zip_path: Path, dest: Path) -> list[Path]:
146
+ with zipfile.ZipFile(zip_path) as archive:
147
+ archive.extractall(dest)
148
+
149
+ tiffs: list[Path] = []
150
+ for root, _, files in os.walk(dest):
151
+ for name in files:
152
+ if name.lower().endswith((".tif", ".tiff")):
153
+ tiffs.append(Path(root) / name)
154
+ tiffs.sort(key=lambda p: _sort_key(p.name))
155
+ return tiffs
156
+
157
+
158
+ def _load_grayscale(path: Path, invert: bool) -> np.ndarray:
159
+ with Image.open(path) as image:
160
+ array = np.array(image.convert("L"), dtype=np.uint8)
161
+ if invert:
162
+ array = 255 - array
163
+ return array
164
+
165
+
166
+ def generate_snake_path_gcode(
167
+ zip_path: str | Path,
168
+ shape_name: str,
169
+ pressure: float,
170
+ valve: int,
171
+ port: int,
172
+ fil_width: float = 0.8,
173
+ invert: bool = True,
174
+ increase_pressure_per_layer: float = 0.1,
175
+ ) -> Path:
176
+ zip_path = Path(zip_path)
177
+ if not zip_path.exists():
178
+ raise FileNotFoundError(f"ZIP file not found: {zip_path}")
179
+
180
+ work_dir = Path(tempfile.mkdtemp(prefix="tiff_gcode_"))
181
+ extract_dir = work_dir / "tiffs"
182
+ extract_dir.mkdir(parents=True, exist_ok=True)
183
+ tiff_files = _extract_zip_tiffs(zip_path, extract_dir)
184
+ if not tiff_files:
185
+ raise ValueError("No TIFF files found in the ZIP archive.")
186
+
187
+ ref_list: list[np.ndarray] = []
188
+ img_list: list[np.ndarray] = []
189
+ for i, path in enumerate(tiff_files):
190
+ img = _load_grayscale(path, invert=invert)
191
+ ref_list.append(img.copy())
192
+ if (i + 1) % 2 == 0:
193
+ img = np.flipud(img)
194
+ img_list.append(img)
195
+
196
+ off_color = 0
197
+ com_port = f"serialPort{port}"
198
+ color_dict: dict[int, int] = {0: 100, 255: valve}
199
+
200
+ setpress_lines = [_setpress_cmd(com_port, pressure, start=True)]
201
+ pressure_on_lines = [_toggle_cmd(com_port, start=True)]
202
+ pressure_off_lines = [_toggle_cmd(com_port, start=False)]
203
+
204
+ gcode_list: list[dict] = []
205
+ dist_sign_long = 1
206
+ current_offsets_x: list[int] = []
207
+ use_flip_y = False
208
+ direction = -1
209
+
210
+ for layers in range(len(img_list)):
211
+ current_image = img_list[layers]
212
+ current_image_ref = ref_list[layers]
213
+ last_image_ref = ref_list[layers - 1] if layers > 0 else None
214
+ y_ref = current_image_ref.shape[0]
215
+
216
+ def find_first_valid_y(row: np.ndarray | None, flip: bool = False) -> int | None:
217
+ if row is None:
218
+ return None
219
+ row_data = np.flip(row) if flip else row
220
+ for j, pixel in enumerate(row_data):
221
+ if np.any(pixel) != off_color:
222
+ return y_ref - 1 - j if flip else j
223
+ return None
224
+
225
+ last_x = last_y = None
226
+ if current_offsets_x:
227
+ use_flip_x = layers % 2 == 1
228
+ last_x = current_offsets_x[-1] if use_flip_x else current_offsets_x[0]
229
+ last_row = (
230
+ last_image_ref[last_x] if last_image_ref is not None else None
231
+ )
232
+ last_y = find_first_valid_y(last_row, flip=use_flip_y)
233
+ current_offsets_x.clear()
234
+
235
+ current_offsets_x = [
236
+ i for i, row in enumerate(current_image_ref) if np.any(row) != off_color
237
+ ]
238
+
239
+ first_x = first_y = None
240
+ if current_offsets_x:
241
+ use_flip_x = layers % 2 == 1
242
+ first_x = current_offsets_x[-1] if use_flip_x else current_offsets_x[0]
243
+ first_row = current_image_ref[first_x]
244
+ first_y = find_first_valid_y(first_row, flip=use_flip_y)
245
+
246
+ if None in (last_x, last_y, first_x, first_y):
247
+ shift_x = shift_y = 0
248
+ else:
249
+ shift_x = (first_x - last_x) * fil_width
250
+ shift_y = (first_y - last_y) * fil_width * dist_sign_long
251
+ if use_flip_y:
252
+ shift_y = -shift_y
253
+
254
+ if len(current_offsets_x) % 2 == 1:
255
+ use_flip_y = not use_flip_y
256
+
257
+ if layers > 0:
258
+ gcode_list.append(
259
+ {"X": shift_y, "Y": shift_x, "Z": fil_width, "Color": 0}
260
+ )
261
+
262
+ for row in current_image_ref:
263
+ if all(p == off_color for p in row):
264
+ dist_sign_long = -dist_sign_long
265
+ dist_sign_long = -dist_sign_long
266
+
267
+ ref_for_path = current_image_ref.copy()
268
+ if (layers + 1) % 2 == 0:
269
+ ref_for_path = np.flipud(ref_for_path)
270
+
271
+ if layers == 0:
272
+ direction = -1
273
+ direction = _gcode_layer(
274
+ ref_for_path,
275
+ current_image,
276
+ gcode_list,
277
+ fil_width,
278
+ direction,
279
+ layers,
280
+ )
281
+
282
+ gcode_path = work_dir / f"{shape_name}_SnakePath_gcode.txt"
283
+ pressure_cur = float(pressure)
284
+
285
+ with open(gcode_path, "w") as f:
286
+ f.write("G91\n")
287
+ for color in color_dict:
288
+ f.write(_valve_cmd(color_dict[color], 0))
289
+ for line in setpress_lines:
290
+ f.write(f"{line}\n")
291
+ for line in pressure_on_lines:
292
+ f.write(f"{line}\n")
293
+
294
+ pressure_next: str | None = None
295
+ for i, move in enumerate(gcode_list):
296
+ prev_color = gcode_list[i - 1]["Color"] if i > 0 else 0
297
+ cur_color = move["Color"]
298
+ if prev_color != cur_color:
299
+ if cur_color == off_color:
300
+ f.write(_valve_cmd(color_dict[prev_color], 0))
301
+ else:
302
+ if prev_color == off_color:
303
+ f.write(_valve_cmd(color_dict[cur_color], 1))
304
+ else:
305
+ f.write(_valve_cmd(color_dict[cur_color], 1))
306
+ f.write(_valve_cmd(color_dict[prev_color], 0))
307
+
308
+ move_type = "G0" if cur_color != off_color else "G1"
309
+ if "Z" in move:
310
+ line = (
311
+ f"{move_type} X{move['X']} Y{move['Y']} Z{fil_width} "
312
+ f"; Color {move['Color']}"
313
+ )
314
+ pressure_cur += increase_pressure_per_layer
315
+ pressure_next = _setpress_cmd(com_port, pressure_cur, start=False)
316
+ else:
317
+ line = (
318
+ f"{move_type} X{move['X']} Y{move['Y']} ; Color {move['Color']}"
319
+ )
320
+ pressure_next = None
321
+
322
+ f.write(f"{line}\n")
323
+ if pressure_next is not None:
324
+ f.write(f"{pressure_next}\n")
325
+ pressure_next = None
326
+
327
+ for color in color_dict:
328
+ f.write(_valve_cmd(color_dict[color], 0))
329
+ for line in pressure_off_lines:
330
+ f.write(f"{line}\n")
331
+
332
+ return gcode_path