Skip to content

Commit 0643bed

Browse files
committed
Add traveler PDF example
1 parent 3dc88ac commit 0643bed

File tree

6 files changed

+734
-0
lines changed

6 files changed

+734
-0
lines changed

examples/.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.10

examples/JetBrainsMono-SemiBold.ttf

271 KB
Binary file not shown.

examples/batching-with-pdfs.py

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
"""
2+
Demo application that batches all STL files in a folder into .form files.
3+
4+
Usage: python3 batching-minimal.py ~/Documents/folder-of-stl-files
5+
6+
Optional flags:
7+
--auto-orient: Auto orient models
8+
--dental-mode: Use dental mode when auto orienting
9+
--auto-support: Auto support models
10+
--username: Username for login if uploading to a remote printer or Fleet Control queue
11+
--password: Password for login if uploading to a remote printer or Fleet Control queue
12+
--upload-to: Upload to a specific printer, IP Address, or Fleet Control Printer Group ID
13+
--traveler-pdf: Generates a traveler pdf
14+
"""
15+
16+
import argparse
17+
import pathlib
18+
import csv
19+
import sys
20+
import tempfile
21+
import typing
22+
from functools import reduce
23+
import operator
24+
25+
import pdf_generation
26+
27+
import requests
28+
import formlabs_local_api_minimal as formlabs
29+
30+
MACHINE_TYPE = "FRML-4-0"
31+
MATERIAL_CODE = "FLGPGR05"
32+
33+
34+
def list_files_in_directory(directory_path: pathlib.Path) -> list[pathlib.Path]:
35+
return [f for f in directory_path.iterdir() if f.is_file() and f.suffix == ".stl"]
36+
37+
38+
def create_scene():
39+
response = requests.post(
40+
"http://localhost:44388/scene/",
41+
json={
42+
"machine_type": MACHINE_TYPE,
43+
"material_code": MATERIAL_CODE,
44+
"layer_thickness_mm": 0.1,
45+
"print_setting": "DEFAULT",
46+
},
47+
)
48+
response.raise_for_status()
49+
return response.json()
50+
51+
52+
def get_screenshot(settings: dict) -> typing.BinaryIO:
53+
"""
54+
Calls the /scene/save-screenshot endpoint with the given settings and returns a temp file handle to the screenshot.
55+
:param settings: Settings to send to the /scene/save-screenshot endpoint
56+
:return: A temp file handle to the saved screenshot.
57+
"""
58+
temp = tempfile.NamedTemporaryFile("rb", suffix=".png", delete_on_close=False)
59+
screenshot_response = requests.post(
60+
"http://localhost:44388/scene/save-screenshot/",
61+
json={"file": temp.name, "image_size_px": 125} | settings,
62+
)
63+
if not screenshot_response.ok:
64+
print(screenshot_response.json())
65+
screenshot_response.raise_for_status()
66+
return temp
67+
68+
69+
parser = argparse.ArgumentParser(description="Process a folder path.")
70+
parser.add_argument("folder", type=str, help="Path to the folder")
71+
parser.add_argument("--auto-orient", action="store_true", help="Auto orient models")
72+
parser.add_argument(
73+
"--dental-mode", action="store_true", help="Use dental mode when auto orienting"
74+
)
75+
parser.add_argument("--auto-support", action="store_true", help="Auto support models")
76+
parser.add_argument("--username", type=str, help="Username for login")
77+
parser.add_argument("--password", type=str, help="Password for login")
78+
parser.add_argument(
79+
"--upload-to",
80+
type=str,
81+
help="Upload to a specific printer, IP Address, or Fleet Control Printer Group ID",
82+
)
83+
parser.add_argument(
84+
"--traveler-pdf", action="store_true", help="Generates a traveler pdf"
85+
)
86+
parser.add_argument(
87+
"--ps-path", type=str, help="Path to directory containing PreFormServer"
88+
)
89+
args = parser.parse_args()
90+
91+
directory_path = pathlib.Path(args.folder).resolve(strict=True)
92+
files_to_batch = list_files_in_directory(directory_path)
93+
print("Files to batch:")
94+
print(files_to_batch)
95+
current_batch = 1
96+
models_in_current_batch = []
97+
model_pdf_data = []
98+
CSV_RESULT_FILENAME = directory_path / "summary.csv"
99+
100+
pathToPreformServer = None
101+
ps_containing_dir = pathlib.Path(args.ps_path) if args.ps_path else pathlib.Path()
102+
if sys.platform == "win32":
103+
pathToPreformServer = (
104+
ps_containing_dir.resolve() / "PreFormServer/PreFormServer.exe"
105+
)
106+
elif sys.platform == "darwin":
107+
pathToPreformServer = (
108+
ps_containing_dir.resolve() / "PreFormServer.app/Contents/MacOS/PreFormServer"
109+
)
110+
else:
111+
print("Unsupported platform")
112+
sys.exit(1)
113+
114+
with formlabs.PreFormApi.start_preform_server(pathToPreformServer=pathToPreformServer):
115+
material_response = requests.get("http://localhost:44388/list-materials")
116+
material_response.raise_for_status()
117+
material_response_json = material_response.json()
118+
printer_types = list(
119+
filter(
120+
lambda printer_type: MACHINE_TYPE
121+
in printer_type["supported_machine_type_ids"],
122+
material_response_json["printer_types"],
123+
)
124+
)
125+
MACHINE_NAME: str = printer_types[0]["label"]
126+
materials = dict(
127+
map(
128+
lambda material: (
129+
material["material_settings"][0]["scene_settings"]["material_code"],
130+
material["label"],
131+
),
132+
reduce(
133+
operator.iconcat,
134+
map(lambda printer_type: printer_type["materials"], printer_types),
135+
[],
136+
),
137+
)
138+
)
139+
140+
MATERIAL_NAME = materials[MATERIAL_CODE]
141+
142+
if args.username and args.password:
143+
login_response = requests.post(
144+
"http://localhost:44388/login/",
145+
json={
146+
"username": args.username,
147+
"password": args.password,
148+
},
149+
)
150+
login_response.raise_for_status()
151+
152+
with open(CSV_RESULT_FILENAME, "w", newline="") as csvfile:
153+
csvwriter = csv.writer(csvfile)
154+
csvwriter.writerow(
155+
["Batch Number", "Batch Print Filename", "Model Source Filename"]
156+
)
157+
158+
def save_batch_form():
159+
global current_batch, models_in_current_batch, model_pdf_data
160+
form_file_name = f"batch_{current_batch}.form"
161+
save_path = directory_path / form_file_name
162+
save_form_response = requests.post(
163+
"http://localhost:44388/scene/save-form/",
164+
json={
165+
"file": str(save_path),
166+
},
167+
)
168+
save_form_response.raise_for_status()
169+
print(f"Saving batch {current_batch} to {save_path}")
170+
for i, model in enumerate(models_in_current_batch):
171+
print(f"{i + 1}. {model['file_name']}")
172+
csvwriter.writerow([current_batch, form_file_name, model["file_name"]])
173+
if args.traveler_pdf:
174+
pdf = pdf_generation.TravelerPDF(
175+
directory_path / f"summary-batch-{current_batch}.pdf",
176+
form_file_name,
177+
)
178+
179+
build_volume_image = get_screenshot({})
180+
181+
get_scene_response = requests.get("http://localhost:44388/scene")
182+
get_scene_response.raise_for_status()
183+
get_scene_response_json = get_scene_response.json()
184+
print_setting = get_scene_response_json["scene_settings"][
185+
"print_setting"
186+
]
187+
layer_thickness_mm = get_scene_response_json["scene_settings"][
188+
"layer_thickness_mm"
189+
]
190+
191+
pdf.add_header(
192+
MACHINE_NAME,
193+
MATERIAL_NAME,
194+
print_setting,
195+
layer_thickness_mm,
196+
build_volume_image,
197+
)
198+
pdf.add_parts(model_pdf_data)
199+
pdf.save()
200+
model_pdf_data = []
201+
current_batch += 1
202+
models_in_current_batch = []
203+
if args.upload_to:
204+
print(f"Uploading batch to {args.upload_to}")
205+
print_response = requests.post(
206+
"http://localhost:44388/scene/print/",
207+
json={
208+
"printer": args.upload_to,
209+
"job_name": form_file_name,
210+
},
211+
)
212+
print_response.raise_for_status()
213+
214+
create_scene()
215+
while len(files_to_batch) > 0:
216+
next_file = files_to_batch.pop()
217+
print(f"Importing {next_file}")
218+
import_model_response = requests.post(
219+
"http://localhost:44388/scene/import-model/",
220+
json={"file": str(directory_path / next_file)},
221+
)
222+
if not import_model_response.ok:
223+
continue
224+
import_model_response.raise_for_status()
225+
new_model_id = import_model_response.json()["id"]
226+
models_in_current_batch.append(
227+
{"model_id": new_model_id, "file_name": next_file}
228+
)
229+
if args.auto_orient:
230+
print(f"Auto orienting {new_model_id}")
231+
auto_orient_params = {"models": [new_model_id]}
232+
if args.dental_mode:
233+
auto_orient_params["mode"] = "DENTAL"
234+
auto_orient_params["tilt"] = 0
235+
auto_orient_response = requests.post(
236+
"http://localhost:44388/scene/auto-orient/",
237+
json=auto_orient_params,
238+
)
239+
auto_orient_response.raise_for_status()
240+
if args.traveler_pdf:
241+
image = get_screenshot({"models": [new_model_id]})
242+
import_model_response_json = import_model_response.json()
243+
model_dimensions = tuple(
244+
map(
245+
lambda dim: float(
246+
import_model_response_json["bounding_box"]["max_corner"][
247+
dim
248+
]
249+
)
250+
- float(
251+
import_model_response_json["bounding_box"]["min_corner"][
252+
dim
253+
]
254+
),
255+
["x", "y", "z"],
256+
)
257+
)
258+
model_pdf_data.append(
259+
pdf_generation.Model(next_file, image, model_dimensions)
260+
)
261+
if args.auto_support:
262+
print(f"Auto supporting {new_model_id}")
263+
auto_support_response = requests.post(
264+
"http://localhost:44388/scene/auto-support/",
265+
json={
266+
"models": [new_model_id],
267+
},
268+
)
269+
auto_support_response.raise_for_status()
270+
print(f"Auto layouting all")
271+
layout_response = requests.post(
272+
"http://localhost:44388/scene/auto-layout/",
273+
json={
274+
"models": "ALL",
275+
},
276+
)
277+
if layout_response.status_code != 200:
278+
print("Not all models can fit, removing model")
279+
model_data = models_in_current_batch.pop()
280+
model_pdf_data.pop()
281+
delete_response = requests.delete(
282+
f"http://localhost:44388/scene/models/{model_data['model_id']}/",
283+
)
284+
delete_response.raise_for_status()
285+
files_to_batch.append(model_data["file_name"])
286+
save_batch_form()
287+
print("Clearing scene")
288+
create_scene()
289+
290+
if len(models_in_current_batch) > 0:
291+
save_batch_form()

0 commit comments

Comments
 (0)