Compare commits

...

13 Commits

Author SHA1 Message Date
Hanif Koh
2d7addc9c4 Decide Whether the Preview Is Dragging Before Consulting the Scene Cache
With the scene cache on, the frame after a release was served from the
cache. Whether the preview still draws its reduced set was decided inside
the scene pass, after the cache key had been compared, so on that frame the
key still matched the last drag frame, the kept reduced frame was shown
again, and the full detail only came back when the next mouse move changed
the camera. The decision now runs at the start of the frame, before the
cache is consulted, so the change of level lands in the release frame.

With the solid model chosen and the cache on: mid-drag the objects are
solids, and the frame after release, with no further input, draws the
toolpaths again.
2026-09-11 20:58:12 +08:00
Hanif Koh
a2f0606d32 Wake the Idle Loop When a Mouse Button Is Released
Releasing a button after a camera drag marks the canvas dirty and leaves the
frame to the next idle event, and on some platforms none follows a release
until the next input arrives. That frame is the one that puts the preview's
full detail back after a drag, so the preview stayed at its reduced set until
the mouse moved. The release now wakes the idle loop, as the wheel handler
already does for the same reason.
2026-09-11 20:40:30 +08:00
Hanif Koh
36cd425bb3 Offer the Sliced Objects as a Solid Model While Dragging
The reductions built for dragging still draw toolpaths, and a plate of large
objects has many even one layer in four. The objects themselves are cheaper:
a mesh costs its triangles once, and the preview already loads the objects
as shells for its translucent ghost. So the Simplification choice gains a
fourth entry, Solid model, that draws the shells opaque in their filament
colours instead of the toolpaths while the user drags.

The visible layer range still holds. The shells are cut at the range's top
and bottom through the shader's z range, and the toolpath set bound while
dragging keeps just those two layers, drawn afterwards so that they cap the
cut with what was really printed there. The prime tower is added to the
shells from its sliced mesh, positioned as the print placed it; it keeps its
opaque colour and so stays out of the translucent ghost. Supports have no
mesh and are not shown in this mode, which the preference text says.

Verified on the tall fixture and on a nine-cube plate: mid-drag the objects
and the tower are solids capped by their top layer, cut to a shortened layer
range, and with the preference off the preview is pixel identical to before.
2026-09-11 20:31:51 +08:00
Hanif Koh
c2eea1cada Keep the Drawn Preview While Nothing in It Moves
Every preview frame redrew the whole scene, whether the camera had moved or a
tooltip had faded. A cursor drifting over the canvas, a hovered legend row, a
notification or the frame rate overlay each cost a full draw of every toolpath,
so a large print stayed slow with nothing happening.

Behind a preference, the window's colour and depth are now copied into a
framebuffer of the same size, sample count and depth format right after the
scene pass, and while nothing that pass draws has changed they are copied
back instead of redrawn; only the panels, the marker and the overlays are
drawn on top. What counts as a change is the camera's matrices, the canvas
size, the hovered and current plate, the world axes, a colour mode change,
and two counters from the viewer: one that libvgcode bumps whenever it
applies an update or binds another set, one the G-code viewer bumps on load,
reset, shell changes and its own toggles. A pending viewer update also forces
a redraw. A driver that refuses the blit disables the cache for the session
and the preview falls back to drawing every frame.

The frames that follow a change cost what they did before; what changes is
that a frame with no change costs a blit. On the tall fixture under llvmpipe,
cursor motion over the canvas produced a frame every 600 ms and now every
110 ms, limited by the input rate. At rest, after a slider click and after a
wheel burst the preview is pixel identical with the preference on and off,
and with it off the preview is pixel identical to before.
2026-09-11 19:53:32 +08:00
Hanif Koh
038e86f02a Close the Steps of Sloped Surfaces in the Shell and Size Layers by Projection
On a dome or a cone the shell showed holes: dashes of inner wall and overhang
wall through gaps, and darkness behind them. Each layer's ring on a slope is
inset from the one below by less than a grid cell, so the height map cannot see
the step, and the interior of the lower layer that fills it, its first inner
wall above all, had been left out. Inner wall segments whose midpoint lies
within a line and a half of an outer wall segment of the same layer now stay on
the shell, found by exact distance against the outer walls bucketed by cell,
and a segment counts as visible from above or below when any of its cells is
the topmost or bottommost occupant, since the exposed band is narrower than the
chords that cross it. Interior roles that are visible that way stay too.

The layer stride at rest was sized from the camera's zoom value and tilt, which
does not describe a perspective view: zoomed in on the test project it reached
64, so each kept ring stood 6 mm tall while the surface curved away under it.
One layer's height is now projected through the camera's own matrices, at the
centre of the toolpaths and at the camera target, and the stride follows that.

On the test project the shell at rest gains 43 k segments for the first inner
walls, 171 k to 214 k, and the dome and cone draw closed. The nine-cube plate
draws 96 k at the default view and 12 k from above.
2026-09-11 17:37:36 +08:00
Hanif Koh
55ef9d2afd Keep Infill Off the Shell and Give Grown Segments a Flat Profile
On a plate of nine tall cubes, sparse infill is 1.36 M of the 1.57 M segments
and the geometric shell test kept 356 k of them: short gyroid segments hugging
a wall have most of their cells on the boundary ring. Infill can never be a
visible surface whatever its geometry, so the interior roles and gap fill are
now off the shell before the geometry is asked. The set drawn at rest on that
plate falls from 255 k to 76 k segments at the default view.

The dragging set kept the surfaces exposed to the neighbouring layer in the
layers it skips, which on those cubes was 171 k segments of footprint jitter
and made a drag slower than resting once layers were merged. It now keeps what
either side of the print can see, from the occupant maps, and the smaller of
the two sets stays bound through a drag: 43 k instead of 262 k here.

Grown segments showed bright stripes on the sides of tall parts. The segment
cross-section is a diamond, full width at mid-height and a point at top and
bottom, so a box grown four layers tall left a notch that deep between itself
and the next one, and its corner normals lit a bright edge every few layers
where the real print has one on every layer. When grown, the four profile
points now become the corners of a rectangle, a flat top and a flat camera-
facing side lit through their own normals, in both of the shader's view
branches. Preference off is pixel identical to before.
2026-09-11 14:57:17 +08:00
Hanif Koh
63d00f7cf1 Keep Only What the Camera's Side Can See in the Layers Skipped at Rest
With the shell at rest and layers merged, the skipped layers kept every segment
exposed to the layer next to it. On the tall fixture that was 25000 wipe tower
segments over its whole height: wherever the tower's footprint edge lands one
grid cell differently from one layer to the next, the cell reads as uncovered,
though hundreds of layers sit on top. The same test also kept whatever lies
under an overhang.

The classification now records the highest and lowest layer occupying each
cell over the whole print, in the pass that already rasterizes the layers, and
a segment is visible from above when its layer is the topmost occupant of at
least half its cells, and from below likewise with the bottommost. A skipped
layer at rest keeps only the segments visible from the side the camera is on,
which the viewer is told along with the stride. The exposed bit stays for the
dragging set, which is not rebuilt with the camera.

On the tall fixture with the shell at rest, against 334 ms at full detail:

  default view    17679 segments   18 ms   (was 37585 / 85 ms)
  from above       7935 segments    7 ms   (was 28682 / 58 ms)

The two-cube fixture still keeps every facing wall, and the classification
stays at 90 ms for 636 layers with the maps merged over each worker's touched
rectangle only.
2026-09-11 13:10:09 +08:00
Hanif Koh
f2143d3c91 Keep Facing Walls on the Shell and Merge Sub-Pixel Layers at Rest
Two cubes standing a few millimetres apart lost the walls that face each
other: the gap-closing that makes sparse infill read as solid bridged the gap
as well, so the footprint became one block and those walls were no longer on
its boundary. The closing now dilates each connected component on its own and
leaves a cell two components both reach empty, so no gap between objects is
ever bridged, and the raw cells are put back after the erosion, which would
otherwise eat into a wall that faces such a gap. On a two-cube fixture with a
2 mm gap every facing wall segment is kept where none was before.

Looking from above, every layer's walls project onto the same outline, yet
each cost a segment. With the shell drawn at rest the walls of one layer in N
are now drawn, N layers tall through a shader uniform so the wall stays solid,
while the exposed top and bottom surfaces of every layer stay so that no step
disappears. N follows the view: how many layers fit in two pixels at the
current zoom and tilt, from 1 side-on and close up to 64 straight down, and it
is left alone while the user is dragging since a change rebuilds the sets. The
same taller drawing applies to the dragging set, which stops it looking
striped, and in shell mode the dragging set keeps the exposed surfaces of the
layers it skips too. The first layer's grown boxes are clamped at the bed.

On the tall fixture with the shell at rest, against 334 ms at full detail:

  default view    37585 segments   85 ms   (was 215797 / 181 ms)
  from above      28682 segments   58 ms

The classification now records which shell segments are exposed from above or
below, in a second bit set alongside the first.
2026-09-10 18:09:22 +08:00
Hanif Koh
e0275c4d69 Let the User Leave Out Infill or Everything But the Shell at Rest Too
A plate of large objects can be slow to draw even when nothing is moving, and
the reductions built for dragging cannot help there because they skip layers.
So a second choice, applied at all times, leaves out the internal infill roles
or everything but the shell while drawing every layer. It is a third index
buffer filled by the same walk as the other two and bound whenever the dragging
set is not; the options need no counterpart since every layer is drawn. The
shell classification is shared, computed once when either choice asks for it.

On the 636-layer three-filament fixture at rest, against 448467 segments in
334 ms at full detail:

  internal infill left out        347208 segments  272 ms
  everything but the shell        215797 segments  181 ms

With the shell at rest and the shell at one layer in four while dragging, the
drag frame is 62 ms. At rest with the choice set to nothing, the preview is
pixel identical to before.
2026-09-10 17:07:33 +08:00
Hanif Koh
77d00c0ee4 Let the User Choose What the Simplified Preview Leaves Out While Dragging
The reduced toolpath set was only ever bound during a camera drag. The slider
check read is_dirty(), which the slider raises inside its own render and
_render_gcode() consumes at the end of the same frame, so it was never true at
the start of the next one; the only time it was true was the frame after a
keyboard or wheel step, which then drew one coarse frame followed by one full
frame. The navigator cube and the mouse wheel never engaged it at all, and the
extra frame requested on every level change was a full-detail frame drawn for
nothing, since the level is chosen before the draw. The reduced buffers were
also built and uploaded on every slider tick with the preference off.

Dragging is now read from ImGui's active id for the sliders and from the
navigator's own flag, a wheel step holds the reduced set for a 150 ms settle
time with the restoring frame scheduled for when it runs out, and nothing is
built while the preference is off.

What the reduced set leaves out is now a choice: skip layers only, skip the
internal infill roles, or keep the shell only. The shell is found geometrically
at load, once per print: every layer is rasterized into a half-millimetre
occupancy grid and closed so that sparse infill reads as solid, a cell is on the
shell when any of its six neighbours is empty, and a segment is kept when at
least half of its cells are. Being geometric it works for the wipe tower, whose
every segment shares one role and which is the bulk of a tall print, where
dropping roles could not. The layer stride is a preference too, one layer in
every N with both ends of the visible range always whole.

On the 636-layer three-filament fixture, frames during a drag against 448467
segments drawn in 334 ms at full detail:

  skip layers only, 1 in 4        110691 segments   83 ms
  skip internal infill, 1 in 4     85661 segments   68 ms
  shell only, 1 in 4               54382 segments   52 ms
  skip internal infill, every layer 347208 segments  255 ms
  shell only, every layer          215797 segments  170 ms

The shell classification takes 63 ms once per load, split across threads. At
rest the preview is pixel identical to before in every configuration, and the
frame after a release or a settled wheel burst is the full one.
2026-09-10 16:39:17 +08:00
Hanif Koh
8fd5d77220 Draw the Preview From a Reduced Toolpath Set While the User Is Dragging
The preview's frame cost is linear in the number of visible segments and nothing
else: at 46 % of the layers the frame is 45 % of the time. A tall wipe tower puts
enough segments on screen that dragging the camera or a slider cannot keep up, and
no amount of shaving per-segment work fixes that - only drawing fewer segments does.

Everything cheaper was measured first and ruled out. Of the remaining frame, 31 % is
vertex shading and 69 % is triangle setup, so both halves scale with segment count.
Screen-space decimation is useless here: the median extrusion segment is 2.5 mm and
only 0.4 % are sub-pixel at full-plate zoom, so a threshold small enough to be
invisible removes nothing. Merging collinear runs is no better - 2.6 % of this
print's moves are collinear with the one before.

So the reduction has to be one the user can see, which means it only happens while
they are dragging, and only if they ask for it. update_enabled_entities() already
walks every vertex to decide what is visible; it now fills a second pair of index
buffers in the same walk, dropping the interior infill roles and keeping one layer
in four. Because both sets are built together, switching between them is a buffer
binding - no rebuild, no per-frame CPU work, which is what made occlusion culling
unattractive. GLCanvas3D flips the flag while the mouse is down on the canvas or
either slider is live, and asks for one more frame when the level changes so the
full-detail frame lands on release. The top of the visible layer range is always
kept whole: it is the surface being looked at, and the only layer drawn at full
colour in top-layer-only mode.

On the 636-layer, 351k-vertex three-filament fixture the set drops from 448467
segments to 85661 (19 %), and a frame during a drag goes from 298 ms to 52 ms.

Off by default, behind Preferences > Graphics > G-code Preview. With it off the
preview is pixel identical to before, and with it on the preview at rest is pixel
identical to the preview with it off - the reduced set is only ever bound mid-drag.
Verified both, plus that releasing the mouse restores full detail at an unchanged
camera. The OpenGL ES path keeps a single set of entities and is untouched.
2026-09-10 13:31:58 +08:00
Hanif Koh
7cf9884952 Draw the Preview's Toolpath Segments From an Index Buffer
The preview's frame cost is dominated by one call: a single instanced draw of
every visible toolpath segment. On a tall multi-filament print the wipe tower
supplies most of those segments, which is why the preview of a large tower is
slow and why shrinking the layer range speeds it up again.

That draw is not fill bound. Shrinking the model to about a fortieth of its
screen area moved the frame from 419 ms to 401 ms, so the cost is per segment,
not per pixel, and it is paid in the vertex shader: five texelFetch calls plus
several cross/normalize per invocation.

Each segment is a box of eight corners, but it was submitted with
glDrawArraysInstanced over a 24 entry array, so every corner was transformed
once per triangle that touches it and the shader ran 24 times per segment. The
same 24 entries are now an element buffer over the eight distinct corners, which
lets the post-transform cache reuse them and drops the shader to 8 runs per
segment. The triangles, their winding and the vertex_id each corner receives are
unchanged.

Measured over 100 frames on the 636-layer, 351k-vertex three-filament fixture,
the segment draw goes from 381 ms to 322 ms per frame. That is a software
rasterizer, where triangle setup dominates and understates the win; the drop in
shader invocations is the transferable part.

Verified by loading the same project in this build and in a build of the parent
commit and comparing the canvas across three states - the default view, a
rotated camera, and a reduced layer range: pixel identical in all three. The
rotated case matters because the shader picks its corner offsets from the camera
direction. The only pixels that differ anywhere on screen are in the G-code text
panel, which prints a per-process object id that varies between any two runs.
2026-09-10 12:23:34 +08:00
Hanif Koh
c2c572d228 Answer the Preview's Per-Frame Time Query From a Cached Sum
The G-code preview's cost is linear in the number of toolpath vertices, and on a
tall multi-filament print the wipe tower dominates that count: it emits a roughly
constant 160-180 moves on every layer whatever the object is, measured at 57-61%
of all moves on a three-filament print.

Four places scanned or allocated across the whole vertex array. None of them
needed to.

get_estimated_time_at re-accumulated the estimated time from vertex 0 on every
call, and its caller is the tool marker tooltip, which ImGui re-renders every
frame while the properties panel is unfolded. It now reads a running sum built
during load from the total the load loop was already keeping, so the value is
the same addition in the same order and the float result is unchanged. At the
351k vertices of a 636-layer test print this drops the call from 238us to
0.006us.

update_view_full_range walked from vertex 0 to find where the layer range starts,
on every slider tick. It now starts at the first vertex of that layer. The index
is derived from the vertices rather than from Layers::Item::range, because
Layers::update folds a vertex whose layer_id arrives out of order into whichever
bucket is open, which makes that range the wrong answer in general; the index
costs four bytes per layer, not per vertex.

update_colors_texture allocated one float per vertex of the whole print on every
slider tick. It now reuses a buffer.

render_legend fetched the layer Zs and the per-layer times from inside loops over
the custom G-code items, and built whole vectors only to test them for emptiness.
The times are hoisted, the Zs are built lazily so a print with no colour change
does not pay for them at all, and the emptiness tests use the existing counters.

No rendering behaviour changes. Verified by loading the same project in this
build and a build of the parent commit under Xvfb and comparing frames across
five interaction states, including the unfolded tooltip whose Time row is the
output of the function that changed: pixel identical.
2026-09-09 21:01:14 +08:00
18 changed files with 1663 additions and 34 deletions

View File

@@ -202,6 +202,39 @@ 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

@@ -158,6 +158,26 @@ 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,6 +98,46 @@ 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,7 +15,12 @@ namespace libvgcode {
//| 2--0-------5--7 |
//| \ | | / |
//| 3-------4 |
static constexpr const std::array<uint8_t, 24> VERTEX_DATA = {
// 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 = {
0, 1, 2, // front spike
0, 2, 3, // front spike
0, 3, 4, // right/bottom body
@@ -31,7 +36,7 @@ void SegmentTemplate::init()
if (m_vao_id != 0)
return;
m_size_in_bytes_gpu += VERTEX_DATA.size() * sizeof(uint8_t);
m_size_in_bytes_gpu += (VERTEX_DATA.size() + INDEX_DATA.size()) * sizeof(uint8_t);
int curr_vertex_array;
glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array));
@@ -51,12 +56,22 @@ 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;
@@ -71,14 +86,15 @@ void SegmentTemplate::shutdown()
void SegmentTemplate::render(size_t count)
{
if (m_vao_id == 0 || m_vbo_id == 0 || count == 0)
if (m_vao_id == 0 || m_vbo_id == 0 || m_ibo_id == 0 || count == 0)
return;
int curr_vertex_array;
glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array));
glsafe(glBindVertexArray(m_vao_id));
glsafe(glDrawArraysInstanced(GL_TRIANGLES, 0, static_cast<GLsizei>(VERTEX_DATA.size()), static_cast<GLsizei>(count)));
glsafe(glDrawElementsInstanced(GL_TRIANGLES, static_cast<GLsizei>(INDEX_DATA.size()), GL_UNSIGNED_BYTE,
nullptr, static_cast<GLsizei>(count)));
glsafe(glBindVertexArray(curr_vertex_array));
}

View File

@@ -40,6 +40,7 @@ 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,6 +25,24 @@ 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,6 +26,8 @@ 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"
@@ -112,7 +114,20 @@ 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"
" float half_height = 0.5 * height_width_angle.x;\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_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"
@@ -133,10 +148,19 @@ 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 eye_normal = (view_matrix * vec4(normalize(pos - endpoint_pos), 0.0)).xyz;\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 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,6 +77,76 @@ 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,6 +17,10 @@
#include <algorithm>
#include <cmath>
#include <numeric>
#include <cfloat>
#include <future>
#include <thread>
#include <unordered_map>
namespace libvgcode {
@@ -759,6 +763,7 @@ 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");
@@ -875,6 +880,12 @@ 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();
@@ -885,9 +896,23 @@ 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);
@@ -996,6 +1021,8 @@ 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;
@@ -1006,6 +1033,9 @@ 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];
}
@@ -1048,6 +1078,20 @@ 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);
@@ -1116,6 +1160,22 @@ 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
@@ -1127,6 +1187,508 @@ 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())
@@ -1134,6 +1696,26 @@ 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
@@ -1181,6 +1763,38 @@ 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
@@ -1209,10 +1823,34 @@ 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) {
@@ -1261,7 +1899,10 @@ 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).
std::vector<float> colors(m_vertices_colors.size());
// 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());
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];
@@ -1319,6 +1960,7 @@ 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)
@@ -1384,6 +2026,54 @@ 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)
{
@@ -1516,8 +2206,10 @@ void ViewerImpl::set_view_visible_range(Interval::value_type min, Interval::valu
float ViewerImpl::get_estimated_time_at(size_t id) const
{
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)]; });
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];
}
Color ViewerImpl::get_vertex_color(const PathVertex& v) const
@@ -1722,6 +2414,10 @@ 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();
@@ -1787,7 +2483,13 @@ 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;
@@ -1868,6 +2570,7 @@ void ViewerImpl::update_view_full_range()
}
m_settings.update_view_full_range = false;
++m_state_version;
}
void ViewerImpl::update_color_ranges()
@@ -1974,7 +2677,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 (m_enabled_segments_count == 0)
if (active_segments_count() == 0)
#endif // ENABLE_OPENGL_ES
return;
@@ -1994,6 +2697,7 @@ 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));
@@ -2033,10 +2737,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, m_enabled_segments_tex_id));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, m_enabled_segments_buf_id));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, active_segments_tex_id()));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, active_segments_buf_id()));
m_segment_template.render(m_enabled_segments_count);
m_segment_template.render(active_segments_count());
#endif // ENABLE_OPENGL_ES
if (curr_cull_face)
@@ -2062,7 +2766,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 (m_enabled_options_count == 0)
if (active_options_count() == 0)
#endif // ENABLE_OPENGL_ES
return;
@@ -2120,10 +2824,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, m_enabled_options_tex_id));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, m_enabled_options_buf_id));
glsafe(glBindTexture(GL_TEXTURE_BUFFER, active_options_tex_id()));
glsafe(glTexBuffer(GL_TEXTURE_BUFFER, GL_R32UI, active_options_buf_id()));
m_option_template.render(m_enabled_options_count);
m_option_template.render(active_options_count());
#endif // ENABLE_OPENGL_ES
if (!curr_cull_face)

View File

@@ -91,6 +91,31 @@ 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);
@@ -234,6 +259,27 @@ 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 };
@@ -288,6 +334,26 @@ 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
//
@@ -326,6 +392,7 @@ 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 };
@@ -460,6 +527,22 @@ 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 };
@@ -467,6 +550,49 @@ 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,6 +1172,29 @@ 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")));
@@ -1573,11 +1596,21 @@ 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); }
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() };
}
//BBS: always load shell at preview
void GCodeViewer::reset_shell()
@@ -1589,6 +1622,7 @@ 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;
@@ -1617,15 +1651,26 @@ void GCodeViewer::reset()
}
//BBS: GUI refactor: add canvas width and height
void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin)
void GCodeViewer::render(int canvas_width, int canvas_height, int right_margin, bool draw_scene)
{
glsafe(::glEnable(GL_DEPTH_TEST));
render_shells(canvas_width, canvas_height);
// 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);
}
if (m_viewer.get_extrusion_roles().empty())
if (m_viewer.get_extrusion_roles_count() == 0)
return;
render_toolpaths();
update_rest_layer_stride();
if (draw_scene)
render_toolpaths();
float legend_height = 0.0f;
render_legend(legend_height, canvas_width, canvas_height, right_margin);
@@ -1907,6 +1952,92 @@ 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]));
@@ -2230,6 +2361,7 @@ 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
@@ -2294,6 +2426,19 @@ 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
@@ -2544,6 +2689,47 @@ 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)
@@ -3408,6 +3594,12 @@ 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;
@@ -3415,7 +3607,10 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
if (item.type != ColorChange)
continue;
const std::vector<float> zs = m_viewer.get_layers_zs();
if (!zs_built) {
zs = m_viewer.get_layers_zs();
zs_built = true;
}
auto lower_b = std::lower_bound(zs.begin(), zs.end(),
static_cast<float>(item.print_z - epsilon()));
if (lower_b == zs.end())
@@ -4562,6 +4757,8 @@ 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});
@@ -4581,7 +4778,6 @@ 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)
{
@@ -4630,7 +4826,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_estimated_times().empty())
if (m_viewer.get_layers_count() == 0)
max_len += ImGui::CalcTextSize(total_str.c_str()).x;
else {
if (m_viewer.get_view_type() == libvgcode::EViewType::FeatureType)

View File

@@ -230,6 +230,20 @@ 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;
@@ -240,6 +254,8 @@ 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;
@@ -272,7 +288,12 @@ 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
void render(int canvas_width, int canvas_height, int right_margin);
// 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(); }
//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);
@@ -339,6 +360,18 @@ 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,6 +1224,7 @@ 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));
@@ -1328,6 +1329,7 @@ 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
@@ -2050,6 +2052,48 @@ 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.
@@ -2070,7 +2114,6 @@ 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);
@@ -2147,6 +2190,13 @@ 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();
@@ -3240,6 +3290,12 @@ 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;
@@ -3945,6 +4001,9 @@ 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();
@@ -4051,6 +4110,11 @@ 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
@@ -5609,6 +5673,9 @@ 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();
@@ -8508,12 +8575,125 @@ void GLCanvas3D::_render_wireframe_overlay()
}
//BBS: GUI refactor: add canvas size as parameters
void GLCanvas3D::_render_gcode(int canvas_width, int canvas_height)
// 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)
{
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,6 +591,10 @@ 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;
@@ -732,6 +736,33 @@ 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;
@@ -1138,6 +1169,9 @@ 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);
@@ -1273,7 +1307,15 @@ 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);
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();
//BBS: render a plane for assemble
void _render_plane() const;
void _render_selection();

View File

@@ -482,6 +482,11 @@ 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();
@@ -490,6 +495,7 @@ 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);
@@ -882,6 +888,7 @@ 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,6 +118,9 @@ 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) {
@@ -182,6 +185,8 @@ 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, 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, std::function<void(std::string)> onchange, const wxString wiki_url)
{
assert(vlist.size() == config_name_index.size());
unsigned int current_index = 0;
@@ -333,9 +333,16 @@ 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](wxCommandEvent& e) {
combobox->GetDropDown().Bind(wxEVT_COMBOBOX, [this, param, config_name_index, onchange](wxCommandEvent& e) {
app_config->set(param, config_name_index[e.GetSelection()]);
if (onchange != nullptr) onchange(config_name_index[e.GetSelection()]);
e.Skip();
});
@@ -700,11 +707,15 @@ 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: this one is only meaningful while the dimming it controls is enabled
// ORCA: these are only meaningful while the option they belong to 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);
@@ -1056,6 +1067,20 @@ 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)
@@ -1938,6 +1963,93 @@ 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,6 +73,8 @@ 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;
@@ -86,7 +88,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, 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_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);