# /// script # requires-python = ">=3.12" # dependencies = ["numpy"] # # [tool.orcaslicer.plugin] # name = "Orca Inspector" # description = "An interactive panel that browses the whole orca.host read-only API and demos every orca.host.ui facility." # author = "SoftFever" # version = "0.0.3" # /// """Orca Inspector — a guided tour of `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, filament slots, config viewer windows 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/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 pages follow (see the plugin docs): - no hardcoded theme: BASE_CSS aliases the host-injected --orca-* variables (with fallbacks so the raw HTML previews in a plain browser) and styles the body and native controls from them — the host injects variables only; - 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 json 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 filament_slots(bundle): """One entry per filament slot: the mounted preset (current_filament_presets) plus its color and material from the merged config.""" cfg = bundle.full_config_value colors = split_config_list(cfg("filament_colour")) types = split_config_list(cfg("filament_type")) return [{"slot": i + 1, "color": colors[i] if i < len(colors) else "", "material": types[i] if i < len(types) else "", "preset": preset_dict(preset) if preset else None} for i, preset in enumerate(bundle.current_filament_presets())] def build_overview(): plater = orca.host.plater() model = orca.host.model() bundle = orca.host.preset_bundle() objects = model.objects() cfg = bundle.full_config_value filaments = filament_slots(bundle) 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], }, } # PresetCollection attribute -> label, in the order the Presets tab stacks its groups. # The bundle also exposes sla_prints/sla_materials, omitted as OrcaSlicer has no SLA. COLLECTIONS = [ ("printers", "Printer"), ("filaments", "Filament"), ("prints", "Process"), ] 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() selected_name = coll.selected_preset_name() shown = names[:MAX_PRESET_NAMES] if selected_name and selected_name not in shown: shown.insert(0, selected_name) # the active preset stays pickable past the cap collections.append({ "key": attr, "label": label, "size": coll.size(), "selected_name": selected_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": shown, "truncated": max(0, len(names) - MAX_PRESET_NAMES), }) return { "collections": collections, "filament_slots": filament_slots(bundle), } 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 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 pages — themed via BASE_CSS, which aliases the injected --orca-* vars # --------------------------------------------------------------------------- # # The host injects theme *variables* only (never element styles), so every page owns # its base look. The fallbacks keep the raw HTML previewable in a plain browser. BASE_CSS = r""" :root { --bg: var(--orca-bg, #ffffff); --fg: var(--orca-fg, #1f2429); --muted: var(--orca-muted, #6b7580); --border: var(--orca-border, #d9dee3); --accent: var(--orca-accent, #009688); --accent-fg: var(--orca-accent-fg, #ffffff); --ui: var(--orca-font, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif); --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; } @media (prefers-color-scheme: dark) { :root { --bg: var(--orca-bg, #2b2d30); --fg: var(--orca-fg, #e4e6e8); --muted: var(--orca-muted, #9aa0a6); --border: var(--orca-border, #3d4043); } } * { box-sizing: border-box; } body { margin:0; background:var(--bg); color:var(--fg); font:13px/1.45 var(--ui); } button { font:inherit; padding:4px 12px; cursor:pointer; border-radius:6px; background:var(--accent); color:var(--accent-fg); border:1px solid var(--accent); } button.secondary { background:transparent; color:var(--fg); border-color:var(--border); } button:disabled { opacity:.55; cursor:default; } input, select { font:inherit; color:var(--fg); background:var(--bg); border:1px solid var(--border); border-radius:6px; padding:4px 8px; } :focus-visible { outline:2px solid var(--accent); outline-offset:1px; } """ # Identity hues for the preset collections, shared by the main page's group styling and # the config viewer window. Deliberately the same in light and dark mode: they say what a # card is, not what theme is on. HUES = {"printers": "#9a6ee8", "filaments": "#ee8f41", "prints": "#4e8df6"} HUE_CSS = "".join(f" .hue-{key} {{ --hue:{color}; }}\n" for key, color in HUES.items()) # The filterable key/value config table, shared by the Config tab and the viewer window. CFG_TABLE_CSS = r""" table.cfg { width:100%; border-collapse:collapse; font-size:12px; } table.cfg th { text-align:left; font-size:11px; letter-spacing:.07em; text-transform:uppercase; color:var(--muted); padding-bottom:4px; } table.cfg td:first-child { font-family:var(--mono); white-space:nowrap; vertical-align:top; } table.cfg td.val { font-family:var(--mono); white-space:pre-wrap; word-break:break-word; } td.val .more { color:var(--accent); cursor:pointer; } """ PAGE = r"""
create_window(style=WINDOW_MODAL) keeps this page interactive until it submits or closes.
""" CHILD_PAGE = r"""
Same create_window() API — one plugin, several windows.
""" # The per-preset config viewer, opened by "View config" on the Presets tab. Its rows are # baked in as JSON at build time, so the window needs no bridge traffic and stays usable # side by side with the panel until closed. CONFIG_PAGE = r""" """ def config_page(collection, name, rows): """CONFIG_PAGE with one preset's rows baked in. `` is escaped so a config value containing e.g. `` cannot break out of the script block.""" payload = json.dumps({"name": name, "hue": HUES.get(collection, ""), "rows": rows}) return CONFIG_PAGE.replace("__DATA__", payload.replace("", "<\\/")) # --------------------------------------------------------------------------- # # the plugin # --------------------------------------------------------------------------- # class OrcaInspectorPanel(orca.script.ScriptPluginCapabilityBase): win = None child = None modal = None cfgwin = None def get_name(self): return "Orca 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 if self.modal is not None: self.modal.close() self.modal = None if self.cfgwin is not None: self.cfgwin.close() self.cfgwin = 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="Orca Inspector", html=PAGE, width=940, height=680, on_message=self.on_message, on_close=self.on_close, ) return orca.ExecutionResult.success("Orca 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", ""), msg.get("gen")) elif command == "mesh": self.send_mesh(msg.get("object", -1), msg.get("volume", -1)) elif command == "preset_config": self.open_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 if self.cfgwin is not None: self.cfgwin.close() self.cfgwin = None print("Orca Inspector closed") def send_section(self, section, gen=None): builder = SECTION_BUILDERS.get(section) # gen echoes the page's refresh counter so a reply that raced a Refresh is dropped there. reply = {"command": "section", "section": section, "gen": gen} 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(int(object_index), int(volume_index))}) except Exception as exc: self.win.post({**reply, "ok": False, "error": str(exc)}) def open_preset_config(self, collection, name): # One viewer at a time: a new pick replaces the previous window. The main page # only hears back about failures. reply = {"command": "preset_config", "collection": collection, "name": name} try: rows = build_preset_config(collection, name) if self.cfgwin is not None: self.cfgwin.close() self.cfgwin = orca.host.ui.create_window( title=f"Orca Inspector — {name}", html=config_page(collection, name, rows), width=520, height=640) self.win.post({**reply, "ok": True}) 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 Orca Inspector", title="Orca Inspector", buttons=msg.get("buttons", "ok"), icon=msg.get("icon", "info")) report(f"user clicked {clicked!r}") elif action == "modal": if self.modal is not None and self.modal.is_open(): report("modal already open") return # The modal window is created asynchronously on the UI thread, # but the handle remains usable for post()/close() and callbacks. self.modal = orca.host.ui.create_window( html=MODAL_PAGE, title="Orca Inspector — modal", width=420, height=280, style=orca.host.ui.WINDOW_MODAL, on_message=lambda m: self.win.post( {**reply, "ok": True, "result": f"modal posted {m!r} while open"}), on_submit=lambda result: self.win.post( {**reply, "ok": True, "result": f"modal submitted {result!r}"}), on_close=lambda: self.win.post( {**reply, "ok": True, "result": "modal closed"})) report("modal opened") 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: # Not `reply`: the close can also come from "child_close" later, so the # on_close payload is labelled neutrally. self.child = orca.host.ui.create_window( title="Orca Inspector — child", html=CHILD_PAGE, width=380, height=240, on_message=self.on_child_message, on_close=lambda: self.win.post( {"command": "ui_result", "action": "child", "ok": True, "result": "child window closed"})) report("child opened") elif action == "child_close": if self.child is not None: self.child.close() report("close requested") # the actual close is logged by on_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( "Orca 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( "Orca 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 OrcaInspectorPlugin(orca.base): def register_capabilities(self): orca.register_capability(OrcaInspectorPanel)