From 43120a407832457e9d26c1317091fef6c0f2b20e Mon Sep 17 00:00:00 2001 From: SoftFever Date: Sat, 25 Jul 2026 16:55:58 +0800 Subject: [PATCH] Improve the Orca Inspector example UI - Theme all pages from the host-injected --orca-* variables - Drop the unsupported SLA collections from the Presets tab - Group presets into hue-coded Printer/Filament/Process sections - Show every filament slot; keep the active preset selectable past the name cap - Open a preset's full config in its own viewer window --- .../orca_inspector_plugin_example_any.py | 495 +++++++++++++----- 1 file changed, 354 insertions(+), 141 deletions(-) diff --git a/sandboxes/orca_inspector_plugin_example_any.py b/sandboxes/orca_inspector_plugin_example_any.py index deada9eab8..41257dd4c6 100644 --- a/sandboxes/orca_inspector_plugin_example_any.py +++ b/sandboxes/orca_inspector_plugin_example_any.py @@ -14,7 +14,7 @@ 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 + 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 @@ -32,9 +32,10 @@ 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; +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(). @@ -45,6 +46,7 @@ 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 @@ -125,6 +127,19 @@ def split_config_list(value): # --------------------------------------------------------------------------- # # 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() @@ -132,17 +147,7 @@ def build_overview(): 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, - }) + filaments = filament_slots(bundle) printer = bundle.current_printer_preset() process = bundle.current_process_preset() @@ -189,13 +194,13 @@ def build_overview(): } -# label -> how to reach the PresetCollection on the bundle +# label -> how to reach the PresetCollection on the bundle, in the order the Presets +# tab stacks its groups. The bundle also exposes sla_prints/sla_materials, but +# OrcaSlicer doesn't support SLA, so they are omitted. COLLECTIONS = [ - ("prints", "Process"), ("printers", "Printer"), ("filaments", "Filament"), - ("sla_prints", "SLA print"), - ("sla_materials", "SLA material"), + ("prints", "Process"), ] @@ -206,21 +211,25 @@ def build_presets(): 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": coll.selected_preset_name(), + "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": names[:MAX_PRESET_NAMES], + "names": shown, "truncated": max(0, len(names) - MAX_PRESET_NAMES), }) return { "collections": collections, - "filament_names": bundle.current_filament_preset_names(), + "filament_slots": filament_slots(bundle), } @@ -231,8 +240,7 @@ def build_preset_config(collection_key, name): 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)} + return config_rows(preset.config_keys(), preset.config_value) def build_config(): @@ -467,102 +475,151 @@ SECTION_BUILDERS = { # --------------------------------------------------------------------------- # -# the page — sidebar app, themed exclusively via the injected --orca-* vars +# 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, +# where the prefers-color-scheme block stands in for the host's color-scheme. +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 — semantic colors, deliberately the same +# in light and dark mode: they say what a card is, not what theme is on. One source +# feeds both the main page's group styling and the config viewer window. +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 +# per-preset 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""" - + - @@ -588,7 +645,7 @@ const SECTIONS = [ ['assembly', '⬡', 'Assembly'], ['uikit', '▣', 'UI Toolkit'], ]; -const S = { current:'overview', cache:{}, busy:{}, colors:[], uiLog:[] }; +const S = { current:'overview', cache:{}, busy:{}, colors:[], uiLog:[], gen:0 }; const $ = id => document.getElementById(id); const esc = s => String(s == null ? '' : s) @@ -598,6 +655,9 @@ const deg = r => num(r * 180 / Math.PI, 1) + '°'; const vec3 = (v, d=2) => v ? '(' + v.map(c => num(c, d)).join(', ') + ')' : '—'; const yn = b => b ? 'yes' : 'no'; const badge = (label, on) => '' + esc(label) + ''; +// Colors land in a style attribute, so only literal hex colors pass through. +const swatch = c => /^#[0-9a-f]{3,8}$/i.test(c || '') + ? '' : ''; const kv = rows => '
' + rows.map(([k, v]) => '
' + esc(k) + '
' + v + '
').join('') + '
'; const bboxRows = (label, bb) => bb ? [ @@ -608,7 +668,8 @@ const bboxRows = (label, bb) => bb ? [ /* ------------------------------------------------------- nav + fetching */ function buildNav() { $('nav').innerHTML = SECTIONS.map(([key, glyph, label]) => - '