mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-16 12:21:11 +00:00
Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a72aa236c4 | ||
|
|
8baeba1341 | ||
|
|
e0a6000d50 | ||
|
|
d2675f15d1 | ||
|
|
5371c85362 | ||
|
|
a243941b72 | ||
|
|
6a1a5d51e9 | ||
|
|
e625845b34 | ||
|
|
01b340dee1 | ||
|
|
00d11a7ba0 | ||
|
|
546b9c621d | ||
|
|
a13089d357 | ||
|
|
fb6c93963d | ||
|
|
7688f7a16c |
@@ -0,0 +1,239 @@
|
||||
# ChromaStack - High Level Design
|
||||
|
||||
## Purpose and scope
|
||||
|
||||
ChromaStack prints a colour picture on a single face of a model: a few filaments are printed one on
|
||||
top of another, the printer swaps filament at fixed layer boundaries, and the *height* of every
|
||||
pixel decides how many of those layers reach it. Filament is translucent, so a pixel that stops
|
||||
part-way through a run shows a blend of that run's filament and whatever is under it, which is
|
||||
where the colours come from - far more of them than there are filaments loaded.
|
||||
|
||||
This is that model inside OrcaSlicer, as a mode of a texture displacement layer. The picture and
|
||||
the filament stack are the input; the output is a grayscale height map and a set of filament swaps.
|
||||
Nothing about it is a new pipeline: the height map is handed to the layer the user painted, so the
|
||||
existing projection, preview and bake turn it into real geometry, and the swaps are handed to the
|
||||
object's height ranges, which is the slicer's own way of printing a Z band in another filament.
|
||||
|
||||
It does not import other filament-painting tools' project files, and it does not choose the filaments or
|
||||
their order - that is the user's artistic decision, and the panel exists to make its result
|
||||
visible before the print.
|
||||
|
||||
## The colour model
|
||||
|
||||
`libslic3r/ChromaStack.hpp` holds the whole of it, free of any GUI dependency, which is what lets the
|
||||
solve run against plain image bytes and be unit tested.
|
||||
|
||||
A **stack** (`ChromaStack`) is a list of **bands** (`ChromaBand`), bottom-up: a filament and
|
||||
how many layers it gets. Alongside them it carries the print's layer height, which the colours
|
||||
depend on directly, and the **base**: how many layers of solid backing the picture sits on.
|
||||
|
||||
The base is not a band. It is a measurement of the model - everything below the painted face, often
|
||||
a hundred layers or more on a tall part - while a band is a handful of layers the user chooses. It
|
||||
prints in the bottom band's filament, because that is what is loaded when the print starts, so it
|
||||
extends that band's height range down to the first layer instead of being a swap of its own. Only
|
||||
the bands are quantized into the height map, which is why the 255-layer limit
|
||||
(`CHROMA_MAX_RELIEF_LAYERS`) bounds the picture and not the stack.
|
||||
|
||||
Each filament carries its **transmission distance** (TD), the usual measure for how deep
|
||||
light gets into it: about 1 for an opaque filament, 4 to 7 for a usefully translucent one, 8 and
|
||||
above for a nearly clear one. TD lives in the filament preset, as
|
||||
`filament_transmission_distance` - it is a property of the material, so it belongs with the
|
||||
material and ships with a profile rather than being retyped per project. It has no effect on
|
||||
slicing.
|
||||
|
||||
`chroma_palette()` turns a stack into the colour it shows at every possible column height, which
|
||||
is all the solver ever needs. The model is Beer-Lambert absorption, layer by layer: a layer of
|
||||
thickness `h` in a filament of transmission distance `TD` passes
|
||||
|
||||
T = exp(-h / (CHROMA_TD_OPAQUE_FRACTION * TD / 3))
|
||||
|
||||
of the light from under it and shows its own colour with the rest, so walking up from an opaque
|
||||
base,
|
||||
|
||||
C_k = (1 - T_k) * c_k + T_k * C_(k-1), C_0 = c_1
|
||||
|
||||
The constants say what TD means in physical terms: a filament stops showing what is behind it at
|
||||
about a tenth of its TD number (`CHROMA_TD_OPAQUE_FRACTION`), and three e-folding lengths of
|
||||
absorption pass 5% of the light, which is where the eye stops seeing through. Those are calibrated
|
||||
against how the community's TD step prints behave, not derived from first principles - TD is an
|
||||
empirical scale.
|
||||
|
||||
How fast that happens is the single most useful fact about a stack, and `chroma_layers_to_opaque()`
|
||||
is it: a filament hides what is under it at about a tenth of its TD, so how many *layers* that takes
|
||||
depends entirely on how thick they are. TD 4 covers in two layers at 0.2 mm and in five at 0.08 mm.
|
||||
Below about two layers a band has no intermediate colours at all - the picture can only use the
|
||||
filament or not use it - which is why an otherwise sound stack prints as flat steps at a coarse
|
||||
layer height. The panel reports the number under the palette ramp, and warns when it falls below
|
||||
two, because nothing else makes it visible: the filaments look fine, the bands look long enough, and
|
||||
the result looks like the colour model is wrong when it is the layer height.
|
||||
|
||||
How many of a picture's layers actually earn their place is a second question, and
|
||||
`chroma_distinct_levels()` answers it: a band eventually hides what is under it, and another layer of
|
||||
it then shows the colour the one below already showed. It is measured against the palette rather than
|
||||
against TD, because how long a band keeps paying depends on what is under it as much as on the
|
||||
filament - a light band over a dark one is finished in four or five layers, while a dark one over a
|
||||
light one goes on moving the colour perceptibly well past the depth at which it is nominally opaque,
|
||||
since the last few percent of light coming through still shifts a near-black colour. `Trim` shortens
|
||||
each band to its last layer that still changes anything.
|
||||
|
||||
Beer-Lambert is also the only model that stays self-consistent applied per layer: transmittance
|
||||
multiplies, so a run of n layers of one filament passes `exp(-n h / d)` whether it is composited in
|
||||
one step or n, and the palette therefore does not depend on how the stack happens to be cut into
|
||||
bands. A unit test asserts exactly that. Compositing is done on linear light, because the quantity
|
||||
being attenuated is light rather than an sRGB code.
|
||||
|
||||
The base is assumed opaque. That is what makes a column's colour depend only on the layers above it,
|
||||
and it is why the panel warns when the base is thin.
|
||||
|
||||
## The solve
|
||||
|
||||
`chroma_solve()` maps each pixel of the picture to the column height whose palette colour is
|
||||
closest to it, perceptually: both sides go to CIELAB and the match is a CIE76 distance. A 1-D
|
||||
palette of a few dozen entries makes the extra accuracy of CIEDE2000 hard to see and the cost of it
|
||||
easy to feel - several hundred million comparisons for a large picture - so the cheaper metric is
|
||||
the deliberate choice here, unlike the texture colour quantizer, which matches against a
|
||||
two-dimensional palette of filament mixes and uses a precomputed CIEDE2000 cube.
|
||||
|
||||
Optional Floyd-Steinberg error diffusion, serpentine, trades the bands a photograph shows across a
|
||||
sky or a cheek for fine stipple. Off by default, since stipple is the artifact rather than the cure
|
||||
for flat-coloured artwork.
|
||||
|
||||
Three things come back: the per-pixel layer count, the same thing as an 8-bit grayscale height map
|
||||
(0 at the base, 255 at the top of the stack), and the palette colours chosen, as an RGB image. The
|
||||
quantization belongs to the solve rather than to the displacement because the relief's steps have
|
||||
to land on layer boundaries, and a grey level is the only channel the displacement reads.
|
||||
|
||||
The solve also reports the mean and worst colour difference it had to accept. That is the only
|
||||
honest answer to "will this stack reproduce my picture": under about 5 is a good match, over about
|
||||
15 means the filaments cannot reach the picture's colours and no number of layers will help.
|
||||
|
||||
## How it attaches to a texture displacement layer
|
||||
|
||||
A ChromaStack layer is an ordinary `TextureDisplacementLayer` whose height map is computed rather than
|
||||
imported:
|
||||
|
||||
- `chroma_source` keeps the picture the user chose, so the solve can be redone whenever the stack,
|
||||
the layer height or the picture changes;
|
||||
- `chroma` is the stack;
|
||||
- `image_data` - the same field every other layer uses - holds whatever the last solve produced.
|
||||
|
||||
So everything downstream needs no changes at all and knows nothing about colour stacks. It also
|
||||
means the solve writes `depth_mm` (the relief is exactly as tall as the layers above the base) and
|
||||
pins `invert`, `midlevel` and `smoothing`: blurring a solved height map moves its steps off the
|
||||
layer boundaries they were solved for.
|
||||
|
||||
Colour by filament swap and the layer's own `color_enabled` (one filament per triangle, via
|
||||
`mmu_segmentation_facets`) are mutually exclusive, and the panel enforces it. They are two
|
||||
different ways of colouring the same surface, and painting the relief per triangle would hide the
|
||||
very stack the heights were solved against.
|
||||
|
||||
## The filament swaps
|
||||
|
||||
`chroma_ranges()` turns the bands into Z bands in the object's own coordinates, merging any two
|
||||
adjacent bands that share a filament, since nothing is swapped between them. The panel writes them
|
||||
onto the object as `layer_config_ranges`, each range carrying `extruder` - which
|
||||
`apply_to_print_region_config()` spreads over every feature filament the range does not otherwise
|
||||
override, i.e. the whole band prints in that filament - and `layer_height`, both because the solve
|
||||
is only valid at the height it was run at and because `layer_height_profile_from_ranges()` reads
|
||||
that option without checking it is there.
|
||||
|
||||
Height ranges rather than colour-change custom G-code: they are object-relative, so they survive
|
||||
the object being moved, they are saved in the project file, and they appear in the object list where
|
||||
the user can see and edit them. The cost is that an object can only have one set of them, so
|
||||
applying a stack replaces whatever ranges the object had, with a confirmation when it had any.
|
||||
|
||||
`ObjectList::rebuild_layer_ranges()` mirrors them in the object tree without touching the
|
||||
selection - changing the selection while a gizmo is open tears the gizmo down under the user.
|
||||
|
||||
## Geometry the model assumes
|
||||
|
||||
A filament swap happens at one Z across the whole plate, and the relief grows along the painted
|
||||
surface's normal. The two agree only where that surface is horizontal, so:
|
||||
|
||||
- the painted patch's height above the bottom of the object is what the base layer count has to
|
||||
match, and the panel measures it from the patch and offers the number rather than leaving the user
|
||||
to work it out;
|
||||
- a patch that is not flat in Z gets a warning, because its colours will only be right where it is
|
||||
level. The relief itself is still computed, since a gently domed plaque is a legitimate thing to
|
||||
print and only the user can judge how much error is acceptable.
|
||||
|
||||
The model itself is the user's: the stack needs a slab of the base thickness with a face to paint,
|
||||
and nothing here creates or thickens geometry.
|
||||
|
||||
## Seeing the colours
|
||||
|
||||
The colours are blends of translucent filaments. No single filament makes them, so they cannot be
|
||||
expressed as the per-triangle filament assignment the 3D scene paints with, and there is nothing for
|
||||
the ordinary view to draw. They exist in two places: the sliced preview, which shows the filament
|
||||
bands as they print, and an overlay the gizmo draws over whatever surface is on screen.
|
||||
|
||||
That overlay is a shader, `resources/shaders/*/chromastack.*`, and it reads the colour from each
|
||||
fragment's own **print Z** rather than from the height map or from anything in the mesh. A filament
|
||||
swap happens at one Z across the whole plate, so the layer a point sits in is what decides its
|
||||
colour whatever the picture says. Two things follow. A band edge lands exactly on the layer boundary
|
||||
however coarse the triangles are - a per-triangle colouring puts it wherever the mesh happens to
|
||||
have an edge. And it keeps working after a bake, when the relief is real geometry, the paint has
|
||||
been cleared and the height map is attached to nothing on screen; the overlay is built from the live
|
||||
preview's displaced mesh before a bake and from the volume's own mesh after one.
|
||||
|
||||
The stack's colours reach the shader as a texture of one texel per printed layer, because a base can
|
||||
be several hundred layers and no uniform array holds that. The mesh carries nothing about the stack,
|
||||
so editing the stack costs a palette upload rather than a mesh rebuild.
|
||||
|
||||
On top of the colour the shader tries to look like printed plastic, because a flat shaded colour
|
||||
reads as a rendering and the whole point of the overlay is judging a print:
|
||||
|
||||
- the terminator is **wrapped** and the grazing angles **glow**, both scaled by the transmission
|
||||
distance of the filament on top - filament translucent enough to be worth stacking scatters light
|
||||
rather than reflecting it, and an opaque stack correctly gets neither;
|
||||
- the walls carry **layer lines**, a groove at every layer boundary that also modulates the
|
||||
specular, so a wall glints in stripes as a printed one does. Terraces get none: a terrace is one
|
||||
layer's top surface and has no lines to show.
|
||||
|
||||
Both effects are driven by the fragment's height, so they are free of any texture or UV, and both
|
||||
fade out through `fwidth` once a pixel covers more than about half a layer - lines finer than a
|
||||
pixel are noise, and a colour read from one layer out of the dozen under a pixel flickers as the
|
||||
camera moves. The palette lookup slides off the texel centres over the same range so the linear
|
||||
filter averages the bands a pixel covers instead of picking one of them.
|
||||
|
||||
The overlay is drawn pulled forward and without depth writes, the same pass the painted-colour
|
||||
overlay uses, since it is the same surface as whatever is underneath it.
|
||||
|
||||
## What the panel shows
|
||||
|
||||
The kind of a layer is the layer's own, chosen when it is added and fixed from then on: **Add layer**
|
||||
offers *Relief layer* or *ChromaStack picture*, and the card carries its kind as a chip. There is no
|
||||
panel-wide mode, so one object can hold a picture on its face and a knurl on its rim, and a card
|
||||
never shows a control that cannot do anything on it.
|
||||
|
||||
What the two cards do not share:
|
||||
|
||||
| Control | Relief card | ChromaStack card |
|
||||
| --- | --- | --- |
|
||||
| Depth | slider | the solve writes it; the card prints the result |
|
||||
| Tile size | repeats the pattern | **Picture size**, the same number named for what it does |
|
||||
| Midlevel, Smoothing, Invert, Edge fade | present | absent: each would move the steps off the layer boundaries |
|
||||
| Colors, Mix filaments, Denoise | one filament per triangle | absent: swaps are how this card colours |
|
||||
| Tile, tile method | present | absent: one copy, never tiled |
|
||||
| Blend | four modes | absent: it is the top layer and replaces what is under it |
|
||||
| Picture, prediction, stack, palette, match, Dither, Solve, Apply swaps | absent | present |
|
||||
|
||||
A ChromaStack card has no grip and cannot be reordered: its colours come from swapping filament at a
|
||||
height, so anything printed above it would bury them. An object may hold only one, refused when the
|
||||
layer is added rather than when the swaps are applied, because the swaps are the object's own height
|
||||
ranges and there is one set of those.
|
||||
|
||||
The card itself, top to bottom: what it is in one line, the picture and the predicted print side by
|
||||
side at the same size, the match figures, the base with the measurement from the painted face offered
|
||||
beside it, a row per band - swatch, filament, layer count, TD - the colours the stack reaches as a
|
||||
ramp, the totals, dither with the layer height it is solved against, and the two actions. Base and
|
||||
band counts are typed fields rather than sliders: a base is measured in hundreds of layers and a band
|
||||
in single digits, and dragging past the end of a slider's range is how a stack silently loses its
|
||||
backing.
|
||||
|
||||
The ramp is computed from the stack alone, before any solve. It is what answers "can these filaments
|
||||
print this picture" without spending a solve to find out they cannot: a short or flat ramp means the
|
||||
filaments are too alike, or too opaque, to give the picture anything to work with.
|
||||
|
||||
A per-slot flag records that the stack has been edited since the last solve, because a stale height
|
||||
map looks exactly like a good one.
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="28" height="27" viewBox="0 0 28 27" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path transform="matrix(0.9 0 0 0.9 1.4 1.35)" d="M11.9888 2.01601C12.8984 0.494662 15.1024 0.494662 16.012 2.01601L18.8677 6.79237C19.1958 7.34113 19.7339 7.73201 20.3572 7.87447L25.7821 9.11446C27.5101 9.50943 28.1913 11.6056 27.0254 12.9408L23.3652 17.1327C22.9449 17.6143 22.7394 18.2469 22.7964 18.8836L23.2936 24.4263C23.4519 26.1918 21.6687 27.4873 20.0386 26.7911L14.9209 24.6056C14.3329 24.3545 13.6679 24.3545 13.0799 24.6056L7.96209 26.7911C6.33196 27.4873 4.54887 26.1918 4.70722 24.4263L5.20434 18.8836C5.26145 18.2469 5.05595 17.6143 4.63544 17.1327L0.97533 12.9408C-0.19048 11.6056 0.490599 9.50943 2.21859 9.11446L7.64364 7.87447C8.2669 7.73201 8.80494 7.34113 9.13302 6.79237L11.9888 2.01601Z" stroke="#949494" stroke-width="2.4" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 876 B |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Qidi",
|
||||
"version": "02.04.00.16",
|
||||
"version": "02.04.00.17",
|
||||
"force_update": "0",
|
||||
"description": "Qidi configurations",
|
||||
"machine_model_list": [
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
"extruder_clearance_height_to_lid": "120",
|
||||
"layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nLOG_Z\nG92 E0\n",
|
||||
"machine_end_gcode": "M141 S0\nM104 S0\nM140 S0\nG1 E-3 F1800\nG0 Z{min(max_print_height, max_layer_z + 3)} F600\nG0 X0 Y0 F12000\n{if max_layer_z < max_print_height / 2}G1 Z{max_print_height / 2 + 10} F600{else}G1 Z{min(max_print_height, max_layer_z + 3)}{endif}",
|
||||
"machine_start_gcode": "PRINT_START BED=[hot_plate_temp_initial_layer] HOTEND=[nozzle_temperature_initial_layer] CHAMBER=[chamber_temperature]\nM83\nM140 S[hot_plate_temp_initial_layer]\nM104 S[nozzle_temperature_initial_layer]\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 3} Z0\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 6}\nG1 Z1 F600\n",
|
||||
"machine_start_gcode": "PRINT_START BED=[bed_temperature_initial_layer_single] HOTEND=[nozzle_temperature_initial_layer] CHAMBER=[chamber_temperature]\nM83\nM140 S[bed_temperature_initial_layer_single]\nM104 S[nozzle_temperature_initial_layer]\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 3} Z0\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 6}\nG1 Z1 F600\n",
|
||||
"thumbnails_format": "PNG",
|
||||
"default_filament_profile": [
|
||||
"Generic PLA @Qidi Q1 Pro 0.4 nozzle"
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
"default_filament_profile": [
|
||||
"Generic PLA @Qidi"
|
||||
],
|
||||
"machine_start_gcode": "G28\nM140 S[hot_plate_temp_initial_layer]\nM190 S[hot_plate_temp_initial_layer]\nM109 S[nozzle_temperature_initial_layer]\nG92 E-19\nG0 Y5 Z0.3 F3600\nG1 X5 E0 F2400\n",
|
||||
"machine_start_gcode": "G28\nM140 S[bed_temperature_initial_layer_single]\nM190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nG92 E-19\nG0 Y5 Z0.3 F3600\nG1 X5 E0 F2400\n",
|
||||
"machine_end_gcode": "M104 S0\nM140 S0\n;Retract the filament\nG92 E0\nG1 E-3 F300\nG28\nM84\n",
|
||||
"scan_first_layer": "0"
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"210x210/COLPIC",
|
||||
"110x110/PNG"
|
||||
],
|
||||
"machine_start_gcode": "PRINT_START\nG28\nM141 S0\nG0 Z50 F600\nM190 S[hot_plate_temp_initial_layer]\nG28 Z\nG29; mesh bed leveling ,comment this code to close it\nG0 X0 Y0 Z50 F6000\nM141 S{overall_chamber_temperature}\nM109 S[nozzle_temperature_initial_layer]\nM106 P3 S255\nM83\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 12} E{-10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 E{10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\n",
|
||||
"machine_start_gcode": "PRINT_START\nG28\nM141 S0\nG0 Z50 F600\nM190 S[bed_temperature_initial_layer_single]\nG28 Z\nG29; mesh bed leveling ,comment this code to close it\nG0 X0 Y0 Z50 F6000\nM141 S{overall_chamber_temperature}\nM109 S[nozzle_temperature_initial_layer]\nM106 P3 S255\nM83\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 12} E{-10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 E{10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\n",
|
||||
"machine_end_gcode": "M141 S0\nM104 S0\nM140 S0\nG1 E-3 F1800\nG0 Z{min(max_print_height, max_layer_z + 3)} F600\nG0 X0 Y0 F12000\n{if max_layer_z < max_print_height / 2}G1 Z{max_print_height / 2 + 10} F600{else}G1 Z{min(max_print_height, max_layer_z + 3)}{endif}",
|
||||
"scan_first_layer": "0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
#version 110
|
||||
|
||||
// What a ChromaStack print looks like, shaded per fragment.
|
||||
//
|
||||
// The colour is not a vertex attribute and not a texture on the surface: it is read from the
|
||||
// fragment's own height. A filament swap happens at one print Z across the whole plate, so the layer
|
||||
// a point sits in is what decides its colour - which means a band edge lands exactly on the layer
|
||||
// boundary however coarse the mesh is, and the same shader is right before a bake (on the preview's
|
||||
// displaced mesh) and after one (on the volume's own).
|
||||
//
|
||||
// On top of that it tries to look like printed plastic rather than like flat shaded colour: the
|
||||
// terminator is wrapped and the grazing angles glow, because filament translucent enough to be worth
|
||||
// stacking scatters light rather than reflecting it; and the walls carry layer lines, because that is
|
||||
// the first thing the eye uses to read a print as a print.
|
||||
|
||||
#define INTENSITY_CORRECTION 0.6
|
||||
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
|
||||
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION)
|
||||
#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION)
|
||||
#define LIGHT_TOP_SHININESS 20.0
|
||||
const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074);
|
||||
#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION)
|
||||
#define INTENSITY_AMBIENT 0.3
|
||||
|
||||
// The stack's colours, one texel per printed layer, bottom to top. A texture rather than a uniform
|
||||
// array because a base can be several hundred layers and no uniform array survives that.
|
||||
uniform sampler2D palette;
|
||||
uniform float palette_size; // texels in `palette`, i.e. the stack's total printed layers
|
||||
uniform float base_z; // world Z of the bottom of the object
|
||||
uniform float layer_height; // mm
|
||||
// How translucent the stack reads, 0 to 1, from the transmission distance of the filament on top.
|
||||
// It drives the wrap and the rim: an opaque stack should not glow.
|
||||
uniform float translucency;
|
||||
uniform float alpha;
|
||||
|
||||
varying vec3 clipping_planes_dots;
|
||||
varying vec3 eye_normal;
|
||||
varying vec3 eye_position;
|
||||
varying vec3 world_normal;
|
||||
varying float world_z;
|
||||
|
||||
void main()
|
||||
{
|
||||
if (any(lessThan(clipping_planes_dots, vec3(0.0))))
|
||||
discard;
|
||||
|
||||
float z_rel = world_z - base_z;
|
||||
float layers = max(z_rel / layer_height, 0.0);
|
||||
// How much of a layer one pixel covers. Everything below is faded out as this grows: layer lines
|
||||
// finer than a pixel are noise, and a colour read from one layer out of the dozen under a pixel
|
||||
// flickers as the camera moves.
|
||||
float px = fwidth(layers);
|
||||
|
||||
// The layer this fragment sits in, 1-based. Up close that is an exact texel of the palette; once a
|
||||
// pixel spans a layer or more it slides to a continuous coordinate so the linear filter averages
|
||||
// the bands it covers instead of picking one of them at random.
|
||||
float stepped = clamp(ceil(layers - 1.0e-4), 1.0, palette_size) - 0.5;
|
||||
float smooth_ = clamp(layers, 0.5, palette_size - 0.5);
|
||||
float u = mix(stepped, smooth_, clamp(px - 0.5, 0.0, 1.0)) / palette_size;
|
||||
vec3 albedo = texture2D(palette, vec2(u, 0.5)).rgb;
|
||||
|
||||
vec3 N = normalize(eye_normal);
|
||||
vec3 V = normalize(-eye_position);
|
||||
|
||||
// Layer lines, on the walls only: a terrace is one layer's top surface and has none to show. The
|
||||
// groove runs round the part at every layer boundary, so it is a function of height alone.
|
||||
float wall = 1.0 - abs(world_normal.z);
|
||||
float phase = fract(layers);
|
||||
float groove = smoothstep(0.0, 0.16, phase) * smoothstep(1.0, 0.84, phase);
|
||||
groove = mix(groove, 0.65, clamp(px * 1.5, 0.0, 1.0)); // fade to flat when too fine to resolve
|
||||
float lines = mix(1.0, 0.84 + 0.16 * groove, wall * 0.9);
|
||||
albedo *= lines;
|
||||
|
||||
// Diffuse, with a wrapped terminator: light entering translucent filament comes back out a little
|
||||
// way round the curve, so the shaded side never goes as dark as an opaque surface's would.
|
||||
float wrap = 0.25 + 0.45 * translucency;
|
||||
float ndotl = dot(N, LIGHT_TOP_DIR);
|
||||
float diffuse = max((ndotl + wrap) / (1.0 + wrap), 0.0) * LIGHT_TOP_DIFFUSE;
|
||||
diffuse += max((dot(N, LIGHT_FRONT_DIR) + wrap) / (1.0 + wrap), 0.0) * LIGHT_FRONT_DIFFUSE;
|
||||
|
||||
// Specular, strongest along a layer line, which is what makes a printed wall glint in stripes
|
||||
// rather than evenly.
|
||||
float spec = LIGHT_TOP_SPECULAR * pow(max(dot(V, reflect(-LIGHT_TOP_DIR, N)), 0.0), LIGHT_TOP_SHININESS);
|
||||
spec *= mix(1.0, 0.35 + 0.65 * groove, wall * 0.9);
|
||||
|
||||
// Scattering out of the edges: a translucent part lights up where it is seen at a grazing angle
|
||||
// and where it is thin. Tinted by the filament, because what comes back out has been through it.
|
||||
float rim = pow(1.0 - clamp(dot(N, V), 0.0, 1.0), 4.0);
|
||||
|
||||
vec3 color = albedo * (INTENSITY_AMBIENT + diffuse) + vec3(spec) + albedo * rim * translucency * 0.55;
|
||||
gl_FragColor = vec4(color, alpha);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#version 110
|
||||
|
||||
uniform mat4 view_model_matrix;
|
||||
uniform mat4 projection_matrix;
|
||||
uniform mat3 view_normal_matrix;
|
||||
uniform mat4 volume_world_matrix;
|
||||
// The volume's normal matrix in world space, so the fragment shader can tell a wall from a terrace -
|
||||
// which is what decides whether layer lines show at all.
|
||||
uniform mat3 world_normal_matrix;
|
||||
// Clipping plane, x = min z, y = max z (the FFF/SLA preview's top/bottom cut).
|
||||
uniform vec2 z_range;
|
||||
// Clipping plane, general orientation (the section view).
|
||||
uniform vec4 clipping_plane;
|
||||
|
||||
// GLModel's P3N3 layout, which is what init_from(indexed_triangle_set) produces - this shader needs
|
||||
// nothing per vertex that the mesh does not already carry, because the colour comes from the
|
||||
// fragment's own height rather than from anything baked into the vertices.
|
||||
attribute vec3 v_position;
|
||||
attribute vec3 v_normal;
|
||||
|
||||
varying vec3 clipping_planes_dots;
|
||||
varying vec3 eye_normal;
|
||||
varying vec3 eye_position;
|
||||
varying vec3 world_normal;
|
||||
varying float world_z;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec4 model_pos = vec4(v_position, 1.0);
|
||||
vec4 world_pos = volume_world_matrix * model_pos;
|
||||
world_z = world_pos.z;
|
||||
world_normal = normalize(world_normal_matrix * v_normal);
|
||||
eye_normal = normalize(view_normal_matrix * v_normal);
|
||||
|
||||
vec4 eye_pos = view_model_matrix * model_pos;
|
||||
eye_position = eye_pos.xyz;
|
||||
gl_Position = projection_matrix * eye_pos;
|
||||
|
||||
clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z);
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
#version 140
|
||||
|
||||
// What a ChromaStack print looks like, shaded per fragment.
|
||||
//
|
||||
// The colour is not a vertex attribute and not a texture on the surface: it is read from the
|
||||
// fragment's own height. A filament swap happens at one print Z across the whole plate, so the layer
|
||||
// a point sits in is what decides its colour - which means a band edge lands exactly on the layer
|
||||
// boundary however coarse the mesh is, and the same shader is right before a bake (on the preview's
|
||||
// displaced mesh) and after one (on the volume's own).
|
||||
//
|
||||
// On top of that it tries to look like printed plastic rather than like flat shaded colour: the
|
||||
// terminator is wrapped and the grazing angles glow, because filament translucent enough to be worth
|
||||
// stacking scatters light rather than reflecting it; and the walls carry layer lines, because that is
|
||||
// the first thing the eye uses to read a print as a print.
|
||||
|
||||
#define INTENSITY_CORRECTION 0.6
|
||||
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
|
||||
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION)
|
||||
#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION)
|
||||
#define LIGHT_TOP_SHININESS 20.0
|
||||
const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074);
|
||||
#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION)
|
||||
#define INTENSITY_AMBIENT 0.3
|
||||
|
||||
// The stack's colours, one texel per printed layer, bottom to top. A texture rather than a uniform
|
||||
// array because a base can be several hundred layers and no uniform array survives that.
|
||||
uniform sampler2D palette;
|
||||
uniform float palette_size; // texels in `palette`, i.e. the stack's total printed layers
|
||||
uniform float base_z; // world Z of the bottom of the object
|
||||
uniform float layer_height; // mm
|
||||
// How translucent the stack reads, 0 to 1, from the transmission distance of the filament on top.
|
||||
// It drives the wrap and the rim: an opaque stack should not glow.
|
||||
uniform float translucency;
|
||||
uniform float alpha;
|
||||
|
||||
in vec3 clipping_planes_dots;
|
||||
in vec3 eye_normal;
|
||||
in vec3 eye_position;
|
||||
in vec3 world_normal;
|
||||
in float world_z;
|
||||
|
||||
out vec4 out_color;
|
||||
|
||||
void main()
|
||||
{
|
||||
if (any(lessThan(clipping_planes_dots, vec3(0.0))))
|
||||
discard;
|
||||
|
||||
float z_rel = world_z - base_z;
|
||||
float layers = max(z_rel / layer_height, 0.0);
|
||||
// How much of a layer one pixel covers. Everything below is faded out as this grows: layer lines
|
||||
// finer than a pixel are noise, and a colour read from one layer out of the dozen under a pixel
|
||||
// flickers as the camera moves.
|
||||
float px = fwidth(layers);
|
||||
|
||||
// The layer this fragment sits in, 1-based. Up close that is an exact texel of the palette; once a
|
||||
// pixel spans a layer or more it slides to a continuous coordinate so the linear filter averages
|
||||
// the bands it covers instead of picking one of them at random.
|
||||
float stepped = clamp(ceil(layers - 1.0e-4), 1.0, palette_size) - 0.5;
|
||||
float smooth_ = clamp(layers, 0.5, palette_size - 0.5);
|
||||
float u = mix(stepped, smooth_, clamp(px - 0.5, 0.0, 1.0)) / palette_size;
|
||||
vec3 albedo = texture(palette, vec2(u, 0.5)).rgb;
|
||||
|
||||
vec3 N = normalize(eye_normal);
|
||||
vec3 V = normalize(-eye_position);
|
||||
|
||||
// Layer lines, on the walls only: a terrace is one layer's top surface and has none to show. The
|
||||
// groove runs round the part at every layer boundary, so it is a function of height alone.
|
||||
float wall = 1.0 - abs(world_normal.z);
|
||||
float phase = fract(layers);
|
||||
float groove = smoothstep(0.0, 0.16, phase) * smoothstep(1.0, 0.84, phase);
|
||||
groove = mix(groove, 0.65, clamp(px * 1.5, 0.0, 1.0)); // fade to flat when too fine to resolve
|
||||
float lines = mix(1.0, 0.84 + 0.16 * groove, wall * 0.9);
|
||||
albedo *= lines;
|
||||
|
||||
// Diffuse, with a wrapped terminator: light entering translucent filament comes back out a little
|
||||
// way round the curve, so the shaded side never goes as dark as an opaque surface's would.
|
||||
float wrap = 0.25 + 0.45 * translucency;
|
||||
float ndotl = dot(N, LIGHT_TOP_DIR);
|
||||
float diffuse = max((ndotl + wrap) / (1.0 + wrap), 0.0) * LIGHT_TOP_DIFFUSE;
|
||||
diffuse += max((dot(N, LIGHT_FRONT_DIR) + wrap) / (1.0 + wrap), 0.0) * LIGHT_FRONT_DIFFUSE;
|
||||
|
||||
// Specular, strongest along a layer line, which is what makes a printed wall glint in stripes
|
||||
// rather than evenly.
|
||||
float spec = LIGHT_TOP_SPECULAR * pow(max(dot(V, reflect(-LIGHT_TOP_DIR, N)), 0.0), LIGHT_TOP_SHININESS);
|
||||
spec *= mix(1.0, 0.35 + 0.65 * groove, wall * 0.9);
|
||||
|
||||
// Scattering out of the edges: a translucent part lights up where it is seen at a grazing angle
|
||||
// and where it is thin. Tinted by the filament, because what comes back out has been through it.
|
||||
float rim = pow(1.0 - clamp(dot(N, V), 0.0, 1.0), 4.0);
|
||||
|
||||
vec3 color = albedo * (INTENSITY_AMBIENT + diffuse) + vec3(spec) + albedo * rim * translucency * 0.55;
|
||||
out_color = vec4(color, alpha);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#version 140
|
||||
|
||||
uniform mat4 view_model_matrix;
|
||||
uniform mat4 projection_matrix;
|
||||
uniform mat3 view_normal_matrix;
|
||||
uniform mat4 volume_world_matrix;
|
||||
// The volume's normal matrix in world space, so the fragment shader can tell a wall from a terrace -
|
||||
// which is what decides whether layer lines show at all.
|
||||
uniform mat3 world_normal_matrix;
|
||||
// Clipping plane, x = min z, y = max z (the FFF/SLA preview's top/bottom cut).
|
||||
uniform vec2 z_range;
|
||||
// Clipping plane, general orientation (the section view).
|
||||
uniform vec4 clipping_plane;
|
||||
|
||||
// GLModel's P3N3 layout, which is what init_from(indexed_triangle_set) produces - this shader needs
|
||||
// nothing per vertex that the mesh does not already carry, because the colour comes from the
|
||||
// fragment's own height rather than from anything baked into the vertices.
|
||||
in vec3 v_position;
|
||||
in vec3 v_normal;
|
||||
|
||||
out vec3 clipping_planes_dots;
|
||||
out vec3 eye_normal;
|
||||
out vec3 eye_position;
|
||||
out vec3 world_normal;
|
||||
out float world_z;
|
||||
|
||||
void main()
|
||||
{
|
||||
vec4 model_pos = vec4(v_position, 1.0);
|
||||
vec4 world_pos = volume_world_matrix * model_pos;
|
||||
world_z = world_pos.z;
|
||||
world_normal = normalize(world_normal_matrix * v_normal);
|
||||
eye_normal = normalize(view_normal_matrix * v_normal);
|
||||
|
||||
vec4 eye_pos = view_model_matrix * model_pos;
|
||||
eye_position = eye_pos.xyz;
|
||||
gl_Position = projection_matrix * eye_pos;
|
||||
|
||||
clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z);
|
||||
}
|
||||
@@ -325,6 +325,8 @@ set(lisbslic3r_sources
|
||||
Geometry/VoronoiUtils.cpp
|
||||
Geometry/VoronoiUtils.hpp
|
||||
Geometry/VoronoiVisualUtils.hpp
|
||||
ChromaStack.cpp
|
||||
ChromaStack.hpp
|
||||
IMEXArrange.cpp
|
||||
IMEXArrange.hpp
|
||||
IMEXHelpers.cpp
|
||||
|
||||
@@ -0,0 +1,394 @@
|
||||
#include "ChromaStack.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <cstdlib>
|
||||
#include <numeric>
|
||||
|
||||
#include <tbb/blocked_range.h>
|
||||
#include <tbb/parallel_for.h>
|
||||
|
||||
#include <miniz.h>
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// How many e-folding lengths of absorption count as "the colour beneath no longer shows". Three
|
||||
// passes 5% of the light, which is about where the eye stops seeing what is under a layer.
|
||||
static constexpr float OPAQUE_EFOLDS = 3.f;
|
||||
|
||||
// TD is a filament property, so a zero or negative value in a preset is a data error rather than a
|
||||
// request for an infinitely opaque filament. Clamp instead: below this the filament is opaque at a
|
||||
// single layer anyway.
|
||||
static constexpr float TD_MIN_MM = 0.05f;
|
||||
static constexpr float TD_MAX_MM = 100.f;
|
||||
|
||||
static float srgb_to_linear(float c)
|
||||
{
|
||||
return c <= 0.04045f ? c / 12.92f : std::pow((c + 0.055f) / 1.055f, 2.4f);
|
||||
}
|
||||
|
||||
static float linear_to_srgb(float c)
|
||||
{
|
||||
return c <= 0.0031308f ? c * 12.92f : 1.055f * std::pow(c, 1.f / 2.4f) - 0.055f;
|
||||
}
|
||||
|
||||
// CIELAB under D65, from non-linear sRGB in [0, 1]. Only used for nearest-colour search, so the
|
||||
// absolute white point matters far less than both sides using the same one.
|
||||
struct Lab
|
||||
{
|
||||
float l = 0.f, a = 0.f, b = 0.f;
|
||||
};
|
||||
|
||||
static Lab srgb_to_lab(float r, float g, float b)
|
||||
{
|
||||
const float rl = srgb_to_linear(r), gl = srgb_to_linear(g), bl = srgb_to_linear(b);
|
||||
const float x = (0.4124f * rl + 0.3576f * gl + 0.1805f * bl) / 0.95047f;
|
||||
const float y = (0.2126f * rl + 0.7152f * gl + 0.0722f * bl);
|
||||
const float z = (0.0193f * rl + 0.1192f * gl + 0.9505f * bl) / 1.08883f;
|
||||
auto f = [](float t) {
|
||||
return t > 0.008856f ? std::cbrt(t) : (7.787f * t + 16.f / 116.f);
|
||||
};
|
||||
const float fx = f(x), fy = f(y), fz = f(z);
|
||||
return Lab{ 116.f * fy - 16.f, 500.f * (fx - fy), 200.f * (fy - fz) };
|
||||
}
|
||||
|
||||
static float delta_e(const Lab &p, const Lab &q)
|
||||
{
|
||||
const float dl = p.l - q.l, da = p.a - q.a, db = p.b - q.b;
|
||||
return std::sqrt(dl * dl + da * da + db * db);
|
||||
}
|
||||
|
||||
int ChromaStack::relief_layers() const
|
||||
{
|
||||
int n = 0;
|
||||
for (const ChromaBand &band : bands)
|
||||
n += std::max(0, band.layers);
|
||||
return n;
|
||||
}
|
||||
|
||||
int ChromaStack::total_layers() const { return std::max(0, base_layers) + relief_layers(); }
|
||||
|
||||
float ChromaStack::depth_mm() const { return float(relief_layers()) * layer_height_mm; }
|
||||
|
||||
std::vector<ColorRGB> chroma_palette(const ChromaStack &stack)
|
||||
{
|
||||
const int total = stack.total_layers();
|
||||
// No bands means no picture and no filament to print the base in, whatever base_layers says.
|
||||
if (total <= 0 || stack.bands.empty() || stack.layer_height_mm <= 0.f)
|
||||
return {};
|
||||
|
||||
// The filament each printed layer is in, bottom-up: the base first, in the bottom band's filament,
|
||||
// then the bands themselves.
|
||||
std::vector<const ChromaFilament *> per_layer;
|
||||
per_layer.reserve(size_t(total));
|
||||
for (int i = 0; i < std::max(0, stack.base_layers); ++i)
|
||||
per_layer.push_back(&stack.bands.front().filament);
|
||||
for (const ChromaBand &band : stack.bands)
|
||||
for (int i = 0; i < band.layers; ++i)
|
||||
per_layer.push_back(&band.filament);
|
||||
|
||||
std::vector<ColorRGB> palette(size_t(total) + 1);
|
||||
|
||||
// Linear light, walking up from an opaque base: C_0 is the bottom layer's own colour, so a
|
||||
// column one layer tall comes out exactly that colour however translucent the filament is.
|
||||
std::array<float, 3> acc{ srgb_to_linear(per_layer.front()->color.r()), srgb_to_linear(per_layer.front()->color.g()),
|
||||
srgb_to_linear(per_layer.front()->color.b()) };
|
||||
auto store = [&palette](size_t k, const std::array<float, 3> &lin) {
|
||||
palette[k] = ColorRGB(std::clamp(linear_to_srgb(lin[0]), 0.f, 1.f), std::clamp(linear_to_srgb(lin[1]), 0.f, 1.f),
|
||||
std::clamp(linear_to_srgb(lin[2]), 0.f, 1.f));
|
||||
};
|
||||
store(0, acc);
|
||||
|
||||
for (int k = 1; k <= total; ++k) {
|
||||
const ChromaFilament &f = *per_layer[size_t(k - 1)];
|
||||
const float td = std::clamp(f.td_mm, TD_MIN_MM, TD_MAX_MM);
|
||||
// The depth at which this filament hides what is behind it, turned into the e-folding length
|
||||
// of its absorption; see CHROMA_TD_OPAQUE_FRACTION.
|
||||
const float efold = CHROMA_TD_OPAQUE_FRACTION * td / OPAQUE_EFOLDS;
|
||||
const float t = std::exp(-stack.layer_height_mm / efold);
|
||||
const std::array<float, 3> own{ srgb_to_linear(f.color.r()), srgb_to_linear(f.color.g()),
|
||||
srgb_to_linear(f.color.b()) };
|
||||
for (int c = 0; c < 3; ++c)
|
||||
acc[size_t(c)] = (1.f - t) * own[size_t(c)] + t * acc[size_t(c)];
|
||||
store(size_t(k), acc);
|
||||
}
|
||||
return palette;
|
||||
}
|
||||
|
||||
ChromaSolve chroma_solve(const uint8_t *rgb, int width, int height, const ChromaStack &stack)
|
||||
{
|
||||
ChromaSolve solve;
|
||||
const int total = stack.total_layers();
|
||||
const int base = std::max(0, stack.base_layers);
|
||||
const int relief = stack.relief_layers();
|
||||
if (rgb == nullptr || width <= 0 || height <= 0 || relief <= 0 || relief > CHROMA_MAX_RELIEF_LAYERS)
|
||||
return solve;
|
||||
|
||||
const std::vector<ColorRGB> palette = chroma_palette(stack);
|
||||
if (int(palette.size()) != total + 1)
|
||||
return solve;
|
||||
|
||||
// Only the heights the solver may choose: base (nothing of the relief printed) up to total.
|
||||
const int choices = relief + 1;
|
||||
const size_t choice_count = size_t(choices);
|
||||
std::vector<Lab> choice_lab(choice_count);
|
||||
std::vector<std::array<uint8_t, 3>> choice_rgb(choice_count);
|
||||
for (int i = 0; i < choices; ++i) {
|
||||
const ColorRGB &c = palette[size_t(base + i)];
|
||||
choice_lab[size_t(i)] = srgb_to_lab(c.r(), c.g(), c.b());
|
||||
choice_rgb[size_t(i)] = { uint8_t(std::lround(c.r() * 255.f)), uint8_t(std::lround(c.g() * 255.f)),
|
||||
uint8_t(std::lround(c.b() * 255.f)) };
|
||||
}
|
||||
|
||||
const size_t count = size_t(width) * size_t(height);
|
||||
solve.width = width;
|
||||
solve.height = height;
|
||||
solve.levels.resize(count);
|
||||
solve.grey.resize(count);
|
||||
solve.preview.resize(count * 3);
|
||||
|
||||
std::vector<float> errors(count, 0.f);
|
||||
|
||||
auto pick = [&](float r, float g, float b, float &out_error) {
|
||||
const Lab target = srgb_to_lab(std::clamp(r, 0.f, 1.f), std::clamp(g, 0.f, 1.f), std::clamp(b, 0.f, 1.f));
|
||||
int best = 0;
|
||||
float best_d = std::numeric_limits<float>::max();
|
||||
for (int i = 0; i < choices; ++i) {
|
||||
const float d = delta_e(target, choice_lab[size_t(i)]);
|
||||
if (d < best_d) {
|
||||
best_d = d;
|
||||
best = i;
|
||||
}
|
||||
}
|
||||
out_error = best_d;
|
||||
return best;
|
||||
};
|
||||
|
||||
// The grey level a chosen height is written as. Rounded to the nearest of 256 levels, which for
|
||||
// any practical layer budget (a few dozen) maps every height to its own level exactly - the
|
||||
// relief's steps stay on layer boundaries, which is the whole point of quantizing here.
|
||||
auto grey_of = [relief](int choice) {
|
||||
return uint8_t(std::lround(255.0 * double(choice) / double(relief)));
|
||||
};
|
||||
|
||||
auto write = [&](size_t px, int choice, float err) {
|
||||
solve.levels[px] = uint16_t(base + choice);
|
||||
solve.grey[px] = grey_of(choice);
|
||||
solve.preview[px * 3] = choice_rgb[size_t(choice)][0];
|
||||
solve.preview[px * 3 + 1] = choice_rgb[size_t(choice)][1];
|
||||
solve.preview[px * 3 + 2] = choice_rgb[size_t(choice)][2];
|
||||
errors[px] = err;
|
||||
};
|
||||
|
||||
if (stack.dither) {
|
||||
// Floyd-Steinberg, serpentine so the error does not drift consistently to one side. The
|
||||
// diffusion buffer holds the two rows the kernel reaches, in sRGB - which is where the
|
||||
// artifact being traded away lives, and what every reference implementation diffuses in.
|
||||
const size_t row_len = size_t(width);
|
||||
std::vector<std::array<float, 3>> row(row_len), next(row_len);
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const uint8_t *src = rgb + size_t(x) * 3;
|
||||
row[size_t(x)] = { src[0] / 255.f, src[1] / 255.f, src[2] / 255.f };
|
||||
}
|
||||
for (int y = 0; y < height; ++y) {
|
||||
if (y + 1 < height)
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const uint8_t *src = rgb + (size_t(y + 1) * size_t(width) + size_t(x)) * 3;
|
||||
next[size_t(x)] = { src[0] / 255.f, src[1] / 255.f, src[2] / 255.f };
|
||||
}
|
||||
const bool ltr = (y % 2) == 0;
|
||||
for (int i = 0; i < width; ++i) {
|
||||
const int x = ltr ? i : width - 1 - i;
|
||||
const size_t px = size_t(y) * size_t(width) + size_t(x);
|
||||
float err = 0.f;
|
||||
const int choice = pick(row[size_t(x)][0], row[size_t(x)][1], row[size_t(x)][2], err);
|
||||
write(px, choice, err);
|
||||
|
||||
std::array<float, 3> residual{};
|
||||
for (int c = 0; c < 3; ++c)
|
||||
residual[size_t(c)] = row[size_t(x)][size_t(c)] - choice_rgb[size_t(choice)][size_t(c)] / 255.f;
|
||||
|
||||
const int ahead = ltr ? x + 1 : x - 1;
|
||||
const int back = ltr ? x - 1 : x + 1;
|
||||
auto spread = [&](std::vector<std::array<float, 3>> &dst, int at, float w) {
|
||||
if (at < 0 || at >= width)
|
||||
return;
|
||||
for (int c = 0; c < 3; ++c)
|
||||
dst[size_t(at)][size_t(c)] += residual[size_t(c)] * w;
|
||||
};
|
||||
spread(row, ahead, 7.f / 16.f);
|
||||
if (y + 1 < height) {
|
||||
spread(next, back, 3.f / 16.f);
|
||||
spread(next, x, 5.f / 16.f);
|
||||
spread(next, ahead, 1.f / 16.f);
|
||||
}
|
||||
}
|
||||
row.swap(next);
|
||||
}
|
||||
} else {
|
||||
tbb::parallel_for(tbb::blocked_range<int>(0, height), [&](const tbb::blocked_range<int> &range) {
|
||||
for (int y = range.begin(); y < range.end(); ++y)
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const size_t px = size_t(y) * size_t(width) + size_t(x);
|
||||
const uint8_t *src = rgb + px * 3;
|
||||
float err = 0.f;
|
||||
write(px, pick(src[0] / 255.f, src[1] / 255.f, src[2] / 255.f, err), err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
solve.mean_delta_e = count == 0 ? 0. : std::accumulate(errors.begin(), errors.end(), 0.) / double(count);
|
||||
solve.max_delta_e = errors.empty() ? 0. : double(*std::max_element(errors.begin(), errors.end()));
|
||||
return solve;
|
||||
}
|
||||
|
||||
float chroma_layers_to_opaque(float td_mm, float layer_height_mm)
|
||||
{
|
||||
if (layer_height_mm <= 0.f)
|
||||
return 0.f;
|
||||
return CHROMA_TD_OPAQUE_FRACTION * std::clamp(td_mm, TD_MIN_MM, TD_MAX_MM) / layer_height_mm;
|
||||
}
|
||||
|
||||
// Below about one dE two colours are the same colour to the eye. Half of that is the threshold for
|
||||
// "this layer changed something", so a step has to be comfortably invisible before a layer is called
|
||||
// wasted.
|
||||
static constexpr float VISIBLE_DE = 0.5f;
|
||||
|
||||
static float step_delta_e(const ColorRGB &a, const ColorRGB &b)
|
||||
{
|
||||
return delta_e(srgb_to_lab(a.r(), a.g(), a.b()), srgb_to_lab(b.r(), b.g(), b.b()));
|
||||
}
|
||||
|
||||
int chroma_distinct_levels(const ChromaStack &stack)
|
||||
{
|
||||
const std::vector<ColorRGB> palette = chroma_palette(stack);
|
||||
const int base = std::max(0, stack.base_layers);
|
||||
if (int(palette.size()) <= base + 1)
|
||||
return 0;
|
||||
int n = 0;
|
||||
for (size_t i = size_t(base) + 1; i < palette.size(); ++i)
|
||||
if (step_delta_e(palette[i - 1], palette[i]) > VISIBLE_DE)
|
||||
++n;
|
||||
return n;
|
||||
}
|
||||
|
||||
void chroma_trim_bands(ChromaStack &stack)
|
||||
{
|
||||
// Walked over the stack's own palette rather than over the filaments' TD, because how fast a band
|
||||
// runs out of anything to say depends on what is under it as much as on the filament. A light band
|
||||
// over a dark one is finished in four or five layers; a dark one over a light one goes on paying
|
||||
// for many more, long past the depth at which it is nominally opaque, because the last few percent
|
||||
// of light coming through still moves a near-black colour perceptibly.
|
||||
const std::vector<ColorRGB> palette = chroma_palette(stack);
|
||||
if (palette.empty())
|
||||
return;
|
||||
int layer = std::max(0, stack.base_layers); // palette index of the layer under the band being looked at
|
||||
for (ChromaBand &band : stack.bands) {
|
||||
int keep = 1;
|
||||
for (int i = 1; i <= band.layers; ++i) {
|
||||
const size_t cur = size_t(layer) + size_t(i);
|
||||
if (cur >= palette.size())
|
||||
break;
|
||||
if (step_delta_e(palette[cur - 1], palette[cur]) > VISIBLE_DE)
|
||||
keep = i; // the last layer of this band that still changes the colour
|
||||
}
|
||||
layer += band.layers;
|
||||
band.layers = std::clamp(keep, 1, band.layers);
|
||||
}
|
||||
}
|
||||
|
||||
int chroma_level_at(const ChromaStack &stack, double z_mm)
|
||||
{
|
||||
const int total = stack.total_layers();
|
||||
if (total <= 0 || stack.layer_height_mm <= 0.f)
|
||||
return 0;
|
||||
// Layer k occupies ((k-1)h, k h], so a point inside it rounds up. The epsilon keeps a surface
|
||||
// sitting exactly on a layer boundary in the layer that printed it rather than in the one above,
|
||||
// which is the difference between a terrace showing its own colour and showing the next band's.
|
||||
const int k = int(std::ceil(z_mm / double(stack.layer_height_mm) - 1e-6));
|
||||
return std::clamp(k, 1, total);
|
||||
}
|
||||
|
||||
std::vector<unsigned char> chroma_encode_height_png(const ChromaSolve &solve)
|
||||
{
|
||||
if (solve.empty())
|
||||
return {};
|
||||
size_t size = 0;
|
||||
void *png = tdefl_write_image_to_png_file_in_memory_ex(solve.grey.data(), solve.width, solve.height, 1, &size,
|
||||
MZ_DEFAULT_LEVEL, MZ_FALSE);
|
||||
if (png == nullptr || size == 0)
|
||||
return {};
|
||||
const unsigned char *bytes = static_cast<const unsigned char *>(png);
|
||||
std::vector<unsigned char> out(bytes, bytes + size);
|
||||
mz_free(png);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<ChromaRange> chroma_ranges(const ChromaStack &stack, double z_bottom)
|
||||
{
|
||||
std::vector<ChromaRange> out;
|
||||
if (stack.layer_height_mm <= 0.f)
|
||||
return out;
|
||||
// The layer height is stored as a float, like every other dimension of a texture displacement
|
||||
// layer, so multiplying it up leaves noise in the last few digits - 5 layers of 0.1 mm come out as
|
||||
// 0.5000000074. The slicer derives its own layer boundaries from the print config in double
|
||||
// precision and lands on 0.5 exactly, and a boundary that misses it by any amount is a boundary a
|
||||
// layer can fall on the wrong side of. Rounding to the nanometre removes the noise without
|
||||
// disturbing any layer height a profile can express.
|
||||
const auto snap = [](double z) { return std::round(z * 1e6) / 1e6; };
|
||||
|
||||
// The base is printed in the bottom band's filament - it is what is loaded when the print starts -
|
||||
// so it extends that band's range down to the first layer instead of being a swap of its own.
|
||||
int layer = std::max(0, stack.base_layers);
|
||||
bool first = true;
|
||||
for (const ChromaBand &band : stack.bands) {
|
||||
if (band.layers <= 0)
|
||||
continue;
|
||||
const double z_min = snap(z_bottom + (first ? 0. : double(layer) * double(stack.layer_height_mm)));
|
||||
first = false;
|
||||
layer += band.layers;
|
||||
const double z_max = snap(z_bottom + double(layer) * double(stack.layer_height_mm));
|
||||
if (!out.empty() && out.back().filament == band.filament.filament)
|
||||
out.back().z_max = z_max; // no swap happens here, so it is one range
|
||||
else
|
||||
out.push_back(ChromaRange{ z_min, z_max, band.filament.filament });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
ChromaStack chroma_default_stack(const std::vector<ChromaFilament> &filaments, float layer_height_mm,
|
||||
int base_layers, int relief_layers)
|
||||
{
|
||||
ChromaStack stack;
|
||||
stack.layer_height_mm = layer_height_mm > 0.f ? layer_height_mm : 0.08f;
|
||||
stack.base_layers = std::max(0, base_layers);
|
||||
if (filaments.empty() || relief_layers <= 0)
|
||||
return stack;
|
||||
|
||||
std::vector<ChromaFilament> ordered = filaments;
|
||||
std::stable_sort(ordered.begin(), ordered.end(), [](const ChromaFilament &a, const ChromaFilament &b) {
|
||||
// Relative luminance of the linear colour: the perceptual order, so a saturated yellow
|
||||
// counts as light and a saturated blue as dark, which a plain r+g+b sum gets wrong.
|
||||
auto lum = [](const ColorRGB &c) {
|
||||
return 0.2126f * srgb_to_linear(c.r()) + 0.7152f * srgb_to_linear(c.g()) + 0.0722f * srgb_to_linear(c.b());
|
||||
};
|
||||
return lum(a.color) > lum(b.color);
|
||||
});
|
||||
|
||||
const int count = int(ordered.size());
|
||||
int left = relief_layers;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
// Spread the remainder over the first bands rather than piling it onto the last one.
|
||||
const int share = left / (count - i) + ((left % (count - i)) > 0 ? 1 : 0);
|
||||
ChromaBand band;
|
||||
band.filament = ordered[size_t(i)];
|
||||
band.layers = std::max(1, share);
|
||||
left -= share;
|
||||
stack.bands.push_back(band);
|
||||
}
|
||||
return stack;
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -0,0 +1,286 @@
|
||||
#ifndef slic3r_ChromaStack_hpp_
|
||||
#define slic3r_ChromaStack_hpp_
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <cereal/cereal.hpp>
|
||||
#include <cereal/types/vector.hpp>
|
||||
|
||||
#include "Color.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// ChromaStack-style colour printing: a picture is reproduced on a single face by stacking thin layers
|
||||
// of a few filaments, swapping filament at fixed layer boundaries, and varying the *height* of each
|
||||
// pixel's column. Where the column is low, the layers above it are missing and the filament below
|
||||
// shows; where it is high, the topmost filament covers everything beneath it. Because filament is
|
||||
// translucent, a column that stops part-way through a run shows a blend of that run's filament and
|
||||
// what is under it, which is what gives far more colours than there are filaments loaded.
|
||||
//
|
||||
// So there are only two things to compute, and they are both here:
|
||||
// - the *palette*: the colour the stack shows at every possible column height (chroma_palette());
|
||||
// - the *height map*: per pixel, the column height whose palette colour is closest to the picture
|
||||
// (chroma_solve()).
|
||||
//
|
||||
// Everything else is machinery that already exists. The height map is handed to a texture
|
||||
// displacement layer as its height texture, so the existing projection/preview/bake pipeline turns
|
||||
// it into real geometry (see TextureDisplacement.hpp), and the filament swaps are handed to the
|
||||
// object's height ranges (ModelObject::layer_config_ranges), which is the slicer's own way of
|
||||
// saying "print this Z band with that filament".
|
||||
|
||||
// The depth of filament that blocks essentially all the light behind it, as a fraction of the
|
||||
// filament's transmission distance (TD).
|
||||
//
|
||||
// TD's scale is empirical, not physical: its values run from about 1
|
||||
// for an opaque filament to 10 and above for a nearly clear one, and the community's calibration
|
||||
// prints (the "seashell" step test) read a TD off the step where the colour beneath stops showing.
|
||||
// That step is about a tenth of the TD number, which is the only thing this constant encodes - a
|
||||
// filament of TD 2 stops hiding what is under it at roughly 0.2 mm, i.e. after two or three layers,
|
||||
// while one of TD 8 needs most of a millimetre. The model below turns that into a per-layer
|
||||
// transmittance; see chroma_palette().
|
||||
static constexpr float CHROMA_TD_OPAQUE_FRACTION = 0.1f;
|
||||
|
||||
// What a fresh stack gets above the base. Twenty-four layers is about two millimetres at a fine layer
|
||||
// height: enough steps for a continuous tonal ramp, and still a print that finishes in an evening.
|
||||
static constexpr int CHROMA_DEFAULT_RELIEF_LAYERS = 24;
|
||||
|
||||
// The most picture layers the solve can express. A column's height above the base is stored as one
|
||||
// byte per pixel in the 8-bit grayscale height map the displacement reads, so beyond this two
|
||||
// different heights would have to share a grey level and the relief's steps would stop landing on
|
||||
// layer boundaries - which is the one thing the quantization exists to guarantee.
|
||||
//
|
||||
// It bounds the bands, not the stack: the base is whatever the model already is under the painted
|
||||
// face, often hundreds of layers on a tall part, and nothing about it is quantized into the height
|
||||
// map. As a limit on the picture it is not one in practice - 255 layers is 20 mm of relief at
|
||||
// 0.08 mm, far past where a picture gains anything.
|
||||
static constexpr int CHROMA_MAX_RELIEF_LAYERS = 255;
|
||||
|
||||
// The most filament changes one stack may hold. Every band is a swap the printer has to make, so this is
|
||||
// a limit on patience rather than on the model; the palette itself is bounded by the layer count.
|
||||
static constexpr size_t CHROMA_MAX_BANDS = 16;
|
||||
|
||||
// One filament as the colour model sees it: what colour it is, and how far light gets into it.
|
||||
struct ChromaFilament
|
||||
{
|
||||
// 0-based index into the project's filaments, which is what a height range's `extruder` option
|
||||
// (1-based) is written from. -1 for a filament that is not loaded, which the GUI refuses to
|
||||
// solve with rather than silently printing in the wrong colour.
|
||||
int filament = -1;
|
||||
ColorRGB color;
|
||||
// The filament preset's own name, so the panel can say which filament this is. Six similar greys
|
||||
// are six identical swatches and six identical numbers otherwise. Stored rather than looked up on
|
||||
// demand because a stack outlives the project's filament list - a preset can be swapped out from
|
||||
// under a saved stack, and then the name is the only record of what it was solved against.
|
||||
std::string name;
|
||||
// Transmission distance, mm, as the filament preset reports it
|
||||
// (`filament_transmission_distance`). Clamped to a sane range when the palette is built: zero
|
||||
// would make a layer infinitely opaque and a negative value is meaningless.
|
||||
float td_mm = 4.f;
|
||||
|
||||
template<class Archive> void serialize(Archive &ar)
|
||||
{
|
||||
float r = color.r(), g = color.g(), b = color.b();
|
||||
ar(filament, r, g, b, td_mm, name);
|
||||
color = ColorRGB(r, g, b);
|
||||
}
|
||||
};
|
||||
|
||||
// A run of consecutive layers printed in one filament. The stack is a list of these, bottom-up, so
|
||||
// a band boundary is exactly where the print pauses and the filament is changed.
|
||||
struct ChromaBand
|
||||
{
|
||||
ChromaFilament filament;
|
||||
int layers = 1;
|
||||
|
||||
template<class Archive> void serialize(Archive &ar) { ar(filament, layers); }
|
||||
};
|
||||
|
||||
// The whole recipe: which filaments, in which order, how thick a layer is, and how much of the
|
||||
// bottom is solid backing rather than picture.
|
||||
struct ChromaStack
|
||||
{
|
||||
std::vector<ChromaBand> bands; // bottom-up; bands.front() is the one the base is printed in
|
||||
|
||||
// The print's layer height, mm. Taken from the print profile rather than chosen here - a stack
|
||||
// solved against 0.08 mm layers prints as the wrong picture at 0.2 mm, so the GUI re-solves when
|
||||
// the profile's layer height changes.
|
||||
float layer_height_mm = 0.08f;
|
||||
|
||||
// Layers of solid backing under the picture: everything the model already is below the painted
|
||||
// face. Not part of any band - the bands are the picture - but printed in the bottom band's
|
||||
// filament, since that is what is loaded when the print starts.
|
||||
//
|
||||
// Keeping it out of the bands is what lets it be what it really is: a measurement of the model,
|
||||
// often a hundred layers or more, while a band is a handful. Folding the two together put a
|
||||
// three-digit number on a slider meant for single digits and made "how thick is the backing" and
|
||||
// "how many layers does the first filament get" the same control, which they are not.
|
||||
//
|
||||
// The colour model treats the base as opaque. That is what makes a column's colour depend only on
|
||||
// the layers above it, and why the panel warns about a base thin enough to let the plate show
|
||||
// through.
|
||||
int base_layers = 4;
|
||||
|
||||
// Whether `base_layers` follows the model or was typed in. It follows by default, and it should:
|
||||
// the painted face is wherever the geometry puts it, and a base that disagrees with it shifts
|
||||
// every colour in the picture - the stack ends up anchored at the bottom of the part instead of
|
||||
// under the face, so the picture's whole range is squeezed into the first few millimetres and
|
||||
// everything above it clamps to the top band.
|
||||
//
|
||||
// It cannot be measured once and left: a layer is created, and its picture chosen, before there
|
||||
// is any paint to measure, and the face moves again whenever the part is scaled or the layer
|
||||
// height changes. So the panel re-measures it every frame while this is set.
|
||||
bool base_auto = true;
|
||||
|
||||
// Spread the quantization error into neighbouring pixels (Floyd-Steinberg, serpentine) instead
|
||||
// of rounding each pixel to its nearest palette colour on its own. A palette of twenty or thirty
|
||||
// colours has to cover a photograph's whole range, so plain rounding leaves visible bands across
|
||||
// a sky or a cheek; diffusing the error trades them for fine stipple, which at a tenth of a
|
||||
// millimetre per pixel reads as the intermediate colour. Off for flat-coloured artwork, where
|
||||
// stipple is the artifact rather than the cure.
|
||||
bool dither = false;
|
||||
|
||||
// Total printed layers, base included: what the tallest column is.
|
||||
int total_layers() const;
|
||||
// Layers the picture is built from, i.e. the bands' own total above the base.
|
||||
int relief_layers() const;
|
||||
// The relief's height in mm, which is the `depth_mm` of the texture displacement layer the
|
||||
// height map is handed to.
|
||||
float depth_mm() const;
|
||||
bool empty() const { return bands.empty(); }
|
||||
|
||||
template<class Archive> void serialize(Archive &ar) { ar(bands, layer_height_mm, base_layers, dither, base_auto); }
|
||||
};
|
||||
|
||||
// The colour the stack shows at every column height: entry k is a column of exactly k printed
|
||||
// layers, so the vector has total_layers() + 1 entries.
|
||||
//
|
||||
// The model is Beer-Lambert absorption, layer by layer. A layer of thickness h in a filament of
|
||||
// transmission distance TD passes
|
||||
//
|
||||
// T = exp(-h / (CHROMA_TD_OPAQUE_FRACTION * TD / 3))
|
||||
//
|
||||
// of the light coming from under it and shows its own colour with the rest, so
|
||||
//
|
||||
// C_k = (1 - T_k) * c_k + T_k * C_(k-1)
|
||||
//
|
||||
// walking up from an opaque base, C_0 = c_1. The divisor is what makes "opaque at
|
||||
// CHROMA_TD_OPAQUE_FRACTION * TD" true: three e-folding lengths pass 5% of the light, which is
|
||||
// where the colour beneath stops being visible to the eye.
|
||||
//
|
||||
// Beer-Lambert is not just the physically honest choice here, it is the only one that stays
|
||||
// consistent when applied per layer: transmittance multiplies, so a run of n layers of one filament
|
||||
// passes exp(-n h / d) whether it is composited in one step or n, and the palette does not depend on
|
||||
// how the stack happens to be cut into bands.
|
||||
//
|
||||
// Compositing is done on linear light and the entries are converted back to sRGB, because the
|
||||
// quantity being attenuated is light, not an sRGB code.
|
||||
std::vector<ColorRGB> chroma_palette(const ChromaStack &stack);
|
||||
|
||||
// What chroma_solve() produced.
|
||||
struct ChromaSolve
|
||||
{
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
// Per pixel, row-major and top-to-bottom like DecodedHeightTexture: the column's printed layer
|
||||
// count, between base_layers and total_layers(). Sixteen bits because it counts from the build
|
||||
// plate, and the base alone can be several hundred layers on a tall part.
|
||||
std::vector<uint16_t> levels;
|
||||
// The same thing as the 8-bit grayscale height map the displacement samples: 0 at base_layers,
|
||||
// 255 at total_layers(). This is what gets encoded as a PNG into the layer's `image_data`, which
|
||||
// is why the solve has to be the thing that owns the quantization - the relief's steps have to
|
||||
// land on layer boundaries, and a grey level is the only channel the displacement reads.
|
||||
std::vector<uint8_t> grey;
|
||||
// Per pixel RGB: the palette colour the solve chose, i.e. what the print is predicted to look
|
||||
// like. Shown in the panel next to the source picture, since the 3D view can only show the
|
||||
// relief - the colours do not exist until the print swaps filament.
|
||||
std::vector<uint8_t> preview;
|
||||
|
||||
// How far the prediction landed from the picture, as a mean and worst-case CIE76 dE over all
|
||||
// pixels. The panel reports it because it is the only honest answer to "will this stack
|
||||
// reproduce my image": under about 5 is a good match, over about 15 means the filaments cannot
|
||||
// reach the picture's colours and no amount of layers will fix it.
|
||||
double mean_delta_e = 0.;
|
||||
double max_delta_e = 0.;
|
||||
|
||||
bool empty() const { return width <= 0 || height <= 0 || levels.empty(); }
|
||||
};
|
||||
|
||||
// Solves `rgb` (width * height * 3 bytes, row-major, top-to-bottom) against the stack's palette.
|
||||
// An empty stack, a zero-sized image or a stack with no relief layers gives an empty result.
|
||||
ChromaSolve chroma_solve(const uint8_t *rgb, int width, int height, const ChromaStack &stack);
|
||||
|
||||
// How many layers of this filament it takes to hide what is under it, at this layer height. The
|
||||
// single most useful number about a stack and the one nothing else makes visible: it is what decides
|
||||
// whether a band is a gradient or a step.
|
||||
//
|
||||
// A filament hides what is behind it at about CHROMA_TD_OPAQUE_FRACTION of its TD (see
|
||||
// chroma_palette()), so this is that depth in layers. Below two the band has no intermediate colours
|
||||
// at all - the picture can only use the filament or not use it - which is what a coarse layer height
|
||||
// does to an otherwise good stack: TD 4 at 0.2 mm covers in two layers, the same filament at 0.08 mm
|
||||
// in five.
|
||||
float chroma_layers_to_opaque(float td_mm, float layer_height_mm);
|
||||
|
||||
// How many of the picture's layers actually change the colour.
|
||||
//
|
||||
// A band eventually stops saying anything: once it hides what is under it, another layer of it shows
|
||||
// the colour the one below already showed. Those layers still cost print time and still take up
|
||||
// relief height, and nothing about the stack makes them visible - the band looks like ten layers of
|
||||
// gradient and is, say, five of gradient and five of flat.
|
||||
//
|
||||
// Measured against the palette rather than against TD, because how long a band keeps paying depends
|
||||
// on what is under it as much as on the filament: a light band over a dark one is finished in four or
|
||||
// five layers, while a dark one over a light one goes on moving the colour perceptibly well past the
|
||||
// depth at which it is nominally opaque.
|
||||
int chroma_distinct_levels(const ChromaStack &stack);
|
||||
|
||||
// Shortens every band to its last layer that still changes the colour. Bands already shorter than
|
||||
// that are left alone: they are still inside their fade, where every layer counts.
|
||||
void chroma_trim_bands(ChromaStack &stack);
|
||||
|
||||
// The printed layer a point `z_mm` above the bottom of the object falls in: 1-based, clamped to the
|
||||
// stack, 0 for a stack that prints nothing. Pair it with chroma_palette() to get the colour shown
|
||||
// there.
|
||||
//
|
||||
// This, rather than the height map, is what decides the colour a surface shows. A filament swap
|
||||
// happens at one print Z across the whole plate, so every surface crossing that height changes colour
|
||||
// whatever the picture says - the terraces of the relief, the walls between them, and the sides of the
|
||||
// model alike. Colouring a preview this way is therefore not an approximation of the print; it is what
|
||||
// the print does, and it keeps working after the relief has been baked into the mesh and the height
|
||||
// map is no longer attached to anything on screen.
|
||||
int chroma_level_at(const ChromaStack &stack, double z_mm);
|
||||
|
||||
// The solve's height map as an 8-bit grayscale PNG, ready to be stored in a
|
||||
// TextureDisplacementLayer's `image_data`.
|
||||
std::vector<unsigned char> chroma_encode_height_png(const ChromaSolve &solve);
|
||||
|
||||
// One filament swap as an object height range: the Z band, in the object's own coordinates (0 at its
|
||||
// bottom), that prints in `filament` (0-based).
|
||||
struct ChromaRange
|
||||
{
|
||||
double z_min = 0.;
|
||||
double z_max = 0.;
|
||||
int filament = -1;
|
||||
};
|
||||
|
||||
// The stack's bands as height ranges, in object coordinates, starting at `z_bottom` (which is 0 for
|
||||
// a stack whose base starts at the object's first layer). Bands printed in the same filament as the
|
||||
// band below are merged, so a stack that repeats a filament does not produce a pointless swap.
|
||||
std::vector<ChromaRange> chroma_ranges(const ChromaStack &stack, double z_bottom = 0.);
|
||||
|
||||
// A starting point for a stack: the given filaments, lightest at the bottom, each with an equal
|
||||
// share of `relief_layers` on top of `base_layers`.
|
||||
//
|
||||
// Light-to-dark is the convention filament-painting guides teach, and it is not arbitrary: a dark
|
||||
// filament over a light one reads as a midtone at one or two layers and as the dark colour at four,
|
||||
// so putting the dark filaments last is what buys the long tonal ramp. The reverse order gives a few
|
||||
// washed-out steps and then nothing.
|
||||
ChromaStack chroma_default_stack(const std::vector<ChromaFilament> &filaments, float layer_height_mm,
|
||||
int base_layers, int relief_layers);
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
#endif // slic3r_ChromaStack_hpp_
|
||||
@@ -1487,7 +1487,7 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
static std::vector<std::string> s_Preset_filament_options {/*"filament_colour", */ "default_filament_colour", "required_nozzle_HRC", "filament_diameter", "pellet_flow_coefficient", "volumetric_speed_coefficients", "filament_type",
|
||||
"filament_soluble", "filament_is_support", "filament_printable", "filament_extruder_compatibility",
|
||||
"filament_max_volumetric_speed", "filament_adaptive_volumetric_speed",
|
||||
"filament_flow_ratio", "filament_density", "filament_adhesiveness_category", "filament_cost", "filament_minimal_purge_on_wipe_tower",
|
||||
"filament_flow_ratio", "filament_density", "filament_transmission_distance", "filament_adhesiveness_category", "filament_cost", "filament_minimal_purge_on_wipe_tower",
|
||||
"filament_tower_interface_pre_extrusion_dist", "filament_tower_interface_pre_extrusion_length", "filament_tower_ironing_area", "filament_tower_interface_purge_volume",
|
||||
"filament_tower_interface_print_temp",
|
||||
"nozzle_temperature", "nozzle_temperature_initial_layer",
|
||||
|
||||
@@ -226,6 +226,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|
||||
"filament_diameter",
|
||||
"volumetric_speed_coefficients",
|
||||
"filament_density",
|
||||
"filament_transmission_distance",
|
||||
"filament_cost",
|
||||
"filament_notes",
|
||||
"outer_wall_acceleration",
|
||||
|
||||
@@ -3441,6 +3441,20 @@ void PrintConfigDef::init_fff_params()
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionFloats { 0. });
|
||||
|
||||
def = this->add("filament_transmission_distance", coFloats);
|
||||
def->label = L("Transmission distance");
|
||||
def->tooltip = L("How deep light gets into this filament, in millimetres, also called its "
|
||||
"transmission distance or TD. About 1 for an opaque filament, 4 to 7 for a usefully "
|
||||
"translucent one, 8 and above for a nearly clear one. Used only to predict the "
|
||||
"colours of a ChromaStack in the texture displacement tool; it has no effect on "
|
||||
"slicing. Measure it with a step test print, or take a published value for the "
|
||||
"filament.");
|
||||
def->sidetext = L("mm");
|
||||
def->min = 0;
|
||||
def->max = 100;
|
||||
def->mode = comAdvanced;
|
||||
def->set_default_value(new ConfigOptionFloats { 4. });
|
||||
|
||||
def = this->add("filament_type", coStrings);
|
||||
def->label = L("Type");
|
||||
def->tooltip = L("Filament material type");
|
||||
|
||||
@@ -1709,6 +1709,9 @@ PRINT_CONFIG_CLASS_DEFINE(
|
||||
((ConfigOptionStrings, volumetric_speed_coefficients))
|
||||
((ConfigOptionInts, filament_adhesiveness_category))
|
||||
((ConfigOptionFloats, filament_density))
|
||||
// Transmission distance: how deep light gets into this filament. Not used by
|
||||
// slicing - it is the colour model of a ChromaStack stack (see ChromaStack.hpp).
|
||||
((ConfigOptionFloats, filament_transmission_distance))
|
||||
((ConfigOptionStrings, filament_type))
|
||||
((ConfigOptionBools, filament_soluble))
|
||||
((ConfigOptionStrings, filament_ids))
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "ChromaStack.hpp"
|
||||
#include "Point.hpp"
|
||||
#include "TriangleSelector.hpp"
|
||||
|
||||
@@ -293,16 +294,33 @@ struct TextureDisplacementLayer
|
||||
// for the boundary to be drawn on.
|
||||
bool color_enabled = false;
|
||||
|
||||
// ChromaStack (see ChromaStack.hpp): when set, `image_data` is not a texture the user picked but the
|
||||
// height map chroma_solve() computed, so that the whole displacement pipeline - preview, UV
|
||||
// editor, bake - goes on reading one grayscale height map and needs to know nothing about colour
|
||||
// stacks. The picture it was solved from is kept here instead, because the solve has to be redone
|
||||
// whenever the stack, the print's layer height or the picture itself changes.
|
||||
//
|
||||
// A ChromaStack layer gets its colour from filament swaps at fixed Z, which is what `chroma`
|
||||
// records and what the object's height ranges carry out. That is a different mechanism from
|
||||
// `color_enabled` (per-triangle filament assignment), and the two are mutually exclusive: the
|
||||
// panel turns colour off on a ChromaStack layer, since painting the relief in one filament per
|
||||
// triangle would hide the very stack the heights were solved against.
|
||||
bool chroma_enabled = false;
|
||||
ChromaStack chroma;
|
||||
std::shared_ptr<std::vector<unsigned char>> chroma_source;
|
||||
|
||||
bool empty() const { return !image_data || image_data->empty(); }
|
||||
|
||||
template<class Archive> void save(Archive &ar) const
|
||||
{
|
||||
std::string blob = image_data ? std::string(image_data->begin(), image_data->end()) : std::string();
|
||||
std::string source = chroma_source ? std::string(chroma_source->begin(), chroma_source->end()) : std::string();
|
||||
ar(slot, name, path, path_in_3mf, blob, depth_mm, tiling_scale, rotation_deg, offset, invert, tile_enabled,
|
||||
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,
|
||||
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, chroma_enabled, chroma,
|
||||
source);
|
||||
}
|
||||
template<class Archive> void load(Archive &ar)
|
||||
{
|
||||
@@ -310,12 +328,14 @@ struct TextureDisplacementLayer
|
||||
int tile_method_int = 0;
|
||||
int projection_method_int = 0;
|
||||
int blend_mode_int = 0;
|
||||
std::string source;
|
||||
ar(slot, name, path, path_in_3mf, blob, depth_mm, tiling_scale, rotation_deg, offset, invert, tile_enabled,
|
||||
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,
|
||||
edge_smoothing_amount, auto_connect_islands, island_groups, lscm_uv_overrides, view_project_projective,
|
||||
view_project_matrix, color_enabled);
|
||||
view_project_matrix, color_enabled, chroma_enabled, chroma, source);
|
||||
image_data = blob.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(blob.begin(), blob.end());
|
||||
chroma_source = source.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(source.begin(), source.end());
|
||||
tile_method = static_cast<TextureTileMethod>(tile_method_int);
|
||||
projection_method = static_cast<TextureProjectionMethod>(projection_method_int);
|
||||
blend_mode = static_cast<TextureBlendMode>(blend_mode_int);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include <wx/checklst.h>
|
||||
#include "slic3r/GUI/GUI_Utils.hpp"
|
||||
#include "slic3r/GUI/Widgets/Button.hpp"
|
||||
#include "slic3r/GUI/Widgets/DropDown.hpp"
|
||||
#include "slic3r/GUI/Widgets/Label.hpp"
|
||||
#include <utility>
|
||||
#include "slic3r/GUI/wxExtensions.hpp"
|
||||
@@ -172,7 +173,26 @@ void AMSMaterialsSetting::create()
|
||||
|
||||
Bind(wxEVT_PAINT, &AMSMaterialsSetting::paintEvent, this);
|
||||
Bind(EVT_SELECTED_COLOR, &AMSMaterialsSetting::on_picker_color, this);
|
||||
m_comboBox_filament->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
|
||||
m_comboBox_filament->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
|
||||
m_comboBox_filament->Bind(EVT_DROPDOWN_ITEM_ACTION, &AMSMaterialsSetting::on_toggle_favorite_filament, this);
|
||||
|
||||
// Favorites toggled in the open list are regrouped just before it opens again, so rows never move
|
||||
// under the cursor. These handlers run ahead of ComboBox's own, which then measure and show the
|
||||
// rebuilt rows; wxEVT_COMBOBOX_DROPDOWN would be too late, as ComboBox sends it after showing the list.
|
||||
auto regroup_favorites = [this]() {
|
||||
if (!m_favorites_regroup || m_comboBox_filament->is_drop_down())
|
||||
return;
|
||||
const FilamentInfos* info = selected_filament_info();
|
||||
const std::string filament_id = info ? info->filament_id : std::string();
|
||||
rebuild_filament_dropdown_items(filament_id);
|
||||
};
|
||||
m_comboBox_filament->Bind(wxEVT_LEFT_DOWN, [regroup_favorites](wxMouseEvent& e) { regroup_favorites(); e.Skip(); });
|
||||
m_comboBox_filament->Bind(wxEVT_LEFT_DCLICK, [regroup_favorites](wxMouseEvent& e) { regroup_favorites(); e.Skip(); });
|
||||
m_comboBox_filament->Bind(wxEVT_KEY_DOWN, [regroup_favorites](wxKeyEvent& e) {
|
||||
if (e.GetKeyCode() == WXK_RETURN || e.GetKeyCode() == WXK_SPACE) // the keys that open the list
|
||||
regroup_favorites();
|
||||
e.Skip();
|
||||
});
|
||||
|
||||
m_comboBox_cali_result->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_cali_result), NULL, this);
|
||||
}
|
||||
@@ -191,11 +211,9 @@ void AMSMaterialsSetting::create_panel_normal(wxWindow* parent)
|
||||
|
||||
m_sizer_filament->Add(0, 0, 0, wxEXPAND, 0);
|
||||
|
||||
#ifdef __APPLE__
|
||||
m_comboBox_filament = new wxComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, AMS_MATERIALS_SETTING_COMBOX_WIDTH, 0, nullptr, wxCB_READONLY);
|
||||
#else
|
||||
m_comboBox_filament = new ::ComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, AMS_MATERIALS_SETTING_COMBOX_WIDTH, 0, nullptr, wxCB_READONLY);
|
||||
#endif
|
||||
m_star_filled = ScalableBitmap(this, "score_star_light", 16);
|
||||
m_star_empty = ScalableBitmap(this, "score_star_outline", 16);
|
||||
|
||||
m_sizer_filament->Add(m_comboBox_filament, 1, wxALIGN_CENTER, 0);
|
||||
|
||||
@@ -420,6 +438,7 @@ void AMSMaterialsSetting::paintEvent(wxPaintEvent &evt)
|
||||
AMSMaterialsSetting::~AMSMaterialsSetting()
|
||||
{
|
||||
m_comboBox_filament->Disconnect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
|
||||
m_comboBox_filament->Unbind(EVT_DROPDOWN_ITEM_ACTION, &AMSMaterialsSetting::on_toggle_favorite_filament, this);
|
||||
m_comboBox_cali_result->Disconnect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_cali_result), NULL, this);
|
||||
}
|
||||
|
||||
@@ -531,10 +550,9 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) {
|
||||
std::string selected_ams_id;
|
||||
PresetBundle *preset_bundle = wxGetApp().preset_bundle;
|
||||
if (preset_bundle) {
|
||||
const FilamentInfos* selected_info = selected_filament_info();
|
||||
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
|
||||
auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()];
|
||||
std::string filament_id = filament_item.filament_id;
|
||||
if (it->filament_id.compare(filament_id) == 0) {
|
||||
if (selected_info && it->filament_id.compare(selected_info->filament_id) == 0) {
|
||||
selected_ams_id = it->filament_id;
|
||||
break;
|
||||
}
|
||||
@@ -682,12 +700,13 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
|
||||
ams_setting_id = "";
|
||||
|
||||
// the combobox item
|
||||
auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()];
|
||||
const FilamentInfos* selected_info = selected_filament_info();
|
||||
const std::string filament_id = selected_info ? selected_info->filament_id : std::string();
|
||||
|
||||
// check filament info (rack-gated per-nozzle blacklist evaluation)
|
||||
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
|
||||
const auto& fila_check_res = sCheckFilamentInfo(preset_bundle, obj, ams_id, slot_id,
|
||||
filament_item.filament_id, ams_filament_id, ams_setting_id);
|
||||
filament_id, ams_filament_id, ams_setting_id);
|
||||
|
||||
if (const auto& prohibit_items = fila_check_res.get_items_by_action("prohibition"); !prohibit_items.empty()) {
|
||||
wxString info_msg;
|
||||
@@ -986,6 +1005,133 @@ static void _collect_filament_info(const wxString& shown_name,
|
||||
query_filament_types[shown_name] = filament.config.get_filament_type();
|
||||
}
|
||||
|
||||
static const char* AMS_FAVORITE_FILAMENT_IDS_KEY = "ams_favorite_filament_ids";
|
||||
|
||||
const AMSMaterialsSetting::FilamentInfos* AMSMaterialsSetting::selected_filament_info() const
|
||||
{
|
||||
const int selection = m_comboBox_filament->GetSelection();
|
||||
if (selection < 0 || selection >= int(m_filament_list_items.size()) || !m_filament_list_items[selection].is_filament)
|
||||
return nullptr;
|
||||
return &m_filament_list_items[selection].filament;
|
||||
}
|
||||
|
||||
std::set<std::string> AMSMaterialsSetting::load_favorite_filament_ids() const
|
||||
{
|
||||
std::set<std::string> favorite_ids;
|
||||
std::vector<std::string> values;
|
||||
const std::string serialized = wxGetApp().app_config->get(AMS_FAVORITE_FILAMENT_IDS_KEY);
|
||||
if (!serialized.empty() && Slic3r::unescape_strings_cstyle(serialized, values)) {
|
||||
favorite_ids.insert(values.begin(), values.end());
|
||||
}
|
||||
return favorite_ids;
|
||||
}
|
||||
|
||||
void AMSMaterialsSetting::save_favorite_filament_ids(const std::set<std::string>& favorite_ids)
|
||||
{
|
||||
std::vector<std::string> values(favorite_ids.begin(), favorite_ids.end());
|
||||
wxGetApp().app_config->set(AMS_FAVORITE_FILAMENT_IDS_KEY, Slic3r::escape_strings_cstyle(values));
|
||||
wxGetApp().app_config->save();
|
||||
}
|
||||
|
||||
void AMSMaterialsSetting::rebuild_filament_dropdown_items(const std::string& selected_filament_id)
|
||||
{
|
||||
std::vector<DropDown::Item> items;
|
||||
m_filament_list_items.clear();
|
||||
|
||||
auto append_header = [&items, this](const wxString& label) {
|
||||
DropDown::Item item;
|
||||
item.text = label;
|
||||
item.style = DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED;
|
||||
items.push_back(item);
|
||||
m_filament_list_items.push_back(FilamentListItem{});
|
||||
};
|
||||
|
||||
auto append_filament = [&items, this](const wxString& name) {
|
||||
auto info_it = map_filament_items.find(name);
|
||||
if (info_it == map_filament_items.end())
|
||||
return;
|
||||
|
||||
DropDown::Item item;
|
||||
item.text = name;
|
||||
items.push_back(item);
|
||||
m_filament_list_items.push_back(FilamentListItem{info_it->second, true});
|
||||
};
|
||||
|
||||
// Favorites are listed again at the top under their own header. Without any, the list is the
|
||||
// plain filament list.
|
||||
const std::set<std::string> favorite_ids = load_favorite_filament_ids();
|
||||
std::vector<wxString> favorites;
|
||||
for (const wxString& name : m_sorted_filament_items) {
|
||||
auto info_it = map_filament_items.find(name);
|
||||
if (info_it != map_filament_items.end() && favorite_ids.count(info_it->second.filament_id) > 0)
|
||||
favorites.push_back(name);
|
||||
}
|
||||
if (!favorites.empty()) {
|
||||
append_header(_L("Favorite filaments"));
|
||||
for (const wxString& name : favorites)
|
||||
append_filament(name);
|
||||
append_header(_L("All filaments"));
|
||||
}
|
||||
for (const wxString& name : m_sorted_filament_items)
|
||||
append_filament(name);
|
||||
|
||||
int selection_idx = -1;
|
||||
if (!selected_filament_id.empty()) {
|
||||
for (int i = 0; i < int(m_filament_list_items.size()); ++i) {
|
||||
if (m_filament_list_items[i].is_filament && m_filament_list_items[i].filament.filament_id == selected_filament_id) {
|
||||
selection_idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear() first: SetItems() keeps the old selection, and SetSelection() ignores an index equal to it,
|
||||
// which would leave the previous filament's name on the combo.
|
||||
m_comboBox_filament->Clear();
|
||||
m_comboBox_filament->SetItems(items);
|
||||
update_favorite_stars();
|
||||
m_comboBox_filament->SetSelection(selection_idx);
|
||||
m_favorites_regroup = false;
|
||||
}
|
||||
|
||||
void AMSMaterialsSetting::update_favorite_stars()
|
||||
{
|
||||
const std::set<std::string> favorite_ids = load_favorite_filament_ids();
|
||||
for (size_t i = 0; i < m_filament_list_items.size(); ++i) {
|
||||
const FilamentListItem& row = m_filament_list_items[i];
|
||||
if (row.is_filament) {
|
||||
const bool favorite = favorite_ids.count(row.filament.filament_id) > 0;
|
||||
m_comboBox_filament->SetItemActionBitmap(i, favorite ? m_star_filled.bmp() : m_star_empty.bmp());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AMSMaterialsSetting::toggle_favorite_filament(const std::string& filament_id)
|
||||
{
|
||||
if (filament_id.empty())
|
||||
return;
|
||||
|
||||
std::set<std::string> favorite_ids = load_favorite_filament_ids();
|
||||
auto iter = favorite_ids.find(filament_id);
|
||||
if (iter == favorite_ids.end())
|
||||
favorite_ids.insert(filament_id);
|
||||
else
|
||||
favorite_ids.erase(iter);
|
||||
|
||||
save_favorite_filament_ids(favorite_ids);
|
||||
}
|
||||
|
||||
void AMSMaterialsSetting::on_toggle_favorite_filament(wxCommandEvent& evt)
|
||||
{
|
||||
const int index = evt.GetInt();
|
||||
if (index < 0 || index >= int(m_filament_list_items.size()) || !m_filament_list_items[index].is_filament)
|
||||
return;
|
||||
|
||||
toggle_favorite_filament(m_filament_list_items[index].filament.filament_id);
|
||||
update_favorite_stars();
|
||||
m_favorites_regroup = true;
|
||||
}
|
||||
|
||||
void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_min, wxString temp_max, wxString k, wxString n)
|
||||
{
|
||||
if (!obj) return;
|
||||
@@ -999,13 +1145,12 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
m_input_k_val->GetTextCtrl()->SetValue(k);
|
||||
m_input_n_val->GetTextCtrl()->SetValue(n);
|
||||
|
||||
wxArrayString filament_items;
|
||||
wxString bambu_filament_name;
|
||||
wxString hint_filament_name; // the hint type to be selected
|
||||
std::unordered_map<wxString, wxString> query_filament_vendors;// some information for sort
|
||||
std::unordered_map<wxString, wxString> query_filament_types; //
|
||||
|
||||
std::set<std::string> filament_id_set;
|
||||
m_sorted_filament_items.clear();
|
||||
// The alias keyed map has to start empty: it is a member, so a stale alias left by an earlier
|
||||
// popup (a different printer, a different nozzle) would resolve to that printer's filament_id.
|
||||
map_filament_items.clear();
|
||||
@@ -1035,7 +1180,7 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
if (alias.empty())
|
||||
continue;
|
||||
|
||||
filament_items.push_back(alias);
|
||||
m_sorted_filament_items.push_back(alias);
|
||||
_collect_filament_info(alias, *filament_it, query_filament_vendors, query_filament_types);
|
||||
|
||||
FilamentInfos filament_infos;
|
||||
@@ -1044,7 +1189,6 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
map_filament_items[alias] = filament_infos;
|
||||
|
||||
if (filament_it->filament_id == ams_filament_id) {
|
||||
hint_filament_name = from_u8(alias);
|
||||
bambu_filament_name = from_u8(alias);
|
||||
|
||||
|
||||
@@ -1215,24 +1359,11 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
|
||||
return name_compare != 0 ? name_compare < 0 : left < right;
|
||||
};
|
||||
|
||||
std::sort(filament_items.begin(), filament_items.end(), _filament_sorter);
|
||||
std::sort(m_sorted_filament_items.begin(), m_sorted_filament_items.end(), _filament_sorter);
|
||||
}
|
||||
|
||||
// traverse the hint selection idx
|
||||
int selection_idx = -1;
|
||||
{
|
||||
for(int i = 0; i < filament_items.size(); i++)
|
||||
{
|
||||
if (hint_filament_name == filament_items[i])
|
||||
{
|
||||
selection_idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_comboBox_filament->Set(filament_items);
|
||||
m_comboBox_filament->SetSelection(selection_idx);
|
||||
rebuild_filament_dropdown_items(ams_filament_id);
|
||||
int selection_idx = m_comboBox_filament->GetSelection();
|
||||
post_select_event(selection_idx);
|
||||
|
||||
if (selection_idx < 0) {
|
||||
@@ -1289,6 +1420,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
int* from_printer = static_cast<int*>(m_comboBox_filament->GetClientData());
|
||||
|
||||
m_filament_type = "";
|
||||
const FilamentInfos* selected_info = selected_filament_info();
|
||||
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
|
||||
if (preset_bundle) {
|
||||
std::ostringstream stream;
|
||||
@@ -1309,9 +1441,8 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
std::string nozzle_diameter_str = stream.str();
|
||||
// Resolve the selection against the same list Popup() built the dropdown from, so the two
|
||||
// halves of the dialog cannot disagree about which filaments this machine can use.
|
||||
const std::string selected = m_comboBox_filament->GetValue().ToStdString();
|
||||
if (!selected.empty()) {
|
||||
const std::string filament_id = map_filament_items[selected].filament_id;
|
||||
if (selected_info) {
|
||||
const std::string& filament_id = selected_info->filament_id;
|
||||
for (Preset *it : preset_bundle->get_filament_presets_for_machine(
|
||||
DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) {
|
||||
if (it->filament_id != filament_id)
|
||||
@@ -1364,7 +1495,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
//reset cali
|
||||
int cali_select_idx = -1;
|
||||
|
||||
if ( !this->obj || m_filament_selection < 0) {
|
||||
if ( !this->obj || m_filament_selection < 0 || !selected_info) {
|
||||
m_input_k_val->Enable(false);
|
||||
m_input_n_val->Enable(false);
|
||||
m_button_confirm->Disable(); // ORCA No need to change style
|
||||
@@ -1383,21 +1514,9 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
|
||||
ams_filament_id = "";
|
||||
ams_setting_id = "";
|
||||
|
||||
if (preset_bundle) {
|
||||
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
|
||||
auto itor = map_filament_items.find(m_comboBox_filament->GetValue().ToStdString());
|
||||
if ( itor != map_filament_items.end()) {
|
||||
ams_filament_id = itor->second.filament_id;
|
||||
ams_setting_id = itor->second.setting_id;
|
||||
break;
|
||||
}
|
||||
|
||||
if (it->alias.compare(m_comboBox_filament->GetValue().ToStdString()) == 0) {
|
||||
ams_filament_id = it->filament_id;
|
||||
ams_setting_id = it->setting_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (selected_info) {
|
||||
ams_filament_id = selected_info->filament_id;
|
||||
ams_setting_id = selected_info->setting_id;
|
||||
}
|
||||
|
||||
wxArrayString items;
|
||||
@@ -1529,6 +1648,11 @@ void AMSMaterialsSetting::on_dpi_changed(const wxRect &suggested_rect)
|
||||
m_button_reset->Rescale(); // ORCA
|
||||
m_button_confirm->Rescale(); // ORCA
|
||||
m_button_close->Rescale(); // ORCA
|
||||
// An open list would keep the measurements of the old stars, so close it before replacing them.
|
||||
m_comboBox_filament->CloseDropdown();
|
||||
m_star_filled.msw_rescale();
|
||||
m_star_empty.msw_rescale();
|
||||
update_favorite_stars();
|
||||
this->Refresh();
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "slic3r/GUI/Widgets/StateColor.hpp"
|
||||
#include <wx/panel.h>
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
#define AMS_MATERIALS_SETTING_DEF_COLOUR wxColour(255, 255, 255)
|
||||
#define AMS_MATERIALS_SETTING_GREY900 wxColour(38, 46, 48)
|
||||
@@ -156,6 +157,7 @@ protected:
|
||||
void create_panel_kn(wxWindow* parent);
|
||||
void on_dpi_changed(const wxRect &suggested_rect) override;
|
||||
void on_select_filament(wxCommandEvent& evt);
|
||||
void on_toggle_favorite_filament(wxCommandEvent& evt);
|
||||
void on_select_cali_result(wxCommandEvent &evt);
|
||||
void on_select_ok(wxCommandEvent &event);
|
||||
void on_select_reset(wxCommandEvent &event);
|
||||
@@ -163,6 +165,11 @@ protected:
|
||||
void on_clr_picker(wxMouseEvent &event);
|
||||
bool is_virtual_tray();
|
||||
void update_widgets();
|
||||
void rebuild_filament_dropdown_items(const std::string& selected_filament_id);
|
||||
void update_favorite_stars();
|
||||
std::set<std::string> load_favorite_filament_ids() const;
|
||||
void save_favorite_filament_ids(const std::set<std::string>& favorite_ids);
|
||||
void toggle_favorite_filament(const std::string& filament_id);
|
||||
|
||||
void update_filament_editing(bool is_printing);
|
||||
// Orca: the variant index of the filament's per-variant options on the nozzle this tray feeds
|
||||
@@ -202,12 +209,7 @@ protected:
|
||||
|
||||
int m_pa_cali_select_id = 0;
|
||||
|
||||
// Orca: native wxComboBox on macOS (the custom ComboBox mis-renders the filament dropdown there)
|
||||
#ifdef __APPLE__
|
||||
wxComboBox *m_comboBox_filament;
|
||||
#else
|
||||
ComboBox *m_comboBox_filament;
|
||||
#endif
|
||||
ComboBox * m_comboBox_cali_result;
|
||||
TextInput* m_readonly_filament;
|
||||
|
||||
@@ -215,7 +217,20 @@ protected:
|
||||
std::string filament_id;
|
||||
std::string setting_id;
|
||||
};
|
||||
std::map<std::string, FilamentInfos> map_filament_items;
|
||||
struct FilamentListItem {
|
||||
FilamentInfos filament;
|
||||
bool is_filament{false};
|
||||
};
|
||||
const FilamentInfos* selected_filament_info() const;
|
||||
|
||||
// Keyed by the same wxString the dropdown shows, so a row's name never has to be converted back.
|
||||
std::map<wxString, FilamentInfos> map_filament_items;
|
||||
std::vector<wxString> m_sorted_filament_items;
|
||||
std::vector<FilamentListItem> m_filament_list_items;
|
||||
ScalableBitmap m_star_filled;
|
||||
ScalableBitmap m_star_empty;
|
||||
// Favorites changed while the list was open; their rows are regrouped before it opens again.
|
||||
bool m_favorites_regroup{false};
|
||||
};
|
||||
|
||||
wxDECLARE_EVENT(EVT_SELECTED_COLOR, wxCommandEvent);
|
||||
|
||||
@@ -114,6 +114,8 @@ std::pair<bool, std::string> GLShadersManager::init()
|
||||
valid &= append_shader("mm_gouraud", { prefix + "mm_gouraud.vs", prefix + "mm_gouraud.fs" });
|
||||
// Fast shaded preview for the texture displacement gizmo (see libslic3r/TextureDisplacement.hpp).
|
||||
valid &= append_shader("texture_displacement_shaded", { prefix + "texture_displacement_shaded.vs", prefix + "texture_displacement_shaded.fs" });
|
||||
// ChromaStack: the printed colours on the model, read per fragment from its own height.
|
||||
valid &= append_shader("chromastack", { prefix + "chromastack.vs", prefix + "chromastack.fs" });
|
||||
// UV-check overlay for the same gizmo: a procedural checker or a distortion heatmap over the
|
||||
// painted patch, to sanity-check the unwrap.
|
||||
valid &= append_shader("texture_displacement_uvcheck", { prefix + "texture_displacement_uvcheck.vs", prefix + "texture_displacement_uvcheck.fs" });
|
||||
|
||||
@@ -1617,7 +1617,7 @@ void ObjectList::list_manipulation(const wxPoint& mouse_pos, bool evt_context_me
|
||||
int obj_idx, vol_idx;
|
||||
get_selected_item_indexes(obj_idx, vol_idx, item);
|
||||
if (obj_idx != -1) {
|
||||
Plater::TakeSnapshot(plater, _u8L("Shift objects to bed"));
|
||||
Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed"));
|
||||
(*m_objects)[obj_idx]->ensure_on_bed();
|
||||
cnv->reload_scene(true, true);
|
||||
update_info_items(obj_idx);
|
||||
@@ -2779,12 +2779,13 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type)
|
||||
GLCanvas3D* cnv = plater->canvas3D();
|
||||
|
||||
switch (type) {
|
||||
case InfoItemType::CustomSupports:
|
||||
case InfoItemType::CustomSupports: {
|
||||
cnv->get_gizmos_manager().reset_all_states();
|
||||
Plater::TakeSnapshot(plater, "Remove support painting");
|
||||
Plater::TakeSnapshot snapshot(plater, "Remove support painting");
|
||||
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
|
||||
mv->supported_facets.reset();
|
||||
break;
|
||||
}
|
||||
|
||||
// BBS: remove CustomSeam
|
||||
|
||||
@@ -2795,19 +2796,21 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type)
|
||||
}
|
||||
break;
|
||||
|
||||
case InfoItemType::MmSegmentation:
|
||||
case InfoItemType::MmSegmentation: {
|
||||
cnv->get_gizmos_manager().reset_all_states();
|
||||
Plater::TakeSnapshot(plater, "Remove color painting");
|
||||
Plater::TakeSnapshot snapshot(plater, "Remove color painting");
|
||||
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
|
||||
mv->mmu_segmentation_facets.reset();
|
||||
break;
|
||||
}
|
||||
|
||||
case InfoItemType::FuzzySkin:
|
||||
case InfoItemType::FuzzySkin: {
|
||||
cnv->get_gizmos_manager().reset_all_states();
|
||||
Plater::TakeSnapshot(plater, _u8L("Remove paint-on fuzzy skin"));
|
||||
Plater::TakeSnapshot snapshot(plater, _u8L("Remove paint-on fuzzy skin"));
|
||||
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
|
||||
mv->fuzzy_skin_facets.reset();
|
||||
break;
|
||||
}
|
||||
|
||||
// BBS: remove Sinking
|
||||
case InfoItemType::Undef : assert(false); break;
|
||||
@@ -2872,6 +2875,37 @@ void ObjectList::del_layer_from_object(const int obj_idx, const t_layer_height_r
|
||||
changed_object(obj_idx);
|
||||
}
|
||||
|
||||
void ObjectList::rebuild_layer_ranges(const int obj_idx)
|
||||
{
|
||||
if (obj_idx < 0 || obj_idx >= int(m_objects->size()))
|
||||
return;
|
||||
const wxDataViewItem obj_item = m_objects_model->GetItemById(obj_idx);
|
||||
if (!obj_item.IsOk())
|
||||
return;
|
||||
|
||||
const t_layer_config_ranges& ranges = object(obj_idx)->layer_config_ranges;
|
||||
// Adding and deleting tree items fires selection events on GTK; suppress them as every other
|
||||
// bulk edit of the tree here does.
|
||||
const bool prevent_events = m_prevent_list_events;
|
||||
m_prevent_list_events = true;
|
||||
wxDataViewItem root = m_objects_model->GetLayerRootItem(obj_item);
|
||||
if (root.IsOk()) {
|
||||
m_objects_model->DeleteChildren(root);
|
||||
if (ranges.empty())
|
||||
m_objects_model->Delete(root);
|
||||
else {
|
||||
for (const auto& range : ranges)
|
||||
add_layer_item(range.first, root);
|
||||
Expand(root);
|
||||
}
|
||||
} else {
|
||||
add_layer_root_item(obj_item);
|
||||
}
|
||||
m_prevent_list_events = prevent_events;
|
||||
|
||||
changed_object(obj_idx);
|
||||
}
|
||||
|
||||
void ObjectList::del_layers_from_object(const int obj_idx)
|
||||
{
|
||||
object(obj_idx)->layer_config_ranges.clear();
|
||||
@@ -6718,7 +6752,7 @@ void ObjectList::OnEditingStarted(wxDataViewEvent &event)
|
||||
else if (col == colSinking) {
|
||||
Plater * plater = wxGetApp().plater();
|
||||
GLCanvas3D *cnv = plater->canvas3D();
|
||||
Plater::TakeSnapshot(plater, _u8L("Shift objects to bed"));
|
||||
Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed"));
|
||||
int obj_idx, vol_idx;
|
||||
get_selected_item_indexes(obj_idx, vol_idx, item);
|
||||
(*m_objects)[obj_idx]->ensure_on_bed();
|
||||
|
||||
@@ -324,6 +324,11 @@ public:
|
||||
void del_settings_from_config(const wxDataViewItem& parent_item);
|
||||
void del_instances_from_object(const int obj_idx);
|
||||
void del_layer_from_object(const int obj_idx, const t_layer_height_range& layer_range);
|
||||
// Mirrors an object's height ranges in the tree after they have been rewritten from outside the
|
||||
// list - the texture displacement gizmo writes a whole set at once for a ChromaStack stack's
|
||||
// filament swaps. Deliberately leaves the selection alone: changing it while a gizmo is open
|
||||
// tears the gizmo down under the user.
|
||||
void rebuild_layer_ranges(const int obj_idx);
|
||||
void del_layers_from_object(const int obj_idx);
|
||||
bool del_from_cut_object(bool is_connector, bool is_model_part = false, bool is_negative_volume = false);
|
||||
bool del_subobject_from_object(const int obj_idx, const int idx, const int type);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -235,7 +235,11 @@ private:
|
||||
ModelVolume* texture_volume();
|
||||
const ModelVolume* texture_volume() const;
|
||||
|
||||
void add_texture_layer();
|
||||
// `chroma` picks the kind: a relief layer, a height map pushed into the surface, or a ChromaStack
|
||||
// picture, a colour picture made of filament swaps. The kind is a property of the layer and is
|
||||
// fixed when it is created - there is no panel-wide mode, so a plaque can carry a picture on its
|
||||
// face and a knurl on its rim at the same time.
|
||||
void add_texture_layer(bool chroma = false);
|
||||
void remove_texture_layer(int slot);
|
||||
// Moves the layer in `slot` to position `to_index` of the slot-ordered stack (an insertion index,
|
||||
// 0..layer count), which is the order blend modes apply in. Layers keep their data; what moves is
|
||||
@@ -399,6 +403,109 @@ private:
|
||||
void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry);
|
||||
void import_custom_texture(TextureDisplacementLayer &layer);
|
||||
|
||||
// ---- ChromaStack (see libslic3r/ChromaStack.hpp) ----
|
||||
//
|
||||
// A ChromaStack layer is an ordinary texture displacement layer whose height map is computed rather
|
||||
// than imported: the picture the user picked is kept in `chroma_source`, the stack in
|
||||
// `chroma`, and `image_data` holds whatever the last solve produced. Everything downstream -
|
||||
// projection, preview, bake - therefore needs no changes at all, and this is the whole of the
|
||||
// feature's GUI side: pick a picture, edit the stack, solve, hand the filament swaps to the
|
||||
// object's height ranges.
|
||||
|
||||
// The project's loaded filaments as the colour model sees them: colour from the project, TD from
|
||||
// each filament's preset. Index is the filament index the swaps are written from.
|
||||
static std::vector<ChromaFilament> chroma_filaments();
|
||||
// The layer height the print profile is set to, mm; 0 when there is no print preset. The solve is
|
||||
// only valid for the layer height it was run at, so the panel compares it against the stack's.
|
||||
static float print_layer_height();
|
||||
|
||||
// Where the active layer's painted patch sits, in the object's own printed coordinates - which is
|
||||
// what the stack has to be lined up with, because a filament swap happens at a print Z and the
|
||||
// relief grows upward from this patch.
|
||||
struct ChromaPatch
|
||||
{
|
||||
bool valid = false;
|
||||
float top_mm = 0.f; // the patch's surface above the bottom of the object
|
||||
float spread_mm = 0.f; // how far from flat the patch is in Z; 0 is a horizontal face
|
||||
// The patch's extent in the plane the texture is projected from, in the bake frame - i.e. the
|
||||
// (x, y) the Z-facing plane of the tri-planar projection samples by. This is what the picture
|
||||
// has to be placed against: tiling_scale and offset are in that space, not in the patch's.
|
||||
Vec2f planar_min = Vec2f::Zero();
|
||||
Vec2f planar_max = Vec2f::Zero();
|
||||
};
|
||||
ChromaPatch chroma_patch(const TextureDisplacementLayer &layer) const;
|
||||
// The painted face's height in layers, which is what the stack's base has to be: -1 when nothing
|
||||
// is painted yet or there is no layer height to divide by.
|
||||
int chroma_measured_base(const TextureDisplacementLayer &layer) const;
|
||||
// Extracting the painted patch is far too expensive to repeat once a frame, which is how often the
|
||||
// panel wants the number. The paint's own timestamp and the transform the patch was measured in are
|
||||
// between them everything the answer depends on.
|
||||
mutable std::array<ChromaPatch, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_patch_cache{};
|
||||
mutable std::array<uint64_t, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_patch_stamp{};
|
||||
mutable std::array<Transform3d, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_patch_trafo{};
|
||||
// The patch is measured in two frames at once - the instance's, for the print height, and the
|
||||
// bake's, for the placement - so both are part of what the cached answer depends on.
|
||||
mutable std::array<Transform3d, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_patch_bake{};
|
||||
|
||||
// Picks a colour picture for the layer, turns ChromaStack on, and seeds a stack from the loaded
|
||||
// filaments and the patch if the layer has none yet. Solves straight away, so the relief appears
|
||||
// without a second click.
|
||||
void chroma_pick_picture(TextureDisplacementLayer &layer);
|
||||
// Re-runs the solve and replaces the layer's height map with its result. False (with the reason
|
||||
// shown to the user) when the layer has no picture, no stack or no relief layers.
|
||||
bool chroma_solve_layer(TextureDisplacementLayer &layer);
|
||||
// Writes the stack's filament swaps onto the object as height ranges, replacing whatever ranges it
|
||||
// had. Takes its own undo snapshot.
|
||||
void chroma_apply_swaps(const TextureDisplacementLayer &layer);
|
||||
// The stack's swaps as the object's height ranges already express them: true when applying would
|
||||
// change nothing, which is what the panel's hint keys off.
|
||||
bool chroma_swaps_applied(const TextureDisplacementLayer &layer) const;
|
||||
// The whole of a ChromaStack card between its header and More settings: the picture and the print
|
||||
// it predicts, the base, the filaments and the colours they reach, and the two actions.
|
||||
void render_chroma_card(TextureDisplacementLayer &layer, float card_pad, float label_w, float wrap_w);
|
||||
|
||||
// The surface drawn in the colours the stack will print, over whatever neutral surface is there:
|
||||
// the live preview's displaced mesh before a bake, the volume's own mesh after one. Every triangle
|
||||
// takes the palette colour of the printed layer its centroid falls in (chroma_level_at()), which
|
||||
// is what the eye will see at that height - so the relief's terraces read as the picture, the walls
|
||||
// between them show the bands they cut through, and looking straight down gives the blended colours
|
||||
// the whole feature exists to produce.
|
||||
//
|
||||
// Nothing else in the application can show this: the colours are blends of translucent filaments
|
||||
// that no single filament can make, so they cannot be expressed as the per-triangle filament
|
||||
// assignment the 3D scene paints with. They exist only here and in the sliced preview.
|
||||
void rebuild_chroma_colors(const TextureDisplacementLayer &layer);
|
||||
bool render_chroma_colors(const TextureDisplacementLayer &layer);
|
||||
GLModel m_chroma_color_model;
|
||||
// The surface the overlay was built from. The mesh alone: the colours are not in it, so a change
|
||||
// to the stack costs a palette upload rather than a mesh rebuild.
|
||||
size_t m_chroma_color_key = 0;
|
||||
// The stack's colours, one texel per printed layer, and what they were built from. A texture
|
||||
// because a uniform array cannot hold a base of several hundred layers.
|
||||
std::unique_ptr<GLTexture> m_chroma_palette_tex;
|
||||
size_t m_chroma_palette_key = 0;
|
||||
bool m_show_chroma_colors = true;
|
||||
|
||||
// What the last solve of each slot reported, for the panel. Not saved: it is a property of the
|
||||
// solve, and a project is reopened with whatever height map the solve produced.
|
||||
struct ChromaReport
|
||||
{
|
||||
bool valid = false;
|
||||
double mean = 0.;
|
||||
double max = 0.;
|
||||
};
|
||||
std::array<ChromaReport, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_report{};
|
||||
// The predicted print, per slot: the palette colours the solve chose, as a panel thumbnail. The
|
||||
// 3D view cannot show this - the colours do not exist until the print swaps filament - so it is
|
||||
// the only place the user sees what the stack will actually produce.
|
||||
std::array<std::unique_ptr<GLTexture>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_preview;
|
||||
// The source picture's thumbnail, beside the prediction, keyed by the image it was built from.
|
||||
std::array<std::unique_ptr<GLTexture>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_source_thumb;
|
||||
std::array<const void *, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_source_key{};
|
||||
// Per slot: the stack has been edited since the height map was solved, so the relief on screen is the
|
||||
// previous stack's. Nothing else can show this - a stale height map looks like a perfectly good one.
|
||||
std::array<bool, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_chroma_stale{};
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -4533,6 +4533,7 @@ void TabFilament::build()
|
||||
optgroup->append_single_option_line("filament_adhesiveness_category", "material_basic_information#adhesiveness-category");
|
||||
|
||||
optgroup->append_single_option_line("filament_density", "material_basic_information#density");
|
||||
optgroup->append_single_option_line("filament_transmission_distance", "material_basic_information#transmission-distance");
|
||||
optgroup->append_single_option_line("filament_shrink", "material_basic_information#shrinkage-xy");
|
||||
optgroup->append_single_option_line("filament_shrinkage_compensation_z", "material_basic_information#shrinkage-z");
|
||||
optgroup->append_single_option_line("filament_cost", "material_basic_information#price");
|
||||
|
||||
@@ -82,6 +82,11 @@ ComboBox::ComboBox(wxWindow *parent,
|
||||
e.SetId(GetId());
|
||||
GetEventHandler()->ProcessEvent(e);
|
||||
});
|
||||
drop.Bind(EVT_DROPDOWN_ITEM_ACTION, [this](wxCommandEvent &e) {
|
||||
e.SetEventObject(this);
|
||||
e.SetId(GetId());
|
||||
GetEventHandler()->ProcessEvent(e);
|
||||
});
|
||||
drop.Bind(EVT_DISMISS, [this](auto &) {
|
||||
drop_down = false;
|
||||
wxCommandEvent e(wxEVT_COMBOBOX_CLOSEUP);
|
||||
@@ -251,7 +256,7 @@ int ComboBox::Append(const wxString &text,
|
||||
int style)
|
||||
{
|
||||
auto valid_bit_map = bitmap.IsOk() ? bitmap : wxNullBitmap;
|
||||
Item item{text, wxEmptyString, valid_bit_map, valid_bit_map, clientData, group_key, group_label};
|
||||
Item item{text, wxEmptyString, valid_bit_map, valid_bit_map, wxNullBitmap, clientData, group_key, group_label};
|
||||
item.style = style;
|
||||
items.push_back(item);
|
||||
SetClientDataType(wxClientData_Void);
|
||||
@@ -297,6 +302,12 @@ void ComboBox::ForceDropdownOpen()
|
||||
}
|
||||
}
|
||||
|
||||
void ComboBox::CloseDropdown()
|
||||
{
|
||||
if (drop.IsShown())
|
||||
drop.DismissAndNotify();
|
||||
}
|
||||
|
||||
unsigned int ComboBox::GetCount() const { return items.size(); }
|
||||
|
||||
void ComboBox::set_replace_text(wxString text, wxString image_name)
|
||||
@@ -350,6 +361,13 @@ void ComboBox::SetItemBitmap(unsigned int n, wxBitmap const &bitmap)
|
||||
drop.Invalidate();
|
||||
}
|
||||
|
||||
void ComboBox::SetItemActionBitmap(unsigned int n, wxBitmap const &bitmap)
|
||||
{
|
||||
if (n >= items.size()) return;
|
||||
items[n].action_icon = bitmap.IsOk() ? bitmap : wxNullBitmap;
|
||||
drop.Invalidate();
|
||||
}
|
||||
|
||||
int ComboBox::DoInsertItems(const wxArrayStringsAdapter &items,
|
||||
unsigned int pos,
|
||||
void ** clientData,
|
||||
@@ -357,7 +375,7 @@ int ComboBox::DoInsertItems(const wxArrayStringsAdapter &items,
|
||||
{
|
||||
if (pos > this->items.size()) return -1;
|
||||
for (int i = 0; i < items.GetCount(); ++i) {
|
||||
Item item { items[i], wxEmptyString, wxNullBitmap, wxNullBitmap, clientData ? clientData[i] : NULL };
|
||||
Item item { items[i], wxEmptyString, wxNullBitmap, wxNullBitmap, wxNullBitmap, clientData ? clientData[i] : NULL };
|
||||
this->items.insert(this->items.begin() + pos, item);
|
||||
++pos;
|
||||
}
|
||||
@@ -419,16 +437,18 @@ void ComboBox::keyDown(wxKeyEvent& event)
|
||||
case WXK_UP:
|
||||
case WXK_DOWN:
|
||||
case WXK_LEFT:
|
||||
case WXK_RIGHT:
|
||||
if ((event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) && GetSelection() > 0) {
|
||||
SetSelection(GetSelection() - 1);
|
||||
} else if ((event.GetKeyCode() == WXK_DOWN || event.GetKeyCode() == WXK_RIGHT) && GetSelection() + 1 < items.size()) {
|
||||
SetSelection(GetSelection() + 1);
|
||||
} else {
|
||||
case WXK_RIGHT: {
|
||||
// Step over disabled rows: the mouse cannot select them either.
|
||||
const int step = (event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) ? -1 : 1;
|
||||
int n = GetSelection() + step;
|
||||
while (n >= 0 && n < int(items.size()) && (items[n].style & DD_ITEM_STYLE_DISABLED))
|
||||
n += step;
|
||||
if (n < 0 || n >= int(items.size()))
|
||||
break;
|
||||
}
|
||||
SetSelection(n);
|
||||
sendComboBoxEvent();
|
||||
break;
|
||||
}
|
||||
case WXK_TAB:
|
||||
HandleAsNavigationKey(event);
|
||||
break;
|
||||
|
||||
@@ -96,10 +96,12 @@ public:
|
||||
|
||||
wxBitmap GetItemBitmap(unsigned int n);
|
||||
void SetItemBitmap(unsigned int n, wxBitmap const &bitmap);
|
||||
void SetItemActionBitmap(unsigned int n, wxBitmap const &bitmap);
|
||||
bool is_drop_down(){return drop_down;}
|
||||
void DeleteOneItem(unsigned int pos) { DoDeleteOneItem(pos); }
|
||||
|
||||
void ForceDropdownOpen();
|
||||
void CloseDropdown();
|
||||
|
||||
protected:
|
||||
virtual int DoInsertItems(const wxArrayStringsAdapter &items,
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "slic3r/GUI/wxExtensions.hpp"
|
||||
|
||||
wxDEFINE_EVENT(EVT_DISMISS, wxCommandEvent);
|
||||
wxDEFINE_EVENT(EVT_DROPDOWN_ITEM_ACTION, wxCommandEvent);
|
||||
|
||||
BEGIN_EVENT_TABLE(DropDown, PopupWindow)
|
||||
|
||||
@@ -242,6 +243,19 @@ static wxSize GetBmpSize(wxBitmap & bmp)
|
||||
#endif
|
||||
}
|
||||
|
||||
// The x-range [first, second) of the column at the right of the rows that holds the items' action
|
||||
// icons, empty when no item has one. render() draws the icons in it and mouseReleased() hit-tests
|
||||
// clicks against it, so the drawn icon and its clickable area always match.
|
||||
std::pair<int, int> DropDown::actionColumn() const
|
||||
{
|
||||
if (actionIconWidth <= 0)
|
||||
return {0, 0};
|
||||
int right = rowSize.x;
|
||||
if (rowSize.y * count > GetSize().y) // position bar
|
||||
right -= 6;
|
||||
return {right - actionIconWidth - FromDIP(16), right};
|
||||
}
|
||||
|
||||
static void _DrawSplitItem(const wxWindow* w, wxDC& dc, wxString split_text, wxPoint start_pt, int item_width, int item_height)
|
||||
{
|
||||
// save dc
|
||||
@@ -371,6 +385,10 @@ void DropDown::render(wxDC &dc)
|
||||
rcContent.width -= szBmp.x + 5;
|
||||
}
|
||||
|
||||
const auto action_column = actionColumn();
|
||||
if (action_column.second > action_column.first)
|
||||
rcContent.width = action_column.first - rcContent.x;
|
||||
|
||||
std::set<wxString> groups;
|
||||
// draw texts & icons
|
||||
int index = 0;
|
||||
@@ -434,13 +452,24 @@ void DropDown::render(wxDC &dc)
|
||||
auto text = group.IsEmpty()
|
||||
? (item.group_key.IsEmpty() ? item.text : item.group_label)
|
||||
: item.text;
|
||||
if (item.action_icon.IsOk()) {
|
||||
auto action_size = GetBmpSize(item.action_icon);
|
||||
wxPoint action_pt(action_column.second - action_size.x - FromDIP(8),
|
||||
rcContent.y + (rcContent.height - action_size.y) / 2);
|
||||
dc.DrawBitmap(item.action_icon, action_pt);
|
||||
}
|
||||
|
||||
if (!text_off && !text.IsEmpty()) {
|
||||
wxSize tSize = dc.GetMultiLineTextExtent(text);
|
||||
if (pt.x + tSize.x > rcContent.GetRight()) {
|
||||
if (is_hover && item.tip.IsEmpty())
|
||||
SetToolTip(text);
|
||||
text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END,
|
||||
rcContent.GetRight() - pt.x);
|
||||
int max_text_width = rcContent.GetRight() - pt.x;
|
||||
if (max_text_width <= 0) {
|
||||
text.clear();
|
||||
} else {
|
||||
if (is_hover && item.tip.IsEmpty())
|
||||
SetToolTip(text);
|
||||
text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END, max_text_width);
|
||||
}
|
||||
}
|
||||
pt.y += (rcContent.height - textSize.y) / 2;
|
||||
dc.SetFont(GetFont());
|
||||
@@ -529,6 +558,7 @@ void DropDown::messureSize()
|
||||
if (!need_sync) return;
|
||||
textSize = wxSize();
|
||||
iconSize = wxSize();
|
||||
actionIconWidth = 0;
|
||||
count = 0;
|
||||
wxClientDC dc(GetParent() ? GetParent() : this);
|
||||
dc.SetFont(GetFont());
|
||||
@@ -565,8 +595,13 @@ void DropDown::messureSize()
|
||||
size1.x += size2.x + (text_off ? 0 : 5);
|
||||
}
|
||||
}
|
||||
if (item.action_icon.IsOk())
|
||||
actionIconWidth = std::max(actionIconWidth, GetBmpSize(item.action_icon).x);
|
||||
if (size1.x > textSize.x) textSize = size1;
|
||||
}
|
||||
// Every row leaves room for the action column, as render() and actionColumn() lay it out.
|
||||
if (actionIconWidth > 0)
|
||||
textSize.x += actionIconWidth + FromDIP(16);
|
||||
if (!align_icon) iconSize.x = 0;
|
||||
wxSize szContent = textSize;
|
||||
if (szContent.x < FromDIP(120) && !use_content_width)
|
||||
@@ -636,6 +671,11 @@ void DropDown::messureSize()
|
||||
e.SetId(GetId());
|
||||
GetEventHandler()->ProcessEvent(e);
|
||||
});
|
||||
subDropDown->Bind(EVT_DROPDOWN_ITEM_ACTION, [this](wxCommandEvent &e) {
|
||||
e.SetEventObject(this);
|
||||
e.SetId(GetId());
|
||||
GetEventHandler()->ProcessEvent(e);
|
||||
});
|
||||
}
|
||||
need_sync = false;
|
||||
}
|
||||
@@ -714,6 +754,15 @@ void DropDown::mouseReleased(wxMouseEvent& event)
|
||||
if (hover_item < 0)
|
||||
return;
|
||||
if (hover_item >= 0 && (subDropDown == nullptr || subDropDown->group.empty())) { // not moved
|
||||
int index = hoverIndex();
|
||||
if (index >= 0 && items[index].action_icon.IsOk()) {
|
||||
const auto action_column = actionColumn();
|
||||
if (event.GetX() >= action_column.first && event.GetX() < action_column.second) {
|
||||
sendDropDownActionEvent(index);
|
||||
paintNow();
|
||||
return;
|
||||
}
|
||||
}
|
||||
sendDropDownEvent();
|
||||
if (mainDropDown)
|
||||
mainDropDown->hover_item = -1; // To Dismiss mainDropDown
|
||||
@@ -830,6 +879,17 @@ void DropDown::sendDropDownEvent()
|
||||
GetEventHandler()->ProcessEvent(event);
|
||||
}
|
||||
|
||||
void DropDown::sendDropDownActionEvent(int index)
|
||||
{
|
||||
if (index < 0 || index >= int(items.size()) || (items[index].style & DD_ITEM_STYLE_DISABLED))
|
||||
return;
|
||||
wxCommandEvent event(EVT_DROPDOWN_ITEM_ACTION, GetId());
|
||||
event.SetEventObject(this);
|
||||
event.SetInt(index);
|
||||
event.SetString(items[index].text);
|
||||
GetEventHandler()->ProcessEvent(event);
|
||||
}
|
||||
|
||||
void DropDown::Dismiss()
|
||||
{
|
||||
if (subDropDown && subDropDown->IsShown())
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <wx/event.h>
|
||||
#include <vector>
|
||||
#include <cstddef>
|
||||
#include <utility>
|
||||
#include <wx/gdicmn.h>
|
||||
#include "slic3r/GUI/Widgets/StateColor.hpp"
|
||||
#include <boost/date_time/posix_time/ptime.hpp>
|
||||
@@ -27,6 +28,7 @@ class wxWindow;
|
||||
#define DD_ITEM_STYLE_DIMMED 0x0004 // gray text, but still selectable
|
||||
|
||||
wxDECLARE_EVENT(EVT_DISMISS, wxCommandEvent);
|
||||
wxDECLARE_EVENT(EVT_DROPDOWN_ITEM_ACTION, wxCommandEvent);
|
||||
|
||||
class DropDown : public PopupWindow
|
||||
{
|
||||
@@ -37,6 +39,7 @@ public:
|
||||
wxString text_static_tips;// display static tips for TextInput.eg.PrinterInfoBox
|
||||
wxBitmap icon;
|
||||
wxBitmap icon_textctrl;// display icon for TextInput.eg.PrinterInfoBox
|
||||
wxBitmap action_icon;
|
||||
void * data{nullptr};
|
||||
wxString group_key{};
|
||||
wxString group_label{};
|
||||
@@ -65,6 +68,7 @@ private:
|
||||
|
||||
wxSize textSize;
|
||||
wxSize iconSize;
|
||||
int actionIconWidth = 0;
|
||||
wxSize rowSize;
|
||||
|
||||
StateHandler state_handler;
|
||||
@@ -135,6 +139,7 @@ private:
|
||||
int hoverIndex();
|
||||
|
||||
int selectedItem();
|
||||
std::pair<int, int> actionColumn() const;
|
||||
|
||||
friend class ComboBox;
|
||||
void messureSize();
|
||||
@@ -148,6 +153,7 @@ private:
|
||||
void mouseWheelMoved(wxMouseEvent &event);
|
||||
|
||||
void sendDropDownEvent();
|
||||
void sendDropDownActionEvent(int index);
|
||||
|
||||
|
||||
DECLARE_EVENT_TABLE()
|
||||
|
||||
@@ -37,6 +37,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_fill_plane_path.cpp
|
||||
test_fill_tpms_adaptive.cpp
|
||||
test_geometry.cpp
|
||||
test_chromastack.cpp
|
||||
test_imex_arrange.cpp
|
||||
test_imex_helpers.cpp
|
||||
test_imex_zones.cpp
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
#include <catch2/catch_approx.hpp>
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/Color.hpp"
|
||||
#include "libslic3r/ChromaStack.hpp"
|
||||
#include "libslic3r/PNGReadWrite.hpp"
|
||||
|
||||
using namespace Slic3r;
|
||||
using Catch::Matchers::WithinAbs;
|
||||
using Catch::Matchers::WithinRel;
|
||||
|
||||
namespace {
|
||||
|
||||
ChromaFilament filament(int index, const ColorRGB &color, float td_mm)
|
||||
{
|
||||
ChromaFilament f;
|
||||
f.filament = index;
|
||||
f.color = color;
|
||||
f.td_mm = td_mm;
|
||||
return f;
|
||||
}
|
||||
|
||||
ChromaBand band(const ChromaFilament &f, int layers)
|
||||
{
|
||||
ChromaBand b;
|
||||
b.filament = f;
|
||||
b.layers = layers;
|
||||
return b;
|
||||
}
|
||||
|
||||
// An opaque filament (TD 1 hides what is under it within 0.1 mm) and a translucent one (TD 8 needs
|
||||
// most of a millimetre), which is the whole range the model has to behave sensibly over.
|
||||
const ChromaFilament WHITE_OPAQUE = filament(0, ColorRGB(1.f, 1.f, 1.f), 1.f);
|
||||
const ChromaFilament BLACK_OPAQUE = filament(1, ColorRGB(0.f, 0.f, 0.f), 1.f);
|
||||
const ChromaFilament BLACK_TRANSLUCENT = filament(2, ColorRGB(0.f, 0.f, 0.f), 8.f);
|
||||
|
||||
float luminance(const ColorRGB &c) { return 0.2126f * c.r() + 0.7152f * c.g() + 0.0722f * c.b(); }
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("A stack's palette has one entry per printed layer", "[ChromaStack]")
|
||||
{
|
||||
ChromaStack stack;
|
||||
stack.layer_height_mm = 0.08f;
|
||||
stack.base_layers = 4;
|
||||
stack.bands = { band(WHITE_OPAQUE, 2), band(BLACK_TRANSLUCENT, 10) };
|
||||
|
||||
// The base is backing under the picture, not part of a band: the bands are the 12 layers the
|
||||
// picture is built from, and the stack is those on top of the 4 the model already has.
|
||||
REQUIRE(stack.relief_layers() == 12);
|
||||
REQUIRE(stack.total_layers() == 16);
|
||||
REQUIRE_THAT(stack.depth_mm(), WithinAbs(12 * 0.08f, 1e-6f));
|
||||
|
||||
const std::vector<ColorRGB> palette = chroma_palette(stack);
|
||||
REQUIRE(palette.size() == 17);
|
||||
// Index 0 is the base's own colour, so a column one layer tall is exactly its filament - whatever
|
||||
// that filament's transmission distance.
|
||||
REQUIRE(palette[0] == palette[1]);
|
||||
REQUIRE_THAT(luminance(palette[1]), WithinAbs(1.f, 1e-3f));
|
||||
}
|
||||
|
||||
TEST_CASE("A run of one filament gives the same palette however it is split into bands", "[ChromaStack]")
|
||||
{
|
||||
// The property that justifies compositing layer by layer at all: transmittance multiplies, so
|
||||
// cutting a run in two must not change the colours. A model without it would make the palette
|
||||
// depend on how the stack was typed in.
|
||||
ChromaStack one;
|
||||
one.layer_height_mm = 0.1f;
|
||||
one.base_layers = 0;
|
||||
one.bands = { band(WHITE_OPAQUE, 2), band(BLACK_TRANSLUCENT, 8) };
|
||||
|
||||
ChromaStack split = one;
|
||||
split.bands = { band(WHITE_OPAQUE, 2), band(BLACK_TRANSLUCENT, 3), band(BLACK_TRANSLUCENT, 5) };
|
||||
|
||||
const std::vector<ColorRGB> a = chroma_palette(one);
|
||||
const std::vector<ColorRGB> b = chroma_palette(split);
|
||||
REQUIRE(a.size() == b.size());
|
||||
for (size_t i = 0; i < a.size(); ++i)
|
||||
REQUIRE_THAT(luminance(a[i]), WithinAbs(luminance(b[i]), 1e-5f));
|
||||
}
|
||||
|
||||
TEST_CASE("A translucent filament darkens gradually and an opaque one at once", "[ChromaStack]")
|
||||
{
|
||||
ChromaStack translucent;
|
||||
translucent.layer_height_mm = 0.08f;
|
||||
translucent.base_layers = 0;
|
||||
translucent.bands = { band(WHITE_OPAQUE, 1), band(BLACK_TRANSLUCENT, 12) };
|
||||
const std::vector<ColorRGB> slow = chroma_palette(translucent);
|
||||
|
||||
// Monotonically darker, and still short of the filament's own black after a single layer: that
|
||||
// gradient is what a ChromaStack stack prints its midtones with.
|
||||
for (size_t i = 2; i < slow.size(); ++i)
|
||||
REQUIRE(luminance(slow[i]) < luminance(slow[i - 1]));
|
||||
REQUIRE(luminance(slow[2]) > 0.5f);
|
||||
// Twelve layers of a TD 8 filament pass under 3% of the light, which is the dark end of the ramp
|
||||
// rather than true black - and that is the honest prediction, not a rounding artifact.
|
||||
REQUIRE(luminance(slow.back()) < 0.25f);
|
||||
|
||||
ChromaStack opaque;
|
||||
opaque.layer_height_mm = 0.08f;
|
||||
opaque.base_layers = 0;
|
||||
opaque.bands = { band(WHITE_OPAQUE, 1), band(BLACK_OPAQUE, 12) };
|
||||
const std::vector<ColorRGB> fast = chroma_palette(opaque);
|
||||
// TD 1 is opaque within about 0.1 mm, so two layers of it leave little of the white showing.
|
||||
REQUIRE(luminance(fast[3]) < 0.2f);
|
||||
}
|
||||
|
||||
TEST_CASE("Solving a picture of the palette's own colors reproduces it exactly", "[ChromaStack]")
|
||||
{
|
||||
ChromaStack stack;
|
||||
stack.layer_height_mm = 0.08f;
|
||||
stack.base_layers = 2;
|
||||
stack.bands = { band(WHITE_OPAQUE, 2), band(BLACK_TRANSLUCENT, 8) };
|
||||
|
||||
const std::vector<ColorRGB> palette = chroma_palette(stack);
|
||||
const int relief = stack.relief_layers();
|
||||
|
||||
// One pixel per reachable height, in order.
|
||||
std::vector<uint8_t> rgb;
|
||||
for (int i = 0; i <= relief; ++i) {
|
||||
const ColorRGB &c = palette[size_t(stack.base_layers + i)];
|
||||
rgb.push_back(uint8_t(std::lround(c.r() * 255.f)));
|
||||
rgb.push_back(uint8_t(std::lround(c.g() * 255.f)));
|
||||
rgb.push_back(uint8_t(std::lround(c.b() * 255.f)));
|
||||
}
|
||||
|
||||
const ChromaSolve solve = chroma_solve(rgb.data(), relief + 1, 1, stack);
|
||||
REQUIRE_FALSE(solve.empty());
|
||||
REQUIRE(solve.width == relief + 1);
|
||||
REQUIRE(solve.levels.size() == size_t(relief + 1));
|
||||
// Every pixel comes back as the colour it was generated from, and the error is rounding only.
|
||||
//
|
||||
// The colour rather than the height: the bottom band prints in the same filament as the base, so
|
||||
// its first layers show exactly the colour the base already showed, and any of those heights is a
|
||||
// correct answer for that pixel. Asserting the height would be asserting which of several equally
|
||||
// right answers the search happens to reach first.
|
||||
for (int i = 0; i <= relief; ++i) {
|
||||
const int chosen = int(solve.levels[size_t(i)]);
|
||||
REQUIRE(chosen >= stack.base_layers);
|
||||
REQUIRE(chosen <= stack.total_layers());
|
||||
const ColorRGB &got = palette[size_t(chosen)];
|
||||
const ColorRGB &want = palette[size_t(stack.base_layers + i)];
|
||||
REQUIRE_THAT(got.r(), WithinAbs(want.r(), 2.f / 255.f));
|
||||
REQUIRE_THAT(got.g(), WithinAbs(want.g(), 2.f / 255.f));
|
||||
REQUIRE_THAT(got.b(), WithinAbs(want.b(), 2.f / 255.f));
|
||||
}
|
||||
// Only 8-bit rounding of the palette separates the two, which near black is still under one dE.
|
||||
REQUIRE(solve.max_delta_e < 2.);
|
||||
|
||||
// The height map's ends are the base and the top of the stack, which is what ties a grey level to
|
||||
// a printed layer.
|
||||
REQUIRE(solve.grey.front() == 0);
|
||||
REQUIRE(solve.grey.back() == 255);
|
||||
}
|
||||
|
||||
TEST_CASE("A picture the filaments cannot reach is reported as a poor match", "[ChromaStack]")
|
||||
{
|
||||
ChromaStack stack;
|
||||
stack.layer_height_mm = 0.08f;
|
||||
stack.base_layers = 2;
|
||||
stack.bands = { band(WHITE_OPAQUE, 2), band(BLACK_TRANSLUCENT, 8) };
|
||||
|
||||
// Saturated red against a stack of white and black: no height can produce it, and the panel's
|
||||
// warning depends on the solve saying so rather than silently picking a grey.
|
||||
const std::vector<uint8_t> red{ 255, 0, 0 };
|
||||
const ChromaSolve solve = chroma_solve(red.data(), 1, 1, stack);
|
||||
REQUIRE_FALSE(solve.empty());
|
||||
REQUIRE(solve.mean_delta_e > 15.);
|
||||
}
|
||||
|
||||
TEST_CASE("Dithering keeps every pixel in the stack's range", "[ChromaStack]")
|
||||
{
|
||||
ChromaStack stack;
|
||||
stack.layer_height_mm = 0.08f;
|
||||
stack.base_layers = 2;
|
||||
stack.bands = { band(WHITE_OPAQUE, 2), band(BLACK_TRANSLUCENT, 8) };
|
||||
stack.dither = true;
|
||||
|
||||
// A flat mid-grey: the error diffusion has to spread it over neighbouring heights without ever
|
||||
// leaving the stack, which is what a clamped palette index would quietly get wrong.
|
||||
std::vector<uint8_t> grey(size_t(16 * 16) * 3, 128);
|
||||
const ChromaSolve solve = chroma_solve(grey.data(), 16, 16, stack);
|
||||
REQUIRE_FALSE(solve.empty());
|
||||
REQUIRE(solve.levels.size() == size_t(16 * 16));
|
||||
const auto [lo, hi] = std::minmax_element(solve.levels.begin(), solve.levels.end());
|
||||
REQUIRE(int(*lo) >= stack.base_layers);
|
||||
REQUIRE(int(*hi) <= stack.total_layers());
|
||||
}
|
||||
|
||||
TEST_CASE("The solved height map is an 8-bit grayscale PNG of the picture's size", "[ChromaStack]")
|
||||
{
|
||||
// The displacement pipeline reads one thing from a layer: an 8-bit grayscale PNG. A solve that
|
||||
// produced anything else would leave the relief silently flat.
|
||||
ChromaStack stack;
|
||||
stack.layer_height_mm = 0.08f;
|
||||
stack.base_layers = 2;
|
||||
stack.bands = { band(WHITE_OPAQUE, 2), band(BLACK_TRANSLUCENT, 6) };
|
||||
|
||||
std::vector<uint8_t> picture(size_t(8 * 4) * 3);
|
||||
for (size_t i = 0; i < picture.size(); ++i)
|
||||
picture[i] = uint8_t(i % 256);
|
||||
|
||||
const ChromaSolve solve = chroma_solve(picture.data(), 8, 4, stack);
|
||||
const std::vector<unsigned char> png = chroma_encode_height_png(solve);
|
||||
REQUIRE_FALSE(png.empty());
|
||||
|
||||
const png::ReadBuf buf{ png.data(), png.size() };
|
||||
png::ImageGreyscale decoded;
|
||||
REQUIRE(png::is_png(buf));
|
||||
REQUIRE(png::decode_png(buf, decoded));
|
||||
REQUIRE(decoded.cols == 8);
|
||||
REQUIRE(decoded.rows == 4);
|
||||
REQUIRE(decoded.buf == solve.grey);
|
||||
}
|
||||
|
||||
TEST_CASE("Each band becomes a height range, and a repeated filament is not a swap", "[ChromaStack]")
|
||||
{
|
||||
ChromaStack stack;
|
||||
stack.layer_height_mm = 0.1f;
|
||||
stack.base_layers = 0;
|
||||
stack.bands = { band(WHITE_OPAQUE, 5), band(BLACK_OPAQUE, 3), band(BLACK_OPAQUE, 2) };
|
||||
|
||||
const std::vector<ChromaRange> ranges = chroma_ranges(stack, 0.);
|
||||
// Three bands, two filaments: the second and third merge, because nothing is swapped between them.
|
||||
REQUIRE(ranges.size() == 2);
|
||||
REQUIRE_THAT(ranges[0].z_min, WithinAbs(0., 1e-9));
|
||||
REQUIRE_THAT(ranges[0].z_max, WithinAbs(0.5, 1e-9));
|
||||
REQUIRE(ranges[0].filament == WHITE_OPAQUE.filament);
|
||||
REQUIRE_THAT(ranges[1].z_min, WithinAbs(0.5, 1e-9));
|
||||
REQUIRE_THAT(ranges[1].z_max, WithinAbs(1.0, 1e-9));
|
||||
REQUIRE(ranges[1].filament == BLACK_OPAQUE.filament);
|
||||
}
|
||||
|
||||
TEST_CASE("A default stack puts the lightest filament at the bottom", "[ChromaStack]")
|
||||
{
|
||||
// Light to dark is what gives the long tonal ramp; the order is the one thing a seeded stack has
|
||||
// to get right, since everything else is a layer count the user can drag.
|
||||
const std::vector<ChromaFilament> loaded{ BLACK_OPAQUE, WHITE_OPAQUE,
|
||||
filament(3, ColorRGB(0.5f, 0.5f, 0.5f), 4.f) };
|
||||
const ChromaStack stack = chroma_default_stack(loaded, 0.08f, 4, 24);
|
||||
|
||||
REQUIRE(stack.bands.size() == 3);
|
||||
REQUIRE(stack.bands.front().filament.filament == WHITE_OPAQUE.filament);
|
||||
REQUIRE(stack.bands.back().filament.filament == BLACK_OPAQUE.filament);
|
||||
REQUIRE(stack.base_layers == 4);
|
||||
// The base is printed in the bottom band, so the relief is what was asked for and the base sits
|
||||
// underneath it.
|
||||
REQUIRE(stack.total_layers() == 4 + 24);
|
||||
REQUIRE(stack.relief_layers() == 24);
|
||||
REQUIRE_THAT(stack.layer_height_mm, WithinRel(0.08f));
|
||||
}
|
||||
|
||||
TEST_CASE("An empty stack or picture solves to nothing", "[ChromaStack]")
|
||||
{
|
||||
ChromaStack empty;
|
||||
REQUIRE(chroma_palette(empty).empty());
|
||||
REQUIRE(chroma_ranges(empty, 0.).empty());
|
||||
|
||||
// A base with nothing on it: there is no picture to solve, and no filament to print the base in.
|
||||
ChromaStack base_only;
|
||||
base_only.layer_height_mm = 0.08f;
|
||||
base_only.base_layers = 4;
|
||||
REQUIRE(base_only.relief_layers() == 0);
|
||||
REQUIRE(chroma_palette(base_only).empty());
|
||||
const std::vector<uint8_t> pixel{ 128, 128, 128 };
|
||||
REQUIRE(chroma_solve(pixel.data(), 1, 1, base_only).empty());
|
||||
|
||||
ChromaStack usable;
|
||||
usable.layer_height_mm = 0.08f;
|
||||
usable.base_layers = 2;
|
||||
usable.bands = { band(WHITE_OPAQUE, 2), band(BLACK_OPAQUE, 4) };
|
||||
REQUIRE(chroma_solve(nullptr, 4, 4, usable).empty());
|
||||
}
|
||||
|
||||
TEST_CASE("The base extends the bottom band's range down to the first layer", "[ChromaStack]")
|
||||
{
|
||||
// The base is not a swap of its own: it prints in whatever is loaded at the start, which is the
|
||||
// bottom band's filament, so the first range has to reach the build plate.
|
||||
ChromaStack stack;
|
||||
stack.layer_height_mm = 0.1f;
|
||||
stack.base_layers = 20;
|
||||
stack.bands = { band(WHITE_OPAQUE, 5), band(BLACK_OPAQUE, 5) };
|
||||
|
||||
const std::vector<ChromaRange> ranges = chroma_ranges(stack, 0.);
|
||||
REQUIRE(ranges.size() == 2);
|
||||
REQUIRE_THAT(ranges[0].z_min, WithinAbs(0., 1e-9));
|
||||
REQUIRE_THAT(ranges[0].z_max, WithinAbs(2.5, 1e-9)); // 20 base + 5 band layers at 0.1 mm
|
||||
REQUIRE_THAT(ranges[1].z_min, WithinAbs(2.5, 1e-9));
|
||||
REQUIRE_THAT(ranges[1].z_max, WithinAbs(3.0, 1e-9));
|
||||
REQUIRE(ranges.back().z_max == Catch::Approx(double(stack.total_layers()) * double(stack.layer_height_mm)));
|
||||
}
|
||||
|
||||
TEST_CASE("A surface's color follows the printed layer it sits at", "[ChromaStack]")
|
||||
{
|
||||
// What the preview colours by: a swap happens at one print Z, so the layer a surface sits in is
|
||||
// what decides its colour, whatever the height map says.
|
||||
ChromaStack stack;
|
||||
stack.layer_height_mm = 0.1f;
|
||||
stack.base_layers = 2;
|
||||
stack.bands = { band(WHITE_OPAQUE, 3), band(BLACK_OPAQUE, 5) };
|
||||
|
||||
REQUIRE(chroma_level_at(stack, 0.05) == 1); // inside the first layer
|
||||
REQUIRE(chroma_level_at(stack, 0.10) == 1); // exactly its top, which that layer printed
|
||||
REQUIRE(chroma_level_at(stack, 0.15) == 2);
|
||||
REQUIRE(chroma_level_at(stack, 0.50) == 5); // the top of the base plus the white band
|
||||
// Clamped at both ends: below the plate and above the stack there is nothing else to show.
|
||||
REQUIRE(chroma_level_at(stack, -1.) == 1);
|
||||
REQUIRE(chroma_level_at(stack, 99.) == stack.total_layers());
|
||||
}
|
||||
|
||||
TEST_CASE("How fast a filament covers is its TD against the layer height", "[ChromaStack]")
|
||||
{
|
||||
// The number that decides whether a band is a gradient or a step, and the one the panel reports:
|
||||
// a filament hides what is under it at about a tenth of its TD, so how many layers that is
|
||||
// depends entirely on how thick they are.
|
||||
REQUIRE_THAT(chroma_layers_to_opaque(4.f, 0.2f), WithinRel(2.f, 1e-4f));
|
||||
REQUIRE_THAT(chroma_layers_to_opaque(4.f, 0.08f), WithinRel(5.f, 1e-4f));
|
||||
REQUIRE_THAT(chroma_layers_to_opaque(8.f, 0.08f), WithinRel(10.f, 1e-4f));
|
||||
// An opaque filament at a coarse layer height covers within one layer, which is where a stack
|
||||
// stops having any colours between its filaments at all.
|
||||
REQUIRE(chroma_layers_to_opaque(1.f, 0.2f) < 1.f);
|
||||
REQUIRE_THAT(chroma_layers_to_opaque(4.f, 0.f), WithinAbs(0.f, 1e-6f));
|
||||
}
|
||||
|
||||
TEST_CASE("A finer layer height buys colors between the filaments", "[ChromaStack]")
|
||||
{
|
||||
// The whole of the user-visible problem in one assertion: the same stack, the same filaments, two
|
||||
// layer heights. The coarse one steps from white to black in two layers; the fine one fades.
|
||||
const auto distinct = [](float layer_height) {
|
||||
ChromaStack stack;
|
||||
stack.layer_height_mm = layer_height;
|
||||
stack.base_layers = 0;
|
||||
stack.bands = { band(WHITE_OPAQUE, 1), band(filament(5, ColorRGB(0.f, 0.f, 0.f), 8.f), 10) };
|
||||
const std::vector<ColorRGB> palette = chroma_palette(stack);
|
||||
int n = 0;
|
||||
for (size_t i = 1; i < palette.size(); ++i)
|
||||
if (std::abs(luminance(palette[i]) - luminance(palette[i - 1])) > 0.02f)
|
||||
++n;
|
||||
return n;
|
||||
};
|
||||
REQUIRE(distinct(0.08f) > distinct(0.3f));
|
||||
}
|
||||
|
||||
TEST_CASE("Layers past a filament's covering depth add no color", "[ChromaStack]")
|
||||
{
|
||||
// The band is twice as long as the filament needs to hide what is under it, so half of it shows a
|
||||
// colour the layer below already showed. That is invisible in the stack and obvious here.
|
||||
ChromaStack stack;
|
||||
stack.layer_height_mm = 0.08f;
|
||||
stack.base_layers = 0;
|
||||
stack.bands = { band(WHITE_OPAQUE, 1), band(filament(5, ColorRGB(0.f, 0.f, 0.f), 4.f), 10) };
|
||||
|
||||
// Trimming keeps every layer that still changes the colour and drops the rest, so the stack gets
|
||||
// shorter and loses none of its colours.
|
||||
const int distinct = chroma_distinct_levels(stack);
|
||||
const int before = stack.relief_layers();
|
||||
chroma_trim_bands(stack);
|
||||
REQUIRE(stack.relief_layers() <= before);
|
||||
REQUIRE(chroma_distinct_levels(stack) == distinct);
|
||||
// The white band is a single layer and has nothing to give up.
|
||||
REQUIRE(stack.bands.front().layers == 1);
|
||||
|
||||
// A light band over a dark one is the case where layers really are wasted: it saturates within a
|
||||
// few layers, and the rest of a long band shows what the layer below already showed.
|
||||
ChromaStack light_over_dark;
|
||||
light_over_dark.layer_height_mm = 0.08f;
|
||||
light_over_dark.base_layers = 0;
|
||||
light_over_dark.bands = { band(filament(5, ColorRGB(0.f, 0.f, 0.f), 4.f), 1), band(WHITE_OPAQUE, 14) };
|
||||
const int kept = chroma_distinct_levels(light_over_dark);
|
||||
chroma_trim_bands(light_over_dark);
|
||||
REQUIRE(light_over_dark.bands.back().layers < 14);
|
||||
REQUIRE(chroma_distinct_levels(light_over_dark) == kept);
|
||||
}
|
||||
Reference in New Issue
Block a user