CyGuy8 Claude Fable 5 commited on
Commit
fb202c1
·
1 Parent(s): ff64d30

Adjustable Pressure Increase Per Layer option, snapped to tenths

Browse files

New "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>

Files changed (4) hide show
  1. README.md +1 -1
  2. app.py +77 -16
  3. tests/test_nozzle_spacing.py +17 -5
  4. tests/test_vector_gcode.py +25 -0
README.md CHANGED
@@ -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.
app.py CHANGED
@@ -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
  "&#9888;&#65039; **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(20))
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 0.1 psi per layer — warn when a print would hit
5270
- # the ceiling (the files clamp there, so later layers hold the maximum).
 
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 + 0.1 * max(0, layer_count - 1)
5282
  if peak > MAX_PRESSURE_PSI + 1e-9:
5283
  messages.insert(
5284
  0,
5285
  f"&#9888;&#65039; The pressure ramp reaches the {MAX_PRESSURE_PSI:g} psi regulator "
5286
- f"limit before the last layer (base {peak_base:g} psi + 0.1/layer x {layer_count} "
 
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
- gcode_pressure_ramp_enabled = gr.Checkbox(label="Increase Pressure Each Layer", value=True)
5812
- gcode_pressure_variable = gr.Checkbox(
5813
- label="Editable Pressure Variable",
5814
- value=True,
5815
- info=(
5816
- "Start each pressure-owning file with a pressureN variable that every "
5817
- "pressure command references, so the print pressure can be changed by "
5818
- "editing one number in the file — no regeneration needed. Uncheck to "
5819
- "write plain numeric pressure commands instead."
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
  "&#9888;&#65039; **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"&#9888;&#65039; 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(
tests/test_nozzle_spacing.py CHANGED
@@ -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 # split columns
2039
- assert option_updates[15]["value"] == 2 # split rows
2040
- assert option_updates[17]["value"] is True # overlapping rows
2041
- assert option_updates[19]["value"] == 1.5 # overlap mm
 
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)
tests/test_vector_gcode.py CHANGED
@@ -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)),