# /// script # requires-python = ">=3.12" # dependencies = ["numpy"] # # [tool.orcaslicer.plugin] # name = "Orca Host Inspector Panel" # description = "An interactive panel that browses the whole orca.host read-only API and demos every orca.host.ui facility." # author = "OrcaSlicer" # version = "2.0.0" # /// """Orca Host Inspector — the worked sample for `orca.host` and `orca.host.ui`. Run it from the Plugins dialog. It opens a NON-MODAL window (OrcaSlicer stays usable) with a sidebar of sections, each exercising one part of the API: Overview plater state, scene statistics, current printer/process/filaments Presets every PresetCollection, preset flags, any preset's full config Config the complete merged full_config as a searchable table Model Model -> Object -> Volume/Instance tree with lazy mesh geometry Assembly objects grouped by module_name, per-instance assemble transforms UI Toolkit live demos of message/show_dialog/create_window/ProgressDialog The page and the plugin talk JSON through the injected `window.orca` bridge: page --orca.postMessage({command:'fetch', section})--> plugin.on_message() page --orca.postMessage({command:'mesh', object, volume})--> " page --orca.postMessage({command:'ui', action, ...})--> " plugin --win.post({command:'section'|'mesh'|..., ok, data})--> page (orca.onMessage) Everything is fetched lazily: a section is built only when it is first shown, and heavy mesh geometry (zero-copy numpy arrays, world-space math, the little point-cloud previews) only when you ask for it. "Refresh" drops the cache and refetches the visible section. Style rules the page follows (see the plugin docs): - no hardcoded colors — only the host-injected --orca-* theme variables, so the panel matches light and dark mode automatically; - self-contained HTML (inline CSS/JS, no external resources); - on_message runs on the UI thread, so long work (the progress demos) is offloaded to a thread and results are pushed back with win.post(). numpy is declared as a dependency for the mesh arrays and 4x4 matrices; every numpy-dependent feature degrades gracefully if it is missing. The assembly transforms need bindings added in July 2026 — on older builds the panel shows what is missing instead of failing. """ import re import threading import time import orca try: import numpy as np except Exception: np = None # Soft caps so a heavy scene cannot produce an unusably large payload. When a # cap trims output the payload says so explicitly rather than truncating silently. MAX_OBJECTS = 50 MAX_VOLUMES = 20 MAX_INSTANCES = 20 MAX_PRESET_NAMES = 500 MAX_PREVIEW_POINTS = 4000 # --------------------------------------------------------------------------- # # small converters — every builder returns plain JSON-able dicts/lists. # Cast numpy scalars with float()/int(): the bridge serializes unknown types # with str(), which would silently turn them into strings. # --------------------------------------------------------------------------- # def vec(v): return [round(float(c), 4) for c in v] def bbox_dict(bb): """host.BoundingBox -> dict (or None when the box is undefined).""" if not bb.defined: return None return {"min": vec(bb.min), "max": vec(bb.max), "size": vec(bb.size), "center": vec(bb.center), "radius": round(float(bb.radius), 4)} def enum_name(value): return getattr(value, "name", str(value)) def config_rows(keys, get_value): """Sorted [{k, v}] for a config; None values become ''. Complete, not sampled.""" return [{"k": k, "v": "" if (v := get_value(k)) is None else str(v)} for k in sorted(keys)] def preset_dict(preset): """Every documented host.Preset attribute, in one dict.""" return { "name": preset.name, "alias": preset.alias, "label": preset.label(), "type": enum_name(preset.type), "file": preset.file, "bundle_id": preset.bundle_id, "config_key_count": len(preset.config_keys()), "is_default": preset.is_default, "is_system": preset.is_system, "is_user": preset.is_user(), "is_external": preset.is_external, "is_visible": preset.is_visible, "is_dirty": preset.is_dirty, "is_compatible": preset.is_compatible, "is_project_embedded": preset.is_project_embedded, "is_from_bundle": preset.is_from_bundle(), } def split_config_list(value): """Split a serialized per-extruder option ("#FF0000;#00FF00") into parts.""" if value is None: return [] return [part.strip().strip('"') for part in str(value).split(";")] # --------------------------------------------------------------------------- # # section builders # --------------------------------------------------------------------------- # def build_overview(): plater = orca.host.plater() model = orca.host.model() bundle = orca.host.preset_bundle() objects = model.objects() cfg = bundle.full_config_value colors = split_config_list(cfg("filament_colour")) types = split_config_list(cfg("filament_type")) filaments = [] for i, preset in enumerate(bundle.current_filament_presets()): filaments.append({ "name": preset.name if preset else "", "color": colors[i] if i < len(colors) else "", "type": types[i] if i < len(types) else "", "is_system": bool(preset.is_system) if preset else False, "is_dirty": bool(preset.is_dirty) if preset else False, }) printer = bundle.current_printer_preset() process = bundle.current_process_preset() return { "plater": { "project_dirty": plater.is_project_dirty(), "presets_dirty": plater.is_presets_dirty(), "in_snapshot": plater.inside_snapshot_capture(), }, "stats": { "objects": len(objects), # derived from instances: ModelObject.printable is only the import-time flag "printable_objects": sum(1 for o in objects if printable_instance_count(o)), "volumes": sum(o.volume_count() for o in objects), "instances": sum(o.instance_count() for o in objects), "triangles": sum(o.facets_count() for o in objects), "materials": model.material_count(), "plate_index": model.current_plate_index(), "max_z": round(float(model.max_z()), 3), "config_keys": len(bundle.full_config_keys()), }, "bbox": bbox_dict(model.bounding_box()), "bbox_approx": bbox_dict(model.bounding_box_approx()), "painted": { "support": model.is_fdm_support_painted(), "seam": model.is_seam_painted(), "multimaterial": model.is_mm_painted(), "fuzzy_skin": model.is_fuzzy_skin_painted(), }, "design": {"designer": model.designer(), "design_id": model.design_id(), "model_id": model.id()}, "setup": { "printer": {"name": printer.name, "is_system": printer.is_system, "is_dirty": printer.is_dirty}, "process": {"name": process.name, "is_system": process.is_system, "is_dirty": process.is_dirty}, "filaments": filaments, "highlights": [{"k": key, "v": str(cfg(key))} for key in ("printer_model", "nozzle_diameter", "printable_height", "layer_height", "sparse_infill_density", "enable_prime_tower") if cfg(key) is not None], }, } # label -> how to reach the PresetCollection on the bundle COLLECTIONS = [ ("prints", "Process"), ("printers", "Printer"), ("filaments", "Filament"), ("sla_prints", "SLA print"), ("sla_materials", "SLA material"), ] def build_presets(): bundle = orca.host.preset_bundle() collections = [] for attr, label in COLLECTIONS: coll = getattr(bundle, attr) names = coll.preset_names() selected = coll.selected_preset() collections.append({ "key": attr, "label": label, "size": coll.size(), "selected_name": coll.selected_preset_name(), # selected = as saved on disk; edited = selected + unsaved modifications "selected": {"name": selected.name, "is_dirty": selected.is_dirty, "config_key_count": len(selected.config_keys())}, "edited": preset_dict(coll.edited_preset()), "names": names[:MAX_PRESET_NAMES], "truncated": max(0, len(names) - MAX_PRESET_NAMES), }) return { "collections": collections, "filament_names": bundle.current_filament_preset_names(), } def build_preset_config(collection_key, name): bundle = orca.host.preset_bundle() if collection_key not in {attr for attr, _ in COLLECTIONS}: raise ValueError(f"unknown collection {collection_key!r}") preset = getattr(bundle, collection_key).find_preset(name) if preset is None: raise ValueError(f"preset {name!r} not found") return {"preset": preset_dict(preset), "rows": config_rows(preset.config_keys(), preset.config_value)} def build_config(): bundle = orca.host.preset_bundle() rows = config_rows(bundle.full_config_keys(), bundle.full_config_value) return {"rows": rows, "count": len(rows)} def volume_dict(index, volume): return { "index": index, "id": volume.id(), "name": volume.name, "type": enum_name(volume.type()), "roles": { "model_part": volume.is_model_part(), "modifier": volume.is_modifier(), "negative": volume.is_negative_volume(), "support_enforcer": volume.is_support_enforcer(), "support_blocker": volume.is_support_blocker(), }, "extruder": volume.extruder_id(), # 1-based; -1 = none/inherited "offset": vec(volume.offset()), "rotation": vec(volume.rotation()), # radians "scale": vec(volume.scaling_factor()), "mirror": vec(volume.mirror()), "facets": volume.facets_count(), "volume_mm3": round(float(volume.volume()), 3), "manifold": volume.is_manifold(), "mesh_errors": volume.mesh_errors_count(), "painted": {"support": volume.is_fdm_support_painted(), "seam": volume.is_seam_painted(), "multimaterial": volume.is_mm_painted(), "fuzzy_skin": volume.is_fuzzy_skin_painted()}, "bbox": bbox_dict(volume.bounding_box()), "config": config_rows(volume.config_keys(), volume.config_value), } def assemble_dict(instance): """Assemble-view placement of one instance; needs bindings added 2026-07. getattr-guarded so the panel still runs on an older OrcaSlicer build.""" if not hasattr(instance, "assemble_offset"): return {"available": False} return { "available": True, "initialized": instance.is_assemble_initialized(), "offset": vec(instance.assemble_offset()), "rotation": vec(instance.assemble_rotation()), # radians "offset_to_assembly": vec(instance.offset_to_assembly()), } def instance_dict(index, instance): return { "index": index, "id": instance.id(), "printable_flag": instance.printable, "is_printable": instance.is_printable(), # also requires inside print volume "offset": vec(instance.offset()), "rotation": vec(instance.rotation()), # radians "scale": vec(instance.scaling_factor()), "mirror": vec(instance.mirror()), "left_handed": instance.is_left_handed(), "bbox": bbox_dict(instance.bounding_box()), "assemble": assemble_dict(instance), } def printable_instance_count(obj): """The GUI's printable toggle writes ModelInstance.printable only; ModelObject.printable keeps its import-time value. So an object's effective printable state must be derived from its instances.""" return sum(1 for i in range(obj.instance_count()) if obj.instance(i).printable) def object_dict(index, obj): volumes = [volume_dict(i, obj.volume(i)) for i in range(min(obj.volume_count(), MAX_VOLUMES))] instances = [instance_dict(i, obj.instance(i)) for i in range(min(obj.instance_count(), MAX_INSTANCES))] return { "index": index, "id": obj.id(), "name": obj.name, "module_name": obj.module_name, "input_file": obj.input_file, "printable": obj.printable, # raw import-time flag, see above "counts": {"volumes": obj.volume_count(), "instances": obj.instance_count(), "printable_instances": printable_instance_count(obj), "facets": obj.facets_count(), "parts": obj.parts_count(), "materials": obj.materials_count(), "mesh_errors": obj.mesh_errors_count()}, "flags": {"multiparts": obj.is_multiparts(), "cut": obj.is_cut(), "custom_layering": obj.has_custom_layering()}, "painted": {"support": obj.is_fdm_support_painted(), "seam": obj.is_seam_painted(), "multimaterial": obj.is_mm_painted(), "fuzzy_skin": obj.is_fuzzy_skin_painted()}, "z_range": [round(float(obj.min_z()), 3), round(float(obj.max_z()), 3)], "bbox": bbox_dict(obj.bounding_box()), # world, all instances "raw_bbox": bbox_dict(obj.raw_mesh_bounding_box()), # untransformed meshes "config": config_rows(obj.config_keys(), obj.config_value), "volumes": volumes, "volumes_omitted": max(0, obj.volume_count() - len(volumes)), "instances": instances, "instances_omitted": max(0, obj.instance_count() - len(instances)), } def build_model(): model = orca.host.model() count = model.object_count() shown = min(count, MAX_OBJECTS) return { # carried along so the extruder swatches never depend on a stale Overview "filament_colors": split_config_list( orca.host.preset_bundle().full_config_value("filament_colour")), "id": model.id(), "object_count": count, "objects_omitted": count - shown, "material_count": model.material_count(), "plate_index": model.current_plate_index(), "max_z": round(float(model.max_z()), 3), "bbox": bbox_dict(model.bounding_box()), "bbox_approx": bbox_dict(model.bounding_box_approx()), "objects": [object_dict(i, model.object(i)) for i in range(shown)], } def build_assembly(): """Group objects by module_name (the assembly path recorded in the 3mf) into a tree, and report each instance's Assemble-view transform.""" model = orca.host.model() root = {"name": "", "groups": {}, "objects": []} count = min(model.object_count(), MAX_OBJECTS) has_modules = False for i in range(count): obj = model.object(i) module = (obj.module_name or "").strip() has_modules = has_modules or bool(module) node = root for part in [p for p in re.split(r"[/\\]", module) if p]: node = node["groups"].setdefault(part, {"name": part, "groups": {}, "objects": []}) instances = [{"index": j, "offset": vec(obj.instance(j).offset()), "printable": obj.instance(j).printable, "assemble": assemble_dict(obj.instance(j))} for j in range(min(obj.instance_count(), MAX_INSTANCES))] bb = bbox_dict(obj.raw_mesh_bounding_box()) node["objects"].append({"index": i, "name": obj.name, "printable_instances": printable_instance_count(obj), "instance_count": obj.instance_count(), "size": bb["size"] if bb else None, "instances": instances, "instances_omitted": obj.instance_count() - len(instances)}) def freeze(node): # dict-of-groups -> sorted list, for stable JSON return {"name": node["name"], "groups": [freeze(node["groups"][k]) for k in sorted(node["groups"])], "objects": node["objects"]} return {"tree": freeze(root), "object_count": count, "objects_omitted": model.object_count() - count, "has_modules": has_modules} def build_mesh(object_index, volume_index): """Heavy on purpose: full TriangleMesh access, numpy views, world-space math and a sampled world-space point cloud for the previews.""" obj = orca.host.model().object(object_index) volume = obj.volume(volume_index) mesh = volume.mesh() data = { "vertex_count": mesh.vertex_count(), "triangle_count": mesh.triangle_count(), "empty": mesh.is_empty(), "manifold": mesh.is_manifold(), "volume_mm3": round(float(mesh.volume()), 3), "bbox": bbox_dict(mesh.bounding_box()), # numpy-free element access (always available, bounds-checked) "vertex0": vec(mesh.vertex(0)) if not mesh.is_empty() else None, "triangle0": list(mesh.triangle(0)) if not mesh.is_empty() else None, "numpy": None, "preview": None, } if np is None or mesh.is_empty(): return data V = np.asarray(mesh.vertices()) # (N, 3) float32, read-only, zero-copy T = np.asarray(mesh.triangles()) # (M, 3) int32, read-only, zero-copy N = np.asarray(mesh.face_normals()) # (M, 3) float32, computed copy tri = V[T].astype(np.float64) # (M, 3, 3) corner positions area = 0.5 * np.linalg.norm(np.cross(tri[:, 1] - tri[:, 0], tri[:, 2] - tri[:, 0]), axis=1).sum() data["numpy"] = { "vertices": f"shape={V.shape} dtype={V.dtype} writeable={V.flags.writeable} " f"zero_copy={V.base is not None}", "triangles": f"shape={T.shape} dtype={T.dtype}", "normals": f"shape={N.shape} dtype={N.dtype}", "surface_area_mm2": round(float(area), 3), } # World-space math under the first instance, using the row-vector convention # world = [V 1] @ (instance_matrix @ volume_matrix).T if obj.instance_count(): instance = obj.instance(0) homog = np.c_[V.astype(np.float64), np.ones(len(V))] world = (homog @ (instance.matrix() @ volume.matrix()).T)[:, :3] sample = world[::max(1, len(world) // MAX_PREVIEW_POINTS)] data["numpy"]["world_bbox"] = {"min": vec(world.min(0)), "max": vec(world.max(0))} data["preview"] = {"points": [vec(p) for p in sample], "sampled": len(sample), "total": len(world)} # The same mesh placed by the Assemble view (new 2026-07 API, guarded). if hasattr(instance, "assemble_matrix") and instance.is_assemble_initialized(): asm = (homog @ (instance.assemble_matrix() @ volume.matrix()).T)[:, :3] data["numpy"]["assemble_bbox"] = {"min": vec(asm.min(0)), "max": vec(asm.max(0))} return data SECTION_BUILDERS = { "overview": build_overview, "presets": build_presets, "config": build_config, "model": build_model, "assembly": build_assembly, } # --------------------------------------------------------------------------- # # the page — sidebar app, themed exclusively via the injected --orca-* vars # --------------------------------------------------------------------------- # PAGE = r"""

Host Inspector

""" # The modal page demoed from the UI Toolkit tab: orca.submit(payload) resolves the # blocking show_dialog() call with that payload; orca.close() resolves it with None. MODAL_PAGE = r"""

Modal dialog

show_dialog() blocks the plugin until this closes, then returns the submitted payload.

""" CHILD_PAGE = r"""

Child window

Same create_window() API — one plugin, several windows.

""" # --------------------------------------------------------------------------- # # the plugin # --------------------------------------------------------------------------- # class HostInspectorPanel(orca.script.ScriptPluginCapabilityBase): win = None child = None def get_name(self): return "Host Inspector" def execute(self): # Capability objects are instantiated once per plugin load, so a second # Run lands on the same instance — close any previous windows first, or # the old panel would keep posting into the new one. if self.win is not None and self.win.is_open(): self.win.close() if self.child is not None: self.child.close() self.child = None # Non-modal: returns immediately. The window is host-owned and lives on # after execute() returns; on_message keeps firing when the page posts. self.win = orca.host.ui.create_window( title="Host Inspector", html=PAGE, width=940, height=680, on_message=self.on_message, on_close=self.on_close, ) return orca.ExecutionResult.success("Host Inspector opened.") # Called on the UI thread when the page posts a message. Every branch posts # a reply carrying ok/error so the page never waits on a silent failure. def on_message(self, msg): msg = msg or {} command = msg.get("command") if command == "fetch": self.send_section(msg.get("section", "")) elif command == "mesh": self.send_mesh(int(msg.get("object", -1)), int(msg.get("volume", -1))) elif command == "preset_config": self.send_preset_config(msg.get("collection", ""), msg.get("name", "")) elif command == "ui": self.run_ui_action(msg) def on_close(self): if self.child is not None: self.child.close() self.child = None print("Host Inspector closed") def send_section(self, section): builder = SECTION_BUILDERS.get(section) reply = {"command": "section", "section": section} if builder is None: self.win.post({**reply, "ok": False, "error": f"unknown section {section!r}"}) return try: self.win.post({**reply, "ok": True, "data": builder()}) except Exception as exc: self.win.post({**reply, "ok": False, "error": str(exc)}) def send_mesh(self, object_index, volume_index): reply = {"command": "mesh", "object": object_index, "volume": volume_index} try: self.win.post({**reply, "ok": True, "data": build_mesh(object_index, volume_index)}) except Exception as exc: self.win.post({**reply, "ok": False, "error": str(exc)}) def send_preset_config(self, collection, name): reply = {"command": "preset_config", "collection": collection, "name": name} try: self.win.post({**reply, "ok": True, "data": build_preset_config(collection, name)}) except Exception as exc: self.win.post({**reply, "ok": False, "error": str(exc)}) # ---------------------------------------------------------------- ui demos def run_ui_action(self, msg): action = msg.get("action", "") reply = {"command": "ui_result", "action": action} def report(result): self.win.post({**reply, "ok": True, "result": result}) try: if action == "message": clicked = orca.host.ui.message( msg.get("text") or "Hello from Host Inspector", title="Host Inspector", buttons=msg.get("buttons", "ok"), icon=msg.get("icon", "info")) report(f"user clicked {clicked!r}") elif action == "modal": # We are on the UI thread, so this nests a modal event loop and # blocks right here until the dialog closes. on_message still # fires while the dialog is open (the log updates live). result = orca.host.ui.show_dialog( html=MODAL_PAGE, title="Host Inspector — modal", width=420, height=280, on_message=lambda m: self.win.post( {**reply, "ok": True, "result": f"modal posted {m!r} while open"})) report(f"show_dialog returned {result!r}") elif action == "progress": threading.Thread(target=self.progress_demo, daemon=True).start() report("started on a worker thread…") elif action == "pulse": threading.Thread(target=self.pulse_demo, daemon=True).start() report("started on a worker thread…") elif action == "child_open": if self.child is not None and self.child.is_open(): report("child already open") else: self.child = orca.host.ui.create_window( title="Host Inspector — child", html=CHILD_PAGE, width=380, height=240, on_message=self.on_child_message, on_close=lambda: self.win.post( {**reply, "ok": True, "result": "child window closed"})) report("child opened") elif action == "child_close": if self.child is not None: self.child.close() else: report("no child window") elif action == "child_state": is_open = self.child is not None and self.child.is_open() report(f"child.is_open() = {is_open}") else: self.win.post({**reply, "ok": False, "error": f"unknown action {action!r}"}) except Exception as exc: self.win.post({**reply, "ok": False, "error": str(exc)}) def on_child_message(self, msg): if (msg or {}).get("command") == "ping": self.child.post({"command": "pong"}) self.win.post({"command": "ui_result", "action": "child", "ok": True, "result": "child pinged; replied with pong"}) # Both demos run on a worker thread: ProgressDialog calls marshal to the UI # thread internally, and win.post() is thread-safe, so the app stays live. def progress_demo(self): style = (orca.host.ui.PD_APP_MODAL | orca.host.ui.PD_AUTO_HIDE | orca.host.ui.PD_CAN_ABORT | orca.host.ui.PD_ELAPSED_TIME | orca.host.ui.PD_ESTIMATED_TIME | orca.host.ui.PD_REMAINING_TIME) outcome = "completed 40 steps" with orca.host.ui.create_progress_dialog( "Host Inspector", "Crunching very important numbers…", maximum=40, style=style) as progress: for step in range(1, 41): time.sleep(0.05) if not progress.update(step, f"Step {step}/40"): outcome = f"cancelled at step {step}" break self.win.post({"command": "ui_result", "action": "progress", "ok": True, "result": outcome}) def pulse_demo(self): # ui.ProgressDialog(...) is the constructor form of create_progress_dialog(). with orca.host.ui.ProgressDialog( "Host Inspector", "Waiting for something indeterminate…", style=orca.host.ui.PD_APP_MODAL | orca.host.ui.PD_AUTO_HIDE) as progress: for _ in range(10): # manual single-shot pulses... time.sleep(0.1) if not progress.pulse("Manual pulse()…"): break progress.start_pulse(80, "Host-timed start_pulse()…") # ...then host-timed time.sleep(1.5) progress.stop_pulse() was_open = progress.is_open() self.win.post({"command": "ui_result", "action": "pulse", "ok": True, "result": f"pulsed manually + via timer (is_open while shown: {was_open})"}) @orca.plugin class HostInspectorPlugin(orca.base): def register_capabilities(self): orca.register_capability(HostInspectorPanel)