mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-15 20:01:31 +00:00
Redesign the texture placement gizmo and give the tile two extents
The on-canvas control for a texture layer's placement was a translucent orange quad with two hairline GL arrows. It moved the texture and nothing else, had no hover state, took no notice of the theme, and sized itself from the volume's bounding box, which left it tiny on small parts and huge on large ones. Rotation and tile size were panel-only numbers. The gizmo is now one tile of the texture lying in the paint patch's tangent plane, so it draws the three values it edits: where it sits is the offset, how it is turned is the rotation, how big it is is the tile extents. It carries four chevron arms for an axis-constrained move (the silhouette of the panel's own Adjust placement icon), a six-dot move grip taken from the drag icon, a corner grip on each corner for the extents, and a ring on a short stem for the rotation. Colours come from ImGuiWrapper's COL_ORCA and the icon grey rather than raw orange and yellow. It is drawn through ImGui's foreground draw list instead of the flat shader, which is what allows antialiased sub-pixel strokes, rounded joins, a contrast halo under every stroke and a numeric readout by the cursor. That halo matters because a volume stays the same neutral grey whatever the theme, so the accent alone cannot carry contrast; the halo strength and the handle fill do. Handle sizes and hit radii are in screen pixels, so the control is equally usable at any zoom or model size, and a tile that projects below 44px is drawn at that floor and dashed to say it is not to scale. Picking is a real point-in-quad test rather than a circle through the corners, so a move can no longer start from empty space beside the tile. Rotating or resizing changes where a given offset places the tile, so every drag recomputes the offset from the centre it means to keep. That is what makes rotate and scale act in place instead of swinging the tile around the anchor, and it leaves one place that writes offset. Tile size gains a second extent. tiling_scale_v holds the v extent in mm and anything <= 0 means "same as u", which is both the default and what every layer written before this meant, so existing layers and older undo snapshots still read back as the square tiles they were. The new layer_tile_mm() states that fallback once and every consumer goes through it: the bake, both preview shaders, the UV pane and the gizmo. The panel shows one slider until the lock is opened, and the lock state is the data rather than a separate flag that could fall out of step with it. The extents now divide after the rotation in apply_uv_transform() and in all four shaders. Dividing two different extents before it would shear the tile rather than rotate it; with a square tile both orders are algebraically identical, so no existing layer changes. The inverse in UVEditorCanvas::uv_delta_to_unwrap() and the island matrix in uv_editor_island_transforms() follow the same reordering, and the texel size used for refinement and step cutting now takes the finer extent.
This commit is contained in:
@@ -470,7 +470,10 @@ V2Resolution recommend_v2_resolution(const indexed_triangle_set
|
||||
const DecodedHeightTexture &tex = decode_height_texture(layer);
|
||||
if (tex.width <= 0)
|
||||
continue;
|
||||
const float texel = layer.tiling_scale / float(tex.width);
|
||||
// Texels are square in both axes (the aspect correction is what keeps them so), so the finer
|
||||
// of the two tile extents sets the finest texel the refinement has to resolve.
|
||||
const Vec2f tile = layer_tile_mm(layer);
|
||||
const float texel = std::min(tile.x(), tile.y()) / float(tex.width);
|
||||
if (out.texel_mm <= 0.f || texel < out.texel_mm) {
|
||||
out.texel_mm = texel;
|
||||
out.pixels_per_edge = analyze_texture_detail(layer).pixels_per_edge;
|
||||
@@ -1678,12 +1681,14 @@ namespace {
|
||||
// of them was most of the per-sample arithmetic.
|
||||
struct UVTransform
|
||||
{
|
||||
float scale, cs, sn, aspect;
|
||||
float inv_u, inv_v, cs, sn, aspect;
|
||||
Vec2f offset;
|
||||
|
||||
UVTransform(const TextureDisplacementLayer &layer, float aspect_)
|
||||
{
|
||||
scale = (layer.tiling_scale > 1e-6f) ? (1.f / layer.tiling_scale) : 1.f;
|
||||
const Vec2f tile = layer_tile_mm(layer);
|
||||
inv_u = 1.f / tile.x();
|
||||
inv_v = 1.f / tile.y();
|
||||
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
|
||||
cs = std::cos(rad);
|
||||
sn = std::sin(rad);
|
||||
@@ -1692,9 +1697,11 @@ struct UVTransform
|
||||
}
|
||||
Vec2f operator()(const Vec2f &planar) const
|
||||
{
|
||||
const Vec2f scaled = planar * scale;
|
||||
Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs);
|
||||
// See apply_uv_transform() for why the aspect correction follows the rotation.
|
||||
// See apply_uv_transform() for why both the tile extents and the aspect correction follow
|
||||
// the rotation rather than preceding it.
|
||||
Vec2f rotated(planar.x() * cs - planar.y() * sn, planar.x() * sn + planar.y() * cs);
|
||||
rotated.x() *= inv_u;
|
||||
rotated.y() *= inv_v;
|
||||
if (aspect > 0.f && aspect != 1.f)
|
||||
rotated.y() *= aspect;
|
||||
return rotated + offset;
|
||||
@@ -1714,24 +1721,26 @@ inline Vec3f triplanar_weights(const Vec3f &normal)
|
||||
|
||||
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect)
|
||||
{
|
||||
const float scale = (layer.tiling_scale > 1e-6f) ? (1.f / layer.tiling_scale) : 1.f;
|
||||
const Vec2f scaled = planar * scale;
|
||||
|
||||
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
|
||||
const float cs = std::cos(rad);
|
||||
const float sn = std::sin(rad);
|
||||
Vec2f rotated(scaled.x() * cs - scaled.y() * sn, scaled.x() * sn + scaled.y() * cs);
|
||||
Vec2f rotated(planar.x() * cs - planar.y() * sn, planar.x() * sn + planar.y() * cs);
|
||||
|
||||
// The tile extents. Dividing here rather than before the rotation is what makes a non-square
|
||||
// tile a rotated *rectangle* on the surface: scaling the two axes of an already-rotated
|
||||
// coordinate by different amounts would be a shear, so the rotation has to come first and the
|
||||
// extents divide in the tile's own frame. With a square tile the two orders agree exactly,
|
||||
// R being linear - which is why this is not a change in behaviour for any existing layer.
|
||||
const Vec2f tile = layer_tile_mm(layer);
|
||||
rotated.x() /= tile.x();
|
||||
rotated.y() /= tile.y();
|
||||
|
||||
// Non-square textures. Without this the [0,1] square of uv covers the whole image whatever its
|
||||
// proportions, so a 2:1 image is squeezed into a square tile and every feature in it comes out
|
||||
// half as wide as it should be. `tiling_scale` is the tile's size along u; the tile is
|
||||
// `tiling_scale * height / width` mm along v, which is exactly what keeps texels square - so
|
||||
// dividing v by that extent is the same as multiplying it by width / height. A square texture has
|
||||
// aspect 1 and is untouched, which is why this changes nothing for the shipped library.
|
||||
//
|
||||
// Applied after the rotation, not before: scaling one axis of an already-rotated coordinate is a
|
||||
// shear, and doing it the other way round would make "Rotation" skew the pattern instead of
|
||||
// turning it.
|
||||
// proportions, so a 2:1 image is squeezed into one tile and every feature in it comes out half
|
||||
// as wide as it should be. The tile is `layer_tile_mm().y() * height / width` mm along v, which
|
||||
// is exactly what keeps texels square - so dividing v by that extent is the same as multiplying
|
||||
// it by width / height. A square texture has aspect 1 and is untouched, which is why this
|
||||
// changes nothing for the shipped library.
|
||||
if (aspect > 0.f && aspect != 1.f)
|
||||
rotated.y() *= aspect;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user