From b5430b53e766bffefcc1b3fc59cc4d2a64f3af6b Mon Sep 17 00:00:00 2001 From: SoftFever Date: Fri, 24 Jul 2026 19:41:30 +0800 Subject: [PATCH] feature/improve_plugin_config_for_slicing_plugin --- .../web/dialog/PluginsConfigDialog/index.html | 1 + .../web/dialog/PluginsConfigDialog/index.js | 53 +- resources/web/dialog/PluginsDialog/index.html | 1 + resources/web/dialog/PluginsDialog/index.js | 36 +- resources/web/dialog/js/plugin-config-ui.js | 130 +++++ sandboxes/orca_twistify_plugin_example_any.py | 491 +++++++++++++++++- src/libslic3r/PrintObject.cpp | 3 +- 7 files changed, 641 insertions(+), 74 deletions(-) create mode 100644 resources/web/dialog/js/plugin-config-ui.js diff --git a/resources/web/dialog/PluginsConfigDialog/index.html b/resources/web/dialog/PluginsConfigDialog/index.html index 8e44bac35d..6ce4d29dcc 100644 --- a/resources/web/dialog/PluginsConfigDialog/index.html +++ b/resources/web/dialog/PluginsConfigDialog/index.html @@ -48,6 +48,7 @@ + diff --git a/resources/web/dialog/PluginsConfigDialog/index.js b/resources/web/dialog/PluginsConfigDialog/index.js index 013ec7ac16..3df85e0a33 100644 --- a/resources/web/dialog/PluginsConfigDialog/index.js +++ b/resources/web/dialog/PluginsConfigDialog/index.js @@ -9,6 +9,11 @@ let selectedCapabilityType = ""; let selectedHasPresetOverride = false; let selectedReadOnly = false; +// Identity of the capability whose custom UI is currently loaded in the frame. Saving re-sends the +// whole capability_config payload, and rebuilding the frame from it would reload the plugin's page +// under the user's cursor; when this still matches, the new values are posted in instead. +let customFrameKey = ""; + function SafeJsonParse(text) { try { return JSON.parse(text); @@ -168,6 +173,10 @@ function IsCurrentCapability(payload) { && String(payload?.capability_type || "") === selectedCapabilityType; } +function CapabilityKey(payload) { + return JSON.stringify([payload?.plugin_key, payload?.capability_name, payload?.capability_type]); +} + function RequestCapabilityConfig() { if (!selectedPluginKey || !selectedCapabilityName) return; @@ -193,6 +202,7 @@ function ClearCapabilityConfigView() { if (custom) { custom.hidden = true; custom.removeAttribute("srcdoc"); + customFrameKey = ""; } if (text) text.value = ""; @@ -248,8 +258,14 @@ function ApplyCapabilityConfig(payload) { if (html) { if (custom) { + const context = OrcaConfigContext(payload, "preset"); custom.hidden = false; - custom.srcdoc = BuildCustomConfigDocument(html, config); + if (customFrameKey === CapabilityKey(payload) && custom.contentWindow) { + custom.contentWindow.postMessage({ __orca: "config", config: config, context: context }, "*"); + } else { + customFrameKey = CapabilityKey(payload); + custom.srcdoc = BuildCustomConfigDocument(html, config, context); + } } if (editor) editor.hidden = true; @@ -260,6 +276,7 @@ function ApplyCapabilityConfig(payload) { if (custom) { custom.hidden = true; custom.removeAttribute("srcdoc"); + customFrameKey = ""; } if (editor) editor.hidden = false; @@ -354,39 +371,6 @@ function ApplyCapabilityConfigSaved(payload) { SetConfigValidation(""); } -// The whole host surface a custom config UI gets: read the config, save one, drop the preset's -// override, and be told when either lands. The frame is sandboxed into an opaque origin, so this -// bridge is its only channel. -function BuildCustomConfigDocument(html, config) { - // Inlined into a " would close the tag early, so escape "<" — the - // literal stays valid JSON. - const seed = JSON.stringify(config).replace(/ -(function () { - var handlers = []; - var current = ${seed}; - window.orca = { - getConfig: function () { return current; }, - saveConfig: function (cfg) { parent.postMessage({ __orca: "save", config: cfg }, "*"); }, - restoreDefaults: function () { parent.postMessage({ __orca: "restore" }, "*"); }, - onConfig: function (cb) { - if (typeof cb !== "function") return; - handlers.push(cb); - try { cb(current); } catch (e) {} - } - }; - window.addEventListener("message", function (event) { - if (!event.data || event.data.__orca !== "config") return; - current = event.data.config || {}; - handlers.forEach(function (handler) { - try { handler(current); } catch (e) {} - }); - }); -})(); -<\/script>`; - return bridge + html; -} - function OnCustomConfigMessage(event) { const custom = document.getElementById("configCustom"); // Only the frame we created, and only while it is actually showing. @@ -432,6 +416,7 @@ document.addEventListener("DOMContentLoaded", () => { // OnCustomConfigMessage matches on the frame's contentWindow, not the origin ("null" when // sandboxed), and ignores anything else. window.addEventListener("message", OnCustomConfigMessage); + OrcaWatchThemeForFrame(() => document.getElementById("configCustom")); SendMessage("request_capabilities"); }); diff --git a/resources/web/dialog/PluginsDialog/index.html b/resources/web/dialog/PluginsDialog/index.html index 1e879339ed..3cee806786 100644 --- a/resources/web/dialog/PluginsDialog/index.html +++ b/resources/web/dialog/PluginsDialog/index.html @@ -15,6 +15,7 @@ + diff --git a/resources/web/dialog/PluginsDialog/index.js b/resources/web/dialog/PluginsDialog/index.js index cd690ef8b1..036fc8552a 100644 --- a/resources/web/dialog/PluginsDialog/index.js +++ b/resources/web/dialog/PluginsDialog/index.js @@ -90,6 +90,7 @@ function OnInit() { // OnCustomConfigMessage matches on the frame's contentWindow, not the origin ("null" when // sandboxed), and ignores anything else. window.addEventListener("message", OnCustomConfigMessage); + OrcaWatchThemeForFrame(() => document.getElementById("configCustom")); document.addEventListener("click", (event) => { if (!event.target.closest(".ctx")) @@ -1077,7 +1078,7 @@ function ApplyCapabilityConfig(payload) { if (html) { if (custom) { custom.hidden = false; - custom.srcdoc = BuildCustomConfigDocument(html, config); + custom.srcdoc = BuildCustomConfigDocument(html, config, OrcaConfigContext(payload, "global")); } if (editor) editor.hidden = true; @@ -1178,39 +1179,6 @@ function ApplyCapabilityConfigSaved(payload) { SetConfigValidation(""); } -// The whole host surface a custom config UI gets: read the config, save one, restore the plugin's -// defaults, and be told when either lands. The frame is sandboxed into an opaque origin, so this -// bridge is its only channel. -function BuildCustomConfigDocument(html, config) { - // Inlined into a " would close the tag early, so escape "<" — the - // literal stays valid JSON. - const seed = JSON.stringify(config).replace(/ -(function () { - var handlers = []; - var current = ${seed}; - window.orca = { - getConfig: function () { return current; }, - saveConfig: function (cfg) { parent.postMessage({ __orca: "save", config: cfg }, "*"); }, - restoreDefaults: function () { parent.postMessage({ __orca: "restore" }, "*"); }, - onConfig: function (cb) { - if (typeof cb !== "function") return; - handlers.push(cb); - try { cb(current); } catch (e) {} - } - }; - window.addEventListener("message", function (event) { - if (!event.data || event.data.__orca !== "config") return; - current = event.data.config || {}; - handlers.forEach(function (handler) { - try { handler(current); } catch (e) {} - }); - }); -})(); -<\/script>`; - return bridge + html; -} - function OnCustomConfigMessage(event) { const custom = document.getElementById("configCustom"); // Only the frame we created, and only while it is actually showing. diff --git a/resources/web/dialog/js/plugin-config-ui.js b/resources/web/dialog/js/plugin-config-ui.js new file mode 100644 index 0000000000..0f16c0c48c --- /dev/null +++ b/resources/web/dialog/js/plugin-config-ui.js @@ -0,0 +1,130 @@ +// The host surface a plugin capability's custom configuration UI gets, shared by the Plugins dialog +// and by a preset's plugin configuration dialog. Both host the page in an iframe sandboxed into an +// opaque origin, so this bridge is its only channel, and both must offer plugin authors exactly the +// same one — hence a single module rather than a copy per dialog. + +// The host theme "contract" (WebViewHostDialog::host_theme_vars_css). The document-start injector +// stamps it on the top-level page only — it returns early in child frames — so a sandboxed config UI +// never sees it unless we hand it over. +const ORCA_THEME_VARS = [ + "--orca-bg", + "--orca-fg", + "--orca-muted", + "--orca-border", + "--orca-accent", + "--orca-accent-fg", + "--orca-font" +]; + +// Read the contract off this page as it is rendering right now, so the frame always opens in the +// live theme rather than whatever the app started in. +function OrcaThemeSnapshot() { + const style = getComputedStyle(document.documentElement); + const vars = {}; + ORCA_THEME_VARS.forEach((name) => { + // Values are host-produced colors and a pre-sanitized font stack; strip anything that could end + // the declaration or the + +""" + + class Twistify(orca.slicing.SlicingPipelineCapabilityBase): def get_name(self): return "Twistify" - + def get_default_config(self): return _DEFAULTS + def has_config_ui(self): + return True + + def get_config_ui(self): + return _CONFIG_UI.replace("__ORCA_DEFAULTS__", json.dumps(_DEFAULTS)) + def execute(self, ctx): if ctx.step != orca.slicing.Step.posSlice or ctx.object is None: return orca.ExecutionResult.success() diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 0d31f6c04c..1a4131aca2 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -1254,7 +1254,8 @@ bool PrintObject::invalidate_state_by_config_options( || opt_key == "interlocking_beam_layer_count" || opt_key == "interlocking_depth" || opt_key == "interlocking_boundary_avoidance" - || opt_key == "interlocking_beam_width") { + || opt_key == "interlocking_beam_width" + || opt_key == "plugin_config_overrides") { steps.emplace_back(posSlice); } else if ( opt_key == "elefant_foot_compensation"