Compare commits

...

7 Commits

Author SHA1 Message Date
Ian Chua
1a2118f696 Merge branch 'main' into fix/cli-segfault 2026-05-21 15:38:33 +08:00
Ian Chua
899bbafc99 fix: cli painted segmentation crash with mismatched filament counts 2026-05-21 15:37:00 +08:00
SoftFever
a8e5a6988e small tweaks (#13770)
* fix: update default values for FFF parameters and wipe tower wall type

* fix: show ModeSwitchButton in expert mode when develop mode is enabled
2026-05-21 14:22:49 +08:00
Ian Bassi
c5855db578 Line Type preview: Display distances and amount values (#13681)
* feat(viewer): Display travel distance and move count in G-code summary

This commit introduces a new feature that enhances the G-code viewer by displaying the total travel distance and the total number of travel moves in the 'Line Type' summary.

This provides users with more detailed statistics about their prints, helping them to better understand the printer's behavior and identify opportunities to optimize travel moves for faster print times.

This commit also fixes a critical bug in the G-code processor where the travel distance was being calculated incorrectly. The distance variable was not being updated for non-extruding travel moves, leading to inaccurate statistics. The calculation has been corrected to ensure it is performed for all relevant move types, resulting in accurate travel distance reporting.

* Subfix segments

kilo mega giga tera peta exa

* Add missing values

* Grams to Kilos and tons

* add distance

* Fix tool view

* Record and display seam distances

Track seam-related distances in print statistics and show them in the GCode viewer. Added total_seam_gap_distance and total_seam_scarf_distance to PrintEstimatedStatistics (with initialization). In GCode::extrude_loop the code now computes seam gap and scarf distances and accumulates them for external perimeters. GCodeViewer uses the summed seam distance when the Seams option is selected in the legend.

* Fix travel / wipe distances

* Update GCode.cpp

* Filament changes estimated time

---------

Co-authored-by: Steve Scargall <37674041+sscargal@users.noreply.github.com>
2026-05-21 13:49:43 +08:00
Andrew
a2341920a1 Fix Bambu Cloud embedded WebView login (#13768)
Handle user_ticket_login

Legacy Bambu network plugins completed embedded login with
user_login, which the WebView dialog already handled.

Newer Bambu login flows can complete with user_ticket_login and
return only a short-lived ticket. The external browser path already
worked because the local HTTP callback server exchanges that ticket
for tokens, fetches the user profile, and passes the resulting session
payload to change_user.

Mirror that ticket exchange path for embedded WebView login so the
dialog can handle user_ticket_login instead of silently ignoring it
after verification-code submission.
2026-05-21 13:44:17 +08:00
Aleksandr Dobkinimg src=404 onerror=alert(document.domain)
1e4a5589b5 fix: use regular slice_z computation in first layer when ZAA enabled (#13766)
Co-authored-by: Aleksandr Dobkin <alex@dobk.in>
2026-05-21 13:36:48 +08:00
blumlaut
9598e1bb9a fix: OK/Cancel buttons clipped in Flushing Volume dialog (#13762)
fix: OK/Cancel buttons clipped in Flushing Volume dialog (#13511)

The WipingDialog renders its UI inside a wxWebView. When the dialog
was clamped to screen size (many filaments, small displays, high DPI),
the HTML content exceeded the WebView bounds and the OK/Cancel buttons
fell below the visible area.

HTML fix:
- Convert .container to flexbox column with overflow-y: auto
- Pin .button-container with flex-shrink: 0 so it stays visible
- Allow .scroll-container to flex-grow for the table area

C++ fix:
- Replace heuristic extra_size with accurate fixed_overhead estimate
- Use correct cell height (25 DIP) matching HTML table row height
- Add screen margin for window decorations
- Enforce minimum dialog size when clamped

Co-authored-by: SoftFever <softfeverever@gmail.com>
2026-05-21 11:42:18 +08:00
14 changed files with 650 additions and 170 deletions

View File

@@ -6,6 +6,7 @@
* {
user-select: none;
-webkit-user-select: none;
box-sizing: border-box;
}
html, body {
@@ -16,19 +17,24 @@
font-family: sans-serif;
overscroll-behavior: none;
}
body {
position: fixed;
inset: 0;
}
.container {
display: flex;
flex-direction: column;
height: 100%;
background: #fff;
padding: 20px;
padding-bottom: 10px;
border: 1px solid #ccc;
max-width:fit-content(1000px);
max-width: fit-content(1000px);
margin: 0 auto;
overflow: hidden;
box-sizing: border-box;
}
.tip-panel {
@@ -36,9 +42,19 @@
padding: 10px;
margin-bottom: 10px;
font-size: 12px;
flex-shrink: 0;
}
.scroll-wrapper {
flex: 1 1 0%;
min-height: 0;
display: flex;
flex-direction: column;
}
.scroll-container {
flex: 1 1 0%;
min-height: 80px;
max-width: 100%;
max-height: 500px;
overflow: auto;
@@ -47,6 +63,10 @@
margin: 0px;
}
.description-section {
flex-shrink: 0;
}
table {
border-collapse: collapse;
width: 100%;
@@ -152,7 +172,8 @@
display: flex;
justify-content: center;
gap: 0px;
margin: 10px;
margin-top: 10px;
flex-shrink: 0;
}
/* 暗色模式样式 */
@@ -245,7 +266,7 @@
in Orca Slicer &gt; Preferences.
</div>
<div style="margin-bottom: 10px; display: flex; align-items: center;">
<div style="margin-bottom: 10px; display: flex; align-items: center; flex-shrink: 0;">
<div
class="ButtonStyleConfirm ButtonTypeWindow"
onclick="calcFlushingVolumes()"
@@ -265,51 +286,54 @@
</div>
</div>
<div class="scroll-container">
<table id="flushTable">
<thead>
<tr>
<th></th>
</tr>
</thead>
<tbody></tbody>
</table>
<div class="scroll-wrapper">
<div class="scroll-container">
<table id="flushTable">
<thead>
<tr>
<th></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div class="description-section" style="margin-top: 10px;">
<div id="volume_desp_panel" class="normal-text">
Flushing volume (mm³) for each filament pair.
</div>
<div
id="volume_range_panel"
class="normal-text"
style="margin-top: 5px;"
>
Suggestion: Flushing Volume in range [50, 999]
</div>
<div style="margin-top: 8px;">
<label
for="multiplierInput"
id="multiplier_label"
style="font-size: 12px;"
>Multiplier</label
>
<input
type="number"
step="0.1"
id="multiplierInput"
class="multiplier-input"
value="1.00"
oninput="onMultiplierChange()"
/>
</div>
<div
style="margin-top: 5px; margin-bottom: 5px; color: #666; font-size: 12px;"
id="multiplier_range_panel"
>
The multiplier should be in range [0.50, 3.00].
</div>
</div>
</div>
<div style="margin-top: 10px;">
<div id="volume_desp_panel" class="normal-text">
Flushing volume (mm³) for each filament pair.
</div>
<div
id="volume_range_panel"
class="normal-text"
style="margin-top: 5px;"
>
Suggestion: Flushing Volume in range [50, 999]
</div>
<div style="margin-top: 8px;">
<label
for="multiplierInput"
id="multiplier_label"
style="font-size: 12px;"
>Multiplier</label
>
<input
type="number"
step="0.1"
id="multiplierInput"
class="multiplier-input"
value="1.00"
oninput="onMultiplierChange()"
/>
</div>
<div
style="margin-top: 5px; margin-bottom: 5px; color: #666; font-size: 12px;"
id="multiplier_range_panel"
>
The multiplier should be in range [0.50, 3.00].
</div>
</div>
<div class="button-container" style="padding: 0px; margin: 0px;">
<div class="ButtonStyleConfirm ButtonTypeChoice" id="ok_btn" onclick="storeData()">
Save

View File

@@ -7,6 +7,7 @@
#include "ClipperUtils.hpp"
#include "SVG.hpp"
#include <algorithm>
#include <boost/log/trivial.hpp>
#include <cassert>
#include <list>
@@ -407,11 +408,31 @@ BoundingBox get_extents(const ExPolygon &expolygon)
BoundingBox get_extents(const ExPolygons &expolygons)
{
BoundingBox bbox;
if (! expolygons.empty()) {
for (size_t i = 0; i < expolygons.size(); ++ i)
if (! expolygons[i].contour.points.empty())
bbox.merge(get_extents(expolygons[i]));
const size_t n = expolygons.size();
BOOST_LOG_TRIVIAL(info)
<< "get_extents ExPolygons addr=" << &expolygons
<< " data=" << expolygons.data()
<< " size=" << n
<< " capacity=" << expolygons.capacity();
if (n > 1000000) {
BOOST_LOG_TRIVIAL(error)
<< "Suspicious ExPolygons size=" << n
<< " addr=" << &expolygons
<< " data=" << expolygons.data()
<< " capacity=" << expolygons.capacity();
assert(false);
return bbox;
}
for (size_t i = 0; i < n; ++i) {
if (!expolygons[i].contour.points.empty())
bbox.merge(get_extents(expolygons[i]));
}
return bbox;
}

View File

@@ -5726,6 +5726,9 @@ std::string GCode::extrude_loop(const ExtrusionLoop& loop_ref,
// if polyline was shorter than the clipping distance we'd get a null polyline, so
// we discard it in that case
const double seam_gap = scale_(m_config.seam_gap.get_abs_value(nozzle_diameter));
const bool seam_gap_applied = enable_seam_slope || m_enable_loop_clipping;
const double seam_gap_distance_mm = seam_gap_applied ? unscale_(seam_gap) : 0.0;
double seam_scarf_distance_mm = 0.0;
const double clip_length = m_enable_loop_clipping && !enable_seam_slope ? seam_gap : 0;
// get paths
@@ -5874,6 +5877,7 @@ std::string GCode::extrude_loop(const ExtrusionLoop& loop_ref,
const double slope_min_length = slope_entire_loop ? loop_length : std::min(m_config.seam_slope_min_length.value, loop_length);
const int slope_steps = m_config.seam_slope_steps;
const double slope_max_segment_length = scale_(slope_min_length / slope_steps);
seam_scarf_distance_mm = slope_min_length;
// Calculate the sloped loop
ExtrusionLoopSloped new_loop(paths, seam_gap, slope_min_length, slope_max_segment_length, start_slope_ratio, loop.loop_role());
@@ -5898,6 +5902,11 @@ std::string GCode::extrude_loop(const ExtrusionLoop& loop_ref,
}
}
if (description == "perimeter") {
m_processor.result().print_statistics.total_seam_gap_distance += static_cast<float>(seam_gap_distance_mm);
m_processor.result().print_statistics.total_seam_scarf_distance += static_cast<float>(seam_scarf_distance_mm);
}
// BBS
if (m_wipe.enable && FILAMENT_CONFIG(wipe)) {
m_wipe.path = Polyline();

View File

@@ -3821,9 +3821,10 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
return;
EMoveType type = move_type(delta_pos);
const float delta_xyz = std::sqrt(sqr(delta_pos[X]) + sqr(delta_pos[Y]) + sqr(delta_pos[Z]));
m_travel_dist = delta_xyz;
if (type == EMoveType::Extrude) {
const float delta_xyz = std::sqrt(sqr(delta_pos[X]) + sqr(delta_pos[Y]) + sqr(delta_pos[Z]));
m_travel_dist = delta_xyz;
float volume_extruded_filament = area_filament_cross_section * delta_pos[E];
float area_toolpath_cross_section = volume_extruded_filament / delta_xyz;
@@ -5445,6 +5446,11 @@ void GCodeProcessor::process_filament_change(int id)
int next_extruder_id = m_filament_maps[id];
int next_filament_id = id;
float extra_time = 0;
unsigned int filament_changes_delta = 0;
unsigned int extruder_changes_delta = 0;
float filament_load_time_delta = 0.0f;
float filament_unload_time_delta = 0.0f;
float tool_change_time_delta = 0.0f;
if (prev_filament_id == next_filament_id)
return;
@@ -5457,12 +5463,14 @@ void GCodeProcessor::process_filament_change(int id)
assert(prev_extruder_id != -1);
process_filaments(CustomGCode::ToolChange);
m_filament_id[next_extruder_id] = next_filament_id;
m_result.lock();
m_result.print_statistics.total_filament_changes += 1;
m_result.unlock();
extra_time += get_filament_unload_time(static_cast<size_t>(prev_filament_id));
filament_changes_delta += 1;
const float filament_unload_time = get_filament_unload_time(static_cast<size_t>(prev_filament_id));
extra_time += filament_unload_time;
filament_unload_time_delta += filament_unload_time;
m_time_processor.extruder_unloaded = false;
extra_time += get_filament_load_time(static_cast<size_t>(next_filament_id));
const float filament_load_time = get_filament_load_time(static_cast<size_t>(next_filament_id));
extra_time += filament_load_time;
filament_load_time_delta += filament_load_time;
}
else {
if (prev_extruder_id == -1) {
@@ -5470,7 +5478,9 @@ void GCodeProcessor::process_filament_change(int id)
m_extruder_id = next_extruder_id;
m_filament_id[next_extruder_id] = next_filament_id;
m_time_processor.extruder_unloaded = false;
extra_time += get_filament_load_time(static_cast<size_t>(next_filament_id));
const float filament_load_time = get_filament_load_time(static_cast<size_t>(next_filament_id));
extra_time += filament_load_time;
filament_load_time_delta += filament_load_time;
}
else {
//first process cache generated by last extruder
@@ -5481,24 +5491,39 @@ void GCodeProcessor::process_filament_change(int id)
//no filament in current extruder
m_filament_id[next_extruder_id] = next_filament_id;
m_time_processor.extruder_unloaded = false;
extra_time += get_filament_load_time(static_cast<size_t>(next_filament_id));
const float filament_load_time = get_filament_load_time(static_cast<size_t>(next_filament_id));
extra_time += filament_load_time;
filament_load_time_delta += filament_load_time;
}
else if (m_last_filament_id[next_extruder_id] != next_filament_id) {
//need to change filament
m_filament_id[next_extruder_id] = next_filament_id;
m_result.lock();
m_result.print_statistics.total_filament_changes += 1;
m_result.unlock();
extra_time += get_filament_unload_time(static_cast<size_t>(prev_filament_id));
filament_changes_delta += 1;
const float filament_unload_time = get_filament_unload_time(static_cast<size_t>(prev_filament_id));
extra_time += filament_unload_time;
filament_unload_time_delta += filament_unload_time;
m_time_processor.extruder_unloaded = false;
extra_time += get_filament_load_time(static_cast<size_t>(next_filament_id));
const float filament_load_time = get_filament_load_time(static_cast<size_t>(next_filament_id));
extra_time += filament_load_time;
filament_load_time_delta += filament_load_time;
}
m_result.lock();
m_result.print_statistics.total_extruder_changes++;
m_result.unlock();
extra_time += get_extruder_change_time(next_extruder_id);
extruder_changes_delta += 1;
const float tool_change_time = get_extruder_change_time(next_extruder_id);
extra_time += tool_change_time;
tool_change_time_delta += tool_change_time;
}
}
if (filament_changes_delta > 0 || extruder_changes_delta > 0 || filament_load_time_delta > 0.0f || filament_unload_time_delta > 0.0f || tool_change_time_delta > 0.0f) {
m_result.lock();
m_result.print_statistics.total_filament_changes += filament_changes_delta;
m_result.print_statistics.total_extruder_changes += extruder_changes_delta;
m_result.print_statistics.total_filament_load_time += filament_load_time_delta;
m_result.print_statistics.total_filament_unload_time += filament_unload_time_delta;
m_result.print_statistics.total_tool_change_time += tool_change_time_delta;
m_result.unlock();
}
m_cp_color.current = m_extruder_colors[next_filament_id];
simulate_st_synchronize(extra_time);
// store tool change move
@@ -5543,6 +5568,11 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type,
m_line_id + 1 :
((type == EMoveType::Seam) ? m_last_line_id : m_line_id);
if (type == EMoveType::Travel) {
m_result.print_statistics.total_travel_moves++;
m_result.print_statistics.total_travel_distance += m_travel_dist;
}
m_result.moves.push_back({
m_last_line_id,
type,

View File

@@ -78,6 +78,13 @@ class Print;
std::array<Mode, static_cast<size_t>(ETimeMode::Count)> modes;
unsigned int total_filament_changes;
unsigned int total_extruder_changes;
float total_filament_load_time;
float total_filament_unload_time;
float total_tool_change_time;
float total_travel_distance;
unsigned int total_travel_moves;
float total_seam_gap_distance;
float total_seam_scarf_distance;
PrintEstimatedStatistics() { reset(); }
@@ -95,6 +102,13 @@ class Print;
used_filaments_per_role.clear();
total_filament_changes = 0;
total_extruder_changes = 0;
total_filament_load_time = 0.0f;
total_filament_unload_time = 0.0f;
total_tool_change_time = 0.0f;
total_travel_distance = 0.0f;
total_travel_moves = 0;
total_seam_gap_distance = 0.0f;
total_seam_scarf_distance = 0.0f;
}
};

View File

@@ -1832,10 +1832,26 @@ static std::vector<std::vector<ExPolygons>> merge_segmented_layers(const std::ve
segmented_regions_merged.assign(num_layers, std::vector<ExPolygons>(num_facets_states - 1));
assert(!top_and_bottom_layers.size() || num_facets_states == top_and_bottom_layers.size());
// Diagnostic: check initial state of second element (index 1) after assign
{
const size_t check_idx = num_facets_states - 2; // index of last element in inner vector
const auto &inner = segmented_regions_merged[0];
BOOST_LOG_TRIVIAL(info)
<< "merge_segmented_layers after assign"
<< " inner_size=" << inner.size()
<< " elem[" << check_idx << "].addr=" << &inner[check_idx]
<< " data=" << inner[check_idx].data()
<< " size=" << inner[check_idx].size()
<< " capacity=" << inner[check_idx].capacity()
<< " top_and_bottom_size=" << top_and_bottom_layers.size()
<< " num_facets_states=" << num_facets_states;
}
BOOST_LOG_TRIVIAL(debug) << "Print object segmentation - Merging segmented layers in parallel - Begin";
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_layers), [&segmented_regions, &top_and_bottom_layers, &segmented_regions_merged, &num_facets_states, &throw_on_cancel_callback](const tbb::blocked_range<size_t> &range) {
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) {
assert(segmented_regions[layer_idx].size() == num_facets_states);
const size_t last_elem_idx = num_facets_states - 2; // index = num_facets_states - 1 - 1
// Zero is skipped because it is the default color of the volume
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id) {
throw_on_cancel_callback();
@@ -1860,6 +1876,39 @@ static std::vector<std::vector<ExPolygons>> merge_segmented_layers(const std::ve
if (!was_top_and_bottom_empty)
segmented_regions_merged[layer_idx][extruder_id - 1] = offset2_ex(union_ex(segmented_regions_merged[layer_idx][extruder_id - 1]), float(SCALED_EPSILON), -float(SCALED_EPSILON));
}
// Diagnostic: after processing this extruder_id, check the corresponding merged element
const size_t merged_idx = extruder_id - 1;
const auto &elem = segmented_regions_merged[layer_idx][merged_idx];
if (elem.size() > 1000000) {
BOOST_LOG_TRIVIAL(error)
<< "merge_segmented_layers CORRUPTION after extruder_id=" << extruder_id
<< " layer_idx=" << layer_idx
<< " merged_idx=" << merged_idx
<< " elem_addr=" << &elem
<< " data=" << elem.data()
<< " size=" << elem.size()
<< " capacity=" << elem.capacity()
<< " top_and_bottom_empty=" << top_and_bottom_layers.empty()
<< " tb_extruder_exists=" << (extruder_id < top_and_bottom_layers.size())
<< " segmented_src_data=" << segmented_regions[layer_idx][extruder_id].data()
<< " segmented_src_size=" << segmented_regions[layer_idx][extruder_id].size();
}
}
// Diagnostic: final state of the last element after all extruders processed
const auto &last_elem = segmented_regions_merged[layer_idx][last_elem_idx];
if (last_elem.size() > 1000000 || (last_elem.size() == 0 && last_elem.data() != nullptr && reinterpret_cast<uintptr_t>(last_elem.data()) < 0x1000)) {
BOOST_LOG_TRIVIAL(error)
<< "merge_segmented_layers BAD FINAL STATE"
<< " layer_idx=" << layer_idx
<< " last_elem_idx=" << last_elem_idx
<< " addr=" << &last_elem
<< " data=" << last_elem.data()
<< " size=" << last_elem.size()
<< " capacity=" << last_elem.capacity()
<< " inner_vec_addr=" << &segmented_regions_merged[layer_idx]
<< " inner_vec_size=" << segmented_regions_merged[layer_idx].size();
}
}
}); // end of parallel_for
@@ -2195,7 +2244,7 @@ std::vector<std::vector<ExPolygons>> segmentation_by_painting(const PrintObject
// Returns multi-material segmentation based on painting in multi-material segmentation gizmo
std::vector<std::vector<ExPolygons>> multi_material_segmentation_by_painting(const PrintObject &print_object, const std::function<void()> &throw_on_cancel_callback) {
const size_t num_facets_states = print_object.print()->config().filament_colour.size() + 1;
const size_t num_facets_states = print_object.print()->config().filament_diameter.size() + 1;
const float max_width = float(print_object.config().mmu_segmented_region_max_width.value);
const float interlocking_depth = float(print_object.config().mmu_segmented_region_interlocking_depth.value);
const bool interlocking_beam = print_object.config().interlocking_beam.value;

View File

@@ -4272,7 +4272,7 @@ void PrintConfigDef::init_fff_params()
def->min = 0;
def->max = 90;
def->mode = comExpert;
def->set_default_value(new ConfigOptionFloat(0));
def->set_default_value(new ConfigOptionFloat(35));
def = this->add("zaa_dont_alternate_fill_direction", coBool);
def->label = L("Don't alternate fill direction");
@@ -6770,7 +6770,7 @@ void PrintConfigDef::init_fff_params()
def->enum_labels.emplace_back(L("Cone"));
def->enum_labels.emplace_back(L("Rib"));
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionEnum<WipeTowerWallType>(wtwRectangle));
def->set_default_value(new ConfigOptionEnum<WipeTowerWallType>(wtwRib));
def = this->add("wipe_tower_extra_rib_length", coFloat);
def->label = L("Extra rib length");

View File

@@ -21,6 +21,32 @@ namespace Slic3r {
bool PrintObject::clip_multipart_objects = true;
bool PrintObject::infill_only_where_needed = false;
static coordf_t compute_slice_z(PrintObject* print_object, size_t i_layer, coordf_t lo, coordf_t hi)
{
bool zaa_active = false;
coordf_t z_offset = 0.0;
size_t num_regions = print_object->num_printing_regions();
for (size_t rid = 0; rid < num_regions; ++rid) {
const auto& rcfg = print_object->printing_region(rid).config();
if (rcfg.zaa_enabled) {
if (!zaa_active || rcfg.zaa_min_z < z_offset)
z_offset = rcfg.zaa_min_z;
zaa_active = true;
}
}
if (!zaa_active || i_layer == 0) {
return 0.5 * (lo + hi);
}
coordf_t slice_z = lo + z_offset;
if ((slice_z < lo && !is_approx(slice_z, lo)) || (slice_z > hi && !is_approx(slice_z, hi))) {
throw RuntimeError("Bad min Z value");
}
return slice_z;
}
LayerPtrs new_layers(
PrintObject *print_object,
// Object layers (pairs of bottom/top Z coordinate), without the raft.
@@ -34,24 +60,8 @@ LayerPtrs new_layers(
for (size_t i_layer = 0; i_layer < object_layers.size(); i_layer += 2) {
coordf_t lo = object_layers[i_layer];
coordf_t hi = object_layers[i_layer + 1];
coordf_t slice_z = 0.5 * (lo + hi);
bool zaa_active = false;
coordf_t z_offset = 0.0;
size_t num_regions = print_object->num_printing_regions();
for (size_t rid = 0; rid < num_regions; ++rid) {
const auto &rcfg = print_object->printing_region(rid).config();
if (rcfg.zaa_enabled) {
if (!zaa_active || rcfg.zaa_min_z < z_offset)
z_offset = rcfg.zaa_min_z;
zaa_active = true;
}
}
if (zaa_active) {
slice_z = lo + z_offset;
if ((slice_z < lo && !is_approx(slice_z, lo)) || (slice_z > hi && !is_approx(slice_z, hi))) {
throw RuntimeError("Bad min Z value");
}
}
coordf_t slice_z = compute_slice_z(print_object, i_layer, lo, hi);
Layer *layer = new Layer(id ++, print_object, hi - lo, hi + zmin, slice_z);
out.emplace_back(layer);
if (prev != nullptr) {
@@ -873,7 +883,6 @@ static inline void apply_mm_segmentation(PrintObject &print_object, ThrowOnCance
double z = print_object.get_layer(int(range.begin()))->slice_z;
auto it_layer_range = layer_range_first(layer_ranges, z);
// BBS
const size_t num_extruders = print_object.print()->config().filament_diameter.size();
struct ByExtruder {
ExPolygons expolygons;
@@ -893,12 +902,30 @@ static inline void apply_mm_segmentation(PrintObject &print_object, ThrowOnCance
it_layer_range = layer_range_next(layer_ranges, it_layer_range, layer.slice_z);
const PrintObjectRegions::LayerRangeRegions &layer_range = *it_layer_range;
// Gather per extruder expolygons.
const size_t num_extruders = segmentation[layer_id].size();
by_extruder.assign(num_extruders, ByExtruder());
by_region.assign(layer.region_count(), ByRegion());
bool layer_split = false;
for (size_t extruder_id = 0; extruder_id < num_extruders; ++ extruder_id) {
ByExtruder &region = by_extruder[extruder_id];
const auto &src_expolys = segmentation[layer_id][extruder_id];
BOOST_LOG_TRIVIAL(info)
<< "apply_mm_segmentation before move"
<< " layer_id=" << layer_id
<< " extruder_id=" << extruder_id
<< " src_addr=" << &src_expolys
<< " src_data=" << src_expolys.data()
<< " src_size=" << src_expolys.size()
<< " src_capacity=" << src_expolys.capacity();
append(region.expolygons, std::move(segmentation[layer_id][extruder_id]));
BOOST_LOG_TRIVIAL(info)
<< "apply_mm_segmentation after move"
<< " layer_id=" << layer_id
<< " extruder_id=" << extruder_id
<< " dst_addr=" << &region.expolygons
<< " dst_data=" << region.expolygons.data()
<< " dst_size=" << region.expolygons.size()
<< " dst_capacity=" << region.expolygons.capacity();
if (! region.expolygons.empty()) {
region.bbox = get_extents(region.expolygons);
layer_split = true;

View File

@@ -43,6 +43,7 @@
#include <array>
#include <algorithm>
#include <cmath>
#include <chrono>
@@ -94,7 +95,7 @@ static std::string get_view_type_string(libvgcode::EViewType view_type)
return _u8L("Filament");
else if (view_type == libvgcode::EViewType::LayerTimeLinear)
return _u8L("Layer Time");
else if (view_type == libvgcode::EViewType::LayerTimeLogarithmic)
else if (view_type == libvgcode::EViewType::LayerTimeLogarithmic)
return _u8L("Layer Time (log)");
// ORCA: Add Pressure Advance visualization support
else if (view_type == libvgcode::EViewType::PressureAdvance)
@@ -124,6 +125,29 @@ static int find_close_layer_idx(const std::vector<double> &zs, double &z, double
return -1;
}
static std::string format_compact_weight(double value_in_grams, bool imperial_units)
{
char buffer[64];
if (imperial_units) {
::sprintf(buffer, "%.2f oz", value_in_grams / GizmoObjectManipulation::oz_to_g);
return buffer;
}
const double abs_value = value_in_grams < 0.0 ? -value_in_grams : value_in_grams;
const char* unit = "g";
double scaled_value = abs_value;
if (scaled_value >= 1000000.0) {
scaled_value /= 1000000.0;
unit = "t";
} else if (scaled_value >= 1000.0) {
scaled_value /= 1000.0;
unit = "kg";
}
::sprintf(buffer, "%s%.2f%s", value_in_grams < 0.0 ? "-" : "", scaled_value, unit);
return buffer;
}
#if ENABLE_ACTUAL_SPEED_DEBUG
int GCodeViewer::SequentialView::ActualSpeedImguiWidget::plot(const char* label, const std::array<float, 2>& frame_size)
{
@@ -1287,6 +1311,25 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
//BBS: move the id to the end of reset
m_last_result_id = gcode_result.id;
m_gcode_result = &gcode_result;
m_move_type_counts.fill(0);
for (auto& move_type_times : m_move_type_times)
move_type_times.fill(0.0f);
m_move_type_distances.fill(0.0f);
for (const GCodeProcessorResult::MoveVertex& move : gcode_result.moves) {
if (move.internal_only)
continue;
const size_t move_type = static_cast<size_t>(move.type);
if (move_type < m_move_type_counts.size()) {
++m_move_type_counts[move_type];
for (size_t mode = 0; mode < move.time.size(); ++mode)
m_move_type_times[move_type][mode] += move.time[mode];
if (move.type == EMoveType::Retract || move.type == EMoveType::Unretract)
m_move_type_distances[move_type] += std::fabs(move.delta_extruder);
else
m_move_type_distances[move_type] += move.travel_dist;
}
}
m_only_gcode_in_preview = only_gcode;
m_sequential_view.gcode_window.load_gcode(gcode_result.filename, gcode_result.lines_ends);
@@ -1449,6 +1492,29 @@ void GCodeViewer::load_as_preview(libvgcode::GCodeInputData&& data)
{
m_loaded_as_preview = true;
m_move_type_counts.fill(0);
for (auto& move_type_times : m_move_type_times)
move_type_times.fill(0.0f);
m_move_type_distances.fill(0.0f);
const size_t normal_time_mode_idx = static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Normal);
for (size_t i = 0; i < data.vertices.size(); ++i) {
const libvgcode::PathVertex& vertex = data.vertices[i];
const size_t move_type = static_cast<size_t>(vertex.type);
if (move_type < m_move_type_counts.size()) {
++m_move_type_counts[move_type];
for (size_t mode = 0; mode < vertex.times.size(); ++mode)
m_move_type_times[move_type][mode] += vertex.times[mode];
if (vertex.type == libvgcode::EMoveType::Retract || vertex.type == libvgcode::EMoveType::Unretract) {
m_move_type_distances[move_type] += std::fabs(vertex.feedrate) * vertex.times[normal_time_mode_idx];
} else if (i > 0) {
const float dx = vertex.position[0] - data.vertices[i - 1].position[0];
const float dy = vertex.position[1] - data.vertices[i - 1].position[1];
const float dz = vertex.position[2] - data.vertices[i - 1].position[2];
m_move_type_distances[move_type] += std::sqrt(dx * dx + dy * dy + dz * dz);
}
}
}
m_viewer.set_extrusion_role_color(libvgcode::EGCodeExtrusionRole::Skirt, { 127, 255, 127 });
m_viewer.set_extrusion_role_color(libvgcode::EGCodeExtrusionRole::ExternalPerimeter, { 255, 255, 0 });
m_viewer.set_extrusion_role_color(libvgcode::EGCodeExtrusionRole::SupportMaterial, { 127, 255, 127 });
@@ -1496,6 +1562,10 @@ void GCodeViewer::reset()
m_extruders_count = 0;
m_filament_diameters = std::vector<float>();
m_filament_densities = std::vector<float>();
m_move_type_counts.fill(0);
for (auto& move_type_times : m_move_type_times)
move_type_times.fill(0.0f);
m_move_type_distances.fill(0.0f);
m_print_statistics.reset();
m_custom_gcode_per_print_z = std::vector<CustomGCode::Item>();
m_left_extruder_filament.clear();
@@ -2676,39 +2746,42 @@ void GCodeViewer::render_all_plates_stats(const std::vector<const GCodeProcessor
columns_offsets.push_back({ std::to_string(it->first + 1), offsets[_u8L("Filament")]});
char buf[64];
double unit_conver = imperial_units ? GizmoObjectManipulation::oz_to_g : 1.0;
float column_sum_m = 0.0f;
float column_sum_g = 0.0f;
if (displayed_columns & ColumnData::Model) {
const std::string weight_text = format_compact_weight(model_used_filaments_g_all_plates[i], imperial_units);
if ((displayed_columns & ~ColumnData::Model) > 0)
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", model_used_filaments_m_all_plates[i], model_used_filaments_g_all_plates[i] / unit_conver);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", model_used_filaments_m_all_plates[i], weight_text.c_str());
else
::sprintf(buf, imperial_units ? "%.2f in %.2f oz" : "%.2f m %.2f g", model_used_filaments_m_all_plates[i], model_used_filaments_g_all_plates[i] / unit_conver);
::sprintf(buf, imperial_units ? "%.2f in %s" : "%.2f m %s", model_used_filaments_m_all_plates[i], weight_text.c_str());
columns_offsets.push_back({ buf, offsets[_u8L("Model")] });
column_sum_m += model_used_filaments_m_all_plates[i];
column_sum_g += model_used_filaments_g_all_plates[i];
}
if (displayed_columns & ColumnData::Support) {
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", support_used_filaments_m_all_plates[i], support_used_filaments_g_all_plates[i] / unit_conver);
const std::string weight_text = format_compact_weight(support_used_filaments_g_all_plates[i], imperial_units);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", support_used_filaments_m_all_plates[i], weight_text.c_str());
columns_offsets.push_back({ buf, offsets[_u8L("Support")] });
column_sum_m += support_used_filaments_m_all_plates[i];
column_sum_g += support_used_filaments_g_all_plates[i];
}
if (displayed_columns & ColumnData::Flushed) {
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", flushed_filaments_m_all_plates[i], flushed_filaments_g_all_plates[i] / unit_conver);
const std::string weight_text = format_compact_weight(flushed_filaments_g_all_plates[i], imperial_units);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", flushed_filaments_m_all_plates[i], weight_text.c_str());
columns_offsets.push_back({ buf, offsets[_u8L("Flushed")] });
column_sum_m += flushed_filaments_m_all_plates[i];
column_sum_g += flushed_filaments_g_all_plates[i];
}
if (displayed_columns & ColumnData::WipeTower) {
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", wipe_tower_used_filaments_m_all_plates[i], wipe_tower_used_filaments_g_all_plates[i] / unit_conver);
const std::string weight_text = format_compact_weight(wipe_tower_used_filaments_g_all_plates[i], imperial_units);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", wipe_tower_used_filaments_m_all_plates[i], weight_text.c_str());
columns_offsets.push_back({ buf, offsets[_u8L("Tower")] });
column_sum_m += wipe_tower_used_filaments_m_all_plates[i];
column_sum_g += wipe_tower_used_filaments_g_all_plates[i];
}
if ((displayed_columns & ~ColumnData::Model) > 0) {
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", column_sum_m, column_sum_g / unit_conver);
const std::string weight_text = format_compact_weight(column_sum_g, imperial_units);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", column_sum_m, weight_text.c_str());
columns_offsets.push_back({ buf, offsets[_u8L("Total")] });
}
@@ -3072,7 +3145,9 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
const PrintEstimatedStatistics::Mode& time_mode = m_print_statistics.modes[static_cast<size_t>(m_viewer.get_time_mode())];
const libvgcode::EViewType curr_view_type = m_viewer.get_view_type();
const int curr_view_type_i = static_cast<int>(curr_view_type);
bool show_estimated_time = time_mode.time > 0.0f && (curr_view_type == libvgcode::EViewType::FeatureType ||
const size_t current_time_mode = static_cast<size_t>(m_viewer.get_time_mode());
const float total_estimated_time = time_mode.time > 0.0f ? time_mode.time : m_viewer.get_estimated_time();
bool show_estimated_time = total_estimated_time > 0.0f && (curr_view_type == libvgcode::EViewType::FeatureType ||
curr_view_type == libvgcode::EViewType::LayerTimeLinear || curr_view_type == libvgcode::EViewType::LayerTimeLogarithmic ||
(curr_view_type == libvgcode::EViewType::ColorPrint && !time_mode.custom_gcode_times.empty()));
@@ -3086,6 +3161,39 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
ImVec2 pos_rect = ImGui::GetCursorScreenPos();
float window_padding = 4.0f * m_scale;
auto format_compact_count = [](unsigned long long value) {
static constexpr const char* suffixes[] = { "", "K", "M", "B", "T", "P", "E" };
constexpr size_t suffix_count = sizeof(suffixes) / sizeof(suffixes[0]);
if (value < 1000)
return std::to_string(value);
size_t suffix_index = 0;
unsigned long long divisor = 1;
while (suffix_index + 1 < suffix_count && value / divisor >= 1000) {
divisor *= 1000;
++suffix_index;
}
const unsigned long long whole = value / divisor;
const unsigned long long tenths = (value % divisor) * 10 / divisor;
std::string ret = std::to_string(whole);
if (tenths != 0)
ret += "." + std::to_string(tenths);
ret += suffixes[suffix_index];
return ret;
};
auto format_percent = [](float percent) {
if (percent == 0.0f)
return std::string("0");
char buffer[64];
percent > 0.001f ? ::sprintf(buffer, "%.1f", percent * 100.0f) : ::sprintf(buffer, "<0.1");
return std::string(buffer);
};
// ORCA dont use background on top bar to give modern look
//draw_list->AddRectFilled(ImVec2(pos_rect.x,pos_rect.y - ImGui::GetStyle().WindowPadding.y),
//ImVec2(pos_rect.x + ImGui::GetWindowWidth() + ImGui::GetFrameHeight(),pos_rect.y + ImGui::GetFrameHeight() + window_padding * 2.5),
@@ -3122,10 +3230,10 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
case EItemType::Line: {
draw_list->AddLine({ pos.x + 1, pos.y + icon_size + 2 }, { pos.x + icon_size - 1, pos.y + 4 }, ImGuiWrapper::to_ImU32(color), 3.0f);
break;
}
case EItemType::None:
break;
}
}
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(20.0 * m_scale, 6.0 * m_scale));
@@ -3297,14 +3405,26 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
return _u8L("from") + " " + std::string(buf1) + " " + _u8L("to") + " " + std::string(buf2) + " " + _u8L("mm");
};
auto role_time_and_percent = [this, time_mode](libvgcode::EGCodeExtrusionRole role) {
auto role_time_and_percent = [this, total_estimated_time](libvgcode::EGCodeExtrusionRole role) {
const float time = m_viewer.get_extrusion_role_estimated_time(role);
return std::make_pair(time, time / time_mode.time);
return std::make_pair(time, total_estimated_time > 0.0f ? time / total_estimated_time : 0.0f);
};
auto travel_time_and_percent = [this, time_mode]() {
auto travel_time_and_percent = [this, total_estimated_time]() {
const float time = m_viewer.get_travels_estimated_time();
return std::make_pair(time, time / time_mode.time);
return std::make_pair(time, total_estimated_time > 0.0f ? time / total_estimated_time : 0.0f);
};
auto format_distance = [imperial_units](float distance_mm) {
char buffer[64];
if (imperial_units) {
::sprintf(buffer, "%.2fin", distance_mm / GizmoObjectManipulation::in_to_mm);
} else if (std::fabs(distance_mm) < 1000.0f) {
::sprintf(buffer, "%.0fmm", distance_mm);
} else {
::sprintf(buffer, "%.2fm", distance_mm / 1000.0f);
}
return std::string(buffer);
};
auto used_filament_per_role = [this, imperial_units](ExtrusionRole role) {
@@ -3409,6 +3529,8 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
std::vector<std::string> used_filaments_length;
std::vector<std::string> used_filaments_weight;
std::string travel_percent;
std::string travel_distance;
std::string travel_moves;
std::vector<double> model_used_filaments_m;
std::vector<double> model_used_filaments_g;
double total_model_used_filament_m = 0, total_model_used_filament_g = 0;
@@ -3528,8 +3650,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
auto [model_used_filament_m, model_used_filament_g] = used_filament_per_role(convert(role));
::sprintf(buffer, imperial_units ? "%.2fin" : "%.2fm", model_used_filament_m); // ORCA dont use spacing between value and unit
used_filaments_length.push_back(buffer);
::sprintf(buffer, imperial_units ? "%.2foz" : "%.2fg", model_used_filament_g); // ORCA dont use spacing between value and unit
used_filaments_weight.push_back(buffer);
used_filaments_weight.push_back(format_compact_weight(model_used_filament_g, imperial_units));
}
}
@@ -3542,10 +3663,19 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
else
percent > 0.001 ? ::sprintf(buffer, "%.1f", percent * 100) : ::sprintf(buffer, "<0.1");
travel_percent = buffer;
percents.push_back(travel_percent);
// Set travel distance and moves for the Travel row Usage columns
travel_distance = format_distance(m_print_statistics.total_travel_distance);
used_filaments_length.push_back(travel_distance);
travel_moves = format_compact_count(m_print_statistics.total_travel_moves);
used_filaments_weight.push_back(travel_moves);
}
// ORCA use % symbol for percentage and use "Usage" for "Used filaments"
offsets = calculate_offsets({ {_u8L("Line Type"), labels}, {_u8L("Time"), times}, {"%", percents}, {"", used_filaments_length}, {"", used_filaments_weight}, {_u8L("Display"), {""}}}, icon_size);
percents.pop_back();
append_headers({{_u8L("Line Type"), offsets[0]}, {_u8L("Time"), offsets[1]}, {"%", offsets[2]}, {_u8L("Usage"), offsets[3]}, {_u8L("Display"), offsets[5]}});
break;
}
@@ -3609,7 +3739,8 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
{
std::vector<std::string> total_filaments;
char buffer[64];
::sprintf(buffer, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", ps.total_used_filament / /*1000*/koef, ps.total_weight / unit_conver);
const std::string total_weight_text = format_compact_weight(ps.total_weight, imperial_units);
::sprintf(buffer, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", ps.total_used_filament / /*1000*/koef, total_weight_text.c_str());
total_filaments.push_back(buffer);
@@ -3644,9 +3775,54 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
default: { break; }
}
auto append_option_item = [this, append_item](libvgcode::EOptionType type, std::vector<float> offsets) {
auto append_option_item_with_type = [this, offsets, append_item](libvgcode::EOptionType type, const ColorRGBA& color, const std::string& label, bool visible) {
append_item(EItemType::Rect, color, {{ label , offsets[0] }}, true, offsets.back()/*ORCA checkbox_pos*/, visible, [this, type, visible]() {
auto append_option_item = [this, append_item, current_time_mode, total_estimated_time, &format_compact_count, &format_percent, &format_distance](libvgcode::EOptionType type, std::vector<float> offsets) {
const bool full_layout = offsets.size() > 4;
auto option_stats = [this, current_time_mode, total_estimated_time, &format_compact_count, &format_percent, &format_distance, full_layout](libvgcode::EOptionType option_type) -> std::array<std::string, 4> {
libvgcode::EMoveType move_type;
bool has_move_type = true;
switch (option_type) {
case libvgcode::EOptionType::Wipes: { move_type = libvgcode::EMoveType::Wipe; break; }
case libvgcode::EOptionType::Retractions: { move_type = libvgcode::EMoveType::Retract; break; }
case libvgcode::EOptionType::Unretractions: { move_type = libvgcode::EMoveType::Unretract; break; }
case libvgcode::EOptionType::Seams: { move_type = libvgcode::EMoveType::Seam; break; }
case libvgcode::EOptionType::ToolChanges: { move_type = libvgcode::EMoveType::ToolChange; break; }
default: { has_move_type = false; break; }
}
if (!has_move_type)
return { "", "", "", "" };
const size_t move_type_idx = static_cast<size_t>(move_type);
float time = m_move_type_times[move_type_idx][current_time_mode];
if (option_type == libvgcode::EOptionType::ToolChanges) {
// Toolchange delays are injected via synchronize() and are not attributed to ToolChange move vertices.
time = m_print_statistics.total_filament_load_time + m_print_statistics.total_filament_unload_time + m_print_statistics.total_tool_change_time;
}
const std::string time_text = full_layout && time > 0.0f ? short_time(get_time_dhms(time)) : "";
const std::string percent_text = full_layout && total_estimated_time > 0.0f ? format_percent(time / total_estimated_time) : "";
const float seam_distance = m_print_statistics.total_seam_gap_distance + m_print_statistics.total_seam_scarf_distance;
const float distance = (option_type == libvgcode::EOptionType::Seams && seam_distance > 0.0f) ? seam_distance : m_move_type_distances[move_type_idx];
const std::string distance_text = full_layout && (option_type == libvgcode::EOptionType::Wipes || option_type == libvgcode::EOptionType::Retractions || option_type == libvgcode::EOptionType::Unretractions || option_type == libvgcode::EOptionType::Seams)
? format_distance(distance)
: "";
const std::string count_text = full_layout ? format_compact_count(m_move_type_counts[move_type_idx]) : "";
return { time_text, percent_text, distance_text, count_text };
};
auto append_option_item_with_type = [this, offsets, append_item, full_layout](libvgcode::EOptionType type, const ColorRGBA& color, const std::string& label, bool visible,
const std::string& time_text, const std::string& percent_text, const std::string& distance_text, const std::string& count_text) {
std::vector<std::pair<std::string, float>> columns_offsets;
columns_offsets.push_back({ label , offsets[0] });
if (full_layout && !time_text.empty())
columns_offsets.push_back({ time_text, offsets[1] });
if (full_layout && !percent_text.empty())
columns_offsets.push_back({ percent_text, offsets[2] });
if (full_layout && !distance_text.empty())
columns_offsets.push_back({ distance_text, offsets[3] });
if (full_layout && !count_text.empty())
columns_offsets.push_back({ count_text, distance_text.empty() ? offsets[3] : offsets[4] });
append_item(EItemType::Rect, color, columns_offsets, true, offsets.back()/*ORCA checkbox_pos*/, visible, [this, type, visible]() {
m_viewer.toggle_option_visibility(type);
update_moves_slider();
});
@@ -3654,18 +3830,33 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
const bool visible = m_viewer.is_option_visible(type);
if (type == libvgcode::EOptionType::Travels) {
//BBS: only display travel time in FeatureType view
append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Travels)), _u8L("Travel"), visible);
append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Travels)), _u8L("Travel"), visible, "", "", "", "");
}
else if (type == libvgcode::EOptionType::Seams) {
const auto option_values = option_stats(type);
append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Seams)), _u8L("Seams"), visible,
option_values[0], option_values[1], option_values[2], option_values[3]);
}
else if (type == libvgcode::EOptionType::Retractions) {
const auto option_values = option_stats(type);
append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Retractions)), _u8L("Retract"), visible,
option_values[0], option_values[1], option_values[2], option_values[3]);
}
else if (type == libvgcode::EOptionType::Unretractions) {
const auto option_values = option_stats(type);
append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Unretractions)), _u8L("Unretract"), visible,
option_values[0], option_values[1], option_values[2], option_values[3]);
}
else if (type == libvgcode::EOptionType::ToolChanges) {
const auto option_values = option_stats(type);
append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::ToolChanges)), _u8L("Filament Changes"), visible,
option_values[0], option_values[1], option_values[2], option_values[3]);
}
else if (type == libvgcode::EOptionType::Wipes) {
const auto option_values = option_stats(type);
append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Wipes)), _u8L("Wipe"), visible,
option_values[0], option_values[1], option_values[2], option_values[3]);
}
else if (type == libvgcode::EOptionType::Seams)
append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Seams)), _u8L("Seams"), visible);
else if (type == libvgcode::EOptionType::Retractions)
append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Retractions)), _u8L("Retract"), visible);
else if (type == libvgcode::EOptionType::Unretractions)
append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Unretractions)), _u8L("Unretract"), visible);
else if (type == libvgcode::EOptionType::ToolChanges)
append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::ToolChanges)), _u8L("Filament Changes"), visible);
else if (type == libvgcode::EOptionType::Wipes)
append_option_item_with_type(type, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Wipes)), _u8L("Wipe"), visible);
};
const libvgcode::EViewType new_view_type = curr_view_type;
@@ -3705,6 +3896,8 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
columns_offsets.push_back({ _u8L("Travel"), offsets[0] });
columns_offsets.push_back({ travel_time, offsets[1] });
columns_offsets.push_back({ travel_percent, offsets[2] });
columns_offsets.push_back({ travel_distance, offsets[3] }); // Usage column
columns_offsets.push_back({ travel_moves, offsets[4] }); // Usage column
append_item(EItemType::Rect, libvgcode::convert(m_viewer.get_option_color(libvgcode::EOptionType::Travels)), columns_offsets, true, offsets.back()/*ORCA checkbox_pos*/, visible, [this, item, visible]() {
m_viewer.toggle_option_visibility(item);
update_moves_slider();
@@ -3796,7 +3989,8 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
size_t i = 0;
const std::vector<uint8_t>& used_extruders_ids = m_viewer.get_used_extruders_ids();
for (uint8_t extruder_id : used_extruders_ids) {
::sprintf(buf, imperial_units ? "%.2f in %.2f g" : "%.2f m %.2f g", model_used_filaments_m[i], model_used_filaments_g[i]);
const std::string weight_text = format_compact_weight(model_used_filaments_g[i], imperial_units);
::sprintf(buf, imperial_units ? "%.2f in %s" : "%.2f m %s", model_used_filaments_m[i], weight_text.c_str());
append_item(EItemType::Rect, libvgcode::convert(m_viewer.get_tool_colors()[extruder_id]), { { _u8L("Extruder") + " " + std::to_string(extruder_id + 1), offsets[0]}, {buf, offsets[1]} });
// append_item(EItemType::Rect, libvgcode::convert(m_viewer.get_tool_colors()[extruder_id]), _u8L("Extruder") + " " + std::to_string(extruder_id + 1),
// true, "", 0.0f, 0.0f, offsets, used_filaments_m[extruder_id], used_filaments_g[extruder_id]);
@@ -3809,7 +4003,8 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
char buf[64];
imgui.text(_u8L("Total") + ":");
ImGui::SameLine();
::sprintf(buf, imperial_units ? "%.2f in / %.2f oz" : "%.2f m / %.2f g", ps.total_used_filament / koef, ps.total_weight / unit_conver);
const std::string total_weight_text = format_compact_weight(ps.total_weight, imperial_units);
::sprintf(buf, imperial_units ? "%.2f in / %s" : "%.2f m / %s", ps.total_used_filament / koef, total_weight_text.c_str());
imgui.text(buf);
ImGui::Dummy({window_padding, window_padding});
@@ -3855,34 +4050,39 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
float column_sum_m = 0.0f;
float column_sum_g = 0.0f;
if (displayed_columns & ColumnData::Model) {
const std::string weight_text = format_compact_weight(model_used_filaments_g[i], imperial_units);
if ((displayed_columns & ~ColumnData::Model) > 0)
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", model_used_filaments_m[i], model_used_filaments_g[i] / unit_conver);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", model_used_filaments_m[i], weight_text.c_str());
else
::sprintf(buf, imperial_units ? "%.2f in %.2f oz" : "%.2f m %.2f g", model_used_filaments_m[i], model_used_filaments_g[i] / unit_conver);
::sprintf(buf, imperial_units ? "%.2f in %s" : "%.2f m %s", model_used_filaments_m[i], weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Model")] });
column_sum_m += model_used_filaments_m[i];
column_sum_g += model_used_filaments_g[i];
}
if (displayed_columns & ColumnData::Support) {
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", support_used_filaments_m[i], support_used_filaments_g[i] / unit_conver);
const std::string weight_text = format_compact_weight(support_used_filaments_g[i], imperial_units);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", support_used_filaments_m[i], weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Support")] });
column_sum_m += support_used_filaments_m[i];
column_sum_g += support_used_filaments_g[i];
}
if (displayed_columns & ColumnData::Flushed) {
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", flushed_filaments_m[i], flushed_filaments_g[i] / unit_conver);
const std::string weight_text = format_compact_weight(flushed_filaments_g[i], imperial_units);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", flushed_filaments_m[i], weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Flushed")]});
column_sum_m += flushed_filaments_m[i];
column_sum_g += flushed_filaments_g[i];
}
if (displayed_columns & ColumnData::WipeTower) {
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", wipe_tower_used_filaments_m[i], wipe_tower_used_filaments_g[i] / unit_conver);
const std::string weight_text = format_compact_weight(wipe_tower_used_filaments_g[i], imperial_units);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", wipe_tower_used_filaments_m[i], weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Tower")] });
column_sum_m += wipe_tower_used_filaments_m[i];
column_sum_g += wipe_tower_used_filaments_g[i];
}
if ((displayed_columns & ~ColumnData::Model) > 0) {
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", column_sum_m, column_sum_g / unit_conver);
const std::string weight_text = format_compact_weight(column_sum_g, imperial_units);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", column_sum_m, weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Total")] });
}
@@ -3908,27 +4108,32 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
std::vector<std::pair<std::string, float>> columns_offsets;
columns_offsets.push_back({ _u8L("Total"), color_print_offsets[_u8L("Filament")]});
if (displayed_columns & ColumnData::Model) {
const std::string weight_text = format_compact_weight(total_model_used_filament_g, imperial_units);
if ((displayed_columns & ~ColumnData::Model) > 0)
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", total_model_used_filament_m, total_model_used_filament_g / unit_conver);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", total_model_used_filament_m, weight_text.c_str());
else
::sprintf(buf, imperial_units ? "%.2f in %.2f oz" : "%.2f m %.2f g", total_model_used_filament_m, total_model_used_filament_g / unit_conver);
::sprintf(buf, imperial_units ? "%.2f in %s" : "%.2f m %s", total_model_used_filament_m, weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Model")] });
}
if (displayed_columns & ColumnData::Support) {
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", total_support_used_filament_m, total_support_used_filament_g / unit_conver);
const std::string weight_text = format_compact_weight(total_support_used_filament_g, imperial_units);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", total_support_used_filament_m, weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Support")] });
}
if (displayed_columns & ColumnData::Flushed) {
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", total_flushed_filament_m, total_flushed_filament_g / unit_conver);
const std::string weight_text = format_compact_weight(total_flushed_filament_g, imperial_units);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", total_flushed_filament_m, weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Flushed")] });
}
if (displayed_columns & ColumnData::WipeTower) {
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", total_wipe_tower_used_filament_m, total_wipe_tower_used_filament_g / unit_conver);
const std::string weight_text = format_compact_weight(total_wipe_tower_used_filament_g, imperial_units);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", total_wipe_tower_used_filament_m, weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Tower")] });
}
if ((displayed_columns & ~ColumnData::Model) > 0) {
::sprintf(buf, imperial_units ? "%.2f in\n%.2f oz" : "%.2f m\n%.2f g", total_model_used_filament_m + total_support_used_filament_m + total_flushed_filament_m + total_wipe_tower_used_filament_m,
(total_model_used_filament_g + total_support_used_filament_g + total_flushed_filament_g + total_wipe_tower_used_filament_g) / unit_conver);
const std::string weight_text = format_compact_weight(total_model_used_filament_g + total_support_used_filament_g + total_flushed_filament_g + total_wipe_tower_used_filament_g, imperial_units);
::sprintf(buf, imperial_units ? "%.2f in\n%s" : "%.2f m\n%s", total_model_used_filament_m + total_support_used_filament_m + total_flushed_filament_m + total_wipe_tower_used_filament_m,
weight_text.c_str());
columns_offsets.push_back({ buf, color_print_offsets[_u8L("Total")] });
}
append_item(EItemType::None, libvgcode::convert(tool_colors[0]), columns_offsets);
@@ -3939,16 +4144,14 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
ImGui::SameLine();
imgui.text(_u8L("Filament change times") + ":");
ImGui::SameLine();
::sprintf(buf, "%d", m_print_statistics.total_filament_changes);
imgui.text(buf);
imgui.text(format_compact_count(m_print_statistics.total_filament_changes));
//display tool change times
ImGui::Dummy({window_padding, window_padding});
ImGui::SameLine();
imgui.text(_u8L("Tool changes") + ":");
ImGui::SameLine();
::sprintf(buf, "%d", m_print_statistics.total_extruder_changes);
imgui.text(buf);
imgui.text(format_compact_count(m_print_statistics.total_extruder_changes));
//BBS display cost
ImGui::Dummy({ window_padding, window_padding });
@@ -4075,8 +4278,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
imgui.text(buffer);
ImGui::SameLine(offsets[3]);
::sprintf(buffer, "%.2f g", used_filament.second);
imgui.text(buffer);
imgui.text(format_compact_weight(used_filament.second, imperial_units));
}
};
@@ -4401,8 +4603,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
::sprintf(buf, imperial_units ? "%.2f in" : "%.2f m", ps.total_used_filament / koef);
imgui.text(buf);
ImGui::SameLine();
::sprintf(buf, imperial_units ? " %.2f oz" : " %.2f g", ps.total_weight / unit_conver);
imgui.text(buf);
imgui.text(" " + format_compact_weight(ps.total_weight, imperial_units));
ImGui::Dummy({ window_padding, window_padding });
ImGui::SameLine();
imgui.text(model_filament_str + ":");
@@ -4412,8 +4613,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
::sprintf(buf, imperial_units ? "%.2f in" : "%.2f m", ps.total_used_filament / koef - exlude_m);
imgui.text(buf);
ImGui::SameLine();
::sprintf(buf, imperial_units ? " %.2f oz" : " %.2f g", (ps.total_weight - exlude_g) / unit_conver);
imgui.text(buf);
imgui.text(" " + format_compact_weight(ps.total_weight - exlude_g, imperial_units));
//BBS: display cost of filaments
ImGui::Dummy({ window_padding, window_padding });
ImGui::SameLine();

View File

@@ -14,6 +14,7 @@
// needed for tech VGCODE_ENABLE_COG_AND_TOOL_MARKERS
#include <libvgcode/include/Types.hpp>
#include <array>
#include <cstdint>
#include <float.h>
#include <set>
@@ -184,6 +185,9 @@ private:
unsigned int m_last_result_id{ 0 };
//BBS: save m_gcode_result as well
const GCodeProcessorResult* m_gcode_result;
std::array<unsigned int, static_cast<size_t>(EMoveType::Count)> m_move_type_counts{};
std::array<std::array<float, static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count)>, static_cast<size_t>(EMoveType::Count)> m_move_type_times{};
std::array<float, static_cast<size_t>(EMoveType::Count)> m_move_type_distances{};
//BBS: add only gcode mode
bool m_only_gcode_in_preview {false};

View File

@@ -279,9 +279,12 @@ ParamsPanel::ParamsPanel( wxWindow* parent, wxWindowID id, const wxPoint& pos, c
});
m_mode_icon->SetToolTip(_L("Cycle settings visibility"));
m_mode_view = new ModeSwitchButton(m_top_panel);
m_mode_view->SetSelection(mode_to_selection(wxGetApp().get_saved_mode()));
if (wxGetApp().get_mode() == comDevelop)
if (wxGetApp().get_mode() == comDevelop) {
m_mode_view->SetSelection(mode_to_selection(comExpert));
m_mode_view->Enable(false);
} else {
m_mode_view->SetSelection(mode_to_selection(wxGetApp().get_saved_mode()));
}
// BBS: new layout
//m_search_btn = new ScalableButton(m_top_panel, wxID_ANY, "search", wxEmptyString, wxDefaultSize, wxDefaultPosition, wxBU_EXACTFIT | wxNO_BORDER, true);
@@ -648,12 +651,13 @@ void ParamsPanel::update_mode()
if (mode_view == nullptr)
return;
mode_view->SetSelection(mode_to_selection(Slic3r::GUI::wxGetApp().get_saved_mode()));
if (app_mode == comDevelop) {
mode_view->SetSelection(mode_to_selection(comExpert));
mode_view->Enable(false);
return;
}
mode_view->SetSelection(mode_to_selection(Slic3r::GUI::wxGetApp().get_saved_mode()));
if (!mode_view->IsEnabled())
mode_view->Enable();
};

View File

@@ -424,9 +424,12 @@ void Tab::create_preset_tab()
});
m_top_sizer->Add(m_mode_icon, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::WideSpacing()));
m_mode_view = new ModeSwitchButton(m_top_panel);
m_mode_view->SetSelection(mode_to_selection(wxGetApp().get_saved_mode()));
if (wxGetApp().get_mode() == comDevelop)
if (wxGetApp().get_mode() == comDevelop) {
m_mode_view->SetSelection(mode_to_selection(comExpert));
m_mode_view->Enable(false);
} else {
m_mode_view->SetSelection(mode_to_selection(wxGetApp().get_saved_mode()));
}
m_top_sizer->AddSpacer(FromDIP(SidebarProps::ElementSpacing()));
m_top_sizer->Add( m_mode_view, 0, wxALIGN_CENTER_VERTICAL);
}

View File

@@ -5,6 +5,7 @@
#include "libslic3r/AppConfig.hpp"
#include "slic3r/GUI/wxExtensions.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/Utils/NetworkAgent.hpp"
#include "libslic3r_version.h"
#include <wx/sizer.h>
@@ -371,7 +372,97 @@ void ZUserLogin::OnScriptMessage(wxWebViewEvent &evt)
{
m_AutotestToken = j["data"]["token"];
}
if (strCmd == "user_login") {
if (strCmd == "user_ticket_login") {
auto* agent = wxGetApp().getAgent();
if (!agent || !m_cloud_agent || !j.contains("data") || !j["data"].is_object() || !j["data"].contains("ticket")) {
wxMessageBox(_L("Login failed. Please try again."), _L("Login"), wxICON_WARNING);
return;
}
const auto provider = m_cloud_agent->get_id();
std::string ticket = j["data"]["ticket"].get<std::string>();
unsigned int token_http_code = 0;
std::string token_body;
int token_result = agent->get_my_token(ticket, &token_http_code, &token_body, provider);
if (token_result != 0) {
BOOST_LOG_TRIVIAL(warning) << "embedded_login: get_my_token failed, http_code=" << token_http_code;
wxMessageBox(_L("Login failed. Please try again."), _L("Login"), wxICON_WARNING);
return;
}
std::string access_token;
std::string refresh_token;
std::string expires_in_str;
std::string refresh_expires_in_str;
try {
json token_j = json::parse(token_body);
if (token_j.contains("accessToken"))
access_token = token_j["accessToken"].get<std::string>();
if (token_j.contains("refreshToken"))
refresh_token = token_j["refreshToken"].get<std::string>();
if (token_j.contains("expiresIn"))
expires_in_str = std::to_string(token_j["expiresIn"].get<double>());
if (token_j.contains("refreshExpiresIn"))
refresh_expires_in_str = std::to_string(token_j["refreshExpiresIn"].get<double>());
} catch (...) {
wxMessageBox(_L("Login failed. Please try again."), _L("Login"), wxICON_WARNING);
return;
}
if (access_token.empty()) {
wxMessageBox(_L("Login failed. Please try again."), _L("Login"), wxICON_WARNING);
return;
}
unsigned int profile_http_code = 0;
std::string profile_body;
int profile_result = agent->get_my_profile(access_token, &profile_http_code, &profile_body, provider);
if (profile_result != 0) {
BOOST_LOG_TRIVIAL(warning) << "embedded_login: get_my_profile failed, http_code=" << profile_http_code;
wxMessageBox(_L("Login failed. Please try again."), _L("Login"), wxICON_WARNING);
return;
}
std::string user_id;
std::string user_name;
std::string user_account;
std::string user_avatar;
try {
json user_j = json::parse(profile_body);
if (user_j.contains("uidStr"))
user_id = user_j["uidStr"].get<std::string>();
if (user_j.contains("name"))
user_name = user_j["name"].get<std::string>();
if (user_j.contains("avatar"))
user_avatar = user_j["avatar"].get<std::string>();
if (user_j.contains("account"))
user_account = user_j["account"].get<std::string>();
} catch (...) {
BOOST_LOG_TRIVIAL(warning) << "embedded_login: profile JSON parse failed";
}
json login_j;
login_j["command"] = "user_login";
login_j["data"]["autotest_token"] = m_AutotestToken;
login_j["data"]["refresh_token"] = refresh_token;
login_j["data"]["token"] = access_token;
login_j["data"]["expires_in"] = expires_in_str;
login_j["data"]["refresh_expires_in"] = refresh_expires_in_str;
login_j["data"]["user"]["uid"] = user_id;
login_j["data"]["user"]["name"] = user_name;
login_j["data"]["user"]["account"] = user_account;
login_j["data"]["user"]["avatar"] = user_avatar;
std::string message_json = login_j.dump();
// End modal dialog first to unblock event loop before processing callbacks
EndModal(wxID_OK);
// Handle message after modal dialog ends to avoid deadlock
// Use wxTheApp->CallAfter to ensure it runs after modal loop exits
wxTheApp->CallAfter([message_json, provider]() { wxGetApp().handle_script_message(message_json, provider); });
}
else if (strCmd == "user_login") {
j["data"]["autotest_token"] = m_AutotestToken;
std::string message_json = j.dump();

View File

@@ -372,27 +372,31 @@ WipingDialog::WipingDialog(wxWindow* parent, const int max_flush_volume) :
wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
this->SetSizer(main_sizer);
this->SetBackgroundColour(*wxWHITE);
auto filament_count= wxGetApp().preset_bundle->project_config.option<ConfigOptionStrings>("filament_colour")->values.size();
wxSize extra_size = { FromDIP(100),FromDIP(235) };
if (filament_count <= 2)
extra_size.y += FromDIP(16) * 3 + FromDIP(32);
else if (filament_count == 3)
extra_size.y += FromDIP(16) * 3;
else if (4 <= filament_count && filament_count <= 8)
extra_size.y += FromDIP(16) * 2;
else
extra_size.y += FromDIP(16);
auto filament_count = wxGetApp().preset_bundle->project_config.option<ConfigOptionStrings>("filament_colour")->values.size();
wxSize max_scroll_size = { FromDIP(1000),FromDIP(500) };
wxSize estimate_size = { (int)(filament_count + 1) * FromDIP(60),(int)(filament_count + 1) * FromDIP(30)+FromDIP(2)};
wxSize scroll_size ={ std::min(max_scroll_size.x,estimate_size.x),std::min(max_scroll_size.y,estimate_size.y) };
wxSize applied_size = scroll_size + extra_size;
// Estimate table scroll area size based on filament count
// Each table cell is ~60x25 DIP, plus headers and borders
wxSize max_scroll_size = { FromDIP(1000), FromDIP(500) };
wxSize table_size = { (int)(filament_count + 1) * FromDIP(60), (int)(filament_count + 1) * FromDIP(25) + FromDIP(2) };
wxSize scroll_size = { std::min(max_scroll_size.x, table_size.x), std::min(max_scroll_size.y, table_size.y) };
// Fixed overhead: padding (~30), tip panel (~70), controls row (~50),
// description/multiplier section (~130), button row (~45) = ~325 DIP
wxSize fixed_overhead = { FromDIP(100), FromDIP(325) };
wxSize applied_size = scroll_size + fixed_overhead;
// Clamp to screen size (leave some margin for window decorations)
wxSize scaled_screen_size = wxGetDisplaySize();
double scale_factor = wxDisplay().GetScaleFactor();
scaled_screen_size = { (int)(scaled_screen_size.x / scale_factor),(int)(scaled_screen_size.y / scale_factor) };
scaled_screen_size = { (int)(scaled_screen_size.x / scale_factor), (int)(scaled_screen_size.y / scale_factor) };
wxSize screen_margin = { FromDIP(40), FromDIP(60) };
scaled_screen_size -= screen_margin;
applied_size = { std::min(applied_size.x,scaled_screen_size.x),std::min(applied_size.y,scaled_screen_size.y) };
applied_size = { std::min(applied_size.x, scaled_screen_size.x), std::min(applied_size.y, scaled_screen_size.y) };
// Ensure a reasonable minimum size so the dialog is usable even when clamped
applied_size.x = std::max(applied_size.x, FromDIP(350));
applied_size.y = std::max(applied_size.y, FromDIP(450));
m_webview = wxWebView::New(this, wxID_ANY,
wxEmptyString,
wxDefaultPosition,