Search improvements. Code refactoring so its easier to maintain. Experimental features for speed dial: connect/disconnect from printer.

This commit is contained in:
Lam Wei Lun
2026-09-09 16:53:58 +08:00
parent c3a21fa0d4
commit 3985200672
15 changed files with 968 additions and 770 deletions
+107 -78
View File
@@ -39,37 +39,60 @@ var qEl = null, listEl = null, favEl = null, clearEl = null, eyeEl = null, count
// ---- pure helpers (no DOM; unit-tested) -------------------------------------
// Pre-normalized haystacks, cached on the action object. The fold is length-preserving (1:1 per
// char) so the ranges FuzzyRangesNorm returns slice the ORIGINAL title/source text correctly. The
// action objects arrive from C++ and are stable for the dialog's lifetime, so we compute these once.
// char) so the ranges FuzzyRangesNorm returns slice the ORIGINAL title/group/source text correctly.
// The action objects arrive from C++ and are stable for the dialog's lifetime, so we compute these once.
function titleNorm(a) {
if (a._tn === undefined)
a._tn = NormText(a.title, false);
return a._tn;
}
function otherNorm(a) {
if (a._on === undefined)
a._on = NormText((a.source || "") + " " + (a.group || ""), false);
return a._on;
// The eyebrow (header) line shows group when present, else source. Settings keep an empty group so
// their source path is the eyebrow; commands / plates / recents carry a non-empty group. Splitting the
// two lets a match range stay aligned to whichever string the eyebrow actually renders.
function groupNorm(a) {
if (a._gn === undefined)
a._gn = NormText(a.group || "", false);
return a._gn;
}
function sourceNorm(a) {
if (a._sn === undefined)
a._sn = NormText(a.source || "", false);
return a._sn;
}
// Relevance score for a single field vs the current query needle, or -1 when there's no match.
// Higher is better: an earlier start and a more contiguous (fewer gaps) match beat a scattered late one.
function matchScoreNorm(haystackNorm) {
if (!searchNeedle) return -1;
var r = FuzzyRangesNorm(haystackNorm || "", searchNeedle);
if (!r) return -1;
var gaps = 0;
for (var i = 1; i < r.length; i++)
gaps += r[i][0] - r[i - 1][1];
return 1000 - r[0][0] * 10 - gaps * 10;
// Match one pre-normalized field vs the current needle. Returns {score, ranges, contiguous} when the
// needle is present, else null. wwRe is a compiled whole-word (\b-bounded) regex for the current needle.
// A whole-word hit is preferred - it highlights the full word (e.g. "orient" in "Auto-Orient", not the
// stray "o" of "Auto") and marks a perfect match. Otherwise FuzzyRangesNorm (which now prefers the
// most-contiguous run) is used. score is higher for an earlier start and fewer gaps; contiguous marks
// a perfect match - the whole needle landed as one unbroken run.
function fieldMatchScore(norm, wwRe) {
if (!searchNeedle) return null;
if (wwRe) {
var m = wwRe.exec(norm || "");
if (m)
return {score: 1000 - m.index * 10, ranges: [[m.index, m.index + m[0].length]], contiguous: true};
}
var r = FuzzyRangesNorm(norm || "", searchNeedle);
if (!r) return null;
var gaps = 0, len = 0;
for (var i = 0; i < r.length; i++) {
if (i > 0)
gaps += r[i][0] - r[i - 1][1];
len += r[i][1] - r[i][0];
}
return {score: 1000 - r[0][0] * 10 - gaps * 10, ranges: r, contiguous: r.length === 1 && len === searchNeedle.length};
}
// Per-action score: title matches rank above a source/group-only match of equal quality.
function actionSearchScore(a) {
var title = matchScoreNorm(titleNorm(a));
var other = matchScoreNorm(otherNorm(a));
if (title < 0 && other < 0) return -1;
return Math.max(title < 0 ? -1e9 : title + 10000, other < 0 ? -1e9 : other);
// Combine the per-field match scores into one comparable value. Ranking tiers, strongest first:
// tier (contiguous/perfect vs fuzzy) > field (title > group > source) > start/gaps.
// The additive weights keep every contiguous match above every fuzzy one regardless of field.
function scoreFields(t, g, s) {
var best = -1;
if (t) best = Math.max(best, (t.contiguous ? 100000 : 0) + 2000 + t.score);
if (g) best = Math.max(best, (g.contiguous ? 100000 : 0) + 1000 + g.score);
if (s) best = Math.max(best, (s.contiguous ? 100000 : 0) + s.score);
return best;
}
// The unified main-phase search: every action (command/plugin/setting) matching the query, ranked
@@ -82,15 +105,27 @@ function searchActions(actions, query) {
matchIndex = {};
if (!q) { searchNeedle = ""; return list.slice(0); }
searchNeedle = NormText(q, false);
// Compiled once per pass, reused over every field: non-global so no exec()/lastIndex state leaks
// between fields, and EscapeRegExp keeps regex metachars in the query literal.
var wwRe = new RegExp("\\b" + EscapeRegExp(searchNeedle) + "\\b");
var scored = [];
for (var i = 0; i < list.length; i++) {
var a = list[i];
var s = actionSearchScore(a);
if (s < 0) continue;
var titleMatch = FuzzyRangesNorm(titleNorm(a), searchNeedle);
matchIndex[a.id] = { title: titleMatch, source: FuzzyRangesNorm(otherNorm(a), searchNeedle), useTitle: !!titleMatch };
scored.push({ a: a, s: s });
var t = fieldMatchScore(titleNorm(a), wwRe);
var g = fieldMatchScore(groupNorm(a), wwRe);
var s = fieldMatchScore(sourceNorm(a), wwRe);
var score = scoreFields(t, g, s);
if (score < 0) continue;
// Ranges are per-field against the ACTUAL text drawn: title for the row-name, and group (or
// source when group is empty) for the eyebrow - so highlight offsets stay aligned to the label.
matchIndex[a.id] = {
title: t ? t.ranges : null,
group: g ? g.ranges : null,
source: s ? s.ranges : null,
useEyebrowGroup: !!(a.group)
};
scored.push({ a: a, s: score });
}
scored.sort(function (x, y) {
if (x.s !== y.s) return y.s - x.s;
@@ -112,7 +147,7 @@ function buildKey() { return phase + "|" + (query || "").trim(); }
function visibleFavourites(favourites, actions) {
// why: a fav whose id has no live action (plugin unloaded/disabled) renders a dead
// monogram tile whose click run()s to a silent no-op; drop it from the quick-bar.
// placeholder tile whose click run()s to a silent no-op; drop it from the quick-bar.
var seen = {};
(actions || []).forEach(function (a) { seen[a.id] = true; });
return (favourites || []).filter(function (id, i, arr) {
@@ -164,21 +199,42 @@ function shouldRenderActionList(query) {
return !!((query || "").trim());
}
// Put an action's pattern pictogram into a tile (search row or favourites tile) when it has one,
// otherwise fall back to the monogram. Toggles the has-icon class so CSS neutralises the hue.
// Monogram code for a tile: title initial, escalated on collision by PREPENDING the source
// initial (pi+ti, e.g. "GC"), then a 1-based ordinal - so same-titled items stay distinct.
// why: ordinal is assigned by id, not by list order - list order is frecency-sorted and
// reshuffles as usage changes, which would otherwise flip who's "1" and who's "2" across runs.
function monogramFor(item, list, titleOf, sourceOf, idOf) {
var items = list || [];
var title = titleOf(item) || " ";
var ti = title.charAt(0).toUpperCase();
var sameTitle = items.filter(function (o) { return (titleOf(o) || " ").charAt(0).toUpperCase() === ti; });
if (sameTitle.length <= 1)
return ti;
var source = sourceOf(item) || " ";
var pi = source.charAt(0).toUpperCase();
var sameSource = sameTitle.filter(function (o) { return (sourceOf(o) || " ").charAt(0).toUpperCase() === pi; });
if (sameSource.length <= 1)
return pi + ti;
sameSource.sort(function (a, b) { return idOf(a) < idOf(b) ? -1 : idOf(a) > idOf(b) ? 1 : 0; });
for (var i = 0; i < sameSource.length; i++)
if (sameSource[i] === item || idOf(sameSource[i]) === idOf(item))
return pi + ti + (i + 1);
return pi + ti;
}
// Action tile code - see monogramFor for the escalation ladder. Settings are actions now, so
// they share this ladder (title initial, then source, then a stable ordinal).
function tileCode(action, actions) {
return monogramFor(action, actions,
function (o) { return o.title; },
function (o) { return o.source; },
function (o) { return o.id; });
}
// Put an action's monogram into a tile (search row or favourites tile). A null action (a tab row
// with no backing action) renders an empty tile.
function fillTile(tile, a) {
tile.classList.remove("has-icon");
if (a && a.icon) {
tile.textContent = "";
var img = document.createElement("img");
img.className = "tile-icon";
img.src = a.icon;
img.alt = "";
tile.appendChild(img);
tile.classList.add("has-icon");
} else {
tile.textContent = a ? tileCode(a, ACTIONS) : "";
}
tile.textContent = a ? tileCode(a, ACTIONS) : "";
}
// The active list for the main phase. A typed query ranks every action (commands/plugins/settings)
@@ -224,38 +280,6 @@ function actionLabel(action, actions) {
return label;
}
// Monogram code for a tile: title initial, escalated on collision by PREPENDING the source
// initial (pi+ti, e.g. "GC"), then a 1-based ordinal - so same-titled items stay distinct.
// why: ordinal is assigned by id, not by list order - list order is frecency-sorted and
// reshuffles as usage changes, which would otherwise flip who's "1" and who's "2" across runs.
function monogramFor(item, list, titleOf, sourceOf, idOf) {
var items = list || [];
var title = titleOf(item) || " ";
var ti = title.charAt(0).toUpperCase();
var sameTitle = items.filter(function (o) { return (titleOf(o) || " ").charAt(0).toUpperCase() === ti; });
if (sameTitle.length <= 1)
return ti;
var source = sourceOf(item) || " ";
var pi = source.charAt(0).toUpperCase();
var sameSource = sameTitle.filter(function (o) { return (sourceOf(o) || " ").charAt(0).toUpperCase() === pi; });
if (sameSource.length <= 1)
return pi + ti;
sameSource.sort(function (a, b) { return idOf(a) < idOf(b) ? -1 : idOf(a) > idOf(b) ? 1 : 0; });
for (var i = 0; i < sameSource.length; i++)
if (sameSource[i] === item || idOf(sameSource[i]) === idOf(item))
return pi + ti + (i + 1);
return pi + ti;
}
// Action tile code - see monogramFor for the escalation ladder. Settings are actions now, so
// they share this ladder (title initial, then source, then a stable ordinal).
function tileCode(action, actions) {
return monogramFor(action, actions,
function (o) { return o.title; },
function (o) { return o.source; },
function (o) { return o.id; });
}
function syncClearButton() {
if (clearEl)
clearEl.hidden = !query;
@@ -542,7 +566,12 @@ function renderActionRow(a, i) {
var left = document.createElement("div");
left.className = "row-left";
var mi = matchIndex[a.id];
var sourceEl = markedText("row-eyebrow", a.group || a.source, mi ? mi.source : null);
// The eyebrow shows group when present, else source. Highlight with the ranges of whichever of the
// two the eyebrow actually renders (so a "Recent Projects"/"Object" header match lights up like a
// setting path does - the offsets are computed against the same string we are marking).
var eyebrow = a.group || a.source;
var eyebrowMatch = mi ? (mi.useEyebrowGroup ? mi.group : mi.source) : null;
var sourceEl = markedText("row-eyebrow", eyebrow, eyebrowMatch);
var line = document.createElement("div");
line.className = "row-line";
var name = markedText("row-name", a.title, mi ? mi.title : null);
@@ -675,8 +704,8 @@ function renderCommandsList() {
updateSelection();
}
// A tab row: no star/unpin (tabs aren't pinnable), tile monogram from the title. Uses tabTitle so
// pages added with an empty text (e.g. Home) still show a label and an icon letter.
// A tab row: no star/unpin (tabs aren't pinnable), placeholder tile (tabs have no pictogram). Uses
// tabTitle so pages added with an empty text (e.g. Home) still show a label.
function renderTabRow(t, i) {
var label = tabTitle(t);
var row = document.createElement("div");
@@ -686,7 +715,7 @@ function renderTabRow(t, i) {
var tile = document.createElement("div");
tile.className = "tile";
tile.style.setProperty("--h", hue(t.id));
tile.textContent = label.charAt(0).toUpperCase();
fillTile(tile, null);
var left = document.createElement("div");
left.className = "row-left";
@@ -59,7 +59,7 @@ assert.equal(ctx.tabTitle({ id: "home" }), "Home",
assert.equal(ctx.tabTitle({ id: "prepare", title: "Prepare" }), "Prepare",
"a populated title is kept as-is");
assert.equal(ctx.tabTitle({ id: "prepare", title: " Prepare" }), "Prepare",
"a leading space from the Notebook button label is trimmed so the icon letter shows");
"a leading space from the Notebook button label is trimmed so the label shows cleanly");
assert.equal(ctx.filterTabs([{ id: "home", title: "" }], "home").length, 1,
"an untitled tab still matches a typed query via the id/title fallback");
assert.equal(ctx.filterTabs([{ id: "prepare", title: " Prepare" }], "prepare").length, 1,
@@ -82,6 +82,33 @@ assert.equal(ctx.searchActions(pool, "layer").length >= 2, true,
assert.equal(ctx.searchActions(pool, "surface")[0].id, "c2",
"a later-but-precise match still ranks by relevance, not by pool type");
// A perfect match (the needle as one contiguous run) outranks a fuzzy match of the same field - and a
// contiguous GROUP/header hit ("Recent Projects") beats a scattered fuzzy TITLE hit ("Retraction Length"),
// which is what the old flat title-bonus ranking got backwards.
const perfectPool = [
{ id: "set", title: "Retraction Length", source: "Process : Quality : Retraction", group: "", input: "" },
{ id: "recent", title: "myproject.3mf", source: "/home/me/projects/myproject.3mf", group: "Recent Projects", input: "" }
];
assert.equal(ctx.searchActions(perfectPool, "recent")[0].id, "recent",
"a contiguous header/group match ranks above a scattered fuzzy title match");
// Within a perfect match, the row-name (title) outranks the header (group): the action whose TITLE
// contains the needle perfectly beats the action whose GROUP does, both being contiguous matches.
const titleFirstPool = [
{ id: "grp", title: "Delete Selected", source: "OrcaSlicer", group: "Object", input: "" },
{ id: "t", title: "Object Preview", source: "OrcaSlicer", group: "View", input: "" }
];
assert.equal(ctx.searchActions(titleFirstPool, "object")[0].id, "t",
"a perfect title match ranks above an equally-perfect group match");
// Highlighting: the needle is matched as a whole word / most-contiguous run, so "orient" lights up the
// whole word in "Auto-Orient" instead of the stray "o" of "Auto" plus "rient" (greedy-leftmost).
const orientPool = [
{ id: "ao", title: "Auto-Orient", source: "OrcaSlicer", group: "Object", input: "" }
];
ctx.searchActions(orientPool, "orient");
assert.deepEqual(ctx.matchIndex.ao.title, [[5, 11]],
"a whole-word match highlights the full word, not a scattered fuzzy pick");
// commandList (the main-phase list) delegates to the ranked search for a typed query and returns
// the mixed recents (no discrimination) for an empty query.
const mixed = [
+2 -13
View File
@@ -43,8 +43,8 @@ body {
cursor: pointer;
color: hsl(var(--h) var(--speed-tile-text-s, 72%) var(--speed-tile-text-l, 38%));
font-weight: 700;
/* why: mirror .tile centering - tileCode can be 2-3 chars (e.g. "EA1") on collision, and a
bare <button> inherits 13px + UA padding, clipping the code (e.g. "AE1"). inline-flex + 12px + pad:0 fits it. */
/* why: mirror .tile centering - icons/placeholder are 18px glyphs, and a bare <button> inherits
13px + UA padding, which would clip them. inline-flex + pad:0 + overflow:hidden fits them. */
font-size: 12px;
padding: 0;
display: inline-flex;
@@ -227,17 +227,6 @@ body {
background: var(--speed-tile-bg, #f0f0f0);
border: 1px solid var(--speed-tile-border, #d8d8d8);
}
/* A tile holding a pattern pictogram: drop the hue fill so the stroked SVG reads cleanly. */
.tile.has-icon,
.fav-tile.has-icon {
background: none;
border-color: transparent;
}
.tile-icon {
width: 18px;
height: 18px;
display: block;
}
.row-left { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
.row-eyebrow {
font-size: 10px;
+46 -11
View File
@@ -34,24 +34,44 @@ function NormText(text, caseSensitive) {
// Match a PRE-normalized haystack against a PRE-normalized needle (both produced by NormText with the
// same caseSensitive flag). Skipping the per-character fold makes repeated matching (per keystroke over a
// cached pool) cheap. Returns ranges in original coordinates, or null on no match.
// Prefers the most-contiguous (smallest-span) occurrence over greedy-leftmost: a scattered match that
// spans a stray earlier character is worse than a tight run later, so "orient" against a normalized
// "auto-orient" returns [[5,11]] (the word) not [[3,4],[6,11]]. A fully contiguous run is the optimum
// and short-circuits early.
function FuzzyRangesNorm(haystackNorm, needleNorm) {
const t = haystackNorm || "";
const needle = needleNorm || "";
if (!needle)
return null;
const ranges = [];
let qi = 0;
for (let i = 0; i < t.length && qi < needle.length; i++) {
if (t[i] === needle[qi]) {
const last = ranges[ranges.length - 1];
if (last && last[1] === i)
last[1] = i + 1;
else
ranges.push([i, i + 1]);
qi++;
const n = t.length, nl = needle.length;
let best = null; // {ranges, span, start}
for (let start = 0; start < n; start++) {
if (t[start] !== needle[0])
continue;
let qi = 0;
const ranges = [];
let lastEnd = start;
for (let i = start; i < n && qi < nl; i++) {
if (t[i] === needle[qi]) {
const last = ranges[ranges.length - 1];
if (last && last[1] === i)
last[1] = i + 1;
else
ranges.push([i, i + 1]);
lastEnd = i + 1;
qi++;
}
}
if (qi !== nl)
continue;
const span = lastEnd - start;
if (!best || span < best.span || (span === best.span && start < best.start)) {
best = { ranges, span, start };
if (span === nl)
return ranges; // can't beat a fully contiguous run
}
}
return qi === needle.length ? ranges : null;
return best ? best.ranges : null;
}
function EscapeRegExp(value) {
@@ -95,3 +115,18 @@ function WholeWordRanges(text, query, caseSensitive) {
ranges.push([match.index, match.index + match[0].length]);
return ranges.length > 0 ? ranges : null;
}
// Same whole-word (\b-bounded) match as WholeWordRanges, but against PRE-normalized haystack/needle
// (NormText output, so offsets stay length-aligned to the original text). Returns the first match as
// [[i, i+len]] in original coordinates, or null. Non-global so the caller can reuse one compiled regex
// across many fields without re-setting lastIndex. Skipping the per-char fold keeps the Speed Dial's
// per-keystroke scan over thousands of cached settings cheap.
function WholeWordRangesNorm(haystackNorm, needleNorm) {
const t = haystackNorm || "";
const needle = needleNorm || "";
if (!needle)
return null;
const re = new RegExp(`\\b${EscapeRegExp(needle)}\\b`);
const match = re.exec(t);
return match ? [[match.index, match.index + match[0].length]] : null;
}
+12 -1
View File
@@ -5,7 +5,7 @@ const vm = require("vm"), assert = require("assert"), fs = require("fs");
const ctx = {};
vm.createContext(ctx);
vm.runInContext(fs.readFileSync(__dirname + "/fuzzy-search.js", "utf8"), ctx);
const { FoldChar, Norm, EscapeRegExp, FuzzyRanges, WholeWordRanges } = ctx;
const { FoldChar, Norm, NormText, EscapeRegExp, FuzzyRanges, WholeWordRanges, FuzzyRangesNorm, WholeWordRangesNorm } = ctx;
// FoldChar / Norm: accents fold, case only folds when case-insensitive.
assert.equal(FoldChar("é"), "e");
@@ -34,4 +34,15 @@ assert.deepEqual(WholeWordRanges("a.b", "a", false), [[0, 1]]); // '.' is a
assert.equal(EscapeRegExp("a.b*"), "a\\.b\\*");
assert.deepEqual(WholeWordRanges("c++ tool", "c", false), [[0, 1]]); // '+' would be a regex error unescaped
// FuzzyRangesNorm: prefers the most-contiguous (smallest-span) occurrence over greedy-leftmost, so a
// stray earlier character does not steal the highlight from a later tight word.
assert.deepEqual(FuzzyRangesNorm(NormText("Auto-Orient", false), NormText("orient", false)), [[5, 11]]);
// A contiguous run inside a larger word is also preferred over a scattered greedy pick.
assert.deepEqual(FuzzyRangesNorm(NormText("AutoOriented", false), NormText("orient", false)), [[4, 10]]);
assert.equal(FuzzyRangesNorm(NormText("Measure", false), NormText("xyz", false)), null); // no subsequence
// WholeWordRangesNorm: \b-bounded literal against a pre-normalized haystack, offsets in original coords.
assert.deepEqual(WholeWordRangesNorm(NormText("Auto-Orient", false), NormText("orient", false)), [[5, 11]]);
assert.equal(WholeWordRangesNorm(NormText("AutoOriented", false), NormText("orient", false)), null); // inside a word
console.log("ok");
+2
View File
@@ -127,6 +127,8 @@ set(SLIC3R_GUI_SOURCES
GUI/SpeedDialDialog.hpp
GUI/ActionRegistry.cpp
GUI/ActionRegistry.hpp
GUI/NativeCommands.cpp
GUI/NativeCommands.hpp
GUI/PluginsConfigDialog.cpp
GUI/PluginsConfigDialog.hpp
GUI/ProcessRunner.cpp
+98 -568
View File
@@ -1,41 +1,29 @@
#include "ActionRegistry.hpp"
#include "calib_dlg.hpp"
#include "GCodeViewer.hpp"
#include "GLCanvas3D.hpp"
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "I18N.hpp"
#include "IMSlider.hpp"
#include "MainFrame.hpp"
#include "NativeCommands.hpp"
#include "Notebook.hpp"
#include "Plater.hpp"
#include "PlateSettingsDialog.hpp"
#include "Search.hpp"
#include "Tab.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include <libslic3r/AppConfig.hpp>
#include <libslic3r/Config.hpp>
#include <libslic3r/PresetBundle.hpp>
#include <libslic3r/Utils.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <wx/thread.h>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/any.hpp>
#include <boost/algorithm/string/trim.hpp>
#include <boost/filesystem.hpp>
#include <boost/nowide/convert.hpp>
#include <algorithm>
#include <cfloat>
#include <cmath>
#include <cstdlib>
#include <ctime>
#include <exception>
#include <fstream>
#include <iterator>
#include <string>
#include <unordered_map>
@@ -145,6 +133,7 @@ constexpr const char* kOrcaSourceKey = "orca";
constexpr const char* kOrcaSourceName = "OrcaSlicer";
constexpr const char* kSettingPrefix = "orca_setting";
constexpr const char* kPlateGotoPrefix = "orca_plate_goto";
constexpr const char* kRecentProjectPrefix = "orca_recent_project";
// Display context for a setting action's eyebrow, e.g. the "Process" in "Process : Quality : Layers".
// Keyed by the option's preset type so the palette reads like the settings sidebar tabs.
@@ -168,7 +157,7 @@ struct SettingAction : AppAction
{
std::string opt_key;
Preset::Type type;
std::wstring category; // localized category, forwarded to jump_to_option
std::wstring category; // localized category, forwarded to jump_to_option
static std::string id_for(const std::string& opt_key, Preset::Type type)
{ return std::string(kSettingPrefix) + ":" + opt_key + ":" + std::to_string(int(type)); }
@@ -191,405 +180,32 @@ struct SettingAction : AppAction
wxGetApp().sidebar().jump_to_option(opt_key, type, category);
return {AppActionRunResult::Level::Success};
}
// The current value's pattern pictogram (e.g. the selected infill pattern), for the search-result
// tile. Defined below after the icon helper it delegates to.
std::string icon() const override;
};
// Jump the preview to a layer selected by a 0-100 percent of the layer range. Best-effort:
// switches to the preview tab and requests a slice (select_view_3D("Preview", false)); if the
// slicer result is already present the slider is repositioned immediately, otherwise the user
// can re-run after slicing.
void go_to_layer(Plater* plater, const std::string& param)
{
if (!plater)
return;
double pct = 50.0;
try {
pct = std::stod(param);
} catch (const std::exception&) {}
pct = std::clamp(pct, 0.0, 100.0);
GLCanvas3D* canvas = plater->get_current_canvas3D();
if (!canvas)
return;
GCodeViewer& viewer = canvas->get_gcode_viewer();
IMSlider* layers = viewer.get_layers_slider();
IMSlider* moves = viewer.get_moves_slider();
if (!layers || layers->GetMaxValue() <= 0)
return; // no slice result yet - the slice request above will populate it
const double max = double(layers->GetMaxValue());
const int target = int(std::lround(pct / 100.0 * max));
layers->SetHigherValue(target);
// In "one layer" mode the lower handle follows the higher one (mirrors arrow-key nav).
if (layers->is_one_layer())
layers->SetLowerValue(target);
layers->set_as_dirty();
if (moves) {
moves->SetHigherValue(moves->GetMaxValue());
moves->set_as_dirty();
}
}
// Select a named camera view ("top"/"front"/...); Plater::select_view dispatches to the current
// panel. Shared by the view_* speed-dial commands.
AppActionRunResult view_command(Plater* plater, const std::string& dir)
{
if (plater)
plater->select_view(dir);
return {AppActionRunResult::Level::Success};
}
// Dispatch a built-in command. The CommandAction stays a thin value; the actual GUI work
// lives here so it can touch the live app state.
AppActionRunResult run_native_command(const std::string& command_key, const std::string& param)
{
GUI_App& app = wxGetApp();
if (app.is_closing())
return {};
Plater* plater = app.plater();
if (command_key == "save_project") {
if (plater)
plater->save_project(false);
return {AppActionRunResult::Level::Success};
}
if (command_key == "save_project_as") {
if (plater)
plater->save_project(true);
return {AppActionRunResult::Level::Success};
}
if (command_key == "load_project") {
if (plater)
plater->load_project();
return {AppActionRunResult::Level::Success};
}
if (command_key == "open_preferences") {
app.open_preferences();
return {AppActionRunResult::Level::Success};
}
if (command_key == "mode_simple" || command_key == "mode_advanced" || command_key == "mode_expert") {
const int mode = command_key == "mode_simple" ? comSimple : command_key == "mode_advanced" ? comAdvanced : comExpert;
app.save_mode(mode);
return {AppActionRunResult::Level::Success};
}
if (command_key == "slice_and_preview") {
if (plater) {
// Actually re-slice (respects the toolbar's current plate/all selection), then show the result.
plater->reslice();
plater->select_view_3D("Preview", false);
if (app.mainframe)
app.mainframe->select_tab(TAB_ID_PREVIEW);
}
return {AppActionRunResult::Level::Success};
}
if (command_key == "go_to_layer") {
if (plater) {
plater->select_view_3D("Preview", false);
if (app.mainframe)
app.mainframe->select_tab(TAB_ID_PREVIEW);
go_to_layer(plater, param);
}
return {AppActionRunResult::Level::Success};
}
// "go_to_tab" is two-phase: the palette collects the tab after activating it, so native
// dispatch here is a no-op (the jump goes through the go_to_tab web command).
if (command_key == "go_to_tab")
return {AppActionRunResult::Level::Success};
// ---- Slice -> Export pipeline. Each Plater method self-guards (empty model / error /
// background-invalid) and then opens its own save dialog / show_error, mirroring the File menu.
if (command_key == "export_gcode") {
if (plater)
plater->export_gcode(false);
return {AppActionRunResult::Level::Success};
}
if (command_key == "export_stl") {
if (plater)
plater->export_stl();
return {AppActionRunResult::Level::Success};
}
if (command_key == "export_3mf") {
if (plater)
plater->export_core_3mf();
return {AppActionRunResult::Level::Success};
}
if (command_key == "export_sliced_file") {
if (plater)
plater->export_gcode_3mf();
return {AppActionRunResult::Level::Success};
}
if (command_key == "export_all_sliced_file") {
if (plater)
plater->export_gcode_3mf(true);
return {AppActionRunResult::Level::Success};
}
// ---- Calibration wizards. Each mirrors the menu handler (MainFrame.cpp): recreate the dialog
// fresh per launch. The palette hides itself and defers dispatch off the webview callback, so a
// ShowModal() here is safe (same path as open_preferences). The 3D panel is ensured below.
auto calib = [&](auto&& open) -> AppActionRunResult {
if (!plater)
return {AppActionRunResult::Level::Info, _L("Open the 3D view first.")};
// Auto-switch to the Prepare (3D) view instead of prompting: set the 3D panel
// synchronously (the wizard's new_project also re-establishes it) and select the
// Prepare notebook page so the tab label matches. The palette is hidden and this
// dispatch is deferred off the webview callback, so a modal on a switched tab is safe.
if (!plater->is_view3D_shown()) {
plater->select_view_3D("3D");
if (MainFrame* mf = wxGetApp().mainframe; mf)
mf->select_tab(TAB_ID_PREPARE);
}
open(plater);
return {AppActionRunResult::Level::Success};
};
if (command_key == "calib_temperature")
return calib([](Plater* p) {
Temp_Calibration_Dlg* dlg = new Temp_Calibration_Dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, p);
dlg->ShowModal();
dlg->Destroy();
});
if (command_key == "calib_max_volumetric")
return calib([](Plater* p) {
MaxVolumetricSpeed_Test_Dlg* dlg = new MaxVolumetricSpeed_Test_Dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, p);
dlg->ShowModal();
dlg->Destroy();
});
if (command_key == "calib_pressure_advance")
return calib([](Plater* p) {
PA_Calibration_Dlg* dlg = new PA_Calibration_Dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, p);
dlg->ShowModal();
dlg->Destroy();
});
if (command_key == "calib_flow_ratio")
return calib([](Plater* p) {
FlowRateCalibrationDialog* dlg = new FlowRateCalibrationDialog((wxWindow*) wxGetApp().mainframe, wxID_ANY, p);
dlg->ShowModal();
dlg->Destroy();
});
if (command_key == "calib_retraction")
return calib([](Plater* p) {
Retraction_Test_Dlg* dlg = new Retraction_Test_Dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, p);
dlg->ShowModal();
dlg->Destroy();
});
if (command_key == "calib_cornering")
return calib([](Plater* p) {
Cornering_Test_Dlg* dlg = new Cornering_Test_Dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, p);
dlg->ShowModal();
dlg->Destroy();
});
if (command_key == "calib_input_shaping_freq")
return calib([](Plater* p) {
Input_Shaping_Freq_Test_Dlg* dlg = new Input_Shaping_Freq_Test_Dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, p);
dlg->ShowModal();
dlg->Destroy();
});
if (command_key == "calib_input_shaping_damp")
return calib([](Plater* p) {
Input_Shaping_Damp_Test_Dlg* dlg = new Input_Shaping_Damp_Test_Dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, p);
dlg->ShowModal();
dlg->Destroy();
});
if (command_key == "calib_vfa")
return calib([](Plater* p) {
VFA_Test_Dlg* dlg = new VFA_Test_Dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, p);
dlg->ShowModal();
dlg->Destroy();
});
// ---- View controls. select_view dispatches to the current panel; named views + perspective
// toggle + fit-to-bed mirror the View menu items (MainFrame.cpp). reset_window_layout is direct.
if (command_key == "view_top")
return view_command(plater, "top");
if (command_key == "view_bottom")
return view_command(plater, "bottom");
if (command_key == "view_front")
return view_command(plater, "front");
if (command_key == "view_rear")
return view_command(plater, "rear");
if (command_key == "view_left")
return view_command(plater, "left");
if (command_key == "view_right")
return view_command(plater, "right");
if (command_key == "view_iso")
return view_command(plater, "iso");
if (command_key == "view_default") {
if (plater) {
plater->select_view("plate");
if (GLCanvas3D* canvas = plater->get_current_canvas3D())
canvas->zoom_to_bed();
}
return {AppActionRunResult::Level::Success};
}
if (command_key == "view_fit_bed") {
if (plater)
if (GLCanvas3D* canvas = plater->get_current_canvas3D())
canvas->zoom_to_bed();
return {AppActionRunResult::Level::Success};
}
if (command_key == "view_toggle_perspective") {
if (plater)
plater->get_camera().select_next_type();
return {AppActionRunResult::Level::Success};
}
if (command_key == "reset_window_layout") {
if (plater)
plater->reset_window_layout();
return {AppActionRunResult::Level::Success};
}
// ---- Object / interaction operations (single-phase). Each mirrors a toolbar/menu action and is
// guarded by an existing can_* / selection check so nothing crashes on empty selection or a busy
// background worker, and returns a friendly Info instead. Structural ops self-update()/schedule a
// re-slice; transform ops (mirror/center/drop) post their own schedule-background event. We only
// need the underlying object (not a specific object index), so a non-capturing lambda is used as
// the guard/op pair below. Rotate/scale by angle/factor, duplicate (modal count dialog) and
// cut/segment/merge (unimplemented on Plater) are deliberately left out of this MVP.
auto obj = [&](bool (*ok)(Plater*), void (*op)(Plater*)) -> AppActionRunResult {
if (!plater)
return {AppActionRunResult::Level::Info, _L("Open the 3D view first.")};
// Object ops read the Prepare (3D) canvas selection, so ensure that view before guarding so
// a launch from the Preview/other tab doesn't report a spuriously empty selection.
if (!plater->is_view3D_shown()) {
plater->select_view_3D("3D");
if (MainFrame* mf = wxGetApp().mainframe; mf)
mf->select_tab(TAB_ID_PREPARE);
}
if (!ok(plater))
return {AppActionRunResult::Level::Info, _L("Select an object first.")};
op(plater);
return {AppActionRunResult::Level::Success};
};
if (command_key == "obj_delete")
return obj([](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->remove_selected(); });
if (command_key == "obj_delete_all")
return obj([](Plater* p) { return p->can_delete_all(); }, [](Plater* p) { p->delete_all_objects_from_model(); });
if (command_key == "obj_mirror_x")
return obj([](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::X); });
if (command_key == "obj_mirror_y")
return obj([](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::Y); });
if (command_key == "obj_mirror_z")
return obj([](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::Z); });
if (command_key == "obj_split_objects")
return obj([](Plater* p) { return p->can_split_to_objects(); }, [](Plater* p) { p->split_object(true); });
if (command_key == "obj_split_parts")
return obj([](Plater* p) { return p->can_split_to_volumes(); }, [](Plater* p) { p->split_volume(); });
if (command_key == "obj_center")
return obj([](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->center_selection(); });
if (command_key == "obj_drop")
return obj([](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->drop_selection(); });
if (command_key == "obj_fit_volume")
return obj([](Plater* p) { return p->can_scale_to_print_volume(); }, [](Plater* p) { p->scale_selection_to_fit_print_volume(); });
if (command_key == "obj_instances_up")
return obj([](Plater* p) { return p->can_increase_instances(); }, [](Plater* p) { p->increase_instances(); });
if (command_key == "obj_instances_down")
return obj([](Plater* p) { return p->can_decrease_instances(); }, [](Plater* p) { p->decrease_instances(); });
if (command_key == "obj_arrange")
return obj([](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->arrange(); });
// Auto-orient has no dedicated can_*; can_arrange covers "objects exist + UI worker idle".
if (command_key == "obj_orient")
return obj([](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->orient(); });
// ---- Plate management. Plates are a filament (FFF) feature: SLA builds a single plate with
// no plate UI, and gcode-only mode has no editable project - so gate every plate op on FFF +
// the normal editor (mirroring where the plate toolbar/menu live). These act on the CURRENT
// plate (delete/duplicate take -1) except plate_goto, which jumps to the index in `param`.
auto plate_plater = [&]() -> Plater* {
return (plater && plater->printer_technology() == ptFFF && !plater->only_gcode_mode()) ? plater : nullptr;
};
const AppActionRunResult plate_unavailable{AppActionRunResult::Level::Info, _L("Plates are a filament (FFF) feature.")};
if (command_key == "plate_add") {
if (Plater* p = plate_plater(); p) {
if (!p->can_add_plate())
return {AppActionRunResult::Level::Info, _L("Cannot add another plate (maximum reached).")};
p->add_plate();
return {AppActionRunResult::Level::Success};
}
return plate_unavailable;
}
if (command_key == "plate_duplicate") {
if (Plater* p = plate_plater(); p) {
if (!p->can_add_plate())
return {AppActionRunResult::Level::Info, _L("Cannot duplicate a plate (maximum reached).")};
p->duplicate_plate();
return {AppActionRunResult::Level::Success};
}
return plate_unavailable;
}
if (command_key == "plate_delete") {
if (Plater* p = plate_plater(); p) {
if (!p->can_delete_plate())
return {AppActionRunResult::Level::Info, _L("Cannot delete the only plate.")};
p->delete_plate();
return {AppActionRunResult::Level::Success};
}
return plate_unavailable;
}
if (command_key == "plate_rename") {
if (Plater* p = plate_plater(); p) {
PartPlate* curr = p->get_partplate_list().get_curr_plate();
PlateNameEditDialog dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, _L("Edit Plate Name"));
dlg.set_plate_name(from_u8(curr->get_plate_name()));
if (dlg.ShowModal() == wxID_YES)
curr->set_plate_name(dlg.get_plate_name().ToUTF8().data());
return {AppActionRunResult::Level::Success};
}
return plate_unavailable;
}
if (command_key == "plate_toggle_lock") {
if (Plater* p = plate_plater(); p) {
PartPlateList& plates = p->get_partplate_list();
const int index = plates.get_curr_plate_index();
p->take_snapshot("lock partplate");
plates.lock_plate(index, !plates.is_locked(index));
return {AppActionRunResult::Level::Success};
}
return plate_unavailable;
}
if (command_key == "plate_goto") {
if (Plater* p = plate_plater(); p) {
PartPlateList& plates = p->get_partplate_list();
const int count = plates.get_plate_count();
if (count <= 0)
return {AppActionRunResult::Level::Info, _L("No plates available.")};
int index = 0;
try {
index = std::stoi(param);
} catch (const std::exception&) {}
index = std::clamp(index, 0, count - 1);
p->select_plate(index, false);
return {AppActionRunResult::Level::Success};
}
return plate_unavailable;
}
return {AppActionRunResult::Level::Info, _L("Unknown command.")};
}
// A built-in command action. source_key is the constant "orca" so a renamed title never
// re-keys the action (matches the plugin source-key contract).
// A built-in command action. Thin value: identity + presentation come from the NativeCommands
// catalog, and run() routes back to it - the catalog is the single source of truth for its
// behaviour. source_key is the constant "orca" so a renamed title never re-keys the action
// (matches the plugin source-key contract).
struct CommandAction : AppAction
{
static std::unique_ptr<CommandAction> make(const NativeCommand& c)
{ return std::unique_ptr<CommandAction>(new CommandAction(c)); }
std::string command_key;
CommandAction(std::string command_key, std::string title, std::string group, std::string input = "")
: AppAction(kCommandPrefix, std::move(title), kOrcaSourceKey, kOrcaSourceName), command_key(std::move(command_key))
AppActionRunResult run(const std::string& param) const override { return NativeCommands::run(command_key, param); }
private:
explicit CommandAction(const NativeCommand& c)
: AppAction(kCommandPrefix, c.title, kOrcaSourceKey, kOrcaSourceName), command_key(c.key)
{
this->kind = AppActionKind::Command;
this->group = std::move(group);
this->input = std::move(input);
this->group = c.group;
this->input = c.input;
}
AppActionRunResult run(const std::string& param) const override { return run_native_command(command_key, param); }
};
std::unique_ptr<AppAction> make_command(std::string key, std::string title, std::string group, std::string input = "")
{ return std::make_unique<CommandAction>(std::move(key), std::move(title), std::move(group), std::move(input)); }
// A dynamic "Go to Plate N" action, one per live plate, rebuilt on every snapshot() (so a
// rename/move immediately shows up). id is keyed by plate index, NOT the display title, so
// renaming a plate never re-keys it - the same contract as SettingAction. A pinned "Go to
@@ -610,174 +226,37 @@ struct PlateAction : AppAction
}
AppActionRunResult run(const std::string& /*param*/) const override
{ return run_native_command("plate_goto", std::to_string(plate_index)); }
{ return NativeCommands::run("plate_goto", std::to_string(plate_index)); }
};
// The built-in palette commands, registered once at init().
std::vector<std::unique_ptr<AppAction>> native_commands()
// A dynamic "Open recent project <name>" action, one per recent project file, rebuilt on every
// snapshot() (like PlateAction) so the list always reflects the current recents. The id is keyed
// by the file PATH, NOT the display title - the same contract as SettingAction/PlateAction, so a
// rename of a project (or a reordered recents list) never re-keys the action. A pinned recent whose
// file is deleted simply stops resolving (visibleFavourites drops dead pins). run() loads the
// project through MainFrame::open_recent_project so the existing missing-file handling is reused.
struct RecentProjectAction : AppAction
{
std::vector<std::unique_ptr<AppAction>> out;
// why: _u8L (std::string) for titles/groups - make_command takes std::string; _L would
// return a wxString and silently fail to convert here.
out.push_back(make_command("slice_and_preview", _u8L("Slice and Preview"), _u8L("Slice & Export")));
// Two-phase commands: activating them collects input in the palette, then runs. Settings are
// not a command here - they're materialised as first-class SettingActions (see materialize_).
out.push_back(make_command("go_to_layer", _u8L("Go to layer (percent)"), _u8L("Commands"), "percent"));
out.push_back(make_command("go_to_tab", _u8L("Go to tab..."), _u8L("Commands"), "tab"));
out.push_back(make_command("load_project", _u8L("Load Project"), _u8L("Commands")));
out.push_back(make_command("save_project", _u8L("Save Project"), _u8L("Commands")));
out.push_back(make_command("save_project_as", _u8L("Save Project As"), _u8L("Commands")));
out.push_back(make_command("open_preferences", _u8L("Preferences"), _u8L("Commands")));
out.push_back(make_command("mode_simple", _u8L("Mode: Simple"), _u8L("Mode")));
out.push_back(make_command("mode_advanced", _u8L("Mode: Advanced"), _u8L("Mode")));
out.push_back(make_command("mode_expert", _u8L("Mode: Expert"), _u8L("Mode")));
std::string file_path;
// Slice -> Export pipeline. Each runs a public Plater method; the methods self-guard (empty
// model / error / background-invalid) and open their own save dialog / show_error.
out.push_back(make_command("export_gcode", _u8L("Export G-code"), _u8L("Slice & Export")));
out.push_back(make_command("export_stl", _u8L("Export STL"), _u8L("Slice & Export")));
out.push_back(make_command("export_3mf", _u8L("Export 3MF"), _u8L("Slice & Export")));
out.push_back(make_command("export_sliced_file", _u8L("Export Sliced File"), _u8L("Slice & Export")));
out.push_back(make_command("export_all_sliced_file", _u8L("Export All Sliced Files"), _u8L("Slice & Export")));
static std::string id_for(const std::string& path)
{ return AppAction::compose_id(kRecentProjectPrefix, path, kOrcaSourceKey); }
// Calibration wizards (one command per dialog mirroring the Calibration menu, MainFrame.cpp).
out.push_back(make_command("calib_temperature", _u8L("Temperature Calibration"), _u8L("Calibration")));
out.push_back(make_command("calib_max_volumetric", _u8L("Max Volumetric Speed Calibration"), _u8L("Calibration")));
out.push_back(make_command("calib_pressure_advance", _u8L("Pressure Advance Calibration"), _u8L("Calibration")));
out.push_back(make_command("calib_flow_ratio", _u8L("Flow Ratio Calibration"), _u8L("Calibration")));
out.push_back(make_command("calib_retraction", _u8L("Retraction Calibration"), _u8L("Calibration")));
out.push_back(make_command("calib_cornering", _u8L("Cornering Calibration"), _u8L("Calibration")));
out.push_back(make_command("calib_input_shaping_freq", _u8L("Input Shaping Frequency Calibration"), _u8L("Calibration")));
out.push_back(make_command("calib_input_shaping_damp", _u8L("Input Shaping Damping Calibration"), _u8L("Calibration")));
out.push_back(make_command("calib_vfa", _u8L("VFA Calibration"), _u8L("Calibration")));
RecentProjectAction(std::string path, std::string title, std::string source)
: AppAction(AppActionId{id_for(path)}, std::move(title), kOrcaSourceKey, std::move(source))
, file_path(std::move(path))
{
this->kind = AppActionKind::Command;
this->group = _u8L("Recent Projects");
}
// View controls (mirror the View menu; most duplicate the Ctrl+0..6 shortcuts).
out.push_back(make_command("view_top", _u8L("View: Top"), _u8L("View")));
out.push_back(make_command("view_bottom", _u8L("View: Bottom"), _u8L("View")));
out.push_back(make_command("view_front", _u8L("View: Front"), _u8L("View")));
out.push_back(make_command("view_rear", _u8L("View: Rear"), _u8L("View")));
out.push_back(make_command("view_left", _u8L("View: Left"), _u8L("View")));
out.push_back(make_command("view_right", _u8L("View: Right"), _u8L("View")));
out.push_back(make_command("view_iso", _u8L("View: Isometric"), _u8L("View")));
out.push_back(make_command("view_default", _u8L("View: Default"), _u8L("View")));
out.push_back(make_command("view_fit_bed", _u8L("Fit Bed to View"), _u8L("View")));
out.push_back(make_command("view_toggle_perspective", _u8L("Toggle Perspective"), _u8L("View")));
out.push_back(make_command("reset_window_layout", _u8L("Reset Window Layout"), _u8L("View")));
// Object operations (single-phase). Each maps to a public Plater method guarded by a can_* /
// selection check in run_native_command; structural ops self-update()/schedule re-slice.
out.push_back(make_command("obj_delete", _u8L("Delete Selected"), _u8L("Object")));
out.push_back(make_command("obj_delete_all", _u8L("Delete All Objects"), _u8L("Object")));
out.push_back(make_command("obj_mirror_x", _u8L("Mirror X"), _u8L("Object")));
out.push_back(make_command("obj_mirror_y", _u8L("Mirror Y"), _u8L("Object")));
out.push_back(make_command("obj_mirror_z", _u8L("Mirror Z"), _u8L("Object")));
out.push_back(make_command("obj_split_objects", _u8L("Split to Objects"), _u8L("Object")));
out.push_back(make_command("obj_split_parts", _u8L("Split to Parts"), _u8L("Object")));
out.push_back(make_command("obj_center", _u8L("Center Selected on Plate"), _u8L("Object")));
out.push_back(make_command("obj_drop", _u8L("Drop to Bed"), _u8L("Object")));
out.push_back(make_command("obj_fit_volume", _u8L("Scale to Fit Print Volume"), _u8L("Object")));
out.push_back(make_command("obj_instances_up", _u8L("Increase Instances"), _u8L("Object")));
out.push_back(make_command("obj_instances_down", _u8L("Decrease Instances"), _u8L("Object")));
out.push_back(make_command("obj_arrange", _u8L("Auto-Arrange"), _u8L("Object")));
out.push_back(make_command("obj_orient", _u8L("Auto-Orient"), _u8L("Object")));
// Plate management. These act on the CURRENT plate (like Plater::delete_plate(-1)); the
// per-plate "Go to Plate N" actions are dynamic and materialised in materialize_plate_actions().
out.push_back(make_command("plate_add", _u8L("Add Plate"), _u8L("Plate")));
out.push_back(make_command("plate_duplicate", _u8L("Duplicate Plate"), _u8L("Plate")));
out.push_back(make_command("plate_delete", _u8L("Delete Plate"), _u8L("Plate")));
out.push_back(make_command("plate_rename", _u8L("Rename Plate"), _u8L("Plate")));
out.push_back(make_command("plate_toggle_lock", _u8L("Toggle Plate Lock"), _u8L("Plate")));
return out;
}
// ---- setting action helpers --------------------------------------------------
// Self-contained base64 encoder (for the tiny pictogram SVGs), avoiding a dependency on the exact
// wxBase64Encode overload/return type across wx versions.
std::string base64_encode(const std::string& data)
{
static const char* tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
auto enc = [&](unsigned n, int pad) {
// pad = number of extraneous bytes in the final group (0, 1 or 2):
// 0 leftover -> 4 chars from all 24 bits
// 2 leftover (pad=1) -> 3 chars then '='
// 1 leftover (pad=2) -> 2 chars then "=="
// The '=' padding always comes LAST; a misplaced '=' decodes as garbage in the webview.
std::string out;
out.push_back(tbl[(n >> 18) & 63]);
out.push_back(tbl[(n >> 12) & 63]);
out.push_back(pad >= 2 ? '=' : tbl[(n >> 6) & 63]);
out.push_back(pad >= 1 ? '=' : tbl[n & 63]);
return out;
};
std::string out;
out.reserve(((data.size() + 2) / 3) * 4);
size_t i = 0;
for (; i + 3 <= data.size(); i += 3)
out += enc(((unsigned char) data[i]) << 16 | ((unsigned char) data[i + 1]) << 8 | ((unsigned char) data[i + 2]), 0);
if (i + 1 == data.size())
out += enc(((unsigned char) data[i]) << 16, 2);
else if (i + 2 == data.size())
out += enc(((unsigned char) data[i]) << 16 | ((unsigned char) data[i + 1]) << 8, 1);
return out;
}
// data:URI for the pattern pictogram icons/param_<key>.svg, or "" when there is no such icon.
// This mirrors the sidebar Choice field (Field.cpp add_item_bitmaps), which loads param_<value>.svg
// per enum value - most settings have no icon, only pattern-style enums (infill/support patterns).
// Base64 data URIs are used so the embedded webview renders them identically on every backend
// (no file:// subresource / CORS restrictions).
std::string setting_icon_for_key(const std::string& key)
{
if (key.empty())
return {};
const std::string path = (boost::filesystem::path(resources_dir()) / "images" / ("param_" + key + ".svg")).string();
// Non-throwing stat: a throwing filesystem_error here would propagate out of snapshot() and
// abort the app (the palette opener). exists(fs ::error_code) never throws.
boost::system::error_code ec;
if (!boost::filesystem::exists(path, ec))
return {};
std::ifstream in(path, std::ios::binary);
if (!in)
return {};
std::string data((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
if (data.empty())
return {};
return "data:image/svg+xml;base64," + base64_encode(data);
}
// The pattern pictogram for a setting's CURRENT value (its enum int), empty when it isn't a
// pattern-style enum or the value has no icon. Used for the search-result tile.
std::string setting_action_icon(const SettingAction& a)
{
Tab* tab = wxGetApp().get_tab(a.type);
if (!tab || !tab->get_config())
return {};
DynamicPrintConfig* config = tab->get_config();
const ConfigOptionDef* def = config->def()->get(a.opt_key);
if (!def || def->type != coEnum || (int(def->type) & int(coVectorType)) != 0)
return {};
// Read the value WITHOUT config->opt_int(): the non-const overload routes through a type-checked
// option<ConfigOptionInt>() that returns null for enum values (type() is coEnum, not coInt) and
// would deref null. Pull the ConfigOption* and dynamic_cast instead (succeeds: enums derive from
// ConfigOptionInt), falling back to the def default when the option is absent.
const ConfigOption* opt = (config->has(a.opt_key) ? config->option(a.opt_key) : def->default_value.get());
const ConfigOptionInt* int_opt = dynamic_cast<const ConfigOptionInt*>(opt);
if (!int_opt)
return {};
const int value = int_opt->getInt();
if (def->enum_keys_map)
for (const auto& kv : *def->enum_keys_map)
if (kv.second == value)
return setting_icon_for_key(kv.first);
return {};
}
std::string SettingAction::icon() const { return setting_action_icon(*this); }
AppActionRunResult run(const std::string& /*param*/) const override
{
if (MainFrame* mf = wxGetApp().mainframe; mf)
mf->open_recent_project(size_t(-1), wxString::FromUTF8(file_path));
return {AppActionRunResult::Level::Success};
}
};
} // namespace
@@ -838,9 +317,10 @@ void ActionRegistry::init()
// Built-in palette commands (Save/Load, Preferences, Mode switch, Slice/Preview, Go to layer).
// Register after plugins so the plugin ids win on any (unlikely) id collision - ids are distinct
// by prefix, so this is order-independent.
for (auto& action : native_commands())
upsert(std::move(action));
// by prefix, so this is order-independent. The catalog lives in NativeCommands - the registry
// only materialises thin CommandAction values from it.
for (const NativeCommand& c : NativeCommands::catalog())
upsert(CommandAction::make(c));
}
void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange change)
@@ -1060,6 +540,7 @@ void ActionRegistry::materialize_setting_actions()
// (above) is the single display/search breadcrumb rather than being duplicated.
auto action = std::make_unique<SettingAction>(opt.opt_key(), opt.type, boost::nowide::narrow(label_w),
std::string(), opt.category_local, boost::nowide::narrow(path));
action->favourite = std::find(favs.begin(), favs.end(), id) != favs.end();
if (auto it = stats.find(id); it != stats.end() && it->is_object()) {
action->count = it->value("count", 0);
@@ -1137,6 +618,55 @@ void ActionRegistry::materialize_plate_actions()
}
}
void ActionRegistry::materialize_recent_project_actions()
{
assert(wxThread::IsMain());
// Persisted per-action state, read ONCE (mirrors materialize_plate_actions) so a relisted recent
// project keeps its recency/favourite when the recents list reorders - the id is path-keyed.
nlohmann::json stats = read_section("stats", nlohmann::json::object());
if (!stats.is_object())
stats = nlohmann::json::object();
const std::vector<std::string> favs = favourite_ids();
// app_config stores recents oldest-first; the palette shows newest-first.
std::vector<std::string> recents = wxGetApp().app_config->get_recent_projects();
std::reverse(recents.begin(), recents.end());
std::unordered_set<std::string> seen;
for (const std::string& path : recents) {
// Skip projects whose file is gone; the stale id is dropped below.
boost::system::error_code ec;
if (path.empty() || !boost::filesystem::exists(boost::filesystem::path(path), ec))
continue;
const std::string id = RecentProjectAction::id_for(path);
seen.insert(id);
// Title = file basename; source/eyebrow = the full path so search can match either.
boost::filesystem::path p(path);
std::string title = p.filename().string();
if (title.empty())
title = path;
auto action = std::make_unique<RecentProjectAction>(path, std::move(title), path);
action->favourite = std::find(favs.begin(), favs.end(), id) != favs.end();
if (auto it = stats.find(id); it != stats.end() && it->is_object()) {
action->count = it->value("count", 0);
action->last = it->value("last", 0LL);
}
m_actions.insert_or_assign(action->id(), std::shared_ptr<AppAction>(std::move(action)));
}
// Drop recent-project actions whose file no longer exists / was removed from the recents list.
for (auto it = m_actions.begin(); it != m_actions.end();) {
if (it->first.rfind(kRecentProjectPrefix, 0) == 0 && !seen.count(it->first))
it = m_actions.erase(it);
else
++it;
}
}
bool ActionRegistry::should_ask(const std::string& id) const
{
assert(wxThread::IsMain());
@@ -1163,6 +693,7 @@ nlohmann::json ActionRegistry::snapshot()
// the palette opens).
materialize_setting_actions();
materialize_plate_actions();
materialize_recent_project_actions();
std::vector<const AppAction*> sorted;
sorted.reserve(m_actions.size());
@@ -1188,8 +719,7 @@ nlohmann::json ActionRegistry::snapshot()
{"source", a->source_name()},
{"group", a->group},
{"input", a->input},
{"shortcut", ""},
{"icon", a->icon()}});
{"shortcut", ""}});
};
nlohmann::json actions = nlohmann::json::array();
+5 -5
View File
@@ -81,11 +81,6 @@ struct AppAction
// commands (e.g. a layer percentage); plugins ignore it.
virtual AppActionRunResult run(const std::string& param = {}) const = 0;
// Optional data:URI for a small pictogram to show in the palette row/tile/the editor
// (e.g. the current infill/pattern). Empty string = fall back to the monogram. Only
// SettingAction overrides this; the base returns an empty string.
virtual std::string icon() const { return {}; }
protected:
// The definition is constructor-set and immutable. Refreshes replace an action
// instead of mutating identity after the registry has indexed it by id.
@@ -187,6 +182,11 @@ private:
// plate UI, so nothing is materialised and stale ids are dropped. Called at the top of snapshot().
void materialize_plate_actions();
// (Re)materialise one "Open recent project <name>" action per recent project file, so the
// palette lists every recent project and can load it by clicking. Keyed by file path (stable);
// files that no longer exist are skipped and their stale ids dropped. Called at the top of snapshot().
void materialize_recent_project_actions();
// Loader callbacks (marshalled to the UI thread) land here. refresh_source rebuilds
// one plugin's whole action set; refresh_capability touches a single capability.
void refresh_source(const std::string& plugin_key, ActionChange change);
+85 -93
View File
@@ -3440,88 +3440,51 @@ void MainFrame::init_menubar_as_editor()
// Temperature
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Temperature"), _L("Temperature Calibration"),
[this](wxCommandEvent&) {
if (!m_temp_calib_dlg)
m_temp_calib_dlg = new Temp_Calibration_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_temp_calib_dlg->ShowModal();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::Temperature); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Max Volumetric Speed
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Max flowrate"), _L("Max flowrate"),
[this](wxCommandEvent&) {
if (!m_vol_test_dlg)
m_vol_test_dlg = new MaxVolumetricSpeed_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_vol_test_dlg->ShowModal();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::MaxVolumetric); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Pressure Advance
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Pressure advance"), _L("Pressure advance"),
[this](wxCommandEvent&) {
if (!m_pa_calib_dlg)
m_pa_calib_dlg = new PA_Calibration_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_pa_calib_dlg->ShowModal();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::PressureAdvance); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Flow rate (Wizard Dialog)
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Flow ratio"), _L("Flow Rate Calibration"),
[this](wxCommandEvent&) {
if (!m_plater) return;
if (!m_flow_rate_calib_dlg)
m_flow_rate_calib_dlg = new FlowRateCalibrationDialog((wxWindow*)this, wxID_ANY, m_plater);
m_flow_rate_calib_dlg->ShowModal();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::FlowRatio); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Retraction
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Retraction"), _L("Retraction"),
[this](wxCommandEvent&) {
if (!m_retraction_calib_dlg)
m_retraction_calib_dlg = new Retraction_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_retraction_calib_dlg->ShowModal();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::Retraction); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Cornering
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("Cornering"), _L("Cornering calibration"),
[this](wxCommandEvent&) {
auto dlg = new Cornering_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::Cornering); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Input Shaping (with submenu)
auto input_shaping_menu = new wxMenu();
append_menu_item(
input_shaping_menu, wxID_ANY, _L("Input Shaping Frequency"), _L("Input Shaping Frequency"),
[this](wxCommandEvent&) {
auto dlg = new Input_Shaping_Freq_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
},
[this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingFreq); },
"", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
append_menu_item(
input_shaping_menu, wxID_ANY, _L("Input Shaping Damping/zeta factor"), _L("Input Shaping Damping/zeta factor"),
[this](wxCommandEvent&) {
auto dlg = new Input_Shaping_Damp_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
},
[this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingDamp); },
"", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
m_topbar->GetCalibMenu()->AppendSubMenu(input_shaping_menu, _L("Input Shaping"));
// VFA
append_menu_item(m_topbar->GetCalibMenu(), wxID_ANY, _L("VFA"), _L("VFA"),
[this](wxCommandEvent&) {
if (!m_vfa_test_dlg)
m_vfa_test_dlg = new VFA_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_vfa_test_dlg->ShowModal();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::VFA); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// help
@@ -3582,89 +3545,52 @@ void MainFrame::init_menubar_as_editor()
// Temperature
append_menu_item(calib_menu, wxID_ANY, _L("Temperature"), _L("Temperature"),
[this](wxCommandEvent&) {
if (!m_temp_calib_dlg)
m_temp_calib_dlg = new Temp_Calibration_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_temp_calib_dlg->ShowModal();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::Temperature); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Max Volumetric Speed
append_menu_item(calib_menu, wxID_ANY, _L("Max flowrate"), _L("Max flowrate"),
[this](wxCommandEvent&) {
if (!m_vol_test_dlg)
m_vol_test_dlg = new MaxVolumetricSpeed_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_vol_test_dlg->ShowModal();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::MaxVolumetric); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Pressure Advance
append_menu_item(calib_menu, wxID_ANY, _L("Pressure advance"), _L("Pressure advance"),
[this](wxCommandEvent&) {
if (!m_pa_calib_dlg)
m_pa_calib_dlg = new PA_Calibration_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_pa_calib_dlg->ShowModal();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::PressureAdvance); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Flowrate (with submenu)
// ORCA: Flow rate (Wizard Dialog)
append_menu_item(calib_menu, wxID_ANY, _L("Flow ratio"), _L("Flow Rate Calibration"),
[this](wxCommandEvent&) {
if (!m_plater) return;
if (!m_flow_rate_calib_dlg)
m_flow_rate_calib_dlg = new FlowRateCalibrationDialog((wxWindow*)this, wxID_ANY, m_plater);
m_flow_rate_calib_dlg->ShowModal();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::FlowRatio); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Retraction
append_menu_item(calib_menu, wxID_ANY, _L("Retraction"), _L("Retraction"),
[this](wxCommandEvent&) {
if (!m_retraction_calib_dlg)
m_retraction_calib_dlg = new Retraction_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_retraction_calib_dlg->ShowModal();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::Retraction); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Cornering
append_menu_item(calib_menu, wxID_ANY, _L("Cornering"), _L("Cornering calibration"),
[this](wxCommandEvent&) {
auto dlg = new Cornering_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::Cornering); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// Input Shaping (with submenu)
auto input_shaping_menu = new wxMenu();
append_menu_item(
input_shaping_menu, wxID_ANY, _L("Input Shaping Frequency"), _L("Input Shaping Frequency"),
[this](wxCommandEvent&) {
auto dlg = new Input_Shaping_Freq_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
},
[this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingFreq); },
"", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
append_menu_item(
input_shaping_menu, wxID_ANY, _L("Input Shaping Damping/zeta factor"), _L("Input Shaping Damping/zeta factor"),
[this](wxCommandEvent&) {
auto dlg = new Input_Shaping_Damp_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
},
[this](wxCommandEvent&) { run_calibration(CalibKind::InputShapingDamp); },
"", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
calib_menu->AppendSubMenu(input_shaping_menu, _L("Input Shaping"));
// VFA
append_menu_item(calib_menu, wxID_ANY, _L("VFA"), _L("VFA"),
[this](wxCommandEvent&) {
if (!m_vfa_test_dlg)
m_vfa_test_dlg = new VFA_Test_Dlg((wxWindow*)this, wxID_ANY, m_plater);
m_vfa_test_dlg->ShowModal();
}, "", nullptr,
[this](wxCommandEvent&) { run_calibration(CalibKind::VFA); }, "", nullptr,
[this]() {return m_plater->is_view3D_shown();; }, this);
// help
append_menu_item(calib_menu, wxID_ANY, _L("Calibration Guide"), _L("Calibration Guide"),
@@ -4424,7 +4350,73 @@ void MainFrame::technology_changed()
// update menu titles
PrinterTechnology pt = plater()->printer_technology();
if (int id = m_menubar->FindMenu(pt == ptFFF ? _omitL("Material Settings") : _L("Filament settings")); id != wxNOT_FOUND)
m_menubar->SetMenuLabel(id, pt == ptSLA ? _omitL("Material Settings") : _L("Filament settings"));
m_menubar->SetMenuLabel(id, pt == ptFFF ? _omitL("Material Settings") : _L("Filament settings"));
}
// Opens the calibration wizard for `calib_kind`, reusing the cached member dialogs the Calibration
// menu builds. This is the single source of truth for the wizard lifecycle: the Calibration menu
// handlers and the Speed Dial native commands both call it, so they share the same per-wizard
// member (fresh on first launch, reused thereafter). Call while the Prepare (3D) panel is shown.
void MainFrame::run_calibration(CalibKind calib_kind)
{
switch (calib_kind) {
case CalibKind::Temperature: {
if (!m_temp_calib_dlg)
m_temp_calib_dlg = new Temp_Calibration_Dlg((wxWindow*) this, wxID_ANY, m_plater);
m_temp_calib_dlg->ShowModal();
break;
}
case CalibKind::MaxVolumetric: {
if (!m_vol_test_dlg)
m_vol_test_dlg = new MaxVolumetricSpeed_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
m_vol_test_dlg->ShowModal();
break;
}
case CalibKind::PressureAdvance: {
if (!m_pa_calib_dlg)
m_pa_calib_dlg = new PA_Calibration_Dlg((wxWindow*) this, wxID_ANY, m_plater);
m_pa_calib_dlg->ShowModal();
break;
}
case CalibKind::FlowRatio: {
if (!m_plater)
break;
if (!m_flow_rate_calib_dlg)
m_flow_rate_calib_dlg = new FlowRateCalibrationDialog((wxWindow*) this, wxID_ANY, m_plater);
m_flow_rate_calib_dlg->ShowModal();
break;
}
case CalibKind::Retraction: {
if (!m_retraction_calib_dlg)
m_retraction_calib_dlg = new Retraction_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
m_retraction_calib_dlg->ShowModal();
break;
}
case CalibKind::Cornering: {
auto dlg = new Cornering_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
break;
}
case CalibKind::InputShapingFreq: {
auto dlg = new Input_Shaping_Freq_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
break;
}
case CalibKind::InputShapingDamp: {
auto dlg = new Input_Shaping_Damp_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
dlg->ShowModal();
dlg->Destroy();
break;
}
case CalibKind::VFA: {
if (!m_vfa_test_dlg)
m_vfa_test_dlg = new VFA_Test_Dlg((wxWindow*) this, wxID_ANY, m_plater);
m_vfa_test_dlg->ShowModal();
break;
}
}
}
+20
View File
@@ -106,6 +106,21 @@ protected:
void on_dpi_changed(const wxRect& suggested_rect) override;
};
// Calibration wizard identity, shared by MainFrame::run_calibration and the Speed Dial command runners.
// Kept in order with the wizard list below.
enum class CalibKind : int
{
Temperature,
MaxVolumetric,
PressureAdvance,
FlowRatio,
Retraction,
Cornering,
InputShapingFreq,
InputShapingDamp,
VFA
};
class MainFrame : public DPIFrame
{
#ifdef __APPLE__
@@ -349,6 +364,11 @@ public:
void technology_changed();
// Opens the calibration wizard for `kind`, reusing the cached member dialogs the Calibration
// menu builds (m_*_calib_dlg). Single source of truth for the wizard lifecycle: the Calibration
// menu handlers and the Speed Dial native commands both call this. Call while the Prepare (3D)
// panel is shown (menu items are gated on is_view3D_shown; the speed dial ensures it first).
void run_calibration(CalibKind calib_kind);
//BBS
void load_url(wxString url);
+500
View File
@@ -0,0 +1,500 @@
#include "NativeCommands.hpp"
#include "calib_dlg.hpp"
#include "Camera.hpp"
#include "GCodeViewer.hpp"
#include "GLCanvas3D.hpp"
#include "GUI.hpp"
#include "GUI_App.hpp"
#include "I18N.hpp"
#include "IMSlider.hpp"
#include "MainFrame.hpp"
#include "Plater.hpp"
#include "PlateSettingsDialog.hpp"
#include "DeviceCore/DevManager.h"
#include <libslic3r/Utils.hpp>
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <exception>
#include <string>
#include <tuple>
#include <utility>
namespace Slic3r { namespace GUI {
namespace {
// Plate ops are an FFF feature: SLA has a single plate and no plate UI, gcode-only mode has no
// editable project - so gate every plate op on FFF + the normal editor.
bool is_fff_plater(Plater* plater) { return plater && plater->printer_technology() == ptFFF && !plater->only_gcode_mode(); }
AppActionRunResult plate_unavailable() { return {AppActionRunResult::Level::Info, _L("Plates are a filament (FFF) feature.")}; }
// Switch to the Prepare (3D) panel so object/calibration ops have a live canvas + selection, and
// the notebook page label matches. A no-op when the 3D panel is already shown.
void ensure_3d_view(Plater* plater)
{
if (plater && !plater->is_view3D_shown()) {
plater->select_view_3D("3D");
if (MainFrame* mf = wxGetApp().mainframe; mf)
mf->select_tab(TAB_ID_PREPARE);
}
}
// Object op guard + run: object ops read the Prepare canvas selection, so ensure that view first so
// a launch from another tab doesn't report a spuriously empty selection.
AppActionRunResult object_op(Plater* plater, bool (*ok)(Plater*), void (*op)(Plater*))
{
if (!plater)
return {AppActionRunResult::Level::Info, _L("Open the 3D view first.")};
ensure_3d_view(plater);
if (!ok(plater))
return {AppActionRunResult::Level::Info, _L("Select an object first.")};
op(plater);
return {AppActionRunResult::Level::Success};
}
// Jump the preview to a layer selected by a 0-100 percent of the layer range. Best-effort: switches
// to the preview tab and requests a slice; if the slicer result is already present the slider is
// repositioned immediately, otherwise the user can re-run after slicing.
void go_to_layer(Plater* plater, const std::string& param)
{
if (!plater)
return;
double pct = 50.0;
try {
pct = std::stod(param);
} catch (const std::exception&) {}
pct = std::clamp(pct, 0.0, 100.0);
GLCanvas3D* canvas = plater->get_current_canvas3D();
if (!canvas)
return;
GCodeViewer& viewer = canvas->get_gcode_viewer();
IMSlider* layers = viewer.get_layers_slider();
IMSlider* moves = viewer.get_moves_slider();
if (!layers || layers->GetMaxValue() <= 0)
return;
const double max = double(layers->GetMaxValue());
const int target = int(std::lround(pct / 100.0 * max));
layers->SetHigherValue(target);
if (layers->is_one_layer())
layers->SetLowerValue(target);
layers->set_as_dirty();
if (moves) {
moves->SetHigherValue(moves->GetMaxValue());
moves->set_as_dirty();
}
}
// Select a named camera view. Plater::select_view dispatches to the current panel.
AppActionRunResult view_command(Plater* plater, const std::string& dir)
{
if (plater)
plater->select_view(dir);
return {AppActionRunResult::Level::Success};
}
// Calibration wizards. Reuses MainFrame::run_calibration so the speed dial shows the same cached
// member dialogs as the Calibration menu (the menu handlers call run_calibration too).
AppActionRunResult calib_command(CalibKind kind)
{
MainFrame* mf = wxGetApp().mainframe;
if (!mf)
return {AppActionRunResult::Level::Info, _L("Open the 3D view first.")};
ensure_3d_view(wxGetApp().plater());
mf->run_calibration(kind);
return {AppActionRunResult::Level::Success};
}
std::vector<NativeCommand> build_command_catalog()
{
std::vector<NativeCommand> out;
auto add = [&](std::string key, std::string title, std::string group,
std::function<AppActionRunResult(const std::string&)> runner, std::string input = {}) {
out.push_back({std::move(key), std::move(title), std::move(group), std::move(input), std::move(runner)});
};
// ---- Slice & Export ----
add("slice_and_preview", _u8L("Slice and Preview"), _u8L("Slice & Export"), [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (plater) {
plater->reslice();
plater->select_view_3D("Preview", false);
if (MainFrame* mf = wxGetApp().mainframe; mf)
mf->select_tab(TAB_ID_PREVIEW);
}
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add(
"go_to_layer", _u8L("Go to layer (percent)"), _u8L("Commands"),
[](const std::string& param) {
Plater* plater = wxGetApp().plater();
if (plater) {
plater->select_view_3D("Preview", false);
if (MainFrame* mf = wxGetApp().mainframe; mf)
mf->select_tab(TAB_ID_PREVIEW);
go_to_layer(plater, param);
}
return AppActionRunResult{AppActionRunResult::Level::Success};
},
"percent");
// "go_to_tab" is two-phase: the palette collects the tab after activating it, so dispatch here
// is a no-op (the jump goes through the go_to_tab web command).
add(
"go_to_tab", _u8L("Go to tab..."), _u8L("Commands"),
[](const std::string&) { return AppActionRunResult{AppActionRunResult::Level::Success}; }, "tab");
add("load_project", _u8L("Load Project"), _u8L("Commands"), [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->load_project();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("save_project", _u8L("Save Project"), _u8L("Commands"), [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->save_project(false);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("save_project_as", _u8L("Save Project As"), _u8L("Commands"), [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->save_project(true);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("open_preferences", _u8L("Preferences"), _u8L("Commands"), [](const std::string&) {
wxGetApp().open_preferences();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Mode ----
add("mode_simple", _u8L("Mode: Simple"), _u8L("Mode"), [](const std::string&) {
wxGetApp().save_mode(comSimple);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("mode_advanced", _u8L("Mode: Advanced"), _u8L("Mode"), [](const std::string&) {
wxGetApp().save_mode(comAdvanced);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("mode_expert", _u8L("Mode: Expert"), _u8L("Mode"), [](const std::string&) {
wxGetApp().save_mode(comExpert);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Export pipeline ----
add("export_gcode", _u8L("Export G-code"), _u8L("Slice & Export"), [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_gcode(false);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("export_stl", _u8L("Export STL"), _u8L("Slice & Export"), [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_stl();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("export_3mf", _u8L("Export 3MF"), _u8L("Slice & Export"), [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_core_3mf();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("export_sliced_file", _u8L("Export Sliced File"), _u8L("Slice & Export"), [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_gcode_3mf(false);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("export_all_sliced_file", _u8L("Export All Sliced Files"), _u8L("Slice & Export"),
[](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_gcode_3mf(true);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Calibration ----
add("calib_temperature", _u8L("Temperature Calibration"), _u8L("Calibration"),
[](const std::string&) { return calib_command(CalibKind::Temperature); });
add("calib_max_volumetric", _u8L("Max Volumetric Speed Calibration"), _u8L("Calibration"),
[](const std::string&) { return calib_command(CalibKind::MaxVolumetric); });
add("calib_pressure_advance", _u8L("Pressure Advance Calibration"), _u8L("Calibration"),
[](const std::string&) { return calib_command(CalibKind::PressureAdvance); });
add("calib_flow_ratio", _u8L("Flow Ratio Calibration"), _u8L("Calibration"),
[](const std::string&) { return calib_command(CalibKind::FlowRatio); });
add("calib_retraction", _u8L("Retraction Calibration"), _u8L("Calibration"),
[](const std::string&) { return calib_command(CalibKind::Retraction); });
add("calib_cornering", _u8L("Cornering Calibration"), _u8L("Calibration"),
[](const std::string&) { return calib_command(CalibKind::Cornering); });
add("calib_input_shaping_freq", _u8L("Input Shaping Frequency Calibration"), _u8L("Calibration"),
[](const std::string&) { return calib_command(CalibKind::InputShapingFreq); });
add("calib_input_shaping_damp", _u8L("Input Shaping Damping Calibration"), _u8L("Calibration"),
[](const std::string&) { return calib_command(CalibKind::InputShapingDamp); });
add("calib_vfa", _u8L("VFA Calibration"), _u8L("Calibration"),
[](const std::string&) { return calib_command(CalibKind::VFA); });
// ---- View ----
for (auto [key, dir, title] :
std::initializer_list<std::tuple<const char*, const char*, const char*>>{{"view_top", "top", "View: Top"},
{"view_bottom", "bottom", "View: Bottom"},
{"view_front", "front", "View: Front"},
{"view_rear", "rear", "View: Rear"},
{"view_left", "left", "View: Left"},
{"view_right", "right", "View: Right"},
{"view_iso", "iso", "View: Isometric"}}) {
std::string k = key, d = dir;
add(k, Slic3r::GUI::I18N::translate_utf8(title), _u8L("View"),
[d](const std::string&) { return view_command(wxGetApp().plater(), d); });
}
add("view_default", _u8L("View: Default"), _u8L("View"), [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (plater) {
plater->select_view("plate");
if (GLCanvas3D* canvas = plater->get_current_canvas3D())
canvas->zoom_to_bed();
}
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("view_fit_bed", _u8L("Fit Bed to View"), _u8L("View"), [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
if (GLCanvas3D* canvas = plater->get_current_canvas3D())
canvas->zoom_to_bed();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("view_toggle_perspective", _u8L("Toggle Perspective"), _u8L("View"), [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->get_camera().select_next_type();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("reset_window_layout", _u8L("Reset Window Layout"), _u8L("View"), [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->reset_window_layout();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Object ----
add("obj_delete", _u8L("Delete Selected"), _u8L("Object"), [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->remove_selected(); });
});
add("obj_delete_all", _u8L("Delete All Objects"), _u8L("Object"), [](const std::string&) {
return object_op(
wxGetApp().plater(), [](Plater* p) { return p->can_delete_all(); }, [](Plater* p) { p->delete_all_objects_from_model(); });
});
add("obj_mirror_x", _u8L("Mirror X"), _u8L("Object"), [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::X); });
});
add("obj_mirror_y", _u8L("Mirror Y"), _u8L("Object"), [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::Y); });
});
add("obj_mirror_z", _u8L("Mirror Z"), _u8L("Object"), [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_mirror(); }, [](Plater* p) { p->mirror(Axis::Z); });
});
add("obj_split_objects", _u8L("Split to Objects"), _u8L("Object"), [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_split_to_objects(); }, [](Plater* p) { p->split_object(true); });
});
add("obj_split_parts", _u8L("Split to Parts"), _u8L("Object"), [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_split_to_volumes(); }, [](Plater* p) { p->split_volume(); });
});
add("obj_center", _u8L("Center Selected on Plate"), _u8L("Object"), [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->center_selection(); });
});
add("obj_drop", _u8L("Drop to Bed"), _u8L("Object"), [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return !p->is_selection_empty(); }, [](Plater* p) { p->drop_selection(); });
});
add("obj_fit_volume", _u8L("Scale to Fit Print Volume"), _u8L("Object"), [](const std::string&) {
return object_op(
wxGetApp().plater(), [](Plater* p) { return p->can_scale_to_print_volume(); },
[](Plater* p) { p->scale_selection_to_fit_print_volume(); });
});
add("obj_instances_up", _u8L("Increase Instances"), _u8L("Object"), [](const std::string&) {
return object_op(
wxGetApp().plater(), [](Plater* p) { return p->can_increase_instances(); }, [](Plater* p) { p->increase_instances(); });
});
add("obj_instances_down", _u8L("Decrease Instances"), _u8L("Object"), [](const std::string&) {
return object_op(
wxGetApp().plater(), [](Plater* p) { return p->can_decrease_instances(); }, [](Plater* p) { p->decrease_instances(); });
});
add("obj_arrange", _u8L("Auto-Arrange"), _u8L("Object"), [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->arrange(); });
});
add("obj_orient", _u8L("Auto-Orient"), _u8L("Object"), [](const std::string&) {
return object_op(wxGetApp().plater(), [](Plater* p) { return p->can_arrange(); }, [](Plater* p) { p->orient(); });
});
// ---- Plate ----
add("plate_add", _u8L("Add Plate"), _u8L("Plate"), [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (!is_fff_plater(plater))
return plate_unavailable();
if (!plater->can_add_plate())
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Cannot add another plate (maximum reached).")};
plater->add_plate();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("plate_duplicate", _u8L("Duplicate Plate"), _u8L("Plate"), [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (!is_fff_plater(plater))
return plate_unavailable();
if (!plater->can_add_plate())
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Cannot duplicate a plate (maximum reached).")};
plater->duplicate_plate();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("plate_delete", _u8L("Delete Plate"), _u8L("Plate"), [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (!is_fff_plater(plater))
return plate_unavailable();
if (!plater->can_delete_plate())
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Cannot delete the only plate.")};
plater->delete_plate();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("plate_rename", _u8L("Rename Plate"), _u8L("Plate"), [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (!is_fff_plater(plater))
return plate_unavailable();
PartPlate* curr = plater->get_partplate_list().get_curr_plate();
PlateNameEditDialog dlg((wxWindow*) wxGetApp().mainframe, wxID_ANY, _L("Edit Plate Name"));
dlg.set_plate_name(from_u8(curr->get_plate_name()));
if (dlg.ShowModal() == wxID_YES)
curr->set_plate_name(dlg.get_plate_name().ToUTF8().data());
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("plate_toggle_lock", _u8L("Toggle Plate Lock"), _u8L("Plate"), [](const std::string&) {
Plater* plater = wxGetApp().plater();
if (!is_fff_plater(plater))
return plate_unavailable();
PartPlateList& plates = plater->get_partplate_list();
const int index = plates.get_curr_plate_index();
plater->take_snapshot("lock partplate");
plates.lock_plate(index, !plates.is_locked(index));
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("plate_goto", _u8L("Go to Plate"), _u8L("Plate"), [](const std::string& param) {
Plater* plater = wxGetApp().plater();
if (!is_fff_plater(plater))
return plate_unavailable();
PartPlateList& plates = plater->get_partplate_list();
const int count = plates.get_plate_count();
if (count <= 0)
return AppActionRunResult{AppActionRunResult::Level::Info, _L("No plates available.")};
int index = 0;
try {
index = std::stoi(param);
} catch (const std::exception&) {}
index = std::clamp(index, 0, count - 1);
plater->select_plate(index, false);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Printer / device connection ----
add("connect_printer", _u8L("Connect Printer"), _u8L("Printer"), [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->connect_to_printer();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("disconnect_printer", _u8L("Disconnect Printer"), _u8L("Printer"), [](const std::string&) {
DeviceManager* dev = wxGetApp().getDeviceManager();
if (!dev)
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Printer connection is unavailable.")};
if (MachineObject* machine = dev->get_selected_machine()) {
machine->disconnect();
dev->set_selected_machine("");
return AppActionRunResult{AppActionRunResult::Level::Success};
}
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Printer is not connected.")};
});
add("sync_ams", _u8L("Synchronize Filament List from AMS"), _u8L("Printer"), [](const std::string&) {
Plater* plater = wxGetApp().plater();
DeviceManager* dev = wxGetApp().getDeviceManager();
if (dev && dev->get_selected_machine() && plater) {
plater->sidebar().sync_ams_list();
return AppActionRunResult{AppActionRunResult::Level::Success};
}
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Connect a printer to synchronize the AMS filament list.")};
});
// ---- Presets / cloud ----
add("preset_bundle", _u8L("Open Preset Bundle"), _u8L("Presets"), [](const std::string&) {
wxGetApp().open_presetbundledialog();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("sync_presets", _u8L("Sync Presets"), _u8L("Presets"), [](const std::string&) {
if (!wxGetApp().is_user_login())
return AppActionRunResult{AppActionRunResult::Level::Info, _L("Sign in to sync presets.")};
wxGetApp().restart_sync_user_preset();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Import ----
add("import_file", _u8L("Import 3MF/STL/STEP/SVG/OBJ/AMF"), _u8L("Import"), [](const std::string&) {
if (Plater* plater = wxGetApp().plater()) {
#ifdef __APPLE__
plater->add_model();
#else
plater->add_file();
#endif
}
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("import_zip_archive", _u8L("Import ZIP Archive"), _u8L("Import"), [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->import_zip_archive();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("import_configs", _u8L("Import Configs"), _u8L("Import"), [](const std::string&) {
if (MainFrame* mf = wxGetApp().mainframe)
mf->load_config_file();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
// ---- Export extras ----
add("export_stl_multi", _u8L("Export All Objects as STLs"), _u8L("Export"), [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_stl(false, false, true);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("export_drc_single", _u8L("Export All Objects as DRC (one file)"), _u8L("Export"), [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_stl(false, false, false, FT_DRC);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("export_drc_multi", _u8L("Export All Objects as DRCs"), _u8L("Export"), [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_stl(false, false, true, FT_DRC);
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("export_toolpaths_obj", _u8L("Export Toolpaths as OBJ"), _u8L("Export"), [](const std::string&) {
if (Plater* plater = wxGetApp().plater())
plater->export_toolpaths_to_obj();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
add("export_config", _u8L("Export Preset Bundle"), _u8L("Export"), [](const std::string&) {
if (MainFrame* mf = wxGetApp().mainframe)
mf->export_config();
return AppActionRunResult{AppActionRunResult::Level::Success};
});
return out;
}
} // namespace
const std::vector<NativeCommand>& NativeCommands::catalog()
{
static const std::vector<NativeCommand> commands = build_command_catalog();
return commands;
}
AppActionRunResult NativeCommands::run(const std::string& key, const std::string& param)
{
GUI_App& app = wxGetApp();
if (app.is_closing())
return {};
for (const NativeCommand& c : catalog())
if (c.key == key)
return c.runner(param);
return {AppActionRunResult::Level::Info, _L("Unknown command.")};
}
}} // namespace Slic3r::GUI
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <functional>
#include <string>
#include <vector>
#include "ActionRegistry.hpp" // for AppActionRunResult
namespace Slic3r { namespace GUI {
// A built-in speed-dial command: identity + how to run it. The registry keeps commands as thin
// values (CommandAction) and routes run() here, so this catalog is the single source of truth for
// the behaviour (runner => an owner method) and the presentation (title/group/input).
struct NativeCommand
{
std::string key;
std::string title;
std::string group;
std::string input; // "settings"/"percent"/"tab" or "" for immediate run
std::function<AppActionRunResult(const std::string& param)> runner;
};
namespace NativeCommands {
// The full built-in command catalog, built once (init()). UI thread only.
const std::vector<NativeCommand>& catalog();
// Dispatches `key` to its runner (unknown keys return a quiet Info). UI thread only.
AppActionRunResult run(const std::string& key, const std::string& param = {});
} // namespace NativeCommands
}} // namespace Slic3r::GUI
+19
View File
@@ -7138,6 +7138,7 @@ struct Plater::priv
void on_action_publish(wxCommandEvent &evt);
void on_action_print_plate(SimpleEvent&);
void open_machine_select_dialog(int plate_idx, PrintFromType print_type = PrintFromType::FROM_NORMAL);
void connect_to_printer();
void on_action_print_all(SimpleEvent&);
void on_action_export_gcode(SimpleEvent&);
void on_action_send_gcode(SimpleEvent&);
@@ -12728,6 +12729,22 @@ void Plater::priv::open_machine_select_dialog(int plate_idx, PrintFromType print
m_select_machine_dlg->ShowModal();
}
void Plater::priv::connect_to_printer()
{
// BBL network (vendor BBL, not print-host) and printer-agents mode surface the real
// machine picker (lists discovered printers, connects on selection). Everything else
// is a print-host printer: the Connection button's dialog (host/API-key config).
PresetBundle* pb = wxGetApp().preset_bundle;
const bool bbl_or_agent = (pb && pb->use_bbl_network()) ||
wxGetApp().app_config->get_bool("use_printer_agents");
if (bbl_or_agent)
open_machine_select_dialog(q->get_partplate_list().get_curr_plate_index());
else {
PhysicalPrinterDialog dlg(q->GetParent());
dlg.ShowModal();
}
}
void Plater::priv::on_action_send_to_multi_machine(SimpleEvent&)
{
if (!m_send_multi_dlg)
@@ -17511,6 +17528,8 @@ Sidebar::DockingState Plater::get_sidebar_docking_state() const { return p->get_
void Plater::reset_window_layout() { p->reset_window_layout(); }
void Plater::connect_to_printer() { p->connect_to_printer(); }
//BBS
void Plater::select_curr_plate_all() { p->select_curr_plate_all(); }
void Plater::remove_curr_plate_all() { p->remove_curr_plate_all(); }
+3
View File
@@ -354,6 +354,9 @@ public:
void add_file();
// Returns false when no object was added (e.g. the user cancelled the load dialog).
bool add_model(bool imperial_units = false, std::string fname = "");
// Opens the connection/management dialog appropriate for the current printer's
// technology: the machine picker for BBL/printer-agents, the print-host dialog otherwise.
void connect_to_printer();
void import_zip_archive();
void import_sl1_archive();
void extract_config_from_project();
+10
View File
@@ -61,3 +61,13 @@ TEST_CASE("Go-to-plate actions are keyed by index, not title", "[speeddial][acti
CHECK(AppAction::compose_id("orca_plate_goto", "0", "orca") == "orca_plate_goto:0:orca");
CHECK(AppAction::compose_id("orca_plate_goto", "2", "orca") == "orca_plate_goto:2:orca");
}
// A dynamic "Open recent project" action is keyed by file path (not the display name), so renaming
// a project or reordering the recents list never re-keys it - the same contract as a setting action.
TEST_CASE("Recent-project actions are keyed by path, not title", "[speeddial][actions]")
{
CHECK(AppAction::compose_id("orca_recent_project", "/a/b/project.3mf", "orca") ==
"orca_recent_project:/a/b/project.3mf:orca");
CHECK(AppAction::compose_id("orca_recent_project", "C:/Data/cube.3mf", "orca") ==
"orca_recent_project:C:/Data/cube.3mf:orca");
}