Compare commits

..
Author SHA1 Message Date
Hanif Koh e266c7b234 Draw the Toolpaths Top-Down When the Camera Looks Down on the Print
The segments come in print order, bottom layer first, which seen from above is back to
front: every hidden fragment is shaded before the one that covers it, and on an integrated
GPU that overdraw is most of the frame. Drawing the instances last to first whenever the
camera looks down lets the depth test reject the hidden fragments instead. Side views and
views from below keep the print order, and the shadow-caster pass is unchanged.
2026-09-25 03:29:25 +08:00
16 changed files with 105 additions and 282 deletions
+23 -57
View File
@@ -30,7 +30,7 @@ var USER_MODE = "simple";
var MODE_RANK = { simple: 0, advanced: 1, expert: 2, develop: 3 };
// Search ranking weights: every contiguous match must outrank every fuzzy one regardless of field,
// and title must outrank group/category, which outranks source.
// and title must outrank group, which outranks source.
var SCORE_CONTIGUOUS = 100000;
var SCORE_TITLE = 2000;
var SCORE_GROUP = 1000;
@@ -118,18 +118,6 @@ function sourceNorm(a) {
a._sn = NormText(a.source || "", false);
return a._sn;
}
function pluginCategoryNorm(a) {
if (a._pcn === undefined)
a._pcn = a.kind === "plugin" ? NormText(T("sd_plugins", "Plugins"), false) : "";
return a._pcn;
}
// Plugin type is searchable metadata, so typing "plugin" can find runnable plugin actions even
// when neither their capability nor plugin name contains that word. Keep both category forms.
function pluginTypeNorm(a) {
if (a._pn === undefined)
a._pn = a.kind === "plugin" ? NormText("plugin plugins", false) : "";
return a._pn;
}
// Search-only alias for the descriptive name when the title differs (e.g. "Reverse on even" vs
// "Overhang reversal"). Never rendered, so no highlight ranges.
function fullNorm(a) {
@@ -164,7 +152,7 @@ function fieldMatchScore(norm, needle, wwRe) {
// Split a query into normalized (folded+lowercased) whitespace-separated tokens. Empty for a blank
// query. These drive the multi-token path: every token must match some field, but different tokens
// may match different fields (the title, group, source breadcrumb, or plugin kind).
// may match different fields (the title, the group, or the source breadcrumb).
function queryTokens(query) {
var norm = NormText(String(query || "").trim(), false);
return norm ? norm.split(/\s+/).filter(Boolean) : [];
@@ -182,19 +170,14 @@ function tokenMatch(a, token, wwRe) {
var g = fieldMatchScore(groupNorm(a), token, wwRe);
var s = fieldMatchScore(sourceNorm(a), token, wwRe);
var f = fieldMatchScore(fullNorm(a), token, wwRe);
var p = fieldMatchScore(pluginCategoryNorm(a), token, wwRe);
var typeMatch = fieldMatchScore(pluginTypeNorm(a), token, wwRe);
if (!t && !g && !s && !f && !p && !typeMatch) return null;
if (!t && !g && !s && !f) return null;
var score = Math.max(
t ? (t.contiguous ? SCORE_CONTIGUOUS : 0) + SCORE_TITLE + t.score : -Infinity,
g ? (g.contiguous ? SCORE_CONTIGUOUS : 0) + SCORE_GROUP + g.score : -Infinity,
s ? (s.contiguous ? SCORE_CONTIGUOUS : 0) + s.score : -Infinity,
f ? (f.contiguous ? SCORE_CONTIGUOUS : 0) + f.score : -Infinity,
p ? (p.contiguous ? SCORE_CONTIGUOUS : 0) + SCORE_GROUP + p.score : -Infinity,
typeMatch ? (typeMatch.contiguous ? SCORE_CONTIGUOUS : 0) + SCORE_GROUP + typeMatch.score : -Infinity
f ? (f.contiguous ? SCORE_CONTIGUOUS : 0) + f.score : -Infinity
);
return { score: score, title: t ? t.ranges : null, group: g ? g.ranges : null, source: s ? s.ranges : null,
plugin: p ? p.ranges : null };
return { score: score, title: t ? t.ranges : null, group: g ? g.ranges : null, source: s ? s.ranges : null };
}
// Merge per-field token ranges into sorted, coalesced ranges for highlighting. Overlapping or adjacent
@@ -218,8 +201,8 @@ function mergeRanges(ranges) {
}
// Combine per-field scores into one value, or null when nothing matched.
// Ranking: contiguous > fuzzy, then title > group/category > source/full alias, then start/gaps.
function scoreFields(t, g, s, f, p, typeMatch) {
// Ranking: contiguous > fuzzy, then title > group > source/full alias, then start/gaps.
function scoreFields(t, g, s, f) {
var best = null;
function consider(m, weight) {
if (!m) return;
@@ -230,8 +213,6 @@ function scoreFields(t, g, s, f, p, typeMatch) {
consider(g, SCORE_GROUP);
consider(s, 0);
consider(f, 0);
consider(p, SCORE_GROUP);
consider(typeMatch, SCORE_GROUP);
return best;
}
@@ -245,8 +226,7 @@ function scoreFields(t, g, s, f, p, typeMatch) {
// - tokens: every whitespace-separated word must match SOME field, but different words may match
// different fields. This is what lets "speed acceleration inner" find "Inner wall" whose path is
// "Process : Speed : Acceleration" (title + source breadcrumb together).
// full_label and canonical plugin kind are searchable aliases. The visible category is also searchable
// so its own match can be highlighted in plugin rows.
// full_label is searchable too but never highlighted, since it is not rendered.
// A phrase match always outranks a distributed token match.
function searchActions(actions, query) {
var q = (query || "").trim();
@@ -271,31 +251,26 @@ function searchActions(actions, query) {
var g = fieldMatchScore(groupNorm(a), searchNeedle, wwRe);
var s = fieldMatchScore(sourceNorm(a), searchNeedle, wwRe);
var f = fieldMatchScore(fullNorm(a), searchNeedle, wwRe);
var p = fieldMatchScore(pluginCategoryNorm(a), searchNeedle, wwRe);
var typeMatch = fieldMatchScore(pluginTypeNorm(a), searchNeedle, wwRe);
var phrase = scoreFields(t, g, s, f, p, typeMatch);
var phrase = scoreFields(t, g, s, f);
var score, ranges;
if (phrase !== null) {
score = phrase + SCORE_PHRASE;
ranges = { title: t ? t.ranges : null, group: g ? g.ranges : null, source: s ? s.ranges : null,
plugin: p ? p.ranges : null };
ranges = { title: t ? t.ranges : null, group: g ? g.ranges : null, source: s ? s.ranges : null };
} else {
// Require every token; a token that matches nothing drops the action immediately. Ranges
// from all matching tokens are merged per field so each matched word highlights.
var sum = 0, titleR = null, groupR = null, sourceR = null, pluginR = null, all = true;
for (var tokenIndex = 0; tokenIndex < searchTokens.length; tokenIndex++) {
var m = tokenMatch(a, searchTokens[tokenIndex], searchTokenRes[tokenIndex]);
var sum = 0, titleR = null, groupR = null, sourceR = null, all = true;
for (var k = 0; k < searchTokens.length; k++) {
var m = tokenMatch(a, searchTokens[k], searchTokenRes[k]);
if (!m) { all = false; break; }
sum += m.score;
if (m.title) (titleR || (titleR = [])).push(m.title);
if (m.group) (groupR || (groupR = [])).push(m.group);
if (m.source) (sourceR || (sourceR = [])).push(m.source);
if (m.plugin) (pluginR || (pluginR = [])).push(m.plugin);
}
if (!all) continue;
score = sum;
ranges = { title: mergeRanges(titleR), group: mergeRanges(groupR), source: mergeRanges(sourceR),
plugin: mergeRanges(pluginR) };
ranges = { title: mergeRanges(titleR), group: mergeRanges(groupR), source: mergeRanges(sourceR) };
}
// 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.
@@ -303,7 +278,6 @@ function searchActions(actions, query) {
title: ranges.title,
group: ranges.group,
source: ranges.source,
plugin: ranges.plugin,
useEyebrowGroup: !!(a.group)
};
scored.push({ a: a, s: score });
@@ -416,7 +390,7 @@ function favDigitFromEvent(e) {
function resultCountText(total, shown, query) {
return (query || "").trim() ?
T("sd_result_count", "Showing %s actions", shown) :
T("sd_result_count", "Showing %s of %s actions", shown, total) :
T("sd_result_count_all", "%s actions", total);
}
@@ -487,12 +461,6 @@ function actionCategory(a) {
return cat || T("sd_other", "Other");
}
function actionEyebrow(a, typedQuery, isRecent) {
if (a && a.kind === "plugin" && (String(typedQuery || "").trim() || isRecent))
return T("sd_plugins", "Plugins");
return (a && (a.group || a.source)) || "";
}
// Stable-bucket actions by category, then order the groups alphabetically. Within a group the incoming
// (frecency) order is kept. Pure so the node-vm test can exercise grouping.
function groupActions(list) {
@@ -743,7 +711,7 @@ window.HandleStudio = function (payload) {
builtKey = "";
if (qEl) {
qEl.value = "";
qEl.placeholder = T("sd_search", "Search actions");
qEl.placeholder = T("sd_search_n", "Search %s actions", ACTIONS.length);
syncClearButton();
}
render({ resize: true, resetScroll: true });
@@ -963,14 +931,12 @@ function beginRow(item, i, mono, ariaLabel) {
function renderActionRow(a, i) {
var on = FAVS.indexOf(a.id) !== -1;
var shell = beginRow(a, i, false, actionLabel(a, ACTIONS));
var typedQuery = String(query || "").trim();
var isRecent = !typedQuery && i < RECENTS.length && RECENTS[i].id === a.id;
var mi = typedQuery ? matchIndex[a.id] : null;
// Plugin search/recent rows show their category; other rows show their group or source breadcrumb.
// Use match ranges from the visible field so category and breadcrumb highlights stay aligned.
var showPluginCategory = a.kind === "plugin" && (typedQuery || isRecent);
var eyebrow = actionEyebrow(a, query, isRecent);
var eyebrowMatch = mi ? (showPluginCategory ? mi.plugin : (mi.useEyebrowGroup ? mi.group : mi.source)) : null;
var mi = matchIndex[a.id];
// 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;
shell.left.insertBefore(markedText("row-eyebrow", eyebrow, eyebrowMatch), shell.line);
shell.line.appendChild(markedText("row-name", a.title, mi ? mi.title : null));
var badge = modeBadge(a, USER_MODE);
@@ -1410,7 +1376,7 @@ function exitPhase() {
// It survives a second-phase exit (which never goes through exitPhase from the commands view),
// so without a reset the cached empty-query key would skip the rebuild and leave stale content.
builtKey = "";
qEl.placeholder = T("sd_search", "Search actions");
qEl.placeholder = T("sd_search_n", "Search %s actions", ACTIONS.length);
render({ resize: true, resetScroll: true });
qEl.focus();
}
@@ -82,20 +82,6 @@ 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");
const pluginPool = [
{ id: "plugin-action", title: "Optimize G-code", source: "Gcode Optimizer", group: "", kind: "plugin" },
{ id: "command-action", title: "Open Preferences", source: "OrcaSlicer", group: "Commands", kind: "command" }
];
assert.deepEqual(ctx.searchActions(pluginPool, "plugin").map(function (a) { return a.id; }), ["plugin-action"],
"the plugin kind makes runnable plugin actions searchable by plugin");
assert.deepEqual(ctx.searchActions(pluginPool, "plugins").map(function (a) { return a.id; }), ["plugin-action"],
"the plural Plugins category also finds plugin actions");
ctx.searchActions(pluginPool, "plugins");
assert.deepEqual(ctx.matchIndex["plugin-action"].plugin, [[0, 7]],
"a category match highlights the visible Plugins label");
assert.deepEqual(ctx.searchActions(pluginPool, "plugin optimize").map(function (a) { return a.id; }), ["plugin-action"],
"plugin kind can match one token while the action title matches another");
// 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.
@@ -202,12 +188,6 @@ assert.equal(ctx.actionCategory({ id: "s", group: "", source: "Filament : Coolin
"a Filament setting groups under Filament");
assert.equal(ctx.actionCategory({ id: "plugin_script_action:Foo:bar.py", group: "", source: "Gcode Optimizer", kind: "plugin" }), "Plugins",
"every plugin shares one Plugins header");
assert.equal(ctx.actionEyebrow({ group: "", source: "Gcode Optimizer", kind: "plugin" }, "plugin"), "Plugins",
"typed results show only the Plugins category");
assert.equal(ctx.actionEyebrow({ group: "", source: "Gcode Optimizer", kind: "plugin" }, "", true), "Plugins",
"recent plugin actions show only the Plugins category");
assert.equal(ctx.actionEyebrow({ group: "", source: "Gcode Optimizer", kind: "plugin" }, ""), "Gcode Optimizer",
"the unfiltered plugin section keeps the source name on non-recent rows");
assert.equal(ctx.actionCategory({ id: "x", group: "", source: "", kind: "command" }), "Other",
"a category-less action falls back to Other");
@@ -456,9 +436,4 @@ assert.equal(ctx.stateFromPayload({}).tooltipExpanded, true, "expansion defaults
assert.equal(ctx.stateFromPayload({ tooltip_expanded: false }).tooltipExpanded, false, "a collapsed payload is honored");
assert.equal(ctx.stateFromPayload({ tooltip_expanded: true }).tooltipExpanded, true, "an expanded payload is honored");
// resultCountText: a search counts the shown matches only ("Showing N actions"); the total is used
// solely for the empty-query count.
assert.equal(ctx.resultCountText(100, 3, "lay"), "Showing 3 actions", "a search reports the shown match count only");
assert.equal(ctx.resultCountText(100, 100, ""), "100 actions", "an empty query reports the total");
console.log("ok");
+5 -31
View File
@@ -5527,28 +5527,12 @@ int CLI::run(int argc, char **argv)
//add the virtual object into unselect list if has
partplate_list.preprocess_exclude_areas(unselected, enable_wrapping_detect);
// Filament ids given on the command line size the tower for STL input. A project
// records its filament use per plate, so count there and keep its tower positions.
const int plate_count = partplate_list.get_plate_count();
const bool from_project = used_filament_set.empty();
std::vector<int> plate_filament_counts(plate_count, static_cast<int>(used_filament_set.size()));
if (from_project)
for (int plate_index = 0; plate_index < plate_count; ++plate_index)
plate_filament_counts[plate_index] = static_cast<int>(partplate_list.get_plate(plate_index)->get_extruders_under_cli(true, m_print_config).size());
// A project only gets a tower the slicer will print: the prime tower enabled, and not
// a by-object print unless a smooth timelapse needs it, as the per-plate arrange decides.
const bool project_tower_allowed = m_print_config.option<ConfigOptionBool>("enable_prime_tower", true)->value &&
(is_smooth_timelapse || !arrange_cfg.is_seq_print);
const auto plate_needs_wipe_tower = [from_project, project_tower_allowed, is_smooth_timelapse](int filament_count) {
if (!from_project)
return filament_count > 0;
return project_tower_allowed && (filament_count > 1 || (filament_count > 0 && is_smooth_timelapse));
};
const int max_filament_count = plate_count > 0 ? *std::max_element(plate_filament_counts.begin(), plate_filament_counts.end()) : 0;
if (plate_needs_wipe_tower(max_filament_count))
if (used_filament_set.size() > 0)
{
//prepare the wipe tower
int plate_count = partplate_list.get_plate_count();
int extruder_size = used_filament_set.size();
auto printer_structure_opt = m_print_config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure");
// This margin only pre-adjusts the default away from the near edges;
// estimate_wipe_tower_polygon below computes the real clamped position.
@@ -5584,11 +5568,7 @@ int CLI::run(int argc, char **argv)
for (int bedid = 0; bedid < MAX_PLATE_COUNT; bedid++) {
int plate_index_valid = std::min(bedid, plate_count - 1);
// Overflow beds may receive objects from any plate, so size them for the busiest one.
const int extruder_size = bedid < plate_count ? plate_filament_counts[bedid] : max_filament_count;
if (!plate_needs_wipe_tower(extruder_size))
continue;
if (bedid < plate_count && !from_project) {
if (bedid < plate_count) {
wipe_x_option->set_at(&wt_x_opt, plate_index_valid, 0);
wipe_y_option->set_at(&wt_y_opt, plate_index_valid, 0);
}
@@ -7044,12 +7024,6 @@ int CLI::run(int argc, char **argv)
}
}
sliced_info.sliced_plates.push_back(sliced_plate_info);
} catch (const Slic3r::SlicingErrors &exs) {
const std::string message = print_fff ? print_fff->slicing_errors_message(exs) : std::string(exs.what());
BOOST_LOG_TRIVIAL(error) << "found slicing or export error for partplate " << index+1 << ": " << message;
boost::nowide::cerr << message << std::endl;
record_exit_reson(outfile_dir, CLI_SLICING_ERROR, index+1, message, sliced_info);
flush_and_exit(CLI_SLICING_ERROR);
} catch (const std::exception &ex) {
BOOST_LOG_TRIVIAL(error) << "found slicing or export error for partplate "<<index+1 << std::endl;
boost::nowide::cerr << ex.what() << std::endl;
+5 -10
View File
@@ -2479,8 +2479,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
{
LifecycleEventContext ctx;
ctx.id = std::to_string(print->model().id().id);
ctx.name = print->get_model_name();
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = path;
ctx.cancellation_check = [print]() { return print->canceled(); };
@@ -2532,8 +2531,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
}
{
LifecycleEventContext ctx;
ctx.id = std::to_string(print->model().id().id);
ctx.name = print->get_model_name();
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Error;
ctx.msg = std::string(path) + "\n" + err_msg;
ctx.cancellation_check = [print]() { return print->canceled(); };
@@ -2557,8 +2555,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
boost::nowide::remove(path_tmp.c_str());
{
LifecycleEventContext ctx;
ctx.id = std::to_string(print->model().id().id);
ctx.name = print->get_model_name();
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Error;
ctx.msg = std::string(path) + "\n" + ex.what();
ctx.cancellation_check = [print]() { return print->canceled(); };
@@ -2671,8 +2668,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
if (ret) {
{
LifecycleEventContext ctx;
ctx.id = std::to_string(print->model().id().id);
ctx.name = print->get_model_name();
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Error;
ctx.msg = std::string(path) + "\nFailed to rename the output G-code file: " + ret.message();
ctx.cancellation_check = [print]() { return print->canceled(); };
@@ -2691,8 +2687,7 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
{
LifecycleEventContext ctx;
ctx.id = std::to_string(print->model().id().id);
ctx.name = print->get_model_name();
ctx.name = std::to_string(print->model().id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = path;
ctx.cancellation_check = [print]() { return print->canceled(); };
+5 -29
View File
@@ -1705,25 +1705,6 @@ StringObjectException Print::check_multi_filament_valid(const Print& print)
// Precondition: Print::validate() requires the Print::apply() to be called its invocation.
//BBS: refine seq-print validation logic
// The exception's own message is just "Errors"; the detail is in the per-object errors,
// whose object id is the PrintObject's.
std::string Print::slicing_errors_message(const SlicingErrors &errors) const
{
std::string message;
for (const SlicingError &error : errors.errors_) {
std::string object_name;
for (const PrintObject *object : m_objects)
if (object->id().id == error.objectId()) {
object_name = object->model_object()->name;
break;
}
if (!message.empty())
message += "\n";
message += object_name.empty() ? std::string(error.what()) : object_name + ": " + error.what();
}
return message;
}
StringObjectException Print::validate(std::vector<StringObjectException> *warnings, Polygons* collison_polygons, std::vector<std::pair<Polygon, float>>* height_polygons) const
{
auto add_warning = [warnings](StringObjectException w) {
@@ -2716,8 +2697,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
{
LifecycleEventContext ctx;
ctx.id = std::to_string(m_model.id().id);
ctx.name = get_model_name();
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.cancellation_check = [this]() { return canceled(); };
fire_lifecycle_event(LifecycleEvent::SliceStarted, ctx);
@@ -3344,8 +3324,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
{
LifecycleEventContext ctx;
ctx.id = std::to_string(m_model.id().id);
ctx.name = get_model_name();
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.cancellation_check = [this]() { return canceled(); };
fire_lifecycle_event(LifecycleEvent::SliceGeometryFinished, ctx);
@@ -4951,8 +4930,7 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
{
{
LifecycleEventContext ctx;
ctx.id = std::to_string(m_model.id().id);
ctx.name = get_model_name();
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = file;
ctx.cancellation_check = [this]() { return canceled(); };
@@ -4982,8 +4960,7 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": found errors when process gcode file %1%") %file.c_str();
{
LifecycleEventContext ctx;
ctx.id = std::to_string(m_model.id().id);
ctx.name = get_model_name();
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Error;
ctx.msg = file + "\n" + ex.what();
ctx.cancellation_check = [this]() { return canceled(); };
@@ -4997,8 +4974,7 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
{
LifecycleEventContext ctx;
ctx.id = std::to_string(m_model.id().id);
ctx.name = get_model_name();
ctx.name = std::to_string(m_model.id().id);
ctx.code = LifecycleEvtCode::Ok;
ctx.msg = file;
ctx.cancellation_check = [this]() { return canceled(); };
-4
View File
@@ -30,8 +30,6 @@
namespace Slic3r {
class SlicingErrors;
class GCode;
class Layer;
class ModelObject;
@@ -969,8 +967,6 @@ public:
// Returns an empty string if valid, otherwise returns an error message.
StringObjectException validate(std::vector<StringObjectException> *warnings = nullptr, Polygons* collison_polygons = nullptr, std::vector<std::pair<Polygon, float>>* height_polygons = nullptr) const override;
// The per-object messages of a SlicingErrors, each prefixed with its object's name.
std::string slicing_errors_message(const SlicingErrors &errors) const;
double skirt_first_layer_height() const;
Flow brim_flow() const;
Flow skirt_flow() const;
+6 -1
View File
@@ -34,6 +34,10 @@ static const char* Segments_Vertex_Shader =
// ORCA: 0 during the shadow caster pass - the bias below shifts eye_position but not
// world_position, so the caster would write a depth the receiver never looks up.
"uniform float bias_scale;\n"
// draw the instances last to first, top layers before the ones they hide, so that early depth
// rejection discards most of the hidden fragments; set when the camera looks down on the print
"uniform int reverse_order;\n"
"uniform int instance_count;\n"
"in int vertex_id;\n"
"out vec3 color;\n"
"// ORCA: realistic view - the light the shadow map is able to block, kept apart from the\n"
@@ -59,7 +63,8 @@ static const char* Segments_Vertex_Shader =
" return top_diffuse + front_diffuse + top_specular;\n"
"}\n"
"void main() {\n"
" int id_a = int(texelFetch(segment_index_tex, gl_InstanceID).r);\n"
" int instance = (reverse_order != 0) ? instance_count - 1 - gl_InstanceID : gl_InstanceID;\n"
" int id_a = int(texelFetch(segment_index_tex, instance).r);\n"
" int id_b = id_a + 1;\n"
" vec3 pos_a = texelFetch(position_tex, id_a).xyz;\n"
" vec3 pos_b = texelFetch(position_tex, id_b).xyz;\n"
+11
View File
@@ -763,6 +763,8 @@ void ViewerImpl::init(const std::string& opengl_context_version)
m_uni_segments_height_width_angle_tex_id = glGetUniformLocation(m_segments_shader_id, "height_width_angle_tex");
m_uni_segments_colors_tex_id = glGetUniformLocation(m_segments_shader_id, "color_tex");
m_uni_segments_segment_index_tex_id = glGetUniformLocation(m_segments_shader_id, "segment_index_tex");
m_uni_segments_reverse_order_id = glGetUniformLocation(m_segments_shader_id, "reverse_order");
m_uni_segments_instance_count_id = glGetUniformLocation(m_segments_shader_id, "instance_count");
// ORCA: realistic view
m_uni_segments_shadow_map_id = glGetUniformLocation(m_segments_shader_id, "shadow_map");
m_uni_segments_shadow_light_vp_id = glGetUniformLocation(m_segments_shader_id, "shadow_light_vp");
@@ -2090,6 +2092,15 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec
glsafe(glUniformMatrix4fv(m_uni_segments_view_matrix_id, 1, GL_FALSE, view_matrix.data()));
glsafe(glUniformMatrix4fv(m_uni_segments_projection_matrix_id, 1, GL_FALSE, projection_matrix.data()));
glsafe(glUniform3fv(m_uni_segments_camera_position_id, 1, camera_position.data()));
// The segments come in print order, bottom layer first. Seen from above, that is back to front,
// and every hidden fragment is shaded before the one that covers it. Drawing them last to first
// lets the depth test reject the hidden ones instead. The camera looks down when the world's
// up axis points towards it, which is the view matrix's (2, 2) entry being positive.
const bool top_down = !m_rendering_shadow_casters && view_matrix[10] > 0.0f;
glsafe(glUniform1i(m_uni_segments_reverse_order_id, top_down ? 1 : 0));
#ifndef ENABLE_OPENGL_ES
glsafe(glUniform1i(m_uni_segments_instance_count_id, static_cast<int>(m_enabled_segments_count)));
#endif // ENABLE_OPENGL_ES
// ORCA: realistic view. The depth pass writes the map it would otherwise read, so it shades
// with the lookup off.
glsafe(glUniform1i(m_uni_segments_shadow_map_id, m_shadow_map_texture_unit));
+2
View File
@@ -362,6 +362,8 @@ private:
int m_uni_segments_height_width_angle_tex_id{ -1 };
int m_uni_segments_colors_tex_id{ -1 };
int m_uni_segments_segment_index_tex_id{ -1 };
int m_uni_segments_reverse_order_id{ -1 };
int m_uni_segments_instance_count_id{ -1 };
int m_uni_segments_shadow_map_id{ -1 };
int m_uni_segments_shadow_light_vp_id{ -1 };
int m_uni_segments_shadow_intensity_id{ -1 };
+4 -4
View File
@@ -12801,6 +12801,9 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
this->background_process.stop();
notification_manager->set_slicing_progress_export_possible();
// Reset the "export G-code path" name, so that the automatic background processing will be enabled again.
const std::string lifecycle_job_name = this->background_process.fff_print() ?
this->background_process.fff_print()->output_filename() : std::string();
this->background_process.reset_export();
// This bool stops showing export finished notification even when process_completed_with_error is false
bool has_error = false;
@@ -12847,10 +12850,7 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
{
Slic3r::LifecycleEventContext ctx;
if (const Print* print = this->background_process.fff_print()) {
ctx.id = std::to_string(print->model().id().id);
ctx.name = print->get_model_name();
}
ctx.name = lifecycle_job_name;
ctx.code = evt.cancelled() ? Slic3r::LifecycleEvtCode::Warn : (has_error ? Slic3r::LifecycleEvtCode::Error : Slic3r::LifecycleEvtCode::Ok);
ctx.msg = evt.cancelled() ? "cancelled" : (has_error ? lifecycle_error_msg : std::string());
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::SlicingJobComplete, ctx);
+44 -68
View File
@@ -9,8 +9,6 @@
#include "Plater.hpp"
#include "Widgets/WebViewHostDialog.hpp"
#include "slic3r/Utils/MacDarkMode.hpp"
#include <algorithm>
#include <wx/dcmemory.h>
@@ -84,38 +82,39 @@ nlohmann::json speed_dial_ui_strings()
{"shortcut_alt", alt},
{"shortcut_ctrl", ctrl},
{"sd_search", _u8L("Search actions")},
{"sd_clear", _u8L("Clear")},
{"sd_recent", _u8L("Recent")},
{"sd_plugins", _u8L("Plugins")},
{"sd_other", _u8L("Other")},
{"sd_no_match_total", _u8L("No actions match (Total: %s)")},
{"sd_no_actions", _u8L("No actions yet")},
{"sd_no_tabs_match", _u8L("No tabs match")},
{"sd_no_tabs", _u8L("No tabs")},
{"sd_result_count", _u8L("Showing %s actions")},
{"sd_search", _u8L("Search actions")},
{"sd_clear", _u8L("Clear")},
{"sd_search_n", _u8L("Search %s actions")},
{"sd_recent", _u8L("Recent")},
{"sd_plugins", _u8L("Plugins")},
{"sd_other", _u8L("Other")},
{"sd_no_match_total", _u8L("No actions match (Total: %s)")},
{"sd_no_actions", _u8L("No actions yet")},
{"sd_no_tabs_match", _u8L("No tabs match")},
{"sd_no_tabs", _u8L("No tabs")},
{"sd_result_count", _u8L("Showing %s of %s actions")},
{"sd_result_count_all", _u8L("%s actions")},
{"sd_tab_count", _u8L("%s tabs")},
{"sd_tab_count", _u8L("%s tabs")},
{"sd_tab_match_count", _u8L("%s matches")},
{"sd_favs_full", _u8L("Favourites are full (%s max)")},
{"sd_go_to_pct", _u8L("Go to %s%% of the layer range")},
{"sd_enter_pct", _u8L("Enter a layer percentage (0-100)")},
{"sd_go_layer_ph", _u8L("Go to layer %% (0-100)")},
{"sd_go_tab_ph", _u8L("Go to tab")},
{"sd_fav_slot", _u8L("Favourite %s (%s)")},
{"sd_pin_fav", _u8L("Pin to favourites (%s)")},
{"sd_unpin_fav", _u8L("Unpin from favourites (%s)")},
{"sd_remove_fav", _u8L("Remove from favourites")},
{"sd_move_left", _u8L("Move left")},
{"sd_move_right", _u8L("Move right")},
{"sd_unpin", _u8L("Unpin")},
{"sd_mode_advanced", _u8L("Advanced")},
{"sd_mode_expert", _u8L("Expert")},
{"sd_mode_develop", _u8L("Developer")},
{"sd_wiki_f1", _u8L("Wiki (F1)")},
{"sd_no_wiki", _u8L("No wiki page for this action")},
{"sd_show_details", _u8L("Show details")},
{"sd_hide_details", _u8L("Hide details")},
{"sd_favs_full", _u8L("Favourites are full (%s max)")},
{"sd_go_to_pct", _u8L("Go to %s%% of the layer range")},
{"sd_enter_pct", _u8L("Enter a layer percentage (0-100)")},
{"sd_go_layer_ph", _u8L("Go to layer %% (0-100)")},
{"sd_go_tab_ph", _u8L("Go to tab")},
{"sd_fav_slot", _u8L("Favourite %s (%s)")},
{"sd_pin_fav", _u8L("Pin to favourites (%s)")},
{"sd_unpin_fav", _u8L("Unpin from favourites (%s)")},
{"sd_remove_fav", _u8L("Remove from favourites")},
{"sd_move_left", _u8L("Move left")},
{"sd_move_right", _u8L("Move right")},
{"sd_unpin", _u8L("Unpin")},
{"sd_mode_advanced", _u8L("Advanced")},
{"sd_mode_expert", _u8L("Expert")},
{"sd_mode_develop", _u8L("Developer")},
{"sd_wiki_f1", _u8L("Wiki (F1)")},
{"sd_no_wiki", _u8L("No wiki page for this action")},
{"sd_show_details", _u8L("Show details")},
{"sd_hide_details", _u8L("Hide details")},
};
}
@@ -179,7 +178,6 @@ void SpeedDialWebDialog::request_show()
if (IsShown()) {
Raise();
focus_webview(browser(), m_page_ready);
repaint_webview();
return;
}
@@ -191,7 +189,6 @@ void SpeedDialWebDialog::request_show()
// Grab focus now and again on wxEVT_ACTIVATE; grabbing directly on the WebKit widget is
// what makes typing reach the search field immediately on open.
focus_webview(browser(), m_page_ready);
repaint_webview();
}
void SpeedDialWebDialog::on_script_message(const nlohmann::json& payload)
@@ -219,7 +216,7 @@ void SpeedDialWebDialog::handle_web_command(const nlohmann::json& payload)
// set_favourite() refuses once the bar hits kFavLimit; tell the page so it can undo the
// pin and show a "favourites are full" hint instead of silently losing the favourite.
const std::string fav_id = payload.value("id", "");
const bool ok = wxGetApp().action_registry().set_favourite(fav_id, payload.value("fav", false));
const bool ok = wxGetApp().action_registry().set_favourite(fav_id, payload.value("fav", false));
if (!ok)
call_web_handler({{"command", "favourite_full"}, {"limit", (int) ActionRegistry::kFavLimit}, {"id", fav_id}});
} else if (command == "reorder_favourites") {
@@ -268,36 +265,14 @@ void SpeedDialWebDialog::resize_to_content(int height)
Layout();
#ifdef __WXOSX__
// WKWebView can lag the dialog's new client size; force the viewport to match so the page is
// never painted (and clipped by the rounded layer) below the footer. Unconditional: on a
// re-open the size is often unchanged, and skipping the sync leaves the fresh render unpainted.
if (wxWebView* wv = browser())
wv->SetSize(GetClientSize());
// never painted (and clipped by the rounded layer) below the footer.
if (wxWebView* wv = browser()) {
const wxSize client = GetClientSize();
if (wv->GetSize() != client)
wv->SetSize(client);
}
#endif
apply_rounded_shape();
// A re-open re-renders at (usually) the same size, so nothing above may generate damage.
// Repaint explicitly so the newly rendered list is shown without needing user input.
repaint_webview();
}
void SpeedDialWebDialog::repaint_webview()
{
wxWebView* wv = browser();
if (!wv)
return;
// Portable invalidate; the platform blocks below reach the widget/layer that actually paints.
wv->Refresh();
#ifdef __WXOSX__
if (void* nb = wv->GetNativeBackend())
WKWebView_force_display(nb);
wv->Update();
#elif defined(__linux__)
// WebKitGTK's WebKitWebView owns its own GdkWindow, so invalidating the wxWebView wrapper
// (the GtkScrolledWindow) does not redraw it.
if (void* nb = wv->GetNativeBackend())
gtk_widget_queue_draw((GtkWidget*) nb);
#else
wv->Update();
#endif
}
// Rounded corners: the webview paints an opaque rectangle, so round the whole top-level window.
@@ -356,8 +331,8 @@ void SpeedDialWebDialog::run_action(const std::string& id, const std::string& ti
return;
// Only plugin actions get the "Run plugin?" confirm. Built-in commands act immediately.
const bool ask = a->kind == AppActionKind::Plugin && reg.should_ask(id);
const std::string atitle = a->title();
const bool ask = a->kind == AppActionKind::Plugin && reg.should_ask(id);
const std::string atitle = a->title();
const ConfigOptionMode required = a->required_mode;
// Settings the current mode hides require a switch first. Ask while the dial is still up; a
@@ -366,15 +341,16 @@ void SpeedDialWebDialog::run_action(const std::string& id, const std::string& ti
const wxString setting = title.empty() ? from_u8(atitle) : from_u8(title);
if (required == comDevelop) {
RichMessageDialog dlg(wxGetApp().mainframe,
wxString::Format(_L("\"%s\" is a Developer setting. Enable Developer mode to edit it?"), setting),
wxString::Format(_L("\"%s\" is a Developer setting. Enable Developer mode to edit it?"),
setting),
_L("Developer setting"), wxOK | wxCANCEL);
if (dlg.ShowModal() != wxID_OK)
return;
wxGetApp().enable_developer_mode();
} else {
RichMessageDialog dlg(wxGetApp().mainframe,
wxString::Format(_L("\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?"), setting,
mode_label(required), mode_label(wxGetApp().get_mode()), mode_label(required)),
wxString::Format(_L("\"%s\" is a %s setting. Switch from %s mode to %s mode to edit it?"),
setting, mode_label(required), mode_label(wxGetApp().get_mode()), mode_label(required)),
_L("Switch settings mode"), wxOK | wxCANCEL);
if (dlg.ShowModal() != wxID_OK)
return;
-4
View File
@@ -28,10 +28,6 @@ private:
void send_actions();
void search_tabs();
void apply_rounded_shape();
// Forces the webview to repaint after it is mapped / re-rendered. The popup is transparent and
// chrome-less, so a missed frame leaves it blank until input; platform-specific because the
// widget that actually paints is not always the wxWebView wrapper.
void repaint_webview();
void on_dpi_changed(const wxRect& suggested_rect) override;
bool m_page_ready{false};
-8
View File
@@ -7184,14 +7184,6 @@ void Tab::activate_selected_page(std::function<void()> throw_if_canceled)
if (!m_active_page)
return;
#ifdef __WXGTK__
// Builds the page off screen, since GTK crashes when it desensitizes a multiline text view
// that was built on screen and hidden before its first size allocation.
const bool hide_view = m_active_page->build_pending() && m_page_view->IsShown();
if (hide_view)
m_page_view->Hide();
ScopeGuard show_view([this, hide_view] { if (hide_view) m_page_view->Show(); });
#endif
m_active_page->activate(m_mode, throw_if_canceled);
update_changed_ui();
update_description_lines();
-1
View File
@@ -12,7 +12,6 @@ extern double mac_max_scaling_factor();
extern void set_miniaturizable(void * window);
void WKWebView_evaluateJavaScript(void * web, wxString const & script, void (*callback)(wxString const &));
void WKWebView_setTransparentBackground(void * web);
void WKWebView_force_display(void * web);
void set_tag_when_enter_full_screen(bool isfullscreen);
void set_title_colour_after_set_title(void * window);
void initGestures(void * view, wxEvtHandler * handler);
-14
View File
@@ -102,20 +102,6 @@ void WKWebView_setTransparentBackground(void * web)
[webView registerForDraggedTypes: @[NSFilenamesPboardType]];
}
// Force a WKWebView to re-lay-out and repaint. Needed for chrome-less popups: the window is
// transparent, so a WKWebView whose layer has no pending frame leaves the whole window invisible
// until the user generates input (scroll/arrow). setNeedsDisplay alone does not always reach the
// web-content layer, so flag layout and both the view and its layer.
void WKWebView_force_display(void * web)
{
WKWebView * webView = (WKWebView*)web;
if (!webView)
return;
[webView setNeedsLayout:YES];
[webView setNeedsDisplay:YES];
[[webView layer] setNeedsDisplay];
}
void openFolderForFile(wxString const & file)
{
NSArray *fileURLs = [NSArray arrayWithObjects:wxCFStringRef(file).AsNSString(), /* ... */ nil];
-26
View File
@@ -505,29 +505,3 @@ TEST_CASE("Sequential printing publishes the nozzle group result", "[Print][Mult
CHECK(gcode.find("; SEQ-ND-OK") != std::string::npos);
}
}
TEST_CASE("Slicing errors are reported per object with the object's name", "[Print]")
{
Print print;
Model model;
init_print({Slic3r::Test::cube(20.)}, print, model);
// Lift the cube off the bed: its first layer is empty, which G-code export reports per object.
ModelObject *object = model.objects.front();
object->name = "floating cube";
object->instances.front()->set_offset(object->instances.front()->get_offset() + Vec3d(0., 0., 2.));
print.apply(model, DynamicPrintConfig::full_print_config());
print.set_status_silent();
ScopedTemporaryFile temp(".gcode");
std::string message;
try {
print.process();
print.export_gcode(temp.string(), nullptr, nullptr);
FAIL("slicing did not report the empty first layer");
} catch (const SlicingErrors &errors) {
REQUIRE(errors.errors_.size() == 1);
message = print.slicing_errors_message(errors);
}
CHECK(message.rfind("floating cube: ", 0) == 0);
CHECK(message.find("empty first layer") != std::string::npos);
}