Compare commits

..
Author SHA1 Message Date
ExPikaPaka 053019261f Merge remote-tracking branch 'origin/image-gizmo' into image-gizmo
# Conflicts:
#	src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp
2026-10-13 08:47:58 +02:00
ExPikaPaka b0d586837b Merge upstream main into image-gizmo
Three conflicts, all in the texture displacement gizmo, all resolved by
keeping both sides:

- Two independent additions landed next to apply_standard_mode_presets():
  upstream's store_panel_state() and this branch's image-mode declarations.

- m_color_tex_source: upstream changed it from a raw const void* to a
  weak_ptr to the image bytes, so a freed and reallocated image can no longer
  pass for the old one. That is the better key and it is kept; the colour
  correction key this branch added sits beside it, and the cache test now
  checks the image, the smoothing and the correction.
2026-10-13 08:43:14 +02:00
HanifKoh 869e450981 Keep Per-Nozzle Values When a User Preset Syncs Through the Cloud (#16355)
The cloud upload sent only the keys that differ from the parent, so a
variant layout equal to the parent's was left out. On download the
loader sized every per-variant value by the missing layout, cut it to
one value and kept the parent's values for the other nozzles and nozzle
volume types. On dual-nozzle printers the right nozzle's settings
silently reverted to the parent's after a sync.

Upload the variant layout always, as the local save already does, and
read copies stored without it against the parent's layout when they
carry one value per parent variant.
2026-10-13 13:59:48 +08:00
hikaru0602 b920116fbd Fix manual tool change handling when loading G-code files (#15379)
Opened or drag-dropped G-code files ignored `; MANUAL_TOOL_CHANGE` comments, so the preview showed every move on the first filament. Only `apply_config(const PrintConfig&)` read `manual_filament_change`, and loading a file goes through `apply_config(const DynamicPrintConfig&)`. Read the option there too, initialize the flag to false, and add a regression test.
2026-10-12 20:03:04 -05:00
Downtime 237586675f fix(gui): remove legacy fixed-function OpenGL texture calls (#16270)
Debug builds asserted on startup because these calls raise GL errors on a core-profile context. Both draws already use shaders, so removing the calls changes nothing on compatibility contexts.
2026-10-12 20:01:57 -05:00
Ian Bassi 6d35ff6b9a Rewrite Fix Model on CGAL 6.2, fix Split to parts, remove Win10 SDK leftovers (#16366)
* Add CGAL-based mesh repair, drop Netfabb remnants

Add repair_mesh() in MeshRepair.cpp, a CGAL-based repair that replaces MeshBoolean::cgal::repair(). It welds cracks, cancels coincident triangles, fixes orientation, fills holes (including planar annular regions), removes local self-intersections, and unites crossing shells with libigl's exact boolean while following the nonzero winding rule used by slicing.

FixModelByCgal now repairs each volume in place without splitting it into parts, and reports whether the mesh was modified so unchanged meshes keep their painting and ids. The texture import repair prompt no longer depends on the Windows SDK, and the FIX_THROUGH_CGAL_ALWAYS conditionals are removed. The commented-out Netfabb/WIN10SDK build code is deleted.

libslic3r_cgal now builds with CGAL_LINKED_WITH_TBB and links TBB. Add docs/HLSD/mesh-repair.md and unit tests in test_mesh_repair.cpp.

* Parallelize repair while preserving separate shells

Repair connected mesh units and shells independently in parallel, keep crossing shells separate, and limit self-intersection remeshing to local defects. Update mesh-repair documentation and tests for the revised behavior.

* Make mesh repair deterministic and report its progress

Flags written from parallel loops were std::vector<bool>, whose neighbouring
bits share a word, so shells could be kept or dropped at random; they are now
std::vector<char>. Holes of up to 100 edges are filled with the search over all
triangles: the search restricted to a 3D Delaunay triangulation picks between
equally good fills differently from run to run.

Shells of fewer than four faces are dropped, as the volumes of two triangles
back to back need not cancel exactly, and a shell that self-intersection removal
changed is split again so the pieces it cut off are dropped too.

repair_mesh() takes a progress callback in place of the cancel callback, and
Fix Model reports each stage. The object's bounding box is now invalidated even
when a cancel or an error stops the repair after some volumes were replaced.

The edge-use sort, union-find and box-overlap code shared by several stages is
factored out, the topology analysed for welding is reused to build the work
units, the orientation pass checks only the edges it cut, and the output is
written from the shells without joining them first.

* Fix Split to parts crashing on flat pieces and staying offered after a repair

ModelVolume::split() added every piece to the object and then deleted the
pieces without a convex hull. When the first piece was flat, as often in a
triangle soup, that deleted the volume being split while split() and its
caller kept using it. A part made only of flat pieces lost all its volumes,
and a soup added thousands of pieces, saving a backup of the object for each
one it deleted. Pieces without a convex hull are now dropped before any joins
the object, the volume being split keeps the first remaining piece, and a part
with no solid piece is left whole. Other parts of the object without a convex
hull are no longer deleted as a side effect.

ModelVolume caches whether its mesh can be split, and set_mesh() kept the
cached value, so after Fix Model, Simplify or Emboss replaced a mesh, Split to
parts could still be offered for a single piece. set_mesh() and reset_mesh()
now clear the cache.

* Import STEP surface models with Split compound enabled

Splitting a compound kept only its solids, so a surface model, which OCCT
reads as a compound of loose faces, gave nothing to mesh and the import failed
with "Meshing of a model file failed or no valid shape". Each shell outside a
solid now becomes a part too, and loose faces stay together as one part, as
they do without splitting.

* Add Fix All for plates and re-enable --repair on the command line

ModelVolume::repair() applies repair_mesh() to a part and replaces the mesh
only when the repair changed it. Fix Model, the new Fix All and Fix All Plates
items of the plate context menu, and the command line's --repair option, which
had been commented out since the Win10 SDK repair was dropped, all use it.

With Keep painted feature after mesh change off, Fix Model cleared the painting
of every part of an object before repairing. Now only a part whose mesh changes
loses its painting, so fixing a whole project does not wipe it. The
notification counts the models that needed no repair instead of listing them as
repaired, and objects that did not change are not refreshed.
2026-10-12 21:59:17 -03:00
HMITCHR 075b03975f Read the initial filament start G-code through get_at() (#16080)
A filament_start_gcode list shorter than the filament count made the Bambu start-of-export path throw std::out_of_range. Use get_at() like the other filament_start_gcode reads, and add a regression test.
2026-10-12 19:55:02 -05:00
Kris Austin a379ec4a67 ci(macOS): retry stapler staple after notarization (#16424)
stapler fetches the notarization ticket from Apple's CloudKit server,
and a dropped connection there (NSURLError -1005, exit 68) failed the
whole macOS universal job twice on 2026-10-12, after notarization had
already been accepted. Rerunning the job passed, but repeats the full
build and both notarizations.

Wrap both stapler calls in the existing retry helper already used for
hdiutil in this step.
2026-10-12 19:36:45 -05:00
Alexandre Folle de Menezes 6700d4c5df Verify and improve AI pt_BR translations (#16377) 2026-10-12 20:57:57 -03:00
Downtimeandkenrap a28c9c3a82 fix(gui): guard gizmo actions before deferred initialization (#16268)
GLGizmosManager can be called while its gizmo list is empty, either at startup before the canvas has loaded the gizmos or after their init() failed. reset_all_states(), open_gizmo(), open_gizmo_by_shortcut() and activate_gizmo() now return early in that case. This fixes a Debug-build assert when a new project resets the gizmos at startup, and an out-of-bounds read in release builds when a gizmo shortcut key is pressed after gizmo init failed. activate_gizmo(Undefined) still returns true, since no gizmo is open. The reset_all_states() guard was first proposed in #16074 by kenrap.

Co-authored-by: kenrap <101818165+kenrap@users.noreply.github.com>
2026-10-12 18:25:17 -05:00
packerlschupfer 3dd547c8be CLI: Fix the -50 error reason assuming a 3mf upload (#16339)
The reason recorded for CLI_NO_SUITABLE_OBJECTS (-50) assumed a 3mf being uploaded. The CLI also slices STL, OBJ and STEP input, so the reason now states what both -50 sites check: the plate is empty, or no object on it is both printable and fully inside the print volume. The exit code is unchanged.

Adds tests/cli/test_cli_no_suitable_objects.sh.
2026-10-12 18:23:55 -05:00
f0e9cdc470 raft expansion (#16416)
Update ConfigManipulation.cpp

Co-authored-by: Accelerator <22743445+acceleratorlastorder@users.noreply.github.com>
Co-authored-by: Ian Bassi <12130714+ianalexis@users.noreply.github.com>
2026-10-12 18:24:25 -03:00
Downtime 58a71331d0 fix(gui): balance ImGui style stacks in gizmo windows (#16269)
Pop the FramePadding style var before end() in the move and rotate gizmo windows. It was pushed inside the window and popped after End(), which failed ImGui's style stack check in Debug builds.
2026-10-12 15:32:59 -05:00
packerlschupfer f127b758ed CLI: Don't rotate objects when arranging unless --allow-rotations is passed (#15033)
The CLI's arrange no longer rotates objects unless --allow-rotations is passed, which matches the GUI's default. STL, OBJ and STEP inputs are arranged even without --arrange, so a part that only fits the bed when rotated now exits with -50 unless --allow-rotations is given. The CLI prints that reason to stderr.
2026-10-12 15:17:38 -05:00
Fernando Marino` 1494de66de Bracket a toolchange's own gcode against FanMover splitting its moves (#15904)
FanMover could split a move inside change_filament_gcode to place a delayed fan command. It computed the split point from the last position the G-code set, but on Bambu printers the firmware moves the head during the T command, so the inserted waypoint sent the head to a spot the template never visits (#15789).

Both toolchange paths, set_extruder() and the Type1 append_tcr(), now wrap the template output in ;_FAN_MOVER_SKIP_START/END tags when fan speed-up or kick-start is on. FanMover keeps fan commands inside them where they are and drops the tags from the output. multifilament_config() also sizes the per-filament options that the wipe tower reads by index.
2026-10-12 14:26:20 -05:00
SoftFever 3ca182a98e Fix H2C start G-code record-data comments (#16406)
This fixes the H2C start G-code template by commenting the two
`========== record data ==========` separator lines.

Those lines are currently emitted as raw text inside
`machine_start_gcode`. Since they are not comments or G-code commands,
they can make the generated start sequence invalid or firmware-dependent
when the template is used. The fix preserves the separators as comments.

Fixes #16218.

Validation:
- Parsed the modified JSON profile with Python `json.loads`.
- Checked both `record data` separator lines now start with `;` after
unescaping the G-code string.
2026-10-13 01:07:25 +08:00
SoftFever 5e19fc0e6c Stop leaking layers on re-slice and honor each object's slice closing radius (#16066)
# Description

Re-slicing no longer leaks layers in two places. Tree supports dropped
the support layers above the topmost overhang without freeing them. And
an object that had sliced itself, then became identical to another
object (for example a copy whose original lost a per-object setting or
modifier), leaked its own layers when it switched to sharing that
object's slices; it now frees them once it takes the shared layers over.
If slicing is cancelled before that point, they still leak, as before.
Tree support also stops copying its collision and avoidance areas on
every lookup, and releases its working cache once supports are generated
instead of keeping it until the next slice. Elephant-foot compensation
now uses each object's own slice closing radius, where the first object
sliced in a session used to fix the value for every later slice.

Slicing output changes only on layers with more than one region printed
with elephant-foot compensation and no raft, and only where an object's
slice closing radius differs from the value the session had fixed.

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

## Tests

Adds a regression test for an object that switches to sharing another
object's slices. The `fff_print` suite passes.

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-10-13 00:28:26 +08:00
SoftFever 3f4709aaa7 Fix stale and hidden highlights in the texture displacement gizmo (#16409)
# Description

Fixes highlights in the texture displacement gizmo that stayed on screen
after the state behind them changed, or were drawn where they could not
be seen. Erase whole model, Select whole model and Capture current view
update the paint highlight at once instead of on the next right-click. A
stroke released outside the 3D view ends on the next mouse move, in
every paint gizmo, rather than leaving the brush stuck in its pressed
colour. The layer textures stop showing an image that has been replaced.
Selecting islands in the UV editor switches a Normal view to Fast, where
their highlight is visible instead of buried under the displaced relief,
and Normal comes back once no island is selected unless another view was
picked in the meantime.

No change to slicing output; the changes are confined to the paint
gizmos and the UV editor.

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-10-13 00:23:33 +08:00
SoftFever c63bd5d474 Switch to the Fast view while UV islands are selected in the texture gizmo
The island highlight was hidden under the Normal view's relief. Normal comes back once no island is selected, unless another view was picked in the meantime.
2026-10-13 00:18:51 +08:00
SoftFever 6263502697 Free a sharing object's own layers only when it takes over the shared ones 2026-10-13 00:15:07 +08:00
HanifKoh 1a9201690d Draw Preview Segments With 16-Bit Indices (#16389)
The segment draw switched to an element buffer of 8-bit indices. GCN 1/2
GPUs cannot read 8-bit indices in hardware, so the driver converts them,
and on a Mac Pro 2013 FirePro under Windows Boot Camp the toolpaths come
out as scattered spikes. 16-bit indices are supported everywhere and keep
the eight shader runs per segment.
2026-10-13 00:09:18 +08:00
SoftFever c6a338f35f Fix stale paint highlights and textures in the texture displacement gizmo
Erase whole model, Select whole model and Capture current view update the
highlight at once. A stroke released outside the 3D view ends on the next mouse
move, in every paint gizmo, instead of leaving the brush in its pressed colour.
The layer textures no longer show an image that was replaced.
2026-10-12 23:18:38 +08:00
HanifKoh 4f2d2ae11a Wait for the PR's Deps Cache in clang-tidy When deps/ Changes (#16374)
* Wait for the PR's Deps Cache in clang-tidy When deps/ Changes

* Fail the clang-tidy Deps Wait When Listing Caches Keeps Failing

* Wait for Deps Through ci_deps_cache.sh in clang-tidy

Reuse the macOS deps wait for Linux: the job names, workflow and cache
ref are passed in, and a new 'own' mode waits for the PR's own build
when it changes deps/. A PR that keeps deps/ now waits for a running base
build with the same deps, and warns when it falls back to older ones. The
wait ends as soon as the deps job fails.
2026-10-12 23:17:41 +08:00
SoftFever c57b5dc6c5 Merge branch 'main' into perf/slicing-core-memory 2026-10-12 22:16:10 +08:00
ExPikaPaka 8ac8a75056 Include the headers GLGizmoImage.cpp uses directly
Adds the includes misc-include-cleaner named: libslic3r/Point.hpp for
Vec2d, Vec3f and Transform3d, GLGizmoTextureDisplacement.hpp for the
base class, IconManager.hpp for the icon lookup, and cmath, cstddef
and cstdlib for round, size_t and abs.
2026-10-12 15:31:21 +02:00
ExPikaPaka 60248cadd3 Move the Image tool's declarations off the line main now uses
store_panel_state() landed in the same place, from the .3mf persistence
branch. Same declarations, a few lines down, after bake_standard().
2026-10-12 15:14:22 +02:00
SoftFever 7149728219 Fix clang-cl dependency builds failing on a precompiled header from the compiler cache 2026-10-12 20:27:37 +08:00
ExPikaPaka c07a082050 Add the pipette icon on a line of its own
Three other open texture-displacement branches add an icon to this list too.
Reflowing the lines they also touch is what turns four one-word additions
into four conflicts; a new line four lines away from the next one merges in
any order. Order in the list is irrelevant - the map keys by file name.
2026-10-12 14:03:43 +02:00
SoftFever f49d49b5a6 Texture displacement: typed values on every panel slider (#16395)
Every slider in the texture displacement panel can now be given an exact
number, either by Ctrl+clicking the track or via a small edit icon at
its right end. Both routes reuse ImGui's own temp-input mode.

Each slider also gets a tooltip with its description and its range, and
the panel's `?` popup lists both ways in.

Draft: description to be refined.
2026-10-12 20:03:43 +08:00
SoftFever 08f2037cbd Texture displacement: save layers, textures and paint in the .3mf (#16148)
## General

Nothing about texture displacement was written to the `.3mf`, so a
painted relief did not survive closing the project.
Everything the user set up is saved now, so a project reopens with the
relief intact and still editable.

### What is saved
- The layer stack: every per-layer setting (depth, midlevel, smoothing,
tiling, projection blend mode, colour) and the per-volume options
- Each layer's texture, as a file in the archive (why? because user
might want to move .3mf to another computer where custom textures are
missing, or he saves .3mf then deletes textures, and returns back to
.3mf)
- The painted region, one mask per layer slot
- The UV work: LSCM seams, island placement, per-vertex overrides
- Editor state: the selected layer and Standard/Pro, so the panel
reopens where it was left

### What is not saved
- The preview mesh. It is a second copy of the model in the file, for
something the app rebuilds quickly on reopen.
- Panel docked/undocked, which goes to the application config instead

## Format

- `<volume id>.json` - the layer stack and the per-volume options
- `<volume id>_<slot>.png` - each layer's texture, referenced from the
JSON through `path_in_3mf`

Paint masks ride along as one per-triangle attribute per slot
(`paint_texture_0..7`), beside
the attributes the existing paint tools already write.

## Compatibility
Both ways:
- an older build ignores what it does not know and still opens the
project, losing only the relief
- a newer build finds nothing in an older project and leaves the volume
as today.
2026-10-12 20:00:17 +08:00
SoftFever d8cae3268a Merge branch 'main' into feature/texdisp-3mf-persistence 2026-10-12 19:47:05 +08:00
chenshj73 7d4ec3494c Fix H2C start G-code record-data comments 2026-10-12 19:30:32 +08:00
ExPikaPaka c92721cabf fix: Negative belongs with the colour corrections, not under Background
It sat below the Background section purely because of source order, which
read as if it were part of it. Reset moves out of both sections and becomes
Reset all, since it clears the corrections and the background key alike and
a reset living inside one of them would look like it only reached that far.
2026-10-12 13:30:31 +02:00
ExPikaPaka 47a4e7447f feat: a real dropper that reads the model, foldable sections, a scrolling panel
- The dropper has its own pipette icon instead of borrowing the placement
  handle's, drawn in the house style: 16 px canvas, 0.4 stroke in #949494,
  teal on the tip, which is the part that carries the meaning.

- It picks off the model as well as off the preview. A click on the 3D view
  while it is armed reads the picture's own colour at that point, through
  the same projection the bake uses, not the pixel on screen: the screen
  pixel carries the lighting and the filament the colour was already
  quantized to, so keying on it would remove the wrong thing. The patch
  frame comes from extract_painted_patch() plus
  texture_displacement_patch_frame(), so a cylindrical or spherical layer
  reads at the same place the bake will.

- Placement, Color and Background fold away, with the triangle-and-text the
  displacement panel's "More settings" link uses rather than ImGui's own
  filled bar, which is louder than anything else in this window. Folded,
  Background still says whether it is removing anything. Open by default and
  remembered for the session only: this is panel state, not something a
  project should carry.

- The body scrolls and the footer is pinned under it, the shape the
  displacement panel already uses. With the panel this tall, Apply and Close
  were the first things to go off the bottom of a short canvas.
2026-10-12 13:21:41 +02:00
ExPikaPaka 4286cb30ba feat: a colour count instead of Two colours, and a pickable background
Two colours was the N=2 case of a more useful control, so it is replaced by
a Colors slider: 0 for off, then 2 to 12. The picture is reduced to that many
colours before the filaments are matched to it, so it prints in at most that
many.

Reduced by k-means in RGB rather than by posterizing the range, because the
useful question is which colours the picture is actually made of. A tartan
asks for its own handful of greens and reds; an even split of the range would
hand it eight shades of mud. The clustering runs over a coarse 16-per-channel
histogram, 4096 weighted points instead of a million pixels, which is what
makes it cheap enough to sit on a slider, and it is seeded the k-means++ way
so a picture with one dominant colour does not get every centre inside it.

This is not the displacement tool's "Mixed colors", which caps how many mixed
filament slots a bake may create. Nothing like it existed.

Background removal can now be told which colour to remove. The four corners
stay the default, because a background that does not reach all four of them
is not a background and that makes the common case one click, but the panel
shows the colour as a swatch that is also a picker, and a dropper beside it
takes the colour from the preview. The dropper reads the decoded image rather
than the screen: the preview is scaled, and it already has the corrections
applied, so a screen read would key the corrected colour against itself.
"Corners" puts it back.

Measured on Tartan, a cube face, four filaments:

  run                    out tris  coloured  filaments  filament shares
  image, no correction      22534     14544          4  1:91% 3:8% 4:1%
  image, 2 colors           18232     10268          2  1:66% 3:34%
  image, 3 colors           24458     16459          3  1:78% 3:18% 4:5%
  image, 5 colors           23972     15978          4  1:90% 3:9% 4:1%

Asking for 2 gives 2 and asking for 3 gives 3. Asking for 5 gives 4, which is
the contract working the right way round: only four filaments are loaded, and
"at most" is the guarantee. No run moves the surface.
2026-10-12 13:01:06 +02:00
HanifKoh cccf53850e Remove Never-Built and Stub Source Files (#16361)
SysInfoDialog, JumpPointSearch and WebUpdatePlugin are in no CMake list on any platform, and nothing includes ConfigExceptions.hpp, wxinit.h or Utils/Profile.hpp. MultiTaskModel holds only empty namespaces. Also drop the empty GUI_App::system_info() and comments that still mention SysInfoDialog.
2026-10-12 18:53:32 +08:00
HanifKoh 5095733323 Fix Include Guards Copied From Other Headers (#16368)
TriangleMeshDeal.hpp reused Timer.hpp's guard and CloneDialog.hpp reused
SingleChoiceDialog.hpp's, so including both headers of a pair silently
dropped the second one. Use #pragma once instead.
2026-10-12 18:52:53 +08:00
HanifKoh 0cec10748e Remove the /bot fix-tidy clang-tidy Fix Bot (#16400)
Its inline suggestions rarely apply: clang-tidy adds a missing include
to the include block at the top of the file, which a PR seldom touches,
and GitHub only accepts review suggestions on lines in the PR's diff.
2026-10-12 18:47:26 +08:00
ExPikaPaka 3ee9c02acf feat: sharpen, two colours, and remove background in the Image tool
Three more corrections, chosen because the output is a handful of flat
filament colours rather than a continuous image: what helps is whatever
decides which filament an area lands in and where the boundary between two
of them falls.

- Sharpen. An unsharp mask over the colour, reusing the sliding-window blur
  the Smoothing slider already uses, run per channel. Worth more here than
  in a photo editor: a soft edge between two colours quantizes into a ragged
  band of both, and sharpening first makes it land in one place.

- Two colours. Thresholds on luma so a logo or line art prints in exactly
  two filaments instead of speckling across several, with the split point on
  a slider.

- Remove background. The one that is not an RGB transform. DecodedHeightTexture
  gains an alpha plane, filled from the source PNG's fourth channel when it
  has one, and the key additionally clears whatever colour the picture's four
  corners share. A transparent texel is not black: sample_layer_color()'s
  covered() now reports it the same way it reports being outside an untiled
  placement, so the triangle keeps the object's own colour and the bake needs
  no new concept. The preview follows: upload_color_texture() box-filters
  coverage into the alpha channel it used to fill with 255, and the shader
  leaves the surface alone where it is below half.

The corners rather than an eyedropper: a background that does not reach all
four of them is not a background, and a picker would be a second interaction
for the case this exists to make one click. The median of the four is taken
so one stray corner cannot decide it.

Measured on Tartan, a cube face, four filaments:

  run                    out tris  coloured  max offset mm  filament shares
  image, no correction      22534     14544       0.000005  1:91% 3:8% 4:1%
  image, sharpen 1.0        24780     16783       0.000004  1:84% 3:12% 4:3%
  image, two colors         14798      6837       0.000005  2:22% 4:78%
  image, bg removed         14120      2620       0.000005  1:29% 3:16% 4:55%

Background removal cuts the painted area from 14544 triangles to 2620, which
is the whole point of it; the threshold collapses to two filaments; sharpen
shifts the shares and adds boundary triangles. None of them moves the
surface. The probe's default scene is now Tartan rather than Colour Bricks,
which has too little luma range for the threshold to show anything.
2026-10-12 12:37:57 +02:00
ExPikaPaka 533aaaed28 feat: add Hue to the Image tool's colour correction
Turns every colour of the picture around the colour wheel without changing
how light it is. This matters more here than in a photo editor: the filament
set is fixed and arbitrary, so hue is what lets the picture meet the palette
rather than the other way round. With orange loaded but no red, a small turn
puts the reds onto the orange instead of onto whatever happens to be nearest.

Implemented as Rodrigues' rotation of the colour about the grey axis, then
the Rec. 709 luma is restored exactly: the rotation preserves R+G+B, not the
weighted luma, and without the correction turning the wheel would also change
which areas read as light and dark.

Measured, same scene and harness as the other corrections:

  run                      out tris   coloured  max offset mm  filament shares
  image, no correction        12066       4052       0.000005  1:71% 2:25% 3:4%
  image, hue +60               8208        268       0.000005  2:100%
  image, hue -120              8208        268       0.000005  3:100%

The reddish brick moves onto a different filament in each direction and the
surface does not move.
2026-10-12 12:22:30 +02:00
ExPikaPaka 4dc87e876e feat: show a drop zone while a file is dragged over the view
Dropping a picture already worked, but a drag had no feedback at all: the
only way to find out whether letting go would do anything was to let go.

While a drag is over the 3D view and the Image tool is open, the canvas now
dims and shows a dashed teal border with "Drop a picture to put it on the
model" in the middle. It goes away on leave and on drop.

Three things worth knowing about how it is wired:

- wxWidgets cannot hand us the file names before the drop on GTK, so the
  zone says nothing about what is actually being dragged. It is worded as an
  invitation rather than a promise, and anything that is not a picture still
  falls through to the model loader exactly as before.

- A drag is not a mouse move as far as the canvas is concerned, so nothing
  would repaint and the zone would appear only once something else happened
  to trigger a frame. set_file_drag_over() marks the canvas dirty and asks
  for an extra frame.

- OnEnter and OnLeave are declared virtual without override, matching the
  class's existing OnDropFiles. Marking only the new ones trips
  -Winconsistent-missing-override on that pre-existing line, which is an
  error here.

The zone is drawn on the foreground draw list from inside the panel's ImGui
frame, so it covers the panel as well as the view.
2026-10-12 12:05:22 +02:00
ExPikaPaka 8bb2a1eec5 feat: match the icon weight, drop a picture onto the view, handle on by default
- The toolbar icon's stroke is 0.4 at its 16 px canvas, which renders at
  1.00 px on the 40 px sprite. Measured rather than judged: every icon on the
  gizmo bar was rasterized at 10x and the median thin run length taken, and
  1.00 px is what thirteen of the seventeen use. The previous 0.75 came out
  at 1.90, the heaviest on the bar.

- Dropping a picture on the 3D view loads it, when the Image tool is the one
  open. It is imported into the user's library on the way, as the panel's
  Import tile does, so it is there next time. With the tool closed a dropped
  PNG goes on doing whatever it did before. This mirrors the SVG drop that
  already fed the text tool, and sits next to it.

- The placement handle comes up by itself the first moment there is something
  to anchor it to, so a picture can be nudged straight after the click that
  placed it. Armed once, so turning it off stays off.

Two things had to change for that last one to be usable rather than annoying:

- A click that misses the handle now falls through to placing the picture.
  on_mouse() used to return false there and swallow it, which is right for
  the displacement tool, where the user turned the mode on deliberately, and
  wrong here, where it is on from the start and would have disabled the
  tool's one gesture.

- update_model_object() re-anchors the handle after a stroke that moved the
  paint. Previously the anchor was only refreshed on a layer switch or
  reorder, so clicking a different face left the handle behind on the old
  one.
2026-10-12 11:48:33 +02:00
SoftFever 3f78959a0c Harden loading and saving of texture displacement in projects
A project no longer reads texture files from outside its archive, and a
malformed value or layer slot drops only the texture layers instead of
failing the whole load. Copies of an object that share a mesh keep their
own texture paint when saved, and projects without texture paint no
longer use extra memory to load.
2026-10-12 17:47:04 +08:00
ExPikaPaka 518f961640 feat: empty start, user images only, untiled preview, dockable Image panel
Five fixes to the Image tool, from using it:

- The layer now starts with no picture on it, so the panel opens on its empty
  frame with a plus in it. It used to seed the first shipped colour texture,
  which put somebody else's picture on screen before the user had chosen one.
  The frame is the only state the panel has until a picture is loaded, and
  clicking it is what creates the layer.

- Its picker offers only the user's own images. The shipped library is
  grayscale height maps, which is what the displacement tool wants and the
  opposite of what an image bake can use: it prints colour, and a height map
  has none. The Built-in section is left out rather than shown full of things
  that would all warn when picked, and the import hints now talk about
  pictures instead of height maps.

- The preview stopped tiling a layer that is not tiled. The shader had no
  tile_enabled uniform and wrapped the image unconditionally, so an untiled
  picture previewed as a repeating pattern the bake would never produce.
  in_tile() now draws the same line DecodedHeightTexture::sample() does, and
  gates the colour and all three relief paths on it. Both the 140 and the 110
  variant; glslangValidator passes on each.

- The panel docks against the right edge of the canvas, where the
  displacement panel is, with the same undock toggle in its title row, so the
  two tools do not put their controls in different corners.

- The toolbar icon's frame is square and its stroke thinner. At 16 px scaled
  to the toolbar a 1 px stroke came out at 2.5 px, heavier than every
  neighbour; 0.75 px matches them.
2026-10-12 11:31:33 +02:00
ExPikaPaka 680a597567 feat: show the picture whole in the Image panel, add tile, move and detail
Five changes to the Image tool, all in its own window bar the presets:

- The picture is shown whole, the full width of the window, instead of a row
  sized thumbnail beside a file name: it is the thing being placed, so that
  was the wrong size for it. The box shows the corrected colour texture, so
  moving a correction slider is visible in the panel as well as on the model;
  a grayscale picture has no colour texture and falls back to the height
  thumbnail. The file name stays underneath as a caption.

- Before anything is loaded the same box is an empty frame with a plus in it,
  in the same place and the same size, rather than a button of its own.
  Clicking either state opens the library, which is also where Import lives.

- Tile, off by default. A photo or a logo goes on once, so a new layer the
  Image tool creates starts with a single copy and Size sets how big that
  copy is. It is a default and not a pin, so the checkbox can turn it on.

- Move, the on-canvas placement handle the displacement tool has: drag the
  handle on the surface to slide the picture, or one of its arrows to slide
  it along one axis.

- Detail, the bake resolution, with the budget pinned to a million triangles.
  Each triangle prints in one filament, so this is how sharp an edge between
  two colours can come out, and it is the only thing left that decides it.
  The auto budget is 750 k, chosen for relief where triangles buy shape;
  here they buy the whole output, so it is worth spending more. The slider is
  seeded with the resolution Auto would have picked for the model, and warns
  when what it asks for no longer fits the budget.

Also fixes a short circuit in the panel: apply_standard_mode_presets(mv) ||
apply_image_mode_presets(mv) skipped the image presets on any frame the
standard ones changed something.

Measured with the texture-bake harness on the tool's shipped configuration
(tiling off, million-triangle budget, the seeded resolution), one cube face,
four filaments:

  run                      out tris   coloured  max offset mm  filament shares
  relief, depth 0.4 mm        41040      33902       0.216000  1:91% 2:6% 3:3%
  image, no correction        12066       4052       0.000005  1:71% 2:25% 3:4%
  image, brightness +0.5       8208        268       0.000005  4:100%
  image, contrast +0.8        11980       3910       0.000005  1:78% 2:20% 3:1%
  image, saturation 0          8208        268       0.000005  3:100%
  image, gamma 2.2            13176       5175       0.000004  1:67% 4:33%
  image, negative              8208        268       0.000005  4:100%

Fewer coloured triangles than the previous run of the same scene because
tiling is now off: one copy covers part of the face rather than repeating
across it. The surface still does not move and every correction still changes
which filaments the picture prints in.
2026-10-12 11:12:57 +02:00
SoftFever 30e48183e3 Merge branch 'main' into feature/texdisp-3mf-persistence
# Conflicts:
#	src/libslic3r/TextureDisplacement.hpp
#	tests/libslic3r/test_texture_displacement.cpp
2026-10-12 16:58:03 +08:00
ExPikaPaka e003b61e17 feat: make the Image gizmo a minimal load, click, correct, apply tool
The first cut was the displacement panel with depth hidden, which left the
brushes, the eight layer slots, the UV editor, seams, tiling and the
mesh-preparation sections in a tool that needs none of them, and offered no
way to correct the picture at all. This replaces that panel and adds the
corrections.

The flow is now: load a picture, click the model to put it there, adjust it,
press Apply.

- The Image tool gets its own window, render_image_input_window(), defined in
  GLGizmoImage.cpp. on_render_input_window() dispatches to it on the first
  line, so the displacement panel goes back to exactly what it was rather
  than being threaded with tests for which tool is drawing.

- A click places the picture: the tool pins SMART_FILL with a pointer cursor,
  so one click floods the surface it lands on out to the first fold sharper
  than the Spread angle. No brush, no erase toggle.

- Projection keeps Flat, Around, Ball and From view. Unwrap is left out and a
  layer arriving on it falls back to Flat: seams, islands and the UV pane are
  an editor's worth of interface for a tool that wants a click and a slider.

- The preview is pinned to the fast one. With no relief to build it is both
  instant and exactly the final colour, since the shader's colour path does
  not depend on depth_mm.

- The window shows only what this case needs: the picture, Spread, the four
  projections, Size, Rotation, the corrections, Apply and Close.

Colour correction is new in libslic3r. TextureDisplacementLayer gains
brightness, contrast, saturation, gamma and color_invert, all defaulting to
the identity, and apply_color_adjustments() applies them to an image's RGB
only, never to the height: a brightness slider has no business moving
geometry. It runs at the single point every bake and preview job reads a
layer's colour from, prepare_sampleable_layers(), and at the two caches that
would otherwise miss a change the image_data pointer does not show: the fast
preview's GPU texture and the mix ranking.

Measured with the texture-bake harness, one cube face painted with a colour
image, four filaments:

  run                      out tris   coloured  max offset mm  filament shares
  relief, depth 0.4 mm        41040      33902       0.216000  1:91% 2:6% 3:3%
  image, no correction        24226      17090       0.000007  1:71% 2:25% 3:4%
  image, brightness +0.5       8204       1068       0.000004  4:100%
  image, contrast +0.8        22688      15552       0.000007  1:75% 2:24% 3:1%
  image, saturation 0          8204       1068       0.000004  3:100%
  image, gamma 2.2            24754      17618       0.000007  1:66% 4:34%
  image, negative              8204       1068       0.000004  4:100%

Max offset is the distance from every output vertex to the original surface,
not a vertex-array diff: the bake refines, so a new vertex only counts as
unmoved if it lies in the old surface. Every correction moves which filaments
the picture prints in, and none of them moves the surface.
2026-10-12 10:20:44 +02:00
ExPikaPaka a800e0c186 feat: add an Image gizmo for zero-displacement colour
A separate toolbar tool for the simple case of texture displacement: put a
colour picture on the model with no relief at all.

It is a thin wrapper, not a fork. GLGizmoImage subclasses
GLGizmoTextureDisplacement and overrides one new protected virtual,
image_mode(). Painting, the layer stack, the paint masks, the texture
library, the previews and the bake are the same code reached the same way;
a zero-depth bake refines the painted area and writes the per-triangle
filament indices exactly as a relief bake does, it just displaces by
nothing.

In the base gizmo, image_mode() does three things:

- apply_image_mode_presets() pins every layer to depth 0 and turns colour on
  wherever the image has any, alongside the existing Standard-mode presets.
- The panel leaves out Depth, Midlevel, Invert, Edge fade and Blend, and the
  Standard/Pro switch, which is what keeps the mesh-preparation sections
  hidden. Bake is disabled when no image carries colour, since that bake
  would refine the mesh and produce nothing.
- add_texture_layer() seeds a new layer from the first library texture that
  has colour, rather than the first one, because most of the shipped library
  is grayscale height maps.

The two tools share one layer stack, so opening the Image tool on a volume
whose layers carry relief flattens them. Only depth changes: the paint, the
image, the tiling, the mapping and any relief already baked into the mesh
are untouched.

The toolbar icon follows Orca's menu-icon style: 16 px, 1 px #949494 frame,
#009688 on the picture inside.

The shortcut is registered but unbound: every single letter the Plater
context can reach is already taken, and a default that shadows one of them
is worse than none. docs/HLSD/keyboard-shortcuts.md now documents that an
empty default chord is a supported choice.

Measured with a probe built against this tree in the texture-bake harness:
a depth-0 bake of a painted cube face leaves every output vertex within
7 nm of the original surface (tolerance 1 um) while colouring 17090 of
24226 output triangles across 3 filaments; the same scene at depth 0.4 mm
moves vertices up to 0.216 mm.
2026-10-12 08:29:10 +02:00
ExPikaPaka ed1f3e52c9 Texture displacement: typed values on every panel slider
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.
2026-10-12 07:57:07 +02:00
ExPikaPaka 8ecfee6a21 Save where the panel was left, and recover a texture the archive lacks
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.
2026-10-09 09:35:35 +02:00
ExPikaPaka fbf4652e12 Fix the build and clang-tidy failures in the persistence change
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.
2026-10-09 09:05:42 +02:00
ExPikaPaka be9ff50130 Merge remote-tracking branch 'origin/main' into feature/texdisp-3mf-persistence
# Conflicts:
#	src/libslic3r/TextureDisplacement.cpp
2026-10-09 09:03:51 +02:00
SoftFever dee1f1715f Merge branch 'main' into feature/texdisp-3mf-persistence 2026-10-09 12:15:16 +08:00
ExPikaPaka fcb1f62bd9 Save texture displacement in the project file
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.
2026-10-05 08:36:22 +02:00
ExPikaPaka 391c53a8da Take the print config by reference while simplifying extrusions
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.
2026-10-01 09:15:15 +02:00
115 changed files with 5904 additions and 3764 deletions
+2 -6
View File
@@ -341,12 +341,12 @@ jobs:
# Notarize main DMG # Notarize main DMG
xcrun notarytool submit "OrcaSlicer_Mac_universal_${{ env.ver }}.dmg" --apple-id "${{ secrets.APPLE_DEV_ACCOUNT }}" --team-id "${{ secrets.TEAM_ID }}" --password "${{ secrets.APP_PWD }}" --wait xcrun notarytool submit "OrcaSlicer_Mac_universal_${{ env.ver }}.dmg" --apple-id "${{ secrets.APPLE_DEV_ACCOUNT }}" --team-id "${{ secrets.TEAM_ID }}" --password "${{ secrets.APP_PWD }}" --wait
xcrun stapler staple OrcaSlicer_Mac_universal_${{ env.ver }}.dmg retry xcrun stapler staple OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
# Notarize profile validator DMG if it exists # Notarize profile validator DMG if it exists
if [ -f "OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg" ]; then if [ -f "OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg" ]; then
xcrun notarytool submit "OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg" --apple-id "${{ secrets.APPLE_DEV_ACCOUNT }}" --team-id "${{ secrets.TEAM_ID }}" --password "${{ secrets.APP_PWD }}" --wait xcrun notarytool submit "OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg" --apple-id "${{ secrets.APPLE_DEV_ACCOUNT }}" --team-id "${{ secrets.TEAM_ID }}" --password "${{ secrets.APP_PWD }}" --wait
xcrun stapler staple OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg retry xcrun stapler staple OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
fi fi
- name: Create DMG without notary - name: Create DMG without notary
@@ -460,10 +460,6 @@ jobs:
- name: Build slicer Win - name: Build slicer Win
if: runner.os == 'Windows' if: runner.os == 'Windows'
working-directory: ${{ github.workspace }} working-directory: ${{ github.workspace }}
# Orca: Removed Netfabb STL fixing service support in favor of CGAL.
# env:
# WindowsSdkDir: 'C:\Program Files (x86)\Windows Kits\10\'
# WindowsSDKVersion: '10.0.26100.0\'
# --tests builds the unit tests too; the unit_tests_windows_* jobs run them. # --tests builds the unit tests too; the unit_tests_windows_* jobs run them.
run: | run: |
# cache-path is the install directory inside the deps build directory. # cache-path is the install directory inside the deps build directory.
+70 -4
View File
@@ -16,6 +16,15 @@ on:
permissions: permissions:
contents: read contents: read
# Listing caches and builds while waiting for a deps cache.
actions: read
# Read by scripts/ci_deps_cache.sh.
env:
REPO: ${{ github.repository }}
WORKFLOW: build_all.yml
BASE_REF: ${{ github.base_ref }}
JOB: build_linux (${{ vars.SELF_HOSTED && 'orca-lnx-server' || 'ubuntu-24.04' }})
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }} group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
@@ -35,7 +44,7 @@ jobs:
# The PR merge commit plus its first parent, the base it is diffed against. # The PR merge commit plus its first parent, the base it is diffed against.
fetch-depth: 2 fetch-depth: 2
- name: Look for changed C++ files - name: Look for changed files
id: changes id: changes
run: | run: |
if git -c core.quotePath=false diff --name-only HEAD^1 -- src tests | grep -qE '\.(cpp|cc|cxx|hpp|h|hxx)$'; then if git -c core.quotePath=false diff --name-only HEAD^1 -- src tests | grep -qE '\.(cpp|cc|cxx|hpp|h|hxx)$'; then
@@ -43,18 +52,75 @@ jobs:
else else
echo "No C++ changes under src/ or tests/." echo "No C++ changes under src/ or tests/."
fi fi
if ! git diff --quiet HEAD^1 -- deps; then
echo "deps=true" >> "$GITHUB_OUTPUT"
fi
echo "deps-key=linux-clang-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }}" >> "$GITHUB_OUTPUT"
# Parsing needs the dependency headers, not a build of this PR's deps/, so # Parsing needs the dependency headers, not a build of this PR's deps/.
# a PR that changes deps/ falls back to the newest cache main has. # Without an exact match the job waits for the build that saves it: this
# PR's own when it changes deps/, otherwise a running push build of the
# base branch with the same deps. With neither, a PR that keeps deps/
# falls back to the newest cache.
- name: Restore cached deps - name: Restore cached deps
id: deps_cache
if: steps.changes.outputs.cpp == 'true' if: steps.changes.outputs.cpp == 'true'
uses: actions/cache/restore@v6 uses: actions/cache/restore@v6
with: with:
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
key: linux-clang-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }} key: ${{ steps.changes.outputs.deps-key }}
- name: Look for a build to wait for
id: wait_check
if: steps.changes.outputs.cpp == 'true' && steps.deps_cache.outputs.cache-hit != 'true'
env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: bash scripts/ci_deps_cache.sh ${{ steps.changes.outputs.deps == 'true' && 'own' || 'check' }}
# Linux Build Deps takes about 35 minutes on GitHub-hosted runners. The
# wait ends early when the deps job fails.
- name: Wait for the deps cache
if: steps.wait_check.outputs.wait-for != ''
timeout-minutes: 100
env:
GH_TOKEN: ${{ github.token }}
KEY: ${{ steps.changes.outputs.deps-key }}
CACHE_REF: ${{ steps.changes.outputs.deps == 'true' && github.ref || format('refs/heads/{0}', github.base_ref) }}
KEY_INPUTS: ${{ steps.wait_check.outputs.key-inputs }}
WAIT_FOR: ${{ steps.wait_check.outputs.wait-for }}
WAIT_SINCE: ${{ steps.wait_check.outputs.wait-since }}
WAIT_MINUTES: 90
run: bash scripts/ci_deps_cache.sh wait
- name: Restore the deps cache after waiting
id: waited_cache
if: steps.wait_check.outputs.wait-for != ''
uses: actions/cache/restore@v6
with:
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
key: ${{ steps.changes.outputs.deps-key }}
- name: Fail without this PR's deps cache
if: steps.changes.outputs.deps == 'true' && steps.wait_check.outcome == 'success' && steps.waited_cache.outputs.cache-hit != 'true'
run: |
echo "::error::No deps cache ${{ steps.changes.outputs.deps-key }}. Check this PR's Linux Build Deps job, then rerun clang-tidy."
exit 1
- name: Restore the newest cached deps
id: newest_cache
if: steps.changes.outputs.deps != 'true' && steps.wait_check.outcome == 'success' && steps.waited_cache.outputs.cache-hit != 'true'
uses: actions/cache/restore@v6
with:
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
key: ${{ steps.changes.outputs.deps-key }}
restore-keys: linux-clang-cache-orcaslicer_deps-build- restore-keys: linux-clang-cache-orcaslicer_deps-build-
fail-on-cache-miss: true fail-on-cache-miss: true
- name: Warn about older deps
if: steps.newest_cache.outcome == 'success' && steps.newest_cache.outputs.cache-hit != 'true'
run: echo "::warning title=Older deps::No cache matches this PR's deps/, so clang-tidy parsed against ${{ steps.newest_cache.outputs.cache-matched-key }}. If it reports errors in code that uses a dependency, rerun clang-tidy once the base branch's Build all has saved the new deps."
- name: Apt-Install Dependencies - name: Apt-Install Dependencies
if: steps.changes.outputs.cpp == 'true' && !vars.SELF_HOSTED if: steps.changes.outputs.cpp == 'true' && !vars.SELF_HOSTED
uses: ./.github/actions/apt-install-deps uses: ./.github/actions/apt-install-deps
-65
View File
@@ -1,65 +0,0 @@
name: clang-tidy fix
# Makes the missing-include fix that /bot fix-tidy (pr-tidy-bot.yml) posts on a
# pull request: scripts/run_clang_tidy.sh --fix on the merge commit the
# clang-tidy check runs on, uploaded as the clang-tidy-fix artifact.
#
# Every push to a PR starts this workflow, and its job skips on that first
# attempt. The bot re-runs it on request. A re-run keeps the pull_request
# context, so the PR's code runs with a read-only token, no secrets and the
# PR's own cache, never main's, whoever asked for it.
on:
pull_request:
branches:
- main
- release/*
permissions:
contents: read
jobs:
fix:
if: github.run_attempt > 1
# A hosted runner, so nothing the PR's code leaves behind outlives the job.
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v7
with:
lfs: 'false'
# The PR merge commit plus its first parent, the base it is diffed against.
fetch-depth: 2
persist-credentials: false
# Parsing needs the dependency headers, not a build of this PR's deps/, so
# a PR that changes deps/ falls back to the newest cache main has.
- name: Restore cached deps
uses: actions/cache/restore@v6
with:
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
key: linux-clang-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }}
restore-keys: linux-clang-cache-orcaslicer_deps-build-
fail-on-cache-miss: true
- name: Apt-Install Dependencies
uses: ./.github/actions/apt-install-deps
# The status says whether findings remain that --fix cannot fix.
- name: Fix the changed lines
run: |
mkdir -p "$RUNNER_TEMP/clang-tidy-fix"
status=0
scripts/run_clang_tidy.sh --base HEAD^1 --yes --fix || status=$?
echo "$status" > "$RUNNER_TEMP/clang-tidy-fix/status"
git -c core.quotePath=false diff --no-color --no-ext-diff --src-prefix=a/ --dst-prefix=b/ \
> "$RUNNER_TEMP/clang-tidy-fix/fix.patch"
- name: Upload the fix
uses: actions/upload-artifact@v7
with:
name: clang-tidy-fix
path: ${{ runner.temp }}/clang-tidy-fix/
retention-days: 7
# A later re-run of the same run replaces the earlier fix.
overwrite: true
-476
View File
@@ -1,476 +0,0 @@
name: PR clang-tidy Bot
# Helps a pull request past the clang-tidy check.
#
# When the check fails on a PR's changes, a comment asks the author to fix it,
# either with scripts/run_clang_tidy.sh --fix or by commenting:
# /bot fix-tidy
#
# That command, from the PR's author or an account with write access, re-runs
# the PR's clang-tidy fix run (clang_tidy_fix.yml), which makes the fix in the
# PR's own context. When it finishes, the fix is posted as one review: each
# include whose place falls on a line the PR's diff shows is a suggestion GitHub
# can commit in one click, and the review's body holds the whole patch with a
# git apply command. Findings --fix cannot fix, such as a using in a header,
# are left for the check to report.
#
# No code from the PR runs here. Nothing is pushed, and the patch is only
# posted when every line it adds is an #include in a C++ file the PR changes.
#
# workflow_run matches the workflows by their name: keep "clang-tidy" and
# "clang-tidy fix" in step with clang_tidy.yml and clang_tidy_fix.yml, and
# TIDY_STEP below with the clang-tidy step's name in clang_tidy.yml.
on:
issue_comment:
types:
- created
workflow_run:
workflows:
- clang-tidy
- clang-tidy fix
types:
- completed
permissions: {}
jobs:
request:
# Skips the job unless a PR comment mentions the command.
if: >-
github.repository == 'OrcaSlicer/OrcaSlicer'
&& github.event_name == 'issue_comment'
&& github.event.issue.pull_request != null
&& contains(github.event.comment.body, '/bot fix-tidy')
permissions:
actions: write # re-run the fix run
pull-requests: write # react to and answer the comment, which is the PR's
issues: write # feedback comment + reactions
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Re-run the PR's clang-tidy fix
uses: actions/github-script@v9
with:
script: |
const MARKER = '<!-- pr-tidy-bot -->';
const FIX_WORKFLOW = 'clang_tidy_fix.yml';
const { owner, repo } = context.repo;
const issue = context.payload.issue;
const comment = context.payload.comment;
// Ignores a comment whose sender is not its author.
if (context.payload.sender.login !== comment.user.login || comment.user.type !== 'User') {
core.info('Ignoring a comment not written by its sender, or by a bot.');
return;
}
const commandLine = (comment.body || '')
.split('\n')
.map((line) => line.trim())
.find((line) => /^\/bot\s+fix-tidy\b/i.test(line));
if (!commandLine) {
core.info('No /bot fix-tidy command found.');
return;
}
const commenter = comment.user.login;
// Answers only the PR's author and maintainers, so the command cannot be used to spam.
if (commenter !== issue.user.login) {
const { data: access } = await github.rest.repos.getCollaboratorPermissionLevel({ owner, repo, username: commenter });
if (!['admin', 'write'].includes(access.permission)) {
core.info(`Ignoring /bot fix-tidy from @${commenter}: not the PR's author and no write access.`);
return;
}
}
const react = (content) =>
github.rest.reactions.createForIssueComment({ owner, repo, comment_id: comment.id, content });
async function refuse(reason) {
await react('-1');
await github.rest.issues.createComment({ owner, repo, issue_number: issue.number, body: `${MARKER}\n@${commenter} ${reason}` });
}
await react('eyes');
if (!/^\/bot\s+fix-tidy$/i.test(commandLine)) {
return refuse('`/bot fix-tidy` takes no arguments.');
}
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: issue.number });
if (pr.state !== 'open') {
return refuse('the PR is not open.');
}
const sha = pr.head.sha.slice(0, 10);
const { data: { workflow_runs: runs } } = await github.rest.actions.listWorkflowRuns({
owner, repo, workflow_id: FIX_WORKFLOW, event: 'pull_request', head_sha: pr.head.sha, per_page: 1
});
const run = runs[0];
// A PR last pushed before clang_tidy_fix.yml existed has no run to re-run.
if (!run) {
return refuse(`there is no clang-tidy fix run for ${sha}, the PR's latest commit. If you just pushed it, ` +
'try again in a minute. Otherwise, push a commit or merge the base branch to start one, then comment `/bot fix-tidy` again.');
}
if (run.status !== 'completed') {
// The first attempt is the one every push starts, and it makes no fix.
return refuse(run.run_attempt > 1
? `a clang-tidy fix for ${sha} is already [running](${run.html_url}). I'll post it when it finishes.`
: `the PR's checks on ${sha} are still starting. Comment \`/bot fix-tidy\` again in a minute.`);
}
// GitHub refuses to re-run a run created over 30 days ago.
try {
await github.rest.actions.reRunWorkflow({ owner, repo, run_id: run.id });
} catch (error) {
core.warning(`Re-running ${run.html_url} failed: ${error.message}`);
return refuse(`GitHub won't re-run the [clang-tidy fix run](${run.html_url}) for ${sha} (${error.message}). ` +
'Push a commit or merge the base branch to start a new one, then comment `/bot fix-tidy` again.');
}
core.info(`Re-ran ${run.html_url}`);
suggest:
# The fix run's re-runs only. Its first attempt makes no fix.
if: >-
github.repository == 'OrcaSlicer/OrcaSlicer'
&& github.event_name == 'workflow_run'
&& github.event.workflow_run.name == 'clang-tidy fix'
&& github.event.workflow_run.event == 'pull_request'
&& github.event.workflow_run.run_attempt > 1
concurrency:
group: ${{ github.workflow }}-suggest-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: false
permissions:
actions: read # download the fix
contents: read # check out the PR head
pull-requests: write # the review
issues: write # feedback comment + reactions
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Find the PR and who asked
id: find
uses: actions/github-script@v9
with:
script: |
const MARKER = '<!-- pr-tidy-bot -->';
const CPP = /^(src|tests)\/.+\.(cpp|cc|cxx|hpp|h|hxx)$/;
const { owner, repo } = context.repo;
const run = context.payload.workflow_run;
// workflow_run lists no PR for a fork's branch, so the PR is found by its branch.
const { data: prs } = await github.rest.pulls.list({
owner, repo, state: 'open', head: `${run.head_repository.owner.login}:${run.head_branch}`
});
const pr = prs[0];
if (!pr) {
core.info('No open PR for this run.');
return;
}
// The latest /bot fix-tidy comment is the one this run answers.
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: pr.number, per_page: 100 });
const request = comments.reverse().find((c) => c.user.type === 'User' && /^\s*\/bot\s+fix-tidy\s*$/im.test(c.body || ''));
const mention = request ? `@${request.user.login} ` : '';
async function refuse(reason) {
if (request) {
await github.rest.reactions.createForIssueComment({ owner, repo, comment_id: request.id, content: '-1' });
}
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body: `${MARKER}\n${mention}${reason}` });
}
if (pr.head.sha !== run.head_sha) {
return refuse('the PR changed while I was making its clang-tidy fix. Comment `/bot fix-tidy` again.');
}
if (run.conclusion !== 'success') {
return refuse(`I couldn't make the clang-tidy fix. See the [run log](${run.html_url}).`);
}
// The right-side lines of each changed C++ file's diff: the lines a
// review comment can be placed on. A file too large for GitHub to
// show its diff has none, and gets no suggestions.
const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, pull_number: pr.number, per_page: 100 });
const ranges = {};
for (const file of files) {
if (file.status === 'removed' || !CPP.test(file.filename)) {
continue;
}
ranges[file.filename] = [...(file.patch || '').matchAll(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/gm)]
.map((m) => [Number(m[1]), Number(m[1]) + Number(m[2] ?? 1) - 1])
.filter(([first, last]) => last >= first);
}
core.setOutput('pr', pr.number);
core.setOutput('head-sha', pr.head.sha);
core.setOutput('head-repo', run.head_repository.full_name);
core.setOutput('ranges', JSON.stringify(ranges));
core.setOutput('mention', mention);
core.setOutput('request-id', request ? request.id : '');
- name: Download the fix
if: steps.find.outputs.pr != ''
uses: actions/download-artifact@v8
with:
name: clang-tidy-fix
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
path: ${{ runner.temp }}/clang-tidy-fix
# The PR's own files, only to apply the patch to. Nothing in them is run.
- name: Checkout the PR head
if: steps.find.outputs.pr != ''
uses: actions/checkout@v7
with:
repository: ${{ steps.find.outputs.head-repo }}
ref: ${{ steps.find.outputs.head-sha }}
path: head
fetch-depth: 1
lfs: 'false'
persist-credentials: false
sparse-checkout: |
src
tests
# The fix was made on the PR's merge commit, so it is applied to the head
# and diffed again, giving line numbers in the PR's own files.
- name: Check the fix and place its suggestions
id: place
if: steps.find.outputs.pr != ''
env:
RANGES: ${{ steps.find.outputs.ranges }}
run: |
python3 -I - "$RUNNER_TEMP/clang-tidy-fix/fix.patch" head "$RUNNER_TEMP" <<'EOF'
import json, os, re, subprocess, sys
patch_path, head, out = sys.argv[1:]
ranges = json.loads(os.environ["RANGES"])
def output(**values):
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as f:
for key, value in values.items():
f.write(f"{key}={value}\n")
patch = open(patch_path, encoding="utf-8").read()
if not patch.strip():
output(result="empty")
sys.exit()
# Every line the patch adds must be an #include or blank, in a C++ file the
# PR changes, and it may not remove, create, rename or change the mode of
# anything.
include = re.compile(r'\s*#\s*include\s*(<[\w./+-]+>|"[\w./+-]+")\s*')
header = re.compile(r"(index [0-9a-f]+\.\.[0-9a-f]+( \d+)?|@@ -\d+(,\d+)? \+\d+(,\d+)? @@.*)")
problems = []
path = None
for line in patch.splitlines():
if line.startswith("diff --git "):
match = re.fullmatch(r"diff --git a/(.+) b/\1", line)
path = match.group(1) if match else None
if path not in ranges:
problems.append(f"{line}: not a C++ file the PR changes")
# git appends a tab to the --- and +++ lines of a path that contains a space.
elif line.removesuffix("\t") in (f"--- a/{path}", f"+++ b/{path}") or header.fullmatch(line):
pass
elif line.startswith(" ") or line == "\\ No newline at end of file":
pass
elif line.startswith("+") and (include.fullmatch(line[1:]) or not line[1:].strip()):
pass
else:
problems.append(f"{path}: {line}")
if problems:
for problem in problems:
print(f"::error::Refused patch line {problem}")
output(result="rejected")
sys.exit()
if subprocess.run(["git", "-C", head, "apply", "--index", os.path.abspath(patch_path)]).returncode != 0:
output(result="apply-failed")
sys.exit()
git = ["git", "-C", head, "-c", "core.quotePath=false", "diff", "--cached", "--no-color", "--no-ext-diff",
"--src-prefix=a/", "--dst-prefix=b/"]
with open(os.path.join(out, "head.patch"), "w", encoding="utf-8") as f:
f.write(subprocess.run(git, check=True, capture_output=True, encoding="utf-8").stdout)
# Each run of added lines becomes one suggestion on the line it goes after
# (before, at the top of a file): that line, replaced by itself and the
# includes. It can only sit on a line the PR's diff shows.
hunks = []
path = None
for line in subprocess.run(git + ["-U0"], check=True, capture_output=True, encoding="utf-8").stdout.splitlines():
if line.startswith("diff --git "):
path = re.fullmatch(r"diff --git a/(.+) b/\1", line).group(1)
elif line.startswith("@@ "):
hunks.append((path, int(re.match(r"@@ -(\d+)", line).group(1)), []))
elif hunks and line.startswith("+") and not line.startswith("+++ "):
hunks[-1][2].append(line[1:])
suggestions = []
total = suggested = 0
for path, after, added in hunks:
count = sum(bool(include.fullmatch(line)) for line in added)
total += count
original = subprocess.run(["git", "-C", head, "show", f"HEAD:{path}"], check=True, capture_output=True,
encoding="utf-8").stdout.splitlines()
line = max(after, 1)
if line > len(original) or not any(first <= line <= last for first, last in ranges[path]):
continue
body = "\n".join([original[line - 1], *added] if after else [*added, original[0]])
# A fence longer than any run of backticks in the line it repeats.
fence = "`" * max([3] + [len(run) + 1 for run in re.findall(r"`+", body)])
suggestions.append({"path": path, "line": line, "side": "RIGHT",
"body": f"{fence}suggestion\n{body}\n{fence}"})
suggested += count
with open(os.path.join(out, "suggestions.json"), "w", encoding="utf-8") as f:
json.dump(suggestions, f)
output(result="ok", includes=total, suggested=suggested)
EOF
- name: Post the fix
if: always() && steps.find.outputs.pr != ''
uses: actions/github-script@v9
env:
PR: ${{ steps.find.outputs.pr }}
MENTION: ${{ steps.find.outputs.mention }}
REQUEST_ID: ${{ steps.find.outputs.request-id }}
HEAD_SHA: ${{ steps.find.outputs.head-sha }}
RESULT: ${{ steps.place.outputs.result }}
INCLUDES: ${{ steps.place.outputs.includes }}
SUGGESTED: ${{ steps.place.outputs.suggested }}
with:
script: |
const fs = require('fs');
const path = require('path');
const MARKER = '<!-- pr-tidy-bot -->';
// GitHub's limit on a review body is 65536 characters.
const MAX_PATCH = 60000;
const { owner, repo } = context.repo;
const { PR, MENTION, REQUEST_ID, HEAD_SHA, RESULT, INCLUDES, SUGGESTED, RUNNER_TEMP } = process.env;
const issue_number = Number(PR);
const sha = HEAD_SHA.slice(0, 10);
const fixUrl = context.payload.workflow_run.html_url;
const runUrl = `${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId}`;
const react = (content) => REQUEST_ID
? github.rest.reactions.createForIssueComment({ owner, repo, comment_id: Number(REQUEST_ID), content })
: Promise.resolve();
async function reply(body, content) {
await react(content);
await github.rest.issues.createComment({ owner, repo, issue_number, body: `${MARKER}\n${MENTION}${body}` });
}
const read = (...parts) => fs.readFileSync(path.join(RUNNER_TEMP, ...parts), 'utf8');
const passes = RESULT && read('clang-tidy-fix', 'status').trim() === '0';
if (RESULT === 'empty') {
return passes
? reply(`clang-tidy passes on ${sha}. There is nothing to fix.`, '+1')
: reply(`clang-tidy has no automatic fix for what it reports on ${sha}. The findings, in the [fix run's log](${fixUrl}), need a manual edit.`, '-1');
}
if (RESULT === 'rejected') {
return reply(`the clang-tidy fix changes more than includes, so I didn't post it. See the [run log](${runUrl}).`, '-1');
}
if (RESULT === 'apply-failed') {
return reply(`the clang-tidy fix doesn't apply to ${sha}. Merge the base branch into the PR, then comment \`/bot fix-tidy\` again.`, '-1');
}
if (RESULT !== 'ok') {
return reply(`something went wrong. See the [run log](${runUrl}).`, '-1');
}
const patch = read('head.patch');
const suggestions = JSON.parse(read('suggestions.json'));
const one = INCLUDES === '1';
const includes = one ? '1 missing include' : `${INCLUDES} missing includes`;
const inline = SUGGESTED === '0'
? (one ? 'It goes on a line this PR does not change, so I can\'t suggest it inline.'
: 'They go on lines this PR does not change, so I can\'t suggest them inline.')
: SUGGESTED === INCLUDES
? (one ? 'It is suggested inline below.' : 'Each is suggested inline below.')
: `${SUGGESTED} of them are suggested inline below; the rest go on lines this PR does not change.`;
const remaining = passes
? `With ${one ? 'it' : 'them'}, clang-tidy passes.`
: `Some findings remain that need a manual edit; see the [fix run's log](${fixUrl}).`;
// A fence longer than any run of backticks in the patch's context lines.
const fence = '`'.repeat(Math.max(3, ...[...patch.matchAll(/`+/g)].map((m) => m[0].length + 1)));
const apply = patch.length > MAX_PATCH
? `The full patch is too large to show here. Download \`fix.patch\` from the [fix run](${fixUrl}) and run \`git apply fix.patch\`.`
: `To apply ${one ? 'it' : 'all of them'}, run this in your checkout of the PR branch:\n\n${fence}sh\ngit apply <<'EOF'\n${patch}EOF\n${fence}`;
const body = (withSuggestions) => `${MARKER}\n${MENTION}clang-tidy's fix for ${sha} adds ${includes}. ` +
`${withSuggestions ? `${inline} ` : ''}${remaining}\n\n${apply}`;
const review = { owner, repo, pull_number: issue_number, commit_id: HEAD_SHA, event: 'COMMENT' };
try {
await github.rest.pulls.createReview({ ...review, body: body(true), comments: suggestions });
} catch (error) {
// A suggestion GitHub will not place still leaves the patch.
core.warning(`Posting the suggestions failed (${error.message}); posting the patch alone.`);
await github.rest.pulls.createReview({ ...review, body: body(false) });
}
await react('+1');
nudge:
if: >-
github.repository == 'OrcaSlicer/OrcaSlicer'
&& github.event_name == 'workflow_run'
&& github.event.workflow_run.name == 'clang-tidy'
&& github.event.workflow_run.event == 'pull_request'
concurrency:
group: ${{ github.workflow }}-nudge-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: false
permissions:
actions: read # the check's steps
pull-requests: write # comment on the PR
issues: write # the comment
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Ask for a fix when the check fails
uses: actions/github-script@v9
with:
script: |
const MARKER = '<!-- pr-tidy-bot-nudge -->';
// The clang-tidy step's name in clang_tidy.yml. A failure in any other
// step is the CI's, not the PR's.
const TIDY_STEP = 'Run clang-tidy on the changed lines';
const { owner, repo } = context.repo;
const run = context.payload.workflow_run;
// workflow_run lists no PR for a fork's branch, so the PR is found by its branch.
const { data: prs } = await github.rest.pulls.list({
owner, repo, state: 'open', head: `${run.head_repository.owner.login}:${run.head_branch}`
});
const pr = prs.find((p) => p.head.sha === run.head_sha);
if (!pr) {
core.info('No open PR at this run\'s commit; a newer run will answer.');
return;
}
const { data: { jobs } } = await github.rest.actions.listJobsForWorkflowRun({ owner, repo, run_id: run.id });
const failed = jobs.some((job) => (job.steps || []).some((step) => step.name === TIDY_STEP && step.conclusion === 'failure'));
const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: pr.number, per_page: 100 });
const previous = comments.find((c) => c.user.type === 'Bot' && (c.body || '').startsWith(MARKER));
if (!failed) {
// Removes the request once the check passes.
if (previous && run.conclusion === 'success') {
await github.rest.issues.deleteComment({ owner, repo, comment_id: previous.id });
}
return;
}
const body = `${MARKER}\n@${pr.user.login} the clang-tidy check found problems in this PR's changes ` +
`at ${run.head_sha.slice(0, 10)} ([log](${run.html_url})). Missing includes can be fixed either way:\n\n` +
'- run `scripts/run_clang_tidy.sh --fix` (`scripts\\run_clang_tidy.ps1 -Fix` on Windows) and push the result, or\n' +
'- comment `/bot fix-tidy`, and I\'ll suggest the fix here for you to apply.\n\n' +
'Other findings, such as a `using` in a header, need a manual edit.';
// Edits the earlier request instead of posting another, so each push does not notify again.
if (previous) {
await github.rest.issues.updateComment({ owner, repo, comment_id: previous.id, body });
} else {
await github.rest.issues.createComment({ owner, repo, issue_number: pr.number, body });
}
-41
View File
@@ -497,47 +497,6 @@ endif()
# Once we get rid of Perl and libslic3r is linked statically, we can get rid of -fPIC # Once we get rid of Perl and libslic3r is linked statically, we can get rid of -fPIC
set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON)
# WIN10SDK_PATH is used to point CMake to the WIN10 SDK installation directory.
# We pick it from environment if it is not defined in another way
# ORCA: Removed Netfabb STL fixing service support in favor of CGAL.
# if(WIN32)
# if(NOT DEFINED WIN10SDK_PATH)
# if(DEFINED ENV{WIN10SDK_PATH})
# set(WIN10SDK_PATH "$ENV{WIN10SDK_PATH}")
# endif()
# endif()
# if(DEFINED WIN10SDK_PATH)
# #BBS: modify win10sdk_path
# if (EXISTS "${WIN10SDK_PATH}/winrt/windows.graphics.printing3d.h")
# set(WIN10SDK_INCLUDE_PATH "${WIN10SDK_PATH}")
# else()
# message("WIN10SDK_PATH is invalid: ${WIN10SDK_PATH}")
# message("${WIN10SDK_PATH}/winrt/windows.graphics.printing3d.h was not found")
# message("STL fixing by the Netfabb service will not be compiled")
# unset(WIN10SDK_PATH)
# endif()
# else()
# # Try to use the default Windows 10 SDK path.
# if (DEFINED ENV{WindowsSdkDir} AND DEFINED ENV{WindowsSDKVersion})
# set(WIN10SDK_INCLUDE_PATH "$ENV{WindowsSdkDir}/Include/$ENV{WindowsSDKVersion}")
# else ()
# set(WIN10SDK_INCLUDE_PATH "C:/Program Files (x86)/Windows Kits/10/Include/10.0.26100.0")
# endif ()
# if (NOT EXISTS "${WIN10SDK_INCLUDE_PATH}/winrt/windows.graphics.printing3d.h")
# message("${WIN10SDK_INCLUDE_PATH}/winrt/windows.graphics.printing3d.h was not found")
# message("STL fixing by the Netfabb service will not be compiled")
# unset(WIN10SDK_INCLUDE_PATH)
# endif()
# endif()
# if(WIN10SDK_INCLUDE_PATH)
# message("Building with Win10 Netfabb STL fixing service support")
# add_definitions(-DHAS_WIN10SDK)
# include_directories(SYSTEM "${WIN10SDK_INCLUDE_PATH}")
# else()
# message("Building without Win10 Netfabb STL fixing service support")
# endif()
# endif()
if (APPLE) if (APPLE)
message("OS X SDK Path: ${CMAKE_OSX_SYSROOT}") message("OS X SDK Path: ${CMAKE_OSX_SYSROOT}")
if (CMAKE_OSX_DEPLOYMENT_TARGET) if (CMAKE_OSX_DEPLOYMENT_TARGET)
+8
View File
@@ -1,10 +1,18 @@
# ccache does not parse the -clang: arguments CMake uses for clang-cl's gcc-style # ccache does not parse the -clang: arguments CMake uses for clang-cl's gcc-style
# depfile, so a cache hit writes the object and no depfile, and Ninja then records # depfile, so a cache hit writes the object and no depfile, and Ninja then records
# no headers for that object. ccache reproduces /showIncludes output on a hit. # no headers for that object. ccache reproduces /showIncludes output on a hit.
#
# Clang also records the modification time of every input in a precompiled header
# and rejects the header once one differs. ccache caches the compile that creates
# the header, so a tree that regenerates cmake_pch.cxx gets back a header stamped
# with the old file's time, and every compile that uses it fails. The build system
# already rebuilds the header when an input changes. This covers the dependencies,
# such as wxWidgets; PrecompiledHeader.cmake does the same for OrcaSlicer's targets.
foreach (_lang C CXX) foreach (_lang C CXX)
if (CMAKE_${_lang}_COMPILER_ID STREQUAL "Clang" AND if (CMAKE_${_lang}_COMPILER_ID STREQUAL "Clang" AND
CMAKE_${_lang}_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") CMAKE_${_lang}_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
set(CMAKE_DEPFILE_FLAGS_${_lang} "/showIncludes") set(CMAKE_DEPFILE_FLAGS_${_lang} "/showIncludes")
set(CMAKE_${_lang}_DEPFILE_FORMAT msvc) set(CMAKE_${_lang}_DEPFILE_FORMAT msvc)
list(APPEND CMAKE_${_lang}_COMPILE_OPTIONS_CREATE_PCH -Xclang -fno-pch-timestamp)
endif () endif ()
endforeach () endforeach ()
+3 -1
View File
@@ -124,7 +124,9 @@ Control on the Global page, and from the `?` key on the page of the view that re
the dialog should list it; the row's section heading is the `section_table` entry the dialog should list it; the row's section heading is the `section_table` entry
above it, so a new section needs an entry there too. Pick a default that does not above it, so a new section needs an entry there too. Pick a default that does not
collide inside its contexts; the `[Shortcuts]` tests check every default against the collide inside its contexts; the `[Shortcuts]` tests check every default against the
others. others. A default `KeyChord{}` leaves the action unbound: it is listed in the dialog
for the user to bind and matches nothing until they do, which is the honest answer
when every key its contexts can reach is already spoken for.
2. Handle it in the dispatcher of its context: `MainFrame::handle_global_shortcut`, 2. Handle it in the dispatcher of its context: `MainFrame::handle_global_shortcut`,
`GLCanvas3D::handle_shortcut`, `ObjectList::dispatch_shortcut`, or a gizmo's `GLCanvas3D::handle_shortcut`, `ObjectList::dispatch_shortcut`, or a gizmo's
`on_tool_shortcut`. A gizmo that opens on a key sets `m_shortcut` in its constructor. `on_tool_shortcut`. A gizmo that opens on a key sets `m_shortcut` in its constructor.
+135
View File
@@ -0,0 +1,135 @@
# Mesh repair — High Level Design
## Purpose and scope
Mesh repair turns a broken triangle mesh into closed, outward oriented shells that bound a solid, so that the
mesh slices predictably and the other mesh tools (booleans, cut, painting) get valid input. It runs on demand:
the *Fix Model* command of the context menu of an object or part and of the warning icon in the object list, *Fix
All* and *Fix All Plates* in the context menu of a plate, the prompt the Cut tool shows when a cut leaves open edges,
the *Repair and import* choice of texture import, and the `--repair` option of the command line. It is built on CGAL
and is the only repair the application has, on every platform.
`repair_mesh()` in `src/libslic3r/MeshRepair.cpp` does the work on one `TriangleMesh`, and `ModelVolume::repair()`
applies it to a part: it replaces the mesh only when the repair changed it, and remaps or drops the painting of a
replaced mesh. The GUI side, `fix_model_with_cgal_gui()` in `src/slic3r/Utils/FixModelByCgal.cpp`, runs it on a
worker thread for each volume of an object, or for one volume, behind a progress dialog. `repair_mesh()` reports its
progress between stages through a callback, which cancels the repair by throwing.
## What the repaired mesh is
The repair keeps what the slicer would print from the input. The slicer fills a layer where the winding number of
the input surface is not zero (`MeshSlicingParams::SlicingMode::Regular` uses Clipper's nonzero fill rule), so:
- Shells crossing each other print as their union. The repair keeps them as separate shells, as the parts of an
object are kept, and makes each of them an outward oriented solid.
- A shell inside a solid that is oriented like it adds nothing, and the repair removes it.
- A shell inside a solid that is oriented inward is a cavity, and stays one.
- A whole mesh turned inside out prints as a solid, and the repair turns it outward.
On top of that, it closes holes and cracks, gives inconsistently oriented triangles the orientation of their
neighbours, drops degenerate, duplicate and stray flat geometry, and removes small self-intersections inside a shell,
such as slivers and folds. Where none of this applies, it reports the mesh as unmodified and the caller keeps the
original mesh, its painting and its object id.
A volume is repaired in place and is never split into several parts: splitting by connectivity would turn a cavity
into a separate solid part and fill it. Each solid of the result is a shell of its own, so *Split to parts* still
separates them. Open pieces of the input whose borders meet exactly, which are pieces of one surface rather than
solids, come out as the one shell they form.
## Pipeline
The input is treated as a triangle soup. Every stage after the first only runs on what the previous ones could
not settle, so a valid mesh passes through quickly.
### Valid parts first
Parts of the input that are connected by vertex indices and are already closed, consistently oriented 2-manifolds
skip the soup stages and go straight to the shell stages. Merging their points with coincident points of other
parts would only make non-manifold junctions out of solids that touch, for example parts of an assembly exported
together or voxels written as separate cubes.
### Soup clean-up
The rest of the soup has its coincident points merged. Triangles with repeated corners are dropped. Coincident
triangles of opposite orientation are a wall between two solids touching face to face, so they cancel in pairs;
of the remaining copies of a triangle one is kept. A connected region that would vanish entirely through
cancelling is a two-sided export instead (every face written in both orientations), and keeps one copy of each
triangle.
Open edges then get their vertices welded within a tolerance of 1e-4 of the bounding box diagonal, which closes the
cracks some exporters leave between triangles that should share corners. A vertex is never welded further than a
quarter of its shortest edge, so welding cannot collapse fine detail. A soup that stays mostly open after welding has
cracks wider than that tolerance, and is welded again with ten and then a hundred times the tolerance.
### Work units
The soup is cut into units repaired independently and in parallel: its connected parts, except that open parts whose
bounding boxes touch share a unit, so that cracks and T-junctions between separately exported patches still close.
### Building the mesh
A unit whose edges are each shared by at most two triangles, running in opposite directions, and whose vertices each
have a single fan of triangles, becomes a `Surface_mesh` directly. Any other unit is oriented first. Triangles are
flipped so that neighbours across an edge shared by two of them agree, each part keeping the orientation the
majority of its area had, which keeps inward oriented cavities inward. Edges where that is impossible or that more
than two triangles share are cut, and each fan of triangles around a vertex gets its own copy of the vertex. If that
still leaves an invalid mesh, CGAL's `orient_polygon_soup()` orients the unit instead, and the parts it flipped
against their majority are reversed back. The linear pass exists because `orient_polygon_soup()` is several times
slower on large soups that need only a few fixes.
### Closing
Compatible borders are stitched, parts that can be stitched once reversed are merged, and borders still open are
snapped onto each other within the weld tolerance, which closes T-junctions. Exactly degenerate faces are removed.
Holes are filled last. Planar border loops that face each other in one plane, one inside the other, bound a single
region, such as a missing annulus at the end of a tube or a cap with features standing on it. Such a group is
triangulated at once with a 2D constrained Delaunay triangulation that fills only the faces at odd nesting depth,
so the openings inside the region stay open. The patch is used only if it closes every loop with the orientation
of the triangles around it. Every other loop is filled on its own with `triangulate_hole()`, which uses a 2D
constrained Delaunay triangulation when the loop is planar. Otherwise it searches the best triangulation among all
triangles over the loop for loops of up to 100 edges, and among the triangles of a 3D Delaunay triangulation for
longer ones, as the full search is cubic in the length of the loop. Beyond 200 edges the full search is not even
used as a fallback, and a fan around the loop's centroid closes a loop nothing else could.
The closed unit is split into its shells. The largest stays in place and the others are copied out, as converting
the whole mesh again costs seconds on large models.
### Shells
The shells of all units, together with the valid parts set aside at the start, are handled independently and in
parallel. Shells enclosing no volume, which hole filling makes out of stray flat patches, are dropped.
Self-intersections inside a shell are removed locally with `experimental::remove_self_intersections()`, which
remeshes the region around them. It is used only for local defects: a shell whose intersections involve more than
5 % of its faces is left as it is, and a result that changes the enclosed volume by more than 0.1 % is undone. On
heavily overlapping ornamental models the remeshing otherwise cuts parts down to slivers, while on genuine defects
it changes the volume by thousandths of a percent. Overlaps left in place slice correctly. Remeshing can cut
small pieces off a shell, so a shell it changed is split again and its pieces enclosing no volume are dropped.
Shells crossing others are found through their bounding boxes and an AABB tree per shell, and are made outward
oriented solids. For every other shell the winding number just outside it is summed from the orientation of the
shells containing it, tested from a point on its largest face. The shell bounds the solid only if the winding
number is zero on exactly one of its sides; it is then oriented so that the solid is inside, or dropped otherwise.
Crossing shells are not united. Slicing unites them anyway, uniting them exactly costs tens of seconds on models
made of many overlapping pieces, and it would fuse parts that *Split to parts* should keep apart.
## Robustness
The repair avoids `experimental::autorefine_and_remove_self_intersections()` and corefinement, which can crash on
messy input, and uses only steps whose failures it can detect. A step that fails keeps its input. CGAL precondition
failures surface as `repair_mesh()` returning false with the message as the error. The output is written in float,
as every mesh in the application is.
The result does not depend on thread scheduling. `libslic3r_cgal` is built with `CGAL_LINKED_WITH_TBB`. Units and
shells are processed in parallel, but each on its own data, and their results are gathered in a fixed order;
sorts compare complete keys, and the detected self-intersections are put in order before they are used. Snapping
stays sequential, as its parallel version gives different results from run to run. Holes are filled with the full
search wherever it is affordable, because the search among Delaunay triangles can choose between equally good fills
differently from run to run, so only non-planar holes of more than 100 edges may still vary. On some inputs
`experimental::remove_self_intersections()` itself places a few vertices differently from run to run.
## Verification
`tests/libslic3r/test_mesh_repair.cpp` covers each kind of defect above on small meshes with known volumes.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g transform="rotate(-45 8 8)"><path d="M8 14.4 6.5 11.2 9.5 11.2 Z" style="fill:#009688"/><path d="M6.5 7.3 6.5 11.2 8 14.4 9.5 11.2 9.5 7.3" style="fill:none;stroke:#949494;stroke-width:0.4;stroke-linejoin:round;stroke-linecap:round"/><rect x="5.5" y="3.8" width="5" height="3.5" rx="0.9" style="fill:none;stroke:#949494;stroke-width:0.4;stroke-linejoin:round"/></g></svg>

After

Width:  |  Height:  |  Size: 458 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><circle cx="5.9" cy="6.3" r="1.25" style="fill:#009688"/><path d="M3.4 11.9 6.4 7.7 8.8 10.7 10.4 8.6 12.6 11.9 Z" style="fill:#009688"/><rect x="2.375" y="2.375" width="11.25" height="11.25" rx="1.25" style="fill:none;stroke:#949494;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round"/></svg>

After

Width:  |  Height:  |  Size: 386 B

+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "Bambulab", "name": "Bambulab",
"url": "http://www.bambulab.com/Parameters/vendor/BBL.json", "url": "http://www.bambulab.com/Parameters/vendor/BBL.json",
"version": "02.08.00.14", "version": "02.08.00.15",
"force_update": "0", "force_update": "0",
"description": "the initial version of BBL configurations", "description": "the initial version of BBL configurations",
"machine_model_list": [ "machine_model_list": [
File diff suppressed because one or more lines are too long
@@ -51,6 +51,11 @@ uniform float tex_aspect;
uniform float rotation_rad; uniform float rotation_rad;
uniform vec2 uv_offset; uniform vec2 uv_offset;
uniform bool invert; uniform bool invert;
// False means the layer places a single copy rather than repeating: outside that copy there is
// simply no texture. DecodedHeightTexture::sample() has always returned 0 there, so the bake got
// this right while the preview wrapped the image and showed a tiled pattern that was never going
// to be printed.
uniform bool tile_enabled;
uniform float midlevel; // the height that means "don't move"; needed by the parallax step uniform float midlevel; // the height that means "don't move"; needed by the parallax step
uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor) uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor)
uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin
@@ -218,6 +223,14 @@ int nearest_palette_entry(vec3 rgb)
return best; return best;
} }
// True where this uv has texture on it: everywhere when the layer repeats, and only inside the
// single placed copy when it does not. The bake's own sampler draws the same line (see
// DecodedHeightTexture::sample), so the preview now stops at the same edge it does.
bool in_tile(vec2 uv)
{
return tile_enabled || (uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0);
}
void main() void main()
{ {
if (any(lessThan(clipping_planes_dots, ZERO))) if (any(lessThan(clipping_planes_dots, ZERO)))
@@ -243,9 +256,9 @@ void main()
? vec2(dot(island_delta_lin.xy, vertex_uv), dot(island_delta_lin.zw, vertex_uv)) + island_delta_tr ? vec2(dot(island_delta_lin.xy, vertex_uv), dot(island_delta_lin.zw, vertex_uv)) + island_delta_tr
: vertex_uv; : vertex_uv;
color_uv = uv; color_uv = uv;
have_uv = true; have_uv = in_tile(uv);
float h = texture2D(height_tex, uv).r; float h = texture2D(height_tex, uv).r;
float k = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0); float k = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0) * (have_uv ? 1.0 : 0.0);
vec3 sigmaS = dFdx(world_pos.xyz); vec3 sigmaS = dFdx(world_pos.xyz);
vec3 sigmaT = dFdy(world_pos.xyz); vec3 sigmaT = dFdy(world_pos.xyz);
vec3 R1 = cross(sigmaT, triangle_normal); vec3 R1 = cross(sigmaT, triangle_normal);
@@ -262,10 +275,12 @@ void main()
// Parallax occlusion mapping: march the view ray through the height shell and shade at the // Parallax occlusion mapping: march the view ray through the height shell and shade at the
// first point where it drops below the displaced surface (see header). // first point where it drops below the displaced surface (see header).
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0); // uv first: outside a single placed copy there is no shell to march through.
vec2 uv = project_uv(tex_pos, triangle_normal);
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0) *
(in_tile(uv) ? 1.0 : 0.0);
vec3 view_dir = normalize(eye_model_pos - tex_pos); vec3 view_dir = normalize(eye_model_pos - tex_pos);
float v_dot_n = dot(view_dir, triangle_normal); float v_dot_n = dot(view_dir, triangle_normal);
vec2 uv = project_uv(tex_pos, triangle_normal);
// The shell the displaced surface lives inside, as signed heights along the normal. Taken from // The shell the displaced surface lives inside, as signed heights along the normal. Taken from
// both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel, // both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel,
@@ -302,7 +317,7 @@ void main()
} }
color_uv = uv; // after the parallax march, so colour and relief stay registered color_uv = uv; // after the parallax march, so colour and relief stay registered
have_uv = true; have_uv = in_tile(uv);
float hL = texture2D(height_tex, uv - vec2(height_tex_texel.x, 0.0)).r; float hL = texture2D(height_tex, uv - vec2(height_tex_texel.x, 0.0)).r;
float hR = texture2D(height_tex, uv + vec2(height_tex_texel.x, 0.0)).r; float hR = texture2D(height_tex, uv + vec2(height_tex_texel.x, 0.0)).r;
@@ -311,7 +326,8 @@ void main()
vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y)); vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y));
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0; float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0); float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0) *
(have_uv ? 1.0 : 0.0);
float cs = cos(rotation_rad); float cs = cos(rotation_rad);
float sn = sin(rotation_rad); float sn = sin(rotation_rad);
@@ -341,7 +357,12 @@ void main()
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as // Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
// the same material under the same light, and the relief this preview exists to show is unaffected. // the same material under the same light, and the relief this preview exists to show is unaffected.
vec3 albedo = uniform_color.rgb; vec3 albedo = uniform_color.rgb;
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0) // The fourth channel is coverage: where the picture is not there, the surface keeps its own
albedo = palette_rgb[nearest_palette_entry(texture2D(color_tex, color_uv).rgb)]; // colour rather than being painted the black that a transparent texel's rgb reads as.
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0) {
vec4 tex_rgba = texture2D(color_tex, color_uv);
if (tex_rgba.a >= 0.5)
albedo = palette_rgb[nearest_palette_entry(tex_rgba.rgb)];
}
gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a); gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
} }
@@ -110,6 +110,11 @@ uniform float tex_aspect;
uniform float rotation_rad; uniform float rotation_rad;
uniform vec2 uv_offset; uniform vec2 uv_offset;
uniform bool invert; uniform bool invert;
// False means the layer places a single copy rather than repeating: outside that copy there is
// simply no texture. DecodedHeightTexture::sample() has always returned 0 there, so the bake got
// this right while the preview wrapped the image and showed a tiled pattern that was never going
// to be printed.
uniform bool tile_enabled;
uniform float midlevel; // the height that means "don't move"; needed by the parallax step uniform float midlevel; // the height that means "don't move"; needed by the parallax step
uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor) uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor)
uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin
@@ -284,6 +289,14 @@ int nearest_palette_entry(vec3 rgb)
return best; return best;
} }
// True where this uv has texture on it: everywhere when the layer repeats, and only inside the
// single placed copy when it does not. The bake's own sampler draws the same line (see
// DecodedHeightTexture::sample), so the preview now stops at the same edge it does.
bool in_tile(vec2 uv)
{
return tile_enabled || (uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0);
}
void main() void main()
{ {
if (any(lessThan(clipping_planes_dots, ZERO))) if (any(lessThan(clipping_planes_dots, ZERO)))
@@ -321,9 +334,9 @@ void main()
? vec2(dot(island_delta_lin.xy, vertex_uv), dot(island_delta_lin.zw, vertex_uv)) + island_delta_tr ? vec2(dot(island_delta_lin.xy, vertex_uv), dot(island_delta_lin.zw, vertex_uv)) + island_delta_tr
: vertex_uv; : vertex_uv;
color_uv = uv; color_uv = uv;
have_uv = true; have_uv = in_tile(uv);
float h = texture(height_tex, uv).r; float h = texture(height_tex, uv).r;
float k = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0); float k = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0) * (have_uv ? 1.0 : 0.0);
vec3 sigmaS = dFdx(world_pos.xyz); vec3 sigmaS = dFdx(world_pos.xyz);
vec3 sigmaT = dFdy(world_pos.xyz); vec3 sigmaT = dFdy(world_pos.xyz);
vec3 R1 = cross(sigmaT, triangle_normal); vec3 R1 = cross(sigmaT, triangle_normal);
@@ -343,10 +356,12 @@ void main()
// Parallax occlusion mapping: march the view ray through the height shell and shade at the // Parallax occlusion mapping: march the view ray through the height shell and shade at the
// first point where it drops below the displaced surface (see header). // first point where it drops below the displaced surface (see header).
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0); // uv first: outside a single placed copy there is no shell to march through.
vec2 uv = project_uv(tex_pos, triangle_normal);
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0) *
(in_tile(uv) ? 1.0 : 0.0);
vec3 view_dir = normalize(eye_model_pos - tex_pos); vec3 view_dir = normalize(eye_model_pos - tex_pos);
float v_dot_n = dot(view_dir, triangle_normal); float v_dot_n = dot(view_dir, triangle_normal);
vec2 uv = project_uv(tex_pos, triangle_normal);
// The shell the displaced surface lives inside, as signed heights along the normal. Taken from // The shell the displaced surface lives inside, as signed heights along the normal. Taken from
// both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel, // both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel,
@@ -383,7 +398,7 @@ void main()
} }
color_uv = uv; // after the parallax march, so colour and relief stay registered color_uv = uv; // after the parallax march, so colour and relief stay registered
have_uv = true; have_uv = in_tile(uv);
float hL = texture(height_tex, uv - vec2(height_tex_texel.x, 0.0)).r; float hL = texture(height_tex, uv - vec2(height_tex_texel.x, 0.0)).r;
float hR = texture(height_tex, uv + vec2(height_tex_texel.x, 0.0)).r; float hR = texture(height_tex, uv + vec2(height_tex_texel.x, 0.0)).r;
@@ -396,7 +411,8 @@ void main()
// uv -> mm is 1/tiling_scale for the triplanar projection, so this turns the uv-space // uv -> mm is 1/tiling_scale for the triplanar projection, so this turns the uv-space
// gradient into a real surface slope. // gradient into a real surface slope.
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0; float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0); float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0) *
(have_uv ? 1.0 : 0.0);
// uv was rotated by project_uv() while t/b are the unrotated model axes, so rotate the // uv was rotated by project_uv() while t/b are the unrotated model axes, so rotate the
// gradient back into the axes' frame. // gradient back into the axes' frame.
float cs = cos(rotation_rad); float cs = cos(rotation_rad);
@@ -429,7 +445,12 @@ void main()
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as // Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
// the same material under the same light, and the relief this preview exists to show is unaffected. // the same material under the same light, and the relief this preview exists to show is unaffected.
vec3 albedo = uniform_color.rgb; vec3 albedo = uniform_color.rgb;
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0) // The fourth channel is coverage: where the picture is not there, the surface keeps its own
albedo = palette_rgb[nearest_palette_entry(texture(color_tex, color_uv).rgb)]; // colour rather than being painted the black that a transparent texel's rgb reads as.
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0) {
vec4 tex_rgba = texture(color_tex, color_uv);
if (tex_rgba.a >= 0.5)
albedo = palette_rgb[nearest_palette_entry(tex_rgba.rgb)];
}
out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a); out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
} }
+43 -17
View File
@@ -1,21 +1,27 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Lets a pull request's macOS deps build wait for a push build of the base # Lets a pull request's macOS deps build, or its clang-tidy job, wait for a
# branch that builds the same deps, from a Linux job. `check` runs after a cache # build that saves the deps cache it needs, from a Linux job. `check` runs after
# miss and names a queued or running base build with the same deps/ and .github/ # a cache miss and names a queued or running push build of the base branch with
# to wait for, unless the pull request changes either. `wait` waits until that # the same deps/ and .github/ to wait for, unless the pull request changes either.
# build saves KEY. actions/cache then confirms the entry can be restored. # `own` names the pull request's own build instead, for clang-tidy when the pull
# request changes deps/. `wait` waits until that build saves KEY under CACHE_REF,
# or its deps job fails. actions/cache then confirms the entry can be restored.
# #
# Environment: GH_TOKEN, REPO, WORKFLOW_REF and BASE_REF, plus KEY_INPUTS, # Environment: GH_TOKEN, REPO, WORKFLOW_REF (or WORKFLOW, the build workflow's
# WAIT_FOR, WAIT_SINCE, WAIT_MINUTES, KEY and ARCH for `wait`. # file name) and BASE_REF, plus HEAD_SHA for `own`, and KEY_INPUTS, WAIT_FOR,
# WAIT_SINCE, WAIT_MINUTES, KEY, CACHE_REF (default the base branch) and ARCH
# (or JOB, the matrix job's name) for `wait`.
# Writes key-inputs, wait-for and wait-since to $GITHUB_OUTPUT. # Writes key-inputs, wait-for and wait-since to $GITHUB_OUTPUT.
set -uo pipefail set -uo pipefail
workflow=${WORKFLOW_REF%%@*} workflow=${WORKFLOW_REF:-}
workflow=${workflow##*/} workflow=${workflow%%@*}
workflow=${WORKFLOW:-${workflow##*/}}
# Must match the job names in build_all.yml, build_check_cache.yml and build_deps.yml. # Must match the job names in build_all.yml, build_check_cache.yml and build_deps.yml.
check_job_name="build_macos_arch (${ARCH:-}) / Check Cache" job=${JOB:-build_macos_arch (${ARCH:-})}
deps_job_name="build_macos_arch (${ARCH:-}) / Build Deps / Build Deps" check_job_name="$job / Check Cache"
deps_job_name="$job / Build Deps / Build Deps"
out() { echo "$1" >> "${GITHUB_OUTPUT:-/dev/stdout}"; } out() { echo "$1" >> "${GITHUB_OUTPUT:-/dev/stdout}"; }
@@ -63,8 +69,27 @@ check() {
fi fi
} }
own() {
out "key-inputs=$(local_key_inputs HEAD)"
local id
# The build can be created a moment after this job.
for _ in 1 2 3 4 5; do
id=$(gh api -X GET "repos/$REPO/actions/workflows/$workflow/runs" \
-f head_sha="$HEAD_SHA" -f event=pull_request -f per_page=1 --jq '.workflow_runs[0].id // empty' < /dev/null)
[ -n "$id" ] && break
sleep 30
done
if [ -n "$id" ]; then
echo "Waiting for the deps cache of build $id of this pull request."
out "wait-for=$id"
out "wait-since=$(date +%s)"
else
echo "No build of this pull request is building its deps."
fi
}
key_saved() { key_saved() {
gh cache list -R "$REPO" --ref "refs/heads/$BASE_REF" --key "$KEY" --json key < /dev/null \ gh cache list -R "$REPO" --ref "${CACHE_REF:-refs/heads/$BASE_REF}" --key "$KEY" --json key < /dev/null \
| jq -e --arg key "$KEY" 'any(.[]; .key == $key)' > /dev/null | jq -e --arg key "$KEY" 'any(.[]; .key == $key)' > /dev/null
} }
@@ -87,11 +112,11 @@ wait_for() {
key_saved && { echo "Found $KEY."; return; } key_saved && { echo "Found $KEY."; return; }
sleep 20 sleep 20
done done
echo "Build $id of $BASE_REF finished its deps without saving $KEY." echo "Build $id finished its deps without saving $KEY."
return ;; return ;;
"completed cancelled") ;; "completed cancelled") ;;
completed*) completed*)
echo "The deps job of build $id of $BASE_REF did not succeed." echo "The deps job of build $id did not succeed."
return ;; return ;;
# The deps job exists only once the build's cache check has succeeded. # The deps job exists only once the build's cache check has succeeded.
"") "")
@@ -100,7 +125,7 @@ wait_for() {
completed*) completed*)
check=$(jq -r --arg name "$check_job_name" 'first(.jobs[] | select(.name == $name) | .conclusion) // ""' <<< "$jobs") check=$(jq -r --arg name "$check_job_name" 'first(.jobs[] | select(.name == $name) | .conclusion) // ""' <<< "$jobs")
if [ -z "$warned" ] && { [ -z "$check" ] || [ "$check" = success ]; }; then if [ -z "$warned" ] && { [ -z "$check" ] || [ "$check" = success ]; }; then
echo "::warning title=Deps wait::Build $id of $BASE_REF has no job named \"$deps_job_name\". If the build jobs were renamed, update scripts/ci_deps_cache.sh. This does not affect this pull request." echo "::warning title=Deps wait::Build $id has no job named \"$deps_job_name\". If the build jobs were renamed, update scripts/ci_deps_cache.sh. This does not affect this pull request."
warned=1 warned=1
fi ;; fi ;;
*) *)
@@ -118,11 +143,12 @@ wait_for() {
seen="$seen $id" seen="$seen $id"
echo "Following build $id of $BASE_REF." echo "Following build $id of $BASE_REF."
done done
echo "No build of $BASE_REF saved $KEY." echo "No build saved $KEY."
} }
case "${1:-}" in case "${1:-}" in
check) check ;; check) check ;;
own) own ;;
wait) wait_for ;; wait) wait_for ;;
*) echo "usage: $0 check|wait" >&2; exit 2 ;; *) echo "usage: $0 check|own|wait" >&2; exit 2 ;;
esac esac
+28 -6
View File
@@ -99,6 +99,7 @@
#include "libslic3r/Preset.hpp" #include "libslic3r/Preset.hpp"
#include "libslic3r/Geometry.hpp" #include "libslic3r/Geometry.hpp"
#include "libslic3r/GCode.hpp" #include "libslic3r/GCode.hpp"
#include "libslic3r/MeshRepair.hpp"
#include "libslic3r/Model.hpp" #include "libslic3r/Model.hpp"
#include "libslic3r/ModelArrange.hpp" #include "libslic3r/ModelArrange.hpp"
#include "libslic3r/Platform.hpp" #include "libslic3r/Platform.hpp"
@@ -206,7 +207,7 @@ std::map<int, std::string> cli_errors = {
{CLI_OBJECT_ORIENT_FAILED, "An error occurred when auto-orienting object(s)."}, {CLI_OBJECT_ORIENT_FAILED, "An error occurred when auto-orienting object(s)."},
{CLI_MODIFIED_PARAMS_TO_PRINTER, "Found modified parameter in printer preset in the 3mf file, which should not be changed."}, {CLI_MODIFIED_PARAMS_TO_PRINTER, "Found modified parameter in printer preset in the 3mf file, which should not be changed."},
{CLI_FILE_VERSION_NOT_SUPPORTED, "Unsupported 3MF version. Please make sure the 3MF file was created with the official version of Bambu Studio, not a beta version."}, {CLI_FILE_VERSION_NOT_SUPPORTED, "Unsupported 3MF version. Please make sure the 3MF file was created with the official version of Bambu Studio, not a beta version."},
{CLI_NO_SUITABLE_OBJECTS, "One of the plate is empty or has no object fully inside it. Please check that the 3mf contains no empty plate in Orca Slicer before uploading."}, {CLI_NO_SUITABLE_OBJECTS, "The plate has nothing to slice: it is empty, or no object on it is both printable and fully inside the print volume."},
{CLI_VALIDATE_ERROR, "There are some incorrect slicing parameters in the 3mf. Please verify the slicing of all plates in Orca Slicer before uploading."}, {CLI_VALIDATE_ERROR, "There are some incorrect slicing parameters in the 3mf. Please verify the slicing of all plates in Orca Slicer before uploading."},
{CLI_OBJECTS_PARTLY_INSIDE, "Some objects are located over the boundary of the heated bed."}, {CLI_OBJECTS_PARTLY_INSIDE, "Some objects are located over the boundary of the heated bed."},
{CLI_EXPORT_CACHE_DIRECTORY_CREATE_FAILED, "Failed creating directory when exporting cache data."}, {CLI_EXPORT_CACHE_DIRECTORY_CREATE_FAILED, "Failed creating directory when exporting cache data."},
@@ -1463,7 +1464,7 @@ int CLI::run(int argc, char **argv)
//int arrange_option; //int arrange_option;
int plate_to_slice = 0, filament_count = 0, duplicate_count = 0, real_duplicate_count = 0, current_extruder_count = 1, new_extruder_count = 1, current_printer_variant_count = 1, current_print_variant_count = 1, new_printer_variant_count = 1; int plate_to_slice = 0, filament_count = 0, duplicate_count = 0, real_duplicate_count = 0, current_extruder_count = 1, new_extruder_count = 1, current_printer_variant_count = 1, current_print_variant_count = 1, new_printer_variant_count = 1;
bool first_file = true, is_bbl_3mf = false, need_arrange = true, has_thumbnails = false, up_config_to_date = false, normative_check = true, duplicate_single_object = false, use_first_fila_as_default = false, minimum_save = false, enable_timelapse = false; bool first_file = true, is_bbl_3mf = false, need_arrange = true, has_thumbnails = false, up_config_to_date = false, normative_check = true, duplicate_single_object = false, use_first_fila_as_default = false, minimum_save = false, enable_timelapse = false;
bool allow_rotations = true, skip_modified_gcodes = false, avoid_extrusion_cali_region = false, skip_useless_pick = false, allow_newer_file = false, current_is_multi_extruder = false, new_is_multi_extruder = false, allow_mix_temp = false, enable_wrapping_detect = false; bool allow_rotations = false, skip_modified_gcodes = false, avoid_extrusion_cali_region = false, skip_useless_pick = false, allow_newer_file = false, current_is_multi_extruder = false, new_is_multi_extruder = false, allow_mix_temp = false, enable_wrapping_detect = false;
Semver file_version; Semver file_version;
std::map<size_t, bool> orients_requirement; std::map<size_t, bool> orients_requirement;
std::vector<Preset*> project_presets; std::vector<Preset*> project_presets;
@@ -5130,9 +5131,19 @@ int CLI::run(int argc, char **argv)
} }
} }
} else if (opt_key == "repair") { } else if (opt_key == "repair") {
// Models are repaired by default. if (m_config.opt_bool(opt_key))
//for (auto &model : m_models) for (Model &model : m_models)
// model.repair(); for (ModelObject *object : model.objects) {
for (ModelVolume *volume : object->volumes) {
MeshRepairStats stats;
std::string error;
if (!volume->repair(true, &stats, &error))
BOOST_LOG_TRIVIAL(warning) << "repair of " << object->name << " / " << volume->name << " failed: " << error;
else if (stats.modified)
BOOST_LOG_TRIVIAL(info) << "repaired " << object->name << " / " << volume->name;
}
object->invalidate_bounding_box();
}
} else { } else {
boost::nowide::cerr << "error: option not implemented yet: " << opt_key << std::endl; boost::nowide::cerr << "error: option not implemented yet: " << opt_key << std::endl;
record_exit_reson(outfile_dir, CLI_UNSUPPORTED_OPERATION, 0, cli_errors[CLI_UNSUPPORTED_OPERATION], sliced_info); record_exit_reson(outfile_dir, CLI_UNSUPPORTED_OPERATION, 0, cli_errors[CLI_UNSUPPORTED_OPERATION], sliced_info);
@@ -5142,6 +5153,7 @@ int CLI::run(int argc, char **argv)
BOOST_LOG_TRIVIAL(info) << "finished model pre-process commands\n"; BOOST_LOG_TRIVIAL(info) << "finished model pre-process commands\n";
bool oriented_or_arranged = false; bool oriented_or_arranged = false;
bool arranged_without_rotations = false;
//BBS: add orient and arrange logic here //BBS: add orient and arrange logic here
for (auto& model : m_models) for (auto& model : m_models)
{ {
@@ -5409,6 +5421,7 @@ int CLI::run(int argc, char **argv)
BOOST_LOG_TRIVIAL(info) << boost::format("start plate %1%'s arranging...") % (i + 1); BOOST_LOG_TRIVIAL(info) << boost::format("start plate %1%'s arranging...") % (i + 1);
arrangement::arrange(selected, unselected, beds, arrange_cfg); arrangement::arrange(selected, unselected, beds, arrange_cfg);
//arrangement::arrange(unprintable, {}, beds, arrange_cfg); //arrangement::arrange(unprintable, {}, beds, arrange_cfg);
arranged_without_rotations |= !arrange_cfg.allow_rotations;
BOOST_LOG_TRIVIAL(info) << boost::format("finished plate %1%'s arranging") % (i + 1); BOOST_LOG_TRIVIAL(info) << boost::format("finished plate %1%'s arranging") % (i + 1);
//step-4: postprocess the bed index and result //step-4: postprocess the bed index and result
@@ -5881,6 +5894,7 @@ int CLI::run(int argc, char **argv)
BOOST_LOG_TRIVIAL(info) << boost::format("start %1% th arranging...")%arrange_count; BOOST_LOG_TRIVIAL(info) << boost::format("start %1% th arranging...")%arrange_count;
arrangement::arrange(selected, unselected, beds, arrange_cfg); arrangement::arrange(selected, unselected, beds, arrange_cfg);
arrangement::arrange(unprintable, {}, beds, arrange_cfg); arrangement::arrange(unprintable, {}, beds, arrange_cfg);
arranged_without_rotations |= !arrange_cfg.allow_rotations;
BOOST_LOG_TRIVIAL(info) << boost::format("finished %1% th arranging...")%arrange_count; BOOST_LOG_TRIVIAL(info) << boost::format("finished %1% th arranging...")%arrange_count;
//Step-4:postprocess by partplate list&&apply the result //Step-4:postprocess by partplate list&&apply the result
@@ -6425,7 +6439,15 @@ int CLI::run(int argc, char **argv)
if (count == 0) { if (count == 0) {
BOOST_LOG_TRIVIAL(error) << "plate "<< index+1<< ": Nothing to be sliced, Either the print is empty or no object is fully inside the print volume before apply." << std::endl; BOOST_LOG_TRIVIAL(error) << "plate "<< index+1<< ": Nothing to be sliced, Either the print is empty or no object is fully inside the print volume before apply." << std::endl;
record_exit_reson(outfile_dir, CLI_NO_SUITABLE_OBJECTS, index+1, cli_errors[CLI_NO_SUITABLE_OBJECTS], sliced_info); std::string no_objects_reason = cli_errors[CLI_NO_SUITABLE_OBJECTS];
if (arranged_without_rotations) {
//ORCA: result.json is only written on Linux, so also print the reason to stderr.
no_objects_reason = "No object is fully inside the print volume. Arrange ran without rotations."
" If an object only fits the bed when rotated, pass --allow-rotations."
" Otherwise the plate is empty or an object is larger than the printable area or height.";
boost::nowide::cerr << "plate " << index+1 << ": " << no_objects_reason << std::endl;
}
record_exit_reson(outfile_dir, CLI_NO_SUITABLE_OBJECTS, index+1, no_objects_reason, sliced_info);
flush_and_exit(CLI_NO_SUITABLE_OBJECTS); flush_and_exit(CLI_NO_SUITABLE_OBJECTS);
} }
else if ((plate_to_slice != 0) || pre_check) { else if ((plate_to_slice != 0) || pre_check) {
+4 -1
View File
@@ -625,6 +625,7 @@ add_library(libslic3r_cgal STATIC
CutSurface.hpp CutSurface.cpp CutSurface.hpp CutSurface.cpp
IntersectionPoints.hpp IntersectionPoints.cpp IntersectionPoints.hpp IntersectionPoints.cpp
MeshBoolean.hpp MeshBoolean.cpp MeshBoolean.hpp MeshBoolean.cpp
MeshRepair.hpp MeshRepair.cpp
TryCatchSignal.hpp TryCatchSignal.cpp TryCatchSignal.hpp TryCatchSignal.cpp
Triangulation.hpp Triangulation.cpp Triangulation.hpp Triangulation.cpp
) )
@@ -652,7 +653,9 @@ if (IS_CLANG_CL)
target_compile_options(libslic3r_cgal PRIVATE -Wno-overriding-option) target_compile_options(libslic3r_cgal PRIVATE -Wno-overriding-option)
endif () endif ()
target_link_libraries(libslic3r_cgal PRIVATE ${_cgal_tgt} admesh libigl mcut boost_libs) # Lets CGAL algorithms given CGAL::Parallel_if_available_tag run on TBB.
target_compile_definitions(libslic3r_cgal PRIVATE CGAL_LINKED_WITH_TBB)
target_link_libraries(libslic3r_cgal PRIVATE ${_cgal_tgt} admesh libigl mcut boost_libs TBB::tbb)
if (MSVC AND "${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") # 32 bit MSVC workaround if (MSVC AND "${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") # 32 bit MSVC workaround
target_compile_definitions(libslic3r_cgal PRIVATE CGAL_DO_NOT_USE_MPZF) target_compile_definitions(libslic3r_cgal PRIVATE CGAL_DO_NOT_USE_MPZF)
+15 -5
View File
@@ -236,19 +236,29 @@ static void getNamedSolids(const TopLoc_Location& location,
int i = 0; int i = 0;
switch (shape_type) { switch (shape_type) {
case TopAbs_COMPOUND: case TopAbs_COMPOUND:
if (!isSplitCompound) {
namedSolids.emplace_back(TopoDS::Compound(transform.Shape()), fullName);
break;
}
case TopAbs_COMPSOLID: case TopAbs_COMPSOLID:
if (!isSplitCompound) { if (!isSplitCompound) {
namedSolids.emplace_back(TopoDS::CompSolid(transform.Shape()), fullName); namedSolids.emplace_back(transform.Shape(), fullName);
} else { } else {
for (explorer.Init(transform.Shape(), TopAbs_SOLID); explorer.More(); explorer.Next()) { for (explorer.Init(transform.Shape(), TopAbs_SOLID); explorer.More(); explorer.Next()) {
i++; i++;
const TopoDS_Shape& currentShape = explorer.Current(); const TopoDS_Shape& currentShape = explorer.Current();
namedSolids.emplace_back(TopoDS::Solid(currentShape), fullName + "-SOLID-" + std::to_string(i)); namedSolids.emplace_back(TopoDS::Solid(currentShape), fullName + "-SOLID-" + std::to_string(i));
} }
// Surface models have no solids: each shell outside a solid is a part, and loose faces stay together.
i = 0;
for (explorer.Init(transform.Shape(), TopAbs_SHELL, TopAbs_SOLID); explorer.More(); explorer.Next())
namedSolids.emplace_back(explorer.Current(), fullName + "-SHELL-" + std::to_string(++i));
TopoDS_Compound loose_faces;
TopoDS_Builder builder;
builder.MakeCompound(loose_faces);
bool has_loose_faces = false;
for (explorer.Init(transform.Shape(), TopAbs_FACE, TopAbs_SHELL); explorer.More(); explorer.Next()) {
builder.Add(loose_faces, explorer.Current());
has_loose_faces = true;
}
if (has_loose_faces)
namedSolids.emplace_back(loose_faces, fullName);
} }
break; break;
case TopAbs_SOLID: case TopAbs_SOLID:
+165 -1
View File
@@ -22,6 +22,7 @@
#include "libslic3r/BrimEarsPoint.hpp" #include "libslic3r/BrimEarsPoint.hpp"
#include "libslic3r/Config.hpp" #include "libslic3r/Config.hpp"
#include "libslic3r/CustomGCode.hpp" #include "libslic3r/CustomGCode.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "libslic3r_version.h" #include "libslic3r_version.h"
#include "bbs_3mf.hpp" #include "bbs_3mf.hpp"
@@ -354,6 +355,13 @@ static constexpr const char* CUSTOM_SUPPORTS_ATTR = "paint_supports";
static constexpr const char* CUSTOM_FUZZY_SKIN_ATTR = "paint_fuzzy_skin"; static constexpr const char* CUSTOM_FUZZY_SKIN_ATTR = "paint_fuzzy_skin";
static constexpr const char* CUSTOM_SEAM_ATTR = "paint_seam"; static constexpr const char* CUSTOM_SEAM_ATTR = "paint_seam";
static constexpr const char* MMU_SEGMENTATION_ATTR = "paint_color"; static constexpr const char* MMU_SEGMENTATION_ATTR = "paint_color";
// Texture displacement. One paint mask per layer slot, mirroring paint_color; the layer stack itself is
// a JSON file in the archive, named by the volume metadata key below (see add_texture_displacement()).
static constexpr const char* TEXTURE_DISPLACEMENT_ATTRS[Slic3r::TEXTURE_DISPLACEMENT_MAX_LAYERS] = {
"paint_texture_0", "paint_texture_1", "paint_texture_2", "paint_texture_3",
"paint_texture_4", "paint_texture_5", "paint_texture_6", "paint_texture_7" };
static constexpr const char* TEXTURE_DISPLACEMENT_KEY = "texture_displacement";
static constexpr const char* TEXTURE_DISPLACEMENT_DIR = "Metadata/texture_displacement/";
// BBS // BBS
static constexpr const char* FACE_PROPERTY_ATTR = "face_property"; static constexpr const char* FACE_PROPERTY_ATTR = "face_property";
@@ -847,6 +855,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
std::vector<std::string> custom_seam; std::vector<std::string> custom_seam;
std::vector<std::string> mmu_segmentation; std::vector<std::string> mmu_segmentation;
std::vector<std::string> fuzzy_skin; std::vector<std::string> fuzzy_skin;
// One per texture displacement layer slot, indexed like `triangles` but stored only as far as the
// slot's last painted triangle: empty when the slot has no paint, which is nearly every file.
std::vector<std::string> texture_displacement[TEXTURE_DISPLACEMENT_MAX_LAYERS];
// BBS // BBS
std::vector<std::string> face_properties; std::vector<std::string> face_properties;
@@ -867,6 +878,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
custom_seam.clear(); custom_seam.clear();
mmu_segmentation.clear(); mmu_segmentation.clear();
fuzzy_skin.clear(); fuzzy_skin.clear();
for (std::vector<std::string> &slot : texture_displacement)
slot.clear();
} }
}; };
@@ -1200,6 +1213,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
/*IdToSlaSupportPointsMap m_sla_support_points; /*IdToSlaSupportPointsMap m_sla_support_points;
IdToSlaDrainHolesMap m_sla_drain_holes;*/ IdToSlaDrainHolesMap m_sla_drain_holes;*/
PathToEmbossShapeFileMap m_path_to_emboss_shape_files; PathToEmbossShapeFileMap m_path_to_emboss_shape_files;
// Texture displacement: every file under Metadata/texture_displacement/, by archive path. Volumes
// are built only after the whole archive has been walked, so by the time a volume names its JSON
// every file it could refer to is already in here, whatever order the archive happened to be in.
std::map<std::string, std::string> m_texture_displacement_files;
void _apply_texture_displacement(ModelVolume &volume, const std::string &json_path);
std::string m_curr_metadata_name; std::string m_curr_metadata_name;
std::string m_curr_characters; std::string m_curr_characters;
std::string m_name; std::string m_name;
@@ -2073,6 +2091,13 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
else if (_is_svg_shape_file(name)) { else if (_is_svg_shape_file(name)) {
_extract_embossed_svg_shape_file(name, archive, stat); _extract_embossed_svg_shape_file(name, archive, stat);
} }
else if (boost::algorithm::istarts_with(name, TEXTURE_DISPLACEMENT_DIR)) {
std::string contents(stat.m_uncomp_size, '\0');
if (mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, contents.data(), stat.m_uncomp_size, 0))
m_texture_displacement_files.emplace(name, std::move(contents));
else
add_error("Error while reading texture displacement data");
}
else if (!dont_load_config && boost::algorithm::iequals(name, SLICE_INFO_CONFIG_FILE)) { else if (!dont_load_config && boost::algorithm::iequals(name, SLICE_INFO_CONFIG_FILE)) {
m_parsing_slice_info = true; m_parsing_slice_info = true;
//extract slice info from archive //extract slice info from archive
@@ -3310,6 +3335,45 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
} }
}*/ }*/
// Restores one volume's texture displacement stack from the JSON it named. Called while the volume is
// being built, which is after the whole archive has been walked - so every file it can refer to is
// already extracted, and no deferral is needed.
//
// A missing or malformed JSON leaves the volume with no layers but keeps the paint mask it already
// loaded, which is the same state as a project saved by a build without the feature: recoverable by
// picking the texture again rather than a hard failure.
void _BBS_3MF_Importer::_apply_texture_displacement(ModelVolume &volume, const std::string &json_path)
{
const auto json = m_texture_displacement_files.find(json_path);
if (json == m_texture_displacement_files.end()) {
add_error("Missing texture displacement data: " + json_path);
return;
}
std::vector<TextureDisplacementLayer> layers;
TextureDisplacementOptions options;
if (!texture_displacement_layers_from_json(json->second, layers, options)) {
add_error("Malformed texture displacement data: " + json_path);
return;
}
// The archive is the only source of a layer's pixels. `path` comes from the project file, so loading
// the project must never read the file it names: that would let whoever made the project pick any
// local (or, on Windows, network) file to read, and the next save would embed it in the archive.
// `path` stays a label of where the texture came from; a layer the archive carries no image for
// loads without one, its settings intact, until the user picks a texture again.
for (TextureDisplacementLayer &layer : layers) {
if (layer.path_in_3mf.empty())
continue;
const auto image = m_texture_displacement_files.find(layer.path_in_3mf);
if (image == m_texture_displacement_files.end()) {
add_error("Missing texture displacement image: " + layer.path_in_3mf);
continue;
}
layer.image_data = std::make_shared<std::vector<unsigned char>>(image->second.begin(), image->second.end());
}
volume.texture_displacement_layers = std::move(layers);
volume.texture_displacement_options = options;
}
void _BBS_3MF_Importer::_extract_embossed_svg_shape_file(const std::string &filename, mz_zip_archive &archive, const mz_zip_archive_file_stat &stat){ void _BBS_3MF_Importer::_extract_embossed_svg_shape_file(const std::string &filename, mz_zip_archive &archive, const mz_zip_archive_file_stat &stat){
assert(m_path_to_emboss_shape_files.find(filename) == m_path_to_emboss_shape_files.end()); assert(m_path_to_emboss_shape_files.find(filename) == m_path_to_emboss_shape_files.end());
auto file = std::make_unique<std::string>(stat.m_uncomp_size, '\0'); auto file = std::make_unique<std::string>(stat.m_uncomp_size, '\0');
@@ -3966,6 +4030,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
m_curr_object->geometry.custom_seam.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR)); m_curr_object->geometry.custom_seam.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR));
m_curr_object->geometry.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR)); m_curr_object->geometry.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR));
m_curr_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR)); m_curr_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR));
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
if (const char *paint = bbs_get_attribute_value_charptr(attributes, num_attributes, TEXTURE_DISPLACEMENT_ATTRS[slot])) {
std::vector<std::string> &mask = m_curr_object->geometry.texture_displacement[slot];
mask.resize(m_curr_object->geometry.triangles.size());
mask.back() = paint;
}
// BBS // BBS
m_curr_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR)); m_curr_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
} }
@@ -5317,6 +5387,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
volume->mmu_segmentation_facets.touch(); volume->mmu_segmentation_facets.touch();
volume->fuzzy_skin_facets.shrink_to_fit(); volume->fuzzy_skin_facets.shrink_to_fit();
volume->fuzzy_skin_facets.touch(); volume->fuzzy_skin_facets.touch();
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
const std::vector<std::string> &mask = sub_object->geometry.texture_displacement[slot];
if (mask.empty())
continue; // no triangle painted on this slot
FacetsAnnotation &facets = volume->texture_displacement_facet(slot);
facets.reserve(triangles_count);
for (size_t i = 0; i < triangles_count && i < mask.size(); ++i)
if (!mask[i].empty())
facets.set_triangle_from_string(i, mask[i]);
facets.shrink_to_fit();
facets.touch();
}
} }
volume->set_type(volume_data->part_type); volume->set_type(volume_data->part_type);
@@ -5332,6 +5414,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
for (const Metadata& metadata : volume_data->metadata) { for (const Metadata& metadata : volume_data->metadata) {
if (metadata.key == NAME_KEY) if (metadata.key == NAME_KEY)
volume->name = metadata.value; volume->name = metadata.value;
else if (metadata.key == TEXTURE_DISPLACEMENT_KEY)
_apply_texture_displacement(*volume, metadata.value);
//else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1")) //else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1"))
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER); // volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
//for old format //for old format
@@ -5496,12 +5580,28 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
volume->seam_facets.shrink_to_fit(); volume->seam_facets.shrink_to_fit();
volume->mmu_segmentation_facets.shrink_to_fit(); volume->mmu_segmentation_facets.shrink_to_fit();
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
const std::vector<std::string> &mask = geometry.texture_displacement[slot];
if (mask.empty())
continue; // written by a build without the feature
FacetsAnnotation &facets = volume->texture_displacement_facet(slot);
facets.reserve(triangles_count);
for (size_t i = 0; i < triangles_count; ++i) {
const size_t index = volume_data.first_triangle_id + i;
if (index < mask.size() && !mask[index].empty())
facets.set_triangle_from_string(i, mask[index]);
}
facets.shrink_to_fit();
facets.touch();
}
volume->set_type(volume_data.part_type); volume->set_type(volume_data.part_type);
// apply the remaining volume's metadata // apply the remaining volume's metadata
for (const Metadata& metadata : volume_data.metadata) { for (const Metadata& metadata : volume_data.metadata) {
if (metadata.key == NAME_KEY) if (metadata.key == NAME_KEY)
volume->name = metadata.value; volume->name = metadata.value;
else if (metadata.key == TEXTURE_DISPLACEMENT_KEY)
_apply_texture_displacement(*volume, metadata.value);
//else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1")) //else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1"))
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER); // volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
//for old format //for old format
@@ -5805,6 +5905,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
current_object->geometry.custom_seam.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR)); current_object->geometry.custom_seam.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR));
current_object->geometry.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR)); current_object->geometry.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR));
current_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR)); current_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR));
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
if (const char *paint = bbs_get_attribute_value_charptr(attributes, num_attributes, TEXTURE_DISPLACEMENT_ATTRS[slot])) {
std::vector<std::string> &mask = current_object->geometry.texture_displacement[slot];
mask.resize(current_object->geometry.triangles.size());
mask.back() = paint;
}
// BBS // BBS
current_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR)); current_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
} }
@@ -6808,6 +6914,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
stream << " <Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\n"; stream << " <Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\n";
stream << " <Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\n"; stream << " <Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\n";
stream << " <Default Extension=\"png\" ContentType=\"image/png\"/>\n"; stream << " <Default Extension=\"png\" ContentType=\"image/png\"/>\n";
stream << " <Default Extension=\"jpg\" ContentType=\"image/jpeg\"/>\n";
stream << " <Default Extension=\"jpeg\" ContentType=\"image/jpeg\"/>\n";
stream << " <Default Extension=\"json\" ContentType=\"application/json\"/>\n";
stream << " <Default Extension=\"gcode\" ContentType=\"text/x.gcode\"/>\n"; stream << " <Default Extension=\"gcode\" ContentType=\"text/x.gcode\"/>\n";
stream << "</Types>"; stream << "</Types>";
@@ -7232,10 +7341,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (iter != m_shared_meshes.end()) if (iter != m_shared_meshes.end())
{ {
const ModelVolume* shared_volume = iter->second.second; const ModelVolume* shared_volume = iter->second.second;
bool same_texture_paint = true;
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS) && same_texture_paint; ++slot)
same_texture_paint = shared_volume->texture_displacement_facet(slot).equals(volume->texture_displacement_facet(slot));
if ((shared_volume->supported_facets.equals(volume->supported_facets)) if ((shared_volume->supported_facets.equals(volume->supported_facets))
&& (shared_volume->seam_facets.equals(volume->seam_facets)) && (shared_volume->seam_facets.equals(volume->seam_facets))
&& (shared_volume->mmu_segmentation_facets.equals(volume->mmu_segmentation_facets)) && (shared_volume->mmu_segmentation_facets.equals(volume->mmu_segmentation_facets))
&& (shared_volume->fuzzy_skin_facets.equals(volume->fuzzy_skin_facets))) && (shared_volume->fuzzy_skin_facets.equals(volume->fuzzy_skin_facets))
&& same_texture_paint)
{ {
auto data = iter->second.first; auto data = iter->second.first;
const_cast<_BBS_3MF_Exporter *>(this)->m_volume_paths.insert({volume, {data->sub_path, data->volumes_objectID.find(iter->second.second)->second}}); const_cast<_BBS_3MF_Exporter *>(this)->m_volume_paths.insert({volume, {data->sub_path, data->volumes_objectID.find(iter->second.second)->second}});
@@ -7651,6 +7764,20 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
output_buffer += "\""; output_buffer += "\"";
} }
// One attribute per texture displacement layer slot. Older readers ignore attributes they do
// not know, so a project written here still opens in a build without the feature - it just
// loses the paint, which is also all it could have done with it.
for (int slot = 0; slot < TEXTURE_DISPLACEMENT_MAX_LAYERS; ++slot) {
const std::string texture_paint = volume->texture_displacement_facet(slot).get_triangle_as_string(i);
if (texture_paint.empty())
continue;
output_buffer += " ";
output_buffer += TEXTURE_DISPLACEMENT_ATTRS[slot];
output_buffer += "=\"";
output_buffer += texture_paint;
output_buffer += "\"";
}
// BBS // BBS
if (i < its.properties.size()) { if (i < its.properties.size()) {
std::string prop_str = its.properties[i].to_string(); std::string prop_str = its.properties[i].to_string();
@@ -8100,6 +8227,41 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return file_path; return file_path;
} }
// Writes a volume's texture displacement stack into the archive and points the volume's metadata at it.
// The layer settings go in as JSON and each layer's texture as the image file it was loaded from, both
// referenced by path - the same split EmbossShape makes for its SVG. The image deliberately stays out of
// the XML: it is binary and routinely megabytes, and base64 in an attribute would bloat the one file
// every reader has to parse just to list the objects.
static void add_texture_displacement(std::stringstream &stream, const ModelVolume &volume, mz_zip_archive &archive,
const std::string &id)
{
if (volume.texture_displacement_layers.empty())
return;
// A copy, because path_in_3mf is only meaningful inside the archive being written and the volume
// being exported is const (and may be saved again, elsewhere, with different paths).
std::vector<TextureDisplacementLayer> layers = volume.texture_displacement_layers;
for (TextureDisplacementLayer &layer : layers) {
layer.path_in_3mf.clear();
if (!layer.image_data || layer.image_data->empty())
continue;
// Always .png: the importer converts whatever the user picked (jpg, bmp, ...) to an 8-bit
// greyscale PNG, so image_data is PNG whatever layer.path happens to be named.
const std::string path = std::string(TEXTURE_DISPLACEMENT_DIR) + id + "_" + std::to_string(layer.slot) + ".png";
// No deflate: a PNG is already compressed, so a second pass only costs time.
if (mz_zip_writer_add_mem(&archive, path.c_str(), layer.image_data->data(), layer.image_data->size(),
MZ_NO_COMPRESSION))
layer.path_in_3mf = path;
}
const std::string json = texture_displacement_layers_to_json(layers, volume.texture_displacement_options);
const std::string json_path = std::string(TEXTURE_DISPLACEMENT_DIR) + id + ".json";
if (!mz_zip_writer_add_mem(&archive, json_path.c_str(), json.data(), json.size(), MZ_DEFAULT_COMPRESSION))
return;
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << TEXTURE_DISPLACEMENT_KEY << "\" "
<< VALUE_ATTR << "=\"" << xml_escape(json_path) << "\"/>\n";
}
bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config, int export_plate_idx, bool save_gcode, bool use_loaded_id) bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config, int export_plate_idx, bool save_gcode, bool use_loaded_id)
{ {
std::stringstream stream; std::stringstream stream;
@@ -8212,6 +8374,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
to_xml(stream, *es, *volume, archive, m_fullpath_sources); to_xml(stream, *es, *volume, archive, m_fullpath_sources);
} }
add_texture_displacement(stream, *volume, archive, std::to_string(volume->id().id));
if (const std::optional<TextConfiguration> &tc = volume->text_configuration; if (const std::optional<TextConfiguration> &tc = volume->text_configuration;
tc.has_value()) tc.has_value())
TextConfigurationSerialization::to_xml(stream, *tc); TextConfigurationSerialization::to_xml(stream, *tc);
+15 -2
View File
@@ -1033,6 +1033,15 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
return buf; return buf;
} }
// FanMover consumes these markers (and drops them) to avoid splitting a G1 inside custom gcode.
// Only written when FanMover runs, the same gate as its construction in GCode::process_layers.
static std::string wrap_custom_gcode_for_fan_mover(const FullPrintConfig &config, const std::string &gcode)
{
if (config.fan_speedup_time.value == 0 && config.fan_kickstart.value <= 0)
return gcode;
return ";_FAN_MOVER_SKIP_START\n" + gcode + ";_FAN_MOVER_SKIP_END\n";
}
std::string WipeTowerIntegration::append_tcr(GCode& gcodegen, const WipeTower::ToolChangeResult& tcr, int new_filament_id, double z) const std::string WipeTowerIntegration::append_tcr(GCode& gcodegen, const WipeTower::ToolChangeResult& tcr, int new_filament_id, double z) const
{ {
if (new_filament_id != -1 && new_filament_id != tcr.new_tool) if (new_filament_id != -1 && new_filament_id != tcr.new_tool)
@@ -1384,6 +1393,9 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
toolchange_gcode_str = gcodegen.placeholder_parser_process("change_filament_gcode", change_filament_gcode, new_filament_id, &config); toolchange_gcode_str = gcodegen.placeholder_parser_process("change_filament_gcode", change_filament_gcode, new_filament_id, &config);
check_add_eol(toolchange_gcode_str); check_add_eol(toolchange_gcode_str);
// Type1 wipe tower path (every BBL printer): bracket the same way set_extruder() does,
// so FanMover doesn't split a G1 inside this custom gcode either.
toolchange_gcode_str = wrap_custom_gcode_for_fan_mover(gcodegen.config(), toolchange_gcode_str);
//BBS //BBS
{ {
@@ -4250,7 +4262,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
config.set_key_value("nozzle_diameter_at_nozzle_id", new ConfigOptionFloats(get_nozzle_diameters_by_nozzle_id(group_result.get()))); config.set_key_value("nozzle_diameter_at_nozzle_id", new ConfigOptionFloats(get_nozzle_diameters_by_nozzle_id(group_result.get())));
config.set_key_value("nozzle_volume_types", new ConfigOptionStrings(get_nozzle_volume_types_by_nozzle_id(group_result.get()))); config.set_key_value("nozzle_volume_types", new ConfigOptionStrings(get_nozzle_volume_types_by_nozzle_id(group_result.get())));
config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index));
std::string filament_start_gcode = this->placeholder_parser_process("filament_start_gcode", print.config().filament_start_gcode.values.at(initial_non_support_extruder_id), initial_non_support_extruder_id,&config); std::string filament_start_gcode = this->placeholder_parser_process("filament_start_gcode", print.config().filament_start_gcode.get_at(initial_non_support_extruder_id), initial_non_support_extruder_id,&config);
file.writeln(filament_start_gcode); file.writeln(filament_start_gcode);
// Mark the first filament used in print. Multi-nozzle printers (H2C) get ";VT%d H%d" where // Mark the first filament used in print. Multi-nozzle printers (H2C) get ";VT%d H%d" where
// H = dynamic ? nozzle_id : -1; existing single-nozzle printers keep the bare ";VT%d" so their // H = dynamic ? nozzle_id : -1; existing single-nozzle printers keep the bare ";VT%d" so their
@@ -10895,7 +10907,8 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
toolchange_gcode_parsed = placeholder_parser_process("change_filament_gcode", change_filament_gcode, new_filament_id, &dyn_config); toolchange_gcode_parsed = placeholder_parser_process("change_filament_gcode", change_filament_gcode, new_filament_id, &dyn_config);
check_add_eol(toolchange_gcode_parsed); check_add_eol(toolchange_gcode_parsed);
gcode += toolchange_gcode_parsed; // toolchange_gcode_parsed is still read below (get_last_z_from_gcode, custom_gcode_changes_tool).
gcode += wrap_custom_gcode_for_fan_mover(m_config, toolchange_gcode_parsed);
//BBS //BBS
{ {
+5
View File
@@ -445,6 +445,11 @@ void FanMover::_process_gcode_line(GCodeReader& reader, const GCodeReader::GCode
std::string extrusion_string = line.raw().substr(6, line.raw().size() - 6); std::string extrusion_string = line.raw().substr(6, line.raw().size() - 6);
current_role = ExtrusionEntity::string_to_role(extrusion_string); current_role = ExtrusionEntity::string_to_role(extrusion_string);
} }
if (line.raw().rfind(";_FAN_MOVER_SKIP_", 0) == 0) {
// Internal markers: read here, then dropped from the output.
m_is_custom_gcode = line.raw().rfind(";_FAN_MOVER_SKIP_START", 0) == 0;
time = -1;
}
if (line.raw().size() > 16) { if (line.raw().size() > 16) {
if (line.raw().rfind("; custom gcode", 0) != std::string::npos) { if (line.raw().rfind("; custom gcode", 0) != std::string::npos) {
if (line.raw().rfind("; custom gcode end", 0) != std::string::npos) if (line.raw().rfind("; custom gcode end", 0) != std::string::npos)
+4
View File
@@ -3598,6 +3598,10 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config)
if (has_scarf_joint_seam != nullptr) if (has_scarf_joint_seam != nullptr)
m_detect_layer_based_on_tag = m_detect_layer_based_on_tag || has_scarf_joint_seam->value; m_detect_layer_based_on_tag = m_detect_layer_based_on_tag || has_scarf_joint_seam->value;
const ConfigOptionBool* manual_filament_change = config.option<ConfigOptionBool>("manual_filament_change");
if (manual_filament_change != nullptr)
m_manual_filament_change = manual_filament_change->value;
const ConfigOptionEnumGeneric *bed_type = config.option<ConfigOptionEnumGeneric>("curr_bed_type"); const ConfigOptionEnumGeneric *bed_type = config.option<ConfigOptionEnumGeneric>("curr_bed_type");
if (bed_type != nullptr) if (bed_type != nullptr)
m_result.bed_type = (BedType)bed_type->value; m_result.bed_type = (BedType)bed_type->value;
+1 -1
View File
@@ -1228,7 +1228,7 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
// m_result.nozzle_group_result, which is handed over only after the stream for the // m_result.nozzle_group_result, which is handed over only after the stream for the
// pre-heat injector's second pass and gates the richer change-time model. // pre-heat injector's second pass and gates the richer change-time model.
std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase> m_nozzle_group_result; std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase> m_nozzle_group_result;
bool m_manual_filament_change; bool m_manual_filament_change{ false };
//BBS: x, y offset for gcode generated //BBS: x, y offset for gcode generated
double m_x_offset{ 0 }; double m_x_offset{ 0 };
-349
View File
@@ -1,349 +0,0 @@
#include "JumpPointSearch.hpp"
#include "BoundingBox.hpp"
#include "ExPolygon.hpp"
#include "Point.hpp"
#include "libslic3r/AStar.hpp"
#include "libslic3r/KDTreeIndirect.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/Polyline.hpp"
#include "libslic3r/libslic3r.h"
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <iterator>
#include <limits>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include <oneapi/tbb/scalable_allocator.h>
//#define DEBUG_FILES
#ifdef DEBUG_FILES
#include "libslic3r/SVG.hpp"
#endif
namespace Slic3r {
// execute fn for each pixel on the line. If fn returns false, terminate the iteration
template<typename PointFn> void dda(coord_t x0, coord_t y0, coord_t x1, coord_t y1, const PointFn &fn)
{
coord_t dx = abs(x1 - x0);
coord_t dy = abs(y1 - y0);
coord_t x = x0;
coord_t y = y0;
coord_t n = 1 + dx + dy;
coord_t x_inc = (x1 > x0) ? 1 : -1;
coord_t y_inc = (y1 > y0) ? 1 : -1;
coord_t error = dx - dy;
dx *= 2;
dy *= 2;
for (; n > 0; --n) {
if (!fn(x, y)) return;
if (error > 0) {
x += x_inc;
error -= dy;
} else {
y += y_inc;
error += dx;
}
}
}
// will draw the line twice, second time with and offset of 1 in the direction of normal
// may call the fn on the same coordiantes multiple times!
template<typename PointFn> void double_dda_with_offset(coord_t x0, coord_t y0, coord_t x1, coord_t y1, const PointFn &fn)
{
Vec2d normal = Point{y1 - y0, x1 - x0}.cast<double>().normalized();
normal.x() = ceil(normal.x());
normal.y() = ceil(normal.y());
Point start_offset = Point(x0, y0) + (normal).cast<coord_t>();
Point end_offset = Point(x1, y1) + (normal).cast<coord_t>();
dda(x0, y0, x1, y1, fn);
dda(start_offset.x(), start_offset.y(), end_offset.x(), end_offset.y(), fn);
}
template<typename CellPositionType, typename CellQueryFn> class JPSTracer
{
public:
// Use incoming_dir [0,0] for starting points, so that all directions are checked from that point
struct Node
{
CellPositionType position;
CellPositionType incoming_dir;
};
JPSTracer(CellPositionType target, CellQueryFn is_passable) : target(target), is_passable(is_passable) {}
private:
CellPositionType target;
CellQueryFn is_passable; // should return boolean whether the cell is passable or not
CellPositionType find_jump_point(CellPositionType start, CellPositionType forward_dir) const
{
CellPositionType next = start + forward_dir;
while (next != target && is_passable(next) && !(is_jump_point(next, forward_dir))) { next = next + forward_dir; }
if (is_passable(next)) {
return next;
} else {
return start;
}
}
bool is_jump_point(CellPositionType pos, CellPositionType forward_dir) const
{
if (abs(forward_dir.x()) + abs(forward_dir.y()) == 2) {
// diagonal
CellPositionType horizontal_check_dir = CellPositionType{forward_dir.x(), 0};
CellPositionType vertical_check_dir = CellPositionType{0, forward_dir.y()};
if (!is_passable(pos - horizontal_check_dir) && is_passable(pos + forward_dir - 2 * horizontal_check_dir)) { return true; }
if (!is_passable(pos - vertical_check_dir) && is_passable(pos + forward_dir - 2 * vertical_check_dir)) { return true; }
if (find_jump_point(pos, horizontal_check_dir) != pos) { return true; }
if (find_jump_point(pos, vertical_check_dir) != pos) { return true; }
return false;
} else { // horizontal or vertical
CellPositionType side_dir = CellPositionType(forward_dir.y(), forward_dir.x());
if (!is_passable(pos + side_dir) && is_passable(pos + forward_dir + side_dir)) { return true; }
if (!is_passable(pos - side_dir) && is_passable(pos + forward_dir - side_dir)) { return true; }
return false;
}
}
public:
template<class Fn> void foreach_reachable(const Node &from, Fn &&fn) const
{
const CellPositionType & pos = from.position;
const CellPositionType & forward_dir = from.incoming_dir;
std::vector<CellPositionType> dirs_to_check{};
if (abs(forward_dir.x()) + abs(forward_dir.y()) == 0) { // special case for starting point
dirs_to_check = all_directions;
} else if (abs(forward_dir.x()) + abs(forward_dir.y()) == 2) {
// diagonal
CellPositionType horizontal_check_dir = CellPositionType{forward_dir.x(), 0};
CellPositionType vertical_check_dir = CellPositionType{0, forward_dir.y()};
if (!is_passable(pos - horizontal_check_dir) && is_passable(pos + forward_dir - 2 * horizontal_check_dir)) {
dirs_to_check.push_back(forward_dir - 2 * horizontal_check_dir);
}
if (!is_passable(pos - vertical_check_dir) && is_passable(pos + forward_dir - 2 * vertical_check_dir)) {
dirs_to_check.push_back(forward_dir - 2 * vertical_check_dir);
}
dirs_to_check.push_back(horizontal_check_dir);
dirs_to_check.push_back(vertical_check_dir);
dirs_to_check.push_back(forward_dir);
} else { // horizontal or vertical
CellPositionType side_dir = CellPositionType(forward_dir.y(), forward_dir.x());
if (!is_passable(pos + side_dir) && is_passable(pos + forward_dir + side_dir)) { dirs_to_check.push_back(forward_dir + side_dir); }
if (!is_passable(pos - side_dir) && is_passable(pos + forward_dir - side_dir)) { dirs_to_check.push_back(forward_dir - side_dir); }
dirs_to_check.push_back(forward_dir);
}
for (const CellPositionType &dir : dirs_to_check) {
CellPositionType jp = find_jump_point(pos, dir);
if (jp != pos) fn(Node{jp, dir});
}
}
float distance(Node a, Node b) const { return (a.position - b.position).template cast<double>().norm(); }
float goal_heuristic(Node n) const { return n.position == target ? -1.f : (target - n.position).template cast<double>().norm(); }
size_t unique_id(Node n) const { return (static_cast<size_t>(uint16_t(n.position.x())) << 16) + static_cast<size_t>(uint16_t(n.position.y())); }
const std::vector<CellPositionType> all_directions{{1, 0}, {1, 1}, {0, 1}, {-1, 1}, {-1, 0}, {-1, -1}, {0, -1}, {1, -1}};
};
void JPSPathFinder::clear()
{
inpassable.clear();
max_search_box.max = Pixel(std::numeric_limits<coord_t>::min(), std::numeric_limits<coord_t>::min());
max_search_box.min = Pixel(std::numeric_limits<coord_t>::max(), std::numeric_limits<coord_t>::max());
add_obstacles(bed_shape);
}
void JPSPathFinder::add_obstacles(const Lines &obstacles)
{
auto store_obstacle = [&](coord_t x, coord_t y) {
max_search_box.max.x() = std::max(max_search_box.max.x(), x);
max_search_box.max.y() = std::max(max_search_box.max.y(), y);
max_search_box.min.x() = std::min(max_search_box.min.x(), x);
max_search_box.min.y() = std::min(max_search_box.min.y(), y);
inpassable.insert(Pixel{x, y});
return true;
};
for (const Line &l : obstacles) {
Pixel start = pixelize(l.a);
Pixel end = pixelize(l.b);
double_dda_with_offset(start.x(), start.y(), end.x(), end.y(), store_obstacle);
}
}
Polyline JPSPathFinder::find_path(const Point &p0, const Point &p1)
{
Pixel start = pixelize(p0);
Pixel end = pixelize(p1);
if (inpassable.empty() || (start - end).cast<float>().norm() < 3.0) { return Polyline{p0, p1}; }
if (inpassable.find(start) != inpassable.end()) {
dda(start.x(), start.y(), end.x(), end.y(), [&](coord_t x, coord_t y) {
if (inpassable.find(Pixel(x, y)) == inpassable.end() || start == end) { // new start not found yet, and xy passable
start = Pixel(x, y);
return false;
}
return true;
});
}
if (inpassable.find(end) != inpassable.end()) {
dda(end.x(), end.y(), start.x(), start.y(), [&](coord_t x, coord_t y) {
if (inpassable.find(Pixel(x, y)) == inpassable.end() || start == end) { // new start not found yet, and xy passable
end = Pixel(x, y);
return false;
}
return true;
});
}
BoundingBox search_box = max_search_box;
search_box.max -= Pixel(1, 1);
search_box.min += Pixel(1, 1);
BoundingBox bounding_square(Points{start, end});
bounding_square.max += Pixel(5, 5);
bounding_square.min -= Pixel(5, 5);
coord_t bounding_square_size = 2 * std::max(bounding_square.size().x(), bounding_square.size().y());
bounding_square.max.x() += (bounding_square_size - bounding_square.size().x()) / 2;
bounding_square.min.x() -= (bounding_square_size - bounding_square.size().x()) / 2;
bounding_square.max.y() += (bounding_square_size - bounding_square.size().y()) / 2;
bounding_square.min.y() -= (bounding_square_size - bounding_square.size().y()) / 2;
// Intersection - limit the search box to a square area around the start and end, to fasten the path searching
search_box.max = search_box.max.cwiseMin(bounding_square.max);
search_box.min = search_box.min.cwiseMax(bounding_square.min);
auto cell_query = [&](Pixel pixel) { return search_box.contains(pixel) && (pixel == start || pixel == end || inpassable.find(pixel) == inpassable.end()); };
JPSTracer<Pixel, decltype(cell_query)> tracer(end, cell_query);
using QNode = astar::QNode<JPSTracer<Pixel, decltype(cell_query)>>;
std::unordered_map<size_t, QNode> astar_cache{};
std::vector<Pixel, PointsAllocator<Pixel>> out_path;
std::vector<decltype(tracer)::Node> out_nodes;
if (!astar::search_route(tracer, {start, {0, 0}}, std::back_inserter(out_nodes), astar_cache)) {
// path not found - just reconstruct the best path from astar cache.
// Note that astar_cache is NOT empty - at least the starting point should always be there
auto coordiante_func = [&astar_cache](size_t idx, size_t dim) { return float(astar_cache[idx].node.position[dim]); };
std::vector<size_t> keys;
keys.reserve(astar_cache.size());
for (const auto &pair : astar_cache) { keys.push_back(pair.first); }
KDTreeIndirect<2, float, decltype(coordiante_func)> kd_tree(coordiante_func, keys);
size_t closest_qnode = find_closest_point(kd_tree, end.cast<float>());
out_path.push_back(end);
while (closest_qnode != astar::Unassigned) {
out_path.push_back(astar_cache[closest_qnode].node.position);
closest_qnode = astar_cache[closest_qnode].parent;
}
} else {
for (const auto &node : out_nodes) { out_path.push_back(node.position); }
out_path.push_back(start);
}
#ifdef DEBUG_FILES
auto scaled_points = [](const Points &ps) {
Points r;
for (const Point &p : ps) { r.push_back(Point::new_scale(p.x(), p.y())); }
return r;
};
auto scaled_point = [](const Point &p) { return Point::new_scale(p.x(), p.y()); };
::Slic3r::SVG svg(debug_out_path(("path_jps" + std::to_string(print_z) + "_" + std::to_string(rand() % 1000)).c_str()).c_str(),
BoundingBox(scaled_point(search_box.min), scaled_point(search_box.max)));
for (const auto &p : inpassable) { svg.draw(scaled_point(p), "black", scale_(0.4)); }
for (const auto &qn : astar_cache) { svg.draw(scaled_point(qn.second.node.position), "blue", scale_(0.3)); }
svg.draw(Polyline(scaled_points(out_path)), "yellow", scale_(0.25));
svg.draw(scaled_point(end), "purple", scale_(0.4));
svg.draw(scaled_point(start), "green", scale_(0.4));
#endif
std::vector<Pixel, PointsAllocator<Pixel>> tmp_path;
tmp_path.reserve(out_path.size());
// Some path found, reverse and remove points that do not change direction
std::reverse(out_path.begin(), out_path.end());
{
tmp_path.push_back(out_path.front()); // first point
for (size_t i = 1; i < out_path.size() - 1; i++) {
if ((out_path[i] - out_path[i - 1]).cast<float>().normalized() != (out_path[i + 1] - out_path[i]).cast<float>().normalized()) { tmp_path.push_back(out_path[i]); }
}
tmp_path.push_back(out_path.back()); // last_point
out_path = tmp_path;
}
#ifdef DEBUG_FILES
svg.draw(Polyline(scaled_points(out_path)), "orange", scale_(0.20));
#endif
tmp_path.clear();
// remove redundant jump points - there are points that change direction but are not needed - this inefficiency arises from the
// usage of grid search The removal alg tries to find the longest Px Px+k path without obstacles. If Px Px+k+1 is blocked, it will
// insert the Px+k point to result and continue search from Px+k
{
tmp_path.push_back(out_path.front()); // first point
size_t index_of_last_stored_point = 0;
for (size_t i = 1; i < out_path.size(); i++) {
if (i - index_of_last_stored_point < 2) continue;
bool passable = true;
auto store_obstacle = [&](coord_t x, coord_t y) {
if (Pixel(x, y) != start && Pixel(x, y) != end && inpassable.find(Pixel(x, y)) != inpassable.end()) {
passable = false;
return false;
}
return true;
};
dda(tmp_path.back().x(), tmp_path.back().y(), out_path[i].x(), out_path[i].y(), store_obstacle);
if (!passable) {
tmp_path.push_back(out_path[i - 1]);
index_of_last_stored_point = i - 1;
}
}
tmp_path.push_back(out_path.back()); // last_point
out_path = tmp_path;
}
#ifdef DEBUG_FILES
svg.draw(Polyline(scaled_points(out_path)), "red", scale_(0.15));
svg.Close();
#endif
// before returing the path, transform it from pixels back to points.
// Also replace the first and last pixel by input points so that result path patches input params exactly.
for (Pixel &p : out_path) { p = unpixelize(p); }
out_path.front() = p0;
out_path.back() = p1;
return Polyline(out_path);
}
} // namespace Slic3r
-39
View File
@@ -1,39 +0,0 @@
#pragma once
#include "Line.hpp"
#ifndef SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_
#define SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_
#include "BoundingBox.hpp"
#include "Polygon.hpp"
#include "libslic3r/Layer.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polyline.hpp"
#include "libslic3r/libslic3r.h"
#include <unordered_map>
#include <unordered_set>
namespace Slic3r {
class JPSPathFinder
{
using Pixel = Point;
std::unordered_set<Pixel, PointHash> inpassable;
coordf_t print_z;
BoundingBox max_search_box;
Lines bed_shape;
const coord_t resolution = scaled(1.5);
Pixel pixelize(const Point &p) { return p / resolution; }
Point unpixelize(const Pixel &p) { return p * resolution; }
public:
JPSPathFinder() = default;
void init_bed_shape(const Points &bed_shape) { this->bed_shape = (to_lines(Polygon{bed_shape})); };
void clear();
void add_obstacles(const Lines &obstacles);
Polyline find_path(const Point &start, const Point &end);
};
} // namespace Slic3r
#endif /* SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_ */
-88
View File
@@ -29,8 +29,6 @@
#include <CGAL/Polygon_mesh_processing/triangulate_hole.h> #include <CGAL/Polygon_mesh_processing/triangulate_hole.h>
#include <CGAL/Polygon_mesh_processing/stitch_borders.h> #include <CGAL/Polygon_mesh_processing/stitch_borders.h>
#include <CGAL/Polygon_mesh_processing/polygon_soup_to_polygon_mesh.h> #include <CGAL/Polygon_mesh_processing/polygon_soup_to_polygon_mesh.h>
#include <CGAL/Polygon_mesh_processing/repair_degeneracies.h>
#include <CGAL/Polygon_mesh_processing/manifoldness.h>
#include <cstdint> #include <cstdint>
#include <map> #include <map>
#include <algorithm> #include <algorithm>
@@ -50,7 +48,6 @@
#include <CGAL/Polygon_mesh_processing/orient_polygon_soup.h> #include <CGAL/Polygon_mesh_processing/orient_polygon_soup.h>
#include <CGAL/Polygon_mesh_processing/repair.h> #include <CGAL/Polygon_mesh_processing/repair.h>
#include <CGAL/Polygon_mesh_processing/remesh.h> #include <CGAL/Polygon_mesh_processing/remesh.h>
#include <CGAL/Polygon_mesh_processing/repair_polygon_soup.h>
#include <CGAL/Polygon_mesh_processing/orientation.h> #include <CGAL/Polygon_mesh_processing/orientation.h>
// BBS: for segment // BBS: for segment
#include <CGAL/mesh_segmentation.h> #include <CGAL/mesh_segmentation.h>
@@ -658,91 +655,6 @@ bool empty(const CGALMesh &mesh)
return mesh.m.is_empty(); return mesh.m.is_empty();
} }
bool repair(TriangleMesh& mesh, RepairedMeshErrors* repaired_errors, std::string* error)
{
namespace PMP = CGAL::Polygon_mesh_processing;
if (mesh.empty())
return true;
try {
// 1) Convert to polygon soup
std::vector<_EpicMesh::Point> points;
std::vector<std::vector<std::size_t>> polygons;
points.reserve(mesh.its.vertices.size());
polygons.reserve(mesh.its.indices.size());
for (const auto& v : mesh.its.vertices)
points.emplace_back(v.x(), v.y(), v.z());
for (const auto& f : mesh.its.indices)
polygons.push_back({size_t(f[0]), size_t(f[1]), size_t(f[2])});
// 2) Aggressive soup cleanup
PMP::repair_polygon_soup(points, polygons);
// 3) Convert soup → mesh
_EpicMesh cgal_mesh;
PMP::polygon_soup_to_polygon_mesh(points, polygons, cgal_mesh);
// 4) Remove degenerate geometry
PMP::remove_degenerate_faces(cgal_mesh);
PMP::remove_isolated_vertices(cgal_mesh);
// 5) Fix remaining non-manifold vertices
PMP::duplicate_non_manifold_vertices(cgal_mesh);
// 6) Boolean union (keeps only outer shell)
_EpicMesh tmp;
if (PMP::corefine_and_compute_union(cgal_mesh, cgal_mesh, tmp)) {
cgal_mesh = std::move(tmp);
}
// If it fails, continue anyway with previous mesh
// 7) Fill holes
if (!CGAL::is_closed(cgal_mesh)) {
using halfedge_descriptor = boost::graph_traits<_EpicMesh>::halfedge_descriptor;
std::vector<halfedge_descriptor> borders;
CGAL::extract_boundary_cycles(cgal_mesh, std::back_inserter(borders));
for (halfedge_descriptor h : borders) {
PMP::triangulate_and_refine_hole(cgal_mesh, h);
}
}
// 8) Final validity check
if (!CGAL::is_closed(cgal_mesh)) {
if (error)
*error = "Repair failed: mesh still open after hole filling.";
return false;
}
// 9) Ensure outward orientation
if (!PMP::does_bound_a_volume(cgal_mesh))
PMP::orient_to_bound_a_volume(cgal_mesh);
// 10) Convert back
indexed_triangle_set its = cgal_to_indexed_triangle_set(cgal_mesh);
RepairedMeshErrors errs{};
errs.facets_removed = 0;
errs.edges_fixed = 0;
mesh = TriangleMesh(std::move(its), errs);
if (repaired_errors)
*repaired_errors = errs;
return true;
} catch (const std::exception& e) {
if (error)
*error = e.what();
return false;
}
}
CGALMeshPtr clone(const CGALMesh &m) CGALMeshPtr clone(const CGALMesh &m)
{ {
return CGALMeshPtr{new CGALMesh{m}}; return CGALMeshPtr{new CGALMesh{m}};
-3
View File
@@ -77,9 +77,6 @@ TriangleMesh merge(std::vector<TriangleMesh> meshes);
bool does_bound_a_volume(const CGALMesh &mesh); bool does_bound_a_volume(const CGALMesh &mesh);
bool empty(const CGALMesh &mesh); bool empty(const CGALMesh &mesh);
// Repair a mesh using CGAL. Returns true on success. Optionally returns a summary of repairs and an error string.
bool repair(TriangleMesh &mesh, RepairedMeshErrors *repaired_errors = nullptr, std::string *error = nullptr);
// Real UV unwrap of an open mesh patch via CGAL's LSCM (Least Squares Conformal Maps) surface // Real UV unwrap of an open mesh patch via CGAL's LSCM (Least Squares Conformal Maps) surface
// parameterization. Returns one UV coordinate per input vertex (same indexing as `mesh.vertices`), // parameterization. Returns one UV coordinate per input vertex (same indexing as `mesh.vertices`),
// or nullopt if `mesh` isn't a single topological disk -- LSCM needs exactly one connected // or nullopt if `mesh` isn't a single topological disk -- LSCM needs exactly one connected
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <cstddef>
#include <functional>
#include <string>
namespace Slic3r {
class TriangleMesh;
struct MeshRepairStats
{
std::size_t welded_vertices = 0;
std::size_t removed_triangles = 0;
std::size_t reversed_shells = 0;
std::size_t filled_holes = 0;
std::size_t removed_shells = 0;
std::size_t self_intersections = 0;
// False when the input already was closed, consistently oriented and free of self-intersections.
bool modified = false;
};
// Returns false, leaving the mesh untouched, if no solid can be built. progress gets the percentage done and may
// throw to cancel. See docs/HLSD/mesh-repair.md.
bool repair_mesh(TriangleMesh &mesh, MeshRepairStats *stats = nullptr, std::string *error = nullptr,
const std::function<void(int)> &progress = {});
} // namespace Slic3r
+38 -21
View File
@@ -29,6 +29,7 @@
#include "TriangleMeshSlicer.hpp" #include "TriangleMeshSlicer.hpp"
#include "TriangleSelector.hpp" #include "TriangleSelector.hpp"
#include "MaterialType.hpp" #include "MaterialType.hpp"
#include "MeshRepair.hpp"
#include "Format/AMF.hpp" #include "Format/AMF.hpp"
#include "Format/svg.hpp" #include "Format/svg.hpp"
@@ -2946,6 +2947,18 @@ size_t ModelVolume::split(unsigned int max_extruders, bool remap_paint)
if (text_configuration.has_value()) if (text_configuration.has_value())
text_configuration.reset(); text_configuration.reset();
// Parts without a convex hull enclose no volume; drop them here, as deleting volumes later could delete this one.
std::vector<std::unique_ptr<ModelVolume>> parts;
for (TriangleMesh &mesh : meshes)
if (!mesh.empty()) {
std::unique_ptr<ModelVolume> part(new ModelVolume(object, *this, std::move(mesh)));
if (const std::shared_ptr<const TriangleMesh> &hull = part->get_convex_hull_shared_ptr();
hull != nullptr && !hull->its.vertices.empty() && !hull->its.indices.empty())
parts.emplace_back(std::move(part));
}
if (parts.empty())
return 1;
std::optional<TriangleSelector::SavedPainting> saved_painting = remap_paint ? save_painting() : std::optional<TriangleSelector::SavedPainting> saved_painting = remap_paint ? save_painting() :
std::optional<TriangleSelector::SavedPainting>{}; std::optional<TriangleSelector::SavedPainting>{};
@@ -2956,26 +2969,22 @@ size_t ModelVolume::split(unsigned int max_extruders, bool remap_paint)
unsigned int extruder_counter = 0; unsigned int extruder_counter = 0;
const Vec3d offset = this->get_offset(); const Vec3d offset = this->get_offset();
for (TriangleMesh &mesh : meshes) { for (std::unique_ptr<ModelVolume> &part : parts) {
if (mesh.empty())
// Repair may have removed unconnected triangles, thus emptying the mesh.
continue;
if (idx == 0) { if (idx == 0) {
this->set_mesh(std::move(mesh)); this->set_mesh(part->m_mesh);
this->calculate_convex_hull(); m_convex_hull = part->m_convex_hull;
this->invalidate_convex_hull_2d(); this->invalidate_convex_hull_2d();
// Assign a new unique ID, so that a new GLVolume will be generated. // Assign a new unique ID, so that a new GLVolume will be generated.
this->set_new_unique_id(); this->set_new_unique_id();
// reset the source to disable reload from disk
this->source = ModelVolume::Source();
// BBS: reset facet annotations // BBS: reset facet annotations
this->reset_extra_facets(); this->reset_extra_facets();
} }
else else
this->object->volumes.insert(this->object->volumes.begin() + (++ivolume), new ModelVolume(object, *this, std::move(mesh))); this->object->volumes.insert(this->object->volumes.begin() + (++ivolume), part.release());
// reset the source to disable reload from disk
this->object->volumes[ivolume]->source = ModelVolume::Source();
this->object->volumes[ivolume]->set_offset(Vec3d::Zero()); this->object->volumes[ivolume]->set_offset(Vec3d::Zero());
this->object->volumes[ivolume]->center_geometry_after_creation(); this->object->volumes[ivolume]->center_geometry_after_creation();
this->object->volumes[ivolume]->translate(offset); this->object->volumes[ivolume]->translate(offset);
@@ -2989,19 +2998,27 @@ size_t ModelVolume::split(unsigned int max_extruders, bool remap_paint)
++ idx; ++ idx;
} }
// discard volumes for which the convex hull was not generated or is degenerate return idx;
size_t i = 0;
while (i < this->object->volumes.size()) {
const std::shared_ptr<const TriangleMesh> &hull = this->object->volumes[i]->get_convex_hull_shared_ptr();
if (hull == nullptr || hull->its.vertices.empty() || hull->its.indices.empty()) {
this->object->delete_volume(i);
--idx;
--i;
}
++i;
} }
return idx; bool ModelVolume::repair(bool keep_painting, MeshRepairStats *stats_out, std::string *error, const std::function<void(int)> &progress)
{
TriangleMesh mesh = this->mesh();
MeshRepairStats stats;
const bool ok = repair_mesh(mesh, &stats, error, progress);
if (stats_out)
*stats_out = stats;
if (!ok || !stats.modified)
return ok;
const std::optional<TriangleSelector::SavedPainting> saved_painting = keep_painting ? save_painting() : std::nullopt;
this->set_mesh(std::move(mesh));
this->calculate_convex_hull();
this->invalidate_convex_hull_2d();
// Assign a new unique ID, so that a new GLVolume will be generated.
this->set_new_unique_id();
this->restore_painting(saved_painting);
return true;
} }
void ModelVolume::translate(const Vec3d& displacement) void ModelVolume::translate(const Vec3d& displacement)
+11 -7
View File
@@ -77,6 +77,7 @@ class Preset;
class BBLProject; class BBLProject;
class KeyStore; class KeyStore;
struct MeshRepairStats;
namespace UndoRedo { namespace UndoRedo {
class StackImpl; class StackImpl;
@@ -887,13 +888,13 @@ public:
// The triangular model. // The triangular model.
const TriangleMesh& mesh() const { return *m_mesh.get(); } const TriangleMesh& mesh() const { return *m_mesh.get(); }
std::shared_ptr<const TriangleMesh> mesh_ptr() const { return m_mesh; } std::shared_ptr<const TriangleMesh> mesh_ptr() const { return m_mesh; }
void set_mesh(const TriangleMesh &mesh) { m_mesh = std::make_shared<const TriangleMesh>(mesh); } void set_mesh(const TriangleMesh &mesh) { m_mesh = std::make_shared<const TriangleMesh>(mesh); m_is_splittable = -1; }
void set_mesh(TriangleMesh &&mesh) { m_mesh = std::make_shared<const TriangleMesh>(std::move(mesh)); } void set_mesh(TriangleMesh &&mesh) { m_mesh = std::make_shared<const TriangleMesh>(std::move(mesh)); m_is_splittable = -1; }
void set_mesh(const indexed_triangle_set &mesh) { m_mesh = std::make_shared<const TriangleMesh>(mesh); } void set_mesh(const indexed_triangle_set &mesh) { m_mesh = std::make_shared<const TriangleMesh>(mesh); m_is_splittable = -1; }
void set_mesh(indexed_triangle_set &&mesh) { m_mesh = std::make_shared<const TriangleMesh>(std::move(mesh)); } void set_mesh(indexed_triangle_set &&mesh) { m_mesh = std::make_shared<const TriangleMesh>(std::move(mesh)); m_is_splittable = -1; }
void set_mesh(std::shared_ptr<const TriangleMesh> &mesh) { m_mesh = mesh; } void set_mesh(std::shared_ptr<const TriangleMesh> &mesh) { m_mesh = mesh; m_is_splittable = -1; }
void set_mesh(std::unique_ptr<const TriangleMesh> &&mesh) { m_mesh = std::move(mesh); } void set_mesh(std::unique_ptr<const TriangleMesh> &&mesh) { m_mesh = std::move(mesh); m_is_splittable = -1; }
void reset_mesh() { m_mesh = std::make_shared<const TriangleMesh>(); } void reset_mesh() { m_mesh = std::make_shared<const TriangleMesh>(); m_is_splittable = -1; }
const std::shared_ptr<const TriangleMesh>& get_mesh_shared_ptr() const { return m_mesh; } const std::shared_ptr<const TriangleMesh>& get_mesh_shared_ptr() const { return m_mesh; }
// Configuration parameters specific to an object model geometry or a modifier volume, // Configuration parameters specific to an object model geometry or a modifier volume,
// overriding the global Slic3r settings and the ModelObject settings. // overriding the global Slic3r settings and the ModelObject settings.
@@ -1043,6 +1044,9 @@ public:
// Return the number of volumes created from this one. // Return the number of volumes created from this one.
// This is useful to assign different materials to different volumes of an object. // This is useful to assign different materials to different volumes of an object.
size_t split(unsigned int max_extruders, bool remap_paint); size_t split(unsigned int max_extruders, bool remap_paint);
// Replaces the mesh by its repair_mesh() result if that changed it. False if no solid can be built.
bool repair(bool keep_painting, MeshRepairStats *stats = nullptr, std::string *error = nullptr,
const std::function<void(int)> &progress = {});
void translate(double x, double y, double z) { translate(Vec3d(x, y, z)); } void translate(double x, double y, double z) { translate(Vec3d(x, y, z)); }
void translate(const Vec3d& displacement); void translate(const Vec3d& displacement);
void scale(const Vec3d& scaling_factors); void scale(const Vec3d& scaling_factors);
+33 -4
View File
@@ -2164,6 +2164,16 @@ int PresetCollection::get_differed_values_to_update(Preset& preset, std::map<std
DynamicPrintConfig temp_config; DynamicPrintConfig temp_config;
std::vector<std::string> dirty_options = preset.config.diff(parent_preset->config); std::vector<std::string> dirty_options = preset.config.diff(parent_preset->config);
// Send the variant layout even when it matches the parent's, as the local save does: the loader
// sizes the per-variant values by it and would otherwise keep only the first one.
std::string extruder_id_name, extruder_variant_name;
std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2);
if (!extruder_id_name.empty())
dirty_options.emplace_back(extruder_id_name);
if (!extruder_variant_name.empty())
dirty_options.emplace_back(extruder_variant_name);
for (auto option: dirty_options) for (auto option: dirty_options)
{ {
ConfigOption *opt_src = preset.config.option(option); ConfigOption *opt_src = preset.config.option(option);
@@ -2628,15 +2638,34 @@ bool PresetCollection::load_user_preset(std::string name, std::map<std::string,
new_config = default_preset.config; new_config = default_preset.config;
} }
extend_default_config_length(cloud_config, false, {});
if (inherit_preset) {
std::string extruder_id_name, extruder_variant_name; std::string extruder_id_name, extruder_variant_name;
std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr; std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2); Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2);
new_config.update_diff_values_to_child_config(cloud_config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2); // Older uploads left out a variant layout equal to the parent's but still sent one value per
// parent variant. Read those values against the parent's layout instead of cutting them to one.
if (inherit_preset && !cloud_config.has(extruder_variant_name)) {
const auto *parent_variants = inherit_preset->config.option<ConfigOptionStrings>(extruder_variant_name);
const size_t variant_count = parent_variants ? parent_variants->size() : 0;
bool values_per_variant = false;
for (const std::string &key : cloud_config.keys()) {
const size_t stride = key_set2->count(key) ? 2 : key_set1->count(key) ? 1 : 0;
const auto *values = dynamic_cast<const ConfigOptionVectorBase *>(cloud_config.option(key));
if (stride > 0 && key != extruder_id_name && values && variant_count > 1 && values->size() == variant_count * stride) {
values_per_variant = true;
break;
} }
}
if (values_per_variant)
for (const std::string &key : {extruder_id_name, extruder_variant_name})
if (!key.empty() && !cloud_config.has(key) && inherit_preset->config.has(key))
cloud_config.set_key_value(key, inherit_preset->config.option(key)->clone());
}
extend_default_config_length(cloud_config, false, {});
if (inherit_preset)
new_config.update_diff_values_to_child_config(cloud_config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
else{ else{
new_config.apply(std::move(cloud_config)); new_config.apply(std::move(cloud_config));
extend_default_config_length(new_config, true, default_preset.config); extend_default_config_length(new_config, true, default_preset.config);
+9
View File
@@ -2991,6 +2991,15 @@ void PrintObject::clear_shared_object()
void PrintObject::copy_layers_from_shared_object() void PrintObject::copy_layers_from_shared_object()
{ {
if (m_shared_object) { if (m_shared_object) {
// Orca: Print::process() calls this once per slice for each sharing object, after clear_shared_object() has
// emptied any earlier alias, so the vectors hold only layers this object sliced itself before it started sharing
// (or nothing). Free them here, as clear_support_layers() / clear_layers() would; those skip deletion while
// m_shared_object is set. The belt-only data they also free is always empty here, because belt printers never
// share layers.
for (SupportLayer *layer : m_support_layers)
delete layer;
for (Layer *layer : m_layers)
delete layer;
m_layers.clear(); m_layers.clear();
m_support_layers.clear(); m_support_layers.clear();
+5 -4
View File
@@ -12866,10 +12866,11 @@ CLITransformConfigDef::CLITransformConfigDef()
//def->cli = "orient|o"; //def->cli = "orient|o";
def->set_default_value(new ConfigOptionInt(0)); def->set_default_value(new ConfigOptionInt(0));
/*def = this->add("repair", coBool); def = this->add("repair", coBool);
def->label = L("Repair"); def->label = L("Repair");
def->tooltip = L("Repair the model's meshes if it is non-manifold mesh."); def->tooltip = L("Repair the meshes of the models as Fix Model does: close holes and cracks, orient them outward and "
def->set_default_value(new ConfigOptionBool(false));*/ "remove self-intersections.");
def->set_default_value(new ConfigOptionBool(false));
def = this->add("rotate", coFloat); def = this->add("rotate", coFloat);
def->label = L("Rotate"); def->label = L("Rotate");
@@ -13085,7 +13086,7 @@ CLIMiscConfigDef::CLIMiscConfigDef()
def = this->add("allow_rotations", coBool); def = this->add("allow_rotations", coBool);
def->label = L("Allow rotation when arranging"); def->label = L("Allow rotation when arranging");
def->tooltip = L("If enabled, Arrange will allow rotation when placing objects."); def->tooltip = L("If enabled, Arrange will allow rotation when placing objects.");
def->set_default_value(new ConfigOptionBool(true)); def->set_default_value(new ConfigOptionBool(false));
def = this->add("align_to_y_axis", coBool); def = this->add("align_to_y_axis", coBool);
def->label = L("Align to Y axis when arranging"); def->label = L("Align to Y axis when arranging");
+3
View File
@@ -982,6 +982,9 @@ void PrintObject::generate_support_material(bool with_belt_brim)
this->_generate_support_material(); this->_generate_support_material();
m_print->throw_if_canceled(); m_print->throw_if_canceled();
} }
// Orca: the tree support collision/avoidance caches and support nodes are only used while this step runs
// (detect_overhangs() rebuilds them from scratch), so don't keep them resident until the next slice.
this->clear_tree_support_preview_cache();
// Belt brim rides here rather than in the brim step because its apron // Belt brim rides here rather than in the brim step because its apron
// prologue introduces print_z values below the object's first layer, and // prologue introduces print_z values below the object's first layer, and
// those must exist before ToolOrdering is built at psWipeTower - one step // those must exist before ToolOrdering is built at psWipeTower - one step
+1 -1
View File
@@ -1532,7 +1532,7 @@ void PrintObject::slice_volumes()
if (min_growth < 0.f || elfoot > 0.f) { if (min_growth < 0.f || elfoot > 0.f) {
// Apply the negative XY compensation. (the ones that is <0) // Apply the negative XY compensation. (the ones that is <0)
ExPolygons trimming; ExPolygons trimming;
static const float eps = float(scale_(m_config.slice_closing_radius.value) * 1.5); const float eps = float(scale_(m_config.slice_closing_radius.value) * 1.5);
if (elfoot > 0.f) { if (elfoot > 0.f) {
ExPolygons expolygons_to_compensate = offset_ex(layer->merged(eps), -eps); ExPolygons expolygons_to_compensate = offset_ex(layer->merged(eps), -eps);
lslices_elfoot_uncompensated[layer_id] = expolygons_to_compensate; lslices_elfoot_uncompensated[layer_id] = expolygons_to_compensate;
+13 -30
View File
@@ -1976,37 +1976,15 @@ coordf_t TreeSupport::get_radius(const SupportNode* node)
return node->radius; return node->radius;
} }
ExPolygons TreeSupport::get_avoidance(coordf_t radius, size_t obj_layer_nr) // Orca: drop_nodes() calls these several times per node per layer, so they return a reference into the
// TreeSupportData cache. The cache only ever inserts, so its entries never move while nodes drop in parallel.
const ExPolygons& TreeSupport::get_avoidance(coordf_t radius, size_t obj_layer_nr)
{ {
#if USE_SUPPORT_3D
if (m_model_volumes) {
bool on_build_plate = m_object_config->support_on_build_plate_only.value;
const Polygons& avoid_polys = m_model_volumes->getAvoidance(radius, obj_layer_nr, TreeSupport3D::TreeModelVolumes::AvoidanceType::FastSafe, on_build_plate, true);
ExPolygons expolys;
for (auto& poly : avoid_polys)
expolys.emplace_back(std::move(poly));
return expolys;
}
return ExPolygons();
#else
return m_ts_data->get_avoidance(radius, obj_layer_nr); return m_ts_data->get_avoidance(radius, obj_layer_nr);
#endif
} }
ExPolygons TreeSupport::get_collision(coordf_t radius, size_t layer_nr) const ExPolygons& TreeSupport::get_collision(coordf_t radius, size_t layer_nr)
{ {
#if USE_SUPPORT_3D
if (m_model_volumes) {
bool on_build_plate = m_object_config->support_on_build_plate_only.value;
const Polygons& collision_polys = m_model_volumes->getCollision(radius, layer_nr, true);
ExPolygons expolys;
for (auto& poly : collision_polys)
expolys.emplace_back(std::move(poly));
return expolys;
}
#else
return m_ts_data->get_collision(radius, layer_nr); return m_ts_data->get_collision(radius, layer_nr);
#endif
return ExPolygons();
} }
Polygons TreeSupport::get_collision_polys(coordf_t radius, size_t layer_nr) Polygons TreeSupport::get_collision_polys(coordf_t radius, size_t layer_nr)
{ {
@@ -2827,7 +2805,12 @@ void TreeSupport::draw_circles()
#endif // SUPPORT_TREE_DEBUG_TO_SVG #endif // SUPPORT_TREE_DEBUG_TO_SVG
SupportLayerPtrs& ts_layers = m_object->support_layers(); SupportLayerPtrs& ts_layers = m_object->support_layers();
auto iter = std::remove_if(ts_layers.begin(), ts_layers.end(), [](SupportLayer* ts_layer) { return ts_layer->height < EPSILON; }); // Orca: the vector owns its layers, so the dropped ones are deleted before they are erased.
// std::stable_partition (unlike std::remove_if) leaves exactly the dropped layers in the tail.
// Only heights below EPSILON are dropped; written as !(height < EPSILON), a NaN height is kept.
auto iter = std::stable_partition(ts_layers.begin(), ts_layers.end(), [](SupportLayer* ts_layer) { return !(ts_layer->height < EPSILON); });
for (auto it = iter; it != ts_layers.end(); ++it)
delete *it;
ts_layers.erase(iter, ts_layers.end()); ts_layers.erase(iter, ts_layers.end());
for (int layer_nr = 0; layer_nr < ts_layers.size(); layer_nr++) { for (int layer_nr = 0; layer_nr < ts_layers.size(); layer_nr++) {
ts_layers[layer_nr]->upper_layer = layer_nr != ts_layers.size() - 1 ? ts_layers[layer_nr + 1] : nullptr; ts_layers[layer_nr]->upper_layer = layer_nr != ts_layers.size() - 1 ? ts_layers[layer_nr + 1] : nullptr;
@@ -3071,7 +3054,7 @@ void TreeSupport::drop_nodes()
//Insert a completely new node and let both original nodes fade. //Insert a completely new node and let both original nodes fade.
Point next_position = (node.position + neighbours[0]) / 2; //Average position of the two nodes. Point next_position = (node.position + neighbours[0]) / 2; //Average position of the two nodes.
coordf_t next_radius = calc_radius(node.dist_mm_to_top+height_next); coordf_t next_radius = calc_radius(node.dist_mm_to_top+height_next);
auto avoid_layer = get_avoidance(next_radius, obj_layer_nr_next); const ExPolygons& avoid_layer = get_avoidance(next_radius, obj_layer_nr_next);
if (group_index == 0) if (group_index == 0)
{ {
//Avoid collisions. //Avoid collisions.
@@ -3275,7 +3258,7 @@ void TreeSupport::drop_nodes()
} }
#endif #endif
coordf_t next_radius = calc_radius(node.dist_mm_to_top + height_next); coordf_t next_radius = calc_radius(node.dist_mm_to_top + height_next);
auto avoidance_next = get_avoidance(next_radius, obj_layer_nr_next); const ExPolygons& avoidance_next = get_avoidance(next_radius, obj_layer_nr_next);
Point to_outside = projection_onto(avoidance_next, node.position); Point to_outside = projection_onto(avoidance_next, node.position);
Point direction_to_outer = to_outside - node.position; Point direction_to_outer = to_outside - node.position;
@@ -3337,7 +3320,7 @@ void TreeSupport::drop_nodes()
p_node->to_buildplate = false; p_node->to_buildplate = false;
return; // from parallel_for_each lambda return; // from parallel_for_each lambda
} }
auto next_collision = get_collision(0, obj_layer_nr_next); const ExPolygons& next_collision = get_collision(0, obj_layer_nr_next);
const bool to_buildplate = !is_inside_ex(m_ts_data->m_layer_outlines[obj_layer_nr_next], next_layer_vertex); const bool to_buildplate = !is_inside_ex(m_ts_data->m_layer_outlines[obj_layer_nr_next], next_layer_vertex);
// don't increase radius if next node will collide partially with the object (STUDIO-7883) // don't increase radius if next node will collide partially with the object (STUDIO-7883)
to_outside = projection_onto(next_collision, next_layer_vertex); to_outside = projection_onto(next_collision, next_layer_vertex);
+2 -2
View File
@@ -536,9 +536,9 @@ private:
coordf_t calc_branch_radius(coordf_t base_radius, coordf_t mm_to_top, double diameter_angle_scale_factor, bool use_min_distance=true); coordf_t calc_branch_radius(coordf_t base_radius, coordf_t mm_to_top, double diameter_angle_scale_factor, bool use_min_distance=true);
coordf_t calc_radius(coordf_t mm_to_top); coordf_t calc_radius(coordf_t mm_to_top);
coordf_t get_radius(const SupportNode* node); coordf_t get_radius(const SupportNode* node);
ExPolygons get_avoidance(coordf_t radius, size_t obj_layer_nr); const ExPolygons& get_avoidance(coordf_t radius, size_t obj_layer_nr);
// layer's expolygon expanded by radius+m_xy_distance // layer's expolygon expanded by radius+m_xy_distance
ExPolygons get_collision(coordf_t radius, size_t layer_nr); const ExPolygons& get_collision(coordf_t radius, size_t layer_nr);
// get Polygons instead of ExPolygons // get Polygons instead of ExPolygons
Polygons get_collision_polys(coordf_t radius, size_t layer_nr); Polygons get_collision_polys(coordf_t radius, size_t layer_nr);
+554 -4
View File
@@ -19,6 +19,7 @@
#include <mutex> #include <mutex>
#include <numeric> #include <numeric>
#include <optional> #include <optional>
#include "nlohmann/json.hpp"
#include <queue> #include <queue>
#include <ratio> #include <ratio>
#include <string> #include <string>
@@ -115,6 +116,20 @@ float DecodedHeightTexture::sample(const Vec2f &uv, bool tile_enabled, TextureTi
return top * (1.f - ty) + bottom * ty; return top * (1.f - ty) + bottom * ty;
} }
float DecodedHeightTexture::sample_alpha(const Vec2f &uv, bool tile_enabled, TextureTileMethod tile_method) const
{
TexelTap tap;
if (!has_alpha())
return 1.f; // no coverage plane means the picture is everywhere, which is the usual case
if (!texel_tap(uv, tile_enabled, tile_method, tap))
return 0.f;
auto at = [this](int x, int y) { return float(alpha[size_t(y) * size_t(width) + size_t(x)]) / 255.f; };
const float top = at(tap.x0, tap.y0) * (1.f - tap.tx) + at(tap.x1, tap.y0) * tap.tx;
const float bottom = at(tap.x0, tap.y1) * (1.f - tap.tx) + at(tap.x1, tap.y1) * tap.tx;
return top * (1.f - tap.ty) + bottom * tap.ty;
}
Vec3f DecodedHeightTexture::sample_color(const Vec2f &uv, bool tile_enabled, TextureTileMethod tile_method) const Vec3f DecodedHeightTexture::sample_color(const Vec2f &uv, bool tile_enabled, TextureTileMethod tile_method) const
{ {
TexelTap tap; TexelTap tap;
@@ -261,6 +276,272 @@ bool height_texture_has_color(const TextureDisplacementLayer &layer)
return decode_height_texture(layer).has_color(); return decode_height_texture(layer).has_color();
} }
namespace {
// Unsharp mask over the colour: blur a copy and push each texel away from it. The blur is the same
// sliding-window helper the Smoothing slider uses, run per channel, so the cost is the image size
// rather than the image size times the radius.
//
// Worth having here for a reason it would not be in a photo editor: the picture is about to be
// reduced to a handful of flat colours, and a soft edge between two of them quantizes into a ragged
// band of both. Sharpening first makes that boundary land in one place.
void sharpen_rgb(DecodedHeightTexture &tex, float amount)
{
if (!tex.has_color() || amount <= 0.f)
return;
const size_t n = size_t(tex.width) * size_t(tex.height);
// A small radius, scaled with the image so a 4k picture is not sharpened a tenth as hard as a
// 512 one. Deliberately not tied to `amount`: that is how much, this is how wide.
const int radius = std::max(1, int(std::lround(double(std::min(tex.width, tex.height)) / 512.0)));
const float k = std::clamp(amount, 0.f, 1.f) * 1.5f;
std::vector<uint8_t> plane(n);
for (int c = 0; c < 3; ++c) {
for (size_t i = 0; i < n; ++i)
plane[i] = tex.rgb[i * 3 + size_t(c)];
smooth_height_pixels_box(plane, tex.width, tex.height, radius);
for (size_t i = 0; i < n; ++i) {
const float sharp = float(tex.rgb[i * 3 + size_t(c)]) + k * (float(tex.rgb[i * 3 + size_t(c)]) - float(plane[i]));
tex.rgb[i * 3 + size_t(c)] = uint8_t(std::lround(std::clamp(sharp, 0.f, 255.f)));
}
}
}
// Reduces the picture to at most `count` colours, by k-means in RGB.
//
// "At most": two of the chosen colours can still match the same filament, in which case the print
// uses fewer. The guarantee runs the right way round, which is the one that matters - asking for
// three colours cannot produce four.
//
// k-means rather than a fixed palette or an even split of the luma range, because the useful
// question is which colours this picture is actually made of. A tartan asks for its own handful of
// greens and reds; posterizing the range would hand it eight shades of mud instead. The clustering
// runs over a coarse histogram, not over the pixels: at 1024 x 1024 that is 4096 weighted points
// instead of a million, which is what makes it cheap enough to sit on a slider.
void reduce_colors(DecodedHeightTexture &tex, int count)
{
if (!tex.has_color() || count < 2 || count > 64)
return;
const size_t n = size_t(tex.width) * size_t(tex.height);
// 16 levels per channel, each bin keeping the mean of what fell in it, so a colour is not
// snapped to a bin corner before the clustering has had a say.
struct Bin { double r = 0, g = 0, b = 0; uint32_t n = 0; };
std::vector<Bin> bins(size_t(16 * 16 * 16));
for (size_t i = 0; i < n; ++i) {
const uint8_t r = tex.rgb[i * 3], g = tex.rgb[i * 3 + 1], b = tex.rgb[i * 3 + 2];
Bin &bin = bins[(size_t(r >> 4) << 8) | (size_t(g >> 4) << 4) | size_t(b >> 4)];
bin.r += r; bin.g += g; bin.b += b; ++bin.n;
}
struct Point { float c[3]; double w; };
std::vector<Point> pts;
pts.reserve(bins.size());
for (const Bin &bin : bins)
if (bin.n > 0)
pts.push_back({ { float(bin.r / bin.n), float(bin.g / bin.n), float(bin.b / bin.n) }, double(bin.n) });
if (pts.size() <= size_t(count))
return; // already no more colours than asked for
// Seeded the k-means++ way, deterministically: the heaviest point first, then each next one is
// whichever point is furthest from everything chosen so far, weighted by how much of the image
// it is. Picking the N fullest bins instead would put every centre inside the one dominant
// colour of a picture that has one.
std::vector<std::array<float, 3>> centre;
{
size_t best = 0;
for (size_t i = 1; i < pts.size(); ++i)
if (pts[i].w > pts[best].w)
best = i;
centre.push_back({ pts[best].c[0], pts[best].c[1], pts[best].c[2] });
std::vector<double> d2(pts.size(), std::numeric_limits<double>::max());
while (int(centre.size()) < count) {
const auto &last = centre.back();
double top = -1.0;
size_t pick = 0;
for (size_t i = 0; i < pts.size(); ++i) {
const double dr = pts[i].c[0] - last[0], dg = pts[i].c[1] - last[1], db = pts[i].c[2] - last[2];
d2[i] = std::min(d2[i], dr * dr + dg * dg + db * db);
if (const double score = d2[i] * pts[i].w; score > top) {
top = score;
pick = i;
}
}
if (top <= 0.0)
break; // fewer distinct colours than asked for
centre.push_back({ pts[pick].c[0], pts[pick].c[1], pts[pick].c[2] });
}
}
// Lloyd's algorithm over the weighted bins. Twelve passes: on a histogram this coarse it has
// stopped moving well before that, and a fixed count keeps the slider's cost predictable.
const int k = int(centre.size());
for (int iter = 0; iter < 12; ++iter) {
std::vector<std::array<double, 3>> sum(size_t(k), { 0.0, 0.0, 0.0 });
std::vector<double> wsum(size_t(k), 0.0);
for (const Point &p : pts) {
int best = 0;
double bd = std::numeric_limits<double>::max();
for (int c = 0; c < k; ++c) {
const double dr = p.c[0] - centre[size_t(c)][0], dg = p.c[1] - centre[size_t(c)][1],
db = p.c[2] - centre[size_t(c)][2];
if (const double d = dr * dr + dg * dg + db * db; d < bd) { bd = d; best = c; }
}
for (int ch = 0; ch < 3; ++ch)
sum[size_t(best)][size_t(ch)] += double(p.c[ch]) * p.w;
wsum[size_t(best)] += p.w;
}
for (int c = 0; c < k; ++c)
if (wsum[size_t(c)] > 0.0)
for (int ch = 0; ch < 3; ++ch)
centre[size_t(c)][size_t(ch)] = float(sum[size_t(c)][size_t(ch)] / wsum[size_t(c)]);
}
// Every texel takes its nearest centre. Parallel by rows, as the tonal pass is.
const size_t rows = size_t(tex.height), cols = size_t(tex.width);
tbb::parallel_for(tbb::blocked_range<size_t>(0, rows), [&](const tbb::blocked_range<size_t> &range) {
for (size_t y = range.begin(); y < range.end(); ++y)
for (size_t x = 0; x < cols; ++x) {
uint8_t *px = tex.rgb.data() + (y * cols + x) * 3;
int best = 0;
float bd = std::numeric_limits<float>::max();
for (int c = 0; c < k; ++c) {
const float dr = float(px[0]) - centre[size_t(c)][0], dg = float(px[1]) - centre[size_t(c)][1],
db = float(px[2]) - centre[size_t(c)][2];
if (const float d = dr * dr + dg * dg + db * db; d < bd) { bd = d; best = c; }
}
for (int ch = 0; ch < 3; ++ch)
px[size_t(ch)] = uint8_t(std::lround(std::clamp(centre[size_t(best)][size_t(ch)], 0.f, 255.f)));
}
});
}
// Keys one colour out, so a logo on a white card prints as the logo and the object's own colour
// shows through the rest.
//
// The corners are the default because a background that does not reach all four of them is not a
// background, and that makes the common case one click; the median of the four is taken so a single
// stray corner cannot decide it. The panel's dropper and swatch override it when the picture needs
// something else keyed out.
void remove_background(DecodedHeightTexture &tex, const TextureDisplacementLayer &layer)
{
if (!tex.has_color() || !layer.bg_remove || tex.width < 2 || tex.height < 2)
return;
const size_t n = size_t(tex.width) * size_t(tex.height);
float key[3];
if (layer.bg_from_corners) {
const size_t corner[4] = { 0, size_t(tex.width) - 1, (size_t(tex.height) - 1) * size_t(tex.width),
n - 1 };
for (int c = 0; c < 3; ++c) {
std::array<float, 4> v{};
for (int i = 0; i < 4; ++i)
v[size_t(i)] = float(tex.rgb[corner[i] * 3 + size_t(c)]);
std::sort(v.begin(), v.end());
key[c] = 0.5f * (v[1] + v[2]);
}
} else {
for (int c = 0; c < 3; ++c)
key[c] = std::clamp(layer.bg_color[c], 0.f, 1.f) * 255.f;
}
// Euclidean in 0..255 RGB, against a tolerance scaled to the diagonal of that cube, so the
// slider reads as "how different from the background still counts as background".
const float limit = std::clamp(layer.bg_tolerance, 0.f, 1.f) * 441.67f;
if (!tex.has_alpha())
tex.alpha.assign(n, 255);
for (size_t i = 0; i < n; ++i) {
const float dr = float(tex.rgb[i * 3]) - key[0];
const float dg = float(tex.rgb[i * 3 + 1]) - key[1];
const float db = float(tex.rgb[i * 3 + 2]) - key[2];
if (std::sqrt(dr * dr + dg * dg + db * db) <= limit)
tex.alpha[i] = 0;
}
}
} // namespace
bool has_color_adjustments(const TextureDisplacementLayer &layer)
{
return layer.brightness != 0.f || layer.contrast != 0.f || layer.saturation != 1.f ||
layer.gamma != 1.f || layer.color_invert || layer.hue != 0.f || layer.sharpness > 0.f ||
layer.color_count >= 2 || layer.bg_remove;
}
void apply_color_adjustments(DecodedHeightTexture &tex, const TextureDisplacementLayer &layer)
{
if (!tex.has_color() || !has_color_adjustments(layer))
return;
// A contrast of -1 collapses the image to mid grey and +1 doubles the slope about it, which is
// the range a single slider can offer without a scale nobody can read off the handle.
const float slope = 1.f + std::clamp(layer.contrast, -1.f, 1.f);
const float bright = std::clamp(layer.brightness, -1.f, 1.f);
const float sat = std::max(0.f, layer.saturation);
const float inv_g = 1.f / std::max(0.01f, layer.gamma);
const bool negate = layer.color_invert;
// Hue is a rotation of the colour about the grey axis. Rodrigues' formula about (1,1,1)/sqrt(3),
// then the luma is put back exactly (the rotation preserves R+G+B, not the Rec. 709 weighting),
// so turning the wheel changes which filament an area matches without also changing how light
// it is.
const float hue_rad = layer.hue * float(M_PI) / 180.f;
const float hue_cos = std::cos(hue_rad), hue_sin = std::sin(hue_rad);
const bool rotate = layer.hue != 0.f;
constexpr float LW[3] = { 0.2126f, 0.7152f, 0.0722f }; // Rec. 709 luma
// Each output byte depends only on its own pixel, so the rows are independent. At 1024 x 1024
// this runs on every preview rebuild and every bake, which is what makes it worth splitting.
const size_t rows = size_t(tex.height);
const size_t cols = size_t(tex.width);
tbb::parallel_for(tbb::blocked_range<size_t>(0, rows), [&](const tbb::blocked_range<size_t> &range) {
for (size_t y = range.begin(); y < range.end(); ++y) {
uint8_t *row = tex.rgb.data() + y * cols * 3;
for (size_t x = 0; x < cols; ++x) {
uint8_t *px = row + x * 3;
float c[3];
for (int k = 0; k < 3; ++k) {
float v = float(px[k]) * (1.f / 255.f);
v = std::pow(v, inv_g);
v = (v - 0.5f) * slope + 0.5f;
v += bright;
c[k] = v;
}
if (rotate) {
const float luma = LW[0] * c[0] + LW[1] * c[1] + LW[2] * c[2];
// The component of the colour across the grey axis, which is what a hue is.
const float mean = (c[0] + c[1] + c[2]) / 3.f;
const float p[3] = { c[0] - mean, c[1] - mean, c[2] - mean };
// k x p for the unit grey axis k = (1,1,1)/sqrt(3).
constexpr float INV_SQRT3 = 0.57735027f;
const float x[3] = { INV_SQRT3 * (p[1] - p[2]), INV_SQRT3 * (p[2] - p[0]),
INV_SQRT3 * (p[0] - p[1]) };
for (int k = 0; k < 3; ++k)
c[k] = mean + p[k] * hue_cos + x[k] * hue_sin;
const float luma_now = LW[0] * c[0] + LW[1] * c[1] + LW[2] * c[2];
for (float &v : c)
v += luma - luma_now;
}
if (sat != 1.f) {
// Rec. 709 luma, the same weights the rest of the colour path uses to decide
// what counts as light and dark.
const float luma = LW[0] * c[0] + LW[1] * c[1] + LW[2] * c[2];
for (float &v : c)
v = luma + (v - luma) * sat;
}
for (int k = 0; k < 3; ++k) {
float v = std::clamp(c[k], 0.f, 1.f);
if (negate)
v = 1.f - v;
px[k] = uint8_t(std::lround(v * 255.f));
}
}
}
});
// Sharpen, threshold and the background key are all passes over the whole image rather than
// per-texel arithmetic, so they come after the loop above and in this order: sharpening a
// thresholded image would only ring along the one edge it has left, and the key has to read the
// colours the user is actually looking at.
sharpen_rgb(tex, layer.sharpness);
reduce_colors(tex, layer.color_count);
remove_background(tex, layer);
}
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer) DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer)
{ {
DecodedHeightTexture result; DecodedHeightTexture result;
@@ -313,6 +594,10 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
result.height = h; result.height = h;
result.pixels.resize(size_t(w) * size_t(h)); result.pixels.resize(size_t(w) * size_t(h));
result.rgb.resize(size_t(w) * size_t(h) * 3); result.rgb.resize(size_t(w) * size_t(h) * 3);
// A fourth channel is coverage, and it is the only thing that can say "the picture is
// not here" as opposed to "the picture is black here". Kept only when the source has it.
if (bpp >= 4)
result.alpha.resize(size_t(w) * size_t(h));
// decode_colored_png() hands its buffer back bottom-up - it is shared with the CLI's // decode_colored_png() hands its buffer back bottom-up - it is shared with the CLI's
// plate-thumbnail loader, which expects that - while this type, and decode_png()'s // plate-thumbnail loader, which expects that - while this type, and decode_png()'s
// grayscale path above, are top-to-bottom. Reverse the rows on the way in so a colour // grayscale path above, are top-to-bottom. Reverse the rows on the way in so a colour
@@ -326,6 +611,8 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
result.rgb[dst * 3 + 1] = g; result.rgb[dst * 3 + 1] = g;
result.rgb[dst * 3 + 2] = b; result.rgb[dst * 3 + 2] = b;
result.pixels[dst] = uint8_t(std::lround(0.299 * r + 0.587 * g + 0.114 * b)); result.pixels[dst] = uint8_t(std::lround(0.299 * r + 0.587 * g + 0.114 * b));
if (bpp >= 4)
result.alpha[dst] = src[3];
} }
} }
@@ -1859,10 +2146,14 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
// Outside a non-tiled placement there is no texture at all - the same hard edge sample() gives the // Outside a non-tiled placement there is no texture at all - the same hard edge sample() gives the
// height. Checked explicitly because sample_color() reports it as black, which is a real colour. // height. Checked explicitly because sample_color() reports it as black, which is a real colour.
auto covered = [&](const Vec2f &planar) { auto covered = [&](const Vec2f &planar) {
if (layer.tile_enabled)
return true;
const Vec2f uv = xf(planar); const Vec2f uv = xf(planar);
return uv.x() >= 0.f && uv.x() < 1.f && uv.y() >= 0.f && uv.y() < 1.f; if (!layer.tile_enabled && !(uv.x() >= 0.f && uv.x() < 1.f && uv.y() >= 0.f && uv.y() < 1.f))
return false;
// Where the picture is transparent there is no colour to print, which is the same answer as
// being outside the placement - not black, which is a colour. Half coverage counts as
// covered: a triangle prints in one filament either way, so the only useful question is
// which side of the edge it falls on.
return texture.sample_alpha(uv, layer.tile_enabled, layer.tile_method) >= 0.5f;
}; };
if (lscm_uv != nullptr) { if (lscm_uv != nullptr) {
@@ -3241,9 +3532,13 @@ std::shared_ptr<std::vector<PreparedLayer>> prepare_sampleable_layers(
const TriangleSelector::TriangleSplittingData &data = facets_data[size_t(layer->slot)]; const TriangleSelector::TriangleSplittingData &data = facets_data[size_t(layer->slot)];
if (data.triangles_to_split.empty()) if (data.triangles_to_split.empty())
continue; continue;
const DecodedHeightTexture tex = decode_height_texture(*layer); DecodedHeightTexture tex = decode_height_texture(*layer);
if (tex.empty() || (need_color && !tex.has_color())) if (tex.empty() || (need_color && !tex.has_color()))
continue; continue;
// The one place the bake and every preview job read a layer's colour from, so correcting it
// here is what keeps what the panel shows and what the bake prints the same image.
if (need_color)
apply_color_adjustments(tex, *layer);
TriangleSelector selector(selector_mesh); TriangleSelector selector(selector_mesh);
selector.deserialize(data, false); selector.deserialize(data, false);
@@ -5166,5 +5461,260 @@ indexed_triangle_set cut_mesh_at_steps(const indexed_triangle_set &mesh, const s
return out; return out;
} }
// ---------------------------------------------------------------------------------------------
// Project persistence. See the declarations in TextureDisplacement.hpp for why this is JSON rather
// than the cereal stream next to them.
// ---------------------------------------------------------------------------------------------
namespace {
nlohmann::json vec2_to_json(const Vec2f &v) { return nlohmann::json::array({ v.x(), v.y() }); }
nlohmann::json vec3_to_json(const Vec3f &v) { return nlohmann::json::array({ v.x(), v.y(), v.z() }); }
Vec2f vec2_from_json(const nlohmann::json &j, const Vec2f &fallback)
{
if (!j.is_array() || j.size() != 2)
return fallback;
return Vec2f(j[0].get<float>(), j[1].get<float>());
}
Vec3f vec3_from_json(const nlohmann::json &j, const Vec3f &fallback)
{
if (!j.is_array() || j.size() != 3)
return fallback;
return Vec3f(j[0].get<float>(), j[1].get<float>(), j[2].get<float>());
}
// Reads key `k` into `out` when it is present and of the expected type, leaving `out` alone otherwise.
// That "leave it alone" is the whole point: every member starts at its struct default, so a project
// written by an older build simply keeps the defaults for whatever it did not know about.
template<class T> void read(const nlohmann::json &j, const char *k, T &out)
{
const auto it = j.find(k);
if (it == j.end())
return;
try {
out = it->get<T>();
} catch (...) {
}
}
void read_enum(const nlohmann::json &j, const char *k, int &out)
{
const auto it = j.find(k);
if (it != j.end() && it->is_number_integer())
out = it->get<int>();
}
} // namespace
std::string texture_displacement_layers_to_json(const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementOptions &options)
{
nlohmann::json root;
root["version"] = 1;
nlohmann::json &opt = root["options"];
opt["displace_border"] = options.displace_border;
opt["smooth_enabled"] = options.smooth_enabled;
opt["smooth_strength"] = options.smooth_strength;
opt["smooth_iterations"] = options.smooth_iterations;
opt["smooth_skip_border"] = options.smooth_skip_border;
opt["pipeline_v2"] = options.pipeline_v2;
opt["v2_refine_mm"] = options.v2_refine_mm;
opt["v2_regularize"] = options.v2_regularize;
opt["v2_max_triangles_k"] = options.v2_max_triangles_k;
opt["v2_relocate"] = options.v2_relocate;
opt["v2_flip_edges"] = options.v2_flip_edges;
opt["color_mix_enabled"] = options.color_mix_enabled;
opt["color_despeckle"] = options.color_despeckle;
opt["color_mix_count"] = options.color_mix_count;
opt["active_slot"] = options.active_slot;
opt["panel_mode"] = options.panel_mode;
nlohmann::json &arr = root["layers"];
arr = nlohmann::json::array();
for (const TextureDisplacementLayer &l : layers) {
nlohmann::json j;
j["slot"] = l.slot;
j["name"] = l.name;
j["path"] = l.path;
j["path_in_3mf"] = l.path_in_3mf;
j["depth_mm"] = l.depth_mm;
j["tiling_scale"] = l.tiling_scale;
j["rotation_deg"] = l.rotation_deg;
j["offset"] = vec2_to_json(l.offset);
j["invert"] = l.invert;
j["midlevel"] = l.midlevel;
j["smoothing"] = l.smoothing;
j["edge_smoothing"] = l.edge_smoothing;
j["edge_smoothing_amount"] = l.edge_smoothing_amount;
j["auto_connect_islands"] = l.auto_connect_islands;
j["tile_enabled"] = l.tile_enabled;
j["tile_method"] = int(l.tile_method);
j["projection_method"] = int(l.projection_method);
j["blend_mode"] = int(l.blend_mode);
j["color_enabled"] = l.color_enabled;
j["lscm_seam_angle_deg"] = l.lscm_seam_angle_deg;
j["island_padding_mm"] = l.island_padding_mm;
j["view_project_right"] = vec3_to_json(l.view_project_right);
j["view_project_up"] = vec3_to_json(l.view_project_up);
j["view_project_projective"] = l.view_project_projective;
j["view_project_matrix"] = l.view_project_matrix;
// Flat pairs rather than nested arrays: shorter, and the reader can simply ignore a trailing
// odd element instead of having to validate every sub-array.
nlohmann::json &seams = j["lscm_seam_edges"];
seams = nlohmann::json::array();
for (const auto &[a, b] : l.lscm_seam_edges) {
seams.push_back(a);
seams.push_back(b);
}
nlohmann::json &islands = j["islands"];
islands = nlohmann::json::array();
for (const TextureIsland &island : l.islands)
islands.push_back(nlohmann::json::array(
{ island.offset.x(), island.offset.y(), island.rotation_deg, island.scale }));
j["island_groups"] = l.island_groups;
nlohmann::json &overrides = j["lscm_uv_overrides"];
overrides = nlohmann::json::array();
for (const auto &[key, uv] : l.lscm_uv_overrides)
overrides.push_back(nlohmann::json::array({ key, uv.x(), uv.y() }));
arr.push_back(std::move(j));
}
return root.dump();
}
bool texture_displacement_layers_from_json(const std::string &text,
std::vector<TextureDisplacementLayer> &layers,
TextureDisplacementOptions &options)
{
nlohmann::json root;
try {
root = nlohmann::json::parse(text);
} catch (...) {
return false;
}
if (!root.is_object())
return false;
TextureDisplacementOptions out_options;
if (const auto it = root.find("options"); it != root.end() && it->is_object()) {
const nlohmann::json &opt = *it;
read(opt, "displace_border", out_options.displace_border);
read(opt, "smooth_enabled", out_options.smooth_enabled);
read(opt, "smooth_strength", out_options.smooth_strength);
read(opt, "smooth_iterations", out_options.smooth_iterations);
read(opt, "smooth_skip_border", out_options.smooth_skip_border);
read(opt, "pipeline_v2", out_options.pipeline_v2);
read(opt, "v2_refine_mm", out_options.v2_refine_mm);
read(opt, "v2_regularize", out_options.v2_regularize);
read(opt, "v2_max_triangles_k", out_options.v2_max_triangles_k);
read(opt, "v2_relocate", out_options.v2_relocate);
read(opt, "v2_flip_edges", out_options.v2_flip_edges);
read(opt, "color_mix_enabled", out_options.color_mix_enabled);
read(opt, "color_despeckle", out_options.color_despeckle);
read(opt, "color_mix_count", out_options.color_mix_count);
read(opt, "active_slot", out_options.active_slot);
read(opt, "panel_mode", out_options.panel_mode);
out_options.active_slot = std::clamp(out_options.active_slot, 0, int(TEXTURE_DISPLACEMENT_MAX_LAYERS) - 1);
out_options.panel_mode = std::clamp(out_options.panel_mode, 0, 1);
}
std::vector<TextureDisplacementLayer> out_layers;
// read() skips a value of the wrong type, but the vector, matrix, seam, island and UV-override arrays
// below read their numbers with get<>(), which throws on one; that rejects the document, like text that
// does not parse.
try {
if (const auto it = root.find("layers"); it != root.end() && it->is_array()) {
for (const nlohmann::json &j : *it) {
if (!j.is_object())
continue;
TextureDisplacementLayer l;
read(j, "slot", l.slot);
// The panel and the paint masks are indexed by slot, so a layer without a valid one rejects the
// document rather than reaching them.
if (l.slot < 0 || l.slot >= int(TEXTURE_DISPLACEMENT_MAX_LAYERS))
return false;
read(j, "name", l.name);
read(j, "path", l.path);
read(j, "path_in_3mf", l.path_in_3mf);
read(j, "depth_mm", l.depth_mm);
read(j, "tiling_scale", l.tiling_scale);
read(j, "rotation_deg", l.rotation_deg);
if (const auto o = j.find("offset"); o != j.end())
l.offset = vec2_from_json(*o, l.offset);
read(j, "invert", l.invert);
read(j, "midlevel", l.midlevel);
read(j, "smoothing", l.smoothing);
read(j, "edge_smoothing", l.edge_smoothing);
read(j, "edge_smoothing_amount", l.edge_smoothing_amount);
read(j, "auto_connect_islands", l.auto_connect_islands);
read(j, "tile_enabled", l.tile_enabled);
read(j, "color_enabled", l.color_enabled);
int tile_method = int(l.tile_method), projection = int(l.projection_method), blend = int(l.blend_mode);
read_enum(j, "tile_method", tile_method);
read_enum(j, "projection_method", projection);
read_enum(j, "blend_mode", blend);
l.tile_method = TextureTileMethod(tile_method);
l.projection_method = TextureProjectionMethod(std::clamp(projection, 0, 4));
l.blend_mode = TextureBlendMode(blend);
read(j, "lscm_seam_angle_deg", l.lscm_seam_angle_deg);
read(j, "island_padding_mm", l.island_padding_mm);
if (const auto v = j.find("view_project_right"); v != j.end())
l.view_project_right = vec3_from_json(*v, l.view_project_right);
if (const auto v = j.find("view_project_up"); v != j.end())
l.view_project_up = vec3_from_json(*v, l.view_project_up);
read(j, "view_project_projective", l.view_project_projective);
if (const auto v = j.find("view_project_matrix"); v != j.end() && v->is_array() && v->size() == 12)
for (size_t i = 0; i < 12; ++i)
l.view_project_matrix[i] = (*v)[i].get<float>();
if (const auto v = j.find("lscm_seam_edges"); v != j.end() && v->is_array())
for (size_t i = 0; i + 1 < v->size(); i += 2)
l.lscm_seam_edges.emplace_back((*v)[i].get<int>(), (*v)[i + 1].get<int>());
if (const auto v = j.find("islands"); v != j.end() && v->is_array())
for (const nlohmann::json &e : *v) {
if (!e.is_array() || e.size() != 4)
continue;
TextureIsland island;
island.offset = Vec2f(e[0].get<float>(), e[1].get<float>());
island.rotation_deg = e[2].get<float>();
island.scale = e[3].get<float>();
l.islands.push_back(island);
}
read(j, "island_groups", l.island_groups);
if (const auto v = j.find("lscm_uv_overrides"); v != j.end() && v->is_array())
for (const nlohmann::json &e : *v) {
if (!e.is_array() || e.size() != 3)
continue;
l.lscm_uv_overrides.emplace_back(e[0].get<int>(), Vec2f(e[1].get<float>(), e[2].get<float>()));
}
out_layers.push_back(std::move(l));
}
}
} catch (const nlohmann::json::exception &) {
return false;
}
layers = std::move(out_layers);
options = out_options;
return true;
}
} // namespace Slic3r } // namespace Slic3r
+80 -3
View File
@@ -293,6 +293,32 @@ struct TextureDisplacementLayer
// for the boundary to be drawn on. // for the boundary to be drawn on.
bool color_enabled = false; bool color_enabled = false;
// Colour correction, applied to the image's RGB before it is matched against the filaments and
// before the preview quantizes it. Never to the height: the relief is what the grayscale says,
// and a brightness slider has no business moving geometry. The defaults below are the identity,
// so a layer that never meets the Image tool behaves exactly as it did.
float brightness = 0.f; // -1 .. 1, added last, after contrast
float contrast = 0.f; // -1 .. 1, pivoted on mid grey; -1 flattens to grey, 1 doubles the slope
float hue = 0.f; // -180 .. 180 degrees, rotated about the grey axis at constant luma
float sharpness = 0.f; // 0 .. 1 unsharp mask amount, 0 is off
// How many colours the picture is allowed to use, 0 for as many as it likes. Reduced by k-means
// in RGB before the filaments are matched, so the result uses at most this many of them: 2 for a
// logo that should be two flat colours, 4 or 5 for a poster, off for a photograph.
int color_count = 0; // 0, or 2 .. 12
// Leaves the background of a picture unprinted, so the object's own colour shows through instead
// of a slab of white behind a logo. Texels the source image marked transparent always count;
// `bg_remove` additionally keys out one colour.
bool bg_remove = false;
float bg_tolerance = 0.15f; // 0 .. 1, distance from the keyed colour that still counts as background
// The colour keyed out. Taken from the median of the picture's four corners while
// `bg_from_corners` is set, which is right for nearly every logo on a flat card; the panel's
// dropper and swatch clear it and put a colour of the user's own here.
bool bg_from_corners = true;
float bg_color[3] = { 1.f, 1.f, 1.f };
float saturation = 1.f; // 0 .. 2, 1 leaves it alone, 0 is grayscale
float gamma = 1.f; // 0.2 .. 5, 1 leaves it alone; above 1 lifts the midtones
bool color_invert = false; // negative of the colour, as opposed to `invert` which flips relief
bool empty() const { return !image_data || image_data->empty(); } bool empty() const { return !image_data || image_data->empty(); }
template<class Archive> void save(Archive &ar) const template<class Archive> void save(Archive &ar) const
@@ -302,7 +328,9 @@ struct TextureDisplacementLayer
static_cast<int>(tile_method), static_cast<int>(projection_method), lscm_seam_angle_deg, islands, static_cast<int>(tile_method), static_cast<int>(projection_method), lscm_seam_angle_deg, islands,
static_cast<int>(blend_mode), midlevel, island_padding_mm, lscm_seam_edges, view_project_right, static_cast<int>(blend_mode), midlevel, island_padding_mm, lscm_seam_edges, view_project_right,
view_project_up, smoothing, edge_smoothing, edge_smoothing_amount, auto_connect_islands, island_groups, view_project_up, smoothing, edge_smoothing, edge_smoothing_amount, auto_connect_islands, island_groups,
lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled); lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled, brightness,
contrast, saturation, gamma, color_invert, hue, sharpness, color_count, bg_remove,
bg_tolerance, bg_from_corners, bg_color[0], bg_color[1], bg_color[2]);
} }
template<class Archive> void load(Archive &ar) template<class Archive> void load(Archive &ar)
{ {
@@ -314,7 +342,8 @@ struct TextureDisplacementLayer
tile_method_int, projection_method_int, lscm_seam_angle_deg, islands, blend_mode_int, midlevel, tile_method_int, projection_method_int, lscm_seam_angle_deg, islands, blend_mode_int, midlevel,
island_padding_mm, lscm_seam_edges, view_project_right, view_project_up, smoothing, edge_smoothing, island_padding_mm, lscm_seam_edges, view_project_right, view_project_up, smoothing, edge_smoothing,
edge_smoothing_amount, auto_connect_islands, island_groups, lscm_uv_overrides, view_project_projective, edge_smoothing_amount, auto_connect_islands, island_groups, lscm_uv_overrides, view_project_projective,
view_project_matrix, color_enabled); view_project_matrix, color_enabled, brightness, contrast, saturation, gamma, color_invert, hue, sharpness,
color_count, bg_remove, bg_tolerance, bg_from_corners, bg_color[0], bg_color[1], bg_color[2]);
image_data = blob.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(blob.begin(), blob.end()); image_data = blob.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(blob.begin(), blob.end());
tile_method = static_cast<TextureTileMethod>(tile_method_int); tile_method = static_cast<TextureTileMethod>(tile_method_int);
projection_method = static_cast<TextureProjectionMethod>(projection_method_int); projection_method = static_cast<TextureProjectionMethod>(projection_method_int);
@@ -397,14 +426,41 @@ struct TextureDisplacementOptions
// finer than the mesh leaves behind, without eating features that are genuinely a facet wide. // finer than the mesh leaves behind, without eating features that are genuinely a facet wide.
int color_despeckle = 2; int color_despeckle = 2;
// Where the panel was left, rather than anything the bake reads. It lives with the volume because
// that is what it describes: reopening a project should put the user back on the layer they were
// editing, in the mode they were editing it in, not on slot 0 in Standard.
int active_slot = 0;
int panel_mode = 0; // 0 Standard, 1 Pro
template<class Archive> void serialize(Archive &ar) template<class Archive> void serialize(Archive &ar)
{ {
ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border, ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border,
pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k, pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k,
v2_relocate, color_mix_enabled, color_despeckle, color_mix_count); v2_relocate, v2_flip_edges, color_mix_enabled, color_despeckle, color_mix_count,
active_slot, panel_mode);
} }
}; };
// Project persistence (see bbs_3mf.cpp). The layer stack and the per-volume options are written to the
// .3mf as JSON rather than through the cereal save()/load() above: those two are positional and
// unversioned, which is right for the undo/redo stack they serve (one session, one binary) but would
// make every future field a project-breaking change. A JSON object tolerates both directions - an
// unknown key is ignored, a missing one keeps the member's default - so old projects keep loading and
// new ones degrade gracefully in older builds.
//
// The texture image itself is *not* in here. It is a binary blob that belongs in the archive as a file
// of its own, exactly as EmbossShape stores its SVG; `path_in_3mf` names that file, and the caller is
// responsible for writing it and for filling `image_data` back in on load.
std::string texture_displacement_layers_to_json(const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementOptions &options);
// Returns false and leaves both outputs untouched when the text is not a valid JSON object, a number in a
// layer's vector, matrix, seam, island or UV-override array has the wrong type, or a layer's slot is
// missing or outside [0, TEXTURE_DISPLACEMENT_MAX_LAYERS). Any other value of the wrong type or shape
// keeps its default.
bool texture_displacement_layers_from_json(const std::string &text,
std::vector<TextureDisplacementLayer> &layers,
TextureDisplacementOptions &options);
// How much detail a height texture carries: central differences of the grey image, the mean gradient // How much detail a height texture carries: central differences of the grey image, the mean gradient
// and the share of texels steeper than 30 grey levels, mapped to how many texels one mesh edge may // and the share of texels steeper than 30 grey levels, mapped to how many texels one mesh edge may
// span (1 for a hard-edged image, 4 for a smooth one). Cached per image, like the decode. // span (1 for a hard-edged image, 4 for a smooth one). Cached per image, like the decode.
@@ -448,11 +504,19 @@ struct DecodedHeightTexture
// has_color() is what the whole colour feature keys off: a layer set to colour a model with a // has_color() is what the whole colour feature keys off: a layer set to colour a model with a
// grayscale texture on it simply colours nothing. // grayscale texture on it simply colours nothing.
std::vector<uint8_t> rgb; std::vector<uint8_t> rgb;
// Coverage: 0 where the picture is not there at all, 255 where it is fully there. Empty when the
// source image had no alpha and nothing has keyed a background out, which is the common case and
// means "opaque everywhere". Distinct from a black texel, which is a colour.
std::vector<uint8_t> alpha;
int width = 0; int width = 0;
int height = 0; int height = 0;
bool empty() const { return width <= 0 || height <= 0 || pixels.empty(); } bool empty() const { return width <= 0 || height <= 0 || pixels.empty(); }
bool has_color() const { return !empty() && rgb.size() == size_t(width) * size_t(height) * 3; } bool has_color() const { return !empty() && rgb.size() == size_t(width) * size_t(height) * 3; }
bool has_alpha() const { return !empty() && alpha.size() == size_t(width) * size_t(height); }
// Coverage at a uv, bilinear, in [0, 1]. 1 everywhere when there is no alpha plane.
float sample_alpha(const Vec2f &uv, bool tile_enabled = true,
TextureTileMethod tile_method = TextureTileMethod::Repeat) const;
// Bilinearly sampled height in [0, 1] at a normalized uv coordinate. When tile_enabled is false, // Bilinearly sampled height in [0, 1] at a normalized uv coordinate. When tile_enabled is false,
// a uv outside [0, 1) samples as 0 - the texture simply is not there, rather than its border // a uv outside [0, 1) samples as 0 - the texture simply is not there, rather than its border
// row/column being smeared outward forever (which is what clamping the coordinate would do, and // row/column being smeared outward forever (which is what clamping the coordinate would do, and
@@ -489,6 +553,19 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
// texture - cheap enough to ask every frame. Smoothing does not change it, so the raw decode is what is read. // texture - cheap enough to ask every frame. Smoothing does not change it, so the raw decode is what is read.
bool height_texture_has_color(const TextureDisplacementLayer &layer); bool height_texture_has_color(const TextureDisplacementLayer &layer);
// True when `layer`'s colour correction is anything other than the identity, so callers can skip
// both the work and the cache invalidation it would otherwise force.
bool has_color_adjustments(const TextureDisplacementLayer &layer);
// Applies that correction to an already decoded image, in place, to `rgb` only. Order is gamma,
// contrast, brightness, saturation, invert, each clamped back into range - the order a photo editor
// applies them in, and the one that keeps contrast pivoted on mid grey rather than on a brightness
// the slider has already moved.
//
// Deliberately not folded into decode_height_texture(): that result is cached per image, shared by
// every layer using the same file, and keyed on the image bytes alone.
void apply_color_adjustments(DecodedHeightTexture &tex, const TextureDisplacementLayer &layer);
// Maps a linear RGB colour in [0, 1] to an index into the caller's palette, or -1 for "no colour". // Maps a linear RGB colour in [0, 1] to an index into the caller's palette, or -1 for "no colour".
// //
// Deliberately a callback rather than a function here: matching a colour to a filament is a // Deliberately a callback rather than a function here: matching a colour to a filament is a
@@ -673,14 +673,14 @@ static bool repair_cluster_smooth(
[&]() { return cancelled(); }, &repair_error); [&]() { return cancelled(); }, &repair_error);
if (repaired) { if (repaired) {
if (cancelled()) return false; if (cancelled()) return false;
BOOST_LOG_TRIVIAL(info) << log_prefix << ": Windows 3D mesh repair finished."; BOOST_LOG_TRIVIAL(info) << log_prefix << ": mesh repair finished.";
if (!resample_face_colors(TriMesh(std::move(repaired_its)))) if (!resample_face_colors(TriMesh(std::move(repaired_its))))
return false; return false;
} else { } else {
BOOST_LOG_TRIVIAL(warning) << log_prefix << ": Windows 3D mesh repair failed: " << repair_error; BOOST_LOG_TRIVIAL(warning) << log_prefix << ": mesh repair failed: " << repair_error;
} }
} else { } else {
BOOST_LOG_TRIVIAL(info) << log_prefix << ": importing mesh without Windows 3D repair."; BOOST_LOG_TRIVIAL(info) << log_prefix << ": importing mesh without repair.";
} }
} }
} }
+1 -4
View File
@@ -1,5 +1,4 @@
#ifndef libslic3r_Timer_hpp_ #pragma once
#define libslic3r_Timer_hpp_
#include "TriangleMesh.hpp" #include "TriangleMesh.hpp"
@@ -10,5 +9,3 @@ public:
static TriangleMesh smooth_triangle_mesh(const TriangleMesh &mesh,bool& ok); static TriangleMesh smooth_triangle_mesh(const TriangleMesh &mesh,bool& ok);
}; };
} // namespace Slic3r } // namespace Slic3r
#endif // libslic3r_Timer_hpp_
-1
View File
@@ -109,7 +109,6 @@ extern std::string format_memsize_MB(size_t n);
extern std::string format_memsize(size_t bytes, unsigned int decimals = 1); extern std::string format_memsize(size_t bytes, unsigned int decimals = 1);
// Return string to be added to the boost::log output to inform about the current process memory allocation. // Return string to be added to the boost::log output to inform about the current process memory allocation.
// The string is non-empty if the loglevel >= info (3) or ignore_loglevel==true. // The string is non-empty if the loglevel >= info (3) or ignore_loglevel==true.
// Latter is used to get the memory info from SysInfoDialog.
extern std::string log_memory_info(bool ignore_loglevel = false); extern std::string log_memory_info(bool ignore_loglevel = false);
extern void disable_multi_threading(); extern void disable_multi_threading();
// Returns the size of physical memory (RAM) in bytes. // Returns the size of physical memory (RAM) in bytes.
+1 -3
View File
@@ -1744,9 +1744,7 @@ std::string format_diameter_to_str(double diameter, int precision)
return oss.str(); return oss.str();
} }
// Returns platform-specific string to be used as log output or parsed in SysInfoDialog. // Returns platform-specific string to be used as log output.
// The latter parses the string with (semi)colons as separators, it should look about as
// "desc1: value1; desc2: value2" or similar (spaces should not matter).
std::string log_memory_info(bool ignore_loglevel) std::string log_memory_info(bool ignore_loglevel)
{ {
std::string out; std::string out;
+6 -4
View File
@@ -21,7 +21,9 @@ namespace libvgcode {
// the triangles that share it: the shader runs 8 times per segment instead of 24. // 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, 8> VERTEX_DATA = { 0, 1, 2, 3, 4, 5, 6, 7 };
static constexpr const std::array<uint8_t, 24> INDEX_DATA = { // 16-bit indices: GCN 1/2 GPUs have no 8-bit index support, and some of their drivers
// emulate it wrongly in instanced draws.
static constexpr const std::array<uint16_t, 24> INDEX_DATA = {
0, 1, 2, // front spike 0, 1, 2, // front spike
0, 2, 3, // front spike 0, 2, 3, // front spike
0, 3, 4, // right/bottom body 0, 3, 4, // right/bottom body
@@ -37,7 +39,7 @@ void SegmentTemplate::init()
if (m_vao_id != 0) if (m_vao_id != 0)
return; return;
m_size_in_bytes_gpu += (VERTEX_DATA.size() + INDEX_DATA.size()) * sizeof(uint8_t); m_size_in_bytes_gpu += VERTEX_DATA.size() * sizeof(uint8_t) + INDEX_DATA.size() * sizeof(uint16_t);
int curr_vertex_array; int curr_vertex_array;
glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array)); glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array));
@@ -61,7 +63,7 @@ void SegmentTemplate::init()
// and restored together with the vao. // and restored together with the vao.
glsafe(glGenBuffers(1, &m_ibo_id)); glsafe(glGenBuffers(1, &m_ibo_id));
glsafe(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 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(glBufferData(GL_ELEMENT_ARRAY_BUFFER, INDEX_DATA.size() * sizeof(uint16_t), INDEX_DATA.data(), GL_STATIC_DRAW));
glsafe(glBindBuffer(GL_ARRAY_BUFFER, curr_array_buffer)); glsafe(glBindBuffer(GL_ARRAY_BUFFER, curr_array_buffer));
glsafe(glBindVertexArray(curr_vertex_array)); glsafe(glBindVertexArray(curr_vertex_array));
@@ -94,7 +96,7 @@ void SegmentTemplate::render(size_t count)
glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array)); glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array));
glsafe(glBindVertexArray(m_vao_id)); glsafe(glBindVertexArray(m_vao_id));
glsafe(glDrawElementsInstanced(GL_TRIANGLES, static_cast<GLsizei>(INDEX_DATA.size()), GL_UNSIGNED_BYTE, glsafe(glDrawElementsInstanced(GL_TRIANGLES, static_cast<GLsizei>(INDEX_DATA.size()), GL_UNSIGNED_SHORT,
nullptr, static_cast<GLsizei>(count))); nullptr, static_cast<GLsizei>(count)));
glsafe(glBindVertexArray(curr_vertex_array)); glsafe(glBindVertexArray(curr_vertex_array));
} }
+2 -3
View File
@@ -186,6 +186,8 @@ set(SLIC3R_GUI_SOURCES
GUI/Gizmos/GLGizmoFlatten.hpp GUI/Gizmos/GLGizmoFlatten.hpp
GUI/Gizmos/GLGizmoFuzzySkin.cpp GUI/Gizmos/GLGizmoFuzzySkin.cpp
GUI/Gizmos/GLGizmoFuzzySkin.hpp GUI/Gizmos/GLGizmoFuzzySkin.hpp
GUI/Gizmos/GLGizmoImage.cpp
GUI/Gizmos/GLGizmoImage.hpp
GUI/Gizmos/GLGizmoMeasure.cpp GUI/Gizmos/GLGizmoMeasure.cpp
GUI/Gizmos/GLGizmoMeasure.hpp GUI/Gizmos/GLGizmoMeasure.hpp
GUI/Gizmos/GLGizmoMeshBoolean.cpp GUI/Gizmos/GLGizmoMeshBoolean.cpp
@@ -416,8 +418,6 @@ set(SLIC3R_GUI_SOURCES
GUI/MultiSendMachineModel.hpp GUI/MultiSendMachineModel.hpp
GUI/MultiTaskManagerPage.cpp GUI/MultiTaskManagerPage.cpp
GUI/MultiTaskManagerPage.hpp GUI/MultiTaskManagerPage.hpp
GUI/MultiTaskModel.cpp
GUI/MultiTaskModel.hpp
GUI/NetworkTestDialog.cpp GUI/NetworkTestDialog.cpp
GUI/NetworkTestDialog.hpp GUI/NetworkTestDialog.hpp
GUI/Notebook.cpp GUI/Notebook.cpp
@@ -802,7 +802,6 @@ set(SLIC3R_GUI_SOURCES
Utils/PrintHost.hpp Utils/PrintHost.hpp
Utils/Process.cpp Utils/Process.cpp
Utils/Process.hpp Utils/Process.hpp
Utils/Profile.hpp
Utils/RaycastManager.cpp Utils/RaycastManager.cpp
Utils/RaycastManager.hpp Utils/RaycastManager.hpp
Utils/Repetier.cpp Utils/Repetier.cpp
+1 -4
View File
@@ -1,5 +1,4 @@
#ifndef slic3r_GUI_SingleChoice_hpp_ #pragma once
#define slic3r_GUI_SingleChoice_hpp_
#include "GUI_Utils.hpp" #include "GUI_Utils.hpp"
#include "Plater.hpp" #include "Plater.hpp"
@@ -33,5 +32,3 @@ private:
}; };
}} // namespace Slic3r::GUI }} // namespace Slic3r::GUI
#endif
-19
View File
@@ -1,19 +0,0 @@
#include <exception>
#include <stdexcept>
#include "libslic3r/Exception.hpp"
namespace Slic3r {
class ConfigError : public Slic3r::RuntimeError {
using Slic3r::RuntimeError::RuntimeError;
};
namespace GUI {
class ConfigGUITypeError : public ConfigError {
using ConfigError::ConfigError;
};
} // namespace GUI
} // namespace Slic3r
+4
View File
@@ -1105,6 +1105,10 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
toggle_field("support_filament", have_support_material || have_skirt); toggle_field("support_filament", have_support_material || have_skirt);
toggle_line("raft_contact_distance", have_raft && !have_support_soluble); toggle_line("raft_contact_distance", have_raft && !have_support_soluble);
// Orca:Hide raft expansion when raft is not enabled
toggle_line("raft_expansion", have_raft);
// Orca: Slim, Strong and Hybrid tree supports size their raft by first layer expansion only.
toggle_field("raft_expansion", !(is_tree(support_type) && (support_style == smsTreeSlim || support_style == smsTreeStrong || support_style == smsTreeHybrid)));
// Orca: First-layer density is available for supports broadly. // Orca: First-layer density is available for supports broadly.
toggle_field("raft_first_layer_density", have_support_material); toggle_field("raft_first_layer_density", have_support_material);
+2 -3
View File
@@ -25,9 +25,8 @@ DockPanel::DockPanel(wxWindow* parent,
const std::string& html, const std::string& html,
MessageHandler on_message, MessageHandler on_message,
CloseHandler on_close, CloseHandler on_close,
CloseHandler on_destroyed, CloseHandler on_destroyed)
const wxString& profile) : WebPanel(parent, web_hosting::orca_bridge_script())
: WebPanel(parent, web_hosting::orca_bridge_script(), profile)
, m_html(html) , m_html(html)
, m_on_message(std::move(on_message)) , m_on_message(std::move(on_message))
, m_on_close(std::move(on_close)) , m_on_close(std::move(on_close))
+1 -4
View File
@@ -6,8 +6,6 @@
#include <optional> #include <optional>
#include <string> #include <string>
#include <wx/string.h>
class wxWindow; class wxWindow;
namespace Slic3r { namespace GUI { namespace Slic3r { namespace GUI {
@@ -29,8 +27,7 @@ public:
const std::string& html, const std::string& html,
MessageHandler on_message, MessageHandler on_message,
CloseHandler on_close, CloseHandler on_close,
CloseHandler on_destroyed, CloseHandler on_destroyed);
const wxString& profile = wxEmptyString);
~DockPanel() override; ~DockPanel() override;
// Main thread only. // Main thread only.
-4
View File
@@ -719,9 +719,6 @@ void GLTexture::render_sub_texture(unsigned int tex_id, float left, float right,
// does not treat anti-aliased icon edges as window transparency. // does not treat anti-aliased icon edges as window transparency.
glsafe(::glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA)); glsafe(::glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA));
glsafe(::glEnable(GL_TEXTURE_2D));
glsafe(::glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE));
glsafe(::glBindTexture(GL_TEXTURE_2D, (GLuint)tex_id)); glsafe(::glBindTexture(GL_TEXTURE_2D, (GLuint)tex_id));
GLModel::Geometry init_data; GLModel::Geometry init_data;
@@ -753,7 +750,6 @@ void GLTexture::render_sub_texture(unsigned int tex_id, float left, float right,
glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); glsafe(::glBindTexture(GL_TEXTURE_2D, 0));
glsafe(::glDisable(GL_TEXTURE_2D));
glsafe(::glDisable(GL_BLEND)); glsafe(::glDisable(GL_BLEND));
} }
-6
View File
@@ -4862,12 +4862,6 @@ void GUI_App::recreate_GUI(const wxString &msg_name)
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "recreate_GUI exit"; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "recreate_GUI exit";
} }
void GUI_App::system_info()
{
//SysInfoDialog dlg;
//dlg.ShowModal();
}
void GUI_App::keyboard_shortcuts(ShortcutContext page, wxWindow* parent) void GUI_App::keyboard_shortcuts(ShortcutContext page, wxWindow* parent)
{ {
KBShortcutsDialog dlg(parent != nullptr ? parent : mainframe, page); KBShortcutsDialog dlg(parent != nullptr ? parent : mainframe, page);
-1
View File
@@ -519,7 +519,6 @@ public:
void check_printer_presets(); void check_printer_presets();
void recreate_GUI(const wxString& message); void recreate_GUI(const wxString& message);
void system_info();
void keyboard_shortcuts(ShortcutContext page, wxWindow* parent = nullptr); // the main frame when null void keyboard_shortcuts(ShortcutContext page, wxWindow* parent = nullptr); // the main frame when null
void troubleshoot(); void troubleshoot();
void load_project(wxWindow *parent, wxString& input_file) const; void load_project(wxWindow *parent, wxString& input_file) const;
+10
View File
@@ -1952,6 +1952,16 @@ void MenuFactory::create_plate_menu()
}, },
"", nullptr, []() { return !plater()->get_partplate_list().get_selected_plate()->get_objects().empty(); }, m_parent); "", nullptr, []() { return !plater()->get_partplate_list().get_selected_plate()->get_objects().empty(); }, m_parent);
// fix objects on current plate
append_menu_item(menu, wxID_ANY, _L("Fix All"), _L("Fix all models on the current plate"),
[](wxCommandEvent&) { obj_list()->fix_all_through_cgal(false); }, "", nullptr,
[]() { return !plater()->get_partplate_list().get_selected_plate()->empty(); }, m_parent);
// fix objects on all plates
append_menu_item(menu, wxID_ANY, _L("Fix All Plates"), _L("Fix all models on all plates"),
[](wxCommandEvent&) { obj_list()->fix_all_through_cgal(true); }, "", nullptr,
[]() { return !plater()->model().objects.empty(); }, m_parent);
// orient objects on current plate // orient objects on current plate
append_menu_item(menu, wxID_ANY, _L("Auto Rotate"), _L("Auto rotate current plate"), append_menu_item(menu, wxID_ANY, _L("Auto Rotate"), _L("Auto rotate current plate"),
[](wxCommandEvent&) { [](wxCommandEvent&) {
+63 -53
View File
@@ -6297,30 +6297,44 @@ void ObjectList::rename_item()
} }
void ObjectList::fix_through_cgal() void ObjectList::fix_through_cgal()
{
std::vector<int> obj_idxs, vol_idxs;
get_selection_indexes(obj_idxs, vol_idxs);
fix_through_cgal(obj_idxs, vol_idxs);
}
void ObjectList::fix_all_through_cgal(bool all_plates)
{
std::vector<int> obj_idxs;
if (all_plates) {
for (int obj_idx = 0; obj_idx < int(m_objects->size()); ++obj_idx)
obj_idxs.push_back(obj_idx);
} else {
for (const ModelObject *mo : wxGetApp().plater()->get_partplate_list().get_selected_plate()->get_objects_on_this_plate())
obj_idxs.push_back(int(std::find(m_objects->begin(), m_objects->end(), mo) - m_objects->begin()));
std::sort(obj_idxs.begin(), obj_idxs.end());
obj_idxs.erase(std::unique(obj_idxs.begin(), obj_idxs.end()), obj_idxs.end());
}
fix_through_cgal(obj_idxs, {});
}
void ObjectList::fix_through_cgal(const std::vector<int> &obj_idxs, const std::vector<int> &vol_idxs)
{ {
// Do not fix anything when a gizmo is open. There might be issues with updates // Do not fix anything when a gizmo is open. There might be issues with updates
// and what is worse, the snapshot time would refer to the internal stack. // and what is worse, the snapshot time would refer to the internal stack.
if (!wxGetApp().plater()->get_view3D_canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::Undefined)) if (obj_idxs.empty() || !wxGetApp().plater()->get_view3D_canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::Undefined))
return; return;
// model_name // model_name
std::vector<std::string> succes_models; std::vector<std::string> succes_models;
// model_name failing reason // model_name failing reason
std::vector<std::pair<std::string, std::string>> failed_models; std::vector<std::pair<std::string, std::string>> failed_models;
size_t unchanged_models = 0;
std::vector<int> obj_idxs, vol_idxs; bool painting_removed = false;
get_selection_indexes(obj_idxs, vol_idxs);
std::vector<std::string> model_names; std::vector<std::string> model_names;
// clear selections from the non-broken models if any exists
// and than fill names of models to repairing
if (vol_idxs.empty()) { if (vol_idxs.empty()) {
#if !FIX_THROUGH_CGAL_ALWAYS
for (int i = int(obj_idxs.size())-1; i >= 0; --i)
if (object(obj_idxs[i])->get_repaired_errors_count() == 0)
obj_idxs.erase(obj_idxs.begin()+i);
#endif // FIX_THROUGH_CGAL_ALWAYS
for (int obj_idx : obj_idxs) for (int obj_idx : obj_idxs)
if (object(obj_idx)) if (object(obj_idx))
model_names.push_back(object(obj_idx)->name); model_names.push_back(object(obj_idx)->name);
@@ -6328,31 +6342,26 @@ void ObjectList::fix_through_cgal()
else { else {
ModelObject* obj = object(obj_idxs.front()); ModelObject* obj = object(obj_idxs.front());
if (obj) { if (obj) {
#if !FIX_THROUGH_CGAL_ALWAYS
for (int i = int(vol_idxs.size()) - 1; i >= 0; --i)
if (obj->get_repaired_errors_count(vol_idxs[i]) == 0)
vol_idxs.erase(vol_idxs.begin() + i);
#endif // FIX_THROUGH_CGAL_ALWAYS
for (int vol_idx : vol_idxs) for (int vol_idx : vol_idxs)
model_names.push_back(obj->volumes[vol_idx]->name); model_names.push_back(obj->volumes[vol_idx]->name);
} }
} }
auto plater = wxGetApp().plater(); auto plater = wxGetApp().plater();
const bool keep_painting = GUI::wxGetApp().app_config->get_bool("keep_painting");
auto fix_and_update_progress = [this, plater, model_names](const int obj_idx, const int vol_idx, auto fix_and_update_progress = [&](const int obj_idx, const int vol_idx, int model_idx, ProgressDialog& progress_dlg)
int model_idx,
ProgressDialog& progress_dlg,
std::vector<std::string>& succes_models,
std::vector<std::pair<std::string, std::string>>& failed_models)
{ {
if (!object(obj_idx)) ModelObject *mo = object(obj_idx);
if (!mo)
return false; return false;
const std::string& model_name = model_names[model_idx]; const std::string& model_name = model_names[model_idx];
wxString msg = _L("Repairing model object"); wxString msg = _L("Repairing model object");
if (model_names.size() == 1) if (model_names.size() == 1)
msg += ": " + from_u8(model_name) + "\n"; msg += ": " + from_u8(model_name) + "\n";
else if (model_names.size() > 10)
msg += ": " + from_u8(model_name) + wxString::Format(" (%d/%d)", model_idx + 1, int(model_names.size())) + "\n";
else { else {
msg += ":\n"; msg += ":\n";
for (int i = 0; i < int(model_names.size()); ++i) for (int i = 0; i < int(model_names.size()); ++i)
@@ -6360,33 +6369,29 @@ void ObjectList::fix_through_cgal()
msg += "\n"; msg += "\n";
} }
const bool keep_painting = GUI::wxGetApp().app_config->get_bool("keep_painting"); auto count_painted = [mo]() { return std::count_if(mo->volumes.begin(), mo->volumes.end(), [](const ModelVolume *mv) { return mv->is_any_painted(); }); };
if (!keep_painting) { const auto painted_before = count_painted();
plater->clear_before_change_mesh(obj_idx);
}
const size_t volumes_before = object(obj_idx)->volumes.size();
std::string res; std::string res;
if (!fix_model_with_cgal_gui(*(object(obj_idx)), vol_idx, progress_dlg, msg, res, keep_painting)) bool modified = false;
if (!fix_model_with_cgal_gui(*mo, vol_idx, progress_dlg, msg, res, keep_painting, &modified))
return false; return false;
//wxGetApp().plater()->changed_mesh(obj_idx);
object(obj_idx)->ensure_on_bed(); if (modified) {
painting_removed = painting_removed || count_painted() < painted_before;
mo->ensure_on_bed();
plater->changed_mesh(obj_idx); plater->changed_mesh(obj_idx);
const size_t volumes_after = object(obj_idx)->volumes.size();
if (volumes_after != volumes_before)
add_volumes_to_object_in_list(obj_idx);
plater->get_partplate_list().notify_instance_update(obj_idx, 0); plater->get_partplate_list().notify_instance_update(obj_idx, 0);
plater->sidebar().obj_list()->update_plate_values_for_items(); plater->sidebar().obj_list()->update_plate_values_for_items();
if (res.empty())
succes_models.push_back(model_name);
else
failed_models.push_back({ model_name, res });
update_item_error_icon(obj_idx, vol_idx); update_item_error_icon(obj_idx, vol_idx);
update_info_items(obj_idx); update_info_items(obj_idx);
}
if (!res.empty())
failed_models.push_back({ model_name, res });
else if (modified)
succes_models.push_back(model_name);
else
++unchanged_models;
return true; return true;
}; };
@@ -6399,11 +6404,7 @@ void ObjectList::fix_through_cgal()
if (vol_idxs.empty()) { if (vol_idxs.empty()) {
int vol_idx{ -1 }; int vol_idx{ -1 };
for (int obj_idx : obj_idxs) { for (int obj_idx : obj_idxs) {
#if !FIX_THROUGH_CGAL_ALWAYS if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg))
if (object(obj_idx)->get_repaired_errors_count(vol_idx) == 0)
continue;
#endif // FIX_THROUGH_CGAL_ALWAYS
if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg, succes_models, failed_models))
break; break;
model_idx++; model_idx++;
} }
@@ -6411,7 +6412,7 @@ void ObjectList::fix_through_cgal()
else { else {
int obj_idx{ obj_idxs.front() }; int obj_idx{ obj_idxs.front() };
for (int vol_idx : vol_idxs) { for (int vol_idx : vol_idxs) {
if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg, succes_models, failed_models)) if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg))
break; break;
model_idx++; model_idx++;
} }
@@ -6419,20 +6420,29 @@ void ObjectList::fix_through_cgal()
// Close the progress dialog // Close the progress dialog
progress_dlg.Update(100, ""); progress_dlg.Update(100, "");
if (painting_removed)
plater->notify_painting_removed();
// Show info notification // Show info notification
wxString msg; std::vector<wxString> blocks;
wxString bullet_suf = "\n - "; wxString bullet_suf = "\n - ";
if (!succes_models.empty()) { if (!succes_models.empty()) {
msg = _L_PLURAL("The following model object has been repaired", "The following model objects have been repaired", succes_models.size()) + ":"; wxString block = _L_PLURAL("The following model object has been repaired", "The following model objects have been repaired", succes_models.size()) + ":";
for (auto& model : succes_models) for (auto& model : succes_models)
msg += bullet_suf + from_u8(model); block += bullet_suf + from_u8(model);
msg += "\n\n"; blocks.push_back(block);
} }
if (!failed_models.empty()) { if (!failed_models.empty()) {
msg += _L_PLURAL("Failed to repair the following model object", "Failed to repair the following model objects", failed_models.size()) + ":\n"; wxString block = _L_PLURAL("Failed to repair the following model object", "Failed to repair the following model objects", failed_models.size()) + ":";
for (auto& model : failed_models) for (auto& model : failed_models)
msg += bullet_suf + from_u8(model.first) + ": " + _(model.second); block += bullet_suf + from_u8(model.first) + ": " + _(model.second);
blocks.push_back(block);
} }
if (unchanged_models > 0)
blocks.push_back(format_wxstr(_L_PLURAL("%1$d model object needed no repair.", "%1$d model objects needed no repair.", unchanged_models), unchanged_models));
wxString msg;
for (const wxString &block : blocks)
msg += (msg.IsEmpty() ? "" : "\n\n") + block;
if (msg.IsEmpty()) if (msg.IsEmpty())
msg = _L("Repairing was canceled"); msg = _L("Repairing was canceled");
plater->get_notification_manager()->push_notification(NotificationType::CgalFinished, NotificationManager::NotificationLevel::PrintInfoShortNotificationLevel, into_u8(msg)); plater->get_notification_manager()->push_notification(NotificationType::CgalFinished, NotificationManager::NotificationLevel::PrintInfoShortNotificationLevel, into_u8(msg));
+3 -3
View File
@@ -47,9 +47,6 @@ typedef double coordf_t;
typedef std::pair<coordf_t, coordf_t> t_layer_height_range; typedef std::pair<coordf_t, coordf_t> t_layer_height_range;
typedef std::map<t_layer_height_range, ModelConfig> t_layer_config_ranges; typedef std::map<t_layer_height_range, ModelConfig> t_layer_config_ranges;
// Manifold mesh may contain self-intersections, so we want to always allow fixing the mesh.
#define FIX_THROUGH_CGAL_ALWAYS 1
namespace GUI { namespace GUI {
enum class Shortcut : uint8_t; enum class Shortcut : uint8_t;
@@ -463,6 +460,9 @@ public:
void split_instances(); void split_instances();
void rename_item(); void rename_item();
void fix_through_cgal(); void fix_through_cgal();
// Fixes every object on the selected plate, or on all plates.
void fix_all_through_cgal(bool all_plates);
void fix_through_cgal(const std::vector<int> &obj_idxs, const std::vector<int> &vol_idxs);
void simplify(); void simplify();
void smooth_mesh(); void smooth_mesh();
void update_item_error_icon(const int obj_idx, int vol_idx) const ; void update_item_error_icon(const int obj_idx, int vol_idx) const ;
+891
View File
@@ -0,0 +1,891 @@
#include "GLGizmoImage.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "libslic3r/format.hpp"
#include "slic3r/GUI/Camera.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GLTexture.hpp"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/IconManager.hpp"
#include "slic3r/GUI/ImGuiWrapper.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/TextureLibrary.hpp"
#include "slic3r/GUI/Shortcuts.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoUtils.hpp"
#include "slic3r/GUI/Jobs/Worker.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <imgui.h>
#include <optional>
#include <string>
#include <vector>
#include <wx/string.h>
namespace Slic3r::GUI {
GLGizmoImage::GLGizmoImage(GLCanvas3D &parent, const std::string &icon_filename, unsigned int sprite_id)
: GLGizmoTextureDisplacement(parent, icon_filename, sprite_id)
{
}
bool GLGizmoImage::on_init()
{
if (!GLGizmoTextureDisplacement::on_init())
return false;
// Everything the base set up stays - it is the same painting machinery - except the key that
// opens it, which is this tool's own.
m_shortcut = Shortcut::GizmoImage;
return true;
}
std::string GLGizmoImage::on_get_name() const
{
return _u8L("Image");
}
wxString GLGizmoImage::handle_snapshot_action_name(bool shift_down, GLGizmoPainterBase::Button button_down) const
{
// Same paint, same masks; only the undo step's wording follows the tool the user is in.
const bool erasing = shift_down || (m_erase_mode != (button_down == GLGizmoPainterBase::Button::Right));
return erasing ? _L("Remove image from the area") : _L("Place image");
}
bool GLGizmoTextureDisplacement::pick_background_at(const Vec2d &mouse_pos)
{
ModelVolume *mv = texture_volume();
ModelObject *mo = m_c->selection_info()->model_object();
TextureDisplacementLayer *layer = mv != nullptr ? active_layer() : nullptr;
if (mv == nullptr || mo == nullptr || layer == nullptr || layer->empty())
return false;
const int idx = texture_volume_raycaster_index();
if (idx < 0)
return false;
const Selection &selection = m_parent.get_selection();
const Transform3d trafo = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix();
const Camera &camera = wxGetApp().plater()->get_camera();
Vec3f hit = Vec3f::Zero(), normal = Vec3f::Zero();
if (!m_c->raycaster()->raycasters()[size_t(idx)]->unproject_on_mesh(
mouse_pos, trafo, camera, hit, normal,
m_c->object_clipper()->get_clipping_plane()))
return false;
// The picture's own colour at that point, read through the projection the bake uses, rather
// than the pixel on screen: the screen pixel carries the lighting and the filament the colour
// was already quantized to, so keying on it would remove the wrong thing.
//
// Uncorrected, deliberately. The key is applied at the end of the correction chain, against
// colours the earlier passes have already moved, so it has to be stated in those terms - the
// same reason the panel's dropper reads the corrected preview.
const indexed_triangle_set &base = mv->mesh().its;
const indexed_triangle_set patch =
extract_painted_patch(base, mv->texture_displacement_facet(layer->slot).get_data());
Vec3f center = Vec3f::Zero(), axis = Vec3f::UnitZ(), average_normal = Vec3f::UnitZ();
if (!patch.indices.empty())
texture_displacement_patch_frame(patch, texture_displacement_vertex_normals(base), center, axis,
average_normal);
DecodedHeightTexture tex = decode_height_texture(*layer);
apply_color_adjustments(tex, *layer);
Vec3f rgb;
if (!sample_layer_color(tex, *layer, hit, normal, rgb, center, axis))
return false;
for (int c = 0; c < 3; ++c)
layer->bg_color[c] = std::clamp(rgb[c], 0.f, 1.f);
layer->bg_from_corners = false;
layer->bg_remove = true;
m_image_bg_picking = false;
m_preview_params_dirty = true;
m_parent.set_as_dirty();
return true;
}
void GLGizmoTextureDisplacement::set_file_drag_over(bool over)
{
if (!image_mode() || m_file_drag_over == over)
return;
m_file_drag_over = over;
// A drag is not a mouse move as far as the canvas is concerned, so nothing would repaint and the
// zone would appear only once something else happened to trigger a frame.
m_parent.set_as_dirty();
m_parent.request_extra_frame();
}
// Drawn over the whole canvas on the foreground list, from inside the panel's ImGui frame, which is
// what puts it above the 3D view and above the panel itself.
void GLGizmoTextureDisplacement::render_image_drop_hint()
{
if (!m_file_drag_over)
return;
const ImVec2 size = ImGui::GetIO().DisplaySize;
if (size.x < 1.f || size.y < 1.f)
return;
ImDrawList *dl = ImGui::GetForegroundDrawList();
const ImU32 teal = ImGui::GetColorU32(ImGuiWrapper::COL_ORCA);
// Dim the view so the zone reads as a state the canvas is in, not as decoration on top of it.
dl->AddRectFilled(ImVec2(0.f, 0.f), size, IM_COL32(0, 0, 0, 70));
const float inset = std::round(ImGui::GetFontSize() * 1.2f);
const ImVec2 a(inset, inset), b(size.x - inset, size.y - inset);
if (b.x <= a.x || b.y <= a.y)
return;
// A dashed border, drawn as runs along the four edges: ImGui 1.83 has no dashed stroke.
const float dash = std::round(ImGui::GetFontSize() * 0.75f);
const float gap = dash;
const float th = std::max(2.f, std::round(ImGui::GetFontSize() * 0.12f));
for (float x = a.x; x < b.x; x += dash + gap) {
const float x1 = std::min(x + dash, b.x);
dl->AddLine(ImVec2(x, a.y), ImVec2(x1, a.y), teal, th);
dl->AddLine(ImVec2(x, b.y), ImVec2(x1, b.y), teal, th);
}
for (float y = a.y; y < b.y; y += dash + gap) {
const float y1 = std::min(y + dash, b.y);
dl->AddLine(ImVec2(a.x, y), ImVec2(a.x, y1), teal, th);
dl->AddLine(ImVec2(b.x, y), ImVec2(b.x, y1), teal, th);
}
// What dropping here will do. Worded as an invitation rather than a promise: anything that is
// not a picture falls through to the model loader, exactly as it did before this tool existed.
const std::string text = _u8L("Drop a picture to put it on the model");
const ImVec2 ts = ImGui::CalcTextSize(text.c_str());
const float pad = std::round(ImGui::GetFontSize() * 0.8f);
const ImVec2 c((a.x + b.x) * 0.5f, (a.y + b.y) * 0.5f);
const ImVec2 r0(c.x - ts.x * 0.5f - pad, c.y - ts.y * 0.5f - pad * 0.6f);
const ImVec2 r1(c.x + ts.x * 0.5f + pad, c.y + ts.y * 0.5f + pad * 0.6f);
dl->AddRectFilled(r0, r1, ImGui::GetColorU32(ImGuiCol_WindowBg, 0.92f), ImGui::GetStyle().FrameRounding);
dl->AddRect(r0, r1, teal, ImGui::GetStyle().FrameRounding, 0, th * 0.5f);
dl->AddText(ImVec2(c.x - ts.x * 0.5f, c.y - ts.y * 0.5f), ImGui::GetColorU32(ImGuiCol_Text), text.c_str());
}
bool GLGizmoTextureDisplacement::drop_image_file(const std::string &path)
{
if (!image_mode() || texture_volume() == nullptr)
return false;
// Imported rather than just read: the same thing the panel's Import tile does, so a dropped
// picture lands in "My images" and is there the next time without hunting for the file again.
std::string error;
const std::optional<TextureLibraryEntry> entry = import_texture_to_library(path, error);
if (!entry) {
show_error(nullptr, error);
return false;
}
// The tool starts with no layer at all, so a drop before anything is loaded has to make one.
if (active_layer() == nullptr)
add_texture_layer();
TextureDisplacementLayer *layer = active_layer();
if (layer == nullptr)
return false;
set_layer_texture(*layer, *entry);
m_parent.set_as_dirty();
return true;
}
// ---------------------------------------------------------------------------------------------
// The Image tool's window.
//
// A member of GLGizmoTextureDisplacement rather than of GLGizmoImage, and defined here rather than
// in that class's own file: it needs the same private machinery the displacement panel uses (the
// layer, the texture picker, the previews, the bake), and keeping it here leaves that 1700 line
// panel untouched instead of threading it with tests for which tool is drawing.
//
// What it deliberately leaves out, all of which only means something once the surface moves or once
// there is more than one picture: brushes and the erase toggle, the layer stack, blend modes, depth,
// midlevel, edge fade, tiling mode, the unwrap projection and its UV editor, seams, the wireframe
// and the mesh-preparation sections.
// ---------------------------------------------------------------------------------------------
void GLGizmoTextureDisplacement::render_image_input_window(float x, float y, float bottom_limit)
{
ModelObject *mo = m_c->selection_info()->model_object();
if (mo == nullptr)
return;
ModelVolume *mv = texture_volume();
// The palette follows project state nobody tells this gizmo about (the filaments in the
// sidebar), so it is checked once a frame and a change re-runs the preview.
cached_palette();
if (m_palette_changed)
m_preview_params_dirty = true;
// Everything this tool fixes, re-pinned each frame so what the preview shows is what Apply will
// produce: Standard's recipe, zero depth with colour on, a click that floods the connected
// surface rather than a brush, and the fast preview, which with no relief to build is both
// instant and exactly the final colour.
// Two statements, not one `||`: short-circuiting would skip the image presets on any frame the
// standard ones changed something.
const bool std_changed = apply_standard_mode_presets(mv);
const bool image_changed = apply_image_mode_presets(mv);
if (std_changed || image_changed)
m_preview_params_dirty = true;
m_tool_type = ToolType::SMART_FILL;
m_cursor_type = TriangleSelector::CursorType::POINTER;
m_erase_mode = false;
m_auto_update = true;
if (!m_use_shaded_preview)
apply_view_mode(1);
float scale = m_parent.get_scale();
#ifdef WIN32
const int dpi = get_dpi_for_window(wxGetApp().GetTopWindow());
scale *= (float) dpi / (float) DPI_DEFAULT;
#endif // WIN32
const float approx_height = m_imgui->scaled(22.f);
y = std::min(y, bottom_limit - approx_height);
// Docked (the default) it is pinned to the right edge of the 3D canvas and cannot be moved, the
// same place and the same rule as the displacement panel, so the two tools do not put their
// controls in different corners. Undocked it becomes an ordinary floating window with a title
// bar to drag it by; the position is deliberately not seeded on undock, so it stays exactly
// where it was and the user simply gains the ability to move it.
ImGuiWindowFlags flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoCollapse;
if (!m_undocked) {
flags |= ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar;
// Right-aligned (pivot 1), so the width the window auto-resized to last frame does not need
// to be known here.
float right = float(m_parent.get_canvas_size().get_width()) - m_imgui->scaled(0.5f);
GizmoImguiSetNextWIndowPos(right, y, 0.f, 0.f, ImGuiCond_Always, 1.0f, 0.0f);
}
ImGuiWrapper::push_toolbar_style(m_parent.get_scale());
GizmoImguiBegin(get_name(), flags);
ensure_panel_icons();
const float previous_body_h = m_panel_body_h;
const float previous_footer_h = m_panel_footer_h;
const ImGuiStyle &style = ImGui::GetStyle();
const bool busy = m_bake_in_progress || m_prepare_in_progress;
const float panel_w = m_imgui->scaled(16.f);
const float frame_h = ImGui::GetFrameHeight();
const float icon_sm = std::round(frame_h * 1.05f);
const float gap_s = std::round(m_imgui->scaled(0.4f));
const float label_w = m_imgui->scaled(4.6f);
const float wrap_w = m_imgui->scaled(15.f);
const ImVec4 orca = ImGuiWrapper::COL_ORCA;
const ImVec4 col_frame = ImGui::GetStyleColorVec4(ImGuiCol_Separator);
const auto hover_tip = [&](const auto &text) {
if (ImGui::IsItemHovered())
m_imgui->tooltip(text, wrap_w);
};
// Label column plus a slider filling the rest of the row, the same shape the displacement panel
// uses, so the two tools read as one family.
const auto float_row = [&](const char *id, const wxString &label, float *v, float v_min, float v_max,
const char *format, bool log) -> bool {
const float x0 = ImGui::GetCursorPosX();
ImGui::AlignTextToFramePadding();
m_imgui->text(label);
ImGui::SameLine();
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX());
return ImGui::SliderFloat(id, v, v_min, v_max, format,
ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0));
};
// Borderless icon button, for the dock toggle: always the grey monochrome variant.
const auto icon_button = [&](int uid, const std::string &iconfile, const wxString &label,
const wxString &tip_text) -> bool {
const auto it = m_panel_icon_map.find(iconfile);
const float pad = std::max(1.f, std::round(icon_sm * 0.14f));
bool clicked = false;
ImGui::PushID(uid);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.f);
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.f, 0.f, 0.f, 0.f));
if (it != m_panel_icon_map.end() && !it->second.empty() && it->second[0]->is_valid()) {
const IconManager::Icon &ic = *it->second[0];
clicked = m_imgui->image_button((ImTextureID) (intptr_t) ic.tex_id,
ImVec2(icon_sm - 2.f * pad, icon_sm - 2.f * pad), ic.tl, ic.br, int(pad));
} else {
clicked = ImGui::Button(label.ToUTF8().data(), ImVec2(0.f, icon_sm));
}
ImGui::PopStyleColor();
ImGui::PopStyleVar();
ImGui::PopID();
hover_tip(tip_text);
return clicked;
};
// A section header that folds its contents away. ImGui's own CollapsingHeader draws a filled bar
// that is louder than anything else in this window, so this is the same triangle-and-text the
// displacement panel's "More settings" link uses, at heading weight.
const auto section = [&](const char *id, const wxString &label, bool &open) {
const float arrow = std::round(ImGui::GetFontSize() * 0.55f);
const ImVec2 ts = ImGui::CalcTextSize(label.ToUTF8().data());
const ImVec2 p = ImGui::GetCursorScreenPos();
ImGui::PushID(id);
if (ImGui::InvisibleButton("##sec", ImVec2(panel_w, ts.y + gap_s)))
open = !open;
const bool hot = ImGui::IsItemHovered();
ImDrawList *dl = ImGui::GetWindowDrawList();
const ImU32 col = ImGui::GetColorU32(hot ? ImGuiCol_Text : ImGuiCol_TextDisabled);
const float cx = p.x + 0.5f * arrow, cy = p.y + 0.5f * ts.y, hs = 0.32f * arrow;
if (open)
dl->AddTriangleFilled(ImVec2(cx - hs, cy - 0.5f * hs), ImVec2(cx + hs, cy - 0.5f * hs),
ImVec2(cx, cy + 0.5f * hs), col);
else
dl->AddTriangleFilled(ImVec2(cx - 0.5f * hs, cy - hs), ImVec2(cx - 0.5f * hs, cy + hs),
ImVec2(cx + 0.5f * hs, cy), col);
dl->AddText(ImVec2(p.x + arrow + gap_s, p.y), col, label.ToUTF8().data());
ImGui::PopID();
return open;
};
// The integer twin of float_row, for the colour count.
const auto int_row = [&](const char *id, const wxString &label, int *v, int v_min, int v_max,
const char *format) -> bool {
const float x0 = ImGui::GetCursorPosX();
ImGui::AlignTextToFramePadding();
m_imgui->text(label);
ImGui::SameLine();
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX());
return ImGui::SliderInt(id, v, v_min, v_max, format, ImGuiSliderFlags_AlwaysClamp);
};
// Framed icon toggle, the displacement panel's one cut down to what this window needs.
const auto icon_toggle = [&](int uid, const std::string &iconfile, bool active, const wxString &label,
const wxString &tip_text) -> bool {
const auto it = m_panel_icon_map.find(iconfile);
const float pad = std::max(1.f, std::round(icon_sm * 0.14f));
bool clicked = false;
ImGui::PushID(uid);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.f);
ImGui::PushStyleColor(ImGuiCol_Button, active ? ImVec4(orca.x, orca.y, orca.z, 0.22f) : ImVec4(0.f, 0.f, 0.f, 0.f));
if (it != m_panel_icon_map.end() && int(it->second.size()) > (active ? 1 : 0) &&
it->second[active ? 1 : 0]->is_valid()) {
const IconManager::Icon &ic = *it->second[active ? 1 : 0];
clicked = m_imgui->image_button((ImTextureID) (intptr_t) ic.tex_id,
ImVec2(icon_sm - 2.f * pad, icon_sm - 2.f * pad), ic.tl, ic.br, int(pad));
} else {
clicked = ImGui::Button(label.ToUTF8().data(), ImVec2(0.f, icon_sm));
}
ImGui::GetWindowDrawList()->AddRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(),
ImGui::GetColorU32(active ? orca : col_frame), style.FrameRounding);
ImGui::PopStyleColor();
ImGui::PopStyleVar();
ImGui::PopID();
hover_tip(tip_text);
return clicked;
};
TextureDisplacementLayer *layer = mv != nullptr ? active_layer() : nullptr;
{
const float x0 = ImGui::GetCursorPosX();
ImGui::AlignTextToFramePadding();
m_imgui->push_bold_font();
m_imgui->text(wxString::FromUTF8(get_name(false)));
m_imgui->pop_bold_font();
ImGui::SameLine();
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + panel_w - icon_sm));
if (icon_button(920, "canvas_drag.svg", m_undocked ? _L("Dock panel") : _L("Undock panel"),
_L("Detach this panel so it can be dragged anywhere over the 3D view, or dock it back "
"against the right edge.")))
m_undocked = !m_undocked;
}
ImGui::Separator();
// Everything between the title and the footer scrolls as one, so Apply and Close stay on screen
// on a short canvas instead of being the first thing pushed off the bottom. Sized from last
// frame's content and footer heights, as the displacement panel's body is.
const float bottom = m_undocked ? ImGui::GetIO().DisplaySize.y : bottom_limit;
const float max_body_h = std::max(m_imgui->scaled(8.f), bottom - ImGui::GetCursorScreenPos().y -
m_panel_footer_h - style.WindowPadding.y -
style.ItemSpacing.y);
const float body_h = m_panel_body_h > 0.f ? std::min(m_panel_body_h + 1.f, max_body_h) : max_body_h;
const float scrollbar_w = m_imgui->scaled(0.5f);
const ImVec4 grab = wxGetApp().dark_mode() ? ImVec4(1.f, 1.f, 1.f, 0.26f) : ImVec4(0.f, 0.f, 0.f, 0.26f);
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarSize, scrollbar_w);
ImGui::PushStyleVar(ImGuiStyleVar_ScrollbarRounding, 0.5f * scrollbar_w);
ImGui::PushStyleColor(ImGuiCol_ScrollbarBg, ImVec4(0.f, 0.f, 0.f, 0.f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, grab);
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, ImVec4(grab.x, grab.y, grab.z, 0.45f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, ImVec4(grab.x, grab.y, grab.z, 0.65f));
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.f, 0.f, 0.f, 0.f));
ImGui::BeginChild("##image_body", ImVec2(panel_w, body_h), false);
// The picture, shown whole and the full width of the window: it is the thing being placed, so a
// row-sized thumbnail beside a file name is the wrong size for it. Clicking it opens the library,
// which is also where Import lives, and before anything is loaded the same box is an empty frame
// with a plus in it - one target, in the same place, whichever state the tool is in.
const auto open_library = [&](int slot) {
m_picker_slot = slot;
m_picker_open_request = true;
};
// A layer with no picture on it is the starting state, not a broken one: the tool creates the
// layer empty and the panel stays on the frame below until a picture has been chosen.
const bool has_picture = layer != nullptr && !layer->empty();
const bool placed = mv != nullptr && mv->is_texture_displacement_painted();
// The handle comes up by itself the first moment there is something to anchor it to, so the
// picture can be nudged straight after the click that placed it without hunting for a toggle.
// Armed once: turning it back off has to stay off, not be undone on the next frame.
if (has_picture && placed && !m_image_handle_armed) {
m_image_handle_armed = true;
if (update_adjust_anchor())
m_adjust_texture_mode = true;
}
if (!has_picture) {
const float box_h = std::round(panel_w * 0.6f);
const ImVec2 p0 = ImGui::GetCursorScreenPos();
m_imgui->disabled_begin(busy || mv == nullptr);
const bool clicked = ImGui::InvisibleButton("##load", ImVec2(panel_w, box_h));
m_imgui->disabled_end();
const bool hot = ImGui::IsItemHovered();
ImDrawList *dl = ImGui::GetWindowDrawList();
const ImU32 col = ImGui::GetColorU32(hot ? orca : col_frame);
dl->AddRect(p0, ImVec2(p0.x + panel_w, p0.y + box_h), col, style.FrameRounding);
const float cx = p0.x + 0.5f * panel_w, cy = p0.y + 0.5f * box_h;
const float arm = std::round(box_h * 0.18f), thick = std::max(1.f, std::round(box_h * 0.035f));
dl->AddLine(ImVec2(cx - arm, cy), ImVec2(cx + arm, cy), col, thick);
dl->AddLine(ImVec2(cx, cy - arm), ImVec2(cx, cy + arm), col, thick);
hover_tip(_u8L("Choose a picture to put on the model."));
if (clicked) {
if (layer == nullptr)
add_texture_layer(); // created with no picture on it, on purpose
open_library(layer != nullptr ? layer->slot : m_active_layer_slot);
}
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + panel_w);
ImGui::TextDisabled("%s", _u8L("Choose a picture, then click the model to put it there.").c_str());
ImGui::PopTextWrapPos();
} else {
// The colour texture is the corrected image at full resolution, which is exactly what should
// be previewed - move a correction slider and the box follows. A grayscale picture has no
// colour texture, so it falls back to the height thumbnail.
GLTexture *shown = get_layer_color_texture(*layer);
if (shown == nullptr)
shown = get_layer_thumbnail(*layer);
const float aspect = std::max(0.05f, layer_texture_aspect(*layer));
const float box_h = std::clamp(std::round(panel_w / aspect), std::round(panel_w * 0.35f),
std::round(panel_w * 1.1f));
const ImVec2 box_min = ImGui::GetCursorScreenPos();
bool box_hit = false;
if (shown != nullptr) {
box_hit = ImGui::ImageButton((ImTextureID) (intptr_t) shown->get_id(), ImVec2(panel_w, box_h),
ImVec2(0.f, 0.f), ImVec2(1.f, 1.f), 0);
} else {
box_hit = ImGui::Button("?##thumb", ImVec2(panel_w, box_h));
}
hover_tip(m_image_bg_picking ?
_u8L("Click the background, here or on the model.") :
_u8L("Choose a different picture. Where you placed it and how it is corrected stay as "
"they are."));
if (box_hit) {
if (!m_image_bg_picking) {
open_library(layer->slot);
} else {
// Read straight out of the decoded image rather than off the screen: the preview is
// scaled, and it already has the corrections applied, so a screen read would key the
// corrected colour against itself.
const ImVec2 rel((ImGui::GetIO().MousePos.x - box_min.x) / panel_w,
(ImGui::GetIO().MousePos.y - box_min.y) / box_h);
const DecodedHeightTexture &tex = decode_height_texture(*layer);
if (tex.has_color()) {
const int px = std::clamp(int(rel.x * float(tex.width)), 0, tex.width - 1);
const int py = std::clamp(int(rel.y * float(tex.height)), 0, tex.height - 1);
const size_t i = (size_t(py) * size_t(tex.width) + size_t(px)) * 3;
for (int c = 0; c < 3; ++c)
layer->bg_color[c] = float(tex.rgb[i + size_t(c)]) / 255.f;
layer->bg_from_corners = false;
layer->bg_remove = true;
m_preview_params_dirty = true;
}
m_image_bg_picking = false;
}
}
const std::string name = layer->name.empty() ? _u8L("Image") : layer->name;
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + panel_w);
ImGui::TextDisabled("%s", name.c_str());
ImGui::PopTextWrapPos();
if (!height_texture_has_color(*layer)) {
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + panel_w);
m_imgui->warning_text(_L("This image is grayscale, so it has no colors to print. Choose a color picture."));
ImGui::PopTextWrapPos();
}
// Placing it: one click floods the surface it lands on, out to the first fold sharper than
// the angle below. No brush, no erase toggle - on a flat side or a cube face the click alone
// is the whole gesture, and the slider is there for the cases where it is not.
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + panel_w);
ImGui::TextDisabled("%s", placed ? _u8L("Click the model to move the picture somewhere else.").c_str() :
_u8L("Click the model to put the picture there.").c_str());
ImGui::PopTextWrapPos();
float fill_angle = m_smart_fill_angle;
if (float_row("##fill_angle", _L("Spread"), &fill_angle, SmartFillAngleMin, SmartFillAngleMax, "%.0f°", false))
m_smart_fill_angle = fill_angle;
hover_tip(_u8L("How far a click spreads from the spot you hit: it keeps going while the surface stays "
"flatter than this, and stops at a sharper fold. Small values keep it to one face."));
ImGui::Separator();
if (section("place", _L("Placement"), m_image_sec_place)) {
// Projection, minus Unwrap: that one is an editor's worth of UI (seams, islands, the UV pane)
// for a tool whose whole point is a click and a few sliders.
{
struct Mapping { const char *file; wxString label; wxString tip; TextureProjectionMethod method; };
const Mapping mappings[] = {
{ "menu_obj_cube.svg", _L("Flat"),
_L("Flat - lays the picture straight onto the surface from whichever axis it faces, and blends "
"where it wraps around an edge. The right choice for a face or a flat side."),
TextureProjectionMethod::Triplanar },
{ "menu_obj_cylinder.svg", _L("Around"),
_L("Around - wraps the picture around the area, for a round part like a mug or a tube."),
TextureProjectionMethod::Cylindrical },
{ "menu_obj_sphere.svg", _L("Ball"),
_L("Ball - wraps the picture around the area in both directions, for a dome or a sphere."),
TextureProjectionMethod::Spherical },
{ "texture_displacement_map_view.svg", _L("From view"),
_L("From view - throws the picture onto the area from where you are looking now, like a slide "
"projector. Line the view up with the surface first, because faces turned away stretch."),
TextureProjectionMethod::ViewProjected },
};
const float x0 = ImGui::GetCursorPosX();
ImGui::AlignTextToFramePadding();
// Plain text, like every other label in this window: the displacement panel greys its
// headings because it has many, and here there are five rows in total.
m_imgui->text(_L_CONTEXT("Mapping", "Texture Displacement"));
ImGui::SameLine();
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
for (int mi = 0; mi < int(IM_ARRAYSIZE(mappings)); ++mi) {
if (mi > 0)
ImGui::SameLine(0.f, gap_s);
const bool on = layer->projection_method == mappings[mi].method;
if (icon_toggle(900 + mi, mappings[mi].file, on, mappings[mi].label, mappings[mi].tip) && !on) {
// Capture the current view the moment "From view" is chosen, so it does something
// sensible immediately rather than projecting from a stale direction.
if (mappings[mi].method == TextureProjectionMethod::ViewProjected)
capture_view_projection(*layer);
layer->projection_method = mappings[mi].method;
m_preview_params_dirty = true;
}
}
}
if (layer->projection_method == TextureProjectionMethod::ViewProjected) {
if (m_imgui->button(_u8L("Capture current view"))) {
capture_view_projection(*layer);
m_preview_params_dirty = true;
}
hover_tip(_u8L("Throws the picture on again from where you are looking now. Orbit the model, press "
"this, and it is re-laid from the new angle."));
}
m_preview_params_dirty |= float_row("##size", _L("Size"), &layer->tiling_scale, 1.f, 300.f, "%.1f mm", true);
hover_tip(_u8L("How wide the picture is on the model, in millimetres."));
m_preview_params_dirty |= float_row("##rotation", _L("Rotation"), &layer->rotation_deg, 0.f, 360.f, "%.0f°", false);
hover_tip(_u8L("Turns the picture on the surface, for lining it up with an edge of the model."));
// Sliding it into place, and whether it repeats. Both belong on one row: the handle is the
// coarse control and Tile decides what it is sliding - one copy, or a pattern.
{
const float x0 = ImGui::GetCursorPosX();
ImGui::AlignTextToFramePadding();
m_imgui->text(_L("Move"));
ImGui::SameLine();
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
if (icon_toggle(910, "texture_displacement_adjust.svg", m_adjust_texture_mode, _L("Move on the model"),
_L("Move on the model - drag the handle that appears on the surface to slide the "
"picture around, or one of its two arrows to slide it along just that axis."))) {
if (!m_adjust_texture_mode) {
update_model_object(); // flush any pending placement before anchoring to it
if (update_adjust_anchor())
m_adjust_texture_mode = true;
else
show_error(nullptr, _u8L("Click the model to put the picture somewhere first."));
} else {
m_adjust_texture_mode = false;
m_adjust_drag_handle = AdjustHandle::None;
}
}
ImGui::SameLine(0.f, gap_s * 2.f);
if (m_imgui->bbl_checkbox(_L("Tile"), layer->tile_enabled))
m_preview_params_dirty = true;
hover_tip(_u8L("Repeats the picture over the area instead of placing a single copy. Off by "
"default, because a photo or a logo goes on once; Size then sets how big that "
"one copy is."));
}
// The one number that decides how sharp the printed picture is. Each triangle prints in a
// single filament, so the mesh under the paint has to be finer than the detail wanted out of
// the image - and the budget above is what stops that running away.
{
TextureDisplacementOptions &opts = mv->texture_displacement_options;
float edge = opts.v2_refine_mm;
m_imgui->disabled_begin(busy);
if (float_row("##resolution", _L("Detail"), &edge, 0.02f, 2.f, "%.2f mm", true)) {
opts.v2_refine_mm = edge;
m_preview_params_dirty = true;
}
m_imgui->disabled_end();
hover_tip(_u8L("How fine the mesh is made under the picture, in millimetres. Each triangle prints "
"in one filament, so this is how sharp an edge between two colors can come out. "
"Smaller costs triangles fast: halving it needs four times as many."));
// Refining past the budget is not an error - the bake simplifies back down to it - but the
// result then carries less of the picture than this slider asks for, and without saying so
// the only sign is a print that came out softer than expected.
const size_t budget = size_t(std::max(0, opts.v2_max_triangles_k)) * 1000;
const size_t needed = estimated_refined_triangles(*mv, edge);
if (budget > 0 && needed > budget * 5 / 4) {
const auto count = [](size_t n) {
return n >= 1000000 ? Slic3r::format("%1$.1f M", double(n) / 1000000.) :
Slic3r::format("%1% k", (n + 500) / 1000);
};
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + panel_w);
m_imgui->warning_text(Slic3r::format(_u8L("Needs about %1% triangles, budget %2% - it will be "
"simplified back down and come out softer."),
count(needed), count(budget)));
ImGui::PopTextWrapPos();
}
}
} // Placement
ImGui::Separator();
if (section("color", _L("Color"), m_image_sec_color)) {
// Colour correction. Every one of these changes which filament each area lands in, which is
// why they are here rather than left to whatever the file happened to contain: a photo that
// prints as mud usually only needs contrast.
m_preview_params_dirty |= float_row("##brightness", _L("Brightness"), &layer->brightness, -1.f, 1.f, "%.2f", false);
hover_tip(_u8L("Lightens or darkens the whole picture before it is matched to your filaments."));
m_preview_params_dirty |= float_row("##contrast", _L("Contrast"), &layer->contrast, -1.f, 1.f, "%.2f", false);
hover_tip(_u8L("Pushes light and dark apart. Raise it when a photo prints as mud; lower it when "
"everything collapses into the lightest and darkest filament."));
m_preview_params_dirty |= float_row("##saturation", _L("Saturation"), &layer->saturation, 0.f, 2.f, "%.2f", false);
hover_tip(_u8L("How strong the colors are. At 0 the picture is gray, so it prints in your grays and "
"blacks; raising it pushes each area towards a purer filament."));
m_preview_params_dirty |= float_row("##hue", _L("Hue"), &layer->hue, -180.f, 180.f, "%.0f°", false);
hover_tip(_u8L("Turns every color of the picture around the color wheel, without changing how light "
"it is. Your filaments are a fixed, arbitrary set, so this is what lets the picture "
"meet them: if you have orange loaded but no red, a small turn puts the reds on it."));
m_preview_params_dirty |= float_row("##gamma", _L("Gamma"), &layer->gamma, 0.2f, 5.f, "%.2f", true);
hover_tip(_u8L("Lifts or drops the midtones without touching the brightest and darkest parts. Use it "
"when the middle of the picture is the part coming out wrong."));
m_preview_params_dirty |= float_row("##sharpen", _L("Sharpen"), &layer->sharpness, 0.f, 1.f, "%.2f", false);
hover_tip(_u8L("Crisps up the edges before the picture is matched to your filaments. Worth more here "
"than in a photo: a soft edge between two colors quantizes into a ragged band of "
"both, and sharpening first makes it land in one place."));
// How many colours the picture may use. 2 is a logo in two flat colours, 4 or 5 a poster,
// off a photograph. This is not the same as the displacement tool's "Mixed colors", which
// caps how many mixed filament slots a bake may create rather than what the picture uses.
{
int count = layer->color_count;
if (int_row("##colors", _L("Colors"), &count, 0, 12, count == 0 ? _u8L("Off").c_str() : "%d")) {
// 1 is not a picture, so the slider steps over it on the way out of Off.
layer->color_count = count == 1 ? 2 : count;
m_preview_params_dirty = true;
}
hover_tip(_u8L("Reduces the picture to this many colors before your filaments are matched to "
"it, so it prints in at most that many. The colors are the ones the picture is "
"actually made of, not an even split of the range. Off leaves it alone."));
}
if (m_imgui->bbl_checkbox(_L("Negative"), layer->color_invert))
m_preview_params_dirty = true;
hover_tip(_u8L("Prints the picture inverted, light for dark and each color for its opposite."));
} // Color
ImGui::Separator();
if (!section("bg", _L("Background"), m_image_sec_bg)) {
// Folded away, but still worth saying whether it is doing anything.
ImGui::SameLine();
ImGui::TextDisabled("%s", layer->bg_remove ? _u8L("removed").c_str() : _u8L("kept").c_str());
} else {
if (m_imgui->bbl_checkbox(_L("Remove background"), layer->bg_remove))
m_preview_params_dirty = true;
hover_tip(_u8L("Leaves the background unprinted, so the object's own color shows through instead of "
"a slab of white behind a logo. A picture that is already transparent needs none of "
"this: those parts are left out either way."));
if (layer->bg_remove) {
// Which colour is being removed, shown rather than described: the swatch is also a
// picker, and the dropper beside it takes the colour from the preview above.
const float x0 = ImGui::GetCursorPosX();
ImGui::AlignTextToFramePadding();
m_imgui->text(_L("Color"));
ImGui::SameLine();
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
float shown_bg[3] = { layer->bg_color[0], layer->bg_color[1], layer->bg_color[2] };
if (layer->bg_from_corners) {
// Nothing has been picked, so show what the corners actually gave: the same median
// the bake will key on, not a placeholder white.
const DecodedHeightTexture &tex = decode_height_texture(*layer);
if (tex.has_color() && tex.width > 1 && tex.height > 1) {
const size_t n = size_t(tex.width) * size_t(tex.height);
const size_t corner[4] = { 0, size_t(tex.width) - 1,
(size_t(tex.height) - 1) * size_t(tex.width), n - 1 };
for (int c = 0; c < 3; ++c) {
std::array<float, 4> v{};
for (int i = 0; i < 4; ++i)
v[size_t(i)] = float(tex.rgb[corner[i] * 3 + size_t(c)]);
std::sort(v.begin(), v.end());
shown_bg[c] = 0.5f * (v[1] + v[2]) / 255.f;
}
}
}
ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX() - icon_sm - gap_s);
if (ImGui::ColorEdit3("##bg_color", shown_bg,
ImGuiColorEditFlags_NoInputs | ImGuiColorEditFlags_NoLabel)) {
for (int c = 0; c < 3; ++c)
layer->bg_color[c] = shown_bg[c];
layer->bg_from_corners = false;
m_preview_params_dirty = true;
}
hover_tip(layer->bg_from_corners ?
_u8L("Taken from the four corners of the picture, which is right for a logo on a "
"plain card. Click to choose a color yourself instead.") :
_u8L("The color being removed. Click to change it."));
ImGui::SameLine(0.f, gap_s);
if (icon_toggle(930, "texture_displacement_pipette.svg", m_image_bg_picking, _L("Pick the color"),
_L("Pick the color - then click the background, either in the preview above or on "
"the model itself.")))
m_image_bg_picking = !m_image_bg_picking;
if (!layer->bg_from_corners) {
ImGui::SameLine(0.f, gap_s);
if (m_imgui->button(_u8L("Corners"))) {
layer->bg_from_corners = true;
m_preview_params_dirty = true;
}
hover_tip(_u8L("Back to taking the color from the picture's four corners."));
}
m_preview_params_dirty |= float_row("##bg_tol", _L("Tolerance"), &layer->bg_tolerance, 0.01f, 0.6f, "%.2f", false);
hover_tip(_u8L("How different from that color still counts as background. Raise it when a "
"shadow or a gradient is left behind; lower it when it starts eating the picture."));
}
} // Background
// Outside both sections, because it clears the colour corrections and the background key
// alike, and a reset that lives inside one of them would look like it only reached that far.
ImGui::Separator();
if (m_imgui->button(_u8L("Reset all"))) {
layer->brightness = 0.f;
layer->contrast = 0.f;
layer->saturation = 1.f;
layer->hue = 0.f;
layer->gamma = 1.f;
layer->sharpness = 0.f;
layer->color_count = 0;
layer->bg_remove = false;
layer->bg_tolerance = 0.15f;
layer->bg_from_corners = true;
layer->bg_color[0] = layer->bg_color[1] = layer->bg_color[2] = 1.f;
m_image_bg_picking = false;
layer->color_invert = false;
m_preview_params_dirty = true;
}
hover_tip(_u8L("Puts every correction and the background back to where they started, leaving the "
"picture as it was loaded. Where you placed it and how big it is are not touched."));
}
// The library popup lives in this window's scope, where its open request and the popup itself
// share one ID stack.
{
const ImVec2 win_min = ImGui::GetWindowPos();
const ImVec2 win_sz = ImGui::GetWindowSize();
render_texture_library_popup(win_min, ImVec2(win_min.x + win_sz.x, win_min.y + win_sz.y));
}
// ImGui sliders report "changed" on every frame of a drag, and a preview rebuild is real work,
// so with the mouse still down it waits for the release.
if (m_preview_params_dirty && !ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
rebuild_preview();
m_preview_params_dirty = false;
}
m_panel_body_h = ImGui::GetCursorPosY() - style.ItemSpacing.y;
ImGui::EndChild();
ImGui::PopStyleColor(5);
ImGui::PopStyleVar(2);
const float footer_top = ImGui::GetCursorScreenPos().y;
ImGui::Separator();
{
const bool painted = placed;
const bool no_colors = mv != nullptr && !any_layer_colors(*mv);
// Colour is the only thing this bake produces, so with a grayscale picture there would be
// nothing to show for it - and it is not a cheap no-op: the bake still refines the painted
// area and replaces the mesh with the result.
const bool can_apply = !busy && mv != nullptr && painted && !no_colors;
const float button_h = std::round(frame_h * 1.25f);
const float button_w = std::max({ ImGui::CalcTextSize(_u8L("Apply").c_str()).x,
ImGui::CalcTextSize(_u8L("Close").c_str()).x,
ImGui::CalcTextSize(_u8L("Stop").c_str()).x,
ImGui::CalcTextSize(_u8L("Working...").c_str()).x }) +
m_imgui->scaled(0.5f);
const float row_y = ImGui::GetCursorPosY();
const float icon_h = 21.f * scale;
const float row_h = std::max(icon_h, button_h);
ImGui::SetCursorPosY(row_y + (row_h - icon_h) * .5f);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(int(m_imgui->scaled(.5f)), style.ItemSpacing.y));
GLGizmoUtils::render_wiki_guide_button(m_parent, scale,
"https://www.orcaslicer.com/wiki/print_prepare/prepare_texture_displacement");
ImGui::PopStyleVar(1);
ImGui::SameLine(panel_w - button_w * 2 - style.ItemSpacing.x);
GLGizmoUtils::push_orca_button_style();
m_imgui->push_bold_font();
m_imgui->disabled_begin(!can_apply);
ImGui::SetCursorPosY(row_y + (row_h - button_h) * .5f);
const std::string apply_label = busy ? _u8L("Working...") : _u8L("Apply");
if (ImGui::Button((apply_label + "##apply").c_str(), ImVec2(button_w, button_h)))
bake_standard();
m_imgui->disabled_end();
m_imgui->pop_bold_font();
GLGizmoUtils::pop_orca_button_style();
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
m_imgui->tooltip(!has_picture ? _u8L("Choose a picture first.") :
!painted ? _u8L("Click the model to place the picture first.") :
no_colors ? _u8L("This picture is grayscale, so there is nothing to print. "
"Choose a color picture.") :
_u8L("Prints the picture where you placed it, in the nearest of your "
"loaded filaments. The surface itself is left exactly where it "
"is: this adds color, not relief."),
wrap_w);
ImGui::SameLine();
if (busy) {
if (ImGui::Button((_u8L("Stop") + "##stop").c_str(), ImVec2(button_w, button_h)))
wxGetApp().plater()->get_ui_job_worker().cancel_all();
hover_tip(_u8L("Stops it. Whatever it had already finished stays on the model, and can be undone."));
} else {
if (ImGui::Button((_u8L("Close") + "##close").c_str(), ImVec2(button_w, button_h)))
m_parent.reset_all_gizmos();
hover_tip(_u8L("Closes the tool. Your picture and where you put it stay with the model."));
}
}
m_panel_footer_h = ImGui::GetCursorScreenPos().y - footer_top;
// Both heights feed back into next frame's layout, so the window needs one more frame whenever
// either of them moved.
if (std::abs(m_panel_body_h - previous_body_h) > 0.5f || std::abs(m_panel_footer_h - previous_footer_h) > 0.5f)
m_parent.request_extra_frame();
// Last, on the foreground list, so it covers the panel as well as the view.
render_image_drop_hint();
GizmoImguiEnd();
ImGuiWrapper::pop_toolbar_style();
}
} // namespace Slic3r::GUI
+46
View File
@@ -0,0 +1,46 @@
#ifndef slic3r_GLGizmoImage_hpp_
#define slic3r_GLGizmoImage_hpp_
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
#include "slic3r/GUI/I18N.hpp"
#include <string>
#include <wx/string.h>
namespace Slic3r::GUI {
class GLCanvas3D;
// "Put this picture on the model": the texture-displacement gizmo with the relief taken out of it.
//
// There is no second engine here and no copy of one. It is GLGizmoTextureDisplacement with
// image_mode() on, which pins every layer to zero depth and colour on (see
// apply_image_mode_presets()) and leaves out the controls that only mean something once the surface
// moves. Painting, the layer stack, the texture library, the previews and the bake are the very same
// code, reached the very same way - a zero-depth bake refines the painted area and writes the
// colours exactly as a relief bake does, it just displaces by nothing.
//
// Its own toolbar button because the two are different jobs, not two settings of one: a user who
// wants a photo on a part should not have to know what a height map is, or find the one slider that
// has to be zero.
class GLGizmoImage : public GLGizmoTextureDisplacement
{
public:
GLGizmoImage(GLCanvas3D &parent, const std::string &icon_filename, unsigned int sprite_id);
protected:
bool image_mode() const override { return true; }
bool on_init() override;
std::string on_get_name() const override;
wxString handle_snapshot_action_name(bool shift_down, Button button_down) const override;
std::string get_gizmo_entering_text() const override { return _u8L("Entering Image painting"); }
std::string get_gizmo_leaving_text() const override { return _u8L("Leaving Image painting"); }
std::string get_action_snapshot_name() const override { return _u8L("Image editing"); }
};
} // namespace Slic3r::GUI
#endif // slic3r_GLGizmoImage_hpp_
@@ -1036,6 +1036,11 @@ bool GLGizmoPainterBase::on_mouse(const wxMouseEvent &mouse_event)
Vec2d mouse_pos = mouse_coord.cast<double>(); Vec2d mouse_pos = mouse_coord.cast<double>();
if (mouse_event.Moving()) { if (mouse_event.Moving()) {
// No button is down, yet a stroke is still open: its release went elsewhere, as a paint drag does not
// capture the mouse. End the stroke as that release would have.
if (m_button_down != Button::None)
gizmo_event(m_button_down == Button::Left ? SLAGizmoEventType::LeftUp : SLAGizmoEventType::RightUp, mouse_pos,
mouse_event.ShiftDown(), mouse_event.AltDown(), false);
gizmo_event(SLAGizmoEventType::Moving, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), false); gizmo_event(SLAGizmoEventType::Moving, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), false);
return false; return false;
} }
@@ -1205,6 +1210,10 @@ void GLGizmoPainterBase::on_set_state()
m_old_mo_id = -1; m_old_mo_id = -1;
//m_iva.release_geometry(); //m_iva.release_geometry();
m_triangle_selectors.clear(); m_triangle_selectors.clear();
// A stroke still open goes with the selectors. Its state must not carry into the next session, where the
// first move would end it as if released and the first press would paint a line from its last position.
m_button_down = Button::None;
m_last_mouse_click = Vec2d::Zero();
//Camera& camera = wxGetApp().plater()->get_camera(); //Camera& camera = wxGetApp().plater()->get_camera();
//camera.look_at(camera.get_position(), m_previous_target, Vec3d::UnitZ()); //camera.look_at(camera.get_position(), m_previous_target, Vec3d::UnitZ());
File diff suppressed because it is too large Load Diff
@@ -154,6 +154,33 @@ public:
// What the cache was built from. // What the cache was built from.
std::vector<ColorRGBA> m_palette_filaments; std::vector<ColorRGBA> m_palette_filaments;
std::vector<std::shared_ptr<std::vector<unsigned char>>> m_palette_images; // the colouring layers' images std::vector<std::shared_ptr<std::vector<unsigned char>>> m_palette_images; // the colouring layers' images
// One layer's colour correction, as a value that can be compared and cached on. It belongs to
// every cache keyed on "what the image looks like", because a correction slider changes the
// pixels while leaving the image_data pointer alone.
struct ColorAdjustKey
{
float brightness = 0.f, contrast = 0.f, saturation = 1.f, gamma = 1.f, hue = 0.f;
float sharpness = 0.f, bg_tolerance = 0.15f;
float bg_r = 1.f, bg_g = 1.f, bg_b = 1.f;
int color_count = 0;
bool invert = false, bg_remove = false, bg_from_corners = true;
bool operator==(const ColorAdjustKey &o) const
{
return brightness == o.brightness && contrast == o.contrast && saturation == o.saturation &&
gamma == o.gamma && hue == o.hue && sharpness == o.sharpness &&
bg_tolerance == o.bg_tolerance && bg_r == o.bg_r && bg_g == o.bg_g && bg_b == o.bg_b &&
color_count == o.color_count && invert == o.invert && bg_remove == o.bg_remove &&
bg_from_corners == o.bg_from_corners;
}
bool operator!=(const ColorAdjustKey &o) const { return !(*this == o); }
static ColorAdjustKey of(const TextureDisplacementLayer &l)
{
return { l.brightness, l.contrast, l.saturation, l.gamma, l.hue,
l.sharpness, l.bg_tolerance, l.bg_color[0], l.bg_color[1], l.bg_color[2],
l.color_count, l.color_invert, l.bg_remove, l.bg_from_corners };
}
};
std::vector<ColorAdjustKey> m_palette_adjusts; // alongside m_palette_images, one per entry
std::optional<std::vector<PaletteEntry>> m_mix_ranking; // rank_mixes(), computed on demand std::optional<std::vector<PaletteEntry>> m_mix_ranking; // rank_mixes(), computed on demand
bool m_palette_mixing = false; bool m_palette_mixing = false;
int m_palette_mix_count = 0; int m_palette_mix_count = 0;
@@ -197,13 +224,52 @@ public:
}; };
static PaintedColors painted_colors(const TriangleMesh &mesh, const TriangleSelector::TriangleSplittingData &paint); static PaintedColors painted_colors(const TriangleMesh &mesh, const TriangleSelector::TriangleSplittingData &paint);
// The view while the UV editor's selected islands are highlighted on the model. The highlight lies on the
// undisplaced surface, so the Normal view's relief hides it wherever it rises: Normal gives way to Fast
// while islands are selected and comes back once none are. A view the user clicks in the View row is
// theirs - the pane's background buttons are not a pick, Height only meaning "the texture view".
struct IslandViewFollow
{
bool shown = false; // the highlight, as of the last step()
bool switched = false; // Fast was switched on for the highlight, so Normal comes back once it is gone
bool keep_normal = false; // the user picked Normal in the View row while the highlight was shown
// The view to apply this frame (0 Normal, 1 Fast), or -1 for none. `view` is the one on screen, numbered as
// apply_view_mode() takes it; `debug_open` a debug stage on screen, which holds Normal itself.
int step(bool shown_now, int view, bool debug_open);
// A View row button clicked, the active one included. The row is drawn after step() in the same frame,
// so `shown` is this frame's.
void picked(int view)
{
switched = false;
keep_normal = shown && view == 0;
}
};
void render_painter_gizmo() override; void render_painter_gizmo() override;
// Intercepts mouse input while "Adjust Texture" mode is on (dragging the on-canvas offset/ // Intercepts mouse input while "Adjust Texture" mode is on (dragging the on-canvas offset/
// rotation handles instead of painting); otherwise forwards to the normal painting handling. // rotation handles instead of painting); otherwise forwards to the normal painting handling.
bool on_mouse(const wxMouseEvent &mouse_event) override; bool on_mouse(const wxMouseEvent &mouse_event) override;
// Loads `path` as the Image tool's picture, importing it into the user's library on the way so
// it is there next time. For a drop onto the 3D view; see PlaterDropTarget::OnDropFiles(). False
// when this is not the Image tool, or the file could not be read.
bool drop_image_file(const std::string &path);
// Called while a file is being dragged over the 3D view, so the Image tool can show that
// dropping it there will do something. Without it the drag has no feedback at all and the only
// way to find out is to let go.
void set_file_drag_over(bool over);
protected: protected:
// True in the Image tool (see GLGizmoImage): this same gizmo restricted to the one case where
// the surface does not move at all - a colour picture laid on the model. Everything behind the
// panel is shared and unchanged: the same layers, the same paint masks, the same bake. This flag
// only tells the panel which of the two tools it is drawing, and makes it pin the values the
// restricted case fixes (see apply_image_mode_presets()).
virtual bool image_mode() const { return false; }
bool on_init() override;
void on_render_input_window(float x, float y, float bottom_limit) override; void on_render_input_window(float x, float y, float bottom_limit) override;
std::string on_get_name() const override; std::string on_get_name() const override;
// Never in the assemble view: its toolbar does not offer this gizmo, and every preview here is drawn // Never in the assemble view: its toolbar does not offer this gizmo, and every preview here is drawn
@@ -221,8 +287,11 @@ protected:
EnforcerBlockerType get_left_button_state_type() const override { return m_erase_mode ? EnforcerBlockerType::NONE : EnforcerBlockerType::ENFORCER; } EnforcerBlockerType get_left_button_state_type() const override { return m_erase_mode ? EnforcerBlockerType::NONE : EnforcerBlockerType::ENFORCER; }
EnforcerBlockerType get_right_button_state_type() const override { return m_erase_mode ? EnforcerBlockerType::ENFORCER : EnforcerBlockerType::NONE; } EnforcerBlockerType get_right_button_state_type() const override { return m_erase_mode ? EnforcerBlockerType::ENFORCER : EnforcerBlockerType::NONE; }
// Paint / Erase, from the panel. See get_left_button_state_type(). Protected because a subclass
// overriding handle_snapshot_action_name() has to read the same flag to name the undo step.
bool m_erase_mode = false;
private: private:
bool on_init() override;
void update_model_object() override; void update_model_object() override;
void update_from_model_object(bool first_update) override; void update_from_model_object(bool first_update) override;
void on_opening() override {} void on_opening() override {}
@@ -267,6 +336,39 @@ private:
// existing vertices, so the mesh has to be prepared first, and remeshing after painting would drop // existing vertices, so the mesh has to be prepared first, and remeshing after painting would drop
// the paint if it were not carried across (see prepare_mesh()). // the paint if it were not carried across (see prepare_mesh()).
void bake_standard(); void bake_standard();
// Copies the panel's own state (active layer, Standard/Pro) onto the volume, so the project file
// can carry it. Cheap and idempotent - called wherever either of the two changes.
void store_panel_state();
// Image mode's fixed values, on top of Standard's and pinned the same way (every frame, so the
// preview cannot disagree with the bake): zero depth on every layer, and colour on wherever the
// picture has any. Those two are what turn the displacement bake into "put this picture on the
// model" without a second code path. Returns true if it actually changed something.
bool apply_image_mode_presets(ModelVolume *mv);
// The Image tool's whole window: load a picture, click the model to place it, correct it, Apply.
// Defined in GLGizmoImage.cpp, beside the class it exists for, rather than here - it is a member
// only so that it can reach the same private machinery the displacement panel uses, and keeping
// it out of this file leaves that panel's 1700 lines alone.
void render_image_input_window(float x, float y, float bottom_limit);
// Set once the Image tool has switched its placement handle on for the current picture, so that
// turning the handle back off stays off instead of being re-armed on the next frame.
bool m_image_handle_armed = false;
// Set between a drag entering the 3D view and leaving or dropping on it; drives the drop zone
// render_image_input_window() draws over the canvas.
bool m_file_drag_over = false;
void render_image_drop_hint();
// Set while the panel's background dropper is armed: the next click on the picture preview takes
// the colour under it instead of opening the library.
bool m_image_bg_picking = false;
// Takes the background colour from the point of the model under `mouse_pos`: the picture's own
// colour there, read through the same projection the bake uses, not the lit pixel on screen.
// False when the ray misses or the layer does not colour that point, leaving the dropper armed.
bool pick_background_at(const Vec2d &mouse_pos);
// Which of the Image panel's sections are unfolded. Panel state for the session, deliberately
// not saved with the project.
bool m_image_sec_place = true;
bool m_image_sec_color = true;
bool m_image_sec_bg = true;
// Queues one prepare_mesh() run on the job worker and commits its result when it lands. Every // Queues one prepare_mesh() run on the job worker and commits its result when it lands. Every
// mesh-preparation button goes through here - Pro's Remesh, Pro's adaptive Subdivide and Standard's // mesh-preparation button goes through here - Pro's Remesh, Pro's adaptive Subdivide and Standard's
@@ -399,11 +501,15 @@ private:
void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry); void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry);
void import_custom_texture(TextureDisplacementLayer &layer); void import_custom_texture(TextureDisplacementLayer &layer);
// Paint / Erase, from the panel. See get_left_button_state_type().
bool m_erase_mode = false;
// Per slot: whether the layer's card shows every setting or only Depth, Tile size and Rotation. // Per slot: whether the layer's card shows every setting or only Depth, Tile size and Rotation.
// Panel state only, not saved with the project. // Panel state only, not saved with the project.
std::array<bool, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_layer_expanded{}; std::array<bool, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_layer_expanded{};
// The slider whose edit icon was just clicked, as its ImGui id, and the frame it was clicked on.
// The row acts on it on the next frame - ImGui's keyboard-focus request only takes effect a frame
// later - and clears it; the frame number is what retires a request whose row has gone off screen
// in the meantime. Panel state only, not saved with the project.
std::string m_slider_type_request;
int m_slider_type_request_frame = 0;
// "Adjust Texture" mode: instead of painting, dragging an on-canvas handle changes the active // "Adjust Texture" mode: instead of painting, dragging an on-canvas handle changes the active
// layer's offset. The handle is a flat panel lying in the paint patch's own tangent plane // layer's offset. The handle is a flat panel lying in the paint patch's own tangent plane
@@ -504,6 +610,11 @@ private:
// The panel's view modes: 0 Normal, 1 Fast, 2 Checker, 3 Distortion. Shared by the View row and the pane's // The panel's view modes: 0 Normal, 1 Fast, 2 Checker, 3 Distortion. Shared by the View row and the pane's
// background buttons, so both switch views the same way. // background buttons, so both switch views the same way.
void apply_view_mode(int mode); void apply_view_mode(int mode);
// The view on screen, numbered as apply_view_mode() takes it.
int view_mode() const;
// Run once a frame, after the pane's commands: switches the view as m_island_view says.
void follow_island_highlight();
IslandViewFollow m_island_view;
// The pane header's thumbnail of the active layer's texture, rebuilt only when its image changes. // The pane header's thumbnail of the active layer's texture, rebuilt only when its image changes.
static constexpr int UV_THUMB_PX = 64; static constexpr int UV_THUMB_PX = 64;
const void *m_uv_thumb_source = nullptr; const void *m_uv_thumb_source = nullptr;
@@ -735,6 +846,8 @@ private:
// in place. Rebuilt whenever the pane's selection differs from the one it was built for. // in place. Rebuilt whenever the pane's selection differs from the one it was built for.
GLModel m_island_overlay_glmodel; GLModel m_island_overlay_glmodel;
std::vector<int> m_island_overlay_selection; std::vector<int> m_island_overlay_selection;
// The pane's selection while the overlay applies - the pane open over an unwrapped LSCM layer - else null.
const std::vector<int> *island_highlight_selection();
void rebuild_island_overlay(const std::vector<int> &selection); void rebuild_island_overlay(const std::vector<int> &selection);
void render_island_overlay(); void render_island_overlay();
// Set on every paint event, cleared when the overlay is rebuilt in render_painter_gizmo(). Kept // Set on every paint event, cleared when the overlay is rebuilt in render_painter_gizmo(). Kept
@@ -915,10 +1028,11 @@ private:
bool m_preview_job_pending = false; bool m_preview_job_pending = false;
void queue_preview_job(); void queue_preview_job();
// Per-slot GPU thumbnail cache for the layer list panel, keyed by the image_data pointer that // Per-slot GPU thumbnail cache for the layer list panel, each keyed by the image_data it was built from
// was current the last time each thumbnail was built (see get_layer_thumbnail()). // (see get_layer_thumbnail()). Held weakly rather than by address, as decode_height_texture()'s cache is:
// an image freed and replaced by another at the same address must not pass for it.
std::array<std::unique_ptr<GLTexture>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnails; std::array<std::unique_ptr<GLTexture>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnails;
std::array<const void *, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_source{}; std::array<std::weak_ptr<const std::vector<unsigned char>>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_source;
// The smoothing each cached thumbnail was built at, so a smoothing change re-uploads it. // The smoothing each cached thumbnail was built at, so a smoothing change re-uploads it.
std::array<float, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_smoothing{}; std::array<float, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_smoothing{};
@@ -926,14 +1040,15 @@ private:
// get_layer_height_texture()). A smoothing change re-uploads it, so the fast preview shows the // get_layer_height_texture()). A smoothing change re-uploads it, so the fast preview shows the
// blur the bake will apply. // blur the bake will apply.
std::unique_ptr<GLTexture> m_height_tex; std::unique_ptr<GLTexture> m_height_tex;
const void *m_height_tex_source = nullptr; std::weak_ptr<const std::vector<unsigned char>> m_height_tex_source;
float m_height_tex_smoothing = -1.f; float m_height_tex_smoothing = -1.f;
// The same, for the layer's *colour*, which the fast preview quantizes per fragment so it shows // The same, for the layer's *colour*, which the fast preview quantizes per fragment so it shows
// the image at texel resolution rather than at the mesh's. Null for a grayscale texture. // the image at texel resolution rather than at the mesh's. Null for a grayscale texture.
std::unique_ptr<GLTexture> m_color_tex; std::unique_ptr<GLTexture> m_color_tex;
const void *m_color_tex_source = nullptr; std::weak_ptr<const std::vector<unsigned char>> m_color_tex_source;
float m_color_tex_smoothing = -1.f; float m_color_tex_smoothing = -1.f;
ColorAdjustKey m_color_tex_adjust;
// Library textures the picker has shown at least once, keyed by file path (see LibraryTexture). // Library textures the picker has shown at least once, keyed by file path (see LibraryTexture).
std::map<std::string, LibraryTexture> m_library_textures; std::map<std::string, LibraryTexture> m_library_textures;
@@ -971,9 +1086,12 @@ private:
PatchUnwrap m_uv_editor_unwrap; PatchUnwrap m_uv_editor_unwrap;
// When set, the panel is a free-floating window the user can drag anywhere (with a title bar to // When set, the panel is a free-floating window the user can drag anywhere (with a title bar to
// grab), instead of being pinned to the right of the gizmo toolbar. Persisted across gizmo // grab), instead of being pinned to the right of the gizmo toolbar. Kept in the application
// open/close within a session, so the choice sticks while working. // config rather than in the project: it describes the user's window, not the model, so opening
// someone else's project must not move their panel.
bool m_undocked = false; bool m_undocked = false;
// The config key behind m_undocked.
static constexpr const char *UNDOCKED_CONFIG_KEY = "texture_displacement_panel_undocked";
// Smooth scrolling for the panel body (everything between the header and the pinned Bake footer). // Smooth scrolling for the panel body (everything between the header and the pinned Bake footer).
// ImGui jumps a fixed number of lines per wheel notch, which on tall layer cards reads as a hard // ImGui jumps a fixed number of lines per wheel notch, which on tall layer cards reads as a hard
@@ -1016,6 +1134,9 @@ private:
std::vector<std::pair<wxString, wxString>> m_shortcuts_bucket_fill; std::vector<std::pair<wxString, wxString>> m_shortcuts_bucket_fill;
// Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")} // Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}
std::vector<std::pair<wxString, wxString>> m_shortcuts_gap_fill; std::vector<std::pair<wxString, wxString>> m_shortcuts_gap_fill;
// The two ways every slider in the panel takes a typed value. They hold whatever tool is selected,
// so they are tacked onto each of the lists above and stand on their own when no tool is active.
std::vector<std::pair<wxString, wxString>> m_shortcuts_sliders;
// Icons for the panel's icon buttons (tools, views, mapping, tiling, layer actions). Loaded through IconManager with // Icons for the panel's icon buttons (tools, views, mapping, tiling, layer actions). Loaded through IconManager with
// the same colour/monochrome variants the main toolbar uses, so an inactive button shows the icon in // the same colour/monochrome variants the main toolbar uses, so an inactive button shows the icon in
+26 -5
View File
@@ -22,6 +22,7 @@
//#include "slic3r/GUI/Gizmos/GLGizmoHollow.hpp" //#include "slic3r/GUI/Gizmos/GLGizmoHollow.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoSeam.hpp" #include "slic3r/GUI/Gizmos/GLGizmoSeam.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp" #include "slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoImage.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp" #include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoSimplify.hpp" #include "slic3r/GUI/Gizmos/GLGizmoSimplify.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoEmboss.hpp" #include "slic3r/GUI/Gizmos/GLGizmoEmboss.hpp"
@@ -201,6 +202,11 @@ void GLGizmosManager::switch_gizmos_icon_filename()
// light/dark switch. // light/dark switch.
gizmo->set_icon_filename("toolbar_texture_displacement.svg"); gizmo->set_icon_filename("toolbar_texture_displacement.svg");
break; break;
case(EType::Image):
// Drawn flat in Orca's menu-icon palette, which reads on either background, so one file
// serves both themes here too.
gizmo->set_icon_filename("toolbar_image.svg");
break;
case(EType::MeshBoolean): case(EType::MeshBoolean):
gizmo->set_icon_filename(m_is_dark ? "toolbar_meshboolean_dark.svg" : "toolbar_meshboolean.svg"); gizmo->set_icon_filename(m_is_dark ? "toolbar_meshboolean_dark.svg" : "toolbar_meshboolean.svg");
break; break;
@@ -252,6 +258,8 @@ bool GLGizmosManager::init()
m_gizmos.emplace_back(new GLGizmoMmuSegmentation(m_parent, m_is_dark ? "mmu_segmentation_dark.svg" : "mmu_segmentation.svg", EType::MmSegmentation)); m_gizmos.emplace_back(new GLGizmoMmuSegmentation(m_parent, m_is_dark ? "mmu_segmentation_dark.svg" : "mmu_segmentation.svg", EType::MmSegmentation));
// One shared icon (no dedicated dark variant yet); it recolours acceptably in both themes. // One shared icon (no dedicated dark variant yet); it recolours acceptably in both themes.
m_gizmos.emplace_back(new GLGizmoTextureDisplacement(m_parent, "toolbar_texture_displacement.svg", EType::TextureDisplacement)); m_gizmos.emplace_back(new GLGizmoTextureDisplacement(m_parent, "toolbar_texture_displacement.svg", EType::TextureDisplacement));
// The same gizmo restricted to the zero-displacement case; see GLGizmoImage.
m_gizmos.emplace_back(new GLGizmoImage(m_parent, "toolbar_image.svg", EType::Image));
m_gizmos.emplace_back(new GLGizmoEmboss(m_parent, m_is_dark ? "toolbar_text_dark.svg" : "toolbar_text.svg", EType::Emboss)); m_gizmos.emplace_back(new GLGizmoEmboss(m_parent, m_is_dark ? "toolbar_text_dark.svg" : "toolbar_text.svg", EType::Emboss));
m_gizmos.emplace_back(new GLGizmoSVG(m_parent)); m_gizmos.emplace_back(new GLGizmoSVG(m_parent));
m_gizmos.emplace_back(new GLGizmoMeasure(m_parent, m_is_dark ? "toolbar_measure_dark.svg" : "toolbar_measure.svg", EType::Measure)); m_gizmos.emplace_back(new GLGizmoMeasure(m_parent, m_is_dark ? "toolbar_measure_dark.svg" : "toolbar_measure.svg", EType::Measure));
@@ -420,7 +428,10 @@ void GLGizmosManager::refresh_on_off_state()
void GLGizmosManager::reset_all_states() void GLGizmosManager::reset_all_states()
{ {
if (! m_enabled || m_serializing) // The restore/new-project event can run before deferred OpenGL resource
// loading has populated the gizmo list. There is nothing to reset until
// initialization completes.
if (! m_enabled || m_serializing || m_gizmos.empty())
return; return;
const EType current = get_current_type(); const EType current = get_current_type();
@@ -435,6 +446,9 @@ void GLGizmosManager::reset_all_states()
bool GLGizmosManager::open_gizmo(EType type) bool GLGizmosManager::open_gizmo(EType type)
{ {
if (m_gizmos.empty())
return false;
int idx = static_cast<int>(type); int idx = static_cast<int>(type);
// re-open same type cause closing // re-open same type cause closing
@@ -522,7 +536,7 @@ bool GLGizmosManager::is_running() const
bool GLGizmosManager::open_gizmo_by_shortcut(Shortcut shortcut) bool GLGizmosManager::open_gizmo_by_shortcut(Shortcut shortcut)
{ {
if (!m_enabled) if (!m_enabled || m_gizmos.empty())
return false; return false;
// The text tool opens without a selection because it creates its own object. // The text tool opens without a selection because it creates its own object.
@@ -585,6 +599,8 @@ bool GLGizmosManager::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_p
return dynamic_cast<GLGizmoFuzzySkin*>(m_gizmos[FuzzySkin].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); return dynamic_cast<GLGizmoFuzzySkin*>(m_gizmos[FuzzySkin].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == TextureDisplacement) else if (m_current == TextureDisplacement)
return dynamic_cast<GLGizmoTextureDisplacement*>(m_gizmos[TextureDisplacement].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); return dynamic_cast<GLGizmoTextureDisplacement*>(m_gizmos[TextureDisplacement].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == Image)
return dynamic_cast<GLGizmoImage*>(m_gizmos[Image].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == MeshBoolean) else if (m_current == MeshBoolean)
return dynamic_cast<GLGizmoMeshBoolean*>(m_gizmos[MeshBoolean].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down); return dynamic_cast<GLGizmoMeshBoolean*>(m_gizmos[MeshBoolean].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == BrimEars) else if (m_current == BrimEars)
@@ -599,6 +615,7 @@ bool GLGizmosManager::is_paint_gizmo()
m_current == EType::MmSegmentation || m_current == EType::MmSegmentation ||
m_current == EType::FuzzySkin || m_current == EType::FuzzySkin ||
m_current == EType::TextureDisplacement || m_current == EType::TextureDisplacement ||
m_current == EType::Image ||
m_current == EType::Seam; m_current == EType::Seam;
} }
@@ -1427,10 +1444,12 @@ void GLGizmosManager::update_hover_state(const EType &type)
bool GLGizmosManager::activate_gizmo(EType type) bool GLGizmosManager::activate_gizmo(EType type)
{ {
assert(!m_gizmos.empty());
// already activated // already activated
if (m_current == type) return true; if (m_current == type)
return true;
if (m_gizmos.empty())
return false;
if (m_current != Undefined) { if (m_current != Undefined) {
// clean up previous gizmo // clean up previous gizmo
@@ -1550,6 +1569,8 @@ std::string get_name_from_gizmo_etype(GLGizmosManager::EType type)
return "Fuzzy Skin Painting"; return "Fuzzy Skin Painting";
case GLGizmosManager::EType::TextureDisplacement: case GLGizmosManager::EType::TextureDisplacement:
return "Texture Displacement"; return "Texture Displacement";
case GLGizmosManager::EType::Image:
return "Image";
default: default:
return ""; return "";
} }
@@ -96,6 +96,7 @@ public:
FuzzySkin, FuzzySkin,
MmSegmentation, MmSegmentation,
TextureDisplacement, TextureDisplacement,
Image,
Emboss, Emboss,
Svg, Svg,
Measure, Measure,
@@ -925,8 +925,9 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
m_last_active_item = current_active_id; m_last_active_item = current_active_id;
last_move_input_window_width = ImGui::GetWindowWidth(); last_move_input_window_width = ImGui::GetWindowWidth();
ImGui::PopStyleVar();
imgui_wrapper->end(); imgui_wrapper->end();
ImGui::PopStyleVar(2); ImGui::PopStyleVar();
ImGuiWrapper::pop_toolbar_style(); ImGuiWrapper::pop_toolbar_style();
} }
@@ -1131,10 +1132,11 @@ void GizmoObjectManipulation::do_render_rotate_window(ImGuiWrapper *imgui_wrappe
m_last_active_item = current_active_id; m_last_active_item = current_active_id;
last_rotate_input_window_width = ImGui::GetWindowWidth(); last_rotate_input_window_width = ImGui::GetWindowWidth();
ImGui::PopStyleVar();
imgui_wrapper->end(); imgui_wrapper->end();
// BBS // BBS
ImGui::PopStyleVar(2); ImGui::PopStyleVar();
ImGuiWrapper::pop_toolbar_style(); ImGuiWrapper::pop_toolbar_style();
} }
-6
View File
@@ -1,6 +0,0 @@
namespace Slic3r {
namespace GUI {
} // namespace GUI
} // namespace Slic3r
-11
View File
@@ -1,11 +0,0 @@
#ifndef slic3r_MultiTaskModel_hpp_
#define slic3r_MultiTaskModel_hpp_
namespace Slic3r {
namespace GUI {
} // namespace GUI
} // namespace Slic3r
#endif
-7
View File
@@ -1880,13 +1880,6 @@ void PartPlate::render_exclude_area(bool force_default_color) {
// draw exclude area // draw exclude area
glsafe(::glDepthMask(GL_FALSE)); glsafe(::glDepthMask(GL_FALSE));
if (m_selected) {
glsafe(::glColor4fv(select_color.data()));
}
else {
glsafe(::glColor4fv(unselect_color.data()));
}
m_exclude_triangles.set_color(m_selected ? select_color : unselect_color); m_exclude_triangles.set_color(m_selected ? select_color : unselect_color);
m_exclude_triangles.render(); m_exclude_triangles.render();
glsafe(::glDepthMask(GL_TRUE)); glsafe(::glDepthMask(GL_TRUE));
+66 -20
View File
@@ -67,6 +67,7 @@
#include "slic3r/GUI/Field.hpp" #include "slic3r/GUI/Field.hpp"
#include "slic3r/GUI/Widgets/PopupWindow.hpp" #include "slic3r/GUI/Widgets/PopupWindow.hpp"
#include <set> #include <set>
#include <wx/filename.h>
#include "slic3r/GUI/Widgets/StaticLine.hpp" #include "slic3r/GUI/Widgets/StaticLine.hpp"
#include "slic3r/GUI/GUI_ObjectSettings.hpp" #include "slic3r/GUI/GUI_ObjectSettings.hpp"
#include "slic3r/GUI/ColorDecomposeDialog.hpp" #include "slic3r/GUI/ColorDecomposeDialog.hpp"
@@ -246,6 +247,7 @@
#include "DeviceCore/DevNozzleSystem.h" // DevNozzle, GetExtNozzles / GetRackNozzles #include "DeviceCore/DevNozzleSystem.h" // DevNozzle, GetExtNozzles / GetRackNozzles
#include "ProjectDirtyStateManager.hpp" #include "ProjectDirtyStateManager.hpp"
#include "Gizmos/GLGizmoSimplify.hpp" // create suggestion notification #include "Gizmos/GLGizmoSimplify.hpp" // create suggestion notification
#include "Gizmos/GLGizmoImage.hpp" // Drop an image onto the Image tool
#include "Gizmos/GLGizmoSVG.hpp" // Drop SVG file #include "Gizmos/GLGizmoSVG.hpp" // Drop SVG file
#include "Gizmos/GizmoObjectManipulation.hpp" #include "Gizmos/GizmoObjectManipulation.hpp"
@@ -7110,6 +7112,9 @@ void Sidebar::can_search()
p->can_search(); p->can_search();
} }
// Defined with the other drop helpers below; forward declared because the drop target uses it.
void set_image_drag_over(Plater &plater, bool over);
class PlaterDropTarget : public wxFileDropTarget class PlaterDropTarget : public wxFileDropTarget
{ {
public: public:
@@ -7119,12 +7124,53 @@ public:
virtual bool OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames); virtual bool OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames);
// A drag has no feedback of its own, so the Image tool is told when one is over the view and
// draws a drop zone for as long as it is. wxWidgets cannot hand us the file names before the
// drop on GTK, so this says nothing about what is being dragged: the zone invites a picture,
// and anything else still falls through to the model loader.
virtual wxDragResult OnEnter(wxCoord x, wxCoord y, wxDragResult def)
{
set_image_drag_over(m_plater, true);
return wxFileDropTarget::OnEnter(x, y, def);
}
virtual void OnLeave()
{
set_image_drag_over(m_plater, false);
wxFileDropTarget::OnLeave();
}
private: private:
MainFrame& m_mainframe; MainFrame& m_mainframe;
Plater& m_plater; Plater& m_plater;
}; };
namespace { namespace {
// The Image gizmo, but only while it is the open one. Null otherwise, which is what keeps a drag or
// a drop from meaning anything different with the tool closed.
GLGizmoImage *open_image_gizmo(Plater &plater)
{
GLCanvas3D *canvas = plater.canvas3D();
if (canvas == nullptr)
return nullptr;
GLGizmosManager &gizmos = canvas->get_gizmos_manager();
if (gizmos.get_current_type() != GLGizmosManager::Image)
return nullptr;
return dynamic_cast<GLGizmoImage *>(gizmos.get_gizmo(GLGizmosManager::Image));
}
// True when `file` is a picture and the Image gizmo is the one currently open, in which case it has
// taken the file. Mirrors emboss_svg() below, which does the same for an SVG and the text tool.
bool drop_image_on_gizmo(Plater &plater, const wxString &file)
{
static const std::set<wxString> image_ext = { "png", "jpg", "jpeg", "bmp" };
const wxString ext = wxFileName(file).GetExt().Lower();
if (image_ext.count(ext) == 0)
return false;
GLGizmoImage *image = open_image_gizmo(plater);
return image != nullptr && image->drop_image_file(into_u8(file));
}
bool emboss_svg(Plater& plater, const wxString &svg_file, const Vec2d& mouse_drop_position) bool emboss_svg(Plater& plater, const wxString &svg_file, const Vec2d& mouse_drop_position)
{ {
std::string svg_file_str = into_u8(svg_file); std::string svg_file_str = into_u8(svg_file);
@@ -7783,6 +7829,12 @@ const std::regex Plater::priv::pattern_zip_amf(".*[.]zip[.]amf", std::regex::ica
const std::regex Plater::priv::pattern_any_amf(".*[.](amf|amf[.]xml|zip[.]amf)", std::regex::icase); const std::regex Plater::priv::pattern_any_amf(".*[.](amf|amf[.]xml|zip[.]amf)", std::regex::icase);
const std::regex Plater::priv::pattern_prusa(".*bbl", std::regex::icase); const std::regex Plater::priv::pattern_prusa(".*bbl", std::regex::icase);
void set_image_drag_over(Plater &plater, bool over)
{
if (GLGizmoImage *image = open_image_gizmo(plater))
image->set_file_drag_over(over);
}
bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames) bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &filenames)
{ {
#ifdef WIN32 #ifdef WIN32
@@ -7790,11 +7842,18 @@ bool PlaterDropTarget::OnDropFiles(wxCoord x, wxCoord y, const wxArrayString &fi
this->MSWUpdateDragImageOnLeave(); this->MSWUpdateDragImageOnLeave();
#endif // WIN32 #endif // WIN32
set_image_drag_over(m_plater, false); // the drag is over, whoever ends up taking the file
m_mainframe.Raise(); m_mainframe.Raise();
m_mainframe.select_tab(TAB_ID_PREPARE); m_mainframe.select_tab(TAB_ID_PREPARE);
if (wxGetApp().is_editor()) if (wxGetApp().is_editor())
m_plater.select_view_3D("3D"); m_plater.select_view_3D("3D");
// A picture dropped while the Image tool is open is that tool's input, not a model to load. Only
// then: with the tool closed a dropped PNG goes on doing whatever it did before.
if (filenames.size() == 1 && drop_image_on_gizmo(m_plater, filenames.Last()))
return true;
// When only one .svg file is dropped on scene // When only one .svg file is dropped on scene
if (filenames.size() == 1) { if (filenames.size() == 1) {
const wxString &filename = filenames.Last(); const wxString &filename = filenames.Last();
@@ -14984,24 +15043,8 @@ bool Plater::priv::can_fix_through_cgal() const
std::vector<int> obj_idxs, vol_idxs; std::vector<int> obj_idxs, vol_idxs;
sidebar->obj_list()->get_selection_indexes(obj_idxs, vol_idxs); sidebar->obj_list()->get_selection_indexes(obj_idxs, vol_idxs);
#if FIX_THROUGH_CGAL_ALWAYS // A closed mesh may still self-intersect, so fixing is always allowed.
// Fixing always.
return ! obj_idxs.empty() || ! vol_idxs.empty(); return ! obj_idxs.empty() || ! vol_idxs.empty();
#else // FIX_THROUGH_CGAL_ALWAYS
// Fixing only if the model is not manifold.
if (vol_idxs.empty()) {
for (auto obj_idx : obj_idxs)
if (model.objects[obj_idx]->get_repaired_errors_count() > 0)
return true;
return false;
}
int obj_idx = obj_idxs.front();
for (auto vol_idx : vol_idxs)
if (model.objects[obj_idx]->get_repaired_errors_count(vol_idx) > 0)
return true;
return false;
#endif // FIX_THROUGH_CGAL_ALWAYS
} }
bool Plater::priv::can_simplify() const bool Plater::priv::can_simplify() const
@@ -22041,14 +22084,17 @@ void Plater::clear_before_change_mesh(int obj_idx)
mv->mmu_segmentation_facets.reset(); mv->mmu_segmentation_facets.reset();
mv->fuzzy_skin_facets.reset(); mv->fuzzy_skin_facets.reset();
} }
if (paint_removed) { if (paint_removed)
// snapshot_time is captured by copy so the lambda knows where to undo/redo to. notify_painting_removed();
}
void Plater::notify_painting_removed()
{
get_notification_manager()->push_notification( get_notification_manager()->push_notification(
NotificationType::CustomSupportsAndSeamRemovedAfterRepair, NotificationType::CustomSupportsAndSeamRemovedAfterRepair,
NotificationManager::NotificationLevel::PrintInfoNotificationLevel, NotificationManager::NotificationLevel::PrintInfoNotificationLevel,
_u8L("Custom supports and color painting were removed before repairing.")); _u8L("Custom supports and color painting were removed before repairing."));
} }
}
void Plater::changed_mesh(int obj_idx) void Plater::changed_mesh(int obj_idx)
{ {
+1
View File
@@ -599,6 +599,7 @@ public:
void reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject &object, bool postpone_error_messages = false); void reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject &object, bool postpone_error_messages = false);
void clear_before_change_mesh(int obj_idx); void clear_before_change_mesh(int obj_idx);
void notify_painting_removed();
void changed_mesh(int obj_idx); void changed_mesh(int obj_idx);
void changed_object(ModelObject &object); void changed_object(ModelObject &object);
+3
View File
@@ -114,6 +114,9 @@ constexpr std::array<ShortcutInfo, size_t(Shortcut::Count)> shortcut_table = {{
SHORTCUT(GizmoAssembly, "gizmo_assembly", L("Gizmo assemble"), PLATER, { 'Y' }), SHORTCUT(GizmoAssembly, "gizmo_assembly", L("Gizmo assemble"), PLATER, { 'Y' }),
SHORTCUT(GizmoBrimEars, "gizmo_brim_ears", L("Gizmo brim ears"), PLATER, { 'E' }), SHORTCUT(GizmoBrimEars, "gizmo_brim_ears", L("Gizmo brim ears"), PLATER, { 'E' }),
SHORTCUT(GizmoDisplacement, "gizmo_displacement", L("Gizmo texture displacement painting"), PLATER, { 'D' }), SHORTCUT(GizmoDisplacement, "gizmo_displacement", L("Gizmo texture displacement painting"), PLATER, { 'D' }),
// Unbound by default: every single letter this context can reach is already taken, and a default
// that shadows one of them is worse than none. The shortcuts dialog lists it for binding.
SHORTCUT(GizmoImage, "gizmo_image", L("Gizmo image painting"), PLATER, KeyChord{}),
// Sliders // Sliders
SHORTCUT(GoToLayer, "go_to_layer", L("Jump to layer"), PREVIEW, { 'G', SHIFT }), SHORTCUT(GoToLayer, "go_to_layer", L("Jump to layer"), PREVIEW, { 'G', SHIFT }),
+1 -1
View File
@@ -38,7 +38,7 @@ enum class Shortcut : uint8_t {
MoveSelectionLeft, MoveSelectionRight, MoveSelectionUp, MoveSelectionDown, RotateSelectionLeft, RotateSelectionRight, MoveSelectionLeft, MoveSelectionRight, MoveSelectionUp, MoveSelectionDown, RotateSelectionLeft, RotateSelectionRight,
// Gizmos // Gizmos
GizmoMove, GizmoRotate, GizmoScale, GizmoFlatten, GizmoCut, GizmoMeshBoolean, GizmoFdmSupports, GizmoSeam, GizmoFuzzySkin, GizmoMove, GizmoRotate, GizmoScale, GizmoFlatten, GizmoCut, GizmoMeshBoolean, GizmoFdmSupports, GizmoSeam, GizmoFuzzySkin,
GizmoMmuSegmentation, GizmoEmboss, GizmoMeasure, GizmoAssembly, GizmoBrimEars, GizmoDisplacement, GizmoMmuSegmentation, GizmoEmboss, GizmoMeasure, GizmoAssembly, GizmoBrimEars, GizmoDisplacement, GizmoImage,
// Sliders // Sliders
GoToLayer, LayerSliderUp, LayerSliderDown, MovesSliderLeft, MovesSliderRight, MovesSliderStart, MovesSliderEnd, GoToLayer, LayerSliderUp, LayerSliderDown, MovesSliderLeft, MovesSliderRight, MovesSliderStart, MovesSliderEnd,
// Painting tools // Painting tools
-232
View File
@@ -1,232 +0,0 @@
#include "SysInfoDialog.hpp"
#include "I18N.hpp"
#include "3DScene.hpp"
#include "GUI.hpp"
#include "../Utils/UndoRedo.hpp"
#include "Plater.hpp"
#include <string>
#include <Eigen/Core>
#include <wx/clipbrd.h>
#include <wx/platinfo.h>
#include "GUI_App.hpp"
#include "MainFrame.hpp"
#include "wxExtensions.hpp"
#include "libslic3r/BlacklistedLibraryCheck.hpp"
#include "libslic3r/Color.hpp"
#include "format.hpp"
#ifdef _WIN32
// The standard Windows includes.
#define WIN32_LEAN_AND_MEAN
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include <psapi.h>
#endif /* _WIN32 */
namespace Slic3r {
namespace GUI {
std::string get_main_info(bool format_as_html)
{
std::stringstream out;
std::string b_start = format_as_html ? "<b>" : "";
std::string b_end = format_as_html ? "</b>" : "";
std::string line_end = format_as_html ? "<br>" : "\n";
if (!format_as_html)
out << b_start << (wxGetApp().is_editor() ? SLIC3R_APP_FULL_NAME : GCODEVIEWER_APP_NAME) << b_end << line_end;
out << b_start << "Version: " << b_end << SLIC3R_VERSION << line_end;
out << b_start << "Build: " << b_end << (wxGetApp().is_editor() ? SLIC3R_BUILD_ID : GCODEVIEWER_BUILD_ID) << line_end;
out << line_end;
out << b_start << "Operating System: " << b_end << wxPlatformInfo::Get().GetOperatingSystemFamilyName() << line_end;
out << b_start << "System Architecture: " << b_end << wxPlatformInfo::Get().GetArchName() << line_end;
out << b_start <<
#if defined _WIN32
"Windows Version: "
#else
// Hopefully some kind of unix / linux.
"System Version: "
#endif
<< b_end << wxPlatformInfo::Get().GetOperatingSystemDescription() << line_end;
out << b_start << "Total RAM size [MB]: " << b_end << Slic3r::format_memsize_MB(Slic3r::total_physical_memory());
return out.str();
}
std::string get_mem_info(bool format_as_html)
{
std::stringstream out;
std::string b_start = format_as_html ? "<b>" : "";
std::string b_end = format_as_html ? "</b>" : "";
std::string line_end = format_as_html ? "<br>" : "\n";
std::string mem_info_str = log_memory_info(true);
std::istringstream mem_info(mem_info_str);
std::string value;
while (std::getline(mem_info, value, ':')) {
out << b_start << (value+": ") << b_end;
std::getline(mem_info, value, ';');
out << value << line_end;
}
const Slic3r::UndoRedo::Stack &stack = wxGetApp().plater()->undo_redo_stack_main();
out << b_start << "RAM size reserved for the Undo / Redo stack: " << b_end << Slic3r::format_memsize_MB(stack.get_memory_limit()) << line_end;
out << b_start << "RAM size occupied by the Undo / Redo stack: " << b_end << Slic3r::format_memsize_MB(stack.memsize()) << line_end << line_end;
return out.str();
}
SysInfoDialog::SysInfoDialog()
: DPIDialog(static_cast<wxWindow*>(wxGetApp().mainframe), wxID_ANY, (wxGetApp().is_editor() ? wxString(SLIC3R_APP_FULL_NAME) : wxString(GCODEVIEWER_APP_NAME)) + " - " + _L("System Information"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
{
wxColour bgr_clr = wxGetApp().get_window_default_clr();//wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);
SetBackgroundColour(bgr_clr);
SetFont(wxGetApp().normal_font());
wxBoxSizer* hsizer = new wxBoxSizer(wxHORIZONTAL);
hsizer->SetMinSize(wxSize(50 * wxGetApp().em_unit(), -1));
auto main_sizer = new wxBoxSizer(wxVERTICAL);
main_sizer->Add(hsizer, 1, wxEXPAND | wxALL, 10);
// logo
m_logo_bmp = ScalableBitmap(this, wxGetApp().logo_name(), 192);
m_logo = new wxStaticBitmap(this, wxID_ANY, m_logo_bmp.bmp());
hsizer->Add(m_logo, 0, wxALIGN_CENTER_VERTICAL);
wxBoxSizer* vsizer = new wxBoxSizer(wxVERTICAL);
hsizer->Add(vsizer, 1, wxEXPAND|wxLEFT, 20);
// title
{
wxStaticText* title = new wxStaticText(this, wxID_ANY, wxGetApp().is_editor() ? SLIC3R_APP_FULL_NAME : GCODEVIEWER_APP_NAME, wxDefaultPosition, wxDefaultSize);
wxFont title_font = wxGetApp().bold_font();
title_font.SetFamily(wxFONTFAMILY_ROMAN);
title_font.SetPointSize(22);
title->SetFont(title_font);
vsizer->Add(title, 0, wxEXPAND | wxTOP, wxGetApp().em_unit()/*50*/);
}
// main_info_text
wxFont font = get_default_font(this);
const auto text_clr = wxGetApp().get_label_clr_default();//wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT);
auto text_clr_str = encode_color(ColorRGB(text_clr.Red(), text_clr.Green(), text_clr.Blue()));
auto bgr_clr_str = encode_color(ColorRGB(bgr_clr.Red(), bgr_clr.Green(), bgr_clr.Blue()));
const int fs = font.GetPointSize() - 1;
int size[] = { static_cast<int>(fs*1.5), static_cast<int>(fs*1.4), static_cast<int>(fs*1.3), fs, fs, fs, fs };
m_html = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHW_SCROLLBAR_NEVER);
{
m_html->SetFonts(font.GetFaceName(), font.GetFaceName(), size);
m_html->SetBorders(2);
const auto text = wxString::Format(
"<html>"
"<body bgcolor= %s link= %s>"
"<font color=%s>"
"%s"
"</font>"
"</body>"
"</html>", bgr_clr_str, text_clr_str, text_clr_str,
get_main_info(true));
m_html->SetPage(text);
vsizer->Add(m_html, 1, wxEXPAND | wxBOTTOM, wxGetApp().em_unit());
}
// opengl_info
m_opengl_info_html = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHW_SCROLLBAR_AUTO);
{
m_opengl_info_html->SetMinSize(wxSize(-1, 16 * wxGetApp().em_unit()));
m_opengl_info_html->SetFonts(font.GetFaceName(), font.GetFaceName(), size);
m_opengl_info_html->SetBorders(10);
wxString blacklisted_libraries_message;
#ifdef WIN32
std::wstring blacklisted_libraries = BlacklistedLibraryCheck::get_instance().get_blacklisted_string().c_str();
if (! blacklisted_libraries.empty())
blacklisted_libraries_message = wxString("<br><b>") + _L("Blacklisted libraries loaded into OrcaSlicer process:") + "</b><br>" + blacklisted_libraries;
#endif // WIN32
const auto text = GUI::format_wxstr(
"<html>"
"<body bgcolor= %s link= %s>"
"<font color=%s>"
"%s<br>%s<br>%s<br>%s"
"</font>"
"</body>"
"</html>", bgr_clr_str, text_clr_str, text_clr_str,
blacklisted_libraries_message,
get_mem_info(true), wxGetApp().get_gl_info(false),
"<b>" + _L("SIMD is supported:") + "</b> " + Eigen::SimdInstructionSetsInUse());
m_opengl_info_html->SetPage(text);
main_sizer->Add(m_opengl_info_html, 1, wxEXPAND | wxBOTTOM, 15);
}
wxStdDialogButtonSizer* buttons = this->CreateStdDialogButtonSizer(wxOK);
m_btn_copy_to_clipboard = new wxButton(this, wxID_ANY, _L("Copy to Clipboard"), wxDefaultPosition, wxDefaultSize);
buttons->Insert(0, m_btn_copy_to_clipboard, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, 5);
m_btn_copy_to_clipboard->Bind(wxEVT_BUTTON, &SysInfoDialog::onCopyToClipboard, this);
this->SetEscapeId(wxID_OK);
this->Bind(wxEVT_BUTTON, &SysInfoDialog::onCloseDialog, this, wxID_OK);
main_sizer->Add(buttons, 0, wxEXPAND | wxRIGHT | wxBOTTOM, 3);
wxGetApp().UpdateDlgDarkUI(this, true);
// this->Bind(wxEVT_LEFT_DOWN, &SysInfoDialog::onCloseDialog, this);
// logo->Bind(wxEVT_LEFT_DOWN, &SysInfoDialog::onCloseDialog, this);
SetSizer(main_sizer);
main_sizer->SetSizeHints(this);
}
void SysInfoDialog::on_dpi_changed(const wxRect &suggested_rect)
{
m_logo_bmp.msw_rescale();
m_logo->SetBitmap(m_logo_bmp.bmp());
wxFont font = get_default_font(this);
const int fs = font.GetPointSize() - 1;
int font_size[] = { static_cast<int>(fs*1.5), static_cast<int>(fs*1.4), static_cast<int>(fs*1.3), fs, fs, fs, fs };
m_html->SetFonts(font.GetFaceName(), font.GetFaceName(), font_size);
m_html->Refresh();
const int& em = em_unit();
msw_buttons_rescale(this, em, { wxID_OK, m_btn_copy_to_clipboard->GetId() });
m_opengl_info_html->SetMinSize(wxSize(-1, 16 * em));
m_opengl_info_html->SetFonts(font.GetFaceName(), font.GetFaceName(), font_size);
m_opengl_info_html->Refresh();
const wxSize& size = wxSize(65 * em, 55 * em);
SetMinSize(size);
Fit();
Refresh();
}
void SysInfoDialog::onCopyToClipboard(wxEvent &)
{
wxTheClipboard->Open();
const auto text = get_main_info(false) + "\n" + wxGetApp().get_gl_info(true);
wxTheClipboard->SetData(new wxTextDataObject(text));
wxTheClipboard->Close();
}
void SysInfoDialog::onCloseDialog(wxEvent &)
{
this->EndModal(wxID_CLOSE);
}
} // namespace GUI
} // namespace Slic3r
-36
View File
@@ -1,36 +0,0 @@
#ifndef slic3r_GUI_SysInfoDialog_hpp_
#define slic3r_GUI_SysInfoDialog_hpp_
#include <wx/event.h>
#include <wx/wx.h>
#include <wx/html/htmlwin.h>
#include "GUI_Utils.hpp"
#include "wxExtensions.hpp"
namespace Slic3r {
namespace GUI {
class SysInfoDialog : public DPIDialog
{
ScalableBitmap m_logo_bmp;
wxStaticBitmap* m_logo;
wxHtmlWindow* m_opengl_info_html;
wxHtmlWindow* m_html;
wxButton* m_btn_copy_to_clipboard;
public:
SysInfoDialog();
protected:
void on_dpi_changed(const wxRect &suggested_rect) override;
private:
void onCopyToClipboard(wxEvent &);
void onCloseDialog(wxEvent &);
};
} // namespace GUI
} // namespace Slic3r
#endif
+1
View File
@@ -3098,6 +3098,7 @@ void TabPrint::build()
optgroup = page->new_optgroup(L("Raft"), L"param_raft"); optgroup = page->new_optgroup(L("Raft"), L"param_raft");
optgroup->append_single_option_line("raft_layers", "support_settings_raft"); optgroup->append_single_option_line("raft_layers", "support_settings_raft");
optgroup->append_single_option_line("raft_contact_distance", "support_settings_raft"); optgroup->append_single_option_line("raft_contact_distance", "support_settings_raft");
optgroup->append_single_option_line("raft_expansion", "support_settings_raft");
optgroup = page->new_optgroup(L("Filament for Supports"), L"param_support_filament"); optgroup = page->new_optgroup(L("Filament for Supports"), L"param_support_filament");
optgroup->append_single_option_line("support_filament", "support_settings_filament#base"); optgroup->append_single_option_line("support_filament", "support_settings_filament#base");
+16 -25
View File
@@ -14,7 +14,7 @@
#include "Widgets/StateColor.hpp" #include "Widgets/StateColor.hpp"
#include "Widgets/StaticLine.hpp" #include "Widgets/StaticLine.hpp"
#include "libslic3r/ColorDecomposeRecipe.hpp" #include "libslic3r/ColorDecomposeRecipe.hpp"
#include "libslic3r/MeshBoolean.hpp" #include "libslic3r/MeshRepair.hpp"
#include "libslic3r/TriangleSelector.hpp" #include "libslic3r/TriangleSelector.hpp"
#include <utility> #include <utility>
@@ -2436,25 +2436,25 @@ void TextureImportDialog::start_computation(bool auto_color, bool initial)
settings.target_colors_num = auto_color ? 0 : (size_t)m_param_color_count; settings.target_colors_num = auto_color ? 0 : (size_t)m_param_color_count;
settings.smooth_weight = m_param_smooth / 10.0; settings.smooth_weight = m_param_smooth / 10.0;
settings.mesh_repair_decision = m_mesh_repair_decision; settings.mesh_repair_decision = m_mesh_repair_decision;
// BBS repairs the mesh through the Windows 3D SDK, which is only available on Windows
// builds that ship the SDK. Orca's CGAL-based repair (MeshBoolean::cgal::repair) works
// on all three platforms, so use that instead.
settings.mesh_repair_callback = [](const indexed_triangle_set& mesh, settings.mesh_repair_callback = [](const indexed_triangle_set& mesh,
indexed_triangle_set& repaired_mesh, indexed_triangle_set& repaired_mesh,
std::function<void(const char*, unsigned)> progress_callback, std::function<void(const char*, unsigned)> progress_callback,
std::function<bool()> cancel_callback, std::function<bool()> cancel_callback,
std::string* error_message) -> bool { std::string* error_message) -> bool {
if (cancel_callback && cancel_callback())
return false;
if (progress_callback) if (progress_callback)
progress_callback(_u8L("Repairing mesh").c_str(), 0); progress_callback(_u8L("Repairing mesh").c_str(), 0);
struct Canceled {};
TriangleMesh tm(mesh); TriangleMesh tm(mesh);
if (!MeshBoolean::cgal::repair(tm, nullptr, error_message)) try {
return false; if (!repair_mesh(tm, nullptr, error_message, [&cancel_callback](int) {
if (cancel_callback && cancel_callback()) if (cancel_callback && cancel_callback())
throw Canceled();
}))
return false; return false;
} catch (const Canceled &) {
return false;
}
repaired_mesh = tm.its; repaired_mesh = tm.its;
if (progress_callback) if (progress_callback)
progress_callback(_u8L("Repairing mesh").c_str(), 100); progress_callback(_u8L("Repairing mesh").c_str(), 100);
@@ -2683,12 +2683,12 @@ void TextureImportDialog::on_mesh_repair_decision_required(wxCommandEvent&)
m_progress_dlg = nullptr; m_progress_dlg = nullptr;
} }
#ifdef HAS_WIN10SDK
Slic3r::GUI::MessageDialog dlg(initial ? GetParent() : this, Slic3r::GUI::MessageDialog dlg(initial ? GetParent() : this,
_L("The mesh has non-manifold geometry or open boundaries. You can import it as-is or repair it with Windows 3D repair service before importing."), _L("The mesh has non-manifold geometry or open boundaries. You can import it as-is or repair it before importing."),
_L("Mesh repair"), wxYES_NO | wxICON_WARNING | wxYES_DEFAULT); _L("Mesh repair"), wxYES_NO | wxCANCEL | wxICON_WARNING | wxYES_DEFAULT);
dlg.SetButtonLabel(wxID_YES, _L("Import without repair")); dlg.SetButtonLabel(wxID_YES, _L("Import without repair"));
dlg.SetButtonLabel(wxID_NO, _L("Repair and import"), true); dlg.SetButtonLabel(wxID_NO, _L("Repair and import"), true);
dlg.SetButtonLabel(wxID_CANCEL, _L("Cancel"));
// "Repair and import" is the recommended action here, so the accent moves off the default YES // "Repair and import" is the recommended action here, so the accent moves off the default YES
// button onto NO. MsgDialog::add_button already styled both as ButtonType::Choice, so restyling // button onto NO. MsgDialog::add_button already styled both as ButtonType::Choice, so restyling
// with the same type swaps only the palette and leaves the geometry alone. // with the same type swaps only the palette and leaves the geometry alone.
@@ -2704,17 +2704,7 @@ void TextureImportDialog::on_mesh_repair_decision_required(wxCommandEvent&)
dlg.Fit(); dlg.Fit();
dlg.CenterOnParent(); dlg.CenterOnParent();
int ret = dlg.ShowModal(); int ret = dlg.ShowModal();
m_mesh_repair_decision = (ret == wxID_NO) if (ret == wxID_CANCEL) {
? Slic3r::TexturePaintingSettings::MeshRepairDecision::RepairAndImport
: Slic3r::TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair;
#else
Slic3r::GUI::MessageDialog dlg(initial ? GetParent() : this,
_L("Please note that the mesh has non-manifold geometry or open boundaries."),
_L("Mesh issue"), wxOK | wxCANCEL | wxICON_WARNING | wxOK_DEFAULT);
dlg.SetButtonLabel(wxID_OK, _L("Continue"), true);
dlg.SetButtonLabel(wxID_CANCEL, _L("Cancel"));
int ret = dlg.ShowModal();
if (ret != wxID_OK) {
m_cancel_flag = true; m_cancel_flag = true;
if (initial) { if (initial) {
m_initial_computation_cancelled = true; m_initial_computation_cancelled = true;
@@ -2727,8 +2717,9 @@ void TextureImportDialog::on_mesh_repair_decision_required(wxCommandEvent&)
} }
return; return;
} }
m_mesh_repair_decision = Slic3r::TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair; m_mesh_repair_decision = (ret == wxID_NO)
#endif ? Slic3r::TexturePaintingSettings::MeshRepairDecision::RepairAndImport
: Slic3r::TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair;
start_computation(auto_color, initial); start_computation(auto_color, initial);
} }
+5
View File
@@ -939,6 +939,7 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt)
m_active_edge = (hit.first >= 0 && is_edge_selected(hit)) ? hit : std::pair<int, int>{ -1, -1 }; m_active_edge = (hit.first >= 0 && is_edge_selected(hit)) ? hit : std::pair<int, int>{ -1, -1 };
m_gesture = (m_active_edge.first >= 0) ? Gesture::MoveEdge : Gesture::Pan; m_gesture = (m_active_edge.first >= 0) ? Gesture::MoveEdge : Gesture::Pan;
} else { } else {
const std::vector<int> selection_before = m_selection;
const int hit = island_at(uv); const int hit = island_at(uv);
if (hit >= 0) { if (hit >= 0) {
if (evt.ShiftDown()) { if (evt.ShiftDown()) {
@@ -969,6 +970,10 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt)
m_selected_island = -1; m_selected_island = -1;
} }
m_gesture = (m_selected_island >= 0) ? Gesture::MoveIsland : Gesture::Pan; m_gesture = (m_selected_island >= 0) ? Gesture::MoveIsland : Gesture::Pan;
// The owner shows the selection on the model and would not hear of this click otherwise in time: a
// click that clears the selection starts no gesture, and one that sets it reports only on release.
if (m_selection != selection_before)
run_command(Command::SelectionChanged);
} }
grab_mouse(); grab_mouse();
Refresh(); Refresh();
+3 -1
View File
@@ -108,10 +108,12 @@ public:
// the command callback - the canvas has the selection and the view, the gizmo has the layer. // the command callback - the canvas has the selection and the view, the gizmo has the layer.
// `value` carries the new setting for the Set* commands (an angle, a flag as 0/1, or an index into // `value` carries the new setting for the Set* commands (an angle, a flag as 0/1, or an index into
// SelectMode / Background) and is 0 otherwise. // SelectMode / Background) and is 0 otherwise.
// SelectionChanged is sent by the canvas itself when a click changes the island selection. It carries
// nothing: the owner reads selected_islands() back, and only needs telling that it should look.
enum class Command { enum class Command {
FrameAll, ToggleSnap, AverageScale, CutSelectedIsland, ProjectFromView, JoinSelected, UnjoinSelected, FrameAll, ToggleSnap, AverageScale, CutSelectedIsland, ProjectFromView, JoinSelected, UnjoinSelected,
Unwrap, SetSeamAngle, SetConnectIslands, SetSelectMode, SetMarkSeams, SetSeamPath, ClearSeams, ClearUVEdits, Unwrap, SetSeamAngle, SetConnectIslands, SetSelectMode, SetMarkSeams, SetSeamPath, ClearSeams, ClearUVEdits,
SetBackground, PickTexture, PaneClosed SetBackground, PickTexture, PaneClosed, SelectionChanged
}; };
void run_command(Command cmd, float value = 0.f); void run_command(Command cmd, float value = 0.f);
using CommandFn = std::function<void(Command, float)>; using CommandFn = std::function<void(Command, float)>;
+2 -3
View File
@@ -27,8 +27,7 @@ WebDialog::WebDialog(wxWindow* parent,
SubmitHandler on_submit, SubmitHandler on_submit,
CloseHandler on_close, CloseHandler on_close,
CloseHandler on_destroyed, CloseHandler on_destroyed,
long wx_style, long wx_style)
const wxString& profile)
: WebViewHostDialog(parent, wxID_ANY, title, wxDefaultPosition, size, wx_style) : WebViewHostDialog(parent, wxID_ANY, title, wxDefaultPosition, size, wx_style)
, m_html(html) , m_html(html)
, m_on_message(std::move(on_message)) , m_on_message(std::move(on_message))
@@ -38,7 +37,7 @@ WebDialog::WebDialog(wxWindow* parent,
{ {
// A tiny bundled bootstrap page brings the webview up; the real plugin HTML // A tiny bundled bootstrap page brings the webview up; the real plugin HTML
// is swapped in via SetPage once the bootstrap finishes loading. // is swapped in via SetPage once the bootstrap finishes loading.
create_webview(web_hosting::BOOTSTRAP_PAGE, title, size, wxSize(320, 240), profile); create_webview(web_hosting::BOOTSTRAP_PAGE, title, size, wxSize(320, 240));
// Paint the window/webview in the themed background so there is no white // Paint the window/webview in the themed background so there is no white
// flash before the (transparent) bootstrap page and plugin HTML render. // flash before the (transparent) bootstrap page and plugin HTML render.
+1 -2
View File
@@ -49,8 +49,7 @@ public:
SubmitHandler on_submit, SubmitHandler on_submit,
CloseHandler on_close, CloseHandler on_close,
CloseHandler on_destroyed, CloseHandler on_destroyed,
long wx_style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER, long wx_style = wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER);
const wxString& profile = wxEmptyString);
~WebDialog() override; ~WebDialog() override;
static void post_message(WebDialog* dialog, const nlohmann::json& data); static void post_message(WebDialog* dialog, const nlohmann::json& data);
+2 -2
View File
@@ -17,7 +17,7 @@
namespace Slic3r { namespace GUI { namespace Slic3r { namespace GUI {
WebPanel::WebPanel(wxWindow* parent, const char* bridge_script, const wxString& profile) WebPanel::WebPanel(wxWindow* parent, const char* bridge_script)
: wxPanel(parent, wxID_ANY) : wxPanel(parent, wxID_ANY)
{ {
SetBackgroundColour(wxGetApp().get_window_default_clr()); SetBackgroundColour(wxGetApp().get_window_default_clr());
@@ -25,7 +25,7 @@ WebPanel::WebPanel(wxWindow* parent, const char* bridge_script, const wxString&
SetSizer(sizer); SetSizer(sizer);
// Never null: WebView::CreateWebView substitutes a placeholder view when no backend is available. // Never null: WebView::CreateWebView substitutes a placeholder view when no backend is available.
m_browser = WebView::CreateWebView(this, web_hosting::bootstrap_url(), profile); m_browser = WebView::CreateWebView(this, web_hosting::bootstrap_url());
m_browser->SetBackgroundColour(GetBackgroundColour()); m_browser->SetBackgroundColour(GetBackgroundColour());
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::theme_user_script())); m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::theme_user_script()));
m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::element_defaults_user_script())); m_browser->AddUserScript(wxString::FromUTF8(WebViewHostDialog::element_defaults_user_script()));
+1 -2
View File
@@ -4,7 +4,6 @@
#include <wx/event.h> #include <wx/event.h>
#include <wx/panel.h> #include <wx/panel.h>
#include <wx/string.h>
#include <wx/webview.h> #include <wx/webview.h>
#include <optional> #include <optional>
@@ -23,7 +22,7 @@ namespace Slic3r { namespace GUI {
class WebPanel : public wxPanel class WebPanel : public wxPanel
{ {
public: public:
WebPanel(wxWindow* parent, const char* bridge_script, const wxString& profile = wxEmptyString); WebPanel(wxWindow* parent, const char* bridge_script);
protected: protected:
wxWebView* browser() const { return m_browser; } wxWebView* browser() const { return m_browser; }
View File
View File
+4 -59
View File
@@ -3,8 +3,6 @@
#include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/Utils/MacDarkMode.hpp" #include "slic3r/Utils/MacDarkMode.hpp"
#include <libslic3r/Utils.hpp>
#include <algorithm> #include <algorithm>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
@@ -14,7 +12,6 @@
#include <exception> #include <exception>
#include <thread> #include <thread>
#include <wx/chartype.h>
#include <wx/setup.h> #include <wx/setup.h>
#include <wx/webview.h> #include <wx/webview.h>
#include <wx/string.h> #include <wx/string.h>
@@ -75,26 +72,6 @@ webkit_javascript_result_unref (WebKitJavascriptResult *js_result);
} }
#endif #endif
// Private storage root for an isolated (plugin) profile. Plugin pages must not reach the shared
// default store's cookies/session (e.g. the OrcaCloud login), so each profile gets its own dir.
// macOS isolates with an in-memory store instead (see CreateWebView), so this is unused there.
#if !defined(__WXOSX__)
static wxString isolated_profile_path(const wxString& profile)
{
wxString safe;
safe.reserve(profile.size());
for (wxString::const_iterator it = profile.begin(); it != profile.end(); ++it) {
const wxChar c = *it;
const bool keep = (c >= wxT('a') && c <= wxT('z')) || (c >= wxT('A') && c <= wxT('Z')) ||
(c >= wxT('0') && c <= wxT('9')) || c == wxT('.') || c == wxT('-') || c == wxT('_');
safe += keep ? c : wxT('_');
}
if (safe.empty())
safe = wxT("default");
return wxString::FromUTF8((boost::filesystem::path(Slic3r::data_dir()) / "web_profiles" / safe.ToStdString()).string());
}
#endif
#ifdef __WIN32__ #ifdef __WIN32__
// Run Download and Install in another thread so we don't block the UI thread // Run Download and Install in another thread so we don't block the UI thread
DWORD DownloadAndInstallWV2RT() { DWORD DownloadAndInstallWV2RT() {
@@ -158,9 +135,6 @@ DWORD DownloadAndInstallWV2RT() {
class WebViewEdge : public wxWebViewEdge class WebViewEdge : public wxWebViewEdge
{ {
public: public:
WebViewEdge() = default;
explicit WebViewEdge(const wxWebViewConfiguration& config) : wxWebViewEdge(config) {}
bool SetUserAgent(const wxString &userAgent) override bool SetUserAgent(const wxString &userAgent) override
{ {
bool dark = userAgent.Contains("dark"); bool dark = userAgent.Contains("dark");
@@ -239,11 +213,6 @@ public:
{ {
} }
explicit WebViewWebKit(const wxWebViewConfiguration& config)
: wxWebViewWebKit(config)
{
}
~WebViewWebKit() override ~WebViewWebKit() override
{ {
RemoveScriptMessageHandler("wx"); RemoveScriptMessageHandler("wx");
@@ -315,7 +284,7 @@ static WebViewRef *webview_ref(wxWebView *webView)
return webView ? static_cast<WebViewRef *>(webView->GetRefData()) : nullptr; return webView ? static_cast<WebViewRef *>(webView->GetRefData()) : nullptr;
} }
wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url, wxString const & profile) wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url)
{ {
#if wxUSE_WEBVIEW_EDGE #if wxUSE_WEBVIEW_EDGE
// Check if a fixed version of edge is present in // Check if a fixed version of edge is present in
@@ -335,36 +304,12 @@ wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url, wxStr
if (!url2.empty()) { url2 = wxURI(url2).BuildURI(); } if (!url2.empty()) { url2 = wxURI(url2).BuildURI(); }
BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << url2.ToUTF8(); BOOST_LOG_TRIVIAL(trace) << __FUNCTION__ << ": " << url2.ToUTF8();
const bool isolated = !profile.IsEmpty();
#ifdef __WIN32__ #ifdef __WIN32__
wxWebView* webView = nullptr; wxWebView* webView = new WebViewEdge;
if (isolated) {
wxWebViewConfiguration config = wxWebView::NewConfiguration(wxWebViewBackendEdge);
config.SetDataPath(isolated_profile_path(profile));
webView = new WebViewEdge(config);
} else {
webView = new WebViewEdge;
}
#elif defined(__WXOSX__) #elif defined(__WXOSX__)
wxWebView* webView = nullptr; wxWebView* webView = new WebViewWebKit;
if (isolated) {
// wx has no SetDataPath on macOS; give the view its own in-memory store instead.
wxWebViewConfiguration config = wxWebView::NewConfiguration(wxWebViewBackendWebKit);
Slic3r::GUI::WKWebViewConfiguration_setNonPersistentDataStore(config.GetNativeConfiguration());
webView = new WebViewWebKit(config);
} else {
webView = new WebViewWebKit;
}
#else #else
wxWebView* webView = nullptr; auto webView = wxWebView::New();
if (isolated) {
wxWebViewConfiguration config = wxWebView::NewConfiguration(wxWebViewBackendWebKit);
config.SetDataPath(isolated_profile_path(profile));
webView = wxWebView::New(config);
} else {
webView = wxWebView::New();
}
#endif #endif
if (webView) { if (webView) {
webView->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); webView->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
+1 -5
View File
@@ -14,11 +14,7 @@ wxDECLARE_EVENT(EVT_WEBVIEW_RECREATED, wxCommandEvent);
class WebView class WebView
{ {
public: public:
// `profile` empty -> the shared default backend store used by all trusted UI. static wxWebView *CreateWebView(wxWindow *parent, wxString const &url);
// `profile` non-empty -> a private store keyed by `profile`, so this view shares no
// cookies/storage/session with the shared store or any other profile. Used for
// plugin-supplied (untrusted) content.
static wxWebView *CreateWebView(wxWindow *parent, wxString const &url, wxString const &profile = wxEmptyString);
#if wxUSE_WEBVIEW_EDGE #if wxUSE_WEBVIEW_EDGE
static bool CheckWebViewRuntime(); static bool CheckWebViewRuntime();
static bool DownloadAndInstallWebViewRuntime(); static bool DownloadAndInstallWebViewRuntime();
+2 -3
View File
@@ -172,8 +172,7 @@ WebViewHostDialog::WebViewHostDialog(wxWindow* parent,
bool WebViewHostDialog::create_webview(const std::string& resource_path, bool WebViewHostDialog::create_webview(const std::string& resource_path,
const wxString& title, const wxString& title,
const wxSize& dialog_size, const wxSize& dialog_size,
const wxSize& min_size, const wxSize& min_size)
const wxString& profile)
{ {
SetTitle(title); SetTitle(title);
SetMinSize(FromDIP(min_size)); SetMinSize(FromDIP(min_size));
@@ -182,7 +181,7 @@ bool WebViewHostDialog::create_webview(const std::string& resource_path,
wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL); wxBoxSizer* topsizer = new wxBoxSizer(wxVERTICAL);
// Created empty and loaded below, after the user scripts are added, so the first load already has them. // Created empty and loaded below, after the user scripts are added, so the first load already has them.
m_browser = WebView::CreateWebView(this, wxEmptyString, profile); m_browser = WebView::CreateWebView(this, wxEmptyString);
if (m_browser == nullptr) { if (m_browser == nullptr) {
wxLogError("Could not init m_browser"); wxLogError("Could not init m_browser");
delete topsizer; delete topsizer;
+1 -2
View File
@@ -38,8 +38,7 @@ public:
bool create_webview(const std::string& resource_path, bool create_webview(const std::string& resource_path,
const wxString& title, const wxString& title,
const wxSize& dialog_size = wxSize(820, 660), const wxSize& dialog_size = wxSize(820, 660),
const wxSize& min_size = wxSize(640, 640), const wxSize& min_size = wxSize(640, 640));
const wxString& profile = wxEmptyString);
void load_url(const wxString& url); void load_url(const wxString& url);
bool run_script(const wxString& script); bool run_script(const wxString& script);
-15
View File
@@ -1,15 +0,0 @@
#ifndef slic3r_wxinit_hpp_
#define slic3r_wxinit_hpp_
#include <wx/wx.h>
#include <wx/intl.h>
#include <wx/html/htmlwin.h>
// Perl redefines a _ macro, so we undef this one
#undef _
// We do want to use translation however, so define it as __ so we can do a find/replace
// later when we no longer need to undef _
#define __(s) wxGetTranslation((s))
#endif
+50 -175
View File
@@ -3,240 +3,115 @@
#include <algorithm> #include <algorithm>
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <cmath>
#include <condition_variable> #include <condition_variable>
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/libslic3r.h"
#include <exception>
#include <cstddef> #include <cstddef>
#include "libslic3r/Exception.hpp" #include <exception>
#include <limits>
#include <mutex> #include <mutex>
#include "slic3r/GUI/Widgets/ProgressDialog.hpp"
#include <optional>
#include <string> #include <string>
#include <thread> #include <thread>
#include <utility>
#include <vector> #include <vector>
#include <wx/string.h> #include <wx/string.h>
#include "libslic3r/MeshBoolean.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Format/bbs_3mf.hpp" #include "libslic3r/Format/bbs_3mf.hpp"
#include "libslic3r/format.hpp" #include "libslic3r/MeshRepair.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/Thread.hpp" #include "libslic3r/Thread.hpp"
#include "../GUI/I18N.hpp" #include "slic3r/GUI/I18N.hpp"
#include "libslic3r/TriangleSelector.hpp" #include "slic3r/GUI/Widgets/ProgressDialog.hpp"
// Orca: This file provides utilities for repairing 3D model meshes using the CGAL library, handling mesh splitting, merging, and boolean operations.
namespace Slic3r { namespace Slic3r {
namespace { namespace {
// Orca: Helper functions for analyzing mesh properties and transformations. class RepairCanceledException : public std::exception
bool is_not_3dimensional_part(const TriangleMesh &mesh)
{ {
// Orca: Determines if a mesh is degenerate or represents a non-3dimensional part by checking volume and bounding box dimensions.
if (mesh.its.indices.empty())
return true;
indexed_triangle_set tmp = mesh.its;
its_remove_degenerate_faces(tmp, true);
if (tmp.indices.empty())
return true;
const BoundingBoxf3 bbox = mesh.bounding_box();
const Vec3d size = bbox.size();
const double min_dim = std::min(size.x(), std::min(size.y(), size.z()));
const double max_dim = std::max(size.x(), std::max(size.y(), size.z()));
if (min_dim <= EPSILON)
return true;
const double volume = std::abs(its_volume(mesh.its));
const double bbox_volume = size.x() * size.y() * size.z();
if (volume <= EPSILON)
return true;
const double min_relative_thickness = 1e-6;
const double min_volume_ratio = 1e-6;
if (min_dim / max_dim <= min_relative_thickness)
return true;
if (bbox_volume > 0.0 && volume / bbox_volume <= min_volume_ratio)
return true;
return false;
}
} // namespace
// Orca: Exception class for handling user-initiated cancellation of model repair operations.
class RepairCanceledException : public std::exception {
public: public:
const char *what() const noexcept override { return "Model repair has been canceled"; } const char *what() const noexcept override { return "Model repair has been canceled"; }
}; };
// Orca: Main function to repair model objects using CGAL, with progress dialog and cancellation support. } // namespace
// Returns false if fixing was canceled. fix_result contains error message if failed.
bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::ProgressDialog &progress_dialog, const wxString &msg_header, std::string &fix_result, bool keep_painting) bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::ProgressDialog &progress_dialog, const wxString &msg_header, std::string &fix_result, bool keep_painting,
bool *modified)
{ {
// Hold SaveObjectGaurd to prevent backup manager from racing concurrent mesh mutations (use-after-free). // Keeps the backup manager from reading a mesh while the worker replaces it.
SaveObjectGaurd backup_gaurd(model_object); SaveObjectGaurd backup_gaurd(model_object);
// Orca: Synchronization primitives for progress updates between worker thread and GUI. std::vector<ModelVolume *> volumes;
if (volume_idx < 0)
volumes = model_object.volumes;
else if (std::size_t(volume_idx) < model_object.volumes.size())
volumes.push_back(model_object.volumes[volume_idx]);
std::mutex mtx; std::mutex mtx;
std::condition_variable condition; std::condition_variable condition;
struct Progress { struct Progress
{
std::string message; std::string message;
int percent = 0; int percent = 0;
bool updated = false; bool updated = false;
} progress; } progress;
std::atomic<bool> canceled = false; std::atomic<bool> canceled = false;
std::atomic<bool> finished = false; std::atomic<bool> finished = false;
bool any_modified = false;
std::string errors;
bool success = false; auto on_progress = [&mtx, &condition, &progress](const std::string &msg, int percent) {
size_t ivolume = 0; std::lock_guard<std::mutex> lock(mtx);
// Orca: Lambda for updating progress from worker thread.
auto on_progress = [&mtx, &condition, &ivolume, &model_object, &progress](const std::string &msg, unsigned prcnt) {
std::unique_lock<std::mutex> lock(mtx);
progress.message = msg; progress.message = msg;
const size_t total = std::max<size_t>(1, model_object.volumes.size()); progress.percent = percent;
progress.percent = int(std::floor((float(prcnt) + float(ivolume) * 100.f) / float(total)));
progress.updated = true; progress.updated = true;
condition.notify_all(); condition.notify_all();
}; };
// Orca: Worker thread that performs the actual model repair operations. auto worker_thread = std::thread([&]() {
auto worker_thread = std::thread([&model_object, volume_idx, &ivolume, on_progress, &success, &canceled, &finished, &fix_result, keep_painting]() {
try {
set_current_thread_name("cgal_fix_model"); set_current_thread_name("cgal_fix_model");
try {
size_t start_volume = volume_idx == -1 ? 0 : size_t(volume_idx); for (std::size_t i = 0; i < volumes.size(); ++i) {
size_t end_volume = volume_idx == -1 ? std::numeric_limits<size_t>::max() : size_t(volume_idx); ModelVolume *volume = volumes[i];
const std::string name = volumes.size() > 1 ? volume->name : std::string();
for (ivolume = start_volume; ivolume < model_object.volumes.size(); ++ivolume) { const auto report = [&](int percent) {
if (volume_idx != -1 && ivolume > end_volume)
break;
if (canceled) if (canceled)
throw RepairCanceledException(); throw RepairCanceledException();
on_progress(name, int((100 * i + std::size_t(percent)) / volumes.size()));
};
report(0);
on_progress(_u8L("Repairing model object"), 10); MeshRepairStats stats;
ModelVolume *volume = model_object.volumes[ivolume];
// Orca: Split splittable volumes into parts for individual processing.
size_t parts_count = 1;
if (volume->is_splittable()) {
parts_count = volume->split(1, keep_painting);
if (parts_count > 1) {
const std::string msg = Slic3r::format(L("Split into %1% parts"), parts_count);
on_progress(msg, 10);
}
}
size_t part_end = std::min(ivolume + parts_count - 1, model_object.volumes.size() - 1);
if (volume_idx != -1)
end_volume = part_end;
size_t removed_parts = 0;
for (size_t idx = part_end + 1; idx > ivolume; --idx) {
const size_t part_idx = idx - 1;
const ModelVolume *part_volume = model_object.volumes[part_idx];
if (!is_not_3dimensional_part(part_volume->mesh()))
continue;
model_object.delete_volume(part_idx);
++removed_parts;
if (part_end > 0)
--part_end;
else
part_end = 0;
if (volume_idx != -1)
end_volume = part_end;
}
if (removed_parts >= parts_count) {
ivolume = part_end;
on_progress(_u8L("Repair finished"), 100);
continue;
}
for (size_t part_idx = ivolume; part_idx <= part_end && part_idx < model_object.volumes.size(); ++part_idx) {
ModelVolume *part_volume = model_object.volumes[part_idx];
TriangleMesh mesh = part_volume->mesh();
if (its_num_open_edges(mesh.its) != 0) {
// Save painting for later remap
const std::optional<TriangleSelector::SavedPainting> saved_painting = keep_painting ?
part_volume->save_painting() :
std::optional<TriangleSelector::SavedPainting>{};
std::string error; std::string error;
if (!MeshBoolean::cgal::repair(mesh, nullptr, &error)) if (!volume->repair(keep_painting, &stats, &error, report))
throw Slic3r::RuntimeError(error.empty() ? _u8L("Repair failed") : error); errors += (errors.empty() ? "" : "\n") + (volumes.size() > 1 ? volume->name + ": " : std::string()) + error;
any_modified = any_modified || stats.modified;
part_volume->set_mesh(std::move(mesh));
part_volume->calculate_convex_hull();
part_volume->invalidate_convex_hull_2d();
part_volume->set_new_unique_id();
// Remap paint back
part_volume->restore_painting(saved_painting);
} }
} } catch (const RepairCanceledException &) {
ivolume = part_end;
on_progress(_u8L("Repair finished"), 100);
}
model_object.invalidate_bounding_box();
if (ivolume > 0)
--ivolume;
on_progress(_u8L("Repair finished"), 100);
success = true;
finished = true;
} catch (RepairCanceledException &) {
canceled = true; canceled = true;
finished = true; } catch (const std::exception &ex) {
on_progress(_u8L("Repair canceled"), 100); errors += (errors.empty() ? "" : "\n") + std::string(ex.what());
} catch (std::exception &ex) {
success = false;
finished = true;
fix_result = ex.what();
on_progress(ex.what(), 100);
} }
// Volumes repaired before a cancel or an error keep their new mesh.
model_object.invalidate_bounding_box();
finished = true;
on_progress(_u8L("Repair finished"), 100);
}); });
// Orca: Main GUI loop to update progress dialog and handle cancellation.
while (!finished) { while (!finished) {
std::unique_lock<std::mutex> lock(mtx); std::unique_lock<std::mutex> lock(mtx);
condition.wait_for(lock, std::chrono::milliseconds(250), [&progress] { return progress.updated; }); condition.wait_for(lock, std::chrono::milliseconds(250), [&progress] { return progress.updated; });
// One percent short of the maximum, so that the dialog does not close itself.
// Decrease progress percent slightly to avoid auto-closing. if (!progress_dialog.Update(std::max(0, progress.percent - 1), msg_header + wxString::FromUTF8(progress.message)))
if (!progress_dialog.Update(progress.percent - 1, msg_header + _(progress.message)))
canceled = true; canceled = true;
else else
progress_dialog.Fit(); progress_dialog.Fit();
progress.updated = false; progress.updated = false;
} }
if (canceled) {
// Nothing to show.
} else if (success) {
fix_result.clear();
}
if (worker_thread.joinable()) if (worker_thread.joinable())
worker_thread.join(); worker_thread.join();
fix_result = canceled ? std::string() : errors;
if (modified)
*modified = any_modified;
return !canceled; return !canceled;
} }
+3 -2
View File
@@ -12,8 +12,9 @@ class Model;
class ModelObject; class ModelObject;
class Print; class Print;
// Return false if fixing was canceled. fix_result is empty on success. // Return false if fixing was canceled. fix_result is empty on success; modified tells whether any mesh changed.
extern bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::ProgressDialog &progress_dlg, const wxString &msg_header, std::string &fix_result, bool keep_painting); extern bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::ProgressDialog &progress_dlg, const wxString &msg_header, std::string &fix_result, bool keep_painting,
bool *modified = nullptr);
} // namespace Slic3r } // namespace Slic3r
-1
View File
@@ -12,7 +12,6 @@ extern double mac_max_scaling_factor();
extern void set_miniaturizable(void * window); extern void set_miniaturizable(void * window);
void WKWebView_evaluateJavaScript(void * web, wxString const & script, void (*callback)(wxString const &)); void WKWebView_evaluateJavaScript(void * web, wxString const & script, void (*callback)(wxString const &));
void WKWebView_setTransparentBackground(void * web); void WKWebView_setTransparentBackground(void * web);
void WKWebViewConfiguration_setNonPersistentDataStore(void * configuration);
void WKWebView_force_display(void * web); void WKWebView_force_display(void * web);
void set_tag_when_enter_full_screen(bool isfullscreen); void set_tag_when_enter_full_screen(bool isfullscreen);
void set_title_colour_after_set_title(void * window); void set_title_colour_after_set_title(void * window);
-9
View File
@@ -102,15 +102,6 @@ void WKWebView_setTransparentBackground(void * web)
[webView registerForDraggedTypes: @[NSFilenamesPboardType]]; [webView registerForDraggedTypes: @[NSFilenamesPboardType]];
} }
// Give a WKWebViewConfiguration its own in-memory website data store, so the view's cookies and
// storage neither come from nor leak into the shared default store. Used to isolate plugin web views.
void WKWebViewConfiguration_setNonPersistentDataStore(void * configuration)
{
WKWebViewConfiguration * config = (WKWebViewConfiguration*)configuration;
if (config)
config.websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore];
}
// Force a WKWebView to re-lay-out and repaint. Needed for chrome-less popups: the window is // Force a WKWebView to re-lay-out and repaint. Needed for chrome-less popups: the window is
// transparent, so a WKWebView whose layer has no pending frame leaves the whole window invisible // transparent, so a WKWebView whose layer has no pending frame leaves the whole window invisible
// until the user generates input (scroll/arrow). setNeedsDisplay alone does not always reach the // until the user generates input (scroll/arrow). setNeedsDisplay alone does not always reach the
-19
View File
@@ -1,19 +0,0 @@
#ifndef slic3r_GUI_Profile_hpp_
#define slic3r_GUI_Profile_hpp_
// Profiling support using the Shiny intrusive profiler
//#define SLIC3R_PROFILE_GUI
#if defined(SLIC3R_PROFILE) && defined(SLIC3R_PROFILE_GUI)
#include <Shiny/Shiny.h>
#define SLIC3R_GUI_PROFILE_FUNC() PROFILE_FUNC()
#define SLIC3R_GUI_PROFILE_BLOCK(name) PROFILE_BLOCK(name)
#define SLIC3R_GUI_PROFILE_UPDATE() PROFILE_UPDATE()
#define SLIC3R_GUI_PROFILE_OUTPUT(x) PROFILE_OUTPUT(x)
#else
#define SLIC3R_GUI_PROFILE_FUNC()
#define SLIC3R_GUI_PROFILE_BLOCK(name)
#define SLIC3R_GUI_PROFILE_UPDATE()
#define SLIC3R_GUI_PROFILE_OUTPUT(x)
#endif
#endif // slic3r_GUI_Profile_hpp_
+2 -3
View File
@@ -390,8 +390,7 @@ py::object ui_create_window(const std::string& html, const std::string& title, i
auto* dlg = new GUI::WebDialog(ui_parent(), wxString::FromUTF8(title), html, auto* dlg = new GUI::WebDialog(ui_parent(), wxString::FromUTF8(title), html,
wxSize(w, h), std::move(msg_adapter), std::move(submit_adapter), wxSize(w, h), std::move(msg_adapter), std::move(submit_adapter),
std::move(on_close), std::move(on_destroyed), PLUGIN_WX_STYLE, std::move(on_close), std::move(on_destroyed), PLUGIN_WX_STYLE);
wxString::FromUTF8(plugin_key));
UiRegistry::instance().bind(new_id, dlg, plugin_key); UiRegistry::instance().bind(new_id, dlg, plugin_key);
if (modal) { if (modal) {
dlg->ShowModal(); dlg->ShowModal();
@@ -488,7 +487,7 @@ py::object ui_create_dock_panel(const std::string& html, const std::string& titl
auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); }; auto on_destroyed = [new_id]() { UiRegistry::instance().remove(new_id); };
auto* panel = new GUI::DockPanel(plater, html, std::move(msg_adapter), make_close_adapter(close_holder), auto* panel = new GUI::DockPanel(plater, html, std::move(msg_adapter), make_close_adapter(close_holder),
std::move(on_destroyed), wxString::FromUTF8(plugin_key)); std::move(on_destroyed));
UiRegistry::instance().bind(new_id, panel, plugin_key); UiRegistry::instance().bind(new_id, panel, plugin_key);
plater->add_dock_pane(panel, GUI::plugin_pane_name(plugin_key, title), wxString::FromUTF8(title), dock, plater->add_dock_pane(panel, GUI::plugin_pane_name(plugin_key, title), wxString::FromUTF8(title), dock,
wxSize(w, h), [panel]() { panel->fire_close(); }); wxSize(w, h), [panel]() { panel->fire_close(); });
+1 -2
View File
@@ -77,8 +77,7 @@ constexpr char PLUGIN_PAGE_BRIDGE_JS[] = R"JS(
} // namespace } // namespace
PluginPage::PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability) PluginPage::PluginPage(wxWindow* parent, std::shared_ptr<PagesPluginCapability> capability)
: GUI::WebPanel(parent, PLUGIN_PAGE_BRIDGE_JS, : GUI::WebPanel(parent, PLUGIN_PAGE_BRIDGE_JS)
wxString::FromUTF8(capability ? capability->identity().plugin_key : std::string()))
, m_cap(std::move(capability)) , m_cap(std::move(capability))
, m_lifetime(std::make_shared<std::atomic<PluginPage*>>(this)) , m_lifetime(std::make_shared<std::atomic<PluginPage*>>(this))
{ {
+16
View File
@@ -31,6 +31,22 @@ set_tests_properties(cli_project_missing_keys PROPERTIES
SKIP_RETURN_CODE 77 SKIP_RETURN_CODE 77
TIMEOUT 900) TIMEOUT 900)
add_test(NAME cli_arrange_rotations
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_arrange_rotations.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON}
${CMAKE_SOURCE_DIR}/resources/profiles/BBL)
set_tests_properties(cli_arrange_rotations PROPERTIES
LABELS "CLI;RequiresApp"
SKIP_RETURN_CODE 77
TIMEOUT 900)
add_test(NAME cli_no_suitable_objects
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_no_suitable_objects.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON}
${CMAKE_SOURCE_DIR}/resources/profiles/BBL)
set_tests_properties(cli_no_suitable_objects PROPERTIES
LABELS "CLI;RequiresApp"
SKIP_RETURN_CODE 77
TIMEOUT 900)
add_test(NAME cli_malformed_input add_test(NAME cli_malformed_input
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_malformed_input.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON}) COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_malformed_input.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON})
set_tests_properties(cli_malformed_input PROPERTIES set_tests_properties(cli_malformed_input PROPERTIES
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
# End-to-end check of the CLI's arrange rotation policy.
#
# Arrange is translate-only unless --allow-rotations is passed, so a bar that fits the bed only
# diagonally is not placed and the plate ends up with nothing fully inside the print volume. The
# run must then say that rotations were off, on stderr as well as in result.json, because
# result.json is written on Linux only. Passing the flag must place the same bar and slice it, and
# an object that fits axis-aligned must be unaffected by the default.
#
# usage: test_cli_arrange_rotations.sh <orca-slicer binary> <python3> <resources/profiles/BBL>
set -u
BIN="${1:-}"
PY="${2:-python3}"
PROFILES="${3:-}"
# 77 is the test's SKIP_RETURN_CODE.
[ -x "$BIN" ] || { echo "SKIP: orca-slicer binary not found: $BIN"; exit 77; }
[ -d "$PROFILES" ] || { echo "FAIL: profiles directory not found: $PROFILES"; exit 1; }
WORK="$(mktemp -d "${TMPDIR:-/tmp}/orca-cli-arrange-rotations.XXXXXX")"
trap 'rm -rf "$WORK"' EXIT
# box <file> <x> <y> <z>: an ASCII STL box, so the test needs no fixture.
box() {
"$PY" - "$@" <<'EOF'
import sys
out, sx, sy, sz = sys.argv[1], float(sys.argv[2]), float(sys.argv[3]), float(sys.argv[4])
v = [(x, y, z) for z in (0, sz) for y in (0, sy) for x in (0, sx)]
with open(out, "w") as f:
f.write("solid box\n")
for a, b, c, d in ((0, 2, 3, 1), (4, 5, 7, 6), (0, 1, 5, 4), (2, 6, 7, 3), (0, 4, 6, 2), (1, 3, 7, 5)):
for tri in ((v[a], v[b], v[c]), (v[a], v[c], v[d])):
f.write("facet normal 0 0 0\nouter loop\n")
for p in tri:
f.write("vertex %g %g %g\n" % p)
f.write("endloop\nendfacet\n")
f.write("endsolid box\n")
EOF
}
# Longer than the 256 mm bed edge, shorter than its 362 mm diagonal: it fits only turned.
box "$WORK/bar.stl" 300 10 5
# Fits axis-aligned, so the default must not change anything for it.
box "$WORK/cube.stl" 10 10 10
# slice <tag> <input> [option...] -> $WORK/<tag>/{rc,stdout,stderr,result.json}
slice() {
local out="$WORK/$1" input="$2"; shift 2
mkdir -p "$out"
timeout 300 "$BIN" --datadir "$out/datadir" \
--load-settings "$PROFILES/machine/Bambu Lab X1 Carbon 0.4 nozzle.json;$PROFILES/process/0.20mm Standard @BBL X1C.json" \
--load-defaultfila "$@" --slice 0 --outputdir "$out" "$input" \
> "$out/stdout" 2> "$out/stderr"
echo "$?" > "$out/rc"
}
# exit_code <tag>: the CLI's negative code as the shell saw it, e.g. -50 arrives as 206.
exit_code() { "$PY" -c "import sys; rc = int(open(sys.argv[1]).read()); print(rc - 256 if rc > 127 else rc)" "$WORK/$1/rc"; }
fail() { echo "FAIL: $*"; exit 1; }
# 1. Default: no flag, so no rotations, so the bar does not fit and the reason names rotation.
slice default "$WORK/bar.stl"
[ "$(exit_code default)" = "-50" ] || fail "default: expected -50, got $(exit_code default)"
grep -q "Arrange ran without rotations" "$WORK/default/stderr" \
|| fail "default: stderr does not mention rotations: $(cat "$WORK/default/stderr")"
"$PY" - "$WORK/default/result.json" <<'EOF' || exit 1
import json, sys
result = json.load(open(sys.argv[1]))
if result["return_code"] != -50:
sys.exit("FAIL: result.json return_code %s, expected -50" % result["return_code"])
if "Arrange ran without rotations" not in result["error_string"]:
sys.exit("FAIL: result.json error_string does not mention rotations: %s" % result["error_string"])
EOF
# 2. The flag is coBool, so it takes no value: it must place the same bar and slice it.
slice rotations "$WORK/bar.stl" --allow-rotations
[ "$(exit_code rotations)" = "0" ] || fail "--allow-rotations: expected 0, got $(exit_code rotations); $(tail -n 5 "$WORK/rotations/stdout")"
[ -s "$WORK/rotations/plate_1.gcode" ] || fail "--allow-rotations: no g-code written"
# 3. An object that fits axis-aligned is unaffected by the default, and says nothing about rotation.
slice cube "$WORK/cube.stl"
[ "$(exit_code cube)" = "0" ] || fail "cube: expected 0, got $(exit_code cube); $(tail -n 5 "$WORK/cube/stdout")"
[ -s "$WORK/cube/plate_1.gcode" ] || fail "cube: no g-code written"
grep -q "Arrange ran without rotations" "$WORK/cube/stderr" && fail "cube: unexpected rotation message on a successful run"
echo "PASS: arrange rotation default, its failure message, and the opt-in flag"
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
# The reason recorded for CLI_NO_SUITABLE_OBJECTS (-50) has to fit the input the CLI was given.
# Any input format is arranged and sliced, so an object larger than the bed empties the plate just
# as a 3mf with an empty plate does, and the recorded reason must not send such a caller off to
# inspect a 3mf or an upload that is not part of their run.
#
# usage: test_cli_no_suitable_objects.sh <orca-slicer binary> <python3> <resources/profiles/BBL>
set -u
BIN="${1:-}"
PY="${2:-python3}"
PROFILES="${3:-}"
# 77 is the test's SKIP_RETURN_CODE.
[ -x "$BIN" ] || { echo "SKIP: orca-slicer binary not found: $BIN"; exit 77; }
[ -d "$PROFILES" ] || { echo "FAIL: profiles directory not found: $PROFILES"; exit 1; }
WORK="$(mktemp -d "${TMPDIR:-/tmp}/orca-cli-no-suitable-objects.XXXXXX")"
trap 'rm -rf "$WORK"' EXIT
# box <file> <x> <y> <z>: an ASCII STL box, so the test needs no fixture.
box() {
"$PY" - "$@" <<'EOF'
import sys
out, sx, sy, sz = sys.argv[1], float(sys.argv[2]), float(sys.argv[3]), float(sys.argv[4])
v = [(x, y, z) for z in (0, sz) for y in (0, sy) for x in (0, sx)]
with open(out, "w") as f:
f.write("solid box\n")
for a, b, c, d in ((0, 2, 3, 1), (4, 5, 7, 6), (0, 1, 5, 4), (2, 6, 7, 3), (0, 4, 6, 2), (1, 3, 7, 5)):
for tri in ((v[a], v[b], v[c]), (v[a], v[c], v[d])):
f.write("facet normal 0 0 0\nouter loop\n")
for p in tri:
f.write("vertex %g %g %g\n" % p)
f.write("endloop\nendfacet\n")
f.write("endsolid box\n")
EOF
}
# Wider than the 256 mm bed and wider than its 362 mm diagonal, so no placement can fit it.
box "$WORK/slab.stl" 400 400 5
# Fits, so the run must succeed: proves the harness itself is sound.
box "$WORK/cube.stl" 10 10 10
# slice <tag> <input> [option...] -> $WORK/<tag>/{rc,stdout,stderr,result.json}
slice() {
local out="$WORK/$1" input="$2"; shift 2
mkdir -p "$out"
timeout 300 "$BIN" --datadir "$out/datadir" \
--load-settings "$PROFILES/machine/Bambu Lab X1 Carbon 0.4 nozzle.json;$PROFILES/process/0.20mm Standard @BBL X1C.json" \
"$@" --slice 0 --outputdir "$out" "$input" \
> "$out/stdout" 2> "$out/stderr"
echo "$?" > "$out/rc"
}
# exit_code <tag>: the CLI's negative code as the shell saw it, e.g. -50 arrives as 206.
exit_code() { "$PY" -c "import sys; rc = int(open(sys.argv[1]).read()); print(rc - 256 if rc > 127 else rc)" "$WORK/$1/rc"; }
fail() { echo "FAIL: $*"; exit 1; }
# --allow-rotations keeps this on the generic reason: a translate-only arrange has its own,
# more specific one. The slab is wider than the 362 mm bed diagonal, so rotation cannot place it.
slice slab "$WORK/slab.stl" --allow-rotations
[ "$(exit_code slab)" = "-50" ] || fail "slab: expected -50, got $(exit_code slab); $(tail -n 5 "$WORK/slab/stdout")"
"$PY" - "$WORK/slab/result.json" <<'EOF' || exit 1
import json, sys
result = json.load(open(sys.argv[1]))
if result["return_code"] != -50:
sys.exit("FAIL: result.json return_code %s, expected -50" % result["return_code"])
reason = result["error_string"]
if not reason.strip():
sys.exit("FAIL: result.json carries no error_string")
# The run was handed an STL and asked for g-code: neither a 3mf nor an upload exists to check.
for word in ("3mf", "3MF", "upload"):
if word in reason:
sys.exit("FAIL: error_string mentions %r for an STL run: %s" % (word, reason))
EOF
slice cube "$WORK/cube.stl"
[ "$(exit_code cube)" = "0" ] || fail "cube: expected 0, got $(exit_code cube); $(tail -n 5 "$WORK/cube/stdout")"
[ -s "$WORK/cube/plate_1.gcode" ] || fail "cube: no g-code written"
echo "PASS: the no-suitable-objects reason fits a non-3mf run"

Some files were not shown because too many files have changed in this diff Show More