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
This commit is contained in:
SoftFever
2026-07-25 16:55:58 +08:00
parent 6e1a6cc678
commit 43120a4078

View File

@@ -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: usable) with a sidebar of sections, each exercising one part of the API:
Overview plater state, scene statistics, current printer/process/filaments 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 Config the complete merged full_config as a searchable table
Model Model -> Object -> Volume/Instance tree with lazy mesh geometry Model Model -> Object -> Volume/Instance tree with lazy mesh geometry
Assembly objects grouped by module_name, per-instance assemble transforms 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 point-cloud previews) only when you ask for it. "Refresh" drops the cache and
refetches the visible section. refetches the visible section.
Style rules the page follows (see the plugin docs): Style rules the pages follow (see the plugin docs):
- no hardcoded colors — only the host-injected --orca-* theme variables, so - no hardcoded theme: BASE_CSS aliases the host-injected --orca-* variables
the panel matches light and dark mode automatically; (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); - self-contained HTML (inline CSS/JS, no external resources);
- on_message runs on the UI thread, so long work (the progress demos) is - 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(). 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. what is missing instead of failing.
""" """
import json
import re import re
import threading import threading
import time import time
@@ -125,6 +127,19 @@ def split_config_list(value):
# --------------------------------------------------------------------------- # # --------------------------------------------------------------------------- #
# section builders # 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(): def build_overview():
plater = orca.host.plater() plater = orca.host.plater()
model = orca.host.model() model = orca.host.model()
@@ -132,17 +147,7 @@ def build_overview():
objects = model.objects() objects = model.objects()
cfg = bundle.full_config_value cfg = bundle.full_config_value
colors = split_config_list(cfg("filament_colour")) filaments = filament_slots(bundle)
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 "<missing preset>",
"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() printer = bundle.current_printer_preset()
process = bundle.current_process_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 = [ COLLECTIONS = [
("prints", "Process"),
("printers", "Printer"), ("printers", "Printer"),
("filaments", "Filament"), ("filaments", "Filament"),
("sla_prints", "SLA print"), ("prints", "Process"),
("sla_materials", "SLA material"),
] ]
@@ -206,21 +211,25 @@ def build_presets():
coll = getattr(bundle, attr) coll = getattr(bundle, attr)
names = coll.preset_names() names = coll.preset_names()
selected = coll.selected_preset() 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({ collections.append({
"key": attr, "key": attr,
"label": label, "label": label,
"size": coll.size(), "size": coll.size(),
"selected_name": coll.selected_preset_name(), "selected_name": selected_name,
# selected = as saved on disk; edited = selected + unsaved modifications # selected = as saved on disk; edited = selected + unsaved modifications
"selected": {"name": selected.name, "is_dirty": selected.is_dirty, "selected": {"name": selected.name, "is_dirty": selected.is_dirty,
"config_key_count": len(selected.config_keys())}, "config_key_count": len(selected.config_keys())},
"edited": preset_dict(coll.edited_preset()), "edited": preset_dict(coll.edited_preset()),
"names": names[:MAX_PRESET_NAMES], "names": shown,
"truncated": max(0, len(names) - MAX_PRESET_NAMES), "truncated": max(0, len(names) - MAX_PRESET_NAMES),
}) })
return { return {
"collections": collections, "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) preset = getattr(bundle, collection_key).find_preset(name)
if preset is None: if preset is None:
raise ValueError(f"preset {name!r} not found") raise ValueError(f"preset {name!r} not found")
return {"preset": preset_dict(preset), return config_rows(preset.config_keys(), preset.config_value)
"rows": config_rows(preset.config_keys(), preset.config_value)}
def build_config(): 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"""<!DOCTYPE html> PAGE = r"""<!DOCTYPE html>
<html> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<style> <style>""" + BASE_CSS + r"""
:root { --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; } body { height:100vh; display:flex; flex-direction:column; overflow:hidden; }
* { box-sizing: border-box; }
body { margin:0; height:100vh; display:flex; flex-direction:column; overflow:hidden; }
header { flex:none; display:flex; align-items:center; gap:10px; header { flex:none; display:flex; align-items:center; gap:10px;
padding:10px 16px; border-bottom:1px solid var(--orca-border); } padding:10px 16px; border-bottom:1px solid var(--border); }
header h1 { font-size:15px; margin:0; } header h1 { font-size:15px; margin:0; }
#stamp { flex:1; color:var(--orca-muted); font-size:12px; } #stamp { flex:1; color:var(--muted); font-size:12px; }
button.secondary { background:transparent; color:var(--orca-fg);
border-color:var(--orca-border); }
button.small { padding:2px 10px; font-size:12px; } button.small { padding:2px 10px; font-size:12px; }
main { flex:1; display:flex; min-height:0; } main { flex:1; display:flex; min-height:0; }
nav { flex:none; width:150px; padding:8px 0; overflow-y:auto; nav { flex:none; width:150px; padding:8px 0; overflow-y:auto;
border-right:1px solid var(--orca-border); } border-right:1px solid var(--border); }
nav .item { padding:8px 14px; cursor:pointer; user-select:none; nav .item { padding:8px 14px; cursor:pointer; user-select:none;
color:var(--orca-muted); border-left:3px solid transparent; } color:var(--muted); border-left:3px solid transparent; }
nav .item:hover { color:var(--orca-fg); } nav .item:hover { color:var(--fg); }
nav .item.active { color:var(--orca-fg); font-weight:600; nav .item.active { color:var(--fg); font-weight:600;
border-left-color:var(--orca-accent); } border-left-color:var(--accent); }
nav .glyph { display:inline-block; width:1.5em; } nav .glyph { display:inline-block; width:1.5em; }
#content { flex:1; overflow:auto; padding:14px 16px 28px; } #content { flex:1; overflow:auto; padding:14px 16px 28px; }
.tiles { display:grid; grid-template-columns:repeat(auto-fill,minmax(108px,1fr)); .tiles { display:grid; grid-template-columns:repeat(auto-fill,minmax(108px,1fr));
gap:10px; margin-bottom:12px; } gap:10px; margin-bottom:12px; }
.tile { border:1px solid var(--orca-border); border-radius:8px; padding:10px 12px; } .tile { border:1px solid var(--border); border-radius:8px; padding:10px 12px; }
.tile .v { font-size:20px; font-weight:600; font-variant-numeric:tabular-nums; } .tile .v { font-size:20px; font-weight:600; font-variant-numeric:tabular-nums; }
.tile .l { font-size:11px; color:var(--orca-muted); margin-top:2px; } .tile .l { font-size:11px; color:var(--muted); margin-top:2px; }
.cards { display:grid; grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); gap:12px; } .cards { display:grid; grid-template-columns:repeat(auto-fit,minmax(260px,1fr)); gap:12px; }
.card { border:1px solid var(--orca-border); border-radius:8px; .card { border:1px solid var(--border); border-radius:8px;
padding:12px 14px; min-width:0; } padding:12px 14px; min-width:0; }
.card h3 { margin:0 0 8px; font-size:11px; letter-spacing:.07em; .card h3 { margin:0 0 8px; font-size:11px; letter-spacing:.07em;
text-transform:uppercase; color:var(--orca-muted); } text-transform:uppercase; color:var(--muted); }
.card.wide { grid-column:1 / -1; } .card.wide { grid-column:1 / -1; }
""" + HUE_CSS + r""" .card.hued { border-top:3px solid var(--hue); }
.dot { display:inline-block; width:9px; height:9px; border-radius:50%;
background:var(--hue); margin-right:6px; }
.pgroup { margin-bottom:18px; }
.pgroup-head { display:flex; align-items:center; gap:8px; flex-wrap:wrap; margin-bottom:8px; }
.pgroup-head h2 { margin:0; font-size:12px; letter-spacing:.07em; text-transform:uppercase; }
.pgroup-head select { flex:0 1 260px; min-width:120px; }
.spacer { flex:1; }
.kv { display:grid; grid-template-columns:minmax(110px,max-content) 1fr; .kv { display:grid; grid-template-columns:minmax(110px,max-content) 1fr;
gap:3px 14px; font-size:12px; } gap:3px 14px; font-size:12px; }
.kv .k { color:var(--orca-muted); } .kv .k { color:var(--muted); }
.kv .v { font-family:var(--mono); word-break:break-word; } .kv .v { font-family:var(--mono); word-break:break-word; }
.badge { display:inline-block; border:1px solid var(--orca-border); .badge { display:inline-block; border:1px solid var(--border);
color:var(--orca-muted); border-radius:999px; padding:1px 8px; color:var(--muted); border-radius:999px; padding:1px 8px;
font-size:11px; margin:1px 3px 1px 0; white-space:nowrap; } font-size:11px; margin:1px 3px 1px 0; white-space:nowrap; }
.badge.on { background:var(--orca-accent); border-color:var(--orca-accent); .badge.on { background:var(--accent); border-color:var(--accent);
color:var(--orca-accent-fg); } color:var(--accent-fg); }
.swatch { display:inline-block; width:12px; height:12px; border-radius:3px; .swatch { display:inline-block; width:12px; height:12px; border-radius:3px;
border:1px solid var(--orca-border); vertical-align:-2px; margin-right:5px; } border:1px solid var(--border); vertical-align:-2px; margin-right:5px; }
.toolbar { display:flex; gap:8px; align-items:center; margin-bottom:10px; } .toolbar { display:flex; gap:8px; align-items:center; margin-bottom:10px; }
.toolbar input { flex:1; max-width:340px; } .toolbar input { flex:1; max-width:340px; }
.count { color:var(--orca-muted); font-size:12px; } .count { color:var(--muted); font-size:12px; }
""" + CFG_TABLE_CSS + r"""
table.cfg { width:100%; border-collapse:collapse; font-size:12px; }
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(--orca-accent); cursor:pointer; }
.node { margin:1px 0; } .node { margin:1px 0; }
.nhead { display:flex; align-items:center; gap:7px; padding:4px 8px; .nhead { display:flex; align-items:center; gap:7px; padding:4px 8px;
border-radius:6px; cursor:pointer; user-select:none; flex-wrap:wrap; } border-radius:6px; cursor:pointer; user-select:none; flex-wrap:wrap; }
.nhead:hover { background:var(--orca-border); } .nhead:hover { background:var(--border); }
.twist { width:1em; flex:none; color:var(--orca-muted); font-size:10px; } .twist { width:1em; flex:none; color:var(--muted); font-size:10px; }
.nname { font-weight:600; } .nname { font-weight:600; }
.nbody { display:none; margin-left:14px; padding:4px 0 4px 12px; .nbody { display:none; margin-left:14px; padding:4px 0 4px 12px;
border-left:1px dotted var(--orca-border); } border-left:1px dotted var(--border); }
.node.open > .nhead .twist { transform:rotate(90deg); } .node.open > .nhead .twist { transform:rotate(90deg); }
.node.open > .nbody { display:block; } .node.open > .nbody { display:block; }
.subhead { margin:8px 0 4px; font-size:11px; letter-spacing:.07em; .subhead { margin:8px 0 4px; font-size:11px; letter-spacing:.07em;
text-transform:uppercase; color:var(--orca-muted); } text-transform:uppercase; color:var(--muted); }
.banner { border:1px solid var(--orca-border); border-left:3px solid var(--orca-accent); .banner { border:1px solid var(--border); border-left:3px solid var(--accent);
border-radius:6px; padding:9px 12px; margin-bottom:12px; font-size:12px; } border-radius:6px; padding:9px 12px; margin-bottom:12px; font-size:12px; }
.muted { color:var(--orca-muted); } .muted { color:var(--muted); }
.mono { font-family:var(--mono); } .mono { font-family:var(--mono); }
canvas.preview { width:100%; height:160px; border:1px solid var(--orca-border); canvas.preview { width:100%; height:160px; border:1px solid var(--border);
border-radius:6px; } border-radius:6px; }
.previews { display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-top:8px; } .previews { display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-top:8px; }
.previews .cap { font-size:11px; color:var(--orca-muted); text-align:center; margin-top:2px; } .previews .cap { font-size:11px; color:var(--muted); text-align:center; margin-top:2px; }
.log { font-family:var(--mono); font-size:12px; border:1px solid var(--orca-border); .log { font-family:var(--mono); font-size:12px; border:1px solid var(--border);
border-radius:6px; padding:8px 10px; max-height:190px; overflow:auto; } border-radius:6px; padding:8px 10px; max-height:190px; overflow:auto; }
.log div { padding:1px 0; } .log div { padding:1px 0; }
fieldset { border:1px solid var(--orca-border); border-radius:6px; margin:0 0 10px; } fieldset { border:1px solid var(--border); border-radius:6px; margin:0 0 10px; }
legend { color:var(--orca-muted); font-size:11px; padding:0 6px; } legend { color:var(--muted); font-size:11px; padding:0 6px; }
.frow { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin:6px 0; } .frow { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin:6px 0; }
label { font-size:12px; color:var(--orca-muted); } label { font-size:12px; color:var(--muted); }
</style> </style>
</head> </head>
<body> <body>
@@ -588,7 +645,7 @@ const SECTIONS = [
['assembly', '', 'Assembly'], ['assembly', '', 'Assembly'],
['uikit', '', 'UI Toolkit'], ['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 $ = id => document.getElementById(id);
const esc = s => String(s == null ? '' : s) 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 vec3 = (v, d=2) => v ? '(' + v.map(c => num(c, d)).join(', ') + ')' : '';
const yn = b => b ? 'yes' : 'no'; const yn = b => b ? 'yes' : 'no';
const badge = (label, on) => '<span class="badge' + (on ? ' on' : '') + '">' + esc(label) + '</span>'; const badge = (label, on) => '<span class="badge' + (on ? ' on' : '') + '">' + esc(label) + '</span>';
// Colors land in a style attribute, so only literal hex colors pass through.
const swatch = c => /^#[0-9a-f]{3,8}$/i.test(c || '')
? '<span class="swatch" style="background:' + c + '"></span>' : '';
const kv = rows => '<div class="kv">' + rows.map(([k, v]) => const kv = rows => '<div class="kv">' + rows.map(([k, v]) =>
'<div class="k">' + esc(k) + '</div><div class="v">' + v + '</div>').join('') + '</div>'; '<div class="k">' + esc(k) + '</div><div class="v">' + v + '</div>').join('') + '</div>';
const bboxRows = (label, bb) => bb ? [ const bboxRows = (label, bb) => bb ? [
@@ -608,7 +668,8 @@ const bboxRows = (label, bb) => bb ? [
/* ------------------------------------------------------- nav + fetching */ /* ------------------------------------------------------- nav + fetching */
function buildNav() { function buildNav() {
$('nav').innerHTML = SECTIONS.map(([key, glyph, label]) => $('nav').innerHTML = SECTIONS.map(([key, glyph, label]) =>
'<div class="item" id="nav-' + key + '" onclick="show(\'' + key + '\')">' + '<div class="item" id="nav-' + key + '" role="button" tabindex="0" ' +
'onclick="show(\'' + key + '\')">' +
'<span class="glyph">' + glyph + '</span>' + label + '</div>').join(''); '<span class="glyph">' + glyph + '</span>' + label + '</div>').join('');
} }
function show(key) { function show(key) {
@@ -622,9 +683,10 @@ function show(key) {
function fetchSection(key) { function fetchSection(key) {
S.busy[key] = true; S.busy[key] = true;
$('content').innerHTML = '<p class="muted">Loading ' + esc(key) + '…</p>'; $('content').innerHTML = '<p class="muted">Loading ' + esc(key) + '…</p>';
orca.postMessage({ command:'fetch', section:key }); orca.postMessage({ command:'fetch', section:key, gen:S.gen });
} }
function refresh() { function refresh() {
S.gen++; // replies to fetches from before this point are dropped
S.cache = {}; S.cache = {};
S.busy = {}; S.busy = {};
show(S.current); show(S.current);
@@ -660,13 +722,12 @@ function renderOverview(d) {
[t.config_keys, 'config keys'], [t.config_keys, 'config keys'],
].map(([v, l]) => '<div class="tile"><div class="v">' + v + '</div><div class="l">' + l + '</div></div>').join(''); ].map(([v, l]) => '<div class="tile"><div class="v">' + v + '</div><div class="l">' + l + '</div></div>').join('');
const filaments = d.setup.filaments.map((f, i) => const filaments = d.setup.filaments.map(f =>
'<div style="margin:3px 0">' + '<div style="margin:3px 0">' + swatch(f.color) +
(f.color ? '<span class="swatch" style="background:' + esc(f.color) + '"></span>' : '') + '<b>' + f.slot + '</b> ' + esc(f.preset ? f.preset.name : '<missing preset>') +
'<b>' + (i + 1) + '</b> ' + esc(f.name) + (f.material ? ' <span class="muted">' + esc(f.material) + '</span>' : '') +
(f.type ? ' <span class="muted">' + esc(f.type) + '</span>' : '') + (f.preset ? badge(f.preset.is_system ? 'system' : 'user', f.preset.is_system) +
badge(f.is_system ? 'system' : 'user', f.is_system) + (f.preset.is_dirty ? badge('modified', true) : '') : '') +
(f.is_dirty ? badge('modified', true) : '') +
'</div>').join('') || '<span class="muted">none</span>'; '</div>').join('') || '<span class="muted">none</span>';
return '<div class="tiles">' + tiles + '</div><div class="cards">' + return '<div class="tiles">' + tiles + '</div><div class="cards">' +
@@ -709,35 +770,65 @@ function presetBadges(p) {
(p.is_visible ? '' : badge('hidden', false)); (p.is_visible ? '' : badge('hidden', false));
} }
function renderPresets(d) { function renderPresets(d) {
return '<div class="banner">Each card is one <span class="mono">host.PresetCollection</span>. ' + const detailCard = g => {
'“Edited” is the selected preset plus any unsaved changes. Pick any preset ' + const p = g.edited;
'and load its complete config via <span class="mono">find_preset().config_value()</span>.</div>' + return '<div class="cards"><div class="card hued">' +
'<div class="cards">' + d.collections.map(c => { '<div style="margin-bottom:6px"><b>' + esc(p.name) + '</b> ' + presetBadges(p) + '</div>' +
const p = c.edited; kv([['label()', esc(p.label)], ['alias', esc(p.alias) || ''],
const options = c.names.map(n => // explicit value= so odd whitespace in names survives ['type', esc(p.type)], ['bundle', esc(p.bundle_id) || ''],
'<option value="' + esc(n) + '"' + (n === c.selected_name ? ' selected' : '') + '>' + ['edited_preset()', p.config_key_count + ' keys' +
(p.is_dirty ? ' · has unsaved changes' : ' · same as saved')],
['selected_preset()', esc(g.selected.name) + ' · ' + g.selected.config_key_count + ' keys'],
['file', '<span class="muted">' + esc(p.file) + '</span>']]) +
'</div></div>';
};
const slotCard = (s, g) => {
const p = s.preset;
if (!p) return '<div class="card hued"><h3>Slot ' + s.slot + '</h3>' +
'<p class="muted">no preset mounted</p></div>';
return '<div class="card hued"><h3>Slot ' + s.slot + '</h3>' +
'<div style="margin-bottom:6px">' + swatch(s.color) + '<b>' + esc(p.name) + '</b> ' +
badge(p.is_system ? 'system' : 'user', p.is_system) +
(p.is_dirty ? badge('modified', true) : '') +
(p.name === g.selected_name ? badge('editing', true) : '') + '</div>' +
kv([['material', esc(s.material) || ''],
['alias', esc(p.alias) || ''],
['config keys', p.config_key_count],
['file', '<span class="muted">' + esc(p.file) + '</span>']]) +
'<div class="frow" style="margin-top:8px"><button class="small secondary" ' +
'data-act="pcfg" data-coll="filaments" data-name="' + esc(p.name) + '">View config</button></div>' +
'</div>';
};
return '<div class="banner">Each group is one <span class="mono">host.PresetCollection</span>; ' +
'the Filament group shows every slot from <span class="mono">current_filament_presets()</span>, ' +
'and “editing” marks the preset open in its settings tab. Pick any preset in a group ' +
'header — “View config” opens its complete config (via ' +
'<span class="mono">find_preset().config_value()</span>) in its own window.</div>' +
d.collections.map(g => {
const options = g.names.map(n => // explicit value= so odd whitespace in names survives
'<option value="' + esc(n) + '"' + (n === g.selected_name ? ' selected' : '') + '>' +
esc(n) + '</option>').join(''); esc(n) + '</option>').join('');
return '<div class="card"><h3>' + esc(c.label) + ' · ' + c.size + ' presets</h3>' + const cards = g.key === 'filaments'
'<div style="margin-bottom:6px"><b>' + esc(p.name) + '</b> ' + presetBadges(p) + '</div>' + ? '<div class="cards">' + (d.filament_slots.map(s => slotCard(s, g)).join('') ||
kv([['label()', esc(p.label)], ['alias', esc(p.alias) || ''], '<p class="muted">no filament slots</p>') + '</div>'
['type', esc(p.type)], ['bundle', esc(p.bundle_id) || ''], : detailCard(g);
['edited_preset()', p.config_key_count + ' keys' + return '<div class="pgroup hue-' + g.key + '">' +
(p.is_dirty ? ' · has unsaved changes' : ' · same as saved')], '<div class="pgroup-head"><span class="dot"></span><h2>' + esc(g.label) + '</h2>' +
['selected_preset()', esc(c.selected.name) + ' · ' + c.selected.config_key_count + ' keys'], '<span class="count">' + g.size + ' presets</span><span class="spacer"></span>' +
['file', '<span class="muted">' + esc(p.file) + '</span>']]) + '<select id="sel-' + g.key + '">' + options + '</select>' +
'<div class="frow" style="margin-top:8px">' + '<button class="small" data-act="pcfg" data-coll="' + g.key + '">View config</button></div>' +
'<select id="sel-' + c.key + '" style="flex:1;min-width:0">' + options + '</select>' + cards +
'<button class="small" data-act="pcfg" data-coll="' + c.key + '">View config</button>' + (g.truncated ? '<p class="muted">select capped, ' + g.truncated + ' more not listed</p>' : '') +
'</div>' + // the config itself opens in a viewer window; this slot only shows failures
(c.truncated ? '<p class="muted">list capped, ' + c.truncated + ' more not shown</p>' : '') + '<div id="pcfg-' + g.key + '"></div></div>';
'<div id="pcfg-' + c.key + '"></div></div>'; }).join('');
}).join('') + '</div>';
} }
function configTable(rows, id) { function configTable(rows, id) {
const body = rows.map(r => { const body = rows.map(r => {
const long = r.v.length > 160; const long = r.v.length > 160;
const shown = long ? esc(r.v.slice(0, 160)) + '<span class="more" data-act="expand"> … show all</span>' const shown = long ? esc(r.v.slice(0, 160)) +
'<span class="more" data-act="expand" role="button" tabindex="0"> … show all</span>'
: esc(r.v); : esc(r.v);
return '<tr data-k="' + esc(r.k.toLowerCase()) + '"><td>' + esc(r.k) + '</td>' + return '<tr data-k="' + esc(r.k.toLowerCase()) + '"><td>' + esc(r.k) + '</td>' +
'<td class="val" data-full="' + esc(r.v) + '">' + shown + '</td></tr>'; '<td class="val" data-full="' + esc(r.v) + '">' + shown + '</td></tr>';
@@ -746,37 +837,36 @@ function configTable(rows, id) {
} }
function renderConfig(d) { function renderConfig(d) {
return '<div class="toolbar"><input id="cfg-search" placeholder="Filter ' + d.count + return '<div class="toolbar"><input id="cfg-search" placeholder="Filter ' + d.count +
' keys…" oninput="filterConfig(this.value)">' + ' keys…" oninput="filterTable(\'cfg-table\', this.value, \'cfg-count\')">' +
'<span class="count" id="cfg-count">' + d.count + ' keys</span></div>' + '<span class="count" id="cfg-count">' + d.count + ' keys</span></div>' +
'<div class="banner">The merged result of printer + process + filament presets — ' + '<div class="banner">The merged result of printer + process + filament presets — ' +
'exactly what <span class="mono">preset_bundle().full_config_value(key)</span> returns for ' + 'exactly what <span class="mono">preset_bundle().full_config_value(key)</span> returns for ' +
'every key in <span class="mono">full_config_keys()</span>.</div>' + 'every key in <span class="mono">full_config_keys()</span>.</div>' +
configTable(d.rows, 'cfg-table'); configTable(d.rows, 'cfg-table');
} }
function filterConfig(q) { function filterTable(tableId, q, countId) {
q = q.trim().toLowerCase(); q = q.trim().toLowerCase();
let visible = 0; let visible = 0;
document.querySelectorAll('#cfg-table tr[data-k]').forEach(tr => { document.querySelectorAll('#' + tableId + ' tr[data-k]').forEach(tr => {
const hit = !q || tr.dataset.k.includes(q) || const hit = !q || tr.dataset.k.includes(q) ||
tr.querySelector('.val').dataset.full.toLowerCase().includes(q); tr.querySelector('.val').dataset.full.toLowerCase().includes(q);
tr.style.display = hit ? '' : 'none'; tr.style.display = hit ? '' : 'none';
if (hit) visible++; if (hit) visible++;
}); });
$('cfg-count').textContent = visible + ' keys'; if (countId) $(countId).textContent = visible + ' keys';
} }
function extruderChip(id) { function extruderChip(id) {
if (id == null || id < 1) return '<span class="badge">extruder —</span>'; if (id == null || id < 1) return '<span class="badge">extruder —</span>';
const color = S.colors[id - 1]; return '<span class="badge">' + swatch(S.colors[id - 1]) + 'extruder ' + id + '</span>';
return '<span class="badge">' + (color ? '<span class="swatch" style="background:' +
esc(color) + '"></span>' : '') + 'extruder ' + id + '</span>';
} }
const VOL_GLYPH = { ModelPart:'', NegativeVolume:'', ParameterModifier:'', const VOL_GLYPH = { ModelPart:'', NegativeVolume:'', ParameterModifier:'',
SupportEnforcer:'', SupportBlocker:'' }; SupportEnforcer:'', SupportBlocker:'' };
function node(head, body, open) { function node(head, body, open) {
return '<div class="node' + (open ? ' open' : '') + '">' + return '<div class="node' + (open ? ' open' : '') + '">' +
'<div class="nhead" data-act="toggle"><span class="twist">▶</span>' + head + '</div>' + '<div class="nhead" data-act="toggle" role="button" tabindex="0" aria-expanded="' +
!!open + '"><span class="twist">▶</span>' + head + '</div>' +
'<div class="nbody">' + body + '</div></div>'; '<div class="nbody">' + body + '</div></div>';
} }
function paintedBadges(p) { function paintedBadges(p) {
@@ -969,7 +1059,7 @@ function drawPoints(canvas, pts, ax, ay) {
PREVIEWS[canvas.id] = { pts, ax, ay }; PREVIEWS[canvas.id] = { pts, ax, ay };
const scaleDpr = window.devicePixelRatio || 1; const scaleDpr = window.devicePixelRatio || 1;
const w = canvas.clientWidth * scaleDpr, h = canvas.clientHeight * scaleDpr; const w = canvas.clientWidth * scaleDpr, h = canvas.clientHeight * scaleDpr;
if (!w || !h) return; // inside a collapsed node; repainted on toggle if (!w || !h) { delete canvas.dataset.painted; return; } // collapsed; repainted on toggle
canvas.dataset.painted = '1'; canvas.dataset.painted = '1';
canvas.width = w; canvas.height = h; canvas.width = w; canvas.height = h;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
@@ -983,7 +1073,7 @@ function drawPoints(canvas, pts, ax, ay) {
(h - 2 * pad) / Math.max(maxY - minY, 1e-6)); (h - 2 * pad) / Math.max(maxY - minY, 1e-6));
const ox = (w - (maxX - minX) * scale) / 2, oy = (h - (maxY - minY) * scale) / 2; const ox = (w - (maxX - minX) * scale) / 2, oy = (h - (maxY - minY) * scale) / 2;
const style = getComputedStyle(document.body); const style = getComputedStyle(document.body);
ctx.strokeStyle = style.getPropertyValue('--orca-accent'); ctx.strokeStyle = style.getPropertyValue('--accent');
ctx.strokeRect(ox, oy, (maxX - minX) * scale, (maxY - minY) * scale); ctx.strokeRect(ox, oy, (maxX - minX) * scale, (maxY - minY) * scale);
ctx.fillStyle = style.color; ctx.fillStyle = style.color;
ctx.globalAlpha = 0.55; ctx.globalAlpha = 0.55;
@@ -992,6 +1082,21 @@ function drawPoints(canvas, pts, ax, ay) {
ctx.fillRect(ox + (p[ax] - minX) * scale - r / 2, ctx.fillRect(ox + (p[ax] - minX) * scale - r / 2,
h - oy - (p[ay] - minY) * scale - r / 2, r, r); h - oy - (p[ay] - minY) * scale - r / 2, r, r);
} }
// A live theme switch rewrites the injected vars and re-stamps data-orca-theme, and a
// resize leaves the bitmap at its old size — both repaint every known preview.
function repaintPreviews() {
document.querySelectorAll('canvas.preview').forEach(c => {
const p = PREVIEWS[c.id];
if (p) drawPoints(c, p.pts, p.ax, p.ay);
});
}
new MutationObserver(repaintPreviews)
.observe(document.documentElement, { attributes:true, attributeFilter:['data-orca-theme'] });
let resizeTimer;
window.addEventListener('resize', () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(repaintPreviews, 150);
});
/* ------------------------------------------------------------ UI toolkit */ /* ------------------------------------------------------------ UI toolkit */
function renderUikit() { function renderUikit() {
@@ -1052,6 +1157,7 @@ $('content').addEventListener('click', e => {
if (act === 'toggle') { if (act === 'toggle') {
const node = t.parentElement; const node = t.parentElement;
node.classList.toggle('open'); node.classList.toggle('open');
t.setAttribute('aria-expanded', node.classList.contains('open'));
// Repaint any preview whose canvas was hidden (zero-size) when its data arrived. // Repaint any preview whose canvas was hidden (zero-size) when its data arrived.
node.querySelectorAll('canvas.preview').forEach(c => { node.querySelectorAll('canvas.preview').forEach(c => {
const p = PREVIEWS[c.id]; const p = PREVIEWS[c.id];
@@ -1062,9 +1168,10 @@ $('content').addEventListener('click', e => {
t.textContent = 'Loading…'; t.textContent = 'Loading…';
orca.postMessage({ command:'mesh', object:+t.dataset.o, volume:+t.dataset.v }); orca.postMessage({ command:'mesh', object:+t.dataset.o, volume:+t.dataset.v });
} else if (act === 'pcfg') { } else if (act === 'pcfg') {
const coll = t.dataset.coll; const coll = t.dataset.coll; // slot buttons carry their preset in data-name
orca.postMessage({ command:'preset_config', collection:coll, orca.postMessage({ command:'preset_config', collection:coll,
name:$('sel-' + coll).value }); name:t.dataset.name !== undefined ? t.dataset.name
: $('sel-' + coll).value });
} else if (act === 'expand') { } else if (act === 'expand') {
const td = t.closest('td'); const td = t.closest('td');
td.textContent = td.dataset.full; td.textContent = td.dataset.full;
@@ -1072,11 +1179,22 @@ $('content').addEventListener('click', e => {
uiAction(t.dataset.ui); uiAction(t.dataset.ui);
} }
}); });
// Keyboard parity for the click-only elements (nav items, tree toggles, "show all"):
// Enter/Space activates the focused one; real form controls keep their native keys.
document.addEventListener('keydown', e => {
if (e.key !== 'Enter' && e.key !== ' ') return;
if (/^(BUTTON|INPUT|SELECT|TEXTAREA)$/.test(e.target.tagName)) return;
const t = e.target.closest('.item, [data-act]');
if (!t) return;
e.preventDefault();
t.click();
});
/* ------------------------------------------------------------- messages */ /* ------------------------------------------------------------- messages */
orca.onMessage(msg => { orca.onMessage(msg => {
if (!msg || !msg.command) return; if (!msg || !msg.command) return;
if (msg.command === 'section') { if (msg.command === 'section') {
if (msg.gen !== S.gen) return; // raced a Refresh; the refetch is in flight
S.cache[msg.section] = msg; S.cache[msg.section] = msg;
S.busy[msg.section] = false; S.busy[msg.section] = false;
stamp(); stamp();
@@ -1085,14 +1203,15 @@ orca.onMessage(msg => {
const container = $('mesh-' + msg.object + '-' + msg.volume); const container = $('mesh-' + msg.object + '-' + msg.volume);
if (!container) return; if (!container) return;
if (msg.ok) renderMeshDetail(container, msg.data); if (msg.ok) renderMeshDetail(container, msg.data);
else container.innerHTML = '<span class="muted">⚠ ' + esc(msg.error) + '</span>'; else container.innerHTML =
'<button class="small secondary" data-act="mesh" data-o="' + msg.object +
'" data-v="' + msg.volume + '">Retry</button> <span class="muted">⚠ ' +
esc(msg.error) + '</span>';
} else if (msg.command === 'preset_config') { } else if (msg.command === 'preset_config') {
// Success opened a viewer window; this slot only shows (or clears) failures.
const container = $('pcfg-' + msg.collection); const container = $('pcfg-' + msg.collection);
if (!container) return; if (!container) return;
container.innerHTML = msg.ok container.innerHTML = msg.ok ? '' : '<p class="muted">⚠ ' + esc(msg.error) + '</p>';
? '<div class="subhead">' + esc(msg.name) + ' · ' + msg.data.rows.length +
' keys</div>' + configTable(msg.data.rows, 'pcfg-table-' + msg.collection)
: '<span class="muted">⚠ ' + esc(msg.error) + '</span>';
} else if (msg.command === 'ui_result') { } else if (msg.command === 'ui_result') {
addLog(msg.ok ? '' + msg.action + ': ' + msg.result addLog(msg.ok ? '' + msg.action + ': ' + msg.result
: '' + msg.action + ' failed: ' + msg.error); : '' + msg.action + ' failed: ' + msg.error);
@@ -1110,28 +1229,33 @@ show('overview');
# The modal page demoed from the UI Toolkit tab: create_window(style=WINDOW_MODAL) # The modal page demoed from the UI Toolkit tab: create_window(style=WINDOW_MODAL)
# keeps the handle alive for callbacks; orca.submit(payload) invokes on_submit. # keeps the handle alive for callbacks; orca.submit(payload) invokes on_submit.
MODAL_PAGE = r"""<!DOCTYPE html> MODAL_PAGE = r"""<!DOCTYPE html>
<html><head><meta charset="utf-8"></head> <html lang="en"><head><meta charset="utf-8"><style>""" + BASE_CSS + r"""
<body style="padding:18px"> body { padding:18px; }
<h3 style="margin-top:0">Modal dialog</h3> h3 { margin-top:0; }
input { width:100%; }
</style></head>
<body>
<h3>Modal dialog</h3>
<p>create_window(style=WINDOW_MODAL) keeps this page interactive until it submits or closes.</p> <p>create_window(style=WINDOW_MODAL) keeps this page interactive until it submits or closes.</p>
<p><input id="note" style="width:100%" value="hello from the modal"></p> <p><input id="note" value="hello from the modal"></p>
<p style="text-align:right"> <p style="text-align:right">
<button style="background:transparent;color:var(--orca-fg);border-color:var(--orca-border)" <button class="secondary" onclick="orca.postMessage({live: document.getElementById('note').value})">Post while open</button>
onclick="orca.postMessage({live: document.getElementById('note').value})">Post while open</button> <button class="secondary" onclick="orca.close()">Cancel</button>
<button style="background:transparent;color:var(--orca-fg);border-color:var(--orca-border)"
onclick="orca.close()">Cancel</button>
<button onclick="orca.submit({note: document.getElementById('note').value})">Submit</button> <button onclick="orca.submit({note: document.getElementById('note').value})">Submit</button>
</p> </p>
</body></html> </body></html>
""" """
CHILD_PAGE = r"""<!DOCTYPE html> CHILD_PAGE = r"""<!DOCTYPE html>
<html><head><meta charset="utf-8"></head> <html lang="en"><head><meta charset="utf-8"><style>""" + BASE_CSS + r"""
<body style="padding:18px"> body { padding:18px; }
<h3 style="margin-top:0">Child window</h3> h3 { margin-top:0; }
</style></head>
<body>
<h3>Child window</h3>
<p>Same create_window() API — one plugin, several windows.</p> <p>Same create_window() API — one plugin, several windows.</p>
<p><button onclick="orca.postMessage({command:'ping'})">Ping the plugin</button></p> <p><button onclick="orca.postMessage({command:'ping'})">Ping the plugin</button></p>
<div id="log" style="color:var(--orca-muted)"></div> <div id="log" style="color:var(--muted)"></div>
<script> <script>
var n = 0; var n = 0;
orca.onMessage(function (msg) { orca.onMessage(function (msg) {
@@ -1142,6 +1266,75 @@ CHILD_PAGE = r"""<!DOCTYPE html>
</body></html> </body></html>
""" """
# The per-preset config viewer, opened by "View config" on the Presets tab. The rows
# are baked in as JSON at build time, so the window needs no bridge traffic at all;
# it stays open (and comparable side by side with the panel) until closed.
CONFIG_PAGE = r"""<!DOCTYPE html>
<html lang="en"><head><meta charset="utf-8"><style>""" + BASE_CSS + CFG_TABLE_CSS + r"""
body { padding:12px 16px 20px; border-top:3px solid var(--hue, var(--accent)); }
.toolbar { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-bottom:10px; }
.toolbar h3 { margin:0; font-size:13px; }
.toolbar input { flex:1; min-width:140px; }
.dot { display:inline-block; width:9px; height:9px; border-radius:50%; flex:none;
background:var(--hue, var(--accent)); }
.count { color:var(--muted); font-size:12px; white-space:nowrap; }
</style></head>
<body>
<div class="toolbar"><span class="dot"></span><h3 id="name"></h3>
<span class="count" id="count"></span>
<input id="q" placeholder="Filter keys…">
<button class="secondary" onclick="orca.close()">Close</button></div>
<div id="table"></div>
<script>
'use strict';
const DATA = __DATA__;
const $ = id => document.getElementById(id);
const esc = s => String(s == null ? '' : s)
.replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
if (DATA.hue) document.body.style.setProperty('--hue', DATA.hue);
$('name').textContent = DATA.name;
$('table').innerHTML = '<table class="cfg" id="t"><tr><th>key</th><th>value</th></tr>' +
DATA.rows.map(r => {
const long = r.v.length > 160;
const shown = long ? esc(r.v.slice(0, 160)) +
'<span class="more" role="button" tabindex="0"> … show all</span>' : esc(r.v);
return '<tr data-k="' + esc(r.k.toLowerCase()) + '"><td>' + esc(r.k) + '</td>' +
'<td class="val" data-full="' + esc(r.v) + '">' + shown + '</td></tr>';
}).join('') + '</table>';
function filter(q) {
q = q.trim().toLowerCase();
let visible = 0;
document.querySelectorAll('#t tr[data-k]').forEach(tr => {
const hit = !q || tr.dataset.k.includes(q) ||
tr.querySelector('.val').dataset.full.toLowerCase().includes(q);
tr.style.display = hit ? '' : 'none';
if (hit) visible++;
});
$('count').textContent = visible + ' keys';
}
$('q').addEventListener('input', e => filter(e.target.value));
$('table').addEventListener('click', e => {
const t = e.target.closest('.more');
if (t) { const td = t.closest('td'); td.textContent = td.dataset.full; }
});
document.addEventListener('keydown', e => {
if ((e.key === 'Enter' || e.key === ' ') && e.target.classList.contains('more')) {
e.preventDefault();
e.target.click();
}
});
filter('');
</script>
</body></html>
"""
def config_page(collection, name, rows):
"""CONFIG_PAGE with one preset's rows baked in. `</` is escaped so a config
value containing e.g. `</script>` 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 # the plugin
@@ -1150,6 +1343,7 @@ class OrcaInspectorPanel(orca.script.ScriptPluginCapabilityBase):
win = None win = None
child = None child = None
modal = None modal = None
cfgwin = None
def get_name(self): def get_name(self):
return "Orca Inspector" return "Orca Inspector"
@@ -1166,6 +1360,9 @@ class OrcaInspectorPanel(orca.script.ScriptPluginCapabilityBase):
if self.modal is not None: if self.modal is not None:
self.modal.close() self.modal.close()
self.modal = None 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 # Non-modal: returns immediately. The window is host-owned and lives on
# after execute() returns; on_message keeps firing when the page posts. # after execute() returns; on_message keeps firing when the page posts.
self.win = orca.host.ui.create_window( self.win = orca.host.ui.create_window(
@@ -1184,11 +1381,11 @@ class OrcaInspectorPanel(orca.script.ScriptPluginCapabilityBase):
msg = msg or {} msg = msg or {}
command = msg.get("command") command = msg.get("command")
if command == "fetch": if command == "fetch":
self.send_section(msg.get("section", "")) self.send_section(msg.get("section", ""), msg.get("gen"))
elif command == "mesh": elif command == "mesh":
self.send_mesh(int(msg.get("object", -1)), int(msg.get("volume", -1))) self.send_mesh(msg.get("object", -1), msg.get("volume", -1))
elif command == "preset_config": elif command == "preset_config":
self.send_preset_config(msg.get("collection", ""), msg.get("name", "")) self.open_preset_config(msg.get("collection", ""), msg.get("name", ""))
elif command == "ui": elif command == "ui":
self.run_ui_action(msg) self.run_ui_action(msg)
@@ -1196,11 +1393,15 @@ class OrcaInspectorPanel(orca.script.ScriptPluginCapabilityBase):
if self.child is not None: if self.child is not None:
self.child.close() self.child.close()
self.child = None self.child = None
if self.cfgwin is not None:
self.cfgwin.close()
self.cfgwin = None
print("Orca Inspector closed") print("Orca Inspector closed")
def send_section(self, section): def send_section(self, section, gen=None):
builder = SECTION_BUILDERS.get(section) builder = SECTION_BUILDERS.get(section)
reply = {"command": "section", "section": 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: if builder is None:
self.win.post({**reply, "ok": False, "error": f"unknown section {section!r}"}) self.win.post({**reply, "ok": False, "error": f"unknown section {section!r}"})
return return
@@ -1213,15 +1414,23 @@ class OrcaInspectorPanel(orca.script.ScriptPluginCapabilityBase):
reply = {"command": "mesh", "object": object_index, "volume": volume_index} reply = {"command": "mesh", "object": object_index, "volume": volume_index}
try: try:
self.win.post({**reply, "ok": True, self.win.post({**reply, "ok": True,
"data": build_mesh(object_index, volume_index)}) "data": build_mesh(int(object_index), int(volume_index))})
except Exception as exc: except Exception as exc:
self.win.post({**reply, "ok": False, "error": str(exc)}) self.win.post({**reply, "ok": False, "error": str(exc)})
def send_preset_config(self, collection, name): def open_preset_config(self, collection, name):
# One viewer at a time: a new pick replaces the previous window. The main
# page only ever hears back about failures (and clears them on success).
reply = {"command": "preset_config", "collection": collection, "name": name} reply = {"command": "preset_config", "collection": collection, "name": name}
try: try:
self.win.post({**reply, "ok": True, rows = build_preset_config(collection, name)
"data": 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: except Exception as exc:
self.win.post({**reply, "ok": False, "error": str(exc)}) self.win.post({**reply, "ok": False, "error": str(exc)})
@@ -1267,15 +1476,19 @@ class OrcaInspectorPanel(orca.script.ScriptPluginCapabilityBase):
if self.child is not None and self.child.is_open(): if self.child is not None and self.child.is_open():
report("child already open") report("child already open")
else: else:
# Fresh payload: `reply` here carries action "child_open", but the close
# can be triggered later by "child_close" too, so label it neutrally.
self.child = orca.host.ui.create_window( self.child = orca.host.ui.create_window(
title="Orca Inspector — child", html=CHILD_PAGE, title="Orca Inspector — child", html=CHILD_PAGE,
width=380, height=240, on_message=self.on_child_message, width=380, height=240, on_message=self.on_child_message,
on_close=lambda: self.win.post( on_close=lambda: self.win.post(
{**reply, "ok": True, "result": "child window closed"})) {"command": "ui_result", "action": "child",
"ok": True, "result": "child window closed"}))
report("child opened") report("child opened")
elif action == "child_close": elif action == "child_close":
if self.child is not None: if self.child is not None:
self.child.close() self.child.close()
report("close requested") # the actual close is logged by on_close
else: else:
report("no child window") report("no child window")
elif action == "child_state": elif action == "child_state":