mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-19 23:12:35 +00:00
Search improvements. Code refactoring so its easier to maintain. Experimental features for speed dial: connect/disconnect from printer.
This commit is contained in:
@@ -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 = [
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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(); }
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user