A tooltip appeared only on the next pixel of mouse movement after its wait
had finished. ImGui hides a window on the first frame it is submitted, to
measure it before showing it, and does so every time a tooltip reappears
because those windows are recycled (the Popup|Tooltip branch of Begin()).
So the frame a tooltip is decided on is never the frame it is seen on, and
one carrying no animation asked for nothing further - leaving it in its
hidden measuring frame until some other event happened to produce the next
one. Before the hover delay this never showed, because the tooltip was
submitted on every mouse-move frame and the movement supplied the second.
It now asks for frames until it has been drawn twice in a row.
that finishes on its own
The hover delay only finished while the mouse was moving: a pointer resting
on a control produces no events, so nothing redrew the canvas and the wait
never reached its end - the tooltip appeared on the next pixel of movement
instead. It asked for its frames with schedule_extra_frame(), which only
arranges a timer whose event an idle event is assumed to follow, and
on_idle renders nothing unless the canvas was already dirty when it ran.
The wait and the animations now both use request_extra_frame(), which ends
on_idle with evt.RequestMore() and so obliges wx to deliver the next idle;
it is what 3DBed's fade uses. Asking for frames as fast as they come is
safe now that which frame is drawn comes from the clock rather than from
the frame count - a redraw that arrives early draws the same frame again.
Nothing explains a control while the control is being used, so no tooltip
appears while a slider is being dragged, a value typed into one, a combo
open or a layer card dragged. The pointer sits on the very thing being
edited, so a tooltip there covered the model exactly while the user was
watching it change. The full wait comes back afterwards rather than the
short one: having just let go of a slider is not a reason to be shown a
picture of it.
AnimatedTexture::texture_id() loses its time-to-next-frame out-parameter,
which nothing asks for any more.
The animations crawled when left alone and raced while the mouse moved.
They were timed by ImGui::GetTime(), and nothing in OrcaSlicer ever sets
io.DeltaTime, so it keeps imgui's default of 1/60 per frame: imgui's clock
counts frames, not seconds. On a canvas that renders only when something
asks it to, that is not a clock. Playback now reads a steady_clock, so a
frame is due when it is due whatever the canvas is doing.
An animation also restarts when its tooltip opens again, not only when a
different one opens: a gap in the frames it was drawn on is what that
looks like from here.
Tooltips now wait for the pointer to rest on a control for 0.45 s. The
panel is dense and a tooltip lands on the model beside it, so with no wait
a sweep from the top of the panel down to Bake flashed a dozen large panels
over the scene. After one has just been up the wait drops to 0.1 s for 0.7
s, so reading several settings in a row stays quick - but not to zero, or
that same sweep would flash again once one tooltip had been seen. A pointer
resting on a control produces no events, so the wait asks for the frame it
will appear on itself.
rest of the panel
The animations advanced one frame per mouse movement and stood still
whenever the mouse did. schedule_extra_frame() only guarantees an idle
event at the moment the next frame is due; GLCanvas3D::on_idle() returns
without rendering anything unless something marked the canvas dirty by
then, and nothing had. set_dirty() on the gizmo is what on_idle reads,
through GLGizmoBase::update_items_state(), to find out there are frames
still to draw.
Thirty-odd more pictures, covering the paint tools, brush size, the
connected-area fill, the five views, the five projections, subdivision,
feature-adaptive refinement, remeshing, hard edges, slivers, geometry
smoothing, filament mixing and denoise.
The generator grew three more drawing languages beside the shaded relief,
each picked by what the setting is rather than by taste: a flat mesh of
triangles where the subject is the mesh or a tool, a section where the
subject is where the surface sits relative to where it was, and a flat ramp
of greys where the subject is which filament a spot ends up as. Lighting a
colour illustration or looking down on a vertical change would both add a
second thing moving between two pictures meant to differ in one.
Mapping is one ball with the checker laid on it five ways, which is the
whole of what choosing a projection does - except Unwrap, which is not a
projection onto the model and so is drawn as the flat layout it produces.
Ten more tooltips get a picture: Depth, Tile size, Repeat/Mirror, Invert,
Smoothing, Edge fade and Blend as stills, Depth, Rotation and Midlevel as
animations. All drawn by scripts/gen_texture_displacement_tips.py from the
same arithmetic the feature uses, one motif throughout, so that whatever
differs between two of them is the setting and nothing else.
Midlevel is drawn in section instead of as a relief. Shading reads slopes,
so a height field moved up or down as a whole shades exactly as it did
before - and where the relief sits relative to the surface is the entire
meaning of that setting. From the side, against the bare surface, it is the
one thing visible.
A single picture is now capped at 10 em tall and centred in its column,
rather than drawn at the tooltip's full width, where a square one came out
as tall as the text beside it was long.
The panel is pinned to the right edge of the canvas, so a tooltip left to
follow the mouse covers the very control it explains. panel_tooltip()
places every tooltip in the gizmo to the left of the panel instead - or of
the texture picker, while a tooltip is coming from inside that popup - the
same placement the picker itself already uses.
A tooltip can now carry pictures under its text: stills, or GIFs that loop
while it is open. Several settings in this panel describe a shape rather
than a number, and a sentence about a shape is read twice and understood
once. Files live in resources/images/texture_displacement and are named by
a tooltip; one that is missing is skipped, so a tooltip without its art yet
is exactly the text tooltip it was before.
AnimatedTexture decodes a file once and uploads a GL texture per frame -
wxImage for stills, wxGIFDecoder for GIFs, whose frames are composited here
with the disposal method each one asks for. An animation restarts when its
tooltip opens and asks the canvas for a redraw timed to its next frame.
Tile and Rotation are wired up as the first two; scripts/
gen_texture_displacement_tips.py draws their relief from the same
arithmetic the feature uses. docs/HLSD/tooltip-illustrations.md covers the
rest.
They were appended at the end of test_texture_displacement.cpp, where every
other open texture-displacement branch also appends, so the four of them
cannot be merged in any order without a conflict in a file none of them
really disagree about. Same tests, moved next to the patch-frame test they
belong with.
An image laid onto a model came out upside down under the ordinary box
(tri-planar) projection, and on three of a cube's six faces it was also
mirrored left to right, which reads as a 180 degree rotation.
DecodedHeightTexture::sample() defines v = 0 as the height map's first
pixel row, i.e. its top. Every projection built v from world +Z, so v
grew upward and the image landed bottom row up. The one path that was
already right is the projective "project from view" matrix, which
measures window y downward.
Both halves of the box basis were wrong:
- v is now negated on all three world planes, so the top of an image
sits at the top of the model.
- u now carries a sign for the side of the plane the face is on, chosen
so that u x v == -normal. Without it the two faces sharing a plane get
one u and one of them is mirrored. The sign flips exactly where that
plane's blend weight (the fourth power of a normal component) reaches
zero, so the blended tri-planar sum stays continuous and the hard axis
switch the old code removed does not come back.
The same v convention was missing from the other mapping modes:
cylindrical (which also had a left handed frame, right x up rather than
up x right, so its wrap ran mirrored), spherical, the affine branch of
view projection, and the flat chart fallback of the LSCM unwrap, which
now reuses the box basis so a cube face unwraps to the orientation
tri-planar would have given it.
The basis table is exported as box_projection_axes() and the gizmo's
adjust_tangent_basis() calls it instead of restating it, so the
placement ring and offset handle cannot drift from the bake. All four
preview shaders get the matching change, including projection_axes(),
whose tangent and bitangent have to flip with the coordinate or the
relief shading inverts.
Re-baking an existing project now places its texture upright, so it will
differ from how it looked before. Geometry already baked into a 3mf is
untouched, so nothing changes until the user re-bakes.
Verified by measuring the sampled texture row and column against world
position for every mapping mode on all six faces of a cube, and by a
full bake of a quadrant coded image, which lands each quadrant within
1e-3 mm of its expected depth.
Every slider in the texture displacement panel now takes an exact number,
either by Ctrl+clicking the track or through a small edit icon at its right
end. Both routes end in ImGui's own temp-input mode: the icon only asks ImGui
to focus the slider, which is what turns it into an input box, so the parsing,
the clamping and the Enter/click-away behaviour stay ImGui's rather than
becoming a second implementation.
The mechanism lives in one helper, typed_row(), with a float and an int form
over it. float_row() and int_row() are thin wrappers on those, and the six
sliders that previously built their widget inline (brush size, smart fill
angle, edge fade amount, remesh sharp angle, refine resolution and the uniform
triangle budget) now go through the same path.
Each slider carries a tooltip with its description and its range. The range is
derived from the bounds and the format string the slider is already given, so
no row states them twice and the tooltip cannot drift from the track. The
tooltip allows hovering while disabled, since several rows are drawn inside
disabled_begin(), where a greyed slider is exactly when the explanation is
wanted.
The panel's "?" popup gains an entry for each of the two ways in, appended to
every tool's shortcut list and shown on their own when no painting tool is
selected, where that popup was previously empty.
The project carried the layer stack but not the user's place in it: a reopened
project landed on slot 0 in Standard, whatever they had been editing. The active
layer and Standard/Pro now live on the volume, next to the rest of its options,
and come back with it. Docked/undocked goes to the application config instead -
it describes the user's window, not the model, so opening someone else's project
must not move their panel.
A layer whose image_data was empty at save time was written with no texture and
came back with none. It now falls back to the file the layer came from, which the
importer put in the user's own texture library, so a project reopened on the same
machine keeps its pixels.
The texture is always stored as .png: the importer converts whatever the user
picked, so the extension was being taken from a name that no longer described the
bytes.
Also adds v2_flip_edges to the cereal field list, which the undo stack was
silently dropping.
The round-trip test named a TextureTileMethod enumerator that does not
exist (MirroredRepeat, not Mirror) and used a bare Approx, which Catch2 v3
keeps in its own namespace. The rest of the file compares floats with
WithinAbs, so these follow it rather than reaching for Catch::Approx.
bbs_3mf.cpp reads and writes the layer JSON but picked up the declarations
through another header, and the same for boost::to_lower.
Profiling build_linux_image.sh: 96 s, of which 51 s in the dependency audit.
appimage_is_elf_file() ran file(1) and grep per candidate. An AppDir holds ~9.6k
of them, 4.8k being the bundled Python runtime, none of them ELF: ~19k processes
for 14 s. Read the four-byte magic instead; checked against the old result on
4000 files, no disagreement.
The dependency walk popped its queue with "${queue[@]:1}", which rebuilds the
whole array each time. At ~4.8k entries that was 22 s of copying. Use a read
index.
96 s -> 12.7 s. The audit still passes.
A painted relief did not survive closing the project: nothing about the feature
was written to the .3mf, so the layers, their textures and the paint mask were
all lost on save. The layer struct's cereal save()/load() serve the undo/redo
stack only, which is one session and one binary stream.
Write the layer stack and the per-volume options as JSON in the archive, and
each layer's texture as the image file it was loaded from, both referenced by
path - the same split EmbossShape already makes for its SVG. The image stays
out of the XML deliberately: it is binary and routinely megabytes, and base64
in an attribute would bloat the one file every reader parses just to list the
objects. JSON rather than the cereal stream next to it because that one is
positional and unversioned, which would make every future field a
project-breaking change; an unknown key is ignored and a missing one keeps its
default, so a project written by either side keeps loading.
The paint masks go in as one attribute per layer slot, mirroring paint_color.
Older readers ignore attributes they do not know, so a project written here
still opens in a build without the feature - it loses the relief, which is all
it could have done with it anyway.
The layers are restored while the volume is being built rather than after the
archive is walked: volumes are constructed only once the whole archive has been
read, so a hook placed after the walk ran before any volume existed and
restored nothing.
GCodeProcessorResult declares a copy assignment, which suppresses the implicit
move assignment, so `*result = std::move(processor.extract_result())` binds to
the copy and duplicates the whole moves array: 96 bytes per move, measured at
147.7 MiB in one allocation for a 597k facet model at 0.08 mm, with the source
staying alive until the export returns.
Add the move assignment. It assigns exactly the same 39 members in the same
order as the copy, including the ones the copy deliberately leaves alone, so a
target that carries its own filament maps and nozzle type keeps them. A move
constructor is intentionally not added: the type holds a std::mutex, so it is
neither copy nor move constructible today and a partial one would leave ten
members uninitialised.
reset() now releases the two print sized vectors rather than clearing them. Its
callers are the paths that discard a result, so the memory went back only at the
next slice.
ViewerImpl::reset() does the same for the two vectors sized by the print, which
is what keeps a discarded preview resident, and counts m_vertices_colors in the
reported CPU memory, where it was missing.
simplify_path, simplify_multi_path and simplify_loop each copied the whole
PrintConfig, 10520 bytes with around 200 heap owning members, once per extrusion
entity, from inside the parallel_for over all layers. LayerRegion.cpp already
binds a reference a few hundred lines above. Three more sites in Layer.cpp do the
same thing.
The tree support changes in the same area: draw_circles() dropped layers from an
owning vector without deleting them, so every re-slice leaked the layers above
the topmost overhang. std::stable_partition rather than std::remove_if, because
remove_if leaves moved-from duplicates of kept pointers in the tail and deleting
those would be a double free. The collision and avoidance accessors returned
ExPolygons by value out of a cache that hands them out by reference, several
times per node per layer. The preview cache is released when the support step
finishes instead of at the start of the next slice; nothing reads it in between.
set_shared_object() now releases the layers the object still owns before it
starts aliasing another object's. Both clear functions are no-ops once the alias
is set, so this cannot free the sharee's layers.
PrintObjectSlice.cpp: eps was a function local static inside a parallel_for, so
slice_closing_radius was latched once per process. This one changes output for
projects that override it per object, or on a re-slice after editing it. The old
behaviour was undefined: whichever object won the race set it for the session.
Stores a painted cube, points its component back at the object that holds it and
expects the load to fail. Without the bound the test does not finish: the work
list grows until the process is killed.
_generate_current_object_list expands component references through a work list
with no bound. An object whose component points back at itself, or a pair that
point at each other, makes the list grow until the process runs out of memory:
a few hundred bytes of XML take the slicer past 20 GB of resident size.
Bound the expansion by the number of objects in the file. A reference chain
longer than that has to revisit an object, so this rejects every cycle and no
acyclic file, however deeply nested. A second bound on the number of expanded
components stops an acyclic graph that fans out exponentially.
They stand on their own: nothing in the texture displacement feature
calls them, and nothing in them knows about textures. Reviewing a
slicer-wide parallelization next to a new gizmo helped neither.
They now live in perf/slicing-optimizations, based on current main.
The bake is covered in the libslic3r suite, but nothing carried its
result through the slicer - where a mesh it cannot use shows up as a
crash or as a print with no toolpaths, which is what was reported on
macOS and Linux.
Paints a cube, bakes it as the gizmo commits it (mesh replaced, hull
recomputed, paint cleared), slices it and checks the G-code has
toolpaths and layers. Once under its budget and once capped, since a
capped bake also goes through the simplification and the repair.
libpng reports a corrupt image by longjmp()ing to the jump buffer the
decoder sets. Both decoders set theirs in a frame holding a PNGDescr and
a vector, and with exceptions enabled MSVC unwinds the stack as part of
longjmp - so returning from that frame afterwards crashed. A truncated
texture segfaulted the Windows test run and was fine everywhere else.
The calls that can fail now sit in two helpers that own nothing but
pointers, and the C++ objects stay in the callers' frames.
texture_displacement_patch_frame picks the world axis least aligned with
the patch's average normal. With no patch there is no normal either, and
the axis came out of the +Z fallback for it - so it was +X, while its
test asked for the upright frame the fallback is meant to give. An empty
patch now answers +Z outright.
The Windows build stopped on test_kdtree.cpp: it calls std::iota without
including <numeric>, which libstdc++ happens to pull in anyway. Added
there, and the same for <limits> and <algorithm>/<cmath> where the
recent changes rely on them being included by something else.
bridge_over_infill's helper for splitting polygons by proximity named
its locals near and far. The Windows headers define both as macros that
expand to nothing, so "Polygons near;" declared nothing and the uses of
it did not compile. Renamed; no behaviour change.
Unwrapped, it ran past the edge of the panel and pulled the layout with
it. Counts under a million now read in thousands, so a 119 k budget is
no longer shown as 0.1 M.
They described the implementation, and a few no longer matched the code.
Each now says what the control does and in what units; five controls that
had no tooltip have one. The catalogs need regenerating.
Nothing removed the even mesh over a texture's flat parts unless the
budget was lowered until simplification had to run. Collapses stay under
a thirty-second of the resolution: on the test map 387584 -> 369876
triangles, with the print unchanged (0.02% of the extrusion).
An earlier attempt was reverted for a 21% heavier result. That was the
colour despeckle switching itself off, not the collapsing: it keyed off
the face parent map, which any collapse empties.
Relief preserved from an earlier bake is counted on top of the budget
instead of eating into it: baking a second area went from ~214 k
triangles to the ~1000 k it was given.
When the resolution needs more triangles than the budget allows, it is
said before the bake under Resolution and again afterwards.
Collecting every candidate within the radius into a vector cost more
than the search itself. Same order, so the seams are unchanged;
align_seam_points ~19.6 s at 0.1 mm / 2000k, was ~21.
detect_surfaces_type, process_external_surfaces and the vertical shells
each waited on their own heaviest layer in turn. The LOTR map plate
slices in ~10.5 min at 0.1 mm / 2000k, was ~11.5; ~87 s at normal
settings, was ~97.
MultiPoint had no rvalue constructor, so the derived move constructors
bound to the const reference and copied; append reserved exactly, so
collecting pieces one by one was quadratic. Colour segmentation ~3 s at
0.1 mm / 2000k, was ~40, and ordinary prints gain too.
Only the slices within the deepest shell offset decide the result, so
the work is done per tile of the face. Top and bottom segmentation
~130 s at 0.1 mm / 2000k, was ~180.
ClipperLib slows down with the number of edges on a scan line, and a
layer cut through a fine relief has tens of thousands of pieces.
detect_surfaces_type ~50 s at 0.1 mm / 2000k, was ~145.
The merge took anything from 3 to 38 minutes at 0.1 mm / 2000k, now
~2.5. Every region is grouped with the islands it overlaps, so the
result is the same.
A colour texture baked at 0.1 mm / 2000k made the top layers thousands
of islands and slicing never finished. Colour segmentation runs per
island, the merge subtracts piece by piece, the support check tests only
nearby islands, and the travel ordering finds crossings through a grid.
A layer split into ~1000 colour fragments (a colour texture baked over a
large top face) made several per-fragment loops redo whole-layer ClipperLib
work, so slicing took ~33 min; it now takes ~3 min with the same output.
- make_fills: clip the layer's no-overlap area to each expolygon's box
before intersecting
- discover_vertical_shells: small-piece filter compares only against the
nearby part of the layer
- bridge_over_infill: whole-layer union/diff/intersections restricted to the
candidate's neighbourhood; fill boundary expanded once per spacing; anchor
tree built only from lines crossing the scan range; bbox pre-check in the
collision test; limiting outline taken directly instead of through
expand(..., 0.3 * flow.spacing()), which offsets by 0.135 scaled units
(flow.spacing() is in mm) and only cost a whole-layer pass per candidate
- Bake job: include GUI.hpp for show_error()
- Texture panel: select/erase whole model next to the paint tools, brush
size slider back on the same row
- Bake: each layer is sampled only on its own painted area; analytic
projections used to stack every layer over every painted region, so the
top layer's texture showed on all of them (colour sampler too)
- Auto resolution follows the texture's texel size and sharpness again
- Unwrap: charts cut by each face's own normal (a cube gives 6 islands, not
12 triangles); non-disk charts (tubes, closed shells) are split until they
flatten; connected nets test real triangle overlap, grow from the largest
chart and are packed side by side
- UV edits are stored per unwrapped copy, so dragging a seam vertex no
longer moves its copies in neighbouring islands
- UV pane: tool strip with unwrap settings moved in from the panel, sharp
HiDPI icons, clearer island/edge/selection drawing with hover, texture
picker from the thumbnail, texture no longer lost on reopen (GL state
from the 3D view, background upload retries)
- Panel: whole-model select/erase as icons in the tools row; inactive
layers' paint shown muted; colour textures shown in colour in the picker
- Built-in displacement texture library
- Tests for unwrap segmentation, connected nets, UV edits and per-layer
sampling
The remote's 3aefae016f is the same tree as local e245f5d069, and every change
in 013ac898ba (continuous smoothing, world-space bake, mirrored/scaled
placements, plate clamp, UV editor framing/HiDPI, layer scrolling) was already
carried forward and reworked by the local commits. Resolved to the local side.