layer-rescue 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- layer_rescue/__init__.py +34 -0
- layer_rescue/__main__.py +5 -0
- layer_rescue/_version.py +1 -0
- layer_rescue/assets/icon.ico +0 -0
- layer_rescue/assets/icon.png +0 -0
- layer_rescue/cli.py +249 -0
- layer_rescue/core.py +29 -0
- layer_rescue/fileio.py +77 -0
- layer_rescue/flow.py +108 -0
- layer_rescue/gcode.py +241 -0
- layer_rescue/geometry.py +216 -0
- layer_rescue/gui.py +433 -0
- layer_rescue/gui_insert.py +298 -0
- layer_rescue/gui_widgets.py +73 -0
- layer_rescue/i18n.py +715 -0
- layer_rescue/insert.py +749 -0
- layer_rescue/machine_state.py +162 -0
- layer_rescue/preview.py +78 -0
- layer_rescue/resume.py +516 -0
- layer_rescue/supports.py +227 -0
- layer_rescue/toolpath.py +234 -0
- layer_rescue/wall.py +257 -0
- layer_rescue-1.0.1.dist-info/METADATA +267 -0
- layer_rescue-1.0.1.dist-info/RECORD +28 -0
- layer_rescue-1.0.1.dist-info/WHEEL +5 -0
- layer_rescue-1.0.1.dist-info/entry_points.txt +2 -0
- layer_rescue-1.0.1.dist-info/licenses/LICENSE +21 -0
- layer_rescue-1.0.1.dist-info/top_level.txt +1 -0
layer_rescue/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Layer Rescue: conservative G-code recovery for interrupted FDM prints."""
|
|
2
|
+
|
|
3
|
+
from ._version import __version__
|
|
4
|
+
from .core import (
|
|
5
|
+
Analysis,
|
|
6
|
+
LayerInfo,
|
|
7
|
+
ResumeError,
|
|
8
|
+
ResumeOptions,
|
|
9
|
+
ResumeReport,
|
|
10
|
+
ZReferenceMode,
|
|
11
|
+
analyze_gcode,
|
|
12
|
+
build_resume_gcode,
|
|
13
|
+
rewrite_gcode_file,
|
|
14
|
+
)
|
|
15
|
+
from .fileio import rewrite_with
|
|
16
|
+
from .insert import InsertOptions, InsertReport, build_insert_gcode, plan_insert
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"__version__",
|
|
20
|
+
"Analysis",
|
|
21
|
+
"LayerInfo",
|
|
22
|
+
"ResumeError",
|
|
23
|
+
"ResumeOptions",
|
|
24
|
+
"ResumeReport",
|
|
25
|
+
"ZReferenceMode",
|
|
26
|
+
"analyze_gcode",
|
|
27
|
+
"build_resume_gcode",
|
|
28
|
+
"rewrite_gcode_file",
|
|
29
|
+
"rewrite_with",
|
|
30
|
+
"InsertOptions",
|
|
31
|
+
"InsertReport",
|
|
32
|
+
"build_insert_gcode",
|
|
33
|
+
"plan_insert",
|
|
34
|
+
]
|
layer_rescue/__main__.py
ADDED
layer_rescue/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.1"
|
|
Binary file
|
|
Binary file
|
layer_rescue/cli.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from . import __version__
|
|
9
|
+
from .core import ResumeError, ResumeOptions, ZReferenceMode, analyze_gcode, rewrite_gcode_file
|
|
10
|
+
from .fileio import rewrite_with
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _parser() -> argparse.ArgumentParser:
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
prog="layer-rescue",
|
|
16
|
+
description="Create conservative layer-resume G-code for interrupted Bambu Lab P1S prints.",
|
|
17
|
+
)
|
|
18
|
+
parser.add_argument("gcode", nargs="?", help="G-code path; Bambu Studio appends this automatically")
|
|
19
|
+
selection = parser.add_mutually_exclusive_group()
|
|
20
|
+
selection.add_argument("--start-layer", type=int, help="first layer to print")
|
|
21
|
+
selection.add_argument("--last-layer", type=int, help="last successfully printed layer")
|
|
22
|
+
selection.add_argument(
|
|
23
|
+
"--part-height",
|
|
24
|
+
type=float,
|
|
25
|
+
help="insert mode: measured height (mm) of a loose part to seat in a printed wall and print on top of",
|
|
26
|
+
)
|
|
27
|
+
selection.add_argument("--part-layer", type=int, help="insert mode: last source layer the loose part contains")
|
|
28
|
+
parser.add_argument("--gui", action="store_true", help="open the graphical layer selector")
|
|
29
|
+
parser.add_argument("--analyze", action="store_true", help="print source information without modifying it")
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"--z-mode",
|
|
32
|
+
choices=[mode.value for mode in ZReferenceMode],
|
|
33
|
+
help="Z reference: retained after uninterrupted power, or manual after a restart",
|
|
34
|
+
)
|
|
35
|
+
parser.add_argument(
|
|
36
|
+
"--confirm-manual-z-aligned",
|
|
37
|
+
action="store_true",
|
|
38
|
+
help="confirm that the nozzle was aligned to touch the last successful layer",
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"--assume-z-known",
|
|
42
|
+
action="store_true",
|
|
43
|
+
help="deprecated alias for --z-mode retained",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--allow-untested",
|
|
47
|
+
action="store_true",
|
|
48
|
+
help="accept the risks and continue with a printer or multi-filament job Layer Rescue has not been tested on",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument("--no-home-corexy", action="store_true", help="do not emit the P1S G28 X CoreXY home")
|
|
51
|
+
parser.add_argument("--no-backup", action="store_true", help="do not create a sibling .bak file")
|
|
52
|
+
parser.add_argument("--nozzle-temp", type=int, help="override detected nozzle temperature")
|
|
53
|
+
parser.add_argument("--bed-temp", type=int, help="override detected bed temperature")
|
|
54
|
+
parser.add_argument("--z-lift", type=float, default=2.0, help="relative safety lift before CoreXY motion (default: 2.0)")
|
|
55
|
+
insert = parser.add_argument_group("insert mode (a loose part seated in a printed wall)")
|
|
56
|
+
insert.add_argument("--wall-height", type=float, help="holding wall height in mm (default: recommended)")
|
|
57
|
+
insert.add_argument("--clearance", type=float, default=0.25, help="gap between part and wall in mm (default: 0.25)")
|
|
58
|
+
insert.add_argument("--wall-lines", type=int, default=4, help="wall thickness in lines (default: 4)")
|
|
59
|
+
insert.add_argument("--brim", type=float, default=5.0, help="brim width around the wall in mm (default: 5)")
|
|
60
|
+
insert.add_argument("--no-chamfer", action="store_true", help="no lead-in chamfer at the wall rim")
|
|
61
|
+
insert.add_argument("--z-fine", type=float, default=0.0, help="added to the part height; negative squishes more")
|
|
62
|
+
insert.add_argument("--standby-temp", type=int, default=140, help="nozzle °C while paused, 0 = keep (default: 140)")
|
|
63
|
+
insert.add_argument("--adhesion-layers", type=int, default=2, help="hotter/slower/no-fan layers on the part (default: 2)")
|
|
64
|
+
insert.add_argument(
|
|
65
|
+
"--confirm-attended",
|
|
66
|
+
action="store_true",
|
|
67
|
+
help="confirm you will seat the part when the printer pauses and watch the first layers",
|
|
68
|
+
)
|
|
69
|
+
insert.add_argument(
|
|
70
|
+
"--no-reprint-supports",
|
|
71
|
+
action="store_true",
|
|
72
|
+
help="do not print the supports below the part height before the pause",
|
|
73
|
+
)
|
|
74
|
+
insert.add_argument("--preview-svg", help="insert mode: also write a top-view SVG of the wall to this path")
|
|
75
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
76
|
+
return parser
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _analysis_json(path: Path) -> str:
|
|
80
|
+
from .machine_state import untested_setup
|
|
81
|
+
|
|
82
|
+
analysis = analyze_gcode(path.read_text(encoding="utf-8", errors="replace"))
|
|
83
|
+
return json.dumps(
|
|
84
|
+
{
|
|
85
|
+
"file": str(path),
|
|
86
|
+
"printer_model": analysis.printer_model,
|
|
87
|
+
"filament_type": analysis.filament_type,
|
|
88
|
+
"first_layer": analysis.first_layer,
|
|
89
|
+
"last_layer": analysis.last_layer,
|
|
90
|
+
"total_layers": analysis.total_layers,
|
|
91
|
+
"first_z": analysis.layers[0].z,
|
|
92
|
+
"last_z": analysis.layers[-1].z,
|
|
93
|
+
"sha256": analysis.source_sha256,
|
|
94
|
+
"warnings": analysis.warnings,
|
|
95
|
+
"untested_setup": untested_setup(analysis),
|
|
96
|
+
},
|
|
97
|
+
indent=2,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _run_insert(args: argparse.Namespace, path: Path) -> int:
|
|
102
|
+
from .insert import InsertOptions, build_insert_gcode, plan_insert
|
|
103
|
+
|
|
104
|
+
if not args.confirm_attended:
|
|
105
|
+
raise ResumeError(
|
|
106
|
+
"Insert mode requires --confirm-attended: you must seat the part when the printer pauses "
|
|
107
|
+
"and watch the first layers."
|
|
108
|
+
)
|
|
109
|
+
part_height = args.part_height
|
|
110
|
+
if part_height is None:
|
|
111
|
+
analysis = analyze_gcode(path.read_text(encoding="utf-8", errors="replace"))
|
|
112
|
+
part_height = analysis.layer(args.part_layer).z
|
|
113
|
+
options = InsertOptions(
|
|
114
|
+
part_height_mm=part_height,
|
|
115
|
+
wall_height_mm=args.wall_height,
|
|
116
|
+
clearance_mm=args.clearance,
|
|
117
|
+
wall_lines=args.wall_lines,
|
|
118
|
+
brim_mm=args.brim,
|
|
119
|
+
chamfer=not args.no_chamfer,
|
|
120
|
+
z_fine_mm=args.z_fine,
|
|
121
|
+
standby_temperature=args.standby_temp,
|
|
122
|
+
adhesion_layers=args.adhesion_layers,
|
|
123
|
+
reprint_supports=not args.no_reprint_supports,
|
|
124
|
+
allow_untested=args.allow_untested,
|
|
125
|
+
z_lift_mm=args.z_lift,
|
|
126
|
+
nozzle_temperature=args.nozzle_temp,
|
|
127
|
+
bed_temperature=args.bed_temp,
|
|
128
|
+
)
|
|
129
|
+
if args.preview_svg:
|
|
130
|
+
from .preview import plan_svg
|
|
131
|
+
|
|
132
|
+
plan = plan_insert(path.read_text(encoding="utf-8", errors="replace"), options)
|
|
133
|
+
Path(args.preview_svg).write_text(plan_svg(plan), encoding="utf-8")
|
|
134
|
+
report, backup = rewrite_with(
|
|
135
|
+
path, lambda text: build_insert_gcode(text, options), create_backup=not args.no_backup
|
|
136
|
+
)
|
|
137
|
+
print(
|
|
138
|
+
json.dumps(
|
|
139
|
+
{
|
|
140
|
+
"status": "converted",
|
|
141
|
+
"mode": "insert",
|
|
142
|
+
"part_height_mm": report.part_height_mm,
|
|
143
|
+
"part_layer": report.part_layer,
|
|
144
|
+
"resume_layer": report.resume_layer,
|
|
145
|
+
"total_layers": report.total_layers,
|
|
146
|
+
"z_offset_mm": report.z_offset_mm,
|
|
147
|
+
"first_layer_thickness_mm": report.first_layer_thickness_mm,
|
|
148
|
+
"wall_layers": report.wall_layers,
|
|
149
|
+
"wall_top_z": report.wall_top_z,
|
|
150
|
+
"park_z": report.park_z,
|
|
151
|
+
"wall_filament_mm": report.wall_filament_mm,
|
|
152
|
+
"support_layers": report.support_layers,
|
|
153
|
+
"last_printed_layer": report.last_printed_layer,
|
|
154
|
+
"nozzle_temperature": report.nozzle_temperature,
|
|
155
|
+
"bed_temperature": report.bed_temperature,
|
|
156
|
+
"backup": str(backup) if backup else None,
|
|
157
|
+
"output_sha256": report.output_sha256,
|
|
158
|
+
"warnings": report.warnings,
|
|
159
|
+
},
|
|
160
|
+
indent=2,
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
return 0
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def post_processing_command() -> str:
|
|
167
|
+
"""The command to paste into Bambu Studio's Post-processing Scripts field."""
|
|
168
|
+
if getattr(sys, "frozen", False):
|
|
169
|
+
return f'"{sys.executable}"'
|
|
170
|
+
return f'"{sys.executable}" -m layer_rescue'
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def main(argv: list[str] | None = None) -> int:
|
|
174
|
+
args = _parser().parse_args(argv)
|
|
175
|
+
if not args.gcode:
|
|
176
|
+
if getattr(sys, "frozen", False):
|
|
177
|
+
# Opened directly (double-click) instead of from Bambu Studio: explain the setup.
|
|
178
|
+
from .gui import show_setup_info
|
|
179
|
+
|
|
180
|
+
return show_setup_info(post_processing_command())
|
|
181
|
+
_parser().error("a G-code path is required")
|
|
182
|
+
path = Path(args.gcode).expanduser().resolve()
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
if args.analyze:
|
|
186
|
+
print(_analysis_json(path))
|
|
187
|
+
return 0
|
|
188
|
+
|
|
189
|
+
if args.part_height is not None or args.part_layer is not None:
|
|
190
|
+
return _run_insert(args, path)
|
|
191
|
+
|
|
192
|
+
if args.gui or (args.start_layer is None and args.last_layer is None):
|
|
193
|
+
from .gui import launch
|
|
194
|
+
|
|
195
|
+
return launch(path)
|
|
196
|
+
|
|
197
|
+
if args.assume_z_known and args.z_mode not in {None, ZReferenceMode.RETAINED.value}:
|
|
198
|
+
raise ResumeError(
|
|
199
|
+
"--assume-z-known is an alias for --z-mode retained and cannot be combined with manual mode."
|
|
200
|
+
)
|
|
201
|
+
z_mode_value = args.z_mode or (ZReferenceMode.RETAINED.value if args.assume_z_known else None)
|
|
202
|
+
if z_mode_value is None:
|
|
203
|
+
raise ResumeError("Batch conversion requires --z-mode retained or --z-mode manual.")
|
|
204
|
+
z_mode = ZReferenceMode(z_mode_value)
|
|
205
|
+
if z_mode is ZReferenceMode.MANUAL and not args.confirm_manual_z_aligned:
|
|
206
|
+
raise ResumeError(
|
|
207
|
+
"Manual mode requires --confirm-manual-z-aligned after the nozzle touches the last successful layer."
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
start_layer = args.start_layer if args.start_layer is not None else args.last_layer + 1
|
|
211
|
+
report = rewrite_gcode_file(
|
|
212
|
+
path,
|
|
213
|
+
ResumeOptions(
|
|
214
|
+
start_layer=start_layer,
|
|
215
|
+
z_reference_mode=z_mode,
|
|
216
|
+
home_corexy=not args.no_home_corexy,
|
|
217
|
+
z_lift_mm=args.z_lift,
|
|
218
|
+
nozzle_temperature=args.nozzle_temp,
|
|
219
|
+
bed_temperature=args.bed_temp,
|
|
220
|
+
allow_untested=args.allow_untested,
|
|
221
|
+
),
|
|
222
|
+
create_backup=not args.no_backup,
|
|
223
|
+
)
|
|
224
|
+
print(
|
|
225
|
+
json.dumps(
|
|
226
|
+
{
|
|
227
|
+
"status": "converted",
|
|
228
|
+
"start_layer": report.start_layer,
|
|
229
|
+
"total_layers": report.total_layers,
|
|
230
|
+
"start_z": report.start_z,
|
|
231
|
+
"nozzle_temperature": report.nozzle_temperature,
|
|
232
|
+
"bed_temperature": report.bed_temperature,
|
|
233
|
+
"z_reference_mode": report.z_reference_mode.value,
|
|
234
|
+
"reference_z": report.reference_z,
|
|
235
|
+
"backup": str(report.backup_path) if report.backup_path else None,
|
|
236
|
+
"output_sha256": report.output_sha256,
|
|
237
|
+
"warnings": report.warnings,
|
|
238
|
+
},
|
|
239
|
+
indent=2,
|
|
240
|
+
)
|
|
241
|
+
)
|
|
242
|
+
return 0
|
|
243
|
+
except (OSError, ResumeError) as exc:
|
|
244
|
+
print(f"layer-rescue: error: {exc}", file=sys.stderr)
|
|
245
|
+
return 2
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
if __name__ == "__main__":
|
|
249
|
+
raise SystemExit(main())
|
layer_rescue/core.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Backward-compatible facade; the implementation lives in the gcode, machine_state, resume and fileio modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .fileio import _available_backup_path, rewrite_gcode_file, rewrite_with
|
|
6
|
+
from .gcode import * # noqa: F401,F403
|
|
7
|
+
from .gcode import ( # noqa: F401
|
|
8
|
+
_code,
|
|
9
|
+
_command,
|
|
10
|
+
_find_layers,
|
|
11
|
+
_find_marker,
|
|
12
|
+
_first_number,
|
|
13
|
+
_format_number,
|
|
14
|
+
_next_extrusion_mode,
|
|
15
|
+
_parameter,
|
|
16
|
+
_parse_config,
|
|
17
|
+
)
|
|
18
|
+
from .machine_state import MachineState, _scan_machine_state, _validate_supported_source # noqa: F401
|
|
19
|
+
from .resume import * # noqa: F401,F403
|
|
20
|
+
from .resume import ( # noqa: F401
|
|
21
|
+
_first_layer_xy,
|
|
22
|
+
_format_z_delta,
|
|
23
|
+
_relative_z_move,
|
|
24
|
+
_relativize_z,
|
|
25
|
+
_resume_preamble,
|
|
26
|
+
_retraction_length,
|
|
27
|
+
_validate_output,
|
|
28
|
+
_z_reference_mode,
|
|
29
|
+
)
|
layer_rescue/fileio.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Atomic in-place rewriting of the G-code file Bambu Studio hands to the post-processor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import tempfile
|
|
8
|
+
from dataclasses import replace
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Callable, TypeVar
|
|
11
|
+
|
|
12
|
+
from .gcode import ResumeError
|
|
13
|
+
from .resume import ResumeOptions, ResumeReport, build_resume_gcode
|
|
14
|
+
|
|
15
|
+
R = TypeVar("R")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _available_backup_path(path: Path) -> Path:
|
|
19
|
+
candidate = path.with_name(path.name + ".layer-rescue.bak")
|
|
20
|
+
counter = 1
|
|
21
|
+
while candidate.exists():
|
|
22
|
+
candidate = path.with_name(path.name + f".layer-rescue.{counter}.bak")
|
|
23
|
+
counter += 1
|
|
24
|
+
return candidate
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def rewrite_with(
|
|
29
|
+
path: str | os.PathLike[str],
|
|
30
|
+
builder: Callable[[str], tuple[str, R]],
|
|
31
|
+
*,
|
|
32
|
+
create_backup: bool = True,
|
|
33
|
+
) -> tuple[R, Path | None]:
|
|
34
|
+
"""Run ``builder`` on the file's text and replace the file atomically, keeping a backup."""
|
|
35
|
+
source_path = Path(path).expanduser().resolve()
|
|
36
|
+
if not source_path.is_file():
|
|
37
|
+
raise ResumeError(f"G-code file does not exist: {source_path}")
|
|
38
|
+
if source_path.suffix.lower() not in {".gcode", ".gco"}:
|
|
39
|
+
raise ResumeError("Post-processing MVP accepts raw .gcode files only.")
|
|
40
|
+
|
|
41
|
+
text = source_path.read_text(encoding="utf-8", errors="replace")
|
|
42
|
+
output, report = builder(text)
|
|
43
|
+
backup_path: Path | None = None
|
|
44
|
+
if create_backup:
|
|
45
|
+
backup_path = _available_backup_path(source_path)
|
|
46
|
+
shutil.copy2(source_path, backup_path)
|
|
47
|
+
|
|
48
|
+
temp_name: str | None = None
|
|
49
|
+
try:
|
|
50
|
+
with tempfile.NamedTemporaryFile(
|
|
51
|
+
mode="w",
|
|
52
|
+
encoding="utf-8",
|
|
53
|
+
newline="",
|
|
54
|
+
dir=source_path.parent,
|
|
55
|
+
prefix=source_path.name + ".",
|
|
56
|
+
suffix=".tmp",
|
|
57
|
+
delete=False,
|
|
58
|
+
) as handle:
|
|
59
|
+
temp_name = handle.name
|
|
60
|
+
handle.write(output)
|
|
61
|
+
handle.flush()
|
|
62
|
+
os.fsync(handle.fileno())
|
|
63
|
+
os.replace(temp_name, source_path)
|
|
64
|
+
finally:
|
|
65
|
+
if temp_name and Path(temp_name).exists():
|
|
66
|
+
Path(temp_name).unlink()
|
|
67
|
+
return report, backup_path
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def rewrite_gcode_file(
|
|
71
|
+
path: str | os.PathLike[str],
|
|
72
|
+
options: ResumeOptions,
|
|
73
|
+
*,
|
|
74
|
+
create_backup: bool = True,
|
|
75
|
+
) -> ResumeReport:
|
|
76
|
+
report, backup_path = rewrite_with(path, lambda text: build_resume_gcode(text, options), create_backup=create_backup)
|
|
77
|
+
return replace(report, backup_path=backup_path)
|
layer_rescue/flow.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Extrusion amounts and print speeds for toolpaths Layer Rescue generates itself."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Mapping
|
|
8
|
+
|
|
9
|
+
from .gcode import NUMBER_RE
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def extrusion_per_mm(width: float, height: float, filament_diameter: float, flow_ratio: float = 1.0) -> float:
|
|
13
|
+
"""Filament length per mm of path for a rounded-rectangle bead (the Slic3r/Bambu flow model)."""
|
|
14
|
+
if width <= 0 or height <= 0 or filament_diameter <= 0:
|
|
15
|
+
raise ValueError("width, height and filament diameter must be positive")
|
|
16
|
+
width = max(width, height)
|
|
17
|
+
bead_area = (width - height) * height + math.pi * (height / 2) ** 2
|
|
18
|
+
filament_area = math.pi * (filament_diameter / 2) ** 2
|
|
19
|
+
return bead_area / filament_area * flow_ratio
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _number(config: Mapping[str, str], key: str, *, relative_to: float | None = None) -> float | None:
|
|
23
|
+
value = config.get(key)
|
|
24
|
+
if not value:
|
|
25
|
+
return None
|
|
26
|
+
match = NUMBER_RE.search(value)
|
|
27
|
+
if not match:
|
|
28
|
+
return None
|
|
29
|
+
number = float(match.group(0))
|
|
30
|
+
if "%" in value.split(",")[0] and relative_to is not None:
|
|
31
|
+
return number / 100 * relative_to
|
|
32
|
+
return number
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _first(config: Mapping[str, str], *keys: str, default: float, relative_to: float | None = None) -> float:
|
|
36
|
+
for key in keys:
|
|
37
|
+
value = _number(config, key, relative_to=relative_to)
|
|
38
|
+
if value is not None and value > 0:
|
|
39
|
+
return value
|
|
40
|
+
return default
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class FlowSettings:
|
|
45
|
+
nozzle_diameter: float = 0.4
|
|
46
|
+
line_width: float = 0.42
|
|
47
|
+
first_layer_line_width: float = 0.5
|
|
48
|
+
filament_diameter: float = 1.75
|
|
49
|
+
flow_ratio: float = 0.98
|
|
50
|
+
wall_speed: float = 60.0 # mm/s
|
|
51
|
+
first_layer_speed: float = 30.0
|
|
52
|
+
travel_speed: float = 200.0
|
|
53
|
+
support_speed: float = 60.0
|
|
54
|
+
retraction_length: float = 0.8
|
|
55
|
+
retraction_speed: float = 30.0
|
|
56
|
+
z_hop: float = 0.4
|
|
57
|
+
min_layer_time: float = 8.0 # s
|
|
58
|
+
min_speed: float = 20.0
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def wall_feed(self) -> int:
|
|
62
|
+
return int(round(self.wall_speed * 60))
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def first_layer_feed(self) -> int:
|
|
66
|
+
return int(round(self.first_layer_speed * 60))
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def support_feed(self) -> int:
|
|
70
|
+
return int(round(self.support_speed * 60))
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def travel_feed(self) -> int:
|
|
74
|
+
return int(round(self.travel_speed * 60))
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def retraction_feed(self) -> int:
|
|
78
|
+
return int(round(self.retraction_speed * 60))
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# The wall is a fixture, not a show piece: print it at moderate speeds whatever the profile says.
|
|
82
|
+
MAX_WALL_SPEED = 100.0
|
|
83
|
+
MAX_FIRST_LAYER_SPEED = 40.0
|
|
84
|
+
MAX_TRAVEL_SPEED = 300.0
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def flow_settings_from_config(config: Mapping[str, str]) -> FlowSettings:
|
|
88
|
+
nozzle = _first(config, "nozzle_diameter", default=0.4)
|
|
89
|
+
return FlowSettings(
|
|
90
|
+
nozzle_diameter=nozzle,
|
|
91
|
+
line_width=_first(config, "outer_wall_line_width", "line_width", default=nozzle * 1.05, relative_to=nozzle),
|
|
92
|
+
first_layer_line_width=_first(
|
|
93
|
+
config, "initial_layer_line_width", "first_layer_extrusion_width", default=nozzle * 1.25, relative_to=nozzle
|
|
94
|
+
),
|
|
95
|
+
filament_diameter=_first(config, "filament_diameter", default=1.75),
|
|
96
|
+
flow_ratio=min(max(_first(config, "filament_flow_ratio", "extrusion_multiplier", default=1.0), 0.5), 1.5),
|
|
97
|
+
wall_speed=min(_first(config, "outer_wall_speed", "external_perimeter_speed", default=60.0), MAX_WALL_SPEED),
|
|
98
|
+
first_layer_speed=min(
|
|
99
|
+
_first(config, "initial_layer_speed", "first_layer_speed", default=30.0), MAX_FIRST_LAYER_SPEED
|
|
100
|
+
),
|
|
101
|
+
travel_speed=min(_first(config, "travel_speed", default=200.0), MAX_TRAVEL_SPEED),
|
|
102
|
+
support_speed=min(_first(config, "support_speed", default=60.0), MAX_WALL_SPEED),
|
|
103
|
+
retraction_length=min(_first(config, "retraction_length", default=0.8), 5.0),
|
|
104
|
+
retraction_speed=min(_first(config, "retraction_speed", default=30.0), 60.0),
|
|
105
|
+
z_hop=min(_first(config, "z_hop", "retract_lift_above", default=0.4), 2.0),
|
|
106
|
+
min_layer_time=_first(config, "slow_down_layer_time", "slowdown_below_layer_time", default=8.0),
|
|
107
|
+
min_speed=_first(config, "slow_down_min_speed", "min_print_speed", default=20.0),
|
|
108
|
+
)
|