Compare commits

..

3 Commits

Author SHA1 Message Date
Hanif Koh
af314cc5c3 Cover the substitutions of a preset held back for its parent
A preset whose parent is not in the collection yet is read once per pass it
waits, and each read appends to the caller's substitutions list. The load drops
what it read before deferring, so only the pass that keeps the preset reports
its substitutions; without that, one preset is listed once per pass and the
count depends on how deep it sits in the hierarchy.
2026-09-10 13:18:08 +08:00
Hanif Koh
f7812bf0f1 Name the missing parent when a dropped preset is resolved from the CLI
When load_presets() drops a preset because its parent does not exist, the
only trace is a log line. The CLI then resolves --load-settings /
--load-filaments against the loaded bundle and reports "Preset was not found
in the loaded bundle", which points at the resolver rather than at the real
cause.

Record the presets dropped for a missing parent in the collection, keyed by
file, and have resolve_preset_config() report that parent by name when the
source file is one of them.
2026-09-09 18:28:53 +08:00
Hanif Koh
fb44569b8e Retry unresolved parents when loading presets from a directory
A preset that inherits another preset from the same directory was dropped at
load time. load_presets() resolves "inherits" with find_preset2(), which
searches m_presets, but the presets it loads are staged in a local vector and
merged into m_presets only after the loop - so no preset could be the parent of
another preset loaded by the same pass. Only a parent loaded earlier (a system
preset, or one in the "base" subdirectory) resolved.

The drop was silent in the GUI. Since the CLI resolves --load-settings /
--load-filaments by matching the source file against the presets in the loaded
bundle, it turns into a hard failure there: a user preset inheriting another
user preset exits -5 with "Preset was not found in the loaded bundle".

Load in passes instead: a preset whose parent is not in the collection yet is
deferred and retried after the pass merges what it loaded, so each pass resolves
one more level of the hierarchy. A pass that resolves nothing reports the
remaining parents as missing, which also terminates an inheritance cycle. The
work list is sorted so the outcome does not depend on directory iteration order.
2026-09-09 18:28:53 +08:00
22 changed files with 350 additions and 1811 deletions

View File

@@ -202,39 +202,6 @@ void AppConfig::set_defaults()
if (get("seq_top_layer_only").empty())
set("seq_top_layer_only", "1");
// ORCA: simplify the preview while the user is dragging: what is left out and one layer in how many is kept
if (get("preview_reduced_detail_while_dragging").empty())
set_bool("preview_reduced_detail_while_dragging", false);
{
const std::string mode = get("preview_reduced_detail_mode");
if (mode != "layers" && mode != "no_infill" && mode != "shell" && mode != "solid")
set("preview_reduced_detail_mode", "no_infill");
}
// ORCA: keep the drawn preview scene while nothing in it changes
if (get("preview_cache_static_scene").empty())
set_bool("preview_cache_static_scene", false);
{
const std::string mode = get("preview_rest_detail_mode");
if (mode != "full" && mode != "no_infill" && mode != "shell")
set("preview_rest_detail_mode", "full");
}
if (get("preview_reduced_detail_layer_stride").empty())
set("preview_reduced_detail_layer_stride", "4");
else {
int stride = 4;
try {
stride = std::stoi(get("preview_reduced_detail_layer_stride"));
}
catch (...) {
stride = 4;
}
set("preview_reduced_detail_layer_stride", std::to_string(std::max(1, std::min(stride, 20))));
}
// ORCA: darken the layers the preview layer slider is not scrubbed to
if (get("preview_dim_previous_layers").empty())
set_bool("preview_dim_previous_layers", false);

View File

@@ -1624,6 +1624,7 @@ void PresetCollection::reset(bool delete_files)
unlock();
m_map_alias_to_profile_name.clear();
m_map_system_profile_renamed.clear();
m_unresolved_parents.clear();
}
void PresetCollection::add_default_preset(const std::vector<std::string> &keys, const Slic3r::StaticPrintConfig &defaults, const std::string &preset_name)
@@ -1676,178 +1677,216 @@ void PresetCollection::load_presets(
}
std::string errors_cummulative;
// Store the loaded presets into a new vector, otherwise the binary search for already existing presets would be broken.
// (see the "Preset already present, not loading" message).
std::deque<Preset> presets_loaded;
//BBS: get the extruder related info for this preset collection
std::string extruder_id_name, extruder_variant_name;
std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2);
//BBS: change to json format
std::vector<boost::filesystem::path> pending;
for (auto &dir_entry : boost::filesystem::directory_iterator(dir))
{
std::string file_name = dir_entry.path().filename().string();
//if (Slic3r::is_ini_file(dir_entry)) {
if (Slic3r::is_json_file(file_name)) {
// Remove the .ini suffix.
std::string name = file_name.erase(file_name.size() - 5);
std::string canonical_name = this->canonical_preset_name(name, resolved_origin);
if (this->find_preset(canonical_name, false)) {
// This happens when there's is a preset (most likely legacy one) with the same name as a system preset
// that's already been loaded from a bundle.
BOOST_LOG_TRIVIAL(warning) << "Preset already present, not loading: " << canonical_name;
continue;
}
try {
Preset preset(m_type, canonical_name, false);
preset.bundle_id = resolved_origin.bundle_id;
preset.file = dir_entry.path().string();
// Load the preset file, apply preset values on top of defaults.
pending.emplace_back(dir_entry.path());
// The iteration order of directory_iterator is unspecified; sort so that the number of passes
// below, and the presets that survive them, do not depend on the filesystem.
std::sort(pending.begin(), pending.end());
size_t loaded_count = 0;
// A preset may inherit another preset from this same directory, but the presets loaded here
// become visible to find_preset2() only once they are merged into m_presets at the end of a
// pass. A preset whose parent has not been merged yet is therefore deferred and retried on a
// further pass instead of being dropped; each pass resolves one more level of the hierarchy.
// A pass that resolves nothing means the remaining parents genuinely do not exist (or form a
// cycle), and only then are they reported as errors.
while (!pending.empty()) {
// Store the loaded presets into a new vector, otherwise the binary search for already existing presets would be broken.
// (see the "Preset already present, not loading" message).
std::deque<Preset> presets_loaded;
// Preset file and the parent name it could not resolve yet.
std::vector<std::pair<boost::filesystem::path, std::string>> deferred;
//BBS: change to json format
for (const auto &preset_path : pending)
{
std::string file_name = preset_path.filename().string();
//if (Slic3r::is_ini_file(dir_entry)) {
if (Slic3r::is_json_file(file_name)) {
// Remove the .ini suffix.
std::string name = file_name.erase(file_name.size() - 5);
std::string canonical_name = this->canonical_preset_name(name, resolved_origin);
if (this->find_preset(canonical_name, false)) {
// This happens when there's is a preset (most likely legacy one) with the same name as a system preset
// that's already been loaded from a bundle.
BOOST_LOG_TRIVIAL(warning) << "Preset already present, not loading: " << canonical_name;
continue;
}
try {
fs::path idx_path(preset.file);
idx_path.replace_extension(".info");
if (fs::exists(idx_path)) {
preset.load_info(idx_path.string());
}
DynamicPrintConfig config;
//BBS: change to json format
//ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule);
std::map<std::string, std::string> key_values;
std::string reason;
ConfigSubstitutions config_substitutions = config.load_from_json(preset.file, substitution_rule, key_values, reason);
if (! config_substitutions.empty())
substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(config_substitutions) });
if (!reason.empty()) {
Preset preset(m_type, canonical_name, false);
preset.bundle_id = resolved_origin.bundle_id;
preset.file = preset_path.string();
// Substitutions reported below are rolled back if this preset ends up deferred,
// so that a retried preset does not report them twice.
const size_t substitutions_before = substitutions.size();
// Load the preset file, apply preset values on top of defaults.
try {
fs::path idx_path(preset.file);
idx_path.replace_extension(".info");
if (fs::exists(idx_path)) {
preset.load_info(idx_path.string());
}
DynamicPrintConfig config;
//BBS: change to json format
//ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule);
std::map<std::string, std::string> key_values;
std::string reason;
ConfigSubstitutions config_substitutions = config.load_from_json(preset.file, substitution_rule, key_values, reason);
if (! config_substitutions.empty())
substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(config_substitutions) });
if (!reason.empty()) {
fs::path file_path(preset.file);
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
file_path.replace_extension(".info");
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
BOOST_LOG_TRIVIAL(error) << boost::format("parse config %1% failed")%preset.file;
++m_errors;
continue;
}
std::string version_str = key_values[BBL_JSON_KEY_VERSION];
boost::optional<Semver> version = Semver::parse(version_str);
if (!version) continue;
preset.version = *version;
if (key_values.find(BBL_JSON_KEY_FILAMENT_ID) != key_values.end())
preset.filament_id = key_values[BBL_JSON_KEY_FILAMENT_ID];
if (key_values.find(BBL_JSON_KEY_DESCRIPTION) != key_values.end())
preset.description = key_values[BBL_JSON_KEY_DESCRIPTION];
if (key_values.find(BBL_JSON_KEY_INSTANTIATION) != key_values.end())
preset.is_visible = key_values[BBL_JSON_KEY_INSTANTIATION] != "false";
//Orca: find and use the inherit config as the base
Preset* inherit_preset = nullptr;
ConfigOption* inherits_config = config.option(BBL_JSON_KEY_INHERITS);
// check inherits_config
if (inherits_config) {
ConfigOptionString * option_str = dynamic_cast<ConfigOptionString *> (inherits_config);
std::string inherits_value = option_str->value;
// Orca: try to find if the parent preset has been renamed
inherit_preset = this->find_preset2(inherits_value);
Preset::normalize_inherits(config, inherit_preset);
}
const Preset& default_preset = this->default_preset_for(config);
if (inherit_preset) {
preset.config = inherit_preset->config;
preset.filament_id = inherit_preset->filament_id;
extend_default_config_length(config, false, {});
preset.config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
}
else {
auto inherits_config2 = dynamic_cast<ConfigOptionString *>(inherits_config);
if ((inherits_config2 && !inherits_config2->value.empty())) {
// The parent may be another preset of this same pass, not merged into
// m_presets yet. Retry once it is; only a pass that resolves nothing
// reports the parent as missing.
substitutions.resize(substitutions_before);
deferred.emplace_back(preset_path, inherits_config2->value);
continue;
}
// We support custom root preset now
// Find a default preset for the config. The PrintPresetCollection provides different default preset based on the "printer_technology" field.
preset.config = default_preset.config;
preset.config.apply(std::move(config));
extend_default_config_length(preset.config, true, default_preset.config);
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load preset: " << name << " and filament_id: " << preset.filament_id << " and base_id: " << preset.base_id;
Preset::normalize(preset.config);
// Report configuration fields, which are misplaced into a wrong group.
std::string incorrect_keys = Preset::remove_invalid_keys(preset.config, default_preset.config);
if (!incorrect_keys.empty()) {
++m_errors;
BOOST_LOG_TRIVIAL(error)
<< "Error in a preset file: The preset \"" << preset.file
<< "\" contains the following incorrect keys: " << incorrect_keys << ", which were removed";
}
if (preset.type == Preset::TYPE_FILAMENT && preset.is_user() && preset.inherits().empty()) {
auto compatible_printers = dynamic_cast<ConfigOptionStrings *>(preset.config.option("compatible_printers", true));
if (compatible_printers && compatible_printers->values.empty()) {
size_t at_pos = name.find('@');
if (at_pos != std::string::npos && at_pos + 1 < name.length()) {
compatible_printers->values.push_back(name.substr(at_pos + 1));
if (!read_only)
preset.save(nullptr);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " added compatible_printers for preset: " << name;
}
}
}
preset.loaded = true;
//BBS: add some workaround for previous incorrect settings
if ((!preset.setting_id.empty())&&(preset.setting_id == preset.base_id))
preset.setting_id.clear();
//BBS: add config related logs
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", preset type %1%, name %2%, path %3%, is_system %4%, is_default %5% is_visible %6%")%Preset::get_type_string(m_type) %preset.name %preset.file %preset.is_system %preset.is_default %preset.is_visible;
// add alias for custom filament preset
set_custom_preset_alias(preset);
} catch (const std::ifstream::failure &err) {
++m_errors;
BOOST_LOG_TRIVIAL(error) << boost::format("The user-config cannot be loaded: %1%. Reason: %2%")%preset.file %err.what();
fs::path file_path(preset.file);
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
file_path.replace_extension(".info");
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
BOOST_LOG_TRIVIAL(error) << boost::format("parse config %1% failed")%preset.file;
//throw Slic3r::RuntimeError(std::string("The selected preset cannot be loaded: ") + preset.file + "\n\tReason: " + err.what());
} catch (const std::runtime_error &err) {
++m_errors;
continue;
BOOST_LOG_TRIVIAL(error) << boost::format("Failed loading the user-config file: %1%. Reason: %2%")%preset.file %err.what();
//throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + preset.file + "\n\tReason: " + err.what());
fs::path file_path(preset.file);
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
file_path.replace_extension(".info");
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
}
std::string version_str = key_values[BBL_JSON_KEY_VERSION];
boost::optional<Semver> version = Semver::parse(version_str);
if (!version) continue;
preset.version = *version;
if (preset_loaded_fn != nullptr)
preset_loaded_fn(preset);
if (key_values.find(BBL_JSON_KEY_FILAMENT_ID) != key_values.end())
preset.filament_id = key_values[BBL_JSON_KEY_FILAMENT_ID];
if (key_values.find(BBL_JSON_KEY_DESCRIPTION) != key_values.end())
preset.description = key_values[BBL_JSON_KEY_DESCRIPTION];
if (key_values.find(BBL_JSON_KEY_INSTANTIATION) != key_values.end())
preset.is_visible = key_values[BBL_JSON_KEY_INSTANTIATION] != "false";
//Orca: find and use the inherit config as the base
Preset* inherit_preset = nullptr;
ConfigOption* inherits_config = config.option(BBL_JSON_KEY_INHERITS);
// check inherits_config
if (inherits_config) {
ConfigOptionString * option_str = dynamic_cast<ConfigOptionString *> (inherits_config);
std::string inherits_value = option_str->value;
// Orca: try to find if the parent preset has been renamed
inherit_preset = this->find_preset2(inherits_value);
Preset::normalize_inherits(config, inherit_preset);
} else {
;
}
const Preset& default_preset = this->default_preset_for(config);
if (inherit_preset) {
preset.config = inherit_preset->config;
preset.filament_id = inherit_preset->filament_id;
extend_default_config_length(config, false, {});
preset.config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
}
else {
auto inherits_config2 = dynamic_cast<ConfigOptionString *>(inherits_config);
if ((inherits_config2 && !inherits_config2->value.empty())) {
BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent %1% for config %2%!")%inherits_config2->value %preset.file;
++m_errors;
continue;
}
// We support custom root preset now
// Find a default preset for the config. The PrintPresetCollection provides different default preset based on the "printer_technology" field.
preset.config = default_preset.config;
preset.config.apply(std::move(config));
extend_default_config_length(preset.config, true, default_preset.config);
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load preset: " << name << " and filament_id: " << preset.filament_id << " and base_id: " << preset.base_id;
Preset::normalize(preset.config);
// Report configuration fields, which are misplaced into a wrong group.
std::string incorrect_keys = Preset::remove_invalid_keys(preset.config, default_preset.config);
if (!incorrect_keys.empty()) {
++m_errors;
BOOST_LOG_TRIVIAL(error)
<< "Error in a preset file: The preset \"" << preset.file
<< "\" contains the following incorrect keys: " << incorrect_keys << ", which were removed";
}
if (preset.type == Preset::TYPE_FILAMENT && preset.is_user() && preset.inherits().empty()) {
auto compatible_printers = dynamic_cast<ConfigOptionStrings *>(preset.config.option("compatible_printers", true));
if (compatible_printers && compatible_printers->values.empty()) {
size_t at_pos = name.find('@');
if (at_pos != std::string::npos && at_pos + 1 < name.length()) {
compatible_printers->values.push_back(name.substr(at_pos + 1));
if (!read_only)
preset.save(nullptr);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " added compatible_printers for preset: " << name;
}
}
}
preset.loaded = true;
//BBS: add some workaround for previous incorrect settings
if ((!preset.setting_id.empty())&&(preset.setting_id == preset.base_id))
preset.setting_id.clear();
//BBS: add config related logs
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", preset type %1%, name %2%, path %3%, is_system %4%, is_default %5% is_visible %6%")%Preset::get_type_string(m_type) %preset.name %preset.file %preset.is_system %preset.is_default %preset.is_visible;
// add alias for custom filament preset
set_custom_preset_alias(preset);
} catch (const std::ifstream::failure &err) {
++m_errors;
BOOST_LOG_TRIVIAL(error) << boost::format("The user-config cannot be loaded: %1%. Reason: %2%")%preset.file %err.what();
fs::path file_path(preset.file);
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
file_path.replace_extension(".info");
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
//throw Slic3r::RuntimeError(std::string("The selected preset cannot be loaded: ") + preset.file + "\n\tReason: " + err.what());
presets_loaded.emplace_back(preset);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " load config successful and preset name is:" << preset.name;
} catch (const std::runtime_error &err) {
++m_errors;
BOOST_LOG_TRIVIAL(error) << boost::format("Failed loading the user-config file: %1%. Reason: %2%")%preset.file %err.what();
//throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + preset.file + "\n\tReason: " + err.what());
fs::path file_path(preset.file);
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
file_path.replace_extension(".info");
if (!read_only && fs::exists(file_path))
fs::remove(file_path);
errors_cummulative += err.what();
errors_cummulative += "\n";
}
if (preset_loaded_fn != nullptr)
preset_loaded_fn(preset);
presets_loaded.emplace_back(preset);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " load config successful and preset name is:" << preset.name;
} catch (const std::runtime_error &err) {
errors_cummulative += err.what();
errors_cummulative += "\n";
}
}
if (presets_loaded.size() > 0)
m_presets.insert(m_presets.end(), std::make_move_iterator(presets_loaded.begin()), std::make_move_iterator(presets_loaded.end()));
sort_presets();
loaded_count += presets_loaded.size();
if (deferred.empty())
break;
if (presets_loaded.empty()) {
for (const auto &entry : deferred) {
BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent %1% for config %2%!")%entry.second %entry.first.string();
m_unresolved_parents[entry.first.string()] = entry.second;
++m_errors;
}
break;
}
pending.clear();
for (auto &entry : deferred)
pending.emplace_back(std::move(entry.first));
}
if (presets_loaded.size() > 0)
m_presets.insert(m_presets.end(), std::make_move_iterator(presets_loaded.begin()), std::make_move_iterator(presets_loaded.end()));
sort_presets();
//BBS: add config related logs
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": loaded %1% presets from %2%, type %3%")%presets_loaded.size() %dir %Preset::get_type_string(m_type);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": loaded %1% presets from %2%, type %3%")%loaded_count %dir %Preset::get_type_string(m_type);
//this->select_preset(first_visible_idx());
if (! errors_cummulative.empty())
throw Slic3r::RuntimeError(errors_cummulative);
@@ -3308,6 +3347,16 @@ Preset* PresetCollection::find_preset(const std::string &name, bool first_visibl
return first_visible_if_not_found ? &this->first_visible() : nullptr;
}
std::string PresetCollection::unresolved_parent(const boost::filesystem::path &file) const
{
for (const auto &[dropped_file, parent] : m_unresolved_parents) {
boost::system::error_code ec;
if (boost::filesystem::equivalent(file, boost::filesystem::path(dropped_file), ec) && !ec)
return parent;
}
return {};
}
Preset* PresetCollection::find_preset2(const std::string& name, bool auto_match/* = true */)
{
auto preset = find_preset(name, false, true);

View File

@@ -2,6 +2,7 @@
#define slic3r_Preset_hpp_
#include <deque>
#include <map>
#include <set>
#include <string>
#include <unordered_map>
@@ -728,6 +729,9 @@ public:
{
return const_cast<PresetCollection*>(this)->find_preset2(name, auto_match);
}
// Name of the parent that kept the preset file from loading, or empty if the file loaded
// (or was never seen). Lets a caller that fails to find a preset explain why it is missing.
std::string unresolved_parent(const boost::filesystem::path &file) const;
size_t first_visible_idx() const;
// Return the index of the first visible, compatible, system base preset
@@ -965,6 +969,8 @@ private:
// Orca: used for validation only
int m_errors = 0;
// Preset files dropped by load_presets() because their parent does not exist, keyed by file path.
std::map<std::string, std::string> m_unresolved_parents;
};
// Printer supports the FFF and SLA technologies, with different set of configuration values,

View File

@@ -506,6 +506,11 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ
}
if (error == "Preset identity is ambiguous")
return false;
// The file was seen but dropped at load time; say so rather than reporting it as unknown.
if (const std::string parent = collection->unresolved_parent(source_path); !parent.empty()) {
error = "Preset was not loaded because its parent preset \"" + parent + "\" was not found";
return false;
}
if (!allow_source_manifest) {
error = "Preset was not found in the loaded bundle";
return false;

View File

@@ -158,26 +158,6 @@ enum class EGCodeExtrusionRole : uint8_t
static constexpr std::size_t GCODE_EXTRUSION_ROLES_COUNT = static_cast<std::size_t>(EGCodeExtrusionRole::COUNT);
//
// ORCA: what the reduced toolpath set drawn while the user is dragging leaves out, on top of
// keeping only one layer in every Viewer::get_reduced_detail_layer_stride() layers
//
enum class EReducedDetailMode : uint8_t
{
// no reduced set is built and Viewer::set_reduced_detail() has no effect
Off,
// every role is kept, only layers are skipped
LayersOnly,
// the interior infill roles are left out
NoInternalInfill,
// only the segments on the visible surface of the print are kept
ShellOnly,
// only the bottom and top of the visible layer range are kept, for a caller that draws the
// print itself some other way and needs just the two faces the range cuts open
EndLayersOnly,
COUNT
};
//
// Option types
//

View File

@@ -98,46 +98,6 @@ public:
//
bool is_dim_previous_layers() const;
void set_dim_previous_layers(bool value);
//
// ORCA: draw the preview from a reduced set of entities: one layer in every
// get_reduced_detail_layer_stride() layers is kept, and on top of that the mode decides which
// roles or segments are left out. The bottom and top of the visible layer range are always kept
// whole. Meant to be held only while the user drags the camera or a slider: the reduced set is
// built alongside the full one, so toggling it never rebuilds anything. Nothing is built while
// the mode is EReducedDetailMode::Off. Has no effect on the OpenGL ES path.
//
void set_reduced_detail(bool value);
bool is_reduced_detail() const;
EReducedDetailMode get_reduced_detail_mode() const;
void set_reduced_detail_mode(EReducedDetailMode mode);
uint32_t get_reduced_detail_layer_stride() const;
void set_reduced_detail_layer_stride(uint32_t value);
//
// ORCA: what is left out even at rest, with every layer drawn. Off draws everything.
// EReducedDetailMode::LayersOnly means the same as Off here.
//
EReducedDetailMode get_rest_detail_mode() const;
void set_rest_detail_mode(EReducedDetailMode mode);
//
// ORCA: with EReducedDetailMode::ShellOnly at rest, draw the walls of one layer in every N, each
// N layers tall, keeping the exposed surfaces of every layer. Choose N from how many layers fit
// in a pixel at the current view and it changes nothing visible.
//
uint32_t get_rest_layer_stride() const;
void set_rest_layer_stride(uint32_t value);
//
// ORCA: whether the camera is above the print. With layers skipped at rest, only the surfaces
// that side of the print can see are kept in the skipped layers.
//
bool is_rest_view_from_above() const;
void set_rest_view_from_above(bool value);
//
// ORCA: a counter that changes whenever what render() draws changes, and whether an update is
// still pending that the next render() will apply. Together they tell a caller whether a frame
// it has kept from an earlier render() can be shown again.
//
uint64_t get_state_version() const;
bool has_pending_updates() const;
float get_dim_previous_layers_brightness() const;
void set_dim_previous_layers_brightness(float value);
//

View File

@@ -15,12 +15,7 @@ namespace libvgcode {
//| 2--0-------5--7 |
//| \ | | / |
//| 3-------4 |
// The eight corners the vertex shader knows how to place. Each is sent once and
// referenced by INDEX_DATA below, so the post-transform cache can reuse it across
// the triangles that share it: the shader runs 8 times per segment instead of 24.
static constexpr const std::array<uint8_t, 8> VERTEX_DATA = { 0, 1, 2, 3, 4, 5, 6, 7 };
static constexpr const std::array<uint8_t, 24> INDEX_DATA = {
static constexpr const std::array<uint8_t, 24> VERTEX_DATA = {
0, 1, 2, // front spike
0, 2, 3, // front spike
0, 3, 4, // right/bottom body
@@ -36,7 +31,7 @@ void SegmentTemplate::init()
if (m_vao_id != 0)
return;
m_size_in_bytes_gpu += (VERTEX_DATA.size() + INDEX_DATA.size()) * sizeof(uint8_t);
m_size_in_bytes_gpu += VERTEX_DATA.size() * sizeof(uint8_t);
int curr_vertex_array;
glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array));
@@ -56,22 +51,12 @@ void SegmentTemplate::init()
glsafe(glVertexAttribIPointer(0, 1, GL_UNSIGNED_BYTE, 0, (const void*)0));
#endif // ENABLE_OPENGL_ES
// The element buffer binding is part of the vao state, so it is left bound here
// and restored together with the vao.
glsafe(glGenBuffers(1, &m_ibo_id));
glsafe(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ibo_id));
glsafe(glBufferData(GL_ELEMENT_ARRAY_BUFFER, INDEX_DATA.size() * sizeof(uint8_t), INDEX_DATA.data(), GL_STATIC_DRAW));
glsafe(glBindBuffer(GL_ARRAY_BUFFER, curr_array_buffer));
glsafe(glBindVertexArray(curr_vertex_array));
}
void SegmentTemplate::shutdown()
{
if (m_ibo_id != 0) {
glsafe(glDeleteBuffers(1, &m_ibo_id));
m_ibo_id = 0;
}
if (m_vbo_id != 0) {
glsafe(glDeleteBuffers(1, &m_vbo_id));
m_vbo_id = 0;
@@ -86,15 +71,14 @@ void SegmentTemplate::shutdown()
void SegmentTemplate::render(size_t count)
{
if (m_vao_id == 0 || m_vbo_id == 0 || m_ibo_id == 0 || count == 0)
if (m_vao_id == 0 || m_vbo_id == 0 || count == 0)
return;
int curr_vertex_array;
glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array));
glsafe(glBindVertexArray(m_vao_id));
glsafe(glDrawElementsInstanced(GL_TRIANGLES, static_cast<GLsizei>(INDEX_DATA.size()), GL_UNSIGNED_BYTE,
nullptr, static_cast<GLsizei>(count)));
glsafe(glDrawArraysInstanced(GL_TRIANGLES, 0, static_cast<GLsizei>(VERTEX_DATA.size()), static_cast<GLsizei>(count)));
glsafe(glBindVertexArray(curr_vertex_array));
}

View File

@@ -40,7 +40,6 @@ private:
//
unsigned int m_vao_id{ 0 };
unsigned int m_vbo_id{ 0 };
unsigned int m_ibo_id{ 0 };
//
// Size of the data sent to gpu, in bytes.
//

View File

@@ -25,24 +25,6 @@ struct Settings
// ORCA: how bright those darkened layers are rendered, 1.0 = unchanged, 0.0 = black
float dim_previous_layers_brightness{ 0.4f };
bool spiral_vase_mode{ false };
// ORCA: while the user drags the camera or a slider, the preview can be drawn from a reduced
// set of entities: one layer in every reduced_detail_layer_stride kept, and on top of that
// whatever reduced_detail_mode leaves out. The reduced sets are built alongside the full ones
// in update_enabled_entities(), so holding reduced_detail costs nothing but a buffer binding.
// Ignored on the OpenGL ES path, which keeps a single set of entities.
bool reduced_detail{ false };
EReducedDetailMode reduced_detail_mode{ EReducedDetailMode::Off };
uint32_t reduced_detail_layer_stride{ 4 };
// ORCA: what is left out even at rest, with every layer drawn. Bound whenever the reduced set
// above is not. Off draws everything.
EReducedDetailMode rest_detail_mode{ EReducedDetailMode::Off };
// ORCA: in ShellOnly rest mode, the walls of one layer in this many are drawn, that many layers
// tall; the exposed surfaces of every layer stay. Meant to follow how many layers fit in a
// pixel at the current view, so that it changes nothing visible.
uint32_t rest_layer_stride{ 1 };
// ORCA: whether the camera is above the print; decides whether the surfaces kept in the
// skipped layers at rest are the ones visible from above or from below
bool rest_view_from_above{ true };
//
// Required update flags
//

View File

@@ -26,8 +26,6 @@ static const char* Segments_Vertex_Shader =
"uniform mat4 view_matrix;\n"
"uniform mat4 projection_matrix;\n"
"uniform vec3 camera_position;\n"
"// ORCA: how many layers each drawn segment stands in for while layers are being skipped\n"
"uniform float height_scale;\n"
"uniform samplerBuffer position_tex;\n"
"uniform samplerBuffer height_width_angle_tex;\n"
"uniform samplerBuffer color_tex;\n"
@@ -114,20 +112,7 @@ static const char* Segments_Vertex_Shader =
"#endif\n"
" float view_right_sign = sign(dot(-camera_view_dir, line_right_dir));\n"
" float view_top_sign = sign(dot(-camera_view_dir, line_up_dir));\n"
" // ORCA: the cross-section is a diamond, full width at mid-height and a point at top and bottom.\n"
" // A segment grown to stand in for skipped layers would leave a notch that deep between itself\n"
" // and the next one, so its four profile points become the corners of a rectangle instead:\n"
" // top to top-left, right to top-right, bottom to bottom-right, left to bottom-left, which keeps\n"
" // the winding and turns the two drawn faces into a flat top and a flat camera-facing side.\n"
" if (height_scale > 1.0) {\n"
" if (signs.y > 0.0) signs = vec2(-1.0, 1.0);\n"
" else if (signs.y < 0.0) signs = vec2(1.0, -1.0);\n"
" else if (signs.x > 0.0) signs = vec2(1.0, 1.0);\n"
" else signs = vec2(-1.0, -1.0);\n"
" }\n"
" // ORCA: a segment standing in for the skipped layers below it grows downward to cover them\n"
" endpoint_pos -= (height_scale - 1.0) * 0.5 * height_width_angle.x * line_up_dir;\n"
" float half_height = 0.5 * height_scale * height_width_angle.x;\n"
" float half_height = 0.5 * height_width_angle.x;\n"
" float half_width = 0.5 * height_width_angle.y;\n"
" vec3 horizontal_dir = half_width * line_right_dir;\n"
" vec3 vertical_dir = half_height * line_up_dir;\n"
@@ -148,19 +133,10 @@ static const char* Segments_Vertex_Shader =
" pos += sign(height_width_angle.z) * horizontal_dir * cos(abs(height_width_angle.z) * 0.5);\n"
" }\n"
" }\n"
" // ORCA: the grown first layer must not reach below the bed\n"
" if (height_scale > 1.0)\n"
" pos.z = max(pos.z, 0.0);\n"
" vec3 eye_position = (view_matrix * vec4(pos, 1.0)).xyz;\n"
" // ORCA: Apply bias to z-position to avoid z-fighting\n"
" eye_position.z += bias;\n"
" vec3 normal_dir = normalize(pos - endpoint_pos);\n"
" // ORCA: a grown box is lit flat: its camera-facing side through the side normal, its top-left\n"
" // corner through the up normal, so the side carries no bright edge every few layers\n"
" if (height_scale > 1.0)\n"
" normal_dir = (signs.x > 0.0) ? horizontal_sign * line_right_dir :\n"
" (signs.y > 0.0) ? vertical_sign * line_up_dir : -horizontal_sign * line_right_dir;\n"
" vec3 eye_normal = (view_matrix * vec4(normal_dir, 0.0)).xyz;\n"
" vec3 eye_normal = (view_matrix * vec4(normalize(pos - endpoint_pos), 0.0)).xyz;\n"
" vec3 color_base = decode_color(texelFetch(color_tex, id).r);\n"
" color = color_base * lighting(eye_position, eye_normal);\n"
" gl_Position = projection_matrix * vec4(eye_position, 1.0);\n"

View File

@@ -77,76 +77,6 @@ bool Viewer::is_dim_previous_layers() const
return m_impl->is_dim_previous_layers();
}
void Viewer::set_reduced_detail(bool value)
{
m_impl->set_reduced_detail(value);
}
bool Viewer::is_reduced_detail() const
{
return m_impl->is_reduced_detail();
}
EReducedDetailMode Viewer::get_reduced_detail_mode() const
{
return m_impl->get_reduced_detail_mode();
}
void Viewer::set_reduced_detail_mode(EReducedDetailMode mode)
{
m_impl->set_reduced_detail_mode(mode);
}
uint32_t Viewer::get_reduced_detail_layer_stride() const
{
return m_impl->get_reduced_detail_layer_stride();
}
void Viewer::set_reduced_detail_layer_stride(uint32_t value)
{
m_impl->set_reduced_detail_layer_stride(value);
}
EReducedDetailMode Viewer::get_rest_detail_mode() const
{
return m_impl->get_rest_detail_mode();
}
void Viewer::set_rest_detail_mode(EReducedDetailMode mode)
{
m_impl->set_rest_detail_mode(mode);
}
uint32_t Viewer::get_rest_layer_stride() const
{
return m_impl->get_rest_layer_stride();
}
void Viewer::set_rest_layer_stride(uint32_t value)
{
m_impl->set_rest_layer_stride(value);
}
bool Viewer::is_rest_view_from_above() const
{
return m_impl->is_rest_view_from_above();
}
void Viewer::set_rest_view_from_above(bool value)
{
m_impl->set_rest_view_from_above(value);
}
uint64_t Viewer::get_state_version() const
{
return m_impl->get_state_version();
}
bool Viewer::has_pending_updates() const
{
return m_impl->has_pending_updates();
}
void Viewer::set_dim_previous_layers(bool value)
{
m_impl->set_dim_previous_layers(value);

View File

@@ -17,10 +17,6 @@
#include <algorithm>
#include <cmath>
#include <numeric>
#include <cfloat>
#include <future>
#include <thread>
#include <unordered_map>
namespace libvgcode {
@@ -763,7 +759,6 @@ void ViewerImpl::init(const std::string& opengl_context_version)
m_uni_segments_view_matrix_id = glGetUniformLocation(m_segments_shader_id, "view_matrix");
m_uni_segments_projection_matrix_id = glGetUniformLocation(m_segments_shader_id, "projection_matrix");
m_uni_segments_camera_position_id = glGetUniformLocation(m_segments_shader_id, "camera_position");
m_uni_segments_height_scale_id = glGetUniformLocation(m_segments_shader_id, "height_scale");
m_uni_segments_positions_tex_id = glGetUniformLocation(m_segments_shader_id, "position_tex");
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");
@@ -880,12 +875,6 @@ void ViewerImpl::reset()
m_travels_time = { 0.0f, 0.0f };
m_vertices.clear();
m_vertices_colors.clear();
// swap rather than clear: these are sized by the print, and a reset means the memory
// should go back, not sit reserved until the next load
for (std::vector<float>& times : m_cumulative_times)
std::vector<float>().swap(times);
std::vector<uint32_t>().swap(m_layer_first_vertex);
std::vector<float>().swap(m_colors_scratch);
m_valid_lines_bitset.clear();
#if VGCODE_ENABLE_COG_AND_TOOL_MARKERS
m_cog_marker.reset();
@@ -896,23 +885,9 @@ void ViewerImpl::reset()
#else
m_enabled_segments_count = 0;
m_enabled_options_count = 0;
m_enabled_segments_reduced_count = 0;
m_enabled_options_reduced_count = 0;
m_enabled_segments_rest_count = 0;
++m_state_version;
m_shell_bitset = BitSet<>();
m_near_shell_bitset = BitSet<>();
m_top_visible_bitset = BitSet<>();
m_bottom_visible_bitset = BitSet<>();
m_settings_used_for_ranges = std::nullopt;
delete_textures(m_enabled_segments_rest_tex_id);
delete_buffers(m_enabled_segments_rest_buf_id);
delete_textures(m_enabled_options_reduced_tex_id);
delete_buffers(m_enabled_options_reduced_buf_id);
delete_textures(m_enabled_segments_reduced_tex_id);
delete_buffers(m_enabled_segments_reduced_buf_id);
delete_textures(m_enabled_options_tex_id);
delete_buffers(m_enabled_options_buf_id);
delete_textures(m_enabled_segments_tex_id);
@@ -1021,8 +996,6 @@ void ViewerImpl::load(GCodeInputData&& gcode_data)
m_tool_colors = std::move(gcode_data.tools_colors);
m_color_print_colors = std::move(gcode_data.color_print_colors);
m_vertices_colors.resize(m_vertices.size());
for (std::vector<float>& times : m_cumulative_times)
times.resize(m_vertices.size());
m_settings.spiral_vase_mode = gcode_data.spiral_vase_mode;
@@ -1033,9 +1006,6 @@ void ViewerImpl::load(GCodeInputData&& gcode_data)
for (size_t j = 0; j < TIME_MODES_COUNT; ++j) {
m_total_time[j] += v.times[j];
// the running total up to and including this vertex is exactly what
// get_estimated_time_at() has to return for it
m_cumulative_times[j][i] = m_total_time[j];
if (v.type == EMoveType::Travel)
m_travels_time[j] += v.times[j];
}
@@ -1078,20 +1048,6 @@ void ViewerImpl::load(GCodeInputData&& gcode_data)
v.layer_duration = m_layers.get_layer_time(m_settings.time_mode, static_cast<size_t>(v.layer_id));
}
// Index of the first vertex of each layer, walked back to front so that a layer with no
// vertex of its own inherits the next layer's index and the array stays non-decreasing.
if (!m_layers.empty()) {
const uint32_t vertices_count = static_cast<uint32_t>(m_vertices.size());
m_layer_first_vertex.assign(m_layers.count(), vertices_count);
for (uint32_t i = vertices_count; i > 0; --i) {
const uint32_t layer_id = m_vertices[i - 1].layer_id;
if (layer_id < m_layer_first_vertex.size())
m_layer_first_vertex[layer_id] = i - 1;
}
for (size_t i = m_layer_first_vertex.size() - 1; i > 0; --i)
m_layer_first_vertex[i - 1] = std::min(m_layer_first_vertex[i - 1], m_layer_first_vertex[i]);
}
if (!m_layers.empty())
m_layers.set_view_range(0, static_cast<uint32_t>(m_layers.count()) - 1);
@@ -1160,22 +1116,6 @@ void ViewerImpl::load(GCodeInputData&& gcode_data)
glsafe(glGenTextures(1, &m_enabled_options_tex_id));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_options_tex_id));
// create (but do not fill) the reduced counterparts of the two buffers above
glsafe(glGenBuffers(1, &m_enabled_segments_reduced_buf_id));
glsafe(glBindBuffer(GL_TEXTURE_BUFFER, m_enabled_segments_reduced_buf_id));
glsafe(glGenTextures(1, &m_enabled_segments_reduced_tex_id));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_segments_reduced_tex_id));
glsafe(glGenBuffers(1, &m_enabled_options_reduced_buf_id));
glsafe(glBindBuffer(GL_TEXTURE_BUFFER, m_enabled_options_reduced_buf_id));
glsafe(glGenTextures(1, &m_enabled_options_reduced_tex_id));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_options_reduced_tex_id));
glsafe(glGenBuffers(1, &m_enabled_segments_rest_buf_id));
glsafe(glBindBuffer(GL_TEXTURE_BUFFER, m_enabled_segments_rest_buf_id));
glsafe(glGenTextures(1, &m_enabled_segments_rest_tex_id));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_segments_rest_tex_id));
glsafe(glBindBuffer(GL_TEXTURE_BUFFER, 0));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, old_bound_texture));
#endif // ENABLE_OPENGL_ES
@@ -1187,508 +1127,6 @@ void ViewerImpl::load(GCodeInputData&& gcode_data)
update_colors();
}
#ifndef ENABLE_OPENGL_ES
// ORCA: the roles that sit inside the part and are hidden by its walls from every angle
static bool is_interior_infill(EGCodeExtrusionRole role)
{
return role == EGCodeExtrusionRole::InternalInfill ||
role == EGCodeExtrusionRole::SolidInfill ||
role == EGCodeExtrusionRole::InternalBridgeInfill;
}
// ORCA: what can never be a visible surface whatever the geometry says: the interior roles, and
// gap fill, which sits between walls. Short sparse-infill segments hugging a wall would otherwise
// pass the geometric test by the thousand.
static bool is_hidden_in_shell(EGCodeExtrusionRole role)
{
return is_interior_infill(role) || role == EGCodeExtrusionRole::GapFill;
}
bool ViewerImpl::reduced_set_keeps(EReducedDetailMode mode, size_t i, const PathVertex& v) const
{
switch (mode) {
case EReducedDetailMode::NoInternalInfill: return !is_interior_infill(v.role);
case EReducedDetailMode::ShellOnly:
// the first inner wall fills the step of a sloped surface between one layer's outer wall
// and the next, too narrow for the grid to see; whatever is the visible top or bottom of a
// step stays whatever its role
return (!is_hidden_in_shell(v.role) && m_shell_bitset[i]) || m_near_shell_bitset[i] ||
m_top_visible_bitset[i] || m_bottom_visible_bitset[i];
default: return true;
}
}
namespace {
// A 2D occupancy grid over the print's footprint, one byte per cell. Only the rectangle a layer
// touches is ever cleared or scanned, so a grid the size of the whole print costs no more than
// the layer needs.
struct OccupancyGrid
{
int nx{ 0 };
int ny{ 0 };
std::vector<uint8_t> cells;
// bounding rectangle of the set cells, inclusive; empty while min > max
int min_x{ 0 };
int min_y{ 0 };
int max_x{ -1 };
int max_y{ -1 };
OccupancyGrid(int nx, int ny) : nx(nx), ny(ny), cells(static_cast<size_t>(nx) * static_cast<size_t>(ny), 0) {}
bool empty() const { return min_x > max_x; }
uint8_t at(int x, int y) const { return cells[static_cast<size_t>(y) * nx + x]; }
uint8_t& at(int x, int y) { return cells[static_cast<size_t>(y) * nx + x]; }
void set(int x, int y) {
at(x, y) = 1;
if (empty()) {
min_x = max_x = x;
min_y = max_y = y;
}
else {
min_x = std::min(min_x, x);
max_x = std::max(max_x, x);
min_y = std::min(min_y, y);
max_y = std::max(max_y, y);
}
}
void clear() {
for (int y = min_y; y <= max_y; ++y)
std::fill_n(&at(min_x, y), max_x - min_x + 1, uint8_t(0));
min_x = min_y = 0;
max_x = max_y = -1;
}
// grow the bounding rectangle by r cells, staying inside the grid
void grow(int r) {
if (empty())
return;
min_x = std::max(0, min_x - r);
min_y = std::max(0, min_y - r);
max_x = std::min(nx - 1, max_x + r);
max_y = std::min(ny - 1, max_y + r);
}
};
// Scratch space for close_gaps(), one per worker
struct ClosingScratch
{
// component label per cell: 0 empty, > 0 a component, WILD a tiny fragment, CONTESTED a cell
// reached by two components' dilations
std::vector<int32_t> labels;
std::vector<std::pair<int, int>> frontier;
std::vector<std::pair<int, int>> next;
std::vector<int> window_sum;
std::vector<uint8_t> raw;
static constexpr int32_t WILD = -1;
static constexpr int32_t CONTESTED = -2;
};
// Morphological closing with a square window of the given radius: fills gaps up to 2 * radius
// cells wide, so that sparse infill or support reads as the solid area it is part of. The dilation
// is done per connected component, and a cell two components both reach stays empty, so the gap
// between two objects standing close together is never bridged and both of their facing walls
// stay on the shell. A separable erosion over running window sums then shrinks the result back.
static void close_gaps(OccupancyGrid& grid, int radius, ClosingScratch& scratch)
{
if (grid.empty() || radius <= 0)
return;
// the dilated area needs room to grow
grid.grow(radius);
const int nx = grid.nx;
const auto idx = [nx](int x, int y) { return static_cast<size_t>(y) * nx + x; };
const auto in_rect = [&](int x, int y) { return x >= grid.min_x && x <= grid.max_x && y >= grid.min_y && y <= grid.max_y; };
std::vector<int32_t>& labels = scratch.labels;
labels.resize(grid.cells.size());
for (int y = grid.min_y; y <= grid.max_y; ++y)
std::fill_n(&labels[idx(grid.min_x, y)], grid.max_x - grid.min_x + 1, 0);
// the raw cells come back at the end: a closing must never lose one, and the erosion below
// would eat into a wall that faces a contested gap
std::vector<uint8_t>& raw = scratch.raw;
raw.resize(grid.cells.size());
for (int y = grid.min_y; y <= grid.max_y; ++y)
std::copy_n(&grid.at(grid.min_x, y), grid.max_x - grid.min_x + 1, &raw[idx(grid.min_x, y)]);
// label the 8-connected components of the raw cells; a fragment too small to be a wall does
// not spread and is absorbed by whichever component reaches it
static constexpr size_t TINY = 8;
int32_t next_label = 1;
std::vector<std::pair<int, int>>& frontier = scratch.frontier;
frontier.clear();
for (int y = grid.min_y; y <= grid.max_y; ++y) {
for (int x = grid.min_x; x <= grid.max_x; ++x) {
if (!grid.at(x, y) || labels[idx(x, y)] != 0)
continue;
std::vector<std::pair<int, int>>& component = scratch.next;
component.clear();
component.emplace_back(x, y);
labels[idx(x, y)] = next_label;
for (size_t head = 0; head < component.size(); ++head) {
const auto [cx, cy] = component[head];
for (int dy = -1; dy <= 1; ++dy) {
for (int dx = -1; dx <= 1; ++dx) {
const int px = cx + dx;
const int py = cy + dy;
if ((dx == 0 && dy == 0) || !in_rect(px, py) || !grid.at(px, py) || labels[idx(px, py)] != 0)
continue;
labels[idx(px, py)] = next_label;
component.emplace_back(px, py);
}
}
}
if (component.size() < TINY) {
for (const auto [cx, cy] : component)
labels[idx(cx, cy)] = ClosingScratch::WILD;
}
else {
frontier.insert(frontier.end(), component.begin(), component.end());
++next_label;
}
}
}
// dilate: each component claims the cells within radius of it, breadth first; a cell already
// claimed by another component is contested and stays empty
for (int step = 0; step < radius; ++step) {
std::vector<std::pair<int, int>>& next = scratch.next;
next.clear();
for (const auto [cx, cy] : frontier) {
const int32_t label = labels[idx(cx, cy)];
if (label <= 0)
continue;
for (int dy = -1; dy <= 1; ++dy) {
for (int dx = -1; dx <= 1; ++dx) {
const int px = cx + dx;
const int py = cy + dy;
if ((dx == 0 && dy == 0) || !in_rect(px, py))
continue;
int32_t& other = labels[idx(px, py)];
if (other == 0 || other == ClosingScratch::WILD) {
other = label;
next.emplace_back(px, py);
}
else if (other != label && other != ClosingScratch::CONTESTED && !grid.at(px, py))
other = ClosingScratch::CONTESTED;
}
}
}
std::swap(frontier, next);
}
for (int y = grid.min_y; y <= grid.max_y; ++y) {
for (int x = grid.min_x; x <= grid.max_x; ++x) {
if (labels[idx(x, y)] > 0)
grid.at(x, y) = 1;
}
}
// erode by the same radius, separably; cells outside the rectangle are empty, which is what a
// shrinking erosion has to see
std::vector<int>& window_sum = scratch.window_sum;
const auto erode = [&](bool horizontal) {
const int outer_n = horizontal ? grid.max_y - grid.min_y + 1 : grid.max_x - grid.min_x + 1;
const int inner_n = horizontal ? grid.max_x - grid.min_x + 1 : grid.max_y - grid.min_y + 1;
window_sum.assign(inner_n + 1, 0);
for (int o = 0; o < outer_n; ++o) {
const auto cell = [&](int i) -> uint8_t& {
return horizontal ? grid.at(grid.min_x + i, grid.min_y + o) : grid.at(grid.min_x + o, grid.min_y + i);
};
for (int i = 0; i < inner_n; ++i)
window_sum[i + 1] = window_sum[i] + cell(i);
for (int i = 0; i < inner_n; ++i) {
const int count = window_sum[std::min(inner_n, i + radius + 1)] - window_sum[std::max(0, i - radius)];
cell(i) = (count == 2 * radius + 1);
}
}
};
erode(true);
erode(false);
for (int y = grid.min_y; y <= grid.max_y; ++y) {
for (int x = grid.min_x; x <= grid.max_x; ++x)
grid.at(x, y) |= raw[idx(x, y)];
}
}
} // namespace
// ORCA: mark the extrusion segments that lie on the visible surface of the print, so that
// EReducedDetailMode::ShellOnly can leave out everything the walls hide. Each layer is rasterized
// into a coarse occupancy grid and closed, so that its footprint is solid whatever the infill;
// a cell is then on the shell when it is filled and any of its six neighbours (four in the layer,
// the layer below, the layer above) is not. A segment is kept when at least half of the cells it
// crosses are shell cells: walls run along the shell, infill only touches it at the ends. Purely
// geometric, so it works as well for the wipe tower, whose every segment shares one role, as for
// the objects.
// The same pass records the highest and lowest layer occupying each cell over the whole print,
// which tells the segments that are the topmost or bottommost thing at their place, the only ones
// a view from above or below sees of a layer. Exposure to the neighbouring layer alone would also
// count whatever sits under an overhang, and the edge of a tower whose footprint lands a cell
// differently from one layer to the next.
void ViewerImpl::update_shell_bitset()
{
m_shell_bitset = BitSet<>(m_vertices.size());
m_near_shell_bitset = BitSet<>(m_vertices.size());
m_top_visible_bitset = BitSet<>(m_vertices.size());
m_bottom_visible_bitset = BitSet<>(m_vertices.size());
if (m_vertices.size() < 2 || m_layers.empty())
return;
float min_x = FLT_MAX;
float min_y = FLT_MAX;
float max_x = -FLT_MAX;
float max_y = -FLT_MAX;
for (const PathVertex& v : m_vertices) {
if (!v.is_extrusion())
continue;
min_x = std::min(min_x, v.position[0]);
min_y = std::min(min_y, v.position[1]);
max_x = std::max(max_x, v.position[0]);
max_y = std::max(max_y, v.position[1]);
}
if (min_x > max_x)
return;
// Half a millimetre separates a wall from the wall behind it; a print too large for that at
// 1024 cells across gets coarser cells rather than a bigger grid. Gaps of up to 5 mm read as
// solid: wide enough to swallow sparse infill, narrow enough to leave real holes open.
static constexpr int MAX_CELLS = 1024;
const float cell = std::max(0.5f, std::max(max_x - min_x, max_y - min_y) / static_cast<float>(MAX_CELLS));
const int radius = static_cast<int>(std::ceil(2.5f / cell));
// room for the closing to grow into, plus the neighbour lookups
const int margin = radius + 2;
const float origin_x = min_x - static_cast<float>(margin) * cell;
const float origin_y = min_y - static_cast<float>(margin) * cell;
const int nx = static_cast<int>((max_x - min_x) / cell) + 1 + 2 * margin;
const int ny = static_cast<int>((max_y - min_y) / cell) + 1 + 2 * margin;
const auto cell_index = [nx](int x, int y) { return static_cast<size_t>(y) * nx + x; };
const auto cell_of = [&](float x, float y) {
const int cx = std::clamp(static_cast<int>((x - origin_x) / cell), margin, nx - 1 - margin);
const int cy = std::clamp(static_cast<int>((y - origin_y) / cell), margin, ny - 1 - margin);
return std::make_pair(cx, cy);
};
// calls f(cx, cy) once per cell the segment starting at vertex i passes through
const auto for_each_cell = [&](size_t i, auto&& f) {
const Vec3& a = m_vertices[i].position;
const Vec3& b = m_vertices[i + 1].position;
const float dx = b[0] - a[0];
const float dy = b[1] - a[1];
const int steps = static_cast<int>(std::sqrt(dx * dx + dy * dy) / (0.5f * cell)) + 1;
int last_x = -1;
int last_y = -1;
for (int s = 0; s <= steps; ++s) {
const float t = static_cast<float>(s) / static_cast<float>(steps);
const auto [cx, cy] = cell_of(a[0] + t * dx, a[1] + t * dy);
if (cx != last_x || cy != last_y) {
f(cx, cy);
last_x = cx;
last_y = cy;
}
}
};
const size_t layers_count = m_layers.count();
// the segments of a layer: [first, last), where segment i runs from vertex i to vertex i + 1
const auto layer_segments = [&](size_t layer) {
const size_t first = m_layer_first_vertex[layer];
const size_t last = (layer + 1 < layers_count) ? m_layer_first_vertex[layer + 1] : m_vertices.size() - 1;
return std::make_pair(first, std::min(last, m_vertices.size() - 1));
};
const auto is_drawn_extrusion = [&](size_t i) { return m_vertices[i].is_extrusion() && m_valid_lines_bitset[i]; };
const OccupancyGrid nothing(nx, ny);
// Classifies the layers in [first_layer, last_layer) and returns the segments kept, plus the
// highest and lowest of these layers occupying each cell. Each call owns its grids, so the layer
// range can be split across threads.
static constexpr int32_t NO_LAYER = -1;
struct Kept {
std::vector<uint32_t> shell;
std::vector<uint32_t> near_shell;
std::vector<int32_t> top;
std::vector<int32_t> bottom;
// the rectangle of cells these layers touched, inclusive; empty while min > max
int min_x{ 0 };
int min_y{ 0 };
int max_x{ -1 };
int max_y{ -1 };
};
const size_t cells_count = static_cast<size_t>(nx) * static_cast<size_t>(ny);
const auto classify_layers = [&](size_t first_layer, size_t last_layer) {
Kept kept;
kept.top.assign(cells_count, NO_LAYER);
kept.bottom.assign(cells_count, NO_LAYER);
std::vector<OccupancyGrid> footprints(3, OccupancyGrid(nx, ny));
OccupancyGrid shell_cells(nx, ny);
// the outer wall segments of the layer, by every cell they cross
std::unordered_map<size_t, std::vector<uint32_t>> outer_walls_by_cell;
ClosingScratch scratch;
const auto footprint = [&](size_t layer) -> OccupancyGrid& { return footprints[layer % 3]; };
const auto prepare = [&](size_t layer) {
OccupancyGrid& g = footprint(layer);
g.clear();
const auto [first, last] = layer_segments(layer);
for (size_t i = first; i < last; ++i) {
if (is_drawn_extrusion(i))
for_each_cell(i, [&](int x, int y) { g.set(x, y); });
}
close_gaps(g, radius, scratch);
};
if (first_layer > 0)
prepare(first_layer - 1);
prepare(first_layer);
for (size_t layer = first_layer; layer < last_layer; ++layer) {
if (layer + 1 < layers_count)
prepare(layer + 1);
const OccupancyGrid& below = (layer > 0) ? footprint(layer - 1) : nothing;
const OccupancyGrid& cur = footprint(layer);
const OccupancyGrid& above = (layer + 1 < layers_count) ? footprint(layer + 1) : nothing;
shell_cells.clear();
if (!cur.empty()) {
kept.min_x = (kept.max_x < kept.min_x) ? cur.min_x : std::min(kept.min_x, cur.min_x);
kept.min_y = (kept.max_y < kept.min_y) ? cur.min_y : std::min(kept.min_y, cur.min_y);
kept.max_x = std::max(kept.max_x, cur.max_x);
kept.max_y = std::max(kept.max_y, cur.max_y);
}
for (int y = cur.min_y; y <= cur.max_y; ++y) {
for (int x = cur.min_x; x <= cur.max_x; ++x) {
if (!cur.at(x, y))
continue;
// layers come in ascending order, so the first occupant is the lowest
int32_t& top = kept.top[cell_index(x, y)];
int32_t& bottom = kept.bottom[cell_index(x, y)];
top = static_cast<int32_t>(layer);
if (bottom == NO_LAYER)
bottom = static_cast<int32_t>(layer);
if (!below.at(x, y) || !above.at(x, y) ||
!cur.at(x - 1, y) || !cur.at(x + 1, y) || !cur.at(x, y - 1) || !cur.at(x, y + 1))
shell_cells.set(x, y);
}
}
const auto [first, last] = layer_segments(layer);
outer_walls_by_cell.clear();
for (size_t i = first; i < last; ++i) {
const EGCodeExtrusionRole role = m_vertices[i].role;
if (is_drawn_extrusion(i) && (role == EGCodeExtrusionRole::ExternalPerimeter || role == EGCodeExtrusionRole::OverhangPerimeter))
for_each_cell(i, [&](int x, int y) { outer_walls_by_cell[cell_index(x, y)].push_back(static_cast<uint32_t>(i)); });
}
// an inner wall segment is the first inner wall when its midpoint lies within a line
// and a half of an outer wall segment of the same layer
const auto beside_outer_wall = [&](size_t i) {
const Vec3& a = m_vertices[i].position;
const Vec3& b = m_vertices[i + 1].position;
const float mx = 0.5f * (a[0] + b[0]);
const float my = 0.5f * (a[1] + b[1]);
const float reach = 1.5f * m_vertices[i].width;
const auto [cx, cy] = cell_of(mx, my);
for (int dy = -1; dy <= 1; ++dy) {
for (int dx = -1; dx <= 1; ++dx) {
const auto it = outer_walls_by_cell.find(cell_index(cx + dx, cy + dy));
if (it == outer_walls_by_cell.end())
continue;
for (uint32_t o : it->second) {
const Vec3& p = m_vertices[o].position;
const Vec3& q = m_vertices[o + 1].position;
const float ex = q[0] - p[0];
const float ey = q[1] - p[1];
const float len2 = ex * ex + ey * ey;
const float t = (len2 > 0.0f) ? std::clamp(((mx - p[0]) * ex + (my - p[1]) * ey) / len2, 0.0f, 1.0f) : 0.0f;
const float ddx = mx - (p[0] + t * ex);
const float ddy = my - (p[1] + t * ey);
if (ddx * ddx + ddy * ddy <= reach * reach)
return true;
}
}
}
return false;
};
for (size_t i = first; i < last; ++i) {
if (!is_drawn_extrusion(i))
continue;
int total = 0;
int on_shell = 0;
for_each_cell(i, [&](int x, int y) {
++total;
on_shell += shell_cells.at(x, y);
});
if (2 * on_shell >= total)
kept.shell.push_back(static_cast<uint32_t>(i));
if (m_vertices[i].role == EGCodeExtrusionRole::Perimeter && beside_outer_wall(i))
kept.near_shell.push_back(static_cast<uint32_t>(i));
}
}
return kept;
};
const size_t workers = std::clamp<size_t>(std::thread::hardware_concurrency(), 1, 8);
const size_t chunk = std::max<size_t>(16, (layers_count + workers - 1) / workers);
std::vector<std::future<Kept>> futures;
for (size_t first = 0; first < layers_count; first += chunk)
futures.emplace_back(std::async(std::launch::async, classify_layers, first, std::min(layers_count, first + chunk)));
std::vector<int32_t> top_layer(cells_count, NO_LAYER);
std::vector<int32_t> bottom_layer(cells_count, NO_LAYER);
for (auto& f : futures) {
const Kept kept = f.get();
for (uint32_t i : kept.shell)
m_shell_bitset.set(i);
for (uint32_t i : kept.near_shell)
m_near_shell_bitset.set(i);
for (int y = kept.min_y; y <= kept.max_y; ++y) {
for (int x = kept.min_x; x <= kept.max_x; ++x) {
const size_t c = cell_index(x, y);
if (kept.top[c] == NO_LAYER)
continue;
top_layer[c] = std::max(top_layer[c], kept.top[c]);
bottom_layer[c] = (bottom_layer[c] == NO_LAYER) ? kept.bottom[c] : std::min(bottom_layer[c], kept.bottom[c]);
}
}
}
// A segment is visible from straight above when its layer is the topmost occupant of any of its
// cells, and from below likewise with the bottommost: the exposed band of a sloped surface is
// narrower than the infill chords that fill it, so touching it is what counts.
struct Visible { std::vector<uint32_t> top; std::vector<uint32_t> bottom; };
const auto find_visible = [&](size_t first_layer, size_t last_layer) {
Visible visible;
for (size_t layer = first_layer; layer < last_layer; ++layer) {
const auto [first, last] = layer_segments(layer);
for (size_t i = first; i < last; ++i) {
if (!is_drawn_extrusion(i))
continue;
int total = 0;
int on_top = 0;
int on_bottom = 0;
for_each_cell(i, [&](int x, int y) {
++total;
on_top += top_layer[cell_index(x, y)] == static_cast<int32_t>(layer);
on_bottom += bottom_layer[cell_index(x, y)] == static_cast<int32_t>(layer);
});
if (on_top > 0)
visible.top.push_back(static_cast<uint32_t>(i));
if (on_bottom > 0)
visible.bottom.push_back(static_cast<uint32_t>(i));
}
}
return visible;
};
std::vector<std::future<Visible>> visible_futures;
for (size_t first = 0; first < layers_count; first += chunk)
visible_futures.emplace_back(std::async(std::launch::async, find_visible, first, std::min(layers_count, first + chunk)));
for (auto& f : visible_futures) {
const Visible visible = f.get();
for (uint32_t i : visible.top)
m_top_visible_bitset.set(i);
for (uint32_t i : visible.bottom)
m_bottom_visible_bitset.set(i);
}
}
#endif // ENABLE_OPENGL_ES
void ViewerImpl::update_enabled_entities()
{
if (m_vertices.empty())
@@ -1696,26 +1134,6 @@ void ViewerImpl::update_enabled_entities()
std::vector<uint32_t> enabled_segments;
std::vector<uint32_t> enabled_options;
#ifndef ENABLE_OPENGL_ES
// ORCA: the reduced sets are filled by the same walk, so switching to them costs no rebuild.
const bool build_reduced = m_settings.reduced_detail_mode != EReducedDetailMode::Off;
const bool build_rest = build_rest_set();
std::vector<uint32_t> enabled_segments_reduced;
std::vector<uint32_t> enabled_options_reduced;
std::vector<uint32_t> enabled_segments_rest;
const uint32_t layer_stride = std::max<uint32_t>(1, m_settings.reduced_detail_layer_stride);
// Only the shell mode knows which segments are exposed surfaces, so only it can skip layers at
// rest, and in either set only it can keep the surfaces of the layers it skips.
const bool shell_reduced = build_reduced && m_settings.reduced_detail_mode == EReducedDetailMode::ShellOnly;
const bool shell_rest = build_rest && m_settings.rest_detail_mode == EReducedDetailMode::ShellOnly;
const uint32_t rest_stride = shell_rest ? std::max<uint32_t>(1, m_settings.rest_layer_stride) : 1;
// Whatever else is dropped, both ends of the visible layer range are kept whole: the top is the
// surface the user is looking at, and the only layer drawn at full color in top-layer-only
// mode; the bottom is exposed whenever the range is cut short.
const Interval& layers_range = m_layers.get_view_range();
if ((shell_reduced || shell_rest) && m_shell_bitset.size != m_vertices.size())
update_shell_bitset();
#endif // ENABLE_OPENGL_ES
Interval range = m_view_range.get_visible();
// when top layer only visualization is enabled, we need to render
@@ -1763,38 +1181,6 @@ void ViewerImpl::update_enabled_entities()
enabled_options.push_back(static_cast<uint32_t>(i));
else
enabled_segments.push_back(static_cast<uint32_t>(i));
#ifndef ENABLE_OPENGL_ES
const bool whole_layer = v.layer_id == layers_range[0] || v.layer_id == layers_range[1];
const bool keep_anyway = whole_layer || !v.is_extrusion();
const bool classified = v.is_extrusion() && m_top_visible_bitset.size == m_vertices.size();
const bool visible_from_above = classified && m_top_visible_bitset[i];
const bool visible_from_below = classified && m_bottom_visible_bitset[i];
if (build_rest && !v.is_option()) {
const bool skipped = !whole_layer && rest_stride > 1 && (v.layer_id % rest_stride) != 0;
// a skipped layer keeps only what the camera's side of the print can see of it
const bool visible = m_settings.rest_view_from_above ? visible_from_above : visible_from_below;
if (skipped ? visible : (keep_anyway || reduced_set_keeps(m_settings.rest_detail_mode, i, v)))
enabled_segments_rest.push_back(static_cast<uint32_t>(i));
}
if (!build_reduced)
continue;
if (m_settings.reduced_detail_mode == EReducedDetailMode::EndLayersOnly) {
if (whole_layer)
(v.is_option() ? enabled_options_reduced : enabled_segments_reduced).push_back(static_cast<uint32_t>(i));
continue;
}
if (!whole_layer && (v.layer_id % layer_stride) != 0) {
// the surfaces of a skipped layer that either side can see stay, so that a step does not vanish
if (shell_reduced && (visible_from_above || visible_from_below))
enabled_segments_reduced.push_back(static_cast<uint32_t>(i));
continue;
}
if (v.is_option())
enabled_options_reduced.push_back(static_cast<uint32_t>(i));
else if (keep_anyway || reduced_set_keeps(m_settings.reduced_detail_mode, i, v))
enabled_segments_reduced.push_back(static_cast<uint32_t>(i));
#endif // ENABLE_OPENGL_ES
}
#ifdef ENABLE_OPENGL_ES
@@ -1823,34 +1209,10 @@ void ViewerImpl::update_enabled_entities()
else
glsafe(glBufferData(GL_TEXTURE_BUFFER, 0, nullptr, GL_STATIC_DRAW));
m_enabled_segments_reduced_count = enabled_segments_reduced.size();
m_enabled_options_reduced_count = enabled_options_reduced.size();
m_enabled_segments_rest_count = enabled_segments_rest.size();
if (build_rest) {
assert(m_enabled_segments_rest_buf_id > 0);
glsafe(glBindBuffer(GL_TEXTURE_BUFFER, m_enabled_segments_rest_buf_id));
glsafe(glBufferData(GL_TEXTURE_BUFFER, enabled_segments_rest.size() * sizeof(uint32_t),
enabled_segments_rest.empty() ? nullptr : enabled_segments_rest.data(), GL_STATIC_DRAW));
}
if (build_reduced) {
assert(m_enabled_segments_reduced_buf_id > 0);
glsafe(glBindBuffer(GL_TEXTURE_BUFFER, m_enabled_segments_reduced_buf_id));
glsafe(glBufferData(GL_TEXTURE_BUFFER, enabled_segments_reduced.size() * sizeof(uint32_t),
enabled_segments_reduced.empty() ? nullptr : enabled_segments_reduced.data(), GL_STATIC_DRAW));
assert(m_enabled_options_reduced_buf_id > 0);
glsafe(glBindBuffer(GL_TEXTURE_BUFFER, m_enabled_options_reduced_buf_id));
glsafe(glBufferData(GL_TEXTURE_BUFFER, enabled_options_reduced.size() * sizeof(uint32_t),
enabled_options_reduced.empty() ? nullptr : enabled_options_reduced.data(), GL_STATIC_DRAW));
}
glsafe(glBindBuffer(GL_TEXTURE_BUFFER, 0));
#endif // ENABLE_OPENGL_ES
m_settings.update_enabled_entities = false;
++m_state_version;
}
static float encode_color(const Color& color) {
@@ -1899,10 +1261,7 @@ void ViewerImpl::update_colors_texture()
// Based on current settings and slider position, we might want to render some
// vertices as dark grey (or darkened, see above). Use either that or the normal color (from the cache).
// Reused across calls: this runs on every slider tick, and the allocation alone is
// 4 bytes per vertex of the whole print each time.
std::vector<float>& colors = m_colors_scratch;
colors.resize(m_vertices_colors.size());
std::vector<float> colors(m_vertices_colors.size());
assert(colors.size() == m_vertices.size() && m_vertices_colors.size() == m_vertices.size());
for (size_t i=0; i<m_vertices.size(); ++i) {
const PathVertex& v = m_vertices[i];
@@ -1960,7 +1319,6 @@ void ViewerImpl::update_colors()
update_colors_texture();
m_settings.update_colors = false;
++m_state_version;
}
void ViewerImpl::render(const Mat4x4& view_matrix, const Mat4x4& projection_matrix)
@@ -2026,54 +1384,6 @@ void ViewerImpl::toggle_top_layer_only_view_range()
update_colors_texture();
}
// ORCA: what the reduced set leaves out, and how many layers it keeps one of. Either changes which
// vertices land in the reduced set, so the sets have to be rebuilt.
void ViewerImpl::set_reduced_detail_mode(EReducedDetailMode mode)
{
if (m_settings.reduced_detail_mode == mode)
return;
m_settings.reduced_detail_mode = mode;
m_settings.update_enabled_entities = true;
}
void ViewerImpl::set_reduced_detail_layer_stride(uint32_t value)
{
value = std::max<uint32_t>(1, value);
if (m_settings.reduced_detail_layer_stride == value)
return;
m_settings.reduced_detail_layer_stride = value;
m_settings.update_enabled_entities = true;
}
void ViewerImpl::set_rest_detail_mode(EReducedDetailMode mode)
{
if (m_settings.rest_detail_mode == mode)
return;
m_settings.rest_detail_mode = mode;
m_settings.update_enabled_entities = true;
}
void ViewerImpl::set_rest_layer_stride(uint32_t value)
{
value = std::max<uint32_t>(1, value);
if (m_settings.rest_layer_stride == value)
return;
m_settings.rest_layer_stride = value;
// only the shell rest set is built from it
if (m_settings.rest_detail_mode == EReducedDetailMode::ShellOnly)
m_settings.update_enabled_entities = true;
}
void ViewerImpl::set_rest_view_from_above(bool value)
{
if (m_settings.rest_view_from_above == value)
return;
m_settings.rest_view_from_above = value;
// it only matters while the shell rest set skips layers
if (m_settings.rest_detail_mode == EReducedDetailMode::ShellOnly && m_settings.rest_layer_stride > 1)
m_settings.update_enabled_entities = true;
}
// ORCA: enable/disable darkening of the layers the layer slider is not scrubbed to
void ViewerImpl::set_dim_previous_layers(bool value)
{
@@ -2206,10 +1516,8 @@ void ViewerImpl::set_view_visible_range(Interval::value_type min, Interval::valu
float ViewerImpl::get_estimated_time_at(size_t id) const
{
const size_t mode = static_cast<size_t>(m_settings.time_mode);
if (mode >= TIME_MODES_COUNT || id >= m_cumulative_times[mode].size())
return 0.0f;
return m_cumulative_times[mode][id];
return std::accumulate(m_vertices.begin(), m_vertices.begin() + id + 1, 0.0f,
[this](float a, const PathVertex& v) { return a + v.times[static_cast<size_t>(m_settings.time_mode)]; });
}
Color ViewerImpl::get_vertex_color(const PathVertex& v) const
@@ -2414,10 +1722,6 @@ size_t ViewerImpl::get_used_cpu_memory() const
ret += sizeof(m_extrusion_roles_colors);
ret += sizeof(m_options_colors);
ret += STDVEC_MEMSIZE(m_vertices, PathVertex);
for (const std::vector<float>& times : m_cumulative_times)
ret += STDVEC_MEMSIZE(times, float);
ret += STDVEC_MEMSIZE(m_layer_first_vertex, uint32_t);
ret += STDVEC_MEMSIZE(m_colors_scratch, float);
ret += m_valid_lines_bitset.size_in_bytes_cpu();
ret += m_height_range.size_in_bytes_cpu();
ret += m_width_range.size_in_bytes_cpu();
@@ -2483,13 +1787,7 @@ void ViewerImpl::update_view_full_range()
const bool travels_visible = m_settings.options_visibility[size_t(EOptionType::Travels)];
const bool wipes_visible = m_settings.options_visibility[size_t(EOptionType::Wipes)];
// Every vertex before m_layer_first_vertex[layers_range[0]] has a smaller layer_id, so the
// loop below would skip all of them on its first condition alone. Starting there turns a scan
// from vertex 0 on every slider tick into a scan of the visible part only; what the loop
// settles on is unchanged.
auto first_it = m_vertices.begin();
if (layers_range[0] < m_layer_first_vertex.size())
first_it += m_layer_first_vertex[layers_range[0]];
while (first_it != m_vertices.end() &&
(first_it->layer_id < layers_range[0] || !is_visible(*first_it, m_settings))) {
++first_it;
@@ -2570,7 +1868,6 @@ void ViewerImpl::update_view_full_range()
}
m_settings.update_view_full_range = false;
++m_state_version;
}
void ViewerImpl::update_color_ranges()
@@ -2677,7 +1974,7 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec
#ifdef ENABLE_OPENGL_ES
if (m_texture_data.get_enabled_segments_count() == 0)
#else
if (active_segments_count() == 0)
if (m_enabled_segments_count == 0)
#endif // ENABLE_OPENGL_ES
return;
@@ -2697,7 +1994,6 @@ 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()));
glsafe(glUniform1f(m_uni_segments_height_scale_id, active_height_scale()));
glsafe(glDisable(GL_CULL_FACE));
@@ -2737,10 +2033,10 @@ void ViewerImpl::render_segments(const Mat4x4& view_matrix, const Mat4x4& projec
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_colors_tex_id));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32F, m_colors_buf_id));
glsafe(glActiveTexture(GL_TEXTURE3));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, active_segments_tex_id()));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, active_segments_buf_id()));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_segments_tex_id));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, m_enabled_segments_buf_id));
m_segment_template.render(active_segments_count());
m_segment_template.render(m_enabled_segments_count);
#endif // ENABLE_OPENGL_ES
if (curr_cull_face)
@@ -2766,7 +2062,7 @@ void ViewerImpl::render_options(const Mat4x4& view_matrix, const Mat4x4& project
#ifdef ENABLE_OPENGL_ES
if (m_texture_data.get_enabled_options_count() == 0)
#else
if (active_options_count() == 0)
if (m_enabled_options_count == 0)
#endif // ENABLE_OPENGL_ES
return;
@@ -2824,10 +2120,10 @@ void ViewerImpl::render_options(const Mat4x4& view_matrix, const Mat4x4& project
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_colors_tex_id));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32F, m_colors_buf_id));
glsafe(glActiveTexture(GL_TEXTURE3));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, active_options_tex_id()));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, active_options_buf_id()));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, m_enabled_options_tex_id));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, m_enabled_options_buf_id));
m_option_template.render(active_options_count());
m_option_template.render(m_enabled_options_count);
#endif // ENABLE_OPENGL_ES
if (!curr_cull_face)

View File

@@ -91,31 +91,6 @@ public:
// 0.0 = black
bool is_dim_previous_layers() const { return m_settings.dim_previous_layers; }
void set_dim_previous_layers(bool value);
//
// Draw the preview from the reduced set of entities. Meant to be held only while the user is
// dragging; the sets are already built, so this is just a buffer binding and never rebuilds.
//
void set_reduced_detail(bool value) {
if (m_settings.reduced_detail != value) {
m_settings.reduced_detail = value;
++m_state_version;
}
}
bool is_reduced_detail() const { return m_settings.reduced_detail; }
EReducedDetailMode get_reduced_detail_mode() const { return m_settings.reduced_detail_mode; }
void set_reduced_detail_mode(EReducedDetailMode mode);
uint32_t get_reduced_detail_layer_stride() const { return m_settings.reduced_detail_layer_stride; }
void set_reduced_detail_layer_stride(uint32_t value);
EReducedDetailMode get_rest_detail_mode() const { return m_settings.rest_detail_mode; }
void set_rest_detail_mode(EReducedDetailMode mode);
uint32_t get_rest_layer_stride() const { return m_settings.rest_layer_stride; }
void set_rest_layer_stride(uint32_t value);
bool is_rest_view_from_above() const { return m_settings.rest_view_from_above; }
void set_rest_view_from_above(bool value);
uint64_t get_state_version() const { return m_state_version; }
bool has_pending_updates() const {
return m_settings.update_view_full_range || m_settings.update_enabled_entities || m_settings.update_colors;
}
float get_dim_previous_layers_brightness() const { return m_settings.dim_previous_layers_brightness; }
void set_dim_previous_layers_brightness(float value);
@@ -259,27 +234,6 @@ private:
//
std::array<float, TIME_MODES_COUNT> m_total_time{ 0.0f, 0.0f };
//
// Running sum of the vertex estimated times, one entry per vertex for each time mode.
// get_estimated_time_at() answers from this instead of re-accumulating the whole vertex
// array, which it was doing once per frame from the tool marker tooltip. The sums are
// built by the same left-to-right addition the accumulate performed, so the value handed
// back is bit-identical, float rounding included.
//
std::array<std::vector<float>, TIME_MODES_COUNT> m_cumulative_times;
//
// For each layer L, the index of the first vertex whose layer_id is >= L (m_vertices.size()
// if there is none). Every vertex before it is guaranteed to belong to an earlier layer, so
// the scan in update_view_full_range() can start there rather than at vertex 0. Derived from
// the vertices themselves rather than from Layers, which buckets an out-of-order vertex into
// the layer that happens to be open, so this stays exact whatever order the vertices arrive in.
//
std::vector<uint32_t> m_layer_first_vertex;
//
// Scratch buffer for update_colors_texture(), kept alive so that a slider drag does not
// allocate and free one float per vertex of the print on every step.
//
std::vector<float> m_colors_scratch;
//
// Detected travel moves times
//
std::array<float, TIME_MODES_COUNT> m_travels_time{ 0.0f, 0.0f };
@@ -334,26 +288,6 @@ private:
// Variables used for toolpaths visibiliity
//
BitSet<> m_valid_lines_bitset;
#ifndef ENABLE_OPENGL_ES
//
// ORCA: bit set for the extrusion segments that lie on the visible surface of the print,
// computed on demand by update_shell_bitset() for EReducedDetailMode::ShellOnly
//
BitSet<> m_shell_bitset;
// the segments that are the topmost, or the bottommost, thing at their place in the whole
// print: what a view from above, or below, sees of a layer, kept even while layers are skipped
BitSet<> m_top_visible_bitset;
BitSet<> m_bottom_visible_bitset;
// the inner wall segments that run right beside an outer wall: the first inner wall, which
// fills the step of a sloped surface between one layer's outer wall and the next, too narrow
// for the grid to see
BitSet<> m_near_shell_bitset;
//
// ORCA: bumped whenever what render() draws changes: on every applied update, on a change of
// the bound set, on load and reset
//
uint64_t m_state_version{ 0 };
#endif // ENABLE_OPENGL_ES
//
// Variables used for toolpaths coloring
//
@@ -392,7 +326,6 @@ private:
int m_uni_segments_view_matrix_id{ -1 };
int m_uni_segments_projection_matrix_id{ -1 };
int m_uni_segments_camera_position_id{ -1 };
int m_uni_segments_height_scale_id{ -1 };
int m_uni_segments_positions_tex_id{ -1 };
int m_uni_segments_height_width_angle_tex_id{ -1 };
int m_uni_segments_colors_tex_id{ -1 };
@@ -527,22 +460,6 @@ private:
unsigned int m_enabled_options_tex_id{ 0 };
size_t m_enabled_options_count{ 0 };
//
// OpenGL buffers to store the reduced sets drawn while Settings::reduced_detail is set
//
unsigned int m_enabled_segments_reduced_buf_id{ 0 };
unsigned int m_enabled_segments_reduced_tex_id{ 0 };
size_t m_enabled_segments_reduced_count{ 0 };
unsigned int m_enabled_options_reduced_buf_id{ 0 };
unsigned int m_enabled_options_reduced_tex_id{ 0 };
size_t m_enabled_options_reduced_count{ 0 };
//
// OpenGL buffer to store the segments drawn at rest while Settings::rest_detail_mode is not Off.
// Every layer is drawn at rest, so the options are the full set and need no counterpart.
//
unsigned int m_enabled_segments_rest_buf_id{ 0 };
unsigned int m_enabled_segments_rest_tex_id{ 0 };
size_t m_enabled_segments_rest_count{ 0 };
//
// Caches for size of data sent to gpu, in bytes
//
size_t m_positions_tex_size{ 0 };
@@ -550,49 +467,6 @@ private:
size_t m_colors_tex_size{ 0 };
size_t m_enabled_segments_tex_size{ 0 };
size_t m_enabled_options_tex_size{ 0 };
// The set the next draw reads from: the reduced one only while the user is dragging, and only
// if a reduced set is being built at all; otherwise the rest set, if one is being built. A rest
// set already smaller than the reduced one, as it is with layers merged looking from above,
// stays bound through the drag: it was right for the camera the drag started from.
bool build_rest_set() const {
return m_settings.rest_detail_mode != EReducedDetailMode::Off && m_settings.rest_detail_mode != EReducedDetailMode::LayersOnly;
}
bool use_reduced_set() const {
return m_settings.reduced_detail && m_settings.reduced_detail_mode != EReducedDetailMode::Off &&
!(build_rest_set() && m_enabled_segments_rest_count < m_enabled_segments_reduced_count);
}
bool use_rest_set() const { return !use_reduced_set() && build_rest_set(); }
// how many layers each drawn segment of the bound set stands in for
float active_height_scale() const {
if (use_reduced_set() && m_settings.reduced_detail_mode != EReducedDetailMode::EndLayersOnly)
return static_cast<float>(std::max<uint32_t>(1, m_settings.reduced_detail_layer_stride));
if (use_rest_set() && m_settings.rest_detail_mode == EReducedDetailMode::ShellOnly)
return static_cast<float>(std::max<uint32_t>(1, m_settings.rest_layer_stride));
return 1.0f;
}
size_t active_segments_count() const {
return use_reduced_set() ? m_enabled_segments_reduced_count : use_rest_set() ? m_enabled_segments_rest_count : m_enabled_segments_count;
}
unsigned int active_segments_buf_id() const {
return use_reduced_set() ? m_enabled_segments_reduced_buf_id : use_rest_set() ? m_enabled_segments_rest_buf_id : m_enabled_segments_buf_id;
}
unsigned int active_segments_tex_id() const {
return use_reduced_set() ? m_enabled_segments_reduced_tex_id : use_rest_set() ? m_enabled_segments_rest_tex_id : m_enabled_segments_tex_id;
}
size_t active_options_count() const {
return use_reduced_set() ? m_enabled_options_reduced_count : m_enabled_options_count;
}
unsigned int active_options_buf_id() const {
return use_reduced_set() ? m_enabled_options_reduced_buf_id : m_enabled_options_buf_id;
}
unsigned int active_options_tex_id() const {
return use_reduced_set() ? m_enabled_options_reduced_tex_id : m_enabled_options_tex_id;
}
// Whether the segment starting at vertex i belongs to the set built under the given mode
bool reduced_set_keeps(EReducedDetailMode mode, size_t i, const PathVertex& v) const;
void update_shell_bitset();
#endif // ENABLE_OPENGL_ES
void update_view_full_range();

View File

@@ -1172,29 +1172,6 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
if (current_top_layer_only != required_top_layer_only)
m_viewer.toggle_top_layer_only_view_range();
// ORCA: simplify the preview while the user is dragging
m_reduced_detail_while_dragging = get_app_config()->get_bool("preview_reduced_detail_while_dragging");
m_reduced_detail_mode = reduced_detail_mode_from_string(get_app_config()->get("preview_reduced_detail_mode"));
m_solid_model_while_dragging = m_reduced_detail_mode == libvgcode::EReducedDetailMode::EndLayersOnly;
m_reduced_detail_layer_stride = static_cast<unsigned int>(std::max(1, std::stoi(get_app_config()->get("preview_reduced_detail_layer_stride"))));
m_rest_detail_mode = reduced_detail_mode_from_string(get_app_config()->get("preview_rest_detail_mode"));
apply_reduced_detail_settings();
++m_scene_version;
// the median z step between layers, robust to the first layer and to variable layer height
{
std::vector<float> steps;
for (size_t i = 1; i < m_viewer.get_layers_count(); ++i) {
const float step = m_viewer.get_layer_z(i) - m_viewer.get_layer_z(i - 1);
if (step > 0.0f)
steps.push_back(step);
}
m_typical_layer_height = 0.0f;
if (!steps.empty()) {
std::nth_element(steps.begin(), steps.begin() + steps.size() / 2, steps.end());
m_typical_layer_height = steps[steps.size() / 2];
}
}
// ORCA: darken the layers the preview layer slider is not scrubbed to
m_viewer.set_dim_previous_layers(get_app_config()->get_bool("preview_dim_previous_layers"));
m_viewer.set_dim_previous_layers_brightness(0.01f * std::stoi(get_app_config()->get("preview_dim_previous_layers_brightness")));
@@ -1596,21 +1573,11 @@ void GCodeViewer::load_as_preview(libvgcode::GCodeInputData&& data)
void GCodeViewer::update_shells_color_by_extruder(const DynamicPrintConfig *config)
{
++m_scene_version;
if (config != nullptr)
m_shells.volumes.update_colors_by_extruder(config, false);
}
void GCodeViewer::set_shell_transparency(float alpha)
{
m_shells.volumes.set_transparency(alpha);
++m_scene_version;
}
std::array<uint64_t, 2> GCodeViewer::scene_version() const
{
return { (m_scene_version << 2) | (m_shells.visible ? 1u : 0u) | (m_no_render_path ? 2u : 0u), m_viewer.get_state_version() };
}
void GCodeViewer::set_shell_transparency(float alpha) { m_shells.volumes.set_transparency(alpha); }
//BBS: always load shell at preview
void GCodeViewer::reset_shell()
@@ -1622,7 +1589,6 @@ void GCodeViewer::reset_shell()
void GCodeViewer::reset()
{
++m_scene_version;
//BBS: should also reset the result id
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": current result id %1% ")%m_last_result_id;
m_last_result_id = -1;
@@ -1651,26 +1617,15 @@ void GCodeViewer::reset()
}
//BBS: GUI refactor: add canvas width and height
void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin, bool draw_scene)
void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
{
glsafe(::glEnable(GL_DEPTH_TEST));
// while dragging in the solid model mode the objects stand in for their toolpaths, cut to the
// visible layer range; the toolpath set then holds only the range's bottom and top layers
const bool solid_model = m_interacting && m_solid_model_while_dragging && m_viewer.is_reduced_detail();
if (draw_scene) {
if (solid_model)
render_solid_model(canvas_width, canvas_height);
else
render_shells(canvas_width, canvas_height);
}
render_shells(canvas_width, canvas_height);
if (m_viewer.get_extrusion_roles_count() == 0)
if (m_viewer.get_extrusion_roles().empty())
return;
update_rest_layer_stride();
if (draw_scene)
render_toolpaths();
render_toolpaths();
float legend_height = 0.0f;
render_legend(legend_height, canvas_width, canvas_height, right_margin);
@@ -1952,92 +1907,6 @@ void GCodeViewer::update_layers_slider_mode()
// TODO m_layers_slider->SetModeAndOnlyExtruder(one_extruder_printed_model, only_extruder);
}
void GCodeViewer::set_interacting(bool interacting)
{
m_interacting = interacting;
m_viewer.set_reduced_detail(m_reduced_detail_while_dragging && interacting);
}
// ORCA: with the shell drawn at rest, layers thinner than a couple of pixels on screen are merged:
// the walls of one layer in N are drawn N layers tall, which looks the same and costs 1/N. N follows
// the view, from 1 side-on and zoomed in to the cap looking straight down, where the walls are edge-on
// and every layer's exposed surfaces are all there is to see. Changing N rebuilds the sets, so it is
// left alone while the user is dragging.
void GCodeViewer::update_rest_layer_stride()
{
if (m_interacting || m_rest_detail_mode != libvgcode::EReducedDetailMode::ShellOnly || m_typical_layer_height <= 0.0f)
return;
static constexpr double MERGE_BELOW_PX = 2.0;
static constexpr unsigned int MAX_STRIDE = 64;
Camera& camera = wxGetApp().plater()->get_camera();
// how tall one layer is on screen: a step of one layer height projected through the camera's
// own matrices at the centre of the toolpaths and at the point the camera looks at, whichever
// is taller. Perspective makes the near corners of a tall print larger than that, but merging
// is judged where the user looks, not at the worst corner.
const Matrix4d view_projection = camera.get_projection_matrix().matrix() * camera.get_view_matrix().matrix();
const std::array<int, 4>& viewport = camera.get_viewport();
const auto to_pixels = [&](const Vec3d& p) {
const Vec4d clip = view_projection * Vec4d(p.x(), p.y(), p.z(), 1.0);
const double w = (std::abs(clip.w()) < 1e-9) ? 1e-9 : clip.w();
return Vec2d(0.5 * viewport[2] * clip.x() / w, 0.5 * viewport[3] * clip.y() / w);
};
const Vec3d step(0.0, 0.0, static_cast<double>(m_typical_layer_height));
double layer_px = 0.0;
for (const Vec3d& p : { m_paths_bounding_box.center(), camera.get_target() })
layer_px = std::max(layer_px, (to_pixels(p + step) - to_pixels(p)).norm());
const unsigned int stride = (layer_px * MAX_STRIDE <= MERGE_BELOW_PX) ? MAX_STRIDE :
std::clamp(static_cast<unsigned int>(MERGE_BELOW_PX / layer_px), 1u, MAX_STRIDE);
m_viewer.set_rest_layer_stride(stride);
m_viewer.set_rest_view_from_above(camera.get_dir_forward().z() < 0.0);
}
// ORCA: libvgcode only builds a reduced set while its mode is not Off, so the preference switch is
// folded into the mode it is given. Every setter goes through here.
void GCodeViewer::apply_reduced_detail_settings()
{
m_viewer.set_reduced_detail_mode(m_reduced_detail_while_dragging ? m_reduced_detail_mode : libvgcode::EReducedDetailMode::Off);
m_viewer.set_reduced_detail_layer_stride(m_reduced_detail_layer_stride);
m_viewer.set_rest_detail_mode(m_rest_detail_mode);
}
void GCodeViewer::set_rest_detail_mode(const std::string& mode)
{
m_rest_detail_mode = reduced_detail_mode_from_string(mode);
apply_reduced_detail_settings();
}
void GCodeViewer::set_reduced_detail_while_dragging(bool value)
{
m_reduced_detail_while_dragging = value;
apply_reduced_detail_settings();
}
void GCodeViewer::set_reduced_detail_mode(const std::string& mode)
{
m_reduced_detail_mode = reduced_detail_mode_from_string(mode);
m_solid_model_while_dragging = m_reduced_detail_mode == libvgcode::EReducedDetailMode::EndLayersOnly;
apply_reduced_detail_settings();
}
void GCodeViewer::set_reduced_detail_layer_stride(unsigned int value)
{
m_reduced_detail_layer_stride = std::max(1u, value);
apply_reduced_detail_settings();
}
libvgcode::EReducedDetailMode GCodeViewer::reduced_detail_mode_from_string(const std::string& mode)
{
if (mode == "full")
return libvgcode::EReducedDetailMode::Off;
if (mode == "solid")
return libvgcode::EReducedDetailMode::EndLayersOnly;
if (mode == "layers")
return libvgcode::EReducedDetailMode::LayersOnly;
if (mode == "shell")
return libvgcode::EReducedDetailMode::ShellOnly;
return libvgcode::EReducedDetailMode::NoInternalInfill;
}
void GCodeViewer::set_layers_z_range(const std::array<unsigned int, 2>& layers_z_range)
{
m_viewer.set_layers_view_range(static_cast<uint32_t>(layers_z_range[0]), static_cast<uint32_t>(layers_z_range[1]));
@@ -2361,7 +2230,6 @@ void GCodeViewer::export_toolpaths_to_obj(const char* filename) const
void GCodeViewer::load_shells(const Print& print, bool initialized, bool force_previewing)
{
++m_scene_version;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": initialized=%1%, force_previewing=%2%")%initialized %force_previewing;
if ((print.id().id == m_shells.print_id)&&(print.get_modified_count() == m_shells.print_modify_count)) {
//BBS: update force previewing logic
@@ -2426,19 +2294,6 @@ void GCodeViewer::load_shells(const Print& print, bool initialized, bool force_p
object_count++;
}
// the prime tower as it was sliced, so that the solid model shows what the print shows; it
// keeps its opaque colour and so never appears among the translucent shells
if (print.is_step_done(psWipeTower) && print.wipe_tower_data().wipe_tower_mesh_data) {
const PrintConfig& config = print.config();
const int plate_idx = print.get_plate_index();
const Vec3d plate_origin = print.get_plate_origin();
const float x = static_cast<float>(config.wipe_tower_x.get_at(plate_idx) + plate_origin.x());
const float y = static_cast<float>(config.wipe_tower_y.get_at(plate_idx) + plate_origin.y());
m_shells.volumes.load_real_wipe_tower_preview(1000, x, y, print.wipe_tower_data().wipe_tower_mesh_data->real_wipe_tower_mesh,
print.wipe_tower_data().wipe_tower_mesh_data->real_brim_mesh, true,
static_cast<float>(config.wipe_tower_rotation_angle), false, initialized);
}
// Orca: disable wipe tower shell
// if (wxGetApp().preset_bundle->printers.get_edited_preset().printer_technology() == ptFFF) {
// // BBS: adds wipe tower's volume
@@ -2689,47 +2544,6 @@ void GCodeViewer::render_shells(int canvas_width, int canvas_height)
glsafe(::glDepthMask(GL_TRUE));
}
// The sliced objects and the prime tower drawn opaque, in their filament colours, cut to the
// visible layer range by the shader's z range. The toolpaths of the range's bottom and top layers
// are drawn afterwards and cap the cut.
void GCodeViewer::render_solid_model(int canvas_width, int canvas_height)
{
if (m_shells.volumes.empty())
return;
GLShaderProgram* shader = wxGetApp().get_shader("gouraud_light");
if (shader == nullptr)
return;
const libvgcode::Interval& layers = m_viewer.get_layers_view_range();
const float z_top = m_viewer.get_layer_z(layers[1]) - m_z_offset + 0.001f;
const float z_bottom = (layers[0] > 0) ? m_viewer.get_layer_z(layers[0] - 1) - m_z_offset - 0.001f : -FLT_MAX;
std::vector<float> alphas;
alphas.reserve(m_shells.volumes.volumes.size());
for (GLVolume* volume : m_shells.volumes.volumes) {
alphas.push_back(volume->color.a());
volume->color.a(1.0f);
volume->set_render_color();
}
m_shells.volumes.set_z_range(z_bottom, z_top);
shader->start_using();
shader->set_uniform("emission_factor", 0.1f);
const Camera& camera = wxGetApp().plater()->get_camera();
shader->set_uniform("z_far", camera.get_far_z());
shader->set_uniform("z_near", camera.get_near_z());
m_shells.volumes.render(GLVolumeCollection::ERenderType::Opaque, false, camera.get_view_matrix(), camera.get_projection_matrix(), {canvas_width, canvas_height});
shader->set_uniform("emission_factor", 0.0f);
shader->stop_using();
m_shells.volumes.set_z_range(-FLT_MAX, FLT_MAX);
size_t k = 0;
for (GLVolume* volume : m_shells.volumes.volumes) {
volume->color.a(alphas[k++]);
volume->set_render_color();
}
}
//BBS
void GCodeViewer::render_all_plates_stats(const std::vector<const GCodeProcessorResult*>& gcode_result_list, bool show /*= true*/) const {
if (!show)
@@ -3594,12 +3408,6 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
std::vector<std::pair<ColorRGBA, std::pair<double, double>>> ret;
ret.reserve(custom_gcode_per_print_z.size());
// Loop invariant, but built lazily: this lambda runs once per extruder on every frame
// and most prints reach neither colour change below, so fetching it up front would cost
// more than the per-item fetch it replaces.
std::vector<float> zs;
bool zs_built = false;
for (const auto& item : custom_gcode_per_print_z) {
if (extruder_id + 1 != static_cast<unsigned char>(item.extruder))
continue;
@@ -3607,10 +3415,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
if (item.type != ColorChange)
continue;
if (!zs_built) {
zs = m_viewer.get_layers_zs();
zs_built = true;
}
const std::vector<float> zs = m_viewer.get_layers_zs();
auto lower_b = std::lower_bound(zs.begin(), zs.end(),
static_cast<float>(item.print_z - epsilon()));
if (lower_b == zs.end())
@@ -4757,8 +4562,6 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
// ORCA: Get layer Zs as doubles
std::vector<double> layer_zs = get_layers_zs();
// loop invariant, same reason as the layer Zs above
const std::vector<float> layer_times = m_viewer.get_layers_estimated_times();
for (Slic3r::CustomGCode::Item custom_gcode : custom_gcode_per_print_z) {
ImGui::Dummy({window_padding, window_padding});
@@ -4778,6 +4581,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
imgui.text(buf);
ImGui::SameLine(max_len * 1.5);
std::vector<float> layer_times = m_viewer.get_layers_estimated_times();
float custom_gcode_time = 0;
if (layer > 0)
{
@@ -4826,7 +4630,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
std::string print_str = _u8L("Model printing time");
std::string total_str = _u8L("Total time");
float max_len = window_padding + 2 * ImGui::GetStyle().ItemSpacing.x;
if (m_viewer.get_layers_count() == 0)
if (m_viewer.get_layers_estimated_times().empty())
max_len += ImGui::CalcTextSize(total_str.c_str()).x;
else {
if (m_viewer.get_view_type() == libvgcode::EViewType::FeatureType)

View File

@@ -230,20 +230,6 @@ private:
bool m_legend_visible{ true };
bool m_legend_enabled{ true };
// ORCA: the reduced-detail-while-dragging preferences, pushed to libvgcode by apply_reduced_detail_settings()
bool m_reduced_detail_while_dragging{ false };
libvgcode::EReducedDetailMode m_reduced_detail_mode{ libvgcode::EReducedDetailMode::NoInternalInfill };
unsigned int m_reduced_detail_layer_stride{ 4 };
libvgcode::EReducedDetailMode m_rest_detail_mode{ libvgcode::EReducedDetailMode::Off };
// ORCA: draw the sliced objects as solid shapes instead of toolpaths while dragging
bool m_solid_model_while_dragging{ false };
void render_solid_model(int canvas_width, int canvas_height);
void apply_reduced_detail_settings();
// whether the user is dragging or a wheel burst is settling, as told by set_interacting()
bool m_interacting{ false };
// the print's typical layer height, for how many layers fit in a pixel at the current view
float m_typical_layer_height{ 0.0f };
void update_rest_layer_stride();
float m_legend_height;
PrintEstimatedStatistics m_print_statistics;
@@ -254,8 +240,6 @@ private:
bool m_contained_in_bed{ true };
mutable bool m_no_render_path { false };
// ORCA: bumped on every change to what the scene pass draws that libvgcode does not track
uint64_t m_scene_version{ 0 };
bool m_is_dark = false;
libvgcode::Viewer m_viewer;
@@ -288,12 +272,7 @@ public:
//BBS: add all plates filament statistics
void render_all_plates_stats(const std::vector<const GCodeProcessorResult*>& gcode_result_list, bool show = true) const;
//BBS: GUI refactor: add canvas width and height
// draw_scene = false records the legend, the sliders and the marker only, for a frame whose
// toolpaths are shown again from a kept image
void render(int canvas_width, int canvas_height, int right_margin, bool draw_scene = true);
// ORCA: what the scene pass draws, as two counters that change whenever it would look different
std::array<uint64_t, 2> scene_version() const;
bool scene_update_pending() const { return m_viewer.has_pending_updates(); }
void render(int canvas_width, int canvas_height, int right_margin);
//BBS
// void _render_calibration_thumbnail_internal(ThumbnailData& thumbnail_data, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager);
// void _render_calibration_thumbnail_framebuffer(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params, PartPlateList& partplate_list, OpenGLManager& opengl_manager);
@@ -360,18 +339,6 @@ public:
void set_dim_previous_layers_brightness(float value) { m_viewer.set_dim_previous_layers_brightness(value); }
float get_dim_previous_layers_brightness() const { return m_viewer.get_dim_previous_layers_brightness(); }
// ORCA: while the user drags the camera or a slider, draw the preview from libvgcode's reduced
// toolpath set, if the preference asks for one
void set_interacting(bool interacting);
bool is_reduced_detail() const { return m_viewer.is_reduced_detail(); }
void set_reduced_detail_while_dragging(bool value);
// the preference's string value: "layers", "no_infill" or "shell"
void set_reduced_detail_mode(const std::string& mode);
void set_reduced_detail_layer_stride(unsigned int value);
// what is left out even at rest: "full", "no_infill" or "shell"
void set_rest_detail_mode(const std::string& mode);
static libvgcode::EReducedDetailMode reduced_detail_mode_from_string(const std::string& mode);
void set_layers_z_range(const std::array<unsigned int, 2>& layers_z_range);
bool is_legend_shown() const { return m_legend_visible && m_legend_enabled; }

View File

@@ -1224,7 +1224,6 @@ GLCanvas3D::GLCanvas3D(wxGLCanvas* canvas, Bed3D &bed)
GLCanvas3D::~GLCanvas3D()
{
_scene_cache_release();
if (_set_current()) {
if (m_fxaa_texture_id != 0) {
glsafe(::glDeleteTextures(1, &m_fxaa_texture_id));
@@ -1329,7 +1328,6 @@ bool GLCanvas3D::init()
void GLCanvas3D::on_change_color_mode(bool is_dark, bool reinit) {
m_is_dark = is_dark;
++m_scene_version;
// Bed color
m_bed.on_change_color_mode(is_dark);
// GcodeViewer color
@@ -2052,48 +2050,6 @@ void GLCanvas3D::render(bool only_init)
}
// draw scene
int hover_id = (m_hover_plate_idxs.size() > 0)?m_hover_plate_idxs.front():-1;
// ORCA: while the preference is on and nothing the scene pass draws has changed since the
// last full frame, that frame is shown again and only the overlays are drawn on top of it
const bool preview_scene = m_canvas_type == ECanvasType::CanvasPreview && m_render_preview && m_gcode_viewer.has_data();
if (preview_scene)
_update_preview_interaction();
bool scene_from_cache = false;
bool scene_to_cache = false;
if (preview_scene && _scene_cache_enabled() && !m_scene_cache.broken) {
const int current_plate = wxGetApp().plater()->get_partplate_list().get_curr_plate_index();
const std::array<uint64_t, 2> viewer_version = m_gcode_viewer.scene_version();
const bool unchanged = m_scene_cache.valid &&
m_scene_cache.width == cnv_size.get_width() && m_scene_cache.height == cnv_size.get_height() &&
m_scene_cache.view == camera.get_view_matrix().matrix() && m_scene_cache.projection == camera.get_projection_matrix().matrix() &&
m_scene_cache.hover_plate == hover_id && m_scene_cache.current_plate == current_plate &&
m_scene_cache.world_axes == m_show_world_axes && m_scene_cache.canvas_version == m_scene_version &&
m_scene_cache.viewer_version == viewer_version && !m_gcode_viewer.scene_update_pending();
if (unchanged) {
_scene_cache_blit(false);
scene_from_cache = !m_scene_cache.broken;
}
if (!scene_from_cache && _scene_cache_prepare(cnv_size.get_width(), cnv_size.get_height())) {
m_scene_cache.valid = false;
m_scene_cache.view = camera.get_view_matrix().matrix();
m_scene_cache.projection = camera.get_projection_matrix().matrix();
m_scene_cache.hover_plate = hover_id;
m_scene_cache.current_plate = current_plate;
m_scene_cache.world_axes = m_show_world_axes;
m_scene_cache.canvas_version = m_scene_version;
m_scene_cache.viewer_version = viewer_version;
scene_to_cache = true;
}
}
else
m_scene_cache.valid = false;
if (scene_from_cache) {
// the preview's panels and its marker are drawn on top of the frame shown again
_render_gcode(cnv_size.get_width(), cnv_size.get_height(), false);
}
else {
glsafe(::glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT));
// Invalidate the shadow map each frame; only the View3D path below rebuilds it. This keeps
// the Preview / Assemble canvases from sampling a stale map with an outdated light matrix.
@@ -2114,6 +2070,7 @@ void GLCanvas3D::render(bool only_init)
show_grid = false;
/* view3D render*/
int hover_id = (m_hover_plate_idxs.size() > 0)?m_hover_plate_idxs.front():-1;
if (m_canvas_type == ECanvasType::CanvasView3D) {
if (!no_partplate)
_render_bed(camera.get_view_matrix(), camera.get_projection_matrix(), !camera.is_looking_downward(), m_show_world_axes);
@@ -2190,13 +2147,6 @@ void GLCanvas3D::render(bool only_init)
if (_is_fxaa_enabled())
_render_fxaa_pass(static_cast<unsigned int>(cnv_size.get_width()), static_cast<unsigned int>(cnv_size.get_height()));
// ORCA: keep the finished scene, with its depth, for the frames that follow
if (scene_to_cache) {
_scene_cache_blit(true);
m_scene_cache.valid = !m_scene_cache.broken;
}
}
// draw overlays
_render_overlays();
@@ -3290,12 +3240,6 @@ void GLCanvas3D::on_idle(wxIdleEvent& evt)
m_dirty |= imgui_requires_extra_frame;
#endif // ENABLE_ENHANCED_IMGUI_SLIDER_FLOAT
m_dirty |= GLTexture::Compressor::has_compressed_texture_to_refresh();
// ORCA: the render timer only wakes the idle loop; the frame that puts the preview's full detail
// back after a wheel burst has to be asked for here, once the settle time is really up
if (m_preview_settle_pending && std::chrono::steady_clock::now() >= m_preview_interaction_until) {
m_preview_settle_pending = false;
m_dirty = true;
}
if (!m_dirty)
return;
@@ -4001,9 +3945,6 @@ void GLCanvas3D::on_mouse_wheel(wxMouseEvent& evt)
evt.SetY(evt.GetY() * scale);
#endif
if (m_canvas_type == CanvasPreview)
note_preview_interaction();
if (wxGetApp().imgui()->update_mouse_data(evt)) {
if (m_canvas_type == CanvasPreview) {
IMSlider* m_layers_slider = get_gcode_viewer().get_layers_slider();
@@ -4110,11 +4051,6 @@ void GLCanvas3D::on_set_color_timer(wxTimerEvent& evt)
}
void GLCanvas3D::note_preview_interaction()
{
m_preview_interaction_until = std::chrono::steady_clock::now() + std::chrono::milliseconds(150);
}
void GLCanvas3D::schedule_extra_frame(int milliseconds)
{
// Schedule idle event right now
@@ -5673,9 +5609,6 @@ void GLCanvas3D::mouse_up_cleanup()
m_mouse.ignore_left_up = false;
m_mouse.ignore_right_up = false;
m_dirty = true;
// ORCA: the frame that follows a release is the one that puts the preview's full detail back,
// and on some platforms no idle event follows a button release until the next input
wxWakeUpIdle();
if (m_canvas->HasCapture())
m_canvas->ReleaseMouse();
@@ -8575,125 +8508,12 @@ void GLCanvas3D::_render_wireframe_overlay()
}
//BBS: GUI refactor: add canvas size as parameters
// ORCA: dragging the camera, the navigator or either slider is when the preview has to keep up
// with continuous input, so that is when the reduced toolpath set earns its visible coarseness.
// A wheel step has no duration, so it holds the reduced set for a settle time instead, and the
// frame that restores the full detail is scheduled for when that time runs out. The level is
// chosen before the draw and before the scene cache is consulted, so a change lands in this very
// frame and never shows a kept frame of the other level.
void GLCanvas3D::_update_preview_interaction()
{
IMSlider* layers_slider = m_gcode_viewer.get_layers_slider();
IMSlider* moves_slider = m_gcode_viewer.get_moves_slider();
const auto now = std::chrono::steady_clock::now();
const bool settling = now < m_preview_interaction_until;
const bool dragging = m_mouse.dragging || m_navigator_dragging || layers_slider->is_dragging() || moves_slider->is_dragging();
m_gcode_viewer.set_interacting(dragging || settling);
if (settling && !dragging && m_gcode_viewer.is_reduced_detail()) {
m_preview_settle_pending = true;
schedule_extra_frame(static_cast<int>(std::chrono::duration_cast<std::chrono::milliseconds>(m_preview_interaction_until - now).count()) + 1);
}
}
bool GLCanvas3D::_scene_cache_enabled() const
{
return wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool("preview_cache_static_scene");
}
// Makes the cache's framebuffer match the window: same size, same sample count, a depth buffer of
// the same format, so that colour and depth can be blitted both ways. Returns false when it cannot.
bool GLCanvas3D::_scene_cache_prepare(int width, int height)
{
SceneCache& cache = m_scene_cache;
glsafe(::glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &cache.default_fbo));
int samples = 0;
glsafe(::glGetIntegerv(GL_SAMPLES, &samples));
int depth_bits = 0;
int stencil_bits = 0;
const GLenum depth_query = (cache.default_fbo == 0) ? GL_DEPTH : GL_DEPTH_ATTACHMENT;
const GLenum stencil_query = (cache.default_fbo == 0) ? GL_STENCIL : GL_DEPTH_ATTACHMENT;
glsafe(::glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, depth_query, GL_FRAMEBUFFER_ATTACHMENT_DEPTH_SIZE, &depth_bits));
glsafe(::glGetFramebufferAttachmentParameteriv(GL_DRAW_FRAMEBUFFER, stencil_query, GL_FRAMEBUFFER_ATTACHMENT_STENCIL_SIZE, &stencil_bits));
const bool stencil = stencil_bits > 0;
if (cache.fbo != 0 && cache.width == width && cache.height == height && cache.samples == samples && cache.stencil == stencil)
return true;
_scene_cache_release();
const GLenum depth_format = stencil ? GL_DEPTH24_STENCIL8 : (depth_bits <= 16) ? GL_DEPTH_COMPONENT16 : (depth_bits >= 32) ? GL_DEPTH_COMPONENT32 : GL_DEPTH_COMPONENT24;
glsafe(::glGenRenderbuffers(1, &cache.color));
glsafe(::glBindRenderbuffer(GL_RENDERBUFFER, cache.color));
if (samples > 0)
glsafe(::glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, GL_RGBA8, width, height));
else
glsafe(::glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, width, height));
glsafe(::glGenRenderbuffers(1, &cache.depth));
glsafe(::glBindRenderbuffer(GL_RENDERBUFFER, cache.depth));
if (samples > 0)
glsafe(::glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, depth_format, width, height));
else
glsafe(::glRenderbufferStorage(GL_RENDERBUFFER, depth_format, width, height));
glsafe(::glBindRenderbuffer(GL_RENDERBUFFER, 0));
glsafe(::glGenFramebuffers(1, &cache.fbo));
glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, cache.fbo));
glsafe(::glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, cache.color));
glsafe(::glFramebufferRenderbuffer(GL_FRAMEBUFFER, stencil ? GL_DEPTH_STENCIL_ATTACHMENT : GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, cache.depth));
const GLenum status = ::glCheckFramebufferStatus(GL_FRAMEBUFFER);
glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, static_cast<GLuint>(cache.default_fbo)));
if (status != GL_FRAMEBUFFER_COMPLETE) {
BOOST_LOG_TRIVIAL(warning) << "Preview scene cache disabled: framebuffer incomplete, status " << status;
_scene_cache_release();
cache.broken = true;
return false;
}
cache.width = width;
cache.height = height;
cache.samples = samples;
cache.stencil = stencil;
return true;
}
// Copies colour and depth from the window into the cache (store) or back (restore). A driver that
// refuses the blit marks the cache broken, and the preview falls back to drawing every frame.
void GLCanvas3D::_scene_cache_blit(bool store)
{
SceneCache& cache = m_scene_cache;
if (cache.fbo == 0)
return;
const GLuint window = static_cast<GLuint>(cache.default_fbo);
glsafe(::glBindFramebuffer(GL_READ_FRAMEBUFFER, store ? window : cache.fbo));
glsafe(::glBindFramebuffer(GL_DRAW_FRAMEBUFFER, store ? cache.fbo : window));
while (::glGetError() != GL_NO_ERROR) {}
::glBlitFramebuffer(0, 0, cache.width, cache.height, 0, 0, cache.width, cache.height, GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT, GL_NEAREST);
const GLenum error = ::glGetError();
glsafe(::glBindFramebuffer(GL_FRAMEBUFFER, window));
if (error != GL_NO_ERROR) {
BOOST_LOG_TRIVIAL(warning) << "Preview scene cache disabled: framebuffer blit failed with GL error " << error;
cache.broken = true;
_scene_cache_release();
}
}
void GLCanvas3D::_scene_cache_release()
{
SceneCache& cache = m_scene_cache;
if (cache.fbo != 0)
glsafe(::glDeleteFramebuffers(1, &cache.fbo));
if (cache.color != 0)
glsafe(::glDeleteRenderbuffers(1, &cache.color));
if (cache.depth != 0)
glsafe(::glDeleteRenderbuffers(1, &cache.depth));
cache.fbo = cache.color = cache.depth = 0;
cache.width = cache.height = 0;
cache.valid = false;
}
void GLCanvas3D::_render_gcode(int canvas_width, int canvas_height, bool draw_scene)
void GLCanvas3D::_render_gcode(int canvas_width, int canvas_height)
{
m_gcode_viewer.render(canvas_width, canvas_height, SLIDER_RIGHT_MARGIN * GCODE_VIEWER_SLIDER_SCALE);
IMSlider *layers_slider = m_gcode_viewer.get_layers_slider();
IMSlider *moves_slider = m_gcode_viewer.get_moves_slider();
m_gcode_viewer.render(canvas_width, canvas_height, SLIDER_RIGHT_MARGIN * GCODE_VIEWER_SLIDER_SCALE, draw_scene);
if (layers_slider->is_need_post_tick_event()) {
auto evt = new wxCommandEvent(EVT_CUSTOMEVT_TICKSCHANGED, m_canvas->GetId());
evt->SetInt((int)layers_slider->get_post_tick_event_type());

View File

@@ -591,10 +591,6 @@ private:
ECursorType m_cursor_type;
GLSelectionRectangle m_rectangle_selection;
bool m_navigator_dragging{ false };
// ORCA: until when a discrete preview interaction (a wheel step) keeps the reduced toolpath set bound
std::chrono::time_point<std::chrono::steady_clock> m_preview_interaction_until{};
// whether the frame that restores the full detail once that time is up is still owed
bool m_preview_settle_pending{ false };
//BBS:add plate related logic
mutable std::vector<int> m_hover_volume_idxs;
@@ -736,33 +732,6 @@ public:
std::array<unsigned int, 2> m_fxaa_texture_size{ 0, 0 };
unsigned int m_ssao_color_texture_id{ 0 };
unsigned int m_ssao_depth_texture_id{ 0 };
// ORCA: the last fully drawn preview scene, shown again while nothing it draws has changed
struct SceneCache
{
unsigned int fbo{ 0 };
unsigned int color{ 0 };
unsigned int depth{ 0 };
int default_fbo{ 0 };
int width{ 0 };
int height{ 0 };
int samples{ 0 };
bool stencil{ false };
// the kept image matches the key below
bool valid{ false };
// a blit failed on this driver: never try again this session
bool broken{ false };
Matrix4d view{ Matrix4d::Zero() };
Matrix4d projection{ Matrix4d::Zero() };
int hover_plate{ -1 };
int current_plate{ -1 };
bool world_axes{ false };
uint64_t canvas_version{ 0 };
std::array<uint64_t, 2> viewer_version{ 0, 0 };
};
SceneCache m_scene_cache;
// bumped on changes to what the scene pass draws that neither the camera nor the viewer track
uint64_t m_scene_version{ 0 };
std::array<unsigned int, 2> m_ssao_texture_size{ { 0, 0 } };
GLModel m_plate_shadow_mask;
std::string m_plate_shadow_mask_key;
@@ -1169,9 +1138,6 @@ public:
void msw_rescale() { m_gcode_viewer.invalidate_legend(); }
void request_extra_frame() { m_extra_frame_requested = true; }
// ORCA: a wheel step is over before the next frame, so it holds the reduced preview for a short
// settle time instead: a burst of steps stays cheap and the full frame lands once they stop
void note_preview_interaction();
void schedule_extra_frame(int milliseconds);
@@ -1307,15 +1273,7 @@ private:
void _render_objects(GLVolumeCollection::ERenderType type, bool with_outline = true);
void _render_wireframe_overlay();
//BBS: GUI refactor: add canvas size as parameters
void _render_gcode(int canvas_width, int canvas_height, bool draw_scene = true);
// ORCA: decides whether the preview draws its reduced set this frame; runs before the scene
// cache is consulted, since the decision changes what the scene pass draws
void _update_preview_interaction();
// ORCA: scene cache, see SceneCache
bool _scene_cache_enabled() const;
bool _scene_cache_prepare(int width, int height);
void _scene_cache_blit(bool store);
void _scene_cache_release();
void _render_gcode(int canvas_width, int canvas_height);
//BBS: render a plane for assemble
void _render_plane() const;
void _render_selection();

View File

@@ -482,11 +482,6 @@ void IMSlider::draw_background_and_groove(const ImRect& bg_rect, const ImRect& g
ImGui::RenderFrame(groove.Min, groove.Max, groove_col, false, 0.5 * groove.GetWidth());
}
bool IMSlider::is_dragging() const
{
return GImGui != nullptr && m_imgui_id != 0 && GImGui->ActiveId == m_imgui_id && GImGui->IO.MouseDown[0];
}
bool IMSlider::horizontal_slider(const char* str_id, int* value, int v_min, int v_max, const ImVec2& size, float scale)
{
ImGuiWindow* window = ImGui::GetCurrentWindow();
@@ -495,7 +490,6 @@ bool IMSlider::horizontal_slider(const char* str_id, int* value, int v_min, int
ImGuiContext& context = *GImGui;
const ImGuiID id = window->GetID(str_id);
m_imgui_id = id;
const ImVec2 pos = window->DC.CursorPos;
const ImRect draw_region(pos, pos + size);
@@ -888,7 +882,6 @@ bool IMSlider::vertical_slider(const char* str_id, int* higher_value, int* lower
ImGuiContext& context = *GImGui;
const ImGuiID id = window->GetID(str_id);
m_imgui_id = id;
const ImVec2 pos = window->DC.CursorPos;
const ImRect draw_region(pos, pos + size);

View File

@@ -118,9 +118,6 @@ public:
//BBS update scroll value changed
bool is_dirty() { return m_dirty; }
// ORCA: whether the mouse is currently holding this slider's handle. Read from ImGui's active
// id rather than from the dirty flag, which is raised and consumed inside a single frame.
bool is_dragging() const;
void set_as_dirty(bool dirty = true) { m_dirty = dirty; }
bool is_need_post_tick_event() { return m_is_need_post_tick_changed_event; }
void reset_post_tick_event(bool val = false) {
@@ -185,8 +182,6 @@ private:
int m_higher_value;
int m_one_layer_value; // ORCA
bool m_dirty = false;
// ORCA: the ImGui id of the slider widget, as of its last render
unsigned int m_imgui_id = 0;
bool m_render_as_disabled{ false };

View File

@@ -318,7 +318,7 @@ wxBoxSizer* PreferencesDialog::create_item_combobox(wxString title, wxString too
return sizer;
}
wxBoxSizer *PreferencesDialog::create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector<wxString> vlist, std::vector<std::string> config_name_index, std::function<void(std::string)> onchange, const wxString wiki_url)
wxBoxSizer *PreferencesDialog::create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector<wxString> vlist, std::vector<std::string> config_name_index, const wxString wiki_url)
{
assert(vlist.size() == config_name_index.size());
unsigned int current_index = 0;
@@ -333,16 +333,9 @@ wxBoxSizer *PreferencesDialog::create_item_combobox(wxString title, wxString too
auto [sizer, combobox] = create_item_combobox_base(title, tooltip, param, vlist, current_index);
// ORCA: this one is only meaningful while the simplification it configures is enabled
if (param == "preview_reduced_detail_mode") {
m_reduced_detail_mode_combo = combobox;
combobox->Enable(app_config->get_bool("preview_reduced_detail_while_dragging"));
}
//// save config
combobox->GetDropDown().Bind(wxEVT_COMBOBOX, [this, param, config_name_index, onchange](wxCommandEvent& e) {
combobox->GetDropDown().Bind(wxEVT_COMBOBOX, [this, param, config_name_index](wxCommandEvent& e) {
app_config->set(param, config_name_index[e.GetSelection()]);
if (onchange != nullptr) onchange(config_name_index[e.GetSelection()]);
e.Skip();
});
@@ -707,15 +700,11 @@ wxBoxSizer *PreferencesDialog::create_item_spinctrl(wxString title, wxString tit
auto input = new SpinInput(m_parent, wxEmptyString, side_label, wxDefaultPosition, DESIGN_INPUT_SIZE, wxSP_ARROW_KEYS, min, max, stoi(app_config->get(param)));
input->SetToolTip(tip);
// ORCA: these are only meaningful while the option they belong to is enabled
// ORCA: this one is only meaningful while the dimming it controls is enabled
if (param == "preview_dim_previous_layers_brightness") {
m_dim_previous_layers_brightness_input = input;
input->Enable(app_config->get_bool("preview_dim_previous_layers"));
}
else if (param == "preview_reduced_detail_layer_stride") {
m_reduced_detail_layer_stride_input = input;
input->Enable(app_config->get_bool("preview_reduced_detail_while_dragging"));
}
m_sizer->Add(input, 0, wxALIGN_CENTER_VERTICAL);
@@ -1067,20 +1056,6 @@ wxBoxSizer *PreferencesDialog::create_item_checkbox(wxString title, wxString too
wxGetApp().mainframe->m_webview->SendCloudProvidersInfo();
}
}
// ORCA: apply the reduced-detail preference immediately to the currently loaded preview
else if (param == "preview_reduced_detail_while_dragging") {
if (m_reduced_detail_mode_combo)
m_reduced_detail_mode_combo->Enable(app_config->get_bool(param));
if (m_reduced_detail_layer_stride_input)
m_reduced_detail_layer_stride_input->Enable(app_config->get_bool(param));
if (Plater* plater = wxGetApp().plater()) {
if (GLCanvas3D* canvas = plater->get_preview_canvas3D()) {
canvas->get_gcode_viewer().set_reduced_detail_while_dragging(app_config->get_bool(param));
canvas->set_as_dirty();
canvas->request_extra_frame();
}
}
}
// ORCA: apply the preview dimming change immediately to the currently loaded preview
else if (param == "preview_dim_previous_layers") {
if (m_dim_previous_layers_brightness_input)
@@ -1963,93 +1938,6 @@ void PreferencesDialog::create_items()
//// GRAPHICS > G-code Preview
g_sizer->Add(create_item_title(_L("G-code Preview")), 1, wxEXPAND);
auto item_reduced_detail_while_dragging = create_item_checkbox(
_L("Simplify preview while dragging"),
_L("While dragging the camera or a preview slider, or zooming with the mouse wheel, draw only part of the toolpaths so that large prints stay responsive. "
"The two options below choose what is left out. The full detail is restored as soon as you let go."),
"preview_reduced_detail_while_dragging"
);
g_sizer->Add(item_reduced_detail_while_dragging);
auto item_reduced_detail_mode = create_item_combobox(
_L("Simplification"),
_L("What the simplified preview leaves out while dragging, on top of skipping layers.\n"
"Skip layers only: every toolpath of the drawn layers is kept.\n"
"Skip internal infill: sparse and solid infill hidden inside the walls is left out.\n"
"Shell only: only the toolpaths on the visible surface of the print are drawn, including the outside of the prime tower. "
"Removes the most of the toolpath modes, and holes narrower than 5 mm are treated as solid.\n"
"Solid model: the sliced objects and the prime tower are drawn as solid shapes in their filament colours instead of toolpaths, "
"cut to the visible layer range with the range's bottom and top layers drawn on top. Cheapest of all; supports are not shown, and the layer setting below does not apply."),
"preview_reduced_detail_mode",
{_L("Skip layers only"), _L("Skip internal infill"), _L("Shell only"), _L("Solid model")},
{"layers", "no_infill", "shell", "solid"},
// ORCA: apply the new mode immediately to the currently loaded preview
[](std::string value) {
if (Plater* plater = wxGetApp().plater()) {
if (GLCanvas3D* canvas = plater->get_preview_canvas3D()) {
canvas->get_gcode_viewer().set_reduced_detail_mode(value);
canvas->set_as_dirty();
canvas->request_extra_frame();
}
}
}
);
g_sizer->Add(item_reduced_detail_mode);
auto item_reduced_detail_layer_stride = create_item_spinctrl(
_L("Draw one layer in every"),
"",
_L("layers"),
_L("How many layers the simplified preview keeps one of while dragging. 1 draws every layer, 4 draws every fourth. "
"The bottom and top of the visible layer range are always drawn whole."),
"preview_reduced_detail_layer_stride",
1,
20,
// ORCA: apply the new stride immediately to the currently loaded preview
[](int value) {
if (Plater* plater = wxGetApp().plater()) {
if (GLCanvas3D* canvas = plater->get_preview_canvas3D()) {
canvas->get_gcode_viewer().set_reduced_detail_layer_stride(static_cast<unsigned int>(value));
canvas->set_as_dirty();
canvas->request_extra_frame();
}
}
}
);
g_sizer->Add(item_reduced_detail_layer_stride);
auto item_rest_detail_mode = create_item_combobox(
_L("Always leave out"),
_L("What the preview leaves out at all times, dragging or not, with every layer drawn. "
"Use it when a plate of large objects is slow to draw even when the view is not moving.\n"
"Nothing: the full preview.\n"
"Internal infill: sparse and solid infill hidden inside the walls is left out.\n"
"Everything but the shell: only the toolpaths on the visible surface of the print are drawn, and layers thinner than a couple of pixels on screen are merged, "
"so that looking from above costs little more than the top surfaces. Holes narrower than 5 mm are treated as solid."),
"preview_rest_detail_mode",
{_L("Nothing"), _L("Internal infill"), _L("Everything but the shell")},
{"full", "no_infill", "shell"},
// ORCA: apply the new mode immediately to the currently loaded preview
[](std::string value) {
if (Plater* plater = wxGetApp().plater()) {
if (GLCanvas3D* canvas = plater->get_preview_canvas3D()) {
canvas->get_gcode_viewer().set_rest_detail_mode(value);
canvas->set_as_dirty();
canvas->request_extra_frame();
}
}
}
);
g_sizer->Add(item_rest_detail_mode);
auto item_cache_static_scene = create_item_checkbox(
_L("Keep the drawn preview while nothing moves"),
_L("Keep the last drawn preview and show it again for frames in which neither the camera nor the toolpaths changed, "
"so that hovering, tooltips and notifications no longer redraw a large print. The scene is redrawn as soon as anything in it changes."),
"preview_cache_static_scene"
);
g_sizer->Add(item_cache_static_scene);
auto item_dim_previous_layers = create_item_checkbox(
_L("Dim lower layers"),
_L("When scrubbing the layer slider in the sliced preview, render the layers below the current one darkened so that only the layer being viewed is shown at full brightness."),

View File

@@ -73,8 +73,6 @@ public:
::CheckBox * m_bambu_cloud_checkbox = {nullptr};
::TextInput *m_backup_interval_textinput = {nullptr};
::SpinInput *m_dim_previous_layers_brightness_input = {nullptr};
::ComboBox * m_reduced_detail_mode_combo = {nullptr};
::SpinInput *m_reduced_detail_layer_stride_input = {nullptr};
::ComboBox * m_network_version_combo = {nullptr};
std::vector<NetworkLibraryVersionInfo> m_available_versions;
@@ -88,7 +86,7 @@ public:
wxBoxSizer *create_item_title(wxString title);
wxBoxSizer *create_item_label(wxString label, const wxString tooltip = "", const wxString wiki_url = "");
wxBoxSizer *create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector<wxString> vlist, std::function<void(wxString)> onchange = {}, const wxString wiki_url = "");
wxBoxSizer *create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector<wxString> vlist, std::vector<std::string> config_name_index, std::function<void(std::string)> onchange = {}, const wxString wiki_url = "");
wxBoxSizer *create_item_combobox(wxString title, wxString tooltip, std::string param, std::vector<wxString> vlist, std::vector<std::string> config_name_index, const wxString wiki_url = "");
wxBoxSizer *create_item_region_combobox(wxString title, wxString tooltip);
wxBoxSizer *create_item_language_combobox(wxString title, wxString tooltip);
wxBoxSizer *create_item_loglevel_combobox(wxString title, wxString tooltip, std::vector<wxString> vlist);

View File

@@ -39,6 +39,20 @@ void write_preset_with_inherits(const DynamicPrintConfig &default_config, const
config.save_to_json(file.string(), name, "User", "1.0.0");
}
// Write a user preset json holding only "inherits" plus the given overrides, the way a GUI-saved
// user preset stores its diff against its parent. Anything else is inherited at load time.
void write_sparse_preset(const fs::path &file, const std::string &name, const std::string &inherits,
const std::vector<std::pair<std::string, std::string>> &overrides)
{
DynamicPrintConfig config;
config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS, true)->value = inherits;
for (const auto &override_pair : overrides)
config.set_deserialize_strict(override_pair.first, override_pair.second);
fs::create_directories(file.parent_path());
config.save_to_json(file.string(), name, "User", "1.0.0");
}
// Add an in-memory preset (no file) with the given inherits value (empty => root preset).
Preset &add_inmemory_preset(PresetCollection &coll, const std::string &name, const std::string &inherits = {})
{
@@ -365,6 +379,100 @@ std::vector<std::string> &compatible_list(PresetCollection &coll, const std::str
} // namespace
TEST_CASE("A user preset inheriting a user preset from the same directory is loaded", "[Preset][Inherits][Regression]")
{
ScopedTemporaryDir temp_dir;
PresetBundle bundle;
// The parent sorts after the child, so the child is necessarily reached before its parent is
// in the collection - the case a single load pass cannot resolve.
const fs::path preset_dir = temp_dir.path() / PRESET_PRINT_NAME;
write_sparse_preset(preset_dir / "AA Child.json", "AA Child", "ZZ Root", {{"layer_height", "0.15"}});
write_sparse_preset(preset_dir / "ZZ Root.json", "ZZ Root", "", {{"layer_height", "0.3"}, {"top_shell_layers", "7"}});
PresetsConfigSubstitutions substitutions;
bundle.prints.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Disable);
REQUIRE(bundle.prints.find_preset("ZZ Root") != nullptr);
const Preset *child = bundle.prints.find_preset("AA Child");
REQUIRE(child != nullptr);
CHECK_FALSE(bundle.has_errors());
REQUIRE(bundle.prints.get_preset_parent(*child) != nullptr);
CHECK(bundle.prints.get_preset_parent(*child)->name == "ZZ Root");
// The child's own override wins, and what it does not override comes from the parent rather
// than from the collection defaults.
CHECK_THAT(child->config.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.15, 1e-9));
CHECK(child->config.opt_int("top_shell_layers") == 7);
}
TEST_CASE("A preset whose parent exists nowhere is reported, not loaded", "[Preset][Inherits][Regression]")
{
ScopedTemporaryDir temp_dir;
PresetBundle bundle;
const fs::path orphan_file = temp_dir.path() / PRESET_PRINT_NAME / "Orphan.json";
write_sparse_preset(orphan_file, "Orphan", "No Such Parent", {{"layer_height", "0.15"}});
PresetsConfigSubstitutions substitutions;
bundle.prints.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Disable);
CHECK(bundle.prints.find_preset("Orphan") == nullptr);
CHECK(bundle.has_errors());
CHECK(bundle.prints.unresolved_parent(orphan_file) == "No Such Parent");
// Resolving the dropped file names the missing parent instead of reporting the file as unknown.
DynamicPrintConfig config;
std::string error;
CHECK_FALSE(bundle.resolve_preset_config(config, Preset::TYPE_PRINT, orphan_file.string(),
ForwardCompatibilitySubstitutionRule::Disable, error, false));
CHECK(error == "Preset was not loaded because its parent preset \"No Such Parent\" was not found");
}
TEST_CASE("Presets inheriting each other in a cycle are reported, not loaded", "[Preset][Inherits][Regression]")
{
ScopedTemporaryDir temp_dir;
PresetBundle bundle;
const fs::path preset_dir = temp_dir.path() / PRESET_PRINT_NAME;
write_sparse_preset(preset_dir / "Ping.json", "Ping", "Pong", {{"layer_height", "0.15"}});
write_sparse_preset(preset_dir / "Pong.json", "Pong", "Ping", {{"layer_height", "0.3"}});
PresetsConfigSubstitutions substitutions;
bundle.prints.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Disable);
CHECK(bundle.prints.find_preset("Ping") == nullptr);
CHECK(bundle.prints.find_preset("Pong") == nullptr);
CHECK(bundle.has_errors());
}
TEST_CASE("A preset held back for its parent reports its substitutions once", "[Preset][Inherits][Regression]")
{
ScopedTemporaryDir temp_dir;
PresetBundle bundle;
// The child sorts before its parent, so it is held back for a pass and its file is read
// twice. The bogus boolean makes every read produce a substitution.
const fs::path preset_dir = temp_dir.path() / PRESET_PRINT_NAME;
fs::create_directories(preset_dir);
std::ofstream((preset_dir / "AA Child.json").string())
<< R"({"type":"process","name":"AA Child","from":"User","version":"1.0.0",)"
<< R"("inherits":"ZZ Root","spiral_mode":"sometimes"})";
write_sparse_preset(preset_dir / "ZZ Root.json", "ZZ Root", "", {{"layer_height", "0.3"}});
PresetsConfigSubstitutions substitutions;
bundle.prints.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Enable);
REQUIRE(bundle.prints.find_preset("AA Child") != nullptr);
// A read that ends in the preset being held back must not leave its substitutions behind,
// otherwise the same preset is listed once per pass it waited.
CHECK(std::count_if(substitutions.begin(), substitutions.end(),
[](const PresetConfigSubstitutions &s) { return s.preset_name == "AA Child"; }) == 1);
}
TEST_CASE("Renamed printer/process names are normalized into compatible lists on load", "[Preset][Rename]")
{
PresetBundle bundle;