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