-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path_02cutstl.py
More file actions
205 lines (163 loc) · 6.41 KB
/
_02cutstl.py
File metadata and controls
205 lines (163 loc) · 6.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# _02cutstl.py — Clean, safe interior cutting with top-gap protection
import os
import subprocess
from stl import mesh
from _10config import get_slic3r_binary, CUT_CONFIG
# -----------------------------------------------------------
# READ CUT HEIGHTS
# -----------------------------------------------------------
def _read_cut_heights(cuts_file):
"""
Read cut heights from cuts.txt → sorted list of floats.
Supports:
- OLD format: one Z per line
20.0
30.0
37.641
45.3
60.0
- NEW format: three columns
index flag z_value/TOP
1 0 20.0000
2 1 30.0000
3 0 37.6410
4 1 45.3000
5 1 TOP
Ignores:
- anything <= ignore_min (usually 0 mm)
- lines with 'TOP' as Z
"""
ignore_min = CUT_CONFIG.get("ignore_cuts_at_or_below_mm", 0.0)
heights = []
with open(cuts_file, "r") as f:
for line in f:
line = line.strip()
if not line:
continue
# Replace comma decimal separator if any
line = line.replace(",", ".")
tokens = line.split()
# Decide which token is the Z component
z_token = None
if len(tokens) >= 3:
# New 3-column format: idx, flag, z_value/TOP
z_token = tokens[2]
elif len(tokens) >= 1:
# Old format: single Z per line (or similar)
z_token = tokens[-1]
if z_token is None:
continue
if z_token.upper() == "TOP":
# TOP is always the final model height, not a cut plane
continue
try:
val = float(z_token)
except ValueError:
continue
if val > ignore_min:
heights.append(val)
return sorted(set(heights))
# -----------------------------------------------------------
# MAIN CUT FUNCTION
# -----------------------------------------------------------
def cutSTL(in_stl, cuts_file, out_folder):
"""
Cut STL into stacked parts using safe interior cuts:
• NEVER cut at z_min or z_max
• NEVER cut too close to top (min_top_gap_mm)
• Use safety offset (cut slightly LOWER inside geometry)
• Works for ANY model height or shape
Cut heights are read from cuts_file. In the NEW pipeline, this file
is typically a 3-column cuts.txt, but legacy one-column formats are
still supported.
"""
print("[cutSTL] Loading STL:", in_stl)
os.makedirs(out_folder, exist_ok=True)
# --- Load STL for z-min/max detection ---
m = mesh.Mesh.from_file(in_stl)
verts = m.vectors.reshape(-1, 3)
z_min = float(verts[:, 2].min())
z_max = float(verts[:, 2].max())
height = z_max - z_min
print(f" Model height = {height:.3f} mm (z_min={z_min:.3f}, z_max={z_max:.3f})")
# --- Read cut heights ---
raw = _read_cut_heights(cuts_file)
print(" Raw cut heights from cuts.txt (after ignore_min):", raw)
# ---------------------------------------------------------
# RULES FOR INTERIOR CUT SELECTION
# ---------------------------------------------------------
# 1. Remove z_min
# 2. Remove z_max
# 3. Remove cuts too close to the top (to avoid thin caps)
min_top_gap = CUT_CONFIG.get("min_top_gap_mm", 2.0)
interior = [
z for z in raw
if (z > z_min + 1e-6) and (z < (z_max - min_top_gap))
]
print(" Interior usable cuts:", interior)
# ---------------------------------------------------------
# If no cuts → copy model as single segment
# ---------------------------------------------------------
base_name = os.path.splitext(os.path.basename(in_stl))[0]
if not interior:
dst = os.path.join(out_folder, f"{base_name}_1.stl")
print(" No interior cuts → output single STL:", dst)
os.replace(in_stl, dst)
return
# ---------------------------------------------------------
# Cutting configuration
# ---------------------------------------------------------
safety_offset = CUT_CONFIG.get("safety_offset_mm", 0.7)
safety_min_edge = CUT_CONFIG.get("safety_min_margin_mm", 0.2)
slicer = get_slic3r_binary()
# Important: process from top → down
interior_desc = sorted(interior, reverse=True)
tmp_parts = []
tmp_counter = 0
# ---------------------------------------------------------
# PERFORM CUTS
# ---------------------------------------------------------
for cut_z in interior_desc:
# Effective cut is slightly below --> ensures slicing inside the model
cut_eff = cut_z - safety_offset
# Clamp to ensure we never cut below z_min
if cut_eff < z_min + safety_min_edge:
cut_eff = z_min + safety_min_edge
print(f" Cutting at nominal {cut_z:.3f} → effective {cut_eff:.3f}")
cmd = [
slicer,
"--dont-arrange",
"--cut", str(cut_eff),
"-o", "out.stl",
in_stl
]
print(" Running:", " ".join(cmd))
res = subprocess.run(cmd)
if res.returncode != 0:
raise RuntimeError(f"Slic3r cut failed at plane {cut_eff:.3f}")
upper = in_stl + "_upper.stl"
lower = in_stl + "_lower.stl"
if not os.path.exists(upper):
raise FileNotFoundError(f" ERROR: missing file {upper}")
if not os.path.exists(lower):
raise FileNotFoundError(f" ERROR: missing file {lower}")
# Move upper chunk to temp list (this is the top fragment)
tmp_name = os.path.join(out_folder, f"_tmp_seg_{tmp_counter}.stl")
os.replace(upper, tmp_name)
tmp_parts.append(tmp_name)
tmp_counter += 1
# Continue cutting by replacing current model with lower part
os.replace(lower, in_stl)
# After all cuts → in_stl = bottom-most chunk
bottom_chunk = in_stl
# Final ordering: bottom-first → top-last
ordered = [bottom_chunk] + list(reversed(tmp_parts))
print(" Final ordered parts:")
for i, part in enumerate(ordered, 1):
print(f" Part {i}: {part}")
# Rename final segments
for i, src in enumerate(ordered, 1):
dst = os.path.join(out_folder, f"{base_name}_{i}.stl")
os.replace(src, dst)
print(f" → Saved: {dst}")
print("[cutSTL] Finished.\n")