Spaces:
Sleeping
Adjustable Pressure Increase Per Layer option, snapped to tenths
Browse filesNew "Pressure Increase Per Layer (psi)" number input to the right of the
ramp and pressure-variable checkboxes (default 0.1). Feeds
increase_pressure_per_layer in both pressure formats: variable-mode ramp
expressions become pressureN + step*k and numeric mode bakes the same
steps. The ramp-ceiling warning computes with the chosen step.
Values snap to the tenths place (the regulator's serial protocol
encodes pressure x 10) via an explicit change handler that rewrites the
input in place — gr.Number's precision= rounds before the handler sees
the value, which left stale text in the box, so it is deliberately not
used. The option joins the staleness fingerprint and the settings
export/import round trip.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- README.md +1 -1
- app.py +77 -16
- tests/test_nozzle_spacing.py +17 -5
- tests/test_vector_gcode.py +25 -0
|
@@ -125,7 +125,7 @@ The **Multi-Nozzle Split** accordion on the **Shapes & G-Code** tab can split on
|
|
| 125 |
- Generated files include pressure preset commands and WAGO valve commands based on the selected pressure, valve, and port; the nozzle number controls layout/spacing assignment.
|
| 126 |
- Pressure commands are written **once per port**: when shapes share a serial port, only the first shape's file carries the pressure variable, preset, toggle, and per-layer ramp — the print host compiles every file onto one timeline, and duplicated toggles would flip the regulator on/off/on at start. Valve commands stay per shape in every file.
|
| 127 |
- **Editable pressure variable** (checkbox, on by default): each pressure-owning file starts with `{preset}{aux_command}globals().update(pressure3 = 25)` (the value from the Shape Settings table, the variable named after the file's serial port), and every pressure command references that variable — the preset is `setpress(pressure3)` and each ramp step is `setpress(round(min(pressure3 + 0.1, 100), 1))`. To print the same file at a different pressure, edit the one number on the first line; the preset and the entire ramp follow, no regeneration needed. The line's exact shape is dictated by the TCode print host: `{aux_command}` keeps it off the motion timeline (only lines containing `{aux_command}`/`serialPort` are routed as commands), and the host execs all `{preset}` commands as one list literal, so the line must be a pure expression — `globals().update(...)` is the expression form of the assignment. Naming the variable per port keeps multi-port prints independent, since the host runs every file in one shared namespace. Unchecking **Editable Pressure Variable** writes plain numeric pressure commands instead (the pre-variable format, e.g. `setpress(25)` with ramp values baked in), as a fallback in case a host cannot run the variable form.
|
| 128 |
-
- Pressure increases by `0.1` psi per layer by default.
|
| 129 |
- Pressures are bounded to the regulator's range: table values clamp to 0–100 psi and snap to the tenths place (the serial protocol encodes `pressure × 10`), the ramp expressions clamp at 100 psi even for hand-edited variable values, and the generation status warns when a ramp would hit the ceiling before the last layer.
|
| 130 |
- Every movement line is emitted as `G1` at one constant speed (no `G0` rapid travel); the WAGO valve commands mark where material is dispensed vs where the head just travels.
|
| 131 |
- **Lead In**: enabled per shape via the **Lead In** column in Shape Settings; the Lead In Options accordion sets the patch geometry. Prints a purge patch before layer 1: **Lead In Position** (Left/Right/Up/Down) picks which side of the shape it sits on, **Lead In Clearance** how far away, and **Lead In Line Direction** which way the purge strokes run — Auto points them at the shape (the historical behavior), or force Horizontal/Vertical (e.g. a patch below the shape with horizontal strokes). The purge always finishes at the patch corner nearest the toolpath start, whatever the line count's parity: strokes running across the approach print the farthest line first and step back toward the shape, and the zigzag starts from whichever end makes the last stroke land on the start corner — so the freshly primed nozzle travels only the short clearance lane to the print, never back across the wet purge lines. For grid-split pieces the clearance is automatically extended by the assembly's remaining extent along the purge axis (reported in the G-code status), so under shared reference motion every nozzle's purge patch lands clear of the whole assembled part instead of on a neighbor's print area. The **Lead In** column in Shape Settings controls dispensing per shape: an opted-out head still travels the shared patch (keeping parallel heads in sync) but keeps its valve shut, and skips the lead-in moves entirely when printing without shared motion.
|
|
|
|
| 125 |
- Generated files include pressure preset commands and WAGO valve commands based on the selected pressure, valve, and port; the nozzle number controls layout/spacing assignment.
|
| 126 |
- Pressure commands are written **once per port**: when shapes share a serial port, only the first shape's file carries the pressure variable, preset, toggle, and per-layer ramp — the print host compiles every file onto one timeline, and duplicated toggles would flip the regulator on/off/on at start. Valve commands stay per shape in every file.
|
| 127 |
- **Editable pressure variable** (checkbox, on by default): each pressure-owning file starts with `{preset}{aux_command}globals().update(pressure3 = 25)` (the value from the Shape Settings table, the variable named after the file's serial port), and every pressure command references that variable — the preset is `setpress(pressure3)` and each ramp step is `setpress(round(min(pressure3 + 0.1, 100), 1))`. To print the same file at a different pressure, edit the one number on the first line; the preset and the entire ramp follow, no regeneration needed. The line's exact shape is dictated by the TCode print host: `{aux_command}` keeps it off the motion timeline (only lines containing `{aux_command}`/`serialPort` are routed as commands), and the host execs all `{preset}` commands as one list literal, so the line must be a pure expression — `globals().update(...)` is the expression form of the assignment. Naming the variable per port keeps multi-port prints independent, since the host runs every file in one shared namespace. Unchecking **Editable Pressure Variable** writes plain numeric pressure commands instead (the pre-variable format, e.g. `setpress(25)` with ramp values baked in), as a fallback in case a host cannot run the variable form.
|
| 128 |
+
- Pressure increases by `0.1` psi per layer by default; the **Pressure Increase Per Layer (psi)** option (next to the ramp checkbox) adjusts the step. Values snap to the tenths place — the regulator's serial protocol encodes `pressure × 10`, so finer steps cannot be printed.
|
| 129 |
- Pressures are bounded to the regulator's range: table values clamp to 0–100 psi and snap to the tenths place (the serial protocol encodes `pressure × 10`), the ramp expressions clamp at 100 psi even for hand-edited variable values, and the generation status warns when a ramp would hit the ceiling before the last layer.
|
| 130 |
- Every movement line is emitted as `G1` at one constant speed (no `G0` rapid travel); the WAGO valve commands mark where material is dispensed vs where the head just travels.
|
| 131 |
- **Lead In**: enabled per shape via the **Lead In** column in Shape Settings; the Lead In Options accordion sets the patch geometry. Prints a purge patch before layer 1: **Lead In Position** (Left/Right/Up/Down) picks which side of the shape it sits on, **Lead In Clearance** how far away, and **Lead In Line Direction** which way the purge strokes run — Auto points them at the shape (the historical behavior), or force Horizontal/Vertical (e.g. a patch below the shape with horizontal strokes). The purge always finishes at the patch corner nearest the toolpath start, whatever the line count's parity: strokes running across the approach print the farthest line first and step back toward the shape, and the zigzag starts from whichever end makes the last stroke land on the start corner — so the freshly primed nozzle travels only the short clearance lane to the print, never back across the wet purge lines. For grid-split pieces the clearance is automatically extended by the assembly's remaining extent along the purge axis (reported in the G-code status), so under shared reference motion every nozzle's purge patch lands clear of the whole assembled part instead of on a neighbor's print area. The **Lead In** column in Shape Settings controls dispensing per shape: an opted-out head still travels the shared patch (keeping parallel heads in sync) but keeps its valve shut, and skips the lead-in moves entirely when printing without shared motion.
|
|
@@ -4807,6 +4807,7 @@ def _gcode_settings_snapshot(
|
|
| 4807 |
lead_in_orientation: str | None = None,
|
| 4808 |
contour_order: str | None = None,
|
| 4809 |
pressure_variable: bool = True,
|
|
|
|
| 4810 |
) -> dict:
|
| 4811 |
"""Fingerprint of every setting that shapes this record's G-code.
|
| 4812 |
|
|
@@ -4841,9 +4842,27 @@ def _gcode_settings_snapshot(
|
|
| 4841 |
"sweep_buffer": round(_coerce_float(sweep_buffer, 0.8), 6),
|
| 4842 |
"contour_order": str(contour_order or CONTOUR_ORDER_LAST),
|
| 4843 |
"pressure_variable": bool(pressure_variable),
|
|
|
|
|
|
|
| 4844 |
}
|
| 4845 |
|
| 4846 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4847 |
GCODE_STALE_MESSAGE = (
|
| 4848 |
"⚠️ **Settings changed since this G-code was generated** — "
|
| 4849 |
"press Generate G-Code again before downloading, visualizing, or printing."
|
|
@@ -4866,6 +4885,7 @@ def check_gcode_staleness(
|
|
| 4866 |
lead_in_orientation: str | None = None,
|
| 4867 |
contour_order: str | None = None,
|
| 4868 |
pressure_variable: bool = True,
|
|
|
|
| 4869 |
) -> str:
|
| 4870 |
"""Warning banner text when generated G-code no longer matches the settings."""
|
| 4871 |
records = _apply_shape_settings(records or [], settings_table)
|
|
@@ -4885,6 +4905,7 @@ def check_gcode_staleness(
|
|
| 4885 |
lead_in_orientation,
|
| 4886 |
contour_order,
|
| 4887 |
pressure_variable,
|
|
|
|
| 4888 |
)
|
| 4889 |
for record in records:
|
| 4890 |
if not record.get("gcode_path"):
|
|
@@ -4932,6 +4953,7 @@ def export_project_settings(
|
|
| 4932 |
nozzle_speed: Any,
|
| 4933 |
contour_order: str | None = None,
|
| 4934 |
pressure_variable: bool = True,
|
|
|
|
| 4935 |
split_columns: Any = None,
|
| 4936 |
split_rows: Any = None,
|
| 4937 |
split_overlapping_layers: Any = None,
|
|
@@ -4976,6 +4998,7 @@ def export_project_settings(
|
|
| 4976 |
"nozzle_speed": _coerce_float(nozzle_speed, 10.0),
|
| 4977 |
"contour_order": str(contour_order or CONTOUR_ORDER_LAST),
|
| 4978 |
"pressure_variable": bool(pressure_variable),
|
|
|
|
| 4979 |
"split_columns": max(1, _coerce_int(split_columns, 2)),
|
| 4980 |
"split_rows": max(1, _coerce_int(split_rows, 1)),
|
| 4981 |
"split_overlapping_layers": bool(split_overlapping_layers),
|
|
@@ -5007,7 +5030,7 @@ def import_project_settings(
|
|
| 5007 |
"""
|
| 5008 |
|
| 5009 |
def _skip_options() -> tuple:
|
| 5010 |
-
return tuple(gr.skip() for _ in range(
|
| 5011 |
|
| 5012 |
paths = _uploaded_file_paths(settings_upload)
|
| 5013 |
if not paths:
|
|
@@ -5071,6 +5094,7 @@ def import_project_settings(
|
|
| 5071 |
option("nozzle_speed"),
|
| 5072 |
option("contour_order"),
|
| 5073 |
option("pressure_variable"),
|
|
|
|
| 5074 |
option("split_columns"),
|
| 5075 |
option("split_rows"),
|
| 5076 |
option("split_overlapping_layers"),
|
|
@@ -5119,9 +5143,13 @@ def generate_dynamic_gcode(
|
|
| 5119 |
lead_in_orientation: str | None = None,
|
| 5120 |
contour_order: str | None = None,
|
| 5121 |
pressure_variable: bool = True,
|
|
|
|
| 5122 |
progress: gr.Progress = gr.Progress(),
|
| 5123 |
) -> tuple:
|
| 5124 |
records = _apply_shape_settings(records or [], settings_table)
|
|
|
|
|
|
|
|
|
|
| 5125 |
messages: list[str] = []
|
| 5126 |
progress(0.02, desc="Slicing shapes…")
|
| 5127 |
_ensure_records_sliced(
|
|
@@ -5226,6 +5254,7 @@ def generate_dynamic_gcode(
|
|
| 5226 |
motion_infill_fractions=motion_infill_fractions,
|
| 5227 |
emit_pressure_commands=owns_port_pressure,
|
| 5228 |
pressure_variable=bool(pressure_variable),
|
|
|
|
| 5229 |
# Same buffer for every shape: the shared motion must match.
|
| 5230 |
sweep_buffer=max(0.0, _coerce_float(sweep_buffer, 0.8)),
|
| 5231 |
lead_in_enabled=bool(lead_in_enabled),
|
|
@@ -5260,14 +5289,16 @@ def generate_dynamic_gcode(
|
|
| 5260 |
lead_in_orientation,
|
| 5261 |
contour_order,
|
| 5262 |
pressure_variable,
|
|
|
|
| 5263 |
)
|
| 5264 |
messages.append(f"Shape {record['idx']}: wrote `{gcode_path.name}`.")
|
| 5265 |
except Exception as exc:
|
| 5266 |
messages.append(f"Shape {record['idx']}: failed ({exc}).")
|
| 5267 |
|
| 5268 |
# Pressure sanity: the regulator tops out at MAX_PRESSURE_PSI, and the
|
| 5269 |
-
# per-layer ramp climbs
|
| 5270 |
-
# the ceiling (the files clamp there, so later layers
|
|
|
|
| 5271 |
if pressure_ramp_enabled and ref_layers is not None and getattr(ref_layers, "layers", None):
|
| 5272 |
layer_count = len(ref_layers.layers)
|
| 5273 |
peak_base = max(
|
|
@@ -5278,12 +5309,13 @@ def generate_dynamic_gcode(
|
|
| 5278 |
),
|
| 5279 |
default=0.0,
|
| 5280 |
)
|
| 5281 |
-
peak = peak_base +
|
| 5282 |
if peak > MAX_PRESSURE_PSI + 1e-9:
|
| 5283 |
messages.insert(
|
| 5284 |
0,
|
| 5285 |
f"⚠️ The pressure ramp reaches the {MAX_PRESSURE_PSI:g} psi regulator "
|
| 5286 |
-
f"limit before the last layer (base {peak_base:g} psi +
|
|
|
|
| 5287 |
"layers); the files hold the maximum from there on. Lower the base pressure or "
|
| 5288 |
"disable the ramp if that is not intended.",
|
| 5289 |
)
|
|
@@ -5808,17 +5840,33 @@ def build_dynamic_demo() -> gr.Blocks:
|
|
| 5808 |
All shapes share one combined nozzle path (each dispenses only its own geometry), so parallel heads always stay in sync.
|
| 5809 |
"""
|
| 5810 |
)
|
| 5811 |
-
|
| 5812 |
-
|
| 5813 |
-
|
| 5814 |
-
|
| 5815 |
-
|
| 5816 |
-
"
|
| 5817 |
-
|
| 5818 |
-
|
| 5819 |
-
|
| 5820 |
-
|
| 5821 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5822 |
with gr.Row(elem_id="gcode-raster-row"):
|
| 5823 |
gcode_raster_pattern = gr.Dropdown(
|
| 5824 |
label="Raster Pattern",
|
|
@@ -6255,6 +6303,7 @@ def build_dynamic_demo() -> gr.Blocks:
|
|
| 6255 |
viz_nozzle_speed,
|
| 6256 |
gcode_contour_order,
|
| 6257 |
gcode_pressure_variable,
|
|
|
|
| 6258 |
split_columns,
|
| 6259 |
split_rows,
|
| 6260 |
split_overlapping_layers,
|
|
@@ -6286,6 +6335,7 @@ def build_dynamic_demo() -> gr.Blocks:
|
|
| 6286 |
viz_nozzle_speed,
|
| 6287 |
gcode_contour_order,
|
| 6288 |
gcode_pressure_variable,
|
|
|
|
| 6289 |
split_columns,
|
| 6290 |
split_rows,
|
| 6291 |
split_overlapping_layers,
|
|
@@ -6314,6 +6364,7 @@ def build_dynamic_demo() -> gr.Blocks:
|
|
| 6314 |
gcode_lead_in_orientation,
|
| 6315 |
gcode_contour_order,
|
| 6316 |
gcode_pressure_variable,
|
|
|
|
| 6317 |
]
|
| 6318 |
shape_settings.change(
|
| 6319 |
fn=check_gcode_staleness,
|
|
@@ -6335,6 +6386,7 @@ def build_dynamic_demo() -> gr.Blocks:
|
|
| 6335 |
gcode_lead_in_orientation,
|
| 6336 |
gcode_contour_order,
|
| 6337 |
gcode_pressure_variable,
|
|
|
|
| 6338 |
):
|
| 6339 |
stale_control.change(
|
| 6340 |
fn=check_gcode_staleness,
|
|
@@ -6342,6 +6394,14 @@ def build_dynamic_demo() -> gr.Blocks:
|
|
| 6342 |
outputs=[gcode_stale_banner],
|
| 6343 |
queue=False,
|
| 6344 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6345 |
|
| 6346 |
apply_rotation_button.click(
|
| 6347 |
fn=apply_shape_z_rotation,
|
|
@@ -6425,6 +6485,7 @@ def build_dynamic_demo() -> gr.Blocks:
|
|
| 6425 |
gcode_lead_in_orientation,
|
| 6426 |
gcode_contour_order,
|
| 6427 |
gcode_pressure_variable,
|
|
|
|
| 6428 |
],
|
| 6429 |
outputs=[shape_records, ref_layers, gcode_downloads, gcode_status, gcode_text_source, gcode_source, gcode_download_all],
|
| 6430 |
).then(
|
|
|
|
| 4807 |
lead_in_orientation: str | None = None,
|
| 4808 |
contour_order: str | None = None,
|
| 4809 |
pressure_variable: bool = True,
|
| 4810 |
+
pressure_increase: float = 0.1,
|
| 4811 |
) -> dict:
|
| 4812 |
"""Fingerprint of every setting that shapes this record's G-code.
|
| 4813 |
|
|
|
|
| 4842 |
"sweep_buffer": round(_coerce_float(sweep_buffer, 0.8), 6),
|
| 4843 |
"contour_order": str(contour_order or CONTOUR_ORDER_LAST),
|
| 4844 |
"pressure_variable": bool(pressure_variable),
|
| 4845 |
+
# Tenths only: the regulator's serial protocol encodes pressure x 10.
|
| 4846 |
+
"pressure_increase": round(max(0.0, _coerce_float(pressure_increase, 0.1)), 1),
|
| 4847 |
}
|
| 4848 |
|
| 4849 |
|
| 4850 |
+
def snap_pressure_increase_to_tenths(value: Any):
|
| 4851 |
+
"""Snap the Pressure Increase Per Layer input to the tenths place.
|
| 4852 |
+
|
| 4853 |
+
The regulator's serial protocol encodes pressure x 10, so finer steps
|
| 4854 |
+
cannot be printed; generation rounds anyway, but snapping the input in
|
| 4855 |
+
place keeps what the user sees equal to what the files will do.
|
| 4856 |
+
Returns gr.skip() when the value is already legal so the corrective
|
| 4857 |
+
update cannot loop.
|
| 4858 |
+
"""
|
| 4859 |
+
typed = _coerce_float(value, 0.1)
|
| 4860 |
+
snapped = round(max(0.0, typed), 1)
|
| 4861 |
+
if abs(snapped - typed) < 1e-9:
|
| 4862 |
+
return gr.skip()
|
| 4863 |
+
return gr.update(value=snapped)
|
| 4864 |
+
|
| 4865 |
+
|
| 4866 |
GCODE_STALE_MESSAGE = (
|
| 4867 |
"⚠️ **Settings changed since this G-code was generated** — "
|
| 4868 |
"press Generate G-Code again before downloading, visualizing, or printing."
|
|
|
|
| 4885 |
lead_in_orientation: str | None = None,
|
| 4886 |
contour_order: str | None = None,
|
| 4887 |
pressure_variable: bool = True,
|
| 4888 |
+
pressure_increase: float = 0.1,
|
| 4889 |
) -> str:
|
| 4890 |
"""Warning banner text when generated G-code no longer matches the settings."""
|
| 4891 |
records = _apply_shape_settings(records or [], settings_table)
|
|
|
|
| 4905 |
lead_in_orientation,
|
| 4906 |
contour_order,
|
| 4907 |
pressure_variable,
|
| 4908 |
+
pressure_increase,
|
| 4909 |
)
|
| 4910 |
for record in records:
|
| 4911 |
if not record.get("gcode_path"):
|
|
|
|
| 4953 |
nozzle_speed: Any,
|
| 4954 |
contour_order: str | None = None,
|
| 4955 |
pressure_variable: bool = True,
|
| 4956 |
+
pressure_increase: float = 0.1,
|
| 4957 |
split_columns: Any = None,
|
| 4958 |
split_rows: Any = None,
|
| 4959 |
split_overlapping_layers: Any = None,
|
|
|
|
| 4998 |
"nozzle_speed": _coerce_float(nozzle_speed, 10.0),
|
| 4999 |
"contour_order": str(contour_order or CONTOUR_ORDER_LAST),
|
| 5000 |
"pressure_variable": bool(pressure_variable),
|
| 5001 |
+
"pressure_increase": round(max(0.0, _coerce_float(pressure_increase, 0.1)), 1),
|
| 5002 |
"split_columns": max(1, _coerce_int(split_columns, 2)),
|
| 5003 |
"split_rows": max(1, _coerce_int(split_rows, 1)),
|
| 5004 |
"split_overlapping_layers": bool(split_overlapping_layers),
|
|
|
|
| 5030 |
"""
|
| 5031 |
|
| 5032 |
def _skip_options() -> tuple:
|
| 5033 |
+
return tuple(gr.skip() for _ in range(21))
|
| 5034 |
|
| 5035 |
paths = _uploaded_file_paths(settings_upload)
|
| 5036 |
if not paths:
|
|
|
|
| 5094 |
option("nozzle_speed"),
|
| 5095 |
option("contour_order"),
|
| 5096 |
option("pressure_variable"),
|
| 5097 |
+
option("pressure_increase"),
|
| 5098 |
option("split_columns"),
|
| 5099 |
option("split_rows"),
|
| 5100 |
option("split_overlapping_layers"),
|
|
|
|
| 5143 |
lead_in_orientation: str | None = None,
|
| 5144 |
contour_order: str | None = None,
|
| 5145 |
pressure_variable: bool = True,
|
| 5146 |
+
pressure_increase: float = 0.1,
|
| 5147 |
progress: gr.Progress = gr.Progress(),
|
| 5148 |
) -> tuple:
|
| 5149 |
records = _apply_shape_settings(records or [], settings_table)
|
| 5150 |
+
# Tenths only: the regulator's serial protocol encodes pressure x 10, so
|
| 5151 |
+
# finer ramp steps cannot be printed.
|
| 5152 |
+
pressure_increase = round(max(0.0, _coerce_float(pressure_increase, 0.1)), 1)
|
| 5153 |
messages: list[str] = []
|
| 5154 |
progress(0.02, desc="Slicing shapes…")
|
| 5155 |
_ensure_records_sliced(
|
|
|
|
| 5254 |
motion_infill_fractions=motion_infill_fractions,
|
| 5255 |
emit_pressure_commands=owns_port_pressure,
|
| 5256 |
pressure_variable=bool(pressure_variable),
|
| 5257 |
+
increase_pressure_per_layer=pressure_increase,
|
| 5258 |
# Same buffer for every shape: the shared motion must match.
|
| 5259 |
sweep_buffer=max(0.0, _coerce_float(sweep_buffer, 0.8)),
|
| 5260 |
lead_in_enabled=bool(lead_in_enabled),
|
|
|
|
| 5289 |
lead_in_orientation,
|
| 5290 |
contour_order,
|
| 5291 |
pressure_variable,
|
| 5292 |
+
pressure_increase,
|
| 5293 |
)
|
| 5294 |
messages.append(f"Shape {record['idx']}: wrote `{gcode_path.name}`.")
|
| 5295 |
except Exception as exc:
|
| 5296 |
messages.append(f"Shape {record['idx']}: failed ({exc}).")
|
| 5297 |
|
| 5298 |
# Pressure sanity: the regulator tops out at MAX_PRESSURE_PSI, and the
|
| 5299 |
+
# per-layer ramp climbs pressure_increase psi per layer — warn when a
|
| 5300 |
+
# print would hit the ceiling (the files clamp there, so later layers
|
| 5301 |
+
# hold the maximum).
|
| 5302 |
if pressure_ramp_enabled and ref_layers is not None and getattr(ref_layers, "layers", None):
|
| 5303 |
layer_count = len(ref_layers.layers)
|
| 5304 |
peak_base = max(
|
|
|
|
| 5309 |
),
|
| 5310 |
default=0.0,
|
| 5311 |
)
|
| 5312 |
+
peak = peak_base + pressure_increase * max(0, layer_count - 1)
|
| 5313 |
if peak > MAX_PRESSURE_PSI + 1e-9:
|
| 5314 |
messages.insert(
|
| 5315 |
0,
|
| 5316 |
f"⚠️ The pressure ramp reaches the {MAX_PRESSURE_PSI:g} psi regulator "
|
| 5317 |
+
f"limit before the last layer (base {peak_base:g} psi + "
|
| 5318 |
+
f"{pressure_increase:g}/layer x {layer_count} "
|
| 5319 |
"layers); the files hold the maximum from there on. Lower the base pressure or "
|
| 5320 |
"disable the ramp if that is not intended.",
|
| 5321 |
)
|
|
|
|
| 5840 |
All shapes share one combined nozzle path (each dispenses only its own geometry), so parallel heads always stay in sync.
|
| 5841 |
"""
|
| 5842 |
)
|
| 5843 |
+
with gr.Row():
|
| 5844 |
+
gcode_pressure_ramp_enabled = gr.Checkbox(
|
| 5845 |
+
label="Increase Pressure Each Layer", value=True, scale=1
|
| 5846 |
+
)
|
| 5847 |
+
gcode_pressure_variable = gr.Checkbox(
|
| 5848 |
+
label="Editable Pressure Variable",
|
| 5849 |
+
value=True,
|
| 5850 |
+
scale=2,
|
| 5851 |
+
info=(
|
| 5852 |
+
"Start each pressure-owning file with a pressureN variable that every "
|
| 5853 |
+
"pressure command references, so the print pressure can be changed by "
|
| 5854 |
+
"editing one number in the file — no regeneration needed. Uncheck to "
|
| 5855 |
+
"write plain numeric pressure commands instead."
|
| 5856 |
+
),
|
| 5857 |
+
)
|
| 5858 |
+
# No precision= here: Gradio would round BEFORE the change
|
| 5859 |
+
# handler sees the value, so the snap-to-tenths correction
|
| 5860 |
+
# below would never know the display needs rewriting.
|
| 5861 |
+
gcode_pressure_increase = gr.Number(
|
| 5862 |
+
label="Pressure Increase Per Layer (psi)",
|
| 5863 |
+
value=0.1,
|
| 5864 |
+
minimum=0.0,
|
| 5865 |
+
step=0.1,
|
| 5866 |
+
scale=1,
|
| 5867 |
+
min_width=170,
|
| 5868 |
+
info="Added each layer when Increase Pressure Each Layer is on. Tenths only.",
|
| 5869 |
+
)
|
| 5870 |
with gr.Row(elem_id="gcode-raster-row"):
|
| 5871 |
gcode_raster_pattern = gr.Dropdown(
|
| 5872 |
label="Raster Pattern",
|
|
|
|
| 6303 |
viz_nozzle_speed,
|
| 6304 |
gcode_contour_order,
|
| 6305 |
gcode_pressure_variable,
|
| 6306 |
+
gcode_pressure_increase,
|
| 6307 |
split_columns,
|
| 6308 |
split_rows,
|
| 6309 |
split_overlapping_layers,
|
|
|
|
| 6335 |
viz_nozzle_speed,
|
| 6336 |
gcode_contour_order,
|
| 6337 |
gcode_pressure_variable,
|
| 6338 |
+
gcode_pressure_increase,
|
| 6339 |
split_columns,
|
| 6340 |
split_rows,
|
| 6341 |
split_overlapping_layers,
|
|
|
|
| 6364 |
gcode_lead_in_orientation,
|
| 6365 |
gcode_contour_order,
|
| 6366 |
gcode_pressure_variable,
|
| 6367 |
+
gcode_pressure_increase,
|
| 6368 |
]
|
| 6369 |
shape_settings.change(
|
| 6370 |
fn=check_gcode_staleness,
|
|
|
|
| 6386 |
gcode_lead_in_orientation,
|
| 6387 |
gcode_contour_order,
|
| 6388 |
gcode_pressure_variable,
|
| 6389 |
+
gcode_pressure_increase,
|
| 6390 |
):
|
| 6391 |
stale_control.change(
|
| 6392 |
fn=check_gcode_staleness,
|
|
|
|
| 6394 |
outputs=[gcode_stale_banner],
|
| 6395 |
queue=False,
|
| 6396 |
)
|
| 6397 |
+
# Snap finer-than-tenths ramp steps back in place (the regulator
|
| 6398 |
+
# resolves tenths); skips when already legal so it cannot loop.
|
| 6399 |
+
gcode_pressure_increase.change(
|
| 6400 |
+
fn=snap_pressure_increase_to_tenths,
|
| 6401 |
+
inputs=[gcode_pressure_increase],
|
| 6402 |
+
outputs=[gcode_pressure_increase],
|
| 6403 |
+
queue=False,
|
| 6404 |
+
)
|
| 6405 |
|
| 6406 |
apply_rotation_button.click(
|
| 6407 |
fn=apply_shape_z_rotation,
|
|
|
|
| 6485 |
gcode_lead_in_orientation,
|
| 6486 |
gcode_contour_order,
|
| 6487 |
gcode_pressure_variable,
|
| 6488 |
+
gcode_pressure_increase,
|
| 6489 |
],
|
| 6490 |
outputs=[shape_records, ref_layers, gcode_downloads, gcode_status, gcode_text_source, gcode_source, gcode_download_all],
|
| 6491 |
).then(
|
|
@@ -2001,6 +2001,17 @@ def test_rotate_all_shapes_at_once(tmp_path) -> None:
|
|
| 2001 |
assert (cleared[1]["target_x"], cleared[1]["target_y"]) == (6.0, 2.0)
|
| 2002 |
|
| 2003 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2004 |
def test_project_settings_export_import_round_trip(tmp_path) -> None:
|
| 2005 |
from app import export_project_settings, import_project_settings
|
| 2006 |
|
|
@@ -2011,7 +2022,7 @@ def test_project_settings_export_import_round_trip(tmp_path) -> None:
|
|
| 2011 |
settings_path, status = export_project_settings(
|
| 2012 |
records, None, 0.4, 0.4, None, "Circle Spiral raster", False, 1.2,
|
| 2013 |
6.0, 7.0, 4, "Down", "Horizontal", 15.0,
|
| 2014 |
-
pressure_variable=False,
|
| 2015 |
split_columns=3, split_rows=2, split_overlapping_rows=True,
|
| 2016 |
split_overlap=1.5,
|
| 2017 |
)
|
|
@@ -2035,10 +2046,11 @@ def test_project_settings_export_import_round_trip(tmp_path) -> None:
|
|
| 2035 |
assert option_updates[8]["value"] == 0.4 # layer height
|
| 2036 |
assert option_updates[11]["value"] == 15.0 # nozzle speed
|
| 2037 |
assert option_updates[13]["value"] is False # pressure variable off
|
| 2038 |
-
assert option_updates[14]["value"] == 3 #
|
| 2039 |
-
assert option_updates[15]["value"] ==
|
| 2040 |
-
assert option_updates[
|
| 2041 |
-
assert option_updates[
|
|
|
|
| 2042 |
|
| 2043 |
# A file listed in the export but not loaded is reported.
|
| 2044 |
partial = import_project_settings([settings_path], _records_from_files(["egg_inside.stl"], None), None)
|
|
|
|
| 2001 |
assert (cleared[1]["target_x"], cleared[1]["target_y"]) == (6.0, 2.0)
|
| 2002 |
|
| 2003 |
|
| 2004 |
+
def test_pressure_increase_input_snaps_to_tenths() -> None:
|
| 2005 |
+
from app import snap_pressure_increase_to_tenths
|
| 2006 |
+
|
| 2007 |
+
# Finer than tenths snaps in place (the regulator encodes pressure x 10).
|
| 2008 |
+
assert snap_pressure_increase_to_tenths(0.26)["value"] == 0.3
|
| 2009 |
+
assert snap_pressure_increase_to_tenths(-0.4)["value"] == 0.0
|
| 2010 |
+
# Already-legal values are skipped so the corrective update cannot loop.
|
| 2011 |
+
snapped = snap_pressure_increase_to_tenths(0.3)
|
| 2012 |
+
assert not (isinstance(snapped, dict) and "value" in snapped)
|
| 2013 |
+
|
| 2014 |
+
|
| 2015 |
def test_project_settings_export_import_round_trip(tmp_path) -> None:
|
| 2016 |
from app import export_project_settings, import_project_settings
|
| 2017 |
|
|
|
|
| 2022 |
settings_path, status = export_project_settings(
|
| 2023 |
records, None, 0.4, 0.4, None, "Circle Spiral raster", False, 1.2,
|
| 2024 |
6.0, 7.0, 4, "Down", "Horizontal", 15.0,
|
| 2025 |
+
pressure_variable=False, pressure_increase=0.3,
|
| 2026 |
split_columns=3, split_rows=2, split_overlapping_rows=True,
|
| 2027 |
split_overlap=1.5,
|
| 2028 |
)
|
|
|
|
| 2046 |
assert option_updates[8]["value"] == 0.4 # layer height
|
| 2047 |
assert option_updates[11]["value"] == 15.0 # nozzle speed
|
| 2048 |
assert option_updates[13]["value"] is False # pressure variable off
|
| 2049 |
+
assert option_updates[14]["value"] == 0.3 # pressure increase per layer
|
| 2050 |
+
assert option_updates[15]["value"] == 3 # split columns
|
| 2051 |
+
assert option_updates[16]["value"] == 2 # split rows
|
| 2052 |
+
assert option_updates[18]["value"] is True # overlapping rows
|
| 2053 |
+
assert option_updates[20]["value"] == 1.5 # overlap mm
|
| 2054 |
|
| 2055 |
# A file listed in the export but not loaded is reported.
|
| 2056 |
partial = import_project_settings([settings_path], _records_from_files(["egg_inside.stl"], None), None)
|
|
@@ -469,6 +469,31 @@ def test_pressure_variable_can_be_disabled(tmp_path) -> None:
|
|
| 469 |
assert _pressure_values(text) == [25.0, 25.1, 25.2]
|
| 470 |
|
| 471 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
def test_gcode_lead_in_runs_once_before_first_layer(tmp_path) -> None:
|
| 473 |
gcode_path = generate_vector_gcode(
|
| 474 |
_stack(box(0.0, 0.0, 0.5, 0.5), box(0.0, 0.0, 0.5, 0.5)),
|
|
|
|
| 469 |
assert _pressure_values(text) == [25.0, 25.1, 25.2]
|
| 470 |
|
| 471 |
|
| 472 |
+
def test_pressure_ramp_increment_is_adjustable(tmp_path) -> None:
|
| 473 |
+
# The "Pressure Increase Per Layer" option feeds
|
| 474 |
+
# increase_pressure_per_layer; both pressure formats honor it.
|
| 475 |
+
layer = box(0.0, 0.0, 2.0, 2.0)
|
| 476 |
+
|
| 477 |
+
def _generate(label: str, variable: bool) -> str:
|
| 478 |
+
path = generate_vector_gcode(
|
| 479 |
+
_stack(layer, layer, layer),
|
| 480 |
+
shape_name=label,
|
| 481 |
+
pressure=25,
|
| 482 |
+
valve=7,
|
| 483 |
+
port=3,
|
| 484 |
+
fil_width=1.0,
|
| 485 |
+
layer_height=1.0,
|
| 486 |
+
pressure_ramp_enabled=True,
|
| 487 |
+
pressure_variable=variable,
|
| 488 |
+
increase_pressure_per_layer=0.5,
|
| 489 |
+
output_dir=tmp_path / label,
|
| 490 |
+
)
|
| 491 |
+
return path.read_text()
|
| 492 |
+
|
| 493 |
+
assert _pressure_values(_generate("with_variable", True)) == [25.0, 25.5, 26.0]
|
| 494 |
+
assert _pressure_values(_generate("numeric", False)) == [25.0, 25.5, 26.0]
|
| 495 |
+
|
| 496 |
+
|
| 497 |
def test_gcode_lead_in_runs_once_before_first_layer(tmp_path) -> None:
|
| 498 |
gcode_path = generate_vector_gcode(
|
| 499 |
_stack(box(0.0, 0.0, 0.5, 0.5), box(0.0, 0.0, 0.5, 0.5)),
|