CyGuy8 commited on
Commit
270d866
·
1 Parent(s): bfab64c

Infinite stls and nozzle spacing

Browse files
Files changed (3) hide show
  1. README.md +13 -12
  2. app.py +0 -0
  3. tests/test_nozzle_spacing.py +157 -0
README.md CHANGED
@@ -10,7 +10,7 @@ short_description: Upload STLs, export TIFF stacks, and generate G-code.
10
 
11
  # STL to G-Code Gradio App
12
 
13
- This project provides a Gradio app that takes up to three uploaded STL files, shows interactive 3D viewers, slices each model along the Z axis, saves slices as TIFF images, generates G-code from those TIFF stacks, previews the resulting tool path (a fast line plot or an animated 3D tube plot), and can visualize all three shapes printing in parallel and export that animation as a GIF.
14
 
15
  ## Prerequisites
16
 
@@ -37,24 +37,25 @@ Then open the local Gradio URL in your browser, upload STL files or load the bun
37
 
38
  ## What the app does
39
 
40
- - Uploads up to three `.stl` files
41
- - Loads bundled sample STL files
42
- - Shows interactive 3D viewers for rotating each model
 
43
  - Shows model extents, face count, vertex count, and watertight status
44
- - Optionally scales loaded STLs per shape, either with independent target X/Y/Z dimensions or by keeping proportions while target dimensions update together
45
  - Lets you choose layer height and XY pixel size
46
  - Produces one `.tif` image per slice
47
  - Encodes material as black (`0`) and empty space as white (`255`) in each TIFF slice
48
  - Lets you step through the slice stack in the browser
49
  - Exports a ZIP containing the generated TIFF images
50
  - Combines generated stacks into a reference TIFF stack
51
- - Converts generated TIFF ZIPs into G-code files with pressure, valve, and port settings per shape
52
  - Offers two G-code generation options: **Use G1 for all moves** (no rapid travel command) and **Use Reference Stack for motion** (all shapes share one nozzle path; each dispenses only its own geometry)
53
- - Calculates X/Y nozzle spacing from shared or separate Shape 1->2 and Shape 2->3 spacing values, then visualizes the resulting nozzle layout
54
- - Previews each shape's generated G-code inline (text boxes under the downloads)
55
- - Visualizes generated or uploaded G-code tool paths, with the source selectable from Shape 1/2/3 or an uploaded file
56
  - Renders the tool path as a fast line plot or an animated 3D tube plot (play/pause, speed, scrub, frame-step, nozzle marker)
57
- - Plots all three shapes using the configured nozzle spacing and animates them printing in parallel, with a server-side GIF export of that animation
58
 
59
  ## Behavior and Implementation Notes
60
 
@@ -93,14 +94,14 @@ The parser also handles standard slicer G-code: single-axis and Z-only moves, ax
93
 
94
  ### G-code Visualization
95
 
96
- The G-code visualization tab renders the generated Shape 1/2/3 G-code or an uploaded `.txt`, `.gcode`, or `.nc` file. It parses `G0`/`G1` movement lines, supports relative (`G91`) and absolute (`G90`) positioning, and offers two render modes:
97
 
98
  - **Line Plot** — fast thin scatter lines (print and travel), with color/opacity controls.
99
  - **Tube Plot with Animation** — mm-width filament tubes (circular, capped, lit) with a client-side build animation (play/pause, speed, scrub, frame-step) and a moving nozzle marker. Filament/travel widths default to the layer height and its quarter.
100
 
101
  ### Parallel Printing Visualization
102
 
103
- The fourth tab plots all three shapes' G-code at once using the nozzle spacing configured on the TIFF-to-G-code tab, each in its own color. Like the visualization tab it has a fast **Line Plot** and an animated **Tube Plot**; the animation advances all parts on a shared cumulative-path-length timeline, so a shorter part finishes first.
104
 
105
  It can also **export the animation as a GIF**, rendered server-side with Matplotlib (the `Agg` CPU backend — no WebGL, no headless browser, and no `ffmpeg`, so it works locally and on Hugging Face). The GIF is line-style with faint grey travel and white, black-outlined nozzle markers drawn on top; controls cover duration, frames per second, elevation/azimuth viewing angle, and travel opacity (0 hides travel).
106
 
 
10
 
11
  # STL to G-Code Gradio App
12
 
13
+ This project provides a Gradio app that takes any number of uploaded STL files, shows a selected-shape 3D viewer, slices each model along the Z axis, saves slices as TIFF images, generates G-code from those TIFF stacks, previews the resulting tool path (a fast line plot or an animated 3D tube plot), and can visualize the shapes printing in parallel and export that animation as a GIF.
14
 
15
  ## Prerequisites
16
 
 
37
 
38
  ## What the app does
39
 
40
+ - Uploads any number of `.stl` files with a single multi-file uploader
41
+ - Loads bundled sample STL files and merges them with already uploaded STLs
42
+ - Syncs the uploaded STL list back into Shape Settings if the table and uploader get out of step
43
+ - Shows an interactive selected-shape 3D viewer for rotating each model
44
  - Shows model extents, face count, vertex count, and watertight status
45
+ - Scales loaded STLs from editable target X/Y/Z dimensions in the Shape Settings table; new rows default to the STL's original dimensions, **Reset Dimensions** restores them, and **Keep Proportions** updates the other target sides from the edited side
46
  - Lets you choose layer height and XY pixel size
47
  - Produces one `.tif` image per slice
48
  - Encodes material as black (`0`) and empty space as white (`255`) in each TIFF slice
49
  - Lets you step through the slice stack in the browser
50
  - Exports a ZIP containing the generated TIFF images
51
  - Combines generated stacks into a reference TIFF stack
52
+ - Converts generated TIFF ZIPs into G-code files with pressure, valve, and port settings per shape from the Shape Settings table
53
  - Offers two G-code generation options: **Use G1 for all moves** (no rapid travel command) and **Use Reference Stack for motion** (all shapes share one nozzle path; each dispenses only its own geometry)
54
+ - Calculates X/Y nozzle spacing from an editable adjacent-pair spacing table, then visualizes the resulting nozzle layout
55
+ - Previews selected generated G-code inline
56
+ - Visualizes generated or uploaded G-code tool paths, with the source selectable from any active generated shape or an uploaded file
57
  - Renders the tool path as a fast line plot or an animated 3D tube plot (play/pause, speed, scrub, frame-step, nozzle marker)
58
+ - Plots the generated shapes using the configured nozzle spacing and animates them printing in parallel, with a server-side GIF export of that animation
59
 
60
  ## Behavior and Implementation Notes
61
 
 
94
 
95
  ### G-code Visualization
96
 
97
+ The G-code visualization tab renders generated shape G-code or an uploaded `.txt`, `.gcode`, or `.nc` file. It parses `G0`/`G1` movement lines, supports relative (`G91`) and absolute (`G90`) positioning, and offers two render modes:
98
 
99
  - **Line Plot** — fast thin scatter lines (print and travel), with color/opacity controls.
100
  - **Tube Plot with Animation** — mm-width filament tubes (circular, capped, lit) with a client-side build animation (play/pause, speed, scrub, frame-step) and a moving nozzle marker. Filament/travel widths default to the layer height and its quarter.
101
 
102
  ### Parallel Printing Visualization
103
 
104
+ The fourth tab plots the generated shapes' G-code at once using the nozzle spacing configured on the TIFF-to-G-code tab, each in its own color. Like the visualization tab it has a fast **Line Plot** and an animated **Tube Plot**; the animation advances all parts on a shared cumulative-path-length timeline, so a shorter part finishes first.
105
 
106
  It can also **export the animation as a GIF**, rendered server-side with Matplotlib (the `Agg` CPU backend — no WebGL, no headless browser, and no `ffmpeg`, so it works locally and on Hugging Face). The GIF is line-style with faint grey travel and white, black-outlined nozzle markers drawn on top; controls cover duration, frames per second, elevation/azimuth viewing angle, and travel opacity (0 hides travel).
107
 
app.py CHANGED
The diff for this file is too large to render. See raw diff
 
tests/test_nozzle_spacing.py CHANGED
@@ -3,7 +3,15 @@ from __future__ import annotations
3
  import numpy as np
4
 
5
  from app import (
 
 
 
 
6
  _format_nozzle_spacing_status,
 
 
 
 
7
  _resolve_nozzle_layout,
8
  )
9
 
@@ -82,6 +90,43 @@ def test_same_spacing_reuses_first_pair_values_for_second_pair() -> None:
82
  assert spacings[1] == {"from": 2, "to": 3, "dx": 7.0, "dy": 2.0}
83
 
84
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  def test_spacing_status_lists_all_nozzle_pair_distances() -> None:
86
  parts = [
87
  _part(1, ((0.0, 0.0, 0.0), (10.0, 10.0, 1.0))),
@@ -103,3 +148,115 @@ def test_spacing_status_lists_all_nozzle_pair_distances() -> None:
103
  assert "Nozzle 1 -> 2: Delta X 13.00 mm, Delta Y 2.00 mm" in status
104
  assert "Nozzle 1 -> 3: Delta X 20.00 mm, Delta Y 4.00 mm" in status
105
  assert "Nozzle 2 -> 3: Delta X 7.00 mm, Delta Y 2.00 mm" in status
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  import numpy as np
4
 
5
  from app import (
6
+ ADVANCED_NOZZLE_SPACING_HEADERS,
7
+ SCALE_MODE_UNIFORM_FACTOR,
8
+ SIMPLE_NOZZLE_SPACING_HEADERS,
9
+ delete_shape_from_settings,
10
  _format_nozzle_spacing_status,
11
+ _shape_settings_rows,
12
+ _spacing_args_from_table,
13
+ _spacing_table_update,
14
+ normalize_shape_dimensions_for_mode,
15
  _resolve_nozzle_layout,
16
  )
17
 
 
90
  assert spacings[1] == {"from": 2, "to": 3, "dx": 7.0, "dy": 2.0}
91
 
92
 
93
+ def test_individual_spacing_has_no_fixed_shape_limit() -> None:
94
+ parts = [
95
+ _part(1, ((0.0, 0.0, 0.0), (10.0, 10.0, 1.0))),
96
+ _part(2, ((0.0, 0.0, 0.0), (4.0, 10.0, 1.0))),
97
+ _part(3, ((0.0, 0.0, 0.0), (6.0, 10.0, 1.0))),
98
+ _part(4, ((0.0, 0.0, 0.0), (2.0, 10.0, 1.0))),
99
+ _part(5, ((0.0, 0.0, 0.0), (3.0, 10.0, 1.0))),
100
+ _part(6, ((0.0, 0.0, 0.0), (4.0, 10.0, 1.0))),
101
+ _part(7, ((0.0, 0.0, 0.0), (5.0, 10.0, 1.0))),
102
+ ]
103
+
104
+ offsets, spacings = _resolve_nozzle_layout(
105
+ parts,
106
+ False,
107
+ 1.0,
108
+ 0.0,
109
+ 2.0,
110
+ 3.0,
111
+ 4.0,
112
+ -1.0,
113
+ 6.0,
114
+ 2.0,
115
+ 1.0,
116
+ 1.0,
117
+ -3.0,
118
+ 0.5,
119
+ )
120
+
121
+ np.testing.assert_allclose(offsets[1], (0.0, 0.0))
122
+ np.testing.assert_allclose(offsets[2], (11.0, 0.0))
123
+ np.testing.assert_allclose(offsets[3], (17.0, 3.0))
124
+ np.testing.assert_allclose(offsets[4], (27.0, 2.0))
125
+ np.testing.assert_allclose(offsets[7], (40.0, 5.5))
126
+ assert spacings[2] == {"from": 3, "to": 4, "dx": 10.0, "dy": -1.0}
127
+ assert spacings[5] == {"from": 6, "to": 7, "dx": 1.0, "dy": 0.5}
128
+
129
+
130
  def test_spacing_status_lists_all_nozzle_pair_distances() -> None:
131
  parts = [
132
  _part(1, ((0.0, 0.0, 0.0), (10.0, 10.0, 1.0))),
 
148
  assert "Nozzle 1 -> 2: Delta X 13.00 mm, Delta Y 2.00 mm" in status
149
  assert "Nozzle 1 -> 3: Delta X 20.00 mm, Delta Y 4.00 mm" in status
150
  assert "Nozzle 2 -> 3: Delta X 7.00 mm, Delta Y 2.00 mm" in status
151
+
152
+
153
+ def test_keep_proportions_uses_most_recently_edited_dimension() -> None:
154
+ records = [
155
+ {
156
+ "idx": 1,
157
+ "name": "wide_part",
158
+ "stl_path": "wide_part.stl",
159
+ "original_x": 10.0,
160
+ "original_y": 20.0,
161
+ "original_z": 5.0,
162
+ "target_x": 10.0,
163
+ "target_y": 20.0,
164
+ "target_z": 5.0,
165
+ "pressure": 25.0,
166
+ "valve": 4,
167
+ "port": 1,
168
+ "color": "#000000",
169
+ "last_scaled_axis": "target_x",
170
+ }
171
+ ]
172
+ settings = _shape_settings_rows(records)
173
+ settings[0][3] = 50.0
174
+
175
+ updated_records, updated_settings = normalize_shape_dimensions_for_mode(
176
+ records,
177
+ settings,
178
+ SCALE_MODE_UNIFORM_FACTOR,
179
+ )
180
+
181
+ assert updated_records[0]["last_scaled_axis"] == "target_y"
182
+ assert updated_settings[0][2:5] == [25.0, 50.0, 12.5]
183
+
184
+
185
+ def test_simple_spacing_table_uses_one_shared_spacing_row() -> None:
186
+ records = [
187
+ {"idx": 1, "name": "first"},
188
+ {"idx": 2, "name": "second"},
189
+ {"idx": 3, "name": "third"},
190
+ ]
191
+
192
+ update = _spacing_table_update(records, [["Shape 1", "Shape 2", 7.0, -1.5]], False)
193
+ gap12x, gap12y, gap23x, gap23y, extra = _spacing_args_from_table(update["value"], False)
194
+
195
+ assert update["headers"] == SIMPLE_NOZZLE_SPACING_HEADERS
196
+ assert update["value"] == [["Same spacing", "All neighboring shapes", 7.0, -1.5]]
197
+ assert (gap12x, gap12y, gap23x, gap23y, extra) == (7.0, -1.5, 7.0, -1.5, [])
198
+
199
+
200
+ def test_advanced_spacing_table_uses_named_shape_pairs() -> None:
201
+ records = [
202
+ {"idx": 1, "name": "first"},
203
+ {"idx": 2, "name": "second"},
204
+ {"idx": 3, "name": "third"},
205
+ ]
206
+
207
+ update = _spacing_table_update(
208
+ records,
209
+ [["Same spacing", "All neighboring shapes", 4.0, 0.5], ["ignored", "ignored", 8.0, 1.0]],
210
+ True,
211
+ )
212
+ gap12x, gap12y, gap23x, gap23y, extra = _spacing_args_from_table(update["value"], True)
213
+
214
+ assert update["headers"] == ADVANCED_NOZZLE_SPACING_HEADERS
215
+ assert update["value"][0][:2] == ["Shape 1: first", "Shape 2: second"]
216
+ assert update["value"][1][:2] == ["Shape 2: second", "Shape 3: third"]
217
+ assert (gap12x, gap12y, gap23x, gap23y, extra) == (4.0, 0.5, 8.0, 1.0, [])
218
+
219
+
220
+ def test_delete_shape_reindexes_without_losing_shape_data() -> None:
221
+ class Event:
222
+ index = (1, 9)
223
+
224
+ records = [
225
+ {"idx": 1, "name": "first", "stl_path": "first.stl", "target_x": 10.0, "target_y": 11.0, "target_z": 12.0, "pressure": 25, "valve": 4, "port": 1, "color": "#111111"},
226
+ {"idx": 2, "name": "middle", "stl_path": "middle.stl", "target_x": 20.0, "target_y": 21.0, "target_z": 22.0, "pressure": 30, "valve": 5, "port": 2, "color": "#222222"},
227
+ {"idx": 3, "name": "last", "stl_path": "last.stl", "target_x": 30.0, "target_y": 31.0, "target_z": 32.0, "pressure": 35, "valve": 6, "port": 3, "color": "#333333"},
228
+ ]
229
+
230
+ outputs = delete_shape_from_settings(records, _shape_settings_rows(records), None, False, 0.0, Event())
231
+ updated_records = outputs[1]
232
+ updated_settings = outputs[2]
233
+
234
+ assert [record["idx"] for record in updated_records] == [1, 2]
235
+ assert [record["stl_path"] for record in updated_records] == ["first.stl", "last.stl"]
236
+ assert [record["target_x"] for record in updated_records] == [10.0, 30.0]
237
+ assert updated_settings[1][0] == 2
238
+ assert updated_settings[1][1] == "last"
239
+ assert updated_settings[1][2:5] == [30.0, 31.0, 32.0]
240
+
241
+
242
+ def test_delete_shape_cooldown_blocks_immediate_second_delete() -> None:
243
+ class Event:
244
+ index = (1, 9)
245
+
246
+ records = [
247
+ {"idx": 1, "name": "first", "stl_path": "first.stl", "target_x": 10.0, "target_y": 11.0, "target_z": 12.0, "pressure": 25, "valve": 4, "port": 1, "color": "#111111"},
248
+ {"idx": 2, "name": "middle", "stl_path": "middle.stl", "target_x": 20.0, "target_y": 21.0, "target_z": 22.0, "pressure": 30, "valve": 5, "port": 2, "color": "#222222"},
249
+ {"idx": 3, "name": "last", "stl_path": "last.stl", "target_x": 30.0, "target_y": 31.0, "target_z": 32.0, "pressure": 35, "valve": 6, "port": 3, "color": "#333333"},
250
+ ]
251
+
252
+ first_outputs = delete_shape_from_settings(records, _shape_settings_rows(records), None, False, 0.0, Event())
253
+ second_outputs = delete_shape_from_settings(
254
+ first_outputs[1],
255
+ first_outputs[2],
256
+ None,
257
+ False,
258
+ first_outputs[-1],
259
+ Event(),
260
+ )
261
+
262
+ assert [record["name"] for record in second_outputs[1]] == ["first", "last"]