mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-15 20:01:31 +00:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbaed8a512 | ||
|
|
597b2abac4 | ||
|
|
ba4de8c1fc | ||
|
|
7bce53a773 | ||
|
|
e65113af6d | ||
|
|
91ab04e823 |
+2267
-3997
File diff suppressed because it is too large
Load Diff
+3068
-5238
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
@@ -1 +1 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><rect x="1.5" y="3.5" width="12" height="8" rx="1" ry="1" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/><path d="M3,5h1v1h-1Z M5,5h1v1h-1Z M7,5h1v1h-1Z M9,5h1v1h-1Z M11,5h1v1h-1Z M4,7h1v1h-1Z M6,7h1v1h-1Z M8,7h1v1h-1Z M10,7h1v1h-1Z M5,9h5v1h-5Z" style="fill:#fefefe;"/></svg>
|
||||
<?xml version="1.0" encoding="UTF-8"?><svg id="a" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><rect x="7" y="12" width="1" height="1" style="fill:#fefefe;"/><path d="M4.5,5.5c0-1.71,1.43-3.09,3.16-3,1.62.08,2.84,1.54,2.84,3.17h0c0,.61-.25,1.2-.69,1.62l-2.31,2.24v.97" style="fill:none; stroke:#fefefe; stroke-linecap:round; stroke-linejoin:round;"/></svg>
|
||||
|
Before Width: | Height: | Size: 415 B After Width: | Height: | Size: 366 B |
@@ -36,6 +36,11 @@ uniform int palette_b[64];
|
||||
uniform float prefer_pure_de; // PREFER_PURE_DE: how much better than a single filament a mix must be
|
||||
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
|
||||
uniform bool has_color_tex;
|
||||
// False means the texture is a single placement, not a repeat. The sampler's wrap mode can express
|
||||
// that for the *height* - a zero border reads as no displacement - but not for colour, because a
|
||||
// zero border is indistinguishable from the image genuinely being black there. So the uv is tested
|
||||
// instead, which is what sample_layer_color() does on the CPU side.
|
||||
uniform bool tile_enabled;
|
||||
uniform bool volume_mirrored;
|
||||
|
||||
uniform mat4 view_model_matrix;
|
||||
@@ -45,6 +50,14 @@ uniform sampler2D height_tex;
|
||||
uniform vec2 height_tex_texel;
|
||||
uniform float depth_mm;
|
||||
uniform float tiling_scale;
|
||||
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - the square tile
|
||||
// every layer had before the two axes could differ - which is also what layer_tile_mm() does on the
|
||||
// CPU side, so an unset uniform behaves exactly as this shader always did.
|
||||
uniform float tiling_scale_v;
|
||||
// Mirror the texture along one axis of the tile. Both default to false, which GLSL guarantees for an
|
||||
// uninitialised bool uniform, so a shader whose caller has not set them behaves exactly as before.
|
||||
uniform bool mirror_u;
|
||||
uniform bool mirror_v;
|
||||
// Height map width / height. Scales the v axis so a non-square image keeps its proportions
|
||||
// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform().
|
||||
uniform float tex_aspect;
|
||||
@@ -54,6 +67,12 @@ uniform bool invert;
|
||||
uniform float midlevel; // the height that means "don't move"; needed by the parallax step
|
||||
uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor)
|
||||
uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin
|
||||
// The placement's rotation and its inverse, so this shader turns a world position into the texture
|
||||
// frame exactly as texture_displacement_bake_frame() does. Both are passed rather than one
|
||||
// transposed here, because GLSL 110 has no transpose(). Identity leaves the old world-aligned
|
||||
// behaviour, which is what an unset uniform gives.
|
||||
uniform mat3 tex_rot; // world -> texture frame
|
||||
uniform mat3 tex_rot_inv; // texture frame -> world
|
||||
uniform bool use_vertex_uv;
|
||||
// 2x3 affine (lin = (m00, m01, m10, m11), tr = (m02, m12)) applied to the dragged island's uv; see the
|
||||
// 140 variant. Identity when nothing is dragged.
|
||||
@@ -162,13 +181,29 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
|
||||
}
|
||||
}
|
||||
|
||||
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
|
||||
vec2 tile_mm()
|
||||
{
|
||||
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
|
||||
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
|
||||
}
|
||||
|
||||
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
|
||||
vec2 mirror_sign()
|
||||
{
|
||||
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
|
||||
}
|
||||
|
||||
vec2 project_uv(vec3 p, vec3 n)
|
||||
{
|
||||
vec2 planar = projection_raw(p, n);
|
||||
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
|
||||
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
|
||||
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
|
||||
// the same division, in the tile's own frame.
|
||||
r *= mirror_sign() / tile_mm();
|
||||
// After the rotation, so the rotation stays a rotation rather than becoming a shear.
|
||||
r.y *= tex_aspect;
|
||||
return r + uv_offset;
|
||||
@@ -237,9 +272,12 @@ void main()
|
||||
|
||||
// World millimetres throughout, like the bake - see the 140 variant.
|
||||
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
|
||||
vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does
|
||||
vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor); // the frame the texture is projected in, as the bake does
|
||||
if (volume_mirrored)
|
||||
triangle_normal = -triangle_normal;
|
||||
// The same normal in that frame. triangle_normal itself stays in world, because the lighting at
|
||||
// the end of this shader needs it there.
|
||||
vec3 tex_normal = tex_rot * triangle_normal;
|
||||
|
||||
// Where the colour is read from. Both branches below already compute the uv this fragment's
|
||||
// *height* came from - including the parallax-marched one on the triplanar path - and the colour
|
||||
@@ -270,14 +308,14 @@ void main()
|
||||
} else if (weight > 0.0) {
|
||||
vec3 t, b;
|
||||
vec2 units_per_mm;
|
||||
projection_axes(tex_pos, triangle_normal, t, b, units_per_mm);
|
||||
projection_axes(tex_pos, tex_normal, t, b, units_per_mm);
|
||||
|
||||
// Parallax occlusion mapping: march the view ray through the height shell and shade at the
|
||||
// first point where it drops below the displaced surface (see header).
|
||||
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
|
||||
vec3 view_dir = normalize(eye_model_pos - tex_pos);
|
||||
float v_dot_n = dot(view_dir, triangle_normal);
|
||||
vec2 uv = project_uv(tex_pos, triangle_normal);
|
||||
vec3 view_dir = normalize(tex_rot * eye_model_pos - tex_pos);
|
||||
float v_dot_n = dot(view_dir, tex_normal);
|
||||
vec2 uv = project_uv(tex_pos, tex_normal);
|
||||
|
||||
// The shell the displaced surface lives inside, as signed heights along the normal. Taken from
|
||||
// both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel,
|
||||
@@ -289,18 +327,18 @@ void main()
|
||||
// How far, in mm, sweeping the ray across the shell slides the sample point sideways. Below half
|
||||
// a texel there is no parallax to find and the march would be pure cost - which is the common
|
||||
// case of looking straight down at a surface.
|
||||
float sweep = length(view_dir - triangle_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
|
||||
if (v_dot_n > 0.05 && sweep > 0.5 * tiling_scale * height_tex_texel.x) {
|
||||
float sweep = length(view_dir - tex_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
|
||||
if (v_dot_n > 0.05 && sweep > 0.5 * min(tile_mm().x, tile_mm().y) * height_tex_texel.x) {
|
||||
// A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the
|
||||
// undisplaced surface. Start at the top of the shell, where the ray is outside the surface
|
||||
// by construction, and step inward; the crossing is what this pixel actually sees.
|
||||
float s = h_hi / v_dot_n;
|
||||
float ds = (h_hi - h_lo) / (v_dot_n * float(PARALLAX_STEPS));
|
||||
vec2 prev_uv = project_uv(tex_pos + view_dir * s, triangle_normal);
|
||||
vec2 prev_uv = project_uv(tex_pos + view_dir * s, tex_normal);
|
||||
float prev_gap = h_hi - amp * (H_AT(prev_uv) - midlevel); // >= 0 by construction
|
||||
for (int i = 0; i < PARALLAX_STEPS; ++i) {
|
||||
s -= ds;
|
||||
vec2 cur_uv = project_uv(tex_pos + view_dir * s, triangle_normal);
|
||||
vec2 cur_uv = project_uv(tex_pos + view_dir * s, tex_normal);
|
||||
float gap = s * v_dot_n - amp * (H_AT(cur_uv) - midlevel);
|
||||
if (gap <= 0.0) {
|
||||
// Crossed between the last two samples - interpolating the hit is what stops it
|
||||
@@ -322,18 +360,23 @@ void main()
|
||||
float hU = texture2D(height_tex, uv + vec2(0.0, height_tex_texel.y)).r;
|
||||
|
||||
vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y));
|
||||
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0);
|
||||
vec2 tile = tile_mm();
|
||||
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
|
||||
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
// One uv unit is tiling_scale mm along u but tiling_scale / tex_aspect mm along v, so the v
|
||||
// component of the gradient carries the extra factor before being rotated back into t/b.
|
||||
vec2 g = vec2(dh_duv.x, dh_duv.y * tex_aspect);
|
||||
// One uv unit is tile.x mm along u but tile.y / tex_aspect mm along v, so each component of
|
||||
// the uv-space gradient carries its own factor before being rotated back into t/b.
|
||||
// Each component carries its own extent and its own mirror sign, both of which project_uv()
|
||||
// applied on the way in; without the sign a mirrored layer would light as though it were not.
|
||||
vec2 ms = mirror_sign();
|
||||
vec2 g = vec2(dh_duv.x * ms.x / tile.x, dh_duv.y * tex_aspect * ms.y / tile.y);
|
||||
// ...and back out of raw-projection units into millimetres along t / b; see the 140 variant.
|
||||
vec2 slope = amplitude * vec2(g.x * cs + g.y * sn, -g.x * sn + g.y * cs) * units_per_mm;
|
||||
|
||||
vec3 gradient = slope.x * t + slope.y * b;
|
||||
// t and b came out of the texture frame, so the gradient is in it too and has to come back
|
||||
// to world before it perturbs the normal the lighting uses.
|
||||
vec3 gradient = tex_rot_inv * (slope.x * t + slope.y * b);
|
||||
gradient -= triangle_normal * dot(triangle_normal, gradient);
|
||||
triangle_normal = normalize(triangle_normal - gradient);
|
||||
}
|
||||
@@ -353,7 +396,9 @@ void main()
|
||||
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
|
||||
// the same material under the same light, and the relief this preview exists to show is unaffected.
|
||||
vec3 albedo = uniform_color.rgb;
|
||||
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
|
||||
bool uv_covered = tile_enabled ||
|
||||
(all(greaterThanEqual(color_uv, vec2(0.0))) && all(lessThan(color_uv, vec2(1.0))));
|
||||
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0 && uv_covered)
|
||||
albedo = palette_rgb[nearest_palette_entry(texture2D(color_tex, color_uv).rgb)];
|
||||
gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,16 @@ uniform bool volume_mirrored;
|
||||
uniform int mode;
|
||||
uniform float checker_freq;
|
||||
uniform float tiling_scale;
|
||||
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - see
|
||||
// layer_tile_mm() on the CPU side, so an unset uniform behaves exactly as this shader always did.
|
||||
uniform float tiling_scale_v;
|
||||
// Mirror along one tile axis; false by default, so an unset uniform is the old behaviour.
|
||||
uniform bool mirror_u;
|
||||
uniform bool mirror_v;
|
||||
uniform vec3 tex_anchor; // the volume's origin in world space
|
||||
// The placement's rotation, so this overlay projects the same frame the bake does. Identity, which
|
||||
// is what an unset uniform gives, leaves the old world-aligned behaviour.
|
||||
uniform mat3 tex_rot; // world -> texture frame
|
||||
uniform float rotation_rad;
|
||||
uniform vec2 uv_offset;
|
||||
uniform bool use_vertex_uv;
|
||||
@@ -79,13 +88,30 @@ vec2 projection_raw(vec3 p, vec3 n)
|
||||
return vec2(box_face_sign(n.z) * p.x, -p.y);
|
||||
}
|
||||
|
||||
|
||||
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
|
||||
vec2 tile_mm()
|
||||
{
|
||||
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
|
||||
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
|
||||
}
|
||||
|
||||
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
|
||||
vec2 mirror_sign()
|
||||
{
|
||||
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
|
||||
}
|
||||
|
||||
vec2 project_uv(vec3 p, vec3 n)
|
||||
{
|
||||
vec2 planar = projection_raw(p, n);
|
||||
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
|
||||
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
|
||||
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
|
||||
// the same division, in the tile's own frame.
|
||||
r *= mirror_sign() / tile_mm();
|
||||
r.y *= tex_aspect; // after the rotation, so the rotation stays a rotation rather than a shear
|
||||
return r + uv_offset;
|
||||
}
|
||||
@@ -105,7 +131,7 @@ void main()
|
||||
|
||||
// World space anchored at the volume's origin, like the bake and the shaded preview.
|
||||
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
|
||||
vec3 tex_pos = world_pos.xyz - tex_anchor;
|
||||
vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor);
|
||||
if (volume_mirrored)
|
||||
triangle_normal = -triangle_normal;
|
||||
|
||||
@@ -113,7 +139,7 @@ void main()
|
||||
if (mode == 1) {
|
||||
base = heatmap(distortion);
|
||||
} else {
|
||||
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, triangle_normal);
|
||||
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, tex_rot * triangle_normal);
|
||||
vec2 c = floor(uv * checker_freq);
|
||||
float check = mod(c.x + c.y, 2.0);
|
||||
base = (check < 0.5) ? vec3(0.22, 0.23, 0.26) : vec3(0.82, 0.83, 0.86);
|
||||
|
||||
@@ -95,6 +95,11 @@ uniform int palette_b[64];
|
||||
uniform float prefer_pure_de; // PREFER_PURE_DE: how much better than a single filament a mix must be
|
||||
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
|
||||
uniform bool has_color_tex;
|
||||
// False means the texture is a single placement, not a repeat. The sampler's wrap mode can express
|
||||
// that for the *height* - a zero border reads as no displacement - but not for colour, because a
|
||||
// zero border is indistinguishable from the image genuinely being black there. So the uv is tested
|
||||
// instead, which is what sample_layer_color() does on the CPU side.
|
||||
uniform bool tile_enabled;
|
||||
uniform bool volume_mirrored;
|
||||
|
||||
uniform mat4 view_model_matrix;
|
||||
@@ -104,6 +109,14 @@ uniform sampler2D height_tex;
|
||||
uniform vec2 height_tex_texel; // (1/width, 1/height) of height_tex
|
||||
uniform float depth_mm;
|
||||
uniform float tiling_scale;
|
||||
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - the square tile
|
||||
// every layer had before the two axes could differ - which is also what layer_tile_mm() does on the
|
||||
// CPU side, so an unset uniform behaves exactly as this shader always did.
|
||||
uniform float tiling_scale_v;
|
||||
// Mirror the texture along one axis of the tile. Both default to false, which GLSL guarantees for an
|
||||
// uninitialised bool uniform, so a shader whose caller has not set them behaves exactly as before.
|
||||
uniform bool mirror_u;
|
||||
uniform bool mirror_v;
|
||||
// Height map width / height. Scales the v axis so a non-square image keeps its proportions
|
||||
// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform().
|
||||
uniform float tex_aspect;
|
||||
@@ -113,6 +126,12 @@ uniform bool invert;
|
||||
uniform float midlevel; // the height that means "don't move"; needed by the parallax step
|
||||
uniform vec3 eye_model_pos; // camera position in the texture frame (world minus tex_anchor)
|
||||
uniform vec3 tex_anchor; // the volume's origin in world space: the texture frame's origin
|
||||
// The placement's rotation and its inverse, so this shader turns a world position into the texture
|
||||
// frame exactly as texture_displacement_bake_frame() does. Both are passed rather than one
|
||||
// transposed here, because GLSL 110 has no transpose(). Identity leaves the old world-aligned
|
||||
// behaviour, which is what an unset uniform gives.
|
||||
uniform mat3 tex_rot; // world -> texture frame
|
||||
uniform mat3 tex_rot_inv; // texture frame -> world
|
||||
uniform bool use_vertex_uv; // true: sample at vertex_uv with a derived tangent frame (LSCM)
|
||||
// A 2x3 affine (columns packed as lin = (m00, m01, m10, m11), tr = (m02, m12)) applied to the uv of
|
||||
// the island currently being dragged in the UV editor (island_active > 0.5). Identity when nothing is
|
||||
@@ -228,13 +247,29 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
|
||||
}
|
||||
}
|
||||
|
||||
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
|
||||
vec2 tile_mm()
|
||||
{
|
||||
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
|
||||
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
|
||||
}
|
||||
|
||||
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
|
||||
vec2 mirror_sign()
|
||||
{
|
||||
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
|
||||
}
|
||||
|
||||
vec2 project_uv(vec3 p, vec3 n)
|
||||
{
|
||||
vec2 planar = projection_raw(p, n);
|
||||
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
|
||||
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
|
||||
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
|
||||
// the same division, in the tile's own frame.
|
||||
r *= mirror_sign() / tile_mm();
|
||||
// After the rotation, so the rotation stays a rotation rather than becoming a shear.
|
||||
r.y *= tex_aspect;
|
||||
return r + uv_offset;
|
||||
@@ -305,9 +340,12 @@ void main()
|
||||
// printed part whatever the instance's scale or rotation, so the preview has to project from the
|
||||
// world position and perturb the world normal.
|
||||
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
|
||||
vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does
|
||||
vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor); // the frame the texture is projected in, as the bake does
|
||||
if (volume_mirrored)
|
||||
triangle_normal = -triangle_normal;
|
||||
// The same normal in that frame. triangle_normal itself stays in world, because the lighting at
|
||||
// the end of this shader needs it there.
|
||||
vec3 tex_normal = tex_rot * triangle_normal;
|
||||
|
||||
// Where the colour is read from. Both branches below already compute the uv this fragment's
|
||||
// *height* came from - including the parallax-marched one on the triplanar path - and the colour
|
||||
@@ -351,14 +389,14 @@ void main()
|
||||
// closed-form uv here, unlike the LSCM case.
|
||||
vec3 t, b;
|
||||
vec2 units_per_mm;
|
||||
projection_axes(tex_pos, triangle_normal, t, b, units_per_mm);
|
||||
projection_axes(tex_pos, tex_normal, t, b, units_per_mm);
|
||||
|
||||
// Parallax occlusion mapping: march the view ray through the height shell and shade at the
|
||||
// first point where it drops below the displaced surface (see header).
|
||||
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
|
||||
vec3 view_dir = normalize(eye_model_pos - tex_pos);
|
||||
float v_dot_n = dot(view_dir, triangle_normal);
|
||||
vec2 uv = project_uv(tex_pos, triangle_normal);
|
||||
vec3 view_dir = normalize(tex_rot * eye_model_pos - tex_pos);
|
||||
float v_dot_n = dot(view_dir, tex_normal);
|
||||
vec2 uv = project_uv(tex_pos, tex_normal);
|
||||
|
||||
// The shell the displaced surface lives inside, as signed heights along the normal. Taken from
|
||||
// both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel,
|
||||
@@ -370,18 +408,18 @@ void main()
|
||||
// How far, in mm, sweeping the ray across the shell slides the sample point sideways. Below half
|
||||
// a texel there is no parallax to find and the march would be pure cost - which is the common
|
||||
// case of looking straight down at a surface.
|
||||
float sweep = length(view_dir - triangle_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
|
||||
if (v_dot_n > 0.05 && sweep > 0.5 * tiling_scale * height_tex_texel.x) {
|
||||
float sweep = length(view_dir - tex_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
|
||||
if (v_dot_n > 0.05 && sweep > 0.5 * min(tile_mm().x, tile_mm().y) * height_tex_texel.x) {
|
||||
// A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the
|
||||
// undisplaced surface. Start at the top of the shell, where the ray is outside the surface
|
||||
// by construction, and step inward; the crossing is what this pixel actually sees.
|
||||
float s = h_hi / v_dot_n;
|
||||
float ds = (h_hi - h_lo) / (v_dot_n * float(PARALLAX_STEPS));
|
||||
vec2 prev_uv = project_uv(tex_pos + view_dir * s, triangle_normal);
|
||||
vec2 prev_uv = project_uv(tex_pos + view_dir * s, tex_normal);
|
||||
float prev_gap = h_hi - amp * (H_AT(prev_uv) - midlevel); // >= 0 by construction
|
||||
for (int i = 0; i < PARALLAX_STEPS; ++i) {
|
||||
s -= ds;
|
||||
vec2 cur_uv = project_uv(tex_pos + view_dir * s, triangle_normal);
|
||||
vec2 cur_uv = project_uv(tex_pos + view_dir * s, tex_normal);
|
||||
float gap = s * v_dot_n - amp * (H_AT(cur_uv) - midlevel);
|
||||
if (gap <= 0.0) {
|
||||
// Crossed between the last two samples - interpolating the hit is what stops it
|
||||
@@ -405,23 +443,28 @@ void main()
|
||||
// Central difference, per uv unit (not per texel).
|
||||
vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y));
|
||||
|
||||
// uv -> mm is 1/tiling_scale for the triplanar projection, so this turns the uv-space
|
||||
// gradient into a real surface slope.
|
||||
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0);
|
||||
// uv -> mm is the reciprocal of the tile extents, applied per component below, which is what
|
||||
// turns the uv-space gradient into a real surface slope.
|
||||
vec2 tile = tile_mm();
|
||||
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
|
||||
// uv was rotated by project_uv() while t/b are the unrotated model axes, so rotate the
|
||||
// gradient back into the axes' frame.
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
// One uv unit is tiling_scale mm along u but tiling_scale / tex_aspect mm along v, so the v
|
||||
// component of the gradient carries the extra factor before being rotated back into t/b.
|
||||
vec2 g = vec2(dh_duv.x, dh_duv.y * tex_aspect);
|
||||
// One uv unit is tile.x mm along u but tile.y / tex_aspect mm along v, so each component of
|
||||
// the uv-space gradient carries its own factor before being rotated back into t/b.
|
||||
// Each component carries its own extent and its own mirror sign, both of which project_uv()
|
||||
// applied on the way in; without the sign a mirrored layer would light as though it were not.
|
||||
vec2 ms = mirror_sign();
|
||||
vec2 g = vec2(dh_duv.x * ms.x / tile.x, dh_duv.y * tex_aspect * ms.y / tile.y);
|
||||
// ...and back out of raw-projection units into millimetres of travel along t / b, which is a
|
||||
// no-op except for the spherical longitude (see projection_axes()). After the inverse rotation,
|
||||
// because units_per_mm is expressed in the t/b frame rather than in uv.
|
||||
vec2 slope = amplitude * vec2(g.x * cs + g.y * sn, -g.x * sn + g.y * cs) * units_per_mm;
|
||||
|
||||
vec3 gradient = slope.x * t + slope.y * b;
|
||||
// t and b came out of the texture frame, so the gradient is in it too and has to come back
|
||||
// to world before it perturbs the normal the lighting uses.
|
||||
vec3 gradient = tex_rot_inv * (slope.x * t + slope.y * b);
|
||||
gradient -= triangle_normal * dot(triangle_normal, gradient);
|
||||
triangle_normal = normalize(triangle_normal - gradient);
|
||||
}
|
||||
@@ -441,7 +484,9 @@ void main()
|
||||
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
|
||||
// the same material under the same light, and the relief this preview exists to show is unaffected.
|
||||
vec3 albedo = uniform_color.rgb;
|
||||
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
|
||||
bool uv_covered = tile_enabled ||
|
||||
(all(greaterThanEqual(color_uv, vec2(0.0))) && all(lessThan(color_uv, vec2(1.0))));
|
||||
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0 && uv_covered)
|
||||
albedo = palette_rgb[nearest_palette_entry(texture(color_tex, color_uv).rgb)];
|
||||
out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,16 @@ uniform bool volume_mirrored;
|
||||
uniform int mode; // 0 checker, 1 distortion
|
||||
uniform float checker_freq; // checker squares per uv unit (one uv unit == one texture tile)
|
||||
uniform float tiling_scale;
|
||||
// Tile extent along v, in mm. Zero or negative means "the same as tiling_scale" - see
|
||||
// layer_tile_mm() on the CPU side, so an unset uniform behaves exactly as this shader always did.
|
||||
uniform float tiling_scale_v;
|
||||
// Mirror along one tile axis; false by default, so an unset uniform is the old behaviour.
|
||||
uniform bool mirror_u;
|
||||
uniform bool mirror_v;
|
||||
uniform vec3 tex_anchor; // the volume's origin in world space
|
||||
// The placement's rotation, so this overlay projects the same frame the bake does. Identity, which
|
||||
// is what an unset uniform gives, leaves the old world-aligned behaviour.
|
||||
uniform mat3 tex_rot; // world -> texture frame
|
||||
uniform float rotation_rad;
|
||||
uniform vec2 uv_offset;
|
||||
uniform bool use_vertex_uv;
|
||||
@@ -90,13 +99,30 @@ vec2 projection_raw(vec3 p, vec3 n)
|
||||
return vec2(box_face_sign(n.z) * p.x, -p.y);
|
||||
}
|
||||
|
||||
|
||||
// Tile extents in mm, u then v, with the v fallback layer_tile_mm() applies.
|
||||
vec2 tile_mm()
|
||||
{
|
||||
float su = (tiling_scale > 1e-6) ? tiling_scale : 1.0;
|
||||
return vec2(su, (tiling_scale_v > 1e-6) ? tiling_scale_v : su);
|
||||
}
|
||||
|
||||
// +1 or -1 per axis, matching layer_mirror_sign() on the CPU side.
|
||||
vec2 mirror_sign()
|
||||
{
|
||||
return vec2(mirror_u ? -1.0 : 1.0, mirror_v ? -1.0 : 1.0);
|
||||
}
|
||||
|
||||
vec2 project_uv(vec3 p, vec3 n)
|
||||
{
|
||||
vec2 planar = projection_raw(p, n);
|
||||
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
|
||||
float cs = cos(rotation_rad);
|
||||
float sn = sin(rotation_rad);
|
||||
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
|
||||
// Extents divide after the rotation, so a tile whose two sides differ stays a rotated rectangle
|
||||
// instead of shearing. With a square tile the two orders agree exactly. The mirror is a sign on
|
||||
// the same division, in the tile's own frame.
|
||||
r *= mirror_sign() / tile_mm();
|
||||
r.y *= tex_aspect; // after the rotation, so the rotation stays a rotation rather than a shear
|
||||
return r + uv_offset;
|
||||
}
|
||||
@@ -117,7 +143,7 @@ void main()
|
||||
|
||||
// World space anchored at the volume's origin, like the bake and the shaded preview.
|
||||
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
|
||||
vec3 tex_pos = world_pos.xyz - tex_anchor;
|
||||
vec3 tex_pos = tex_rot * (world_pos.xyz - tex_anchor);
|
||||
if (volume_mirrored)
|
||||
triangle_normal = -triangle_normal;
|
||||
|
||||
@@ -125,7 +151,7 @@ void main()
|
||||
if (mode == 1) {
|
||||
base = heatmap(distortion);
|
||||
} else {
|
||||
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, triangle_normal);
|
||||
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(tex_pos, tex_rot * triangle_normal);
|
||||
vec2 c = floor(uv * checker_freq);
|
||||
float check = mod(c.x + c.y, 2.0);
|
||||
// Two distinct greys, plus a faint tint on one set so orientation is readable at a glance.
|
||||
|
||||
@@ -1,957 +0,0 @@
|
||||
# /// script
|
||||
# requires-python = ">=3.12"
|
||||
#
|
||||
# [tool.orcaslicer.plugin]
|
||||
# id = "preset-update-demo"
|
||||
# name = "Preset Update Demo"
|
||||
# description = "Edits any setting of the currently edited process, printer, or filament preset through the preset host API."
|
||||
# author = "OrcaSlicer contributors"
|
||||
# version = "0.2.0"
|
||||
# type = "script"
|
||||
# ///
|
||||
"""Preset Update Demo — edit any setting of the current preset from a plugin window.
|
||||
|
||||
Install this file in the local ``orca_plugins`` directory, enable the plugin, then run
|
||||
"Preset Update Demo" from the Plugins dialog. The window lists every setting of the
|
||||
preset the matching settings tab is editing right now — process, printer, or filament —
|
||||
with its current serialized value, and writes a value back through
|
||||
``orca.host.preset.<type>.update(...)``.
|
||||
|
||||
Process and printer each have one edited preset, taken from
|
||||
``current_process_preset()`` / ``current_printer_preset()``. Filament presets are
|
||||
addressed by slot instead: the window reads one preset per slot from
|
||||
``current_filament_presets()`` (slot numbers are one-based, matching Orca's labels),
|
||||
shows the selected slot's preset, and writes through
|
||||
``orca.host.preset.filament.update(slot, key, value)``.
|
||||
|
||||
Any assigned slot can be edited: the backend selects that slot's preset through Orca's
|
||||
normal preset selection before it writes, so applying can switch the filament tab and Orca
|
||||
may ask about unsaved changes first. Only an empty slot, whose mount resolves to no preset,
|
||||
is unavailable — the window says so instead of offering an edit. A slot whose preset is not
|
||||
the one the filament settings already have open gets an informational note that applying
|
||||
will switch to it; it is never presented as a refusal. Two slots that mount the same preset
|
||||
are one preset, so editing through either of them changes both; the window marks those
|
||||
slots and says which they are.
|
||||
|
||||
Every row is one ``config_keys()`` entry carrying ``config_value(key)``.
|
||||
|
||||
What ``update()`` does, and what it does not:
|
||||
|
||||
* it runs the same validation and UI update path as editing the setting by hand, so
|
||||
the change lands in the preset the tab is editing and is never written to disk here
|
||||
— the preset shows as modified in OrcaSlicer once it differs from the saved one;
|
||||
* it accepts only settings of that preset category. A key that belongs to another
|
||||
category, or a value the setting would reject, raises an error and writes nothing;
|
||||
* it is refused when the selected printer's technology does not use that tab (the
|
||||
print and filament tabs do nothing under SLA), so this demo covers the three FFF
|
||||
categories only;
|
||||
* a setting whose page has not been built yet still runs its owning settings group's
|
||||
checks and callbacks; behavior that requires an actual widget (such as widget-specific
|
||||
parsing or clamping) only runs when the field exists;
|
||||
* on a filament slot it selects that slot's preset first, so Orca may prompt about
|
||||
unsaved changes in the filament tab before the value is written.
|
||||
|
||||
Values are OrcaSlicer's serialized config syntax — the same text a preset file stores —
|
||||
so numbers are plain, percentages keep their ``%``, booleans are ``1``/``0``, choices use
|
||||
the setting's internal key, and lists are semicolon-separated.
|
||||
"""
|
||||
|
||||
import orca
|
||||
|
||||
|
||||
# The three preset categories the main tabs edit. Each key is both the value the page
|
||||
# sends and the attribute name on orca.host.preset that carries the matching editor.
|
||||
KINDS = (
|
||||
{"kind": "process", "label": "Process"},
|
||||
{"kind": "printer", "label": "Printer"},
|
||||
{"kind": "filament", "label": "Filament"},
|
||||
)
|
||||
KIND_KEYS = {item["kind"] for item in KINDS}
|
||||
|
||||
|
||||
def _editor(kind):
|
||||
"""The host.PresetEditor for a category (orca.host.preset.process / printer).
|
||||
|
||||
Filament is not reached through this helper: it is addressed by slot, so it calls
|
||||
orca.host.preset.filament.update(slot, key, value) directly.
|
||||
"""
|
||||
return getattr(orca.host.preset, kind)
|
||||
|
||||
|
||||
def _current_preset(bundle, kind):
|
||||
"""The single preset the matching settings tab edits right now, as a host.Preset.
|
||||
|
||||
Only the process and printer categories have one such preset; filament presets are
|
||||
addressed by slot instead (see _slot_presets).
|
||||
"""
|
||||
if kind == "process":
|
||||
return bundle.current_process_preset()
|
||||
return bundle.current_printer_preset()
|
||||
|
||||
|
||||
def _slot_presets(bundle):
|
||||
"""The filament preset mounted in each slot, in slot order; a slot may be None."""
|
||||
return list(bundle.current_filament_presets())
|
||||
|
||||
|
||||
def _preset_is_open(preset, edited):
|
||||
"""Whether this slot's preset is the one the filament settings already have open.
|
||||
|
||||
A slot with an assigned preset can be edited either way: applying to another slot makes
|
||||
the backend select that slot's preset first. This only distinguishes the slot that
|
||||
switches nothing from the ones that will switch the filament tab.
|
||||
"""
|
||||
return preset is not None and preset.name == edited.name
|
||||
|
||||
|
||||
def _filament_slots(presets, edited):
|
||||
"""One entry per mounted slot: its one-based number, preset name, and open flag.
|
||||
|
||||
The number is the slot label Orca shows; the name is None for a slot that mounts no
|
||||
preset, which is the only kind that cannot be edited.
|
||||
"""
|
||||
return [
|
||||
{
|
||||
"slot": index + 1,
|
||||
"name": None if preset is None else preset.name,
|
||||
"open": _preset_is_open(preset, edited),
|
||||
}
|
||||
for index, preset in enumerate(presets)
|
||||
]
|
||||
|
||||
|
||||
def _as_slot(value):
|
||||
"""A requested slot as an int, or None when it is not a whole number."""
|
||||
if isinstance(value, bool):
|
||||
return None
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
if isinstance(value, float) and value.is_integer():
|
||||
return int(value)
|
||||
return None
|
||||
|
||||
|
||||
def _clamp_slot(slot, count):
|
||||
"""The slot to display: the requested one, kept inside the available range."""
|
||||
if count <= 0:
|
||||
return 0
|
||||
number = _as_slot(slot)
|
||||
if number is None or number < 1:
|
||||
return 1
|
||||
return number if number <= count else count
|
||||
|
||||
|
||||
def _parse_slot(slot, count):
|
||||
"""Validate a requested filament slot against the mounted slots (one-based)."""
|
||||
if count == 0:
|
||||
raise RuntimeError("No filament slots are mounted")
|
||||
number = _as_slot(slot)
|
||||
if number is None:
|
||||
raise RuntimeError("A filament slot number is required")
|
||||
if number < 1 or number > count:
|
||||
raise RuntimeError("Filament slot %d is out of range (1-%d)" % (number, count))
|
||||
return number
|
||||
|
||||
|
||||
def _rows(preset):
|
||||
"""Every setting of the preset, sorted by key, as [{k, v}].
|
||||
|
||||
config_value() returns None for a key the preset does not carry; the page shows
|
||||
those as empty rather than "None".
|
||||
"""
|
||||
rows = []
|
||||
for key in sorted(preset.config_keys()):
|
||||
value = preset.config_value(key)
|
||||
rows.append({"k": key, "v": "" if value is None else str(value)})
|
||||
return rows
|
||||
|
||||
|
||||
def _state(bundle, kind, slot, message="", error=""):
|
||||
"""The payload the page renders: shown preset, rows, slots, result line."""
|
||||
payload = {"command": "state", "kind": kind, "message": message, "error": error}
|
||||
if kind == "filament":
|
||||
presets = _slot_presets(bundle)
|
||||
edited = bundle.filaments.edited_preset()
|
||||
effective = _clamp_slot(slot, len(presets))
|
||||
preset = None if effective == 0 else presets[effective - 1]
|
||||
payload["slots"] = _filament_slots(presets, edited)
|
||||
payload["slot"] = effective
|
||||
# An empty slot has nothing to edit; any assigned slot is editable, and one that is
|
||||
# not already open makes the backend select it first.
|
||||
payload["assigned"] = preset is not None
|
||||
payload["switches"] = preset is not None and not _preset_is_open(preset, edited)
|
||||
payload["edited_name"] = edited.name
|
||||
else:
|
||||
preset = _current_preset(bundle, kind)
|
||||
payload["preset"] = None if preset is None else {
|
||||
"name": preset.name,
|
||||
"is_dirty": bool(preset.is_dirty),
|
||||
"is_system": bool(preset.is_system),
|
||||
}
|
||||
payload["rows"] = [] if preset is None else _rows(preset)
|
||||
return payload
|
||||
|
||||
|
||||
def _apply(kind, key, value, slot=None):
|
||||
"""Set one setting of the preset the page is showing, through the host API.
|
||||
|
||||
A filament preset is addressed by slot number; process and printer each resolve
|
||||
their single edited preset themselves, so no slot is passed to them.
|
||||
"""
|
||||
if kind == "filament":
|
||||
orca.host.preset.filament.update(slot, key, value)
|
||||
return
|
||||
_editor(kind).update(key, value)
|
||||
|
||||
|
||||
PAGE = r"""<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
:root {
|
||||
--bg: var(--orca-bg, #fafafa);
|
||||
--fg: var(--orca-fg, #1b1e23);
|
||||
--muted: var(--orca-muted, #6b7280);
|
||||
--border: var(--orca-border, #dadee3);
|
||||
--accent: var(--orca-accent, #0e7c86);
|
||||
--accent-fg: var(--orca-accent-fg, #ffffff);
|
||||
--ui: var(--orca-font, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif);
|
||||
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
--surface: rgba(127, 127, 127, .09);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: var(--orca-bg, #23262a);
|
||||
--fg: var(--orca-fg, #e6e8ea);
|
||||
--muted: var(--orca-muted, #9aa1a9);
|
||||
--border: var(--orca-border, #383c42);
|
||||
}
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { height: 100%; }
|
||||
body {
|
||||
margin: 0; background: var(--bg); color: var(--fg);
|
||||
font: 13px/1.45 var(--ui);
|
||||
display: flex; flex-direction: column; overflow: hidden;
|
||||
}
|
||||
button { font: inherit; cursor: pointer; }
|
||||
:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
||||
|
||||
/* header ------------------------------------------------------------- */
|
||||
header {
|
||||
flex: none; display: flex; align-items: center; gap: 10px;
|
||||
padding: 9px 14px; border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.logo { color: var(--accent); font-size: 15px; line-height: 1; }
|
||||
h1 { margin: 0; font-size: 14px; font-weight: 650; letter-spacing: .01em; }
|
||||
|
||||
.seg {
|
||||
display: flex; gap: 2px; margin-left: auto; padding: 2px;
|
||||
border: 1px solid var(--border); border-radius: 9px; background: var(--surface);
|
||||
}
|
||||
.seg button {
|
||||
border: 0; background: transparent; color: var(--muted);
|
||||
padding: 4px 13px; border-radius: 7px;
|
||||
transition: background .15s ease, color .15s ease;
|
||||
}
|
||||
.seg button:hover { color: var(--fg); }
|
||||
.seg button[aria-pressed="true"] { background: var(--accent); color: var(--accent-fg); }
|
||||
|
||||
.iconbtn {
|
||||
width: 28px; height: 28px; border: 1px solid var(--border); border-radius: 7px;
|
||||
background: transparent; color: var(--fg); display: grid; place-items: center;
|
||||
font-size: 13px; transition: background .15s ease;
|
||||
}
|
||||
.iconbtn:hover:enabled { background: var(--surface); }
|
||||
.iconbtn:disabled { opacity: .5; }
|
||||
|
||||
/* preset bar --------------------------------------------------------- */
|
||||
.presetbar {
|
||||
flex: none; display: flex; align-items: center; gap: 8px;
|
||||
padding: 7px 14px; border-bottom: 1px solid var(--border); font-size: 12px;
|
||||
}
|
||||
.presetbar .name {
|
||||
font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
max-width: 60%;
|
||||
}
|
||||
.spacer { flex: 1; }
|
||||
.tag {
|
||||
border: 1px solid var(--border); color: var(--muted); border-radius: 999px;
|
||||
padding: 1px 8px; font-size: 11px; white-space: nowrap;
|
||||
}
|
||||
.tag.on { border-color: transparent; background: var(--accent); color: var(--accent-fg); }
|
||||
|
||||
/* filament slots ----------------------------------------------------- */
|
||||
.slotbar { flex: none; padding: 7px 14px 8px; border-bottom: 1px solid var(--border); }
|
||||
.slotrow { display: flex; align-items: center; gap: 9px; min-width: 0; }
|
||||
.slotlabel {
|
||||
flex: none; font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
.slots { flex: 1; min-width: 0; display: flex; flex-wrap: wrap; gap: 6px;
|
||||
max-height: 84px; overflow-y: auto; }
|
||||
.chip {
|
||||
display: inline-flex; align-items: center; gap: 6px; flex: none; max-width: 190px;
|
||||
padding: 2px 10px 2px 3px; border: 1px solid var(--border); border-radius: 999px;
|
||||
background: transparent; color: var(--fg);
|
||||
transition: background .15s ease, color .15s ease, border-color .15s ease;
|
||||
}
|
||||
.chip:hover { background: var(--surface); }
|
||||
.chip .n {
|
||||
flex: none; min-width: 19px; height: 19px; padding: 0 4px; border-radius: 999px;
|
||||
display: grid; place-items: center; background: var(--surface); color: var(--muted);
|
||||
font-size: 11px; font-variant-numeric: tabular-nums;
|
||||
}
|
||||
/* the slot whose preset the filament settings already have open, so applying switches nothing */
|
||||
.chip.open .n { box-shadow: inset 0 0 0 1px var(--accent); color: var(--fg); }
|
||||
.chip .s {
|
||||
min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
font-size: 11px; color: var(--muted);
|
||||
}
|
||||
.chip[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); }
|
||||
.chip[aria-pressed="true"] .n {
|
||||
background: rgba(255, 255, 255, .22); color: var(--accent-fg); box-shadow: none;
|
||||
}
|
||||
.chip[aria-pressed="true"] .s { color: var(--accent-fg); }
|
||||
/* a slot that mounts the preset the page is showing: editing either changes both */
|
||||
.chip.linked::after {
|
||||
content: ""; flex: none; width: 5px; height: 5px;
|
||||
border-radius: 50%; background: var(--accent);
|
||||
}
|
||||
.slotnote {
|
||||
margin-top: 7px; padding-left: 8px; border-left: 2px solid var(--accent);
|
||||
font-size: 11px; line-height: 1.5; color: var(--muted);
|
||||
}
|
||||
.slotnote:empty { display: none; }
|
||||
|
||||
/* panes -------------------------------------------------------------- */
|
||||
main { flex: 1; display: flex; min-height: 0; }
|
||||
|
||||
.list-pane {
|
||||
flex: none; width: 45%; min-width: 250px; min-height: 0;
|
||||
display: flex; flex-direction: column; border-right: 1px solid var(--border);
|
||||
}
|
||||
.search {
|
||||
flex: none; display: flex; align-items: center; gap: 8px;
|
||||
padding: 8px 10px; border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.search input {
|
||||
flex: 1; min-width: 0; font: inherit; color: var(--fg); background: var(--bg);
|
||||
border: 1px solid var(--border); border-radius: 7px; padding: 4px 9px;
|
||||
}
|
||||
.rows { flex: 1; min-height: 0; overflow-y: auto; padding: 3px 0; }
|
||||
|
||||
.row {
|
||||
display: flex; align-items: baseline; gap: 8px; width: 100%;
|
||||
padding: 5px 10px; text-align: left; border: 0; border-left: 3px solid transparent;
|
||||
background: transparent; color: var(--fg);
|
||||
}
|
||||
.row:hover { background: var(--surface); }
|
||||
.row[aria-selected="true"] { background: var(--surface); border-left-color: var(--accent); }
|
||||
.row .k {
|
||||
flex: 1; min-width: 0; font-family: var(--mono); font-size: 12px;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.row .v {
|
||||
flex: none; max-width: 40%; font-family: var(--mono); font-size: 11px;
|
||||
color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.row .dot {
|
||||
flex: none; align-self: center; width: 6px; height: 6px; border-radius: 50%;
|
||||
background: var(--accent); visibility: hidden;
|
||||
}
|
||||
.row.changed .dot { visibility: visible; }
|
||||
@keyframes flash {
|
||||
0% { box-shadow: inset 0 0 0 2px var(--accent); }
|
||||
100% { box-shadow: inset 0 0 0 2px transparent; }
|
||||
}
|
||||
.row.flash { animation: flash .7s ease-out; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.row.flash { animation: none; }
|
||||
}
|
||||
|
||||
.empty { color: var(--muted); padding: 14px 12px; }
|
||||
.nil { font-style: italic; opacity: .75; }
|
||||
|
||||
/* editor ------------------------------------------------------------- */
|
||||
.edit-pane {
|
||||
flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 9px;
|
||||
padding: 12px 14px; overflow-y: auto;
|
||||
}
|
||||
.edit-key { font-family: var(--mono); font-size: 13px; font-weight: 600; overflow-wrap: anywhere; }
|
||||
.lbl { font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
|
||||
.current {
|
||||
margin: 0; max-height: 108px; overflow: auto;
|
||||
font-family: var(--mono); font-size: 12px;
|
||||
background: var(--surface); border: 1px solid var(--border); border-radius: 7px;
|
||||
padding: 7px 10px; white-space: pre-wrap; overflow-wrap: anywhere;
|
||||
}
|
||||
textarea {
|
||||
width: 100%; min-height: 74px; resize: vertical;
|
||||
font-family: var(--mono); font-size: 12px; color: var(--fg); background: var(--bg);
|
||||
border: 1px solid var(--border); border-radius: 7px; padding: 7px 10px;
|
||||
}
|
||||
.actions { display: flex; gap: 8px; align-items: center; }
|
||||
button.primary {
|
||||
padding: 5px 14px; border-radius: 7px; border: 1px solid var(--accent);
|
||||
background: var(--accent); color: var(--accent-fg);
|
||||
}
|
||||
button.ghost {
|
||||
padding: 5px 12px; border-radius: 7px; border: 1px solid var(--border);
|
||||
background: transparent; color: var(--fg);
|
||||
}
|
||||
button.primary:disabled, button.ghost:disabled { opacity: .5; cursor: default; }
|
||||
button.primary:hover:enabled { filter: brightness(1.08); }
|
||||
button.ghost:hover:enabled { background: var(--surface); }
|
||||
|
||||
.notice {
|
||||
padding: 8px 11px; border: 1px solid var(--border); border-left: 3px solid var(--accent);
|
||||
border-radius: 7px; background: var(--surface);
|
||||
font-size: 12px; line-height: 1.5; color: var(--muted);
|
||||
}
|
||||
.notice b { color: var(--fg); font-weight: 600; }
|
||||
.notice:empty { display: none; }
|
||||
|
||||
.syntax {
|
||||
margin-top: 2px; padding: 9px 11px; border: 1px solid var(--border);
|
||||
border-left: 3px solid var(--accent); border-radius: 7px;
|
||||
font-size: 12px; line-height: 1.55; color: var(--muted);
|
||||
}
|
||||
.syntax b { color: var(--fg); font-weight: 600; }
|
||||
.syntax code { font-family: var(--mono); font-size: 11.5px; color: var(--fg); }
|
||||
|
||||
/* status ------------------------------------------------------------- */
|
||||
footer {
|
||||
flex: none; min-height: 30px; padding: 7px 14px;
|
||||
border-top: 1px solid var(--border); font-size: 12px; color: var(--muted);
|
||||
}
|
||||
footer.ok { color: #0f8a5f; }
|
||||
footer.err { color: #c8382c; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<span class="logo">◈</span>
|
||||
<h1>Preset update</h1>
|
||||
<div class="seg" id="seg"></div>
|
||||
<button class="iconbtn" id="refresh" title="Reload the current values">⟳</button>
|
||||
<button class="iconbtn" id="close" title="Close">✕</button>
|
||||
</header>
|
||||
|
||||
<div class="presetbar">
|
||||
<span class="name" id="pname">Loading…</span>
|
||||
<span id="ptag"></span>
|
||||
<span class="spacer"></span>
|
||||
<span class="tag" id="pcount">0 settings</span>
|
||||
</div>
|
||||
|
||||
<div class="slotbar" id="slotbar" hidden>
|
||||
<div class="slotrow">
|
||||
<span class="slotlabel">Filament slots</span>
|
||||
<div class="slots" id="slots"></div>
|
||||
</div>
|
||||
<div class="slotnote" id="slotnote"></div>
|
||||
</div>
|
||||
|
||||
<main>
|
||||
<section class="list-pane">
|
||||
<div class="search">
|
||||
<input id="q" type="search" placeholder="Search settings…" autocomplete="off" spellcheck="false">
|
||||
<span class="tag" id="qcount">0</span>
|
||||
</div>
|
||||
<div class="rows" id="rows"></div>
|
||||
</section>
|
||||
|
||||
<section class="edit-pane">
|
||||
<div class="edit-key" id="ekey">Select a setting</div>
|
||||
<div class="lbl">Current value</div>
|
||||
<pre class="current" id="ecur">—</pre>
|
||||
<label class="lbl" for="evalue">New value</label>
|
||||
<textarea id="evalue" spellcheck="false" autocomplete="off"
|
||||
placeholder="Serialized value" disabled></textarea>
|
||||
<div class="notice" id="notice"></div>
|
||||
<div class="actions">
|
||||
<button class="primary" id="apply" disabled>Apply</button>
|
||||
<button class="ghost" id="reset" disabled>Reset to current</button>
|
||||
</div>
|
||||
<div class="syntax">
|
||||
<b>Values use OrcaSlicer's serialized config syntax</b> — the same text a preset
|
||||
file stores. Numbers are plain (<code>0.2</code>, <code>3</code>) and percentages keep
|
||||
their sign (<code>15%</code>). On/off is <code>1</code> or <code>0</code>. A choice uses
|
||||
the setting's own key, e.g. <code>grid</code>. Lists are semicolon-separated
|
||||
(<code>0.4;0.4;0.4</code>, or <code>"#FF0000";"#00FF00"</code>). Text keeps its
|
||||
escapes, so a line break stays <code>\n</code>. A key that does not belong to this
|
||||
preset, or a value the setting would reject, comes back as an error and writes
|
||||
nothing. The edit lands in the preset the tab is editing and stays unsaved.
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer id="status"></footer>
|
||||
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
const SEG = [['process', 'Process'], ['printer', 'Printer'], ['filament', 'Filament']];
|
||||
const IDLE = 'Edits apply to the preset shown here and stay unsaved until you save '
|
||||
+ 'or export it.';
|
||||
|
||||
const S = {
|
||||
kind: 'process',
|
||||
slot: 1, // selected filament slot (one-based); ignored by the other kinds
|
||||
slots: [], // [{slot, name, open}] from current_filament_presets()
|
||||
preset: null,
|
||||
rows: [],
|
||||
selected: null,
|
||||
changed: {}, // "kind/preset/key" -> true, for edits made in this window
|
||||
pending: null, // {kind, slot, preset, key} of an apply awaiting its reply
|
||||
error: '', // the last error the plugin reported, if any
|
||||
assigned: true, // filament only: the shown slot mounts a preset, so it is editable
|
||||
switches: false, // filament only: applying will select that preset in the filament tab
|
||||
editedName: null, // filament only: the preset the filament settings have open
|
||||
busy: true,
|
||||
filter: '',
|
||||
statusText: '',
|
||||
statusCls: '',
|
||||
};
|
||||
|
||||
const $ = id => document.getElementById(id);
|
||||
const esc = s => String(s == null ? '' : s)
|
||||
.replace(/[&<>"']/g, c => ({'&': '&', '<': '<', '>': '>', '"': '"', "'": '''}[c]));
|
||||
|
||||
/* ------------------------------------------------------------------ paint */
|
||||
function paint() {
|
||||
paintSeg();
|
||||
const p = S.preset;
|
||||
const slotTag = S.kind === 'filament' && S.slot
|
||||
? '<span class="tag">slot ' + S.slot + '</span>' : '';
|
||||
$('pname').textContent = p ? p.name : (S.busy ? 'Loading…' : 'No preset');
|
||||
$('pname').title = p ? p.name : '';
|
||||
$('ptag').innerHTML = slotTag + (p
|
||||
? '<span class="tag">' + (p.is_system ? 'system' : 'user') + '</span>'
|
||||
+ (p.is_dirty ? '<span class="tag on">modified</span>' : '<span class="tag">saved</span>')
|
||||
: '');
|
||||
$('pcount').textContent = S.rows.length + (S.rows.length === 1 ? ' setting' : ' settings');
|
||||
$('refresh').disabled = S.busy;
|
||||
paintSlots();
|
||||
paintNotice();
|
||||
renderList();
|
||||
paintEditor();
|
||||
paintStatus();
|
||||
}
|
||||
|
||||
function paintSeg() {
|
||||
// Built once (see buildSeg) so a click never loses focus; only the state is repainted.
|
||||
document.querySelectorAll('#seg button').forEach(b =>
|
||||
b.setAttribute('aria-pressed', String(b.dataset.kind === S.kind)));
|
||||
}
|
||||
|
||||
/* the filament slot strip: one chip per mounted slot, shown for filament only */
|
||||
function paintSlots() {
|
||||
const show = S.kind === 'filament' && (S.slots.length > 0 || S.busy);
|
||||
$('slotbar').hidden = !show;
|
||||
if (!show) { $('slots').innerHTML = ''; $('slotnote').innerHTML = ''; return; }
|
||||
// Rebuilding the chips drops keyboard focus; put it back on the same slot's chip.
|
||||
const focused = document.activeElement && document.activeElement.dataset
|
||||
? document.activeElement.dataset.slot : null;
|
||||
$('slots').innerHTML = S.slots.map(s => {
|
||||
const selected = s.slot === S.slot;
|
||||
const linked = !selected && sharesSelected(s.name);
|
||||
const label = s.name == null ? '<span class="nil">empty slot</span>' : esc(s.name);
|
||||
const hint = s.name ? ': ' + s.name : ': no preset';
|
||||
return '<button type="button" class="chip' + (linked ? ' linked' : '')
|
||||
+ (s.open ? ' open' : '') + '"'
|
||||
+ ' data-slot="' + s.slot + '" aria-pressed="' + selected + '"'
|
||||
+ ' title="' + esc('Filament slot ' + s.slot + hint
|
||||
+ (s.open ? ' — open in the filament settings' : '')) + '">'
|
||||
+ '<span class="n">' + s.slot + '</span><span class="s">' + label + '</span></button>';
|
||||
}).join('');
|
||||
$('slotnote').innerHTML = shareNote();
|
||||
if (focused) {
|
||||
const chip = document.querySelector('#slots .chip[data-slot="' + focused + '"]');
|
||||
if (chip) chip.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function slotName(slot) {
|
||||
const found = S.slots.find(s => s.slot === slot);
|
||||
return found && found.name ? found.name : null;
|
||||
}
|
||||
|
||||
// Applying selects the slot's preset first, so say when that switches the filament tab. An
|
||||
// empty slot is the only unavailable one, and it says so instead of offering an edit.
|
||||
function paintNotice() {
|
||||
const el = $('notice');
|
||||
if (S.kind !== 'filament' || S.busy) { el.innerHTML = ''; return; }
|
||||
// With nothing mounted the list already says so; avoid naming a slot number here.
|
||||
if (!S.slots.length) { el.innerHTML = ''; return; }
|
||||
const name = S.preset ? S.preset.name : null;
|
||||
if (!name) {
|
||||
el.innerHTML = 'Slot ' + S.slot + ' holds no filament preset. Assign one in the '
|
||||
+ 'OrcaSlicer filament settings first.';
|
||||
return;
|
||||
}
|
||||
if (!S.switches) { el.innerHTML = ''; return; }
|
||||
const open = S.editedName
|
||||
? ', while the filament settings have <b>' + esc(S.editedName) + '</b> open'
|
||||
: '';
|
||||
el.innerHTML = 'Slot ' + S.slot + ' uses <b>' + esc(name) + '</b>' + open
|
||||
+ '. Applying switches the filament tab to <b>' + esc(name) + '</b> first — Orca may '
|
||||
+ 'ask about unsaved changes there — then sets the value.';
|
||||
}
|
||||
|
||||
function sharesSelected(name) {
|
||||
const current = slotName(S.slot);
|
||||
return !!name && !!current && name === current;
|
||||
}
|
||||
|
||||
function listSlots(numbers) {
|
||||
if (numbers.length === 1) return String(numbers[0]);
|
||||
return numbers.slice(0, -1).join(', ') + ' and ' + numbers[numbers.length - 1];
|
||||
}
|
||||
|
||||
// Slots mounting the same preset are one preset, so say which slots an edit reaches.
|
||||
function shareNote() {
|
||||
const name = slotName(S.slot);
|
||||
if (!name) return '';
|
||||
const sharing = S.slots.filter(s => s.name === name).map(s => s.slot);
|
||||
if (sharing.length < 2) return '';
|
||||
return 'Slots ' + listSlots(sharing) + ' mount the same filament preset, so an edit '
|
||||
+ 'here changes every one of them.';
|
||||
}
|
||||
|
||||
function changedKey(presetName, key) {
|
||||
return S.kind + '/' + (presetName || '?') + '/' + key;
|
||||
}
|
||||
|
||||
function paintStatus() {
|
||||
const el = $('status');
|
||||
if (S.statusText) {
|
||||
el.textContent = S.statusText;
|
||||
el.className = S.statusCls;
|
||||
} else if (S.busy) {
|
||||
el.textContent = 'Working…';
|
||||
el.className = '';
|
||||
} else {
|
||||
el.textContent = IDLE;
|
||||
el.className = '';
|
||||
}
|
||||
}
|
||||
|
||||
function setStatus(text, cls) {
|
||||
S.statusText = text || '';
|
||||
S.statusCls = cls || '';
|
||||
paintStatus();
|
||||
}
|
||||
|
||||
function visibleRows() {
|
||||
const q = S.filter.trim().toLowerCase();
|
||||
if (!q) return S.rows;
|
||||
return S.rows.filter(r => r.k.toLowerCase().includes(q) || r.v.toLowerCase().includes(q));
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
const scroller = $('rows');
|
||||
const top = scroller.scrollTop;
|
||||
$('qcount').textContent = String(visibleRows().length);
|
||||
|
||||
if (!S.rows.length) {
|
||||
let text = 'This preset reports no settings.';
|
||||
if (S.busy) text = 'Loading settings…';
|
||||
else if (S.error) text = 'These values could not be read — see the message below.';
|
||||
else if (S.kind === 'filament' && !S.slots.length) text = 'No filament slots are mounted.';
|
||||
else if (S.kind === 'filament' && !S.preset) text = 'This filament slot has no preset.';
|
||||
scroller.innerHTML = '<div class="empty">' + text + '</div>';
|
||||
return;
|
||||
}
|
||||
const rows = visibleRows();
|
||||
if (!rows.length) {
|
||||
scroller.innerHTML = '<div class="empty">No setting matches “' + esc(S.filter) + '”.</div>';
|
||||
return;
|
||||
}
|
||||
scroller.innerHTML = rows.map(r => {
|
||||
const value = r.v === '' ? '<span class="nil">empty</span>' : esc(r.v);
|
||||
return '<button type="button" class="row'
|
||||
+ (S.changed[changedKey(S.preset && S.preset.name, r.k)] ? ' changed' : '') + '"'
|
||||
+ ' data-key="' + esc(r.k) + '"'
|
||||
+ ' aria-selected="' + (r.k === S.selected) + '"'
|
||||
+ ' title="' + esc(r.k + ' = ' + r.v) + '">'
|
||||
+ '<span class="k">' + esc(r.k) + '</span>'
|
||||
+ '<span class="v">' + value + '</span>'
|
||||
+ '<span class="dot"></span></button>';
|
||||
}).join('');
|
||||
scroller.scrollTop = top;
|
||||
}
|
||||
|
||||
function paintEditor() {
|
||||
const row = S.rows.find(r => r.k === S.selected) || null;
|
||||
// Every assigned slot is editable now that applying selects the slot's preset first, so
|
||||
// Apply is off only for an empty slot, a missing selection, or a request in flight.
|
||||
const selected = !!row && !S.busy;
|
||||
$('ekey').textContent = row ? row.k : 'Select a setting';
|
||||
$('ecur').textContent = row ? (row.v === '' ? '(empty)' : row.v) : '—';
|
||||
$('evalue').disabled = !selected;
|
||||
$('apply').disabled = !selected || !S.assigned;
|
||||
$('reset').disabled = !selected;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------- actions */
|
||||
function buildSeg() {
|
||||
$('seg').innerHTML = SEG.map(([kind, label]) =>
|
||||
'<button type="button" data-kind="' + kind + '">' + label + '</button>').join('');
|
||||
}
|
||||
|
||||
function selectKey(key) {
|
||||
S.selected = key;
|
||||
const row = S.rows.find(r => r.k === key);
|
||||
$('evalue').value = row ? row.v : '';
|
||||
// Repaint the rows in place so the scroll position of a long list is kept.
|
||||
document.querySelectorAll('#rows .row').forEach(el =>
|
||||
el.setAttribute('aria-selected', String(el.dataset.key === key)));
|
||||
paintEditor();
|
||||
}
|
||||
|
||||
function switchKind(kind) {
|
||||
S.kind = kind;
|
||||
S.selected = null;
|
||||
S.pending = null;
|
||||
S.preset = null;
|
||||
S.rows = [];
|
||||
S.busy = true;
|
||||
$('evalue').value = '';
|
||||
setStatus('', '');
|
||||
paint();
|
||||
orca.postMessage({command: 'state', kind: kind, slot: S.slot});
|
||||
}
|
||||
|
||||
// Two slots that mount the same preset show the same settings, so only a switch to a
|
||||
// different preset drops the current selection and the value being typed.
|
||||
function selectSlot(slot) {
|
||||
if (S.busy || slot === S.slot) return;
|
||||
const same = !!slotName(slot) && slotName(slot) === slotName(S.slot);
|
||||
S.slot = slot;
|
||||
S.pending = null;
|
||||
S.busy = true;
|
||||
if (!same) {
|
||||
S.preset = null;
|
||||
S.rows = [];
|
||||
S.selected = null;
|
||||
$('evalue').value = '';
|
||||
}
|
||||
setStatus('', '');
|
||||
paint();
|
||||
orca.postMessage({command: 'state', kind: S.kind, slot: slot});
|
||||
}
|
||||
|
||||
function reload() {
|
||||
if (S.busy) return;
|
||||
S.busy = true;
|
||||
S.pending = null;
|
||||
setStatus('', '');
|
||||
paint();
|
||||
orca.postMessage({command: 'state', kind: S.kind, slot: S.slot});
|
||||
}
|
||||
|
||||
function applyValue() {
|
||||
const row = S.rows.find(r => r.k === S.selected);
|
||||
// Mirror the Apply button: only a missing selection, a request in flight, or an empty
|
||||
// slot blocks an edit, so Enter cannot send one the button itself would refuse.
|
||||
if (!row || S.busy || !S.assigned) return;
|
||||
S.pending = {
|
||||
kind: S.kind, slot: S.slot,
|
||||
preset: S.preset ? S.preset.name : null, key: row.k,
|
||||
};
|
||||
S.busy = true;
|
||||
setStatus('', '');
|
||||
paint();
|
||||
orca.postMessage({command: 'apply', kind: S.kind, slot: S.slot, key: row.k,
|
||||
value: $('evalue').value});
|
||||
}
|
||||
|
||||
function flashKey(key) {
|
||||
document.querySelectorAll('#rows .row').forEach(el => {
|
||||
if (el.dataset.key !== key) return;
|
||||
el.classList.remove('flash');
|
||||
void el.offsetWidth; // restart the animation if it ran on this row already
|
||||
el.classList.add('flash');
|
||||
el.addEventListener('animationend', () => el.classList.remove('flash'), {once: true});
|
||||
});
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------- wiring */
|
||||
buildSeg();
|
||||
|
||||
$('seg').addEventListener('click', e => {
|
||||
const button = e.target.closest('[data-kind]');
|
||||
if (!button || S.busy || button.dataset.kind === S.kind) return;
|
||||
switchKind(button.dataset.kind);
|
||||
});
|
||||
|
||||
$('rows').addEventListener('click', e => {
|
||||
const row = e.target.closest('.row');
|
||||
if (row && !S.busy) selectKey(row.dataset.key);
|
||||
});
|
||||
|
||||
$('slots').addEventListener('click', e => {
|
||||
const chip = e.target.closest('[data-slot]');
|
||||
if (chip && !S.busy) selectSlot(Number(chip.dataset.slot));
|
||||
});
|
||||
|
||||
$('q').addEventListener('input', e => {
|
||||
S.filter = e.target.value;
|
||||
renderList();
|
||||
});
|
||||
|
||||
$('apply').addEventListener('click', applyValue);
|
||||
|
||||
$('reset').addEventListener('click', () => {
|
||||
const row = S.rows.find(r => r.k === S.selected);
|
||||
if (row) $('evalue').value = row.v;
|
||||
});
|
||||
|
||||
$('evalue').addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); applyValue(); }
|
||||
else if (e.key === 'Escape') { e.preventDefault(); $('reset').click(); }
|
||||
});
|
||||
|
||||
$('refresh').addEventListener('click', reload);
|
||||
$('close').addEventListener('click', () => orca.close());
|
||||
|
||||
/* -------------------------------------------------------------- messages */
|
||||
orca.onMessage(msg => {
|
||||
if (!msg || msg.command !== 'state') return;
|
||||
if (msg.kind !== S.kind) return; // a reply for a category we already left
|
||||
|
||||
const pending = S.pending;
|
||||
S.busy = false;
|
||||
S.pending = null;
|
||||
S.slots = msg.slots || [];
|
||||
// Adopt the slot the plugin actually used (it clamps a slot that no longer exists).
|
||||
if (S.kind === 'filament' && typeof msg.slot === 'number') S.slot = msg.slot;
|
||||
S.preset = msg.preset || null;
|
||||
S.rows = msg.rows || [];
|
||||
S.error = msg.error || '';
|
||||
// Filament editability is a slot concern; process and printer have a single edited preset.
|
||||
S.assigned = S.kind === 'filament' ? !!msg.assigned : true;
|
||||
S.switches = S.kind === 'filament' ? !!msg.switches : false;
|
||||
S.editedName = S.kind === 'filament' ? (msg.edited_name || null) : null;
|
||||
if (S.selected && !S.rows.some(r => r.k === S.selected)) S.selected = null;
|
||||
|
||||
if (msg.error) {
|
||||
// Keep the typed value so it can be corrected, and keep the current selection.
|
||||
setStatus('⚠ ' + msg.error, 'err');
|
||||
} else {
|
||||
if (pending) S.changed[changedKey(pending.preset, pending.key)] = true;
|
||||
setStatus(msg.message || '', msg.message ? 'ok' : '');
|
||||
}
|
||||
|
||||
paint();
|
||||
|
||||
if (!msg.error && pending) {
|
||||
const row = S.rows.find(r => r.k === pending.key);
|
||||
if (row) { // show the value as the preset now stores it
|
||||
$('evalue').value = row.v;
|
||||
flashKey(row.k);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
paint();
|
||||
orca.postMessage({command: 'state', kind: S.kind, slot: S.slot});
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
class PresetUpdateDemo(orca.script.ScriptPluginCapabilityBase):
|
||||
window = None
|
||||
kind = "process" # the category the open window is showing, for a re-run refresh
|
||||
slot = 1 # the filament slot the open window is showing
|
||||
|
||||
def get_name(self):
|
||||
return "Preset update demo"
|
||||
|
||||
def execute(self):
|
||||
try:
|
||||
if self.window is not None and self.window.is_open():
|
||||
self._post_state(self.kind, self.slot)
|
||||
return orca.ExecutionResult.success("Preset update demo refreshed.")
|
||||
self.window = orca.host.ui.create_window(
|
||||
title="Preset update demo", html=PAGE, width=820, height=560,
|
||||
on_message=self.on_message, on_close=self.on_close,
|
||||
)
|
||||
return orca.ExecutionResult.success("Preset update demo opened.")
|
||||
except Exception as error:
|
||||
message = "Preset update demo failed: %s" % error
|
||||
orca.host.ui.message(message, title="Preset update demo", buttons="ok", icon="error")
|
||||
return orca.ExecutionResult.failure(orca.PluginResult.RecoverableError, message)
|
||||
|
||||
def on_close(self):
|
||||
self.window = None
|
||||
|
||||
def _post_state(self, kind="process", slot=1, message="", error=""):
|
||||
try:
|
||||
payload = _state(orca.host.preset_bundle(), kind, slot, message, error)
|
||||
except Exception as state_error:
|
||||
payload = {"command": "state", "kind": kind, "slot": slot, "slots": [],
|
||||
"assigned": False, "switches": False, "edited_name": None,
|
||||
"preset": None, "rows": [], "message": message,
|
||||
"error": str(state_error)}
|
||||
if self.window is not None and self.window.is_open():
|
||||
self.window.post(payload)
|
||||
|
||||
def on_message(self, message):
|
||||
kind = "process"
|
||||
slot = 1
|
||||
try:
|
||||
message = message or {}
|
||||
kind = message.get("kind", "process")
|
||||
if not isinstance(kind, str) or kind not in KIND_KEYS:
|
||||
raise RuntimeError("Unknown preset category %r" % (kind,))
|
||||
self.kind = kind
|
||||
slot = message.get("slot", 1)
|
||||
|
||||
command = message.get("command")
|
||||
if command == "state":
|
||||
self.slot = slot
|
||||
self._post_state(kind, slot)
|
||||
return
|
||||
if command == "apply":
|
||||
key = message.get("key")
|
||||
value = message.get("value")
|
||||
if not isinstance(key, str) or not key:
|
||||
raise RuntimeError("A setting key is required")
|
||||
if not isinstance(value, str):
|
||||
raise RuntimeError("A value string is required")
|
||||
where = ""
|
||||
if kind == "filament":
|
||||
presets = _slot_presets(orca.host.preset_bundle())
|
||||
# Validate the slot strictly: an out-of-range slot must fail loudly
|
||||
# rather than edit a different filament. A slot with no preset has
|
||||
# nothing to write, which is the only slot shape the backend refuses.
|
||||
slot = _parse_slot(slot, len(presets))
|
||||
if presets[slot - 1] is None:
|
||||
raise RuntimeError("Filament slot %d has no preset to edit" % slot)
|
||||
_apply(kind, key, value, slot)
|
||||
where = " on slot %d" % slot
|
||||
else:
|
||||
_apply(kind, key, value)
|
||||
shown = value if value else "(empty)"
|
||||
self.slot = slot
|
||||
self._post_state(
|
||||
kind, slot,
|
||||
message='Set %s = "%s"%s. The edit stays in the preset tab until you save it.'
|
||||
% (key, shown, where),
|
||||
)
|
||||
return
|
||||
raise RuntimeError("Unknown command %r" % (command,))
|
||||
except Exception as error:
|
||||
safe_kind = kind if isinstance(kind, str) and kind in KIND_KEYS else "process"
|
||||
self._post_state(safe_kind, slot, error=str(error))
|
||||
|
||||
|
||||
@orca.plugin
|
||||
class PresetUpdateDemoPackage(orca.base):
|
||||
def register_capabilities(self):
|
||||
orca.register_capability(PresetUpdateDemo)
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "TextureDisplacement.hpp"
|
||||
|
||||
#include <Eigen/Core>
|
||||
#include <Eigen/SVD>
|
||||
#include <Eigen/Geometry>
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -722,7 +723,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;
|
||||
@@ -2164,7 +2168,7 @@ 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;
|
||||
|
||||
// `normalized_uv` drops the two steps that only mean something for a coordinate measured on the
|
||||
@@ -2172,7 +2176,18 @@ struct UVTransform
|
||||
// rotation and offset. See apply_uv_transform(); TextureProjectionMethod::ModelUV is what needs it.
|
||||
UVTransform(const TextureDisplacementLayer &layer, float aspect_, bool normalized_uv = false)
|
||||
{
|
||||
scale = (normalized_uv || layer.tiling_scale <= 1e-6f) ? 1.f : (1.f / layer.tiling_scale);
|
||||
// The mirror folds into the reciprocal: it is a negation of the same coordinate, so there is
|
||||
// nothing to do per sample beyond carrying the sign here. A normalized uv has no millimetre
|
||||
// tiling to divide by, but a mirror is still a mirror, so the sign applies either way.
|
||||
const Vec2f sign = layer_mirror_sign(layer);
|
||||
if (normalized_uv) {
|
||||
inv_u = sign.x();
|
||||
inv_v = sign.y();
|
||||
} else {
|
||||
const Vec2f tile = layer_tile_mm(layer);
|
||||
inv_u = sign.x() / tile.x();
|
||||
inv_v = sign.y() / tile.y();
|
||||
}
|
||||
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
|
||||
cs = std::cos(rad);
|
||||
sn = std::sin(rad);
|
||||
@@ -2181,9 +2196,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;
|
||||
@@ -2203,26 +2220,43 @@ inline Vec3f triplanar_weights(const Vec3f &normal)
|
||||
|
||||
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect, bool normalized_uv)
|
||||
{
|
||||
// A uv that is already normalized has no millimetre scale to divide by and no aspect to correct -
|
||||
// see the header. UVTransform does the same, and the two must stay in step.
|
||||
const float scale = (normalized_uv || layer.tiling_scale <= 1e-6f) ? 1.f : (1.f / layer.tiling_scale);
|
||||
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.
|
||||
//
|
||||
// The mirror rides along as a sign on the same division. It is in the tile's own frame too, so
|
||||
// it mirrors along the tile's axes rather than the world's, and because the offset is what
|
||||
// positions the tile, mirroring about the tile's middle is a matter of the offset the gizmo
|
||||
// writes alongside it, not of anything here.
|
||||
// A uv that is already normalized has no millimetre scale to divide by - see the header - but it
|
||||
// can still be mirrored, so the sign is applied in both cases and only the extents are skipped.
|
||||
// UVTransform does the same, and the two must stay in step.
|
||||
const Vec2f sign = layer_mirror_sign(layer);
|
||||
if (normalized_uv) {
|
||||
rotated.x() *= sign.x();
|
||||
rotated.y() *= sign.y();
|
||||
} else {
|
||||
const Vec2f tile = layer_tile_mm(layer);
|
||||
rotated.x() *= sign.x() / tile.x();
|
||||
rotated.y() *= sign.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.
|
||||
// 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.
|
||||
//
|
||||
// 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.
|
||||
// Applied after the rotation, like the extents, and for the same reason: scaling one axis of an
|
||||
// already-rotated coordinate before turning it would be a shear.
|
||||
if (!normalized_uv && aspect > 0.f && aspect != 1.f)
|
||||
rotated.y() *= aspect;
|
||||
|
||||
@@ -3566,13 +3600,36 @@ void texture_displacement_patch_frame(const indexed_triangle_set &patch, const s
|
||||
(an.y() <= an.x() && an.y() <= an.z()) ? Vec3f::UnitY() : Vec3f::UnitZ();
|
||||
}
|
||||
|
||||
Matrix3d texture_displacement_frame_rotation(const Transform3d &volume_to_world)
|
||||
{
|
||||
// Separated out by polar decomposition rather than by assuming the linear part is a clean
|
||||
// rotation times a scale: a sheared or mirrored placement is legal here. A reflection is left
|
||||
// behind with the scale on purpose, so a mirrored instance still makes the bake frame's
|
||||
// determinant negative and the bake still knows to flip the normals it derives.
|
||||
const Matrix3d linear = volume_to_world.linear();
|
||||
Eigen::JacobiSVD<Matrix3d> svd(linear, Eigen::ComputeFullU | Eigen::ComputeFullV);
|
||||
Matrix3d rotation = svd.matrixU() * svd.matrixV().transpose();
|
||||
if (rotation.determinant() < 0.0) {
|
||||
Matrix3d u = svd.matrixU();
|
||||
u.col(2) *= -1.0;
|
||||
rotation = u * svd.matrixV().transpose();
|
||||
}
|
||||
return rotation.transpose(); // world -> texture frame
|
||||
}
|
||||
|
||||
Transform3d texture_displacement_bake_frame(const Transform3d &volume_to_world)
|
||||
{
|
||||
// World orientation and scale, but the origin moved to where the volume's own origin sits: the
|
||||
// texture then rides with the model when it is moved about the plate (and a single, untiled stamp
|
||||
// starts on the model rather than at the plate's corner), while a tile is still `tiling_scale`
|
||||
// printed millimetres whatever the instance's scale.
|
||||
return Eigen::Translation3d(-volume_to_world.translation()) * volume_to_world;
|
||||
// The model's own orientation, world scale, and the origin moved to where the volume's origin
|
||||
// sits. So the texture rides with the model however it is moved *and* however it is turned,
|
||||
// while a tile is still `tiling_scale` printed millimetres whatever the instance's scale, and a
|
||||
// single untiled stamp starts on the model rather than at the plate's corner.
|
||||
//
|
||||
// Orientation used to come from the world too, which meant turning a part on the plate slid the
|
||||
// decoration across its surface. Turning a part is a print-time decision - clearance, supports,
|
||||
// warping - and it should not be a design one.
|
||||
Transform3d unrotate = Transform3d::Identity();
|
||||
unrotate.linear() = texture_displacement_frame_rotation(volume_to_world);
|
||||
return unrotate * Eigen::Translation3d(-volume_to_world.translation()) * volume_to_world;
|
||||
}
|
||||
|
||||
Transform3d texture_displacement_volume_to_world(const ModelVolume &volume)
|
||||
@@ -5783,7 +5840,13 @@ std::string texture_displacement_layers_to_json(const std::vector<TextureDisplac
|
||||
|
||||
j["depth_mm"] = l.depth_mm;
|
||||
j["tiling_scale"] = l.tiling_scale;
|
||||
j["rotation_deg"] = l.rotation_deg;
|
||||
// The v extent and the mirroring. Written unconditionally, including the <= 0 "same as u"
|
||||
// sentinel, so a project says plainly that the tile was square rather than leaving a reader
|
||||
// to infer it from a missing key.
|
||||
j["tiling_scale_v"] = l.tiling_scale_v;
|
||||
j["mirror_u"] = l.mirror_u;
|
||||
j["mirror_v"] = l.mirror_v;
|
||||
j["rotation_deg"] = l.rotation_deg;
|
||||
j["offset"] = vec2_to_json(l.offset);
|
||||
j["invert"] = l.invert;
|
||||
j["midlevel"] = l.midlevel;
|
||||
@@ -5891,6 +5954,12 @@ bool texture_displacement_layers_from_json(const std::string
|
||||
|
||||
read(j, "depth_mm", l.depth_mm);
|
||||
read(j, "tiling_scale", l.tiling_scale);
|
||||
// Absent in a project written before the tile could be rectangular or mirrored, and
|
||||
// then each keeps its default: -1 for the v extent, which layer_tile_mm() reads as
|
||||
// "square", and false for the mirrors. So an older project loads as what it was.
|
||||
read(j, "tiling_scale_v", l.tiling_scale_v);
|
||||
read(j, "mirror_u", l.mirror_u);
|
||||
read(j, "mirror_v", l.mirror_v);
|
||||
read(j, "rotation_deg", l.rotation_deg);
|
||||
if (const auto o = j.find("offset"); o != j.end())
|
||||
l.offset = vec2_from_json(*o, l.offset);
|
||||
|
||||
@@ -225,7 +225,17 @@ struct TextureDisplacementLayer
|
||||
std::shared_ptr<std::vector<unsigned char>> image_data;
|
||||
|
||||
float depth_mm = 0.4f; // maximum displacement along the surface normal, in mm
|
||||
float tiling_scale = 12.5f; // size of one texture tile, in mm
|
||||
float tiling_scale = 12.5f; // size of one texture tile along u, in mm
|
||||
// Size of one tile along v, in mm. Anything <= 0 means "whatever tiling_scale is", which is both
|
||||
// the default and what a layer created before the tile could be non-square meant - so an older
|
||||
// undo snapshot still comes back as the square tile it was.
|
||||
float tiling_scale_v = -1.f;
|
||||
// Mirror the texture along one axis of the tile. Set by dragging a width or height handle back
|
||||
// through the tile's middle, which is how a side handle behaves in an image editor: the extent
|
||||
// cannot go negative, so the sign lives here instead. Both default to off, which is what every
|
||||
// layer written before this meant.
|
||||
bool mirror_u = false;
|
||||
bool mirror_v = false;
|
||||
float rotation_deg = 0.f;
|
||||
Vec2f offset = Vec2f::Zero();
|
||||
bool invert = false;
|
||||
@@ -362,7 +372,8 @@ struct TextureDisplacementLayer
|
||||
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, tiling_scale_v,
|
||||
mirror_u, mirror_v);
|
||||
}
|
||||
template<class Archive> void load(Archive &ar)
|
||||
{
|
||||
@@ -374,7 +385,7 @@ struct TextureDisplacementLayer
|
||||
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, tiling_scale_v, mirror_u, mirror_v);
|
||||
image_data = blob.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(blob.begin(), blob.end());
|
||||
tile_method = static_cast<TextureTileMethod>(tile_method_int);
|
||||
projection_method = static_cast<TextureProjectionMethod>(projection_method_int);
|
||||
@@ -383,6 +394,25 @@ struct TextureDisplacementLayer
|
||||
};
|
||||
|
||||
|
||||
// The layer's tile extents in mm, u first. The v extent falls back to the u one whenever
|
||||
// tiling_scale_v is unset, so "unset means square" is written down once and every consumer - the
|
||||
// bake, the preview shaders, the UV pane and the placement gizmo - agrees on what a layer with no
|
||||
// v extent means.
|
||||
inline Vec2f layer_tile_mm(const TextureDisplacementLayer &layer)
|
||||
{
|
||||
const float su = (layer.tiling_scale > 1e-6f) ? layer.tiling_scale : 1.f;
|
||||
return Vec2f(su, (layer.tiling_scale_v > 1e-6f) ? layer.tiling_scale_v : su);
|
||||
}
|
||||
|
||||
// +1 or -1 per axis, for the layer's mirroring. A separate factor from the extents rather than a
|
||||
// signed extent: the extents are a size the panel shows and the refinement measures texels against,
|
||||
// and letting one of them go negative would mean every one of those places had to remember to take
|
||||
// the magnitude.
|
||||
inline Vec2f layer_mirror_sign(const TextureDisplacementLayer &layer)
|
||||
{
|
||||
return Vec2f(layer.mirror_u ? -1.f : 1.f, layer.mirror_v ? -1.f : 1.f);
|
||||
}
|
||||
|
||||
// Settings that apply to the whole layer stack rather than to one layer, held per ModelVolume next
|
||||
// to texture_displacement_layers and consumed by build_texture_displacement().
|
||||
struct TextureDisplacementOptions
|
||||
@@ -668,14 +698,19 @@ Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
|
||||
// projection method, without going through project_texture_displacement_uv()'s own dispatch
|
||||
// (which only knows how to compute the *analytic* methods from a single vertex + normal).
|
||||
// `aspect` is the height map's width / height. It scales the v axis so a non-square image is not
|
||||
// squeezed into a square tile: `tiling_scale` is the tile's size along u, and the tile is
|
||||
// `tiling_scale * height / width` mm along v, which keeps texels square. 1 (the default) is the
|
||||
// square case and leaves the coordinate exactly as it always was.
|
||||
// squeezed into a square tile: the tile is `layer_tile_mm().y() * height / width` mm along v, which
|
||||
// keeps texels square. 1 (the default) is the square case and leaves the coordinate exactly as it
|
||||
// always was.
|
||||
//
|
||||
// The tile extents divide *after* the rotation, not before. With one extent that made no difference
|
||||
// (R is linear, so R(p / s) == R(p) / s), but with two it is what keeps a non-square tile a rotated
|
||||
// rectangle on the surface instead of a sheared parallelogram. The layer's mirroring is a sign on
|
||||
// that same division.
|
||||
//
|
||||
// `normalized_uv` says the coordinate is already a uv rather than a millimetre measurement on the
|
||||
// model - which is what TextureProjectionMethod::ModelUV hands in. Then neither the tiling scale (mm
|
||||
// model - which is what TextureProjectionMethod::ModelUV hands in. Then neither the tile extents (mm
|
||||
// per tile is meaningless for it) nor the aspect correction (the artist's layout already accounts for
|
||||
// the image's proportions) applies, and only the rotation and offset are left.
|
||||
// the image's proportions) applies, and only the rotation, the mirroring and the offset are left.
|
||||
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect = 1.f,
|
||||
bool normalized_uv = false);
|
||||
|
||||
@@ -1047,6 +1082,10 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
|
||||
Transform3d texture_displacement_volume_to_world(const ModelVolume &volume);
|
||||
// The frame the bake and the previews project the texture in: `volume_to_world` with its translation
|
||||
// removed, i.e. world orientation and scale about the volume's own origin. See build_texture_displacement().
|
||||
// The rotation that carries a world direction into the texture's frame, i.e. the inverse of the
|
||||
// placement's own rotation. Exposed because the preview shaders rebuild the frame themselves from a
|
||||
// world position and have to turn it the same way the bake does, or the two disagree.
|
||||
Matrix3d texture_displacement_frame_rotation(const Transform3d &volume_to_world);
|
||||
Transform3d texture_displacement_bake_frame(const Transform3d &volume_to_world);
|
||||
|
||||
// Area-weighted vertex normals of `its` - the directions the bake both projects and displaces along.
|
||||
|
||||
@@ -85,96 +85,6 @@ static std::string float_to_string_with_precision(float value, int precision = 3
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
static std::string colour_to_ams_string(const wxColour& color)
|
||||
{
|
||||
char col_buf[10];
|
||||
sprintf(col_buf, "%02X%02X%02X%02X", (int)color.Red(), (int)color.Green(), (int)color.Blue(), (int)color.Alpha());
|
||||
return col_buf;
|
||||
}
|
||||
|
||||
static wxColour mix_colour(const wxColour& left, const wxColour& right, double ratio)
|
||||
{
|
||||
ratio = std::max(0.0, std::min(1.0, ratio));
|
||||
auto mix_channel = [ratio](unsigned char l, unsigned char r) {
|
||||
return static_cast<unsigned char>(std::round(l + (r - l) * ratio));
|
||||
};
|
||||
return wxColour(mix_channel(left.Red(), right.Red()),
|
||||
mix_channel(left.Green(), right.Green()),
|
||||
mix_channel(left.Blue(), right.Blue()),
|
||||
mix_channel(left.Alpha(), right.Alpha()));
|
||||
}
|
||||
|
||||
static std::vector<ColorPickerPopup::ColorItem> collect_ams_color_items(DevFilaSystem* fila_system)
|
||||
{
|
||||
std::vector<ColorPickerPopup::ColorItem> items;
|
||||
if (!fila_system) return items;
|
||||
|
||||
for (const auto& ams_pair : fila_system->GetAmsList()) {
|
||||
DevAms* ams = ams_pair.second;
|
||||
if (!ams) continue;
|
||||
|
||||
for (const auto& tray_pair : ams->GetTrays()) {
|
||||
DevAmsTray* tray = tray_pair.second;
|
||||
if (!tray || !tray->is_tray_info_ready()) continue;
|
||||
|
||||
ColorPickerPopup::ColorItem item;
|
||||
for (const std::string& col : tray->cols) {
|
||||
item.colors.emplace_back(DevAmsTray::decode_color(col));
|
||||
}
|
||||
if (item.colors.empty() && !tray->color.empty()) {
|
||||
item.colors.emplace_back(DevAmsTray::decode_color(tray->color));
|
||||
}
|
||||
if (item.colors.empty()) continue;
|
||||
|
||||
item.ctype = item.colors.size() > 1 ? tray->ctype : 2;
|
||||
items.emplace_back(std::move(item));
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
static std::vector<ColorPickerPopup::ColorItem> collect_preset_color_items(const std::string& filament_id)
|
||||
{
|
||||
std::vector<ColorPickerPopup::ColorItem> items;
|
||||
if (filament_id.empty()) return items;
|
||||
|
||||
auto* clr_query = GUI::wxGetApp().get_filament_color_code_query();
|
||||
if (!clr_query) return items;
|
||||
|
||||
auto* agent = wxGetApp().getAgent();
|
||||
const std::string catalog_id = agent ? agent->from_orca_filament_id(filament_id) : filament_id;
|
||||
FilamentColorCodes* color_codes = clr_query->GetFilaInfoMap(wxString::FromUTF8(catalog_id));
|
||||
if (!color_codes || !color_codes->GetFilamentColor2CodeMap()) return items;
|
||||
|
||||
std::set<std::string> seen;
|
||||
for (const auto& color_pair : *color_codes->GetFilamentColor2CodeMap()) {
|
||||
const FilamentColor& fila_color = color_pair.first;
|
||||
FilamentColorCode* color_code = color_pair.second;
|
||||
if (!color_code || fila_color.GetColors().empty()) continue;
|
||||
|
||||
ColorPickerPopup::ColorItem item;
|
||||
item.ctype = fila_color.ColorCount() < 2 ? 2 :
|
||||
(fila_color.m_color_type == FilamentColor::ColorType::GRADIENT_CLR ? 0 : 1);
|
||||
item.name = color_code->GetFilaColorName();
|
||||
|
||||
std::string key = std::to_string(item.ctype);
|
||||
for (const wxColour& color : fila_color.GetColors()) {
|
||||
item.colors.emplace_back(color);
|
||||
char rgba[9];
|
||||
std::snprintf(rgba, sizeof(rgba), "%02X%02X%02X%02X",
|
||||
static_cast<unsigned>(color.Red()), static_cast<unsigned>(color.Green()),
|
||||
static_cast<unsigned>(color.Blue()), static_cast<unsigned>(color.Alpha()));
|
||||
key += "|" + std::string(rgba);
|
||||
}
|
||||
|
||||
if (item.colors.empty() || !seen.insert(key).second) continue;
|
||||
items.emplace_back(std::move(item));
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
AMSMaterialsSetting::AMSMaterialsSetting(wxWindow *parent, wxWindowID id)
|
||||
: DPIDialog(parent, id, _L("AMS Materials Setting"), wxDefaultPosition, wxDefaultSize, wxCAPTION | wxCLOSE_BOX)
|
||||
, m_color_picker_popup(ColorPickerPopup(this))
|
||||
@@ -830,19 +740,8 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
|
||||
nozzle_temp_min.ToLong(&nozzle_temp_min_int);
|
||||
nozzle_temp_max.ToLong(&nozzle_temp_max_int);
|
||||
wxColour color = m_clr_picker->m_colour;
|
||||
std::string tray_color = colour_to_ams_string(color);
|
||||
std::vector<std::string> tray_colors;
|
||||
int tray_ctype = 2;
|
||||
// Only firmware advertising fun2[23] receives the manual multi-color fields.
|
||||
if (obj->is_support_filament_manual_multi_color) {
|
||||
for (const wxColour& selected_color : m_clr_picker->m_cols) {
|
||||
tray_colors.emplace_back(colour_to_ams_string(selected_color));
|
||||
}
|
||||
if (tray_colors.empty()) {
|
||||
tray_colors.emplace_back(tray_color);
|
||||
}
|
||||
tray_ctype = tray_colors.size() > 1 ? m_clr_picker->ctype : 2;
|
||||
}
|
||||
char col_buf[10];
|
||||
sprintf(col_buf, "%02X%02X%02X%02X", (int)color.Red(), (int)color.Green(), (int)color.Blue(), (int)color.Alpha());
|
||||
|
||||
if (ams_filament_id.empty() || nozzle_temp_min.empty() || nozzle_temp_max.empty() || m_filament_type.empty()) {
|
||||
BOOST_LOG_TRIVIAL(trace) << "Invalid Setting id";
|
||||
@@ -861,33 +760,13 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
|
||||
<< ", tray_info_idx (filament_id) = " << ams_filament_id
|
||||
<< ", setting_id = " << ams_setting_id
|
||||
<< ", tray_type = " << m_filament_type
|
||||
<< ", tray_color = " << tray_color
|
||||
<< ", tray_color = " << col_buf
|
||||
<< ", nozzle_temp_min = " << nozzle_temp_min_int
|
||||
<< ", nozzle_temp_max = " << nozzle_temp_max_int;
|
||||
|
||||
// set filament
|
||||
if (m_is_third) {
|
||||
obj->command_ams_filament_settings(ams_id, slot_id, ams_filament_id, ams_setting_id, tray_color, m_filament_type, nozzle_temp_min_int, nozzle_temp_max_int,
|
||||
tray_colors, tray_ctype);
|
||||
|
||||
// Refresh the edited tray immediately while its MQTT acknowledgement is pending.
|
||||
DevAmsTray* tray = nullptr;
|
||||
if (is_virtual_tray()) {
|
||||
for (auto& virtual_tray : obj->vt_slot) {
|
||||
if (virtual_tray.id == std::to_string(ams_id)) {
|
||||
tray = &virtual_tray;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tray = obj->GetFilaSystem()->GetAmsTray(std::to_string(ams_id), std::to_string(slot_id));
|
||||
}
|
||||
if (tray) {
|
||||
tray->color = tray_color;
|
||||
tray->cols = tray_colors;
|
||||
tray->ctype = tray_ctype;
|
||||
tray->set_hold_count();
|
||||
}
|
||||
obj->command_ams_filament_settings(ams_id, slot_id, ams_filament_id, ams_setting_id, std::string(col_buf), m_filament_type, nozzle_temp_min_int, nozzle_temp_max_int);
|
||||
}
|
||||
|
||||
//reset param
|
||||
@@ -1044,15 +923,8 @@ void AMSMaterialsSetting::set_ctype(int ctype)
|
||||
|
||||
void AMSMaterialsSetting::on_picker_color(wxCommandEvent& event)
|
||||
{
|
||||
std::vector<wxColour> colors = m_color_picker_popup.get_selected_colours();
|
||||
if (colors.empty()) {
|
||||
unsigned int color_num = event.GetInt();
|
||||
colors.emplace_back(color_num >> 24 & 0xFF, color_num >> 16 & 0xFF, color_num >> 8 & 0xFF, color_num & 0xFF);
|
||||
}
|
||||
|
||||
set_ctype(m_color_picker_popup.get_selected_ctype());
|
||||
set_color(colors.front());
|
||||
set_colors(colors);
|
||||
unsigned int color_num = event.GetInt();
|
||||
set_color(wxColour(color_num>>24&0xFF, color_num>>16&0xFF, color_num>>8&0xFF, color_num&0xFF));
|
||||
}
|
||||
|
||||
void AMSMaterialsSetting::on_clr_picker(wxMouseEvent &event)
|
||||
@@ -1067,12 +939,14 @@ void AMSMaterialsSetting::on_clr_picker(wxMouseEvent &event)
|
||||
}
|
||||
}
|
||||
|
||||
m_color_picker_popup.set_ams_colours(collect_ams_color_items(obj->GetFilaSystem().get()));
|
||||
m_color_picker_popup.set_preset_colours(collect_preset_color_items(ams_filament_id));
|
||||
m_color_picker_popup.set_def_colour(m_clr_picker->m_colour, m_clr_picker->m_cols, m_clr_picker->ctype);
|
||||
std::vector<wxColour> ams_colors;
|
||||
obj->GetFilaSystem()->CollectAmsColors(ams_colors);
|
||||
|
||||
wxPoint img_pos = m_clr_picker->ClientToScreen(wxPoint(0, 0));
|
||||
wxPoint popup_pos(img_pos.x - m_color_picker_popup.GetSize().x - FromDIP(95), img_pos.y - FromDIP(65));
|
||||
m_color_picker_popup.Position(popup_pos, wxSize(0, 0));
|
||||
m_color_picker_popup.set_ams_colours(ams_colors);
|
||||
m_color_picker_popup.set_def_colour(m_clr_picker->m_colour);
|
||||
m_color_picker_popup.Popup();
|
||||
}
|
||||
|
||||
@@ -1861,46 +1735,7 @@ void ColorPicker::doRender(wxDC& dc)
|
||||
auto radius = m_show_full ? size.x / 2 - FromDIP(1) : size.x / 2;
|
||||
if (m_selected) radius -= FromDIP(1);
|
||||
|
||||
if (m_show_full && m_cols.size() > 1) {
|
||||
if (ctype == 0) {
|
||||
const double center_x = size.x / 2.0;
|
||||
const double center_y = size.y / 2.0;
|
||||
const double draw_radius = radius;
|
||||
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||
for (int x = 0; x < size.x; ++x) {
|
||||
const double dx = x + 0.5 - center_x;
|
||||
if (std::abs(dx) > draw_radius) continue;
|
||||
|
||||
const double half_height = std::sqrt(std::max(0.0, draw_radius * draw_radius - dx * dx));
|
||||
const int top = std::max(0, static_cast<int>(std::ceil(center_y - half_height)));
|
||||
const int bottom = std::min(size.y - 1, static_cast<int>(std::floor(center_y + half_height)));
|
||||
const double pos = size.x > 1 ? static_cast<double>(x) / (size.x - 1) : 0.0;
|
||||
const double scaled = pos * (m_cols.size() - 1);
|
||||
const size_t idx = std::min(static_cast<size_t>(scaled), m_cols.size() - 2);
|
||||
const double ratio = scaled - idx;
|
||||
|
||||
if (top <= bottom) {
|
||||
dc.SetBrush(wxBrush(mix_colour(m_cols[idx], m_cols[idx + 1], ratio)));
|
||||
dc.DrawRectangle(x, top, 1, bottom - top + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
float ev_angle = 360.0 / m_cols.size();
|
||||
const float overlap = 2.0f;
|
||||
wxPoint center(size.x / 2, size.y / 2);
|
||||
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||
dc.SetBrush(wxBrush(m_cols.front()));
|
||||
dc.DrawCircle(center.x, center.y, radius);
|
||||
for (int i = 0; i < m_cols.size(); i++) {
|
||||
dc.SetBrush(m_cols[i]);
|
||||
float startAngle = 270.0f + i * ev_angle;
|
||||
float endAngle = startAngle + ev_angle;
|
||||
dc.DrawEllipticArc(center.x - radius, center.y - radius, 2 * radius, 2 * radius, startAngle - overlap, endAngle + overlap);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (alpha == 0) {
|
||||
if (alpha == 0) {
|
||||
wxSize bmp_size = m_bitmap_transparent_def.GetBmpSize();
|
||||
int center_x = (size.x - bmp_size.x) / 2;
|
||||
int center_y = (size.y - bmp_size.y) / 2;
|
||||
@@ -1942,6 +1777,52 @@ void ColorPicker::doRender(wxDC& dc)
|
||||
dc.SetPen(wxPen(wxColour("#6B6B6B")));
|
||||
dc.SetBrush(*wxTRANSPARENT_BRUSH);
|
||||
dc.DrawCircle(size.x / 2, size.y / 2, radius);
|
||||
|
||||
if (m_cols.size() > 1) {
|
||||
if (ctype == 0) {
|
||||
int left = FromDIP(0);
|
||||
float total_width = size.x;
|
||||
int gwidth = std::round(total_width / (m_cols.size() - 1));
|
||||
|
||||
for (int i = 0; i < m_cols.size() - 1; i++) {
|
||||
|
||||
if ((left + gwidth) > (size.x)) {
|
||||
gwidth = size.x - left;
|
||||
}
|
||||
|
||||
auto rect = wxRect(left, 0, gwidth, size.y);
|
||||
dc.GradientFillLinear(rect, m_cols[i], m_cols[i + 1], wxEAST);
|
||||
left += gwidth;
|
||||
}
|
||||
if (wxGetApp().dark_mode()) {
|
||||
dc.DrawBitmap(m_bitmap_border_dark, wxPoint(0, 0));
|
||||
}
|
||||
else {
|
||||
dc.DrawBitmap(m_bitmap_border, wxPoint(0, 0));
|
||||
}
|
||||
}
|
||||
else {
|
||||
float ev_angle = 360.0 / m_cols.size();
|
||||
float startAngle = 270.0;
|
||||
float endAngle = 270.0;
|
||||
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||
for (int i = 0; i < m_cols.size(); i++) {
|
||||
dc.SetBrush(m_cols[i]);
|
||||
endAngle += ev_angle;
|
||||
endAngle = endAngle > 360.0 ? endAngle - 360.0 : endAngle;
|
||||
wxPoint center(size.x / 2, size.y / 2);
|
||||
dc.DrawEllipticArc(center.x - radius, center.y - radius, 2 * radius, 2 * radius, startAngle, endAngle);
|
||||
startAngle += ev_angle;
|
||||
startAngle = startAngle > 360.0 ? startAngle - 360.0 : startAngle;
|
||||
}
|
||||
if (wxGetApp().dark_mode()) {
|
||||
dc.DrawBitmap(m_bitmap_border_dark, wxPoint(0, 0));
|
||||
}
|
||||
else {
|
||||
dc.DrawBitmap(m_bitmap_border, wxPoint(0, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_is_empty) {
|
||||
@@ -2010,21 +1891,19 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
|
||||
m_ams_fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED);
|
||||
|
||||
//other
|
||||
m_other_fg_sizer = new wxFlexGridSizer(0, 8, 0, 0);
|
||||
m_other_fg_sizer->SetFlexibleDirection(wxBOTH);
|
||||
m_other_fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED);
|
||||
wxFlexGridSizer* fg_sizer;
|
||||
fg_sizer = new wxFlexGridSizer(0, 8, 0, 0);
|
||||
fg_sizer->SetFlexibleDirection(wxBOTH);
|
||||
fg_sizer->SetNonFlexibleGrowMode(wxFLEX_GROWMODE_SPECIFIED);
|
||||
|
||||
|
||||
for (wxColour col : m_def_colors) {
|
||||
auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize);
|
||||
cp->set_color(col);
|
||||
cp->set_colors({col});
|
||||
cp->ctype = 2;
|
||||
cp->set_selected(false);
|
||||
cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238)));
|
||||
m_color_pickers.push_back(cp);
|
||||
m_default_color_pickers.push_back(cp);
|
||||
m_other_fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
|
||||
fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
|
||||
cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) {
|
||||
set_def_colour(cp->m_colour);
|
||||
|
||||
@@ -2091,7 +1970,7 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
|
||||
m_sizer_box->Add(m_sizer_ams, 1, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
|
||||
m_sizer_box->Add(m_ams_fg_sizer, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
|
||||
m_sizer_box->Add(m_sizer_other, 1, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
|
||||
m_sizer_box->Add(m_other_fg_sizer, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
|
||||
m_sizer_box->Add(fg_sizer, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
|
||||
m_sizer_box->Add(m_sizer_custom, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(10));
|
||||
m_sizer_box->Add(m_custom_cp, 0, wxEXPAND|wxLEFT|wxRIGHT, FromDIP(16));
|
||||
m_sizer_box->Add(0, 0, 0, wxTOP, FromDIP(10));
|
||||
@@ -2102,7 +1981,7 @@ ColorPickerPopup::ColorPickerPopup(wxWindow* parent)
|
||||
m_def_color_box->Fit();
|
||||
|
||||
m_sizer_main->Add(m_def_color_box, 0, wxALL | wxEXPAND, 10);
|
||||
SetSizerAndFit(m_sizer_main);
|
||||
SetSizer(m_sizer_main);
|
||||
Layout();
|
||||
Fit();
|
||||
|
||||
@@ -2140,15 +2019,13 @@ void ColorPickerPopup::on_custom_clr_picker(wxMouseEvent& /*event*/)
|
||||
wxPostEvent(GetParent(), evt);
|
||||
}
|
||||
|
||||
void ColorPickerPopup::set_ams_colours(const std::vector<ColorItem>& ams)
|
||||
void ColorPickerPopup::set_ams_colours(std::vector<wxColour> ams)
|
||||
{
|
||||
m_ams_fg_sizer->Clear(false);
|
||||
if (m_ams_color_pickers.size() > 0) {
|
||||
for (ColorPicker* col_pick:m_ams_color_pickers) {
|
||||
|
||||
std::vector<ColorPicker*>::iterator iter = find(m_color_pickers.begin(), m_color_pickers.end(), col_pick);
|
||||
if (iter != m_color_pickers.end()) {
|
||||
col_pick->Hide();
|
||||
col_pick->Destroy();
|
||||
m_color_pickers.erase(iter);
|
||||
}
|
||||
@@ -2158,22 +2035,17 @@ void ColorPickerPopup::set_ams_colours(const std::vector<ColorItem>& ams)
|
||||
}
|
||||
|
||||
|
||||
m_ams_color_items = ams;
|
||||
for (const ColorItem& item : m_ams_color_items) {
|
||||
if (item.colors.empty()) continue;
|
||||
|
||||
m_ams_colors = ams;
|
||||
for (wxColour col : m_ams_colors) {
|
||||
auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize);
|
||||
cp->set_color(item.colors.front());
|
||||
cp->set_colors(item.colors);
|
||||
cp->ctype = item.colors.size() > 1 ? item.ctype : 2;
|
||||
cp->set_show_full(true);
|
||||
cp->set_color(col);
|
||||
cp->set_selected(false);
|
||||
cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238)));
|
||||
m_color_pickers.push_back(cp);
|
||||
m_ams_color_pickers.push_back(cp);
|
||||
m_ams_fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
|
||||
cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) {
|
||||
set_def_colour(cp->m_colour, cp->m_cols, cp->ctype);
|
||||
set_def_colour(cp->m_colour);
|
||||
|
||||
wxCommandEvent evt(EVT_SELECTED_COLOR);
|
||||
unsigned long g_col = ((cp->m_colour.Red() & 0xff) << 24) + ((cp->m_colour.Green() & 0xff) << 16) + ((cp->m_colour.Blue() & 0xff) << 8) + (cp->m_colour.Alpha() & 0xff);
|
||||
@@ -2186,68 +2058,9 @@ void ColorPickerPopup::set_ams_colours(const std::vector<ColorItem>& ams)
|
||||
Fit();
|
||||
}
|
||||
|
||||
void ColorPickerPopup::set_preset_colours(const std::vector<ColorItem>& preset_colors)
|
||||
{
|
||||
m_other_fg_sizer->Clear(false);
|
||||
if (!m_preset_color_pickers.empty()) {
|
||||
for (ColorPicker* col_pick : m_preset_color_pickers) {
|
||||
auto iter = std::find(m_color_pickers.begin(), m_color_pickers.end(), col_pick);
|
||||
if (iter != m_color_pickers.end()) {
|
||||
col_pick->Hide();
|
||||
col_pick->Destroy();
|
||||
m_color_pickers.erase(iter);
|
||||
}
|
||||
}
|
||||
m_preset_color_pickers.clear();
|
||||
}
|
||||
|
||||
for (const ColorItem& item : preset_colors) {
|
||||
if (item.colors.empty()) continue;
|
||||
|
||||
auto cp = new ColorPicker(m_def_color_box, wxID_ANY, wxDefaultPosition, wxDefaultSize);
|
||||
cp->set_color(item.colors.front());
|
||||
cp->set_colors(item.colors);
|
||||
cp->ctype = item.colors.size() > 1 ? item.ctype : 2;
|
||||
cp->set_show_full(true);
|
||||
cp->set_selected(false);
|
||||
cp->SetBackgroundColour(StateColor::darkModeColorFor(wxColour(238,238,238)));
|
||||
if (!item.name.empty()) {
|
||||
cp->SetToolTip(item.name);
|
||||
}
|
||||
|
||||
m_color_pickers.push_back(cp);
|
||||
m_preset_color_pickers.push_back(cp);
|
||||
cp->Bind(wxEVT_LEFT_DOWN, [this, cp](auto& e) {
|
||||
set_def_colour(cp->m_colour, cp->m_cols, cp->ctype);
|
||||
|
||||
wxCommandEvent evt(EVT_SELECTED_COLOR);
|
||||
unsigned long g_col = ((cp->m_colour.Red() & 0xff) << 24) + ((cp->m_colour.Green() & 0xff) << 16) + ((cp->m_colour.Blue() & 0xff) << 8) + (cp->m_colour.Alpha() & 0xff);
|
||||
evt.SetInt(g_col);
|
||||
wxPostEvent(GetParent(), evt);
|
||||
});
|
||||
}
|
||||
|
||||
const bool show_default_colours = m_preset_color_pickers.empty();
|
||||
for (ColorPicker* cp : m_default_color_pickers) {
|
||||
cp->Show(show_default_colours);
|
||||
}
|
||||
for (ColorPicker* cp : (show_default_colours ? m_default_color_pickers : m_preset_color_pickers)) {
|
||||
m_other_fg_sizer->Add(cp, 0, wxALL, FromDIP(3));
|
||||
}
|
||||
|
||||
m_other_fg_sizer->Layout();
|
||||
Layout();
|
||||
Fit();
|
||||
}
|
||||
|
||||
void ColorPickerPopup::set_def_colour(wxColour col, std::vector<wxColour> cols, int ctype)
|
||||
void ColorPickerPopup::set_def_colour(wxColour col)
|
||||
{
|
||||
m_def_col = col;
|
||||
if (cols.empty()) {
|
||||
cols.push_back(col);
|
||||
}
|
||||
m_def_cols = cols;
|
||||
m_def_ctype = m_def_cols.size() > 1 ? ctype : 2;
|
||||
|
||||
for (ColorPicker* cp : m_color_pickers) {
|
||||
if (cp->m_selected) {
|
||||
@@ -2256,8 +2069,7 @@ void ColorPickerPopup::set_def_colour(wxColour col, std::vector<wxColour> cols,
|
||||
}
|
||||
|
||||
for (ColorPicker* cp : m_color_pickers) {
|
||||
if (!cp->IsShown()) continue;
|
||||
if (cp->ctype == m_def_ctype && cp->m_cols == m_def_cols) {
|
||||
if (cp->m_colour == m_def_col) {
|
||||
cp->set_selected(true);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -82,22 +82,14 @@ public:
|
||||
class ColorPickerPopup : public PopupWindow
|
||||
{
|
||||
public:
|
||||
struct ColorItem
|
||||
{
|
||||
std::vector<wxColour> colors;
|
||||
int ctype = 2;
|
||||
wxString name;
|
||||
};
|
||||
|
||||
ScalableBitmap m_ts_bitmap_custom;
|
||||
wxStaticBitmap* m_ts_stbitmap_custom;
|
||||
StaticBox* m_custom_cp;
|
||||
StaticBox* m_def_color_box;
|
||||
wxFlexGridSizer* m_ams_fg_sizer;
|
||||
wxFlexGridSizer* m_other_fg_sizer;
|
||||
wxColour m_def_col;
|
||||
std::vector<wxColour> m_def_colors;
|
||||
std::vector<ColorItem> m_ams_color_items;
|
||||
std::vector<wxColour> m_ams_colors;
|
||||
std::vector<ColorPicker*> m_color_pickers;
|
||||
std::vector<ColorPicker*> m_ams_color_pickers;
|
||||
|
||||
@@ -105,20 +97,13 @@ public:
|
||||
ColorPickerPopup(wxWindow* parent);
|
||||
~ColorPickerPopup() {};
|
||||
void on_custom_clr_picker(wxMouseEvent& event);
|
||||
void set_ams_colours(const std::vector<ColorItem>& ams);
|
||||
void set_preset_colours(const std::vector<ColorItem>& preset_colors);
|
||||
void set_def_colour(wxColour col, std::vector<wxColour> cols = {}, int ctype = 2);
|
||||
const std::vector<wxColour>& get_selected_colours() const { return m_def_cols; }
|
||||
int get_selected_ctype() const { return m_def_ctype; }
|
||||
void set_ams_colours(std::vector<wxColour> ams);
|
||||
void set_def_colour(wxColour col);
|
||||
void paintEvent(wxPaintEvent& evt);
|
||||
virtual void OnDismiss() wxOVERRIDE;
|
||||
virtual bool ProcessLeftDown(wxMouseEvent& event) wxOVERRIDE;
|
||||
|
||||
private:
|
||||
std::vector<ColorPicker*> m_default_color_pickers;
|
||||
std::vector<ColorPicker*> m_preset_color_pickers;
|
||||
std::vector<wxColour> m_def_cols;
|
||||
int m_def_ctype = 2;
|
||||
public:
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -747,9 +747,7 @@ Print::ApplyStatus BackgroundSlicingProcess::apply(const Model& model, const Dyn
|
||||
|
||||
// Orca: prevent resetting under gcode viewer mode
|
||||
if (invalidated != PrintBase::APPLY_STATUS_UNCHANGED) {
|
||||
// Use the app’s plater accessor instead of going through mainframe, otherwise anything during startup that reaches apply()
|
||||
// (loading presets, config or input files, e.g. from the CLI) therefore dereferences a null `mainframe` and crashes.
|
||||
const auto plater = GUI::wxGetApp().plater();
|
||||
const auto plater = GUI::wxGetApp().mainframe->m_plater;
|
||||
if (plater && plater->only_gcode_mode()) {
|
||||
invalidated = PrintBase::APPLY_STATUS_UNCHANGED;
|
||||
}
|
||||
|
||||
@@ -1798,8 +1798,7 @@ int MachineObject::command_ams_calibrate(int ams_id)
|
||||
return command_with_dialog(m_agent->command_ams_calibrate(get_dev_id(), ams_id, MachineObject::m_sequence_id++, is_lan_mode_printer()));
|
||||
}
|
||||
|
||||
int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type,
|
||||
int nozzle_temp_min, int nozzle_temp_max, const std::vector<std::string>& tray_colors, int tray_ctype)
|
||||
int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max)
|
||||
{
|
||||
int tag_tray_id = 0;
|
||||
int tag_ams_id = ams_id;
|
||||
@@ -1828,10 +1827,6 @@ int MachineObject::command_ams_filament_settings(int ams_id, int slot_id, std::s
|
||||
j["print"]["nozzle_temp_min"] = nozzle_temp_min;
|
||||
j["print"]["nozzle_temp_max"] = nozzle_temp_max;
|
||||
j["print"]["tray_type"] = tray_type;
|
||||
if (!tray_colors.empty()) {
|
||||
j["print"]["cols"] = tray_colors;
|
||||
j["print"]["ctype"] = tray_ctype;
|
||||
}
|
||||
|
||||
return this->publish_json(j);
|
||||
}
|
||||
@@ -5483,7 +5478,6 @@ void MachineObject::parse_new_info(json print)
|
||||
is_support_pa_mode = (get_flag_bits_no_border(fun2, 3) == 1);
|
||||
is_support_remote_dry = (get_flag_bits_no_border(fun2, 5) == 1);
|
||||
is_support_check_track_switch_match_slice_printer = get_flag_bits_no_border(fun2, 19) == 1;
|
||||
is_support_filament_manual_multi_color = (get_flag_bits_no_border(fun2, 23) == 1);
|
||||
|
||||
if (DevPrinterConfigUtil::support_print_check_firmware_for_tpu_left(printer_type)) {
|
||||
m_firmware_support_print_tpu_left = get_flag_bits_no_border(fun2, 7) == 1;
|
||||
|
||||
@@ -674,7 +674,6 @@ public:
|
||||
bool is_support_remote_dry = false;
|
||||
bool is_support_check_track_switch_match_slice_printer{false};
|
||||
bool is_support_pa_mode{false};
|
||||
bool is_support_filament_manual_multi_color{false};
|
||||
std::optional<bool> m_firmware_support_print_tpu_left;
|
||||
|
||||
bool installed_upgrade_kit{false};
|
||||
@@ -805,7 +804,7 @@ public:
|
||||
int command_ams_switch_filament(bool switch_filament);
|
||||
int command_ams_air_print_detect(bool air_print_detect);
|
||||
int command_ams_calibrate(int ams_id);
|
||||
int command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max, const std::vector<std::string>& tray_colors = {}, int tray_ctype = 2);
|
||||
int command_ams_filament_settings(int ams_id, int slot_id, std::string filament_id, std::string setting_id, std::string tray_color, std::string tray_type, int nozzle_temp_min, int nozzle_temp_max);
|
||||
int command_ams_select_tray(std::string tray_id);
|
||||
int command_ams_refresh_rfid(int ams_id, int slot_id);
|
||||
int command_ams_control(std::string action);
|
||||
|
||||
@@ -96,7 +96,7 @@ void FilamentColorCodeQuery::LoadFromLocal()
|
||||
const auto& fila_color_strs = json_data_item["fila_color"].get<std::vector<wxString>>();
|
||||
for (const auto& color_str : fila_color_strs) {
|
||||
if (color_str.size() > 3) /* Skip the value like "#0"*/{
|
||||
fila_color.AddColor(wxColour(color_str));
|
||||
fila_color.m_colors.emplace(wxColour(color_str));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,17 +87,6 @@ struct FilamentColor
|
||||
|
||||
public:
|
||||
size_t ColorCount() const noexcept { return m_colors.size(); }
|
||||
// Preserve catalog endpoint order for display while retaining set-based lookup identity.
|
||||
const std::vector<wxColour>& GetColors() const noexcept { return m_color_list; }
|
||||
void AddColor(const wxColour& color)
|
||||
{
|
||||
if (m_colors.insert(color).second) m_color_list.push_back(color);
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<wxColour> m_color_list;
|
||||
|
||||
public:
|
||||
|
||||
void EndSet(int ctype)
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -508,25 +508,120 @@ private:
|
||||
// is where the live adjustment is applied. Called whenever the panel moves or reverts it.
|
||||
void invalidate_adjust_textures();
|
||||
|
||||
// "Adjust Texture" mode: instead of painting, dragging an on-canvas handle changes the active
|
||||
// layer's offset. The handle is a flat panel lying in the paint patch's own tangent plane
|
||||
// (a "pan" - drag anywhere on it for free 2D movement), plus two arrows along the patch's
|
||||
// own U/V axes that constrain the drag to just that one axis for precise nudging. Anchored to
|
||||
// the centroid/average-normal of the active layer's current paint patch (see
|
||||
// libslic3r::compute_layer_paint_anchor()), so nothing is drawn if it has nothing painted yet.
|
||||
// "Adjust Texture" mode: instead of painting, the active layer's placement is a control lying
|
||||
// on the model. The gizmo is one tile of the texture in the paint patch's own tangent plane:
|
||||
// where it sits is the layer's offset, how it is turned is rotation_deg, how big it is is the
|
||||
// tile extents - so each handle is one of the values it edits rather than a stand-in for it.
|
||||
// Anchored to the centroid/average-normal of the active layer's paint patch (see
|
||||
// libslic3r::compute_layer_paint_anchor()), so nothing is drawn until something is painted.
|
||||
//
|
||||
// NOTE: the drag direction/sign below is this session's best-effort reasoning about which way
|
||||
// the texture should appear to move as the handle is dragged - it could not be visually
|
||||
// confirmed while writing it (no way to render/see pixels in this environment), so it may
|
||||
// need a one-line sign flip once actually tested.
|
||||
// Drawn through ImGui's foreground draw list rather than the "flat" shader, which is what gives
|
||||
// it antialiased sub-pixel strokes, rounded joins, a contrast halo and the numeric readout. The
|
||||
// cost is that render_adjust_texture_gizmo() has to be called from inside the gizmo's ImGui
|
||||
// frame (see on_render_input_window()), not from on_render() with the rest of the GL drawing.
|
||||
// Move is the whole interior, free in both axes. AxisMove is the four arrows, each holding a
|
||||
// move to one of the tile's axes. EdgeScale is the four edge bars, one tile extent each, and
|
||||
// CornerScale the four corner grips, which take both extents at once.
|
||||
//
|
||||
// The arrows and the bars share the same four rays out of the tile, so they are kept apart by
|
||||
// distance rather than direction: the bar hugs its edge, then a clear gap, then the arrow well
|
||||
// outside it. adjust_pick() leans on that - see the order it tests them in.
|
||||
enum class AdjustHandle { None, Move, AxisMove, EdgeScale, CornerScale, Rotate };
|
||||
|
||||
// The gizmo's geometry for one frame, in screen pixels. Worked out once and shared by the
|
||||
// drawing and the hit test, so the two cannot disagree about where a handle is. Everything
|
||||
// except the frame itself is a fixed number of pixels, which is what keeps the control equally
|
||||
// usable on a 5 mm part and a 500 mm one - the old handle scaled with the volume's bounding box
|
||||
// and so was unusable at both ends.
|
||||
struct AdjustFrame
|
||||
{
|
||||
bool valid = false;
|
||||
Vec2d center = Vec2d::Zero();
|
||||
// +u+v, +u-v, -u-v, -u+v. A scale grip sits on each.
|
||||
std::array<Vec2d, 4> corner{};
|
||||
// +u, -u, +v, -v: the middle of each edge, and the two ends of the bar that sits just
|
||||
// outside it. The bar's ends are kept rather than recomputed so the drawing and the hit test
|
||||
// cannot end up with it in two different places.
|
||||
std::array<Vec2d, 4> edge_mid{};
|
||||
std::array<Vec2d, 4> bar_a{};
|
||||
std::array<Vec2d, 4> bar_b{};
|
||||
// The axis arrow further out on the same ray: where its shaft starts and where its chevron
|
||||
// ends. Kept, like the bar's ends, so the drawing and the hit test cannot disagree.
|
||||
std::array<Vec2d, 4> arm_base{};
|
||||
std::array<Vec2d, 4> arm_tip{};
|
||||
std::array<Vec2d, 4> arm_dir{}; // unit, pointing outward from the centre
|
||||
// A ring is placed off every corner, but only one is drawn and only that one can be picked:
|
||||
// four marks for a single operation is clutter, and whichever corner the cursor is near is
|
||||
// the one worth offering. `rotate_shown` says which, and see update_rotate_corner() for why
|
||||
// it does not simply track the nearest.
|
||||
std::array<Vec2d, 4> rotate_at{};
|
||||
int rotate_shown = 0;
|
||||
// The tile projects to less than ADJUST_MIN_TILE_PX across, so the frame was drawn at that
|
||||
// floor instead. It is dashed to say so, and the readout still gives the true millimetres.
|
||||
bool clamped = false;
|
||||
// What the frame was built from, in mesh-local mm: the tile's own (rotated) edge directions
|
||||
// and its extents. The drag arithmetic reuses these rather than recomputing them, so it
|
||||
// cannot end up working against a different tile than the one on screen.
|
||||
Vec3f e_u = Vec3f::UnitX(), e_v = Vec3f::UnitY();
|
||||
Vec2f tile_mm = Vec2f::Ones(); // (u, v) as stored on the layer
|
||||
Vec2f half_planar = Vec2f::Ones(); // half extents in planar mm, v already aspect-corrected
|
||||
Vec2f center_planar = Vec2f::Zero();
|
||||
};
|
||||
|
||||
bool update_adjust_anchor(); // recomputes m_adjust_anchor_pos/normal; false if nothing painted
|
||||
// The selected instance's volume-to-world transform, which the bake frame is derived from.
|
||||
bool adjust_volume_to_world(Transform3d &out) const;
|
||||
// The anchor, carried from mesh-local into the bake frame. Converted on every use rather than
|
||||
// stored converted: the object can be moved, rotated or scaled while this mode is open, and a
|
||||
// stored copy would quietly describe the shape it used to be. The expensive part - finding the
|
||||
// paint patch's centroid - stays cached in the local members.
|
||||
bool adjust_anchor_in_frame(Vec3f &pos, Vec3f &normal) const;
|
||||
// Drops every scrap of live gizmo state. Called from each of the several ways out of the mode -
|
||||
// the panel's toggle, collapsing the card, switching to seam marking, closing the gizmo - so a
|
||||
// stale hover highlight or a half-finished drag cannot survive into the next time it is opened.
|
||||
void end_adjust_texture_mode();
|
||||
bool on_mouse_adjust_texture(const wxMouseEvent &mouse_event);
|
||||
void render_adjust_texture_gizmo();
|
||||
// This frame's gizmo geometry, or an invalid frame when there is nothing to draw.
|
||||
AdjustFrame adjust_frame() const;
|
||||
// Which handle is under `mouse`, with `index` set to the arm or corner it belongs to. Tested
|
||||
// outermost first; see the hit-area board of the design for the order and the radii.
|
||||
AdjustHandle adjust_pick(const Vec2d &mouse, const AdjustFrame &f, int &index) const;
|
||||
// Chooses which corner carries the rotate ring. The nearest to the cursor, but it only moves once
|
||||
// the newcomer is clearly nearer, because without that margin the ring flits between two corners
|
||||
// every time the cursor wanders along the line equidistant from both. Never moves during a drag.
|
||||
void update_rotate_corner(const AdjustFrame &f, const Vec2d &mouse);
|
||||
|
||||
// Where the tile's centre sits, in planar mm relative to the anchor, for a layer's placement.
|
||||
// Not the patch centroid: the gizmo *is* the texture's placement, so it has to travel as the
|
||||
// offset changes - pinning it to the centroid is what made the old handle look broken, the
|
||||
// texture sliding while the handle stayed put.
|
||||
static Vec2f adjust_handle_planar(const TextureDisplacementLayer &layer, float aspect);
|
||||
// The exact inverse: the offset that puts the tile's centre at `planar`. Rotating or resizing
|
||||
// the tile changes where a given offset places it, so every drag recomputes the offset from the
|
||||
// centre it wants to keep - which is what makes rotate and scale act *in place* rather than
|
||||
// swinging the tile around the anchor.
|
||||
static Vec2f adjust_offset_for_planar(const Vec2f &planar, float rot_deg, const Vec2f &tile_mm,
|
||||
const Vec2f &mirror_sign, float aspect);
|
||||
// The mesh-local-to-world transform of the selected instance, the (unrotated) tangent basis and
|
||||
// the lifted anchor the whole gizmo is built on. False when there is nothing to build on - which
|
||||
// is the single place that decides the gizmo is not drawable, so the drawing, the hit test and
|
||||
// the drag cannot disagree about it.
|
||||
bool adjust_basis(Transform3d &trafo, Vec3f &u_axis, Vec3f &v_axis, Vec3f &base) const;
|
||||
// Screen-space quad of a tile with the given placement. Used for the pre-drag ghost, which is
|
||||
// drawn from the captured start placement rather than from the layer, the layer having already
|
||||
// been moved by then.
|
||||
bool adjust_quad_for(const Vec2f ¢er_planar, float rot_deg, const Vec2f &tile_mm,
|
||||
std::array<Vec2d, 4> &out) const;
|
||||
// Where the cursor meets the drag plane, in planar mm relative to the anchor. The plane is
|
||||
// deliberately the *fixed* anchor plane rather than the tile's current position: measuring a
|
||||
// drag against a plane the drag itself moves is a feedback loop.
|
||||
bool adjust_planar_under(const Vec2d &mouse, Vec2f &planar) const;
|
||||
// Draws a small '+'/'-' next to the mouse over the 3D view while painting/selecting, so it is
|
||||
// obvious whether the next stroke adds paint (default) or erases it (Shift). Uses ImGui's
|
||||
// foreground draw list, so it must be called from inside the gizmo's ImGui frame.
|
||||
void render_paint_cursor_hint();
|
||||
// Mesh-local tangent-plane basis at m_adjust_anchor_normal: box_projection_axes(), so dragging
|
||||
// Bake-frame tangent-plane basis at m_adjust_anchor_normal: box_projection_axes(), so dragging
|
||||
// on-canvas maps consistently onto offset whichever face the anchor is on.
|
||||
void adjust_tangent_basis(Vec3f &u_axis, Vec3f &v_axis) const;
|
||||
|
||||
@@ -1118,23 +1213,47 @@ private:
|
||||
// See the "Adjust Texture" block of private methods above.
|
||||
bool m_adjust_texture_mode = false;
|
||||
bool m_adjust_anchor_valid = false;
|
||||
Vec3f m_adjust_anchor_pos = Vec3f::Zero(); // mesh-local
|
||||
Vec3f m_adjust_anchor_normal = Vec3f::UnitZ(); // mesh-local
|
||||
// Mesh-local, as compute_layer_paint_anchor() produces them. Everything else about the gizmo
|
||||
// works in the bake frame - world orientation and scale, origin at the volume's own, which is
|
||||
// where the texture is projected (texture_displacement_bake_frame()) - so these go through
|
||||
// adjust_anchor_in_frame() at the point of use. A tile is so many *printed* millimetres whatever
|
||||
// the instance's scale, which is why measuring the gizmo in mesh-local millimetres made its tile
|
||||
// differ from the rendered one by exactly that scale.
|
||||
Vec3f m_adjust_anchor_pos = Vec3f::Zero();
|
||||
Vec3f m_adjust_anchor_normal = Vec3f::UnitZ();
|
||||
|
||||
// Pan: free drag anywhere on the flat panel, moves offset along both axes. AxisU/AxisV: drag
|
||||
// the corresponding arrow, moves offset along only that one axis.
|
||||
enum class AdjustHandle { None, Pan, AxisU, AxisV };
|
||||
AdjustHandle m_adjust_drag_handle = AdjustHandle::None;
|
||||
Vec2f m_adjust_drag_start_offset = Vec2f::Zero();
|
||||
// Anchor-relative planar position (see project_planar()) of the point under the mouse at the
|
||||
// moment the current drag started; every subsequent frame's delta is measured against this,
|
||||
// rather than accumulated frame-to-frame, to avoid drift.
|
||||
Vec2f m_adjust_drag_start_planar = Vec2f::Zero();
|
||||
// What the cursor is over, and what is being dragged. `index` picks the arm or the corner
|
||||
// (0..3, in AdjustFrame's order); it is -1 for Move and Rotate, which have only one of each.
|
||||
// Only ever one of each is set, so two handles can never both look live.
|
||||
// Which corner currently carries the rotate ring. Persists between frames, which is what gives
|
||||
// update_rotate_corner() something to apply its margin against.
|
||||
int m_adjust_rotate_corner = 0;
|
||||
AdjustHandle m_adjust_hover_kind = AdjustHandle::None;
|
||||
int m_adjust_hover_index = -1;
|
||||
AdjustHandle m_adjust_drag_kind = AdjustHandle::None;
|
||||
int m_adjust_drag_index = -1;
|
||||
|
||||
// Lazily-built unit quad (the pan panel) and unit line-with-arrowhead (reused, rotated, for
|
||||
// both the U and V axis arrows), transformed into place at render time.
|
||||
GLModel m_adjust_panel_glmodel;
|
||||
GLModel m_adjust_arrow_glmodel;
|
||||
// Everything the live drag is measured against, captured once on mouse-down. Every frame's
|
||||
// delta is taken from these rather than accumulated frame to frame, so a long drag cannot
|
||||
// drift away from the cursor.
|
||||
Vec2f m_adjust_drag_start_center = Vec2f::Zero(); // tile centre, planar mm
|
||||
Vec2f m_adjust_drag_start_grab = Vec2f::Zero(); // the grabbed point, planar mm
|
||||
// How far the grab landed from the handle's own position, in planar mm. A scale drag reads the
|
||||
// extents straight off the cursor, so without subtracting this the tile would jump by however
|
||||
// far inside the generous hit radius the click happened to fall.
|
||||
Vec2f m_adjust_drag_grab_bias = Vec2f::Zero();
|
||||
float m_adjust_drag_start_rot = 0.f;
|
||||
Vec2f m_adjust_drag_start_tile = Vec2f::Ones(); // (u, v) extents as stored
|
||||
float m_adjust_drag_start_angle = 0.f; // planar angle of the grab, radians
|
||||
// The mirroring the layer had when the drag began. A width or height handle dragged back through
|
||||
// the tile's middle toggles its axis against this, rather than against whatever the last frame
|
||||
// left behind, so dragging back and forth across the middle cannot ratchet the flag.
|
||||
bool m_adjust_drag_start_mirror_u = false;
|
||||
bool m_adjust_drag_start_mirror_v = false;
|
||||
|
||||
// The live value to put in the readout pill, built by the drag and drawn by the renderer in the
|
||||
// same frame. Empty when no drag is running, which is also when no pill is drawn.
|
||||
std::string m_adjust_readout;
|
||||
|
||||
std::map<std::string, wxString> m_desc;
|
||||
|
||||
|
||||
@@ -83,13 +83,11 @@ namespace Slic3r::GUI::GLGizmoUtils {
|
||||
bool clicked = toolbar_circular_button(icon, "##wiki_guide_btn", scale);
|
||||
if (url && *url && clicked)
|
||||
wxLaunchDefaultBrowser(wxString::FromUTF8(url));
|
||||
// ImGuiWrapper::tooltip keeps the text readable on the dark tooltip background whatever text colour the
|
||||
// host window pushed; a negative wrap position keeps the URL on one line.
|
||||
if (ImGui::IsItemHovered()){
|
||||
if(url && *url)
|
||||
wxGetApp().imgui()->tooltip(_u8L("Wiki Guide") + "\n" + url, -1.f);
|
||||
ImGui::SetTooltip("%s\n%s", _u8L("Wiki Guide").c_str(), url);
|
||||
else
|
||||
wxGetApp().imgui()->tooltip(_u8L("Wiki Guide"), -1.f);
|
||||
ImGui::SetTooltip("%s", _u8L("Wiki Guide").c_str());
|
||||
}
|
||||
return clicked; // for dynamically generated links
|
||||
}
|
||||
@@ -103,9 +101,9 @@ namespace Slic3r::GUI::GLGizmoUtils {
|
||||
wxLaunchDefaultBrowser(wxString::FromUTF8(url));
|
||||
if (ImGui::IsItemHovered()){
|
||||
if(url && *url)
|
||||
wxGetApp().imgui()->tooltip(_u8L("Video Guide") + "\n" + url, -1.f);
|
||||
ImGui::SetTooltip("%s\n%s", _u8L("Video Guide").c_str(), url);
|
||||
else
|
||||
wxGetApp().imgui()->tooltip(_u8L("Video Guide"), -1.f);
|
||||
ImGui::SetTooltip("%s", _u8L("Video Guide").c_str());
|
||||
}
|
||||
return clicked; // for dynamically generated links
|
||||
}
|
||||
|
||||
@@ -857,12 +857,11 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
|
||||
if (render_combo(imgui_wrapper, "", modes, selection_idx, 0, coord_combo_width)) {
|
||||
combox_changed = true;
|
||||
}
|
||||
ImGuiWrapper::pop_combo_style();
|
||||
// After pop_combo_style(): its light popup background would hide the tooltip's white text.
|
||||
if (ImGui::IsItemHovered()) {
|
||||
auto tooltip_str = _L("Coordinate system used for transform actions.");
|
||||
imgui_wrapper->tooltip(tooltip_str, imgui_wrapper->calc_text_size(tooltip_str).x + 3 * space_size);
|
||||
}
|
||||
ImGuiWrapper::pop_combo_style();
|
||||
|
||||
// ORCA use TextColored to match axes color
|
||||
float offset_to_center = (unit_size - ImGui::CalcTextSize("O").x) / 2;
|
||||
@@ -965,12 +964,11 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
|
||||
if (render_combo(imgui_wrapper, "", align_modes, align_mode_idx, 0, coord_combo_width))
|
||||
m_align_to_parent = align_mode_idx + 1 == align_modes.size();
|
||||
ImGui::PopID();
|
||||
ImGuiWrapper::pop_combo_style();
|
||||
// After pop_combo_style(): its light popup background would hide the tooltip's white text.
|
||||
if (ImGui::IsItemHovered()) {
|
||||
wxString tooltip_str = _L("Reference for alignment");
|
||||
imgui_wrapper->tooltip(tooltip_str, imgui_wrapper->calc_text_size(tooltip_str).x + 3 * space_size);
|
||||
}
|
||||
ImGuiWrapper::pop_combo_style();
|
||||
|
||||
const bool dark_mode = wxGetApp().dark_mode();
|
||||
int current_axis = -1;
|
||||
@@ -1379,12 +1377,11 @@ void GizmoObjectManipulation::do_render_scale_input_window(ImGuiWrapper* imgui_w
|
||||
if (render_combo(imgui_wrapper, "", modes, selection_idx, 0, coord_combo_width)) {
|
||||
combox_changed = true;
|
||||
}
|
||||
ImGuiWrapper::pop_combo_style();
|
||||
// After pop_combo_style(): its light popup background would hide the tooltip's white text.
|
||||
if (ImGui::IsItemHovered()) {
|
||||
auto tooltip_str = _L("Coordinate system used for transform actions.");
|
||||
imgui_wrapper->tooltip(tooltip_str, imgui_wrapper->calc_text_size(tooltip_str).x + 3 * space_size);
|
||||
}
|
||||
ImGuiWrapper::pop_combo_style();
|
||||
|
||||
// ORCA use TextColored to match axes color
|
||||
float offset_to_center = (unit_size - ImGui::CalcTextSize("O").x) / 2;
|
||||
|
||||
@@ -1332,21 +1332,6 @@ boost::any ConfigOptionsGroup::get_config_value2(const DynamicPrintConfig& confi
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool ConfigOptionsGroup::commit_config_value(const t_config_option_key& opt_key, int opt_index)
|
||||
{
|
||||
if (m_config == nullptr)
|
||||
return false;
|
||||
|
||||
// m_opt_map is filled while the option lines are created, so it still resolves the option when
|
||||
// the group's controls (and thus its fields) have not been built yet.
|
||||
const std::string opt_id = option_id_in_map(m_opt_map, opt_key, opt_index);
|
||||
if (opt_id.empty())
|
||||
return false;
|
||||
|
||||
on_change_OG(opt_id, get_config_value2(*m_config, opt_key, opt_index));
|
||||
return true;
|
||||
}
|
||||
|
||||
Field* ConfigOptionsGroup::get_fieldc(const t_config_option_key& opt_key, int opt_index)
|
||||
{
|
||||
Field* field = get_field(opt_key);
|
||||
|
||||
@@ -119,37 +119,6 @@ using column_t = std::function<wxWindow*(wxWindow* parent, const Line&)>;
|
||||
using t_optionfield_map = std::map<t_config_option_key, t_field>;
|
||||
using t_opt_map = std::map< std::string, std::pair<std::string, int> >;
|
||||
|
||||
// The option id a group's option map holds for opt_key, and the index it carries. `opt_index` is
|
||||
// an in/out parameter: a negative value asks for the option's single entry, an explicit one selects
|
||||
// that index. Returns an empty string when the map does not own the option, or when a negative index
|
||||
// matches several entries -- a vector option the GUI edits one index at a time, which has no single
|
||||
// value to commit. A group fills the map while its option lines are created (OptionsGroup::append_line
|
||||
// / ConfigOptionsGroup::get_option), so this resolves an option even before its controls (and its
|
||||
// field) have been built.
|
||||
inline std::string option_id_in_map(const t_opt_map& options, const t_config_option_key& opt_key, int& opt_index)
|
||||
{
|
||||
if (opt_index >= 0) {
|
||||
for (const auto& opt : options) {
|
||||
if (opt.second.first == opt_key && opt.second.second == opt_index) {
|
||||
opt_index = opt.second.second;
|
||||
return opt.first;
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string opt_id;
|
||||
for (const auto& opt : options) {
|
||||
if (opt.second.first != opt_key)
|
||||
continue;
|
||||
if (!opt_id.empty())
|
||||
return {}; // several entries: a vector option, not one value
|
||||
opt_id = opt.first;
|
||||
opt_index = opt.second.second;
|
||||
}
|
||||
return opt_id;
|
||||
}
|
||||
|
||||
class OptionsGroup {
|
||||
public:
|
||||
const bool staticbox{true};
|
||||
@@ -363,15 +332,6 @@ public:
|
||||
boost::any get_config_value(const DynamicPrintConfig& config, const std::string& opt_key, int opt_index = -1);
|
||||
// BBS: restore all pages in preset
|
||||
boost::any get_config_value2(const DynamicPrintConfig& config, const std::string& opt_key, int opt_index = -1);
|
||||
// Re-run this group's committed-change path for one of its options without a widget event:
|
||||
// build the option's value from the config this group edits (get_config_value2, the same form
|
||||
// the option's field reports) and dispatch on_change_OG(), so the group's option-specific
|
||||
// conversion (e.g. the printer_agent canonicalization) and its registered change handler run
|
||||
// exactly as for a hand edit. The option map is filled while the option lines are created, so
|
||||
// this resolves an option whose field is not built yet (a lazy page, or an option the GUI has
|
||||
// no field for). Returns false when this group does not own the option, or when opt_index < 0
|
||||
// matches several entries (a vector option the GUI edits index by index has no single value).
|
||||
bool commit_config_value(const t_config_option_key& opt_key, int opt_index = -1);
|
||||
Field* get_fieldc(const t_config_option_key& opt_key, int opt_index);
|
||||
std::pair<OG_CustomCtrl*, bool*> get_custom_ctrl_with_blinking_ptr(const t_config_option_key& opt_key, int opt_index/* = -1*/);
|
||||
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
#include "slic3r/GUI/DeviceCore/DevConfigUtil.h"
|
||||
#include <deque>
|
||||
#include <exception>
|
||||
#include <stdexcept>
|
||||
#include "libslic3r/LifecycleEvents.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "slic3r/GUI/SettingsIndex.hpp"
|
||||
@@ -1646,101 +1645,6 @@ void Tab::load_config(const DynamicPrintConfig& config)
|
||||
}
|
||||
}
|
||||
|
||||
// The option keys a preset of `type` may hold. The tab's config shares the global
|
||||
// print_config_def with every other preset type, so membership has to be checked against the
|
||||
// preset type's own key list instead of the config definition, or a printer setting could be
|
||||
// written into the process preset.
|
||||
static const std::vector<std::string>& preset_type_options(Preset::Type type)
|
||||
{
|
||||
static const std::vector<std::string> none;
|
||||
switch (type) {
|
||||
case Preset::TYPE_PRINT: return Preset::print_options();
|
||||
case Preset::TYPE_FILAMENT: return Preset::filament_options();
|
||||
case Preset::TYPE_PRINTER: return Preset::printer_options();
|
||||
default: return none;
|
||||
}
|
||||
}
|
||||
|
||||
static const char* preset_type_name(Preset::Type type)
|
||||
{
|
||||
switch (type) {
|
||||
case Preset::TYPE_PRINT: return "process";
|
||||
case Preset::TYPE_FILAMENT: return "filament";
|
||||
case Preset::TYPE_PRINTER: return "printer";
|
||||
default: return "preset";
|
||||
}
|
||||
}
|
||||
|
||||
// Set one option of the currently edited preset from its serialized string form, then run the
|
||||
// same update path a hand edit runs. The edited preset is not saved: it only becomes dirty,
|
||||
// exactly as if the field had been edited by hand.
|
||||
void Tab::apply_preset_config_value(const std::string& opt_key, const std::string& value)
|
||||
{
|
||||
if (m_config == nullptr)
|
||||
throw std::runtime_error("The preset configuration is not available");
|
||||
if (wxGetApp().plater() == nullptr)
|
||||
throw std::runtime_error("OrcaSlicer main window is not available");
|
||||
|
||||
const std::vector<std::string>& options = preset_type_options(m_type);
|
||||
if (std::find(options.begin(), options.end(), opt_key) == options.end())
|
||||
throw std::invalid_argument(format("Unknown %1% preset setting \"%2%\"", preset_type_name(m_type), opt_key));
|
||||
|
||||
// Parse strictly, keeping a backup: the strict deserializer can already have mutated the option
|
||||
// when it rejects a bad value, so a failed update has to restore the preset it was given.
|
||||
const ConfigOption* previous_option = m_config->option(opt_key);
|
||||
std::unique_ptr<ConfigOption> previous(previous_option != nullptr ? previous_option->clone() : nullptr);
|
||||
try {
|
||||
m_config->set_deserialize_strict(opt_key, value);
|
||||
} catch (const ConfigurationError& err) {
|
||||
if (previous)
|
||||
m_config->set_key_value(opt_key, previous.release());
|
||||
else
|
||||
m_config->erase(opt_key);
|
||||
throw std::invalid_argument(err.what());
|
||||
}
|
||||
|
||||
// Run the same commit path the GUI runs for this setting, so its key-specific behaviour
|
||||
// (ConfigOptionsGroup::on_change_OG, including the printer_agent canonicalization, the group's
|
||||
// pre-checks and Tab::on_value_change()) runs exactly as for a hand edit. A built field supplies
|
||||
// the field's typed/clamped value; otherwise the owning ConfigOptionsGroup builds the value from
|
||||
// the config it edits and dispatches the same on_change_OG(). The field is refreshed from the
|
||||
// config just committed first, so it reports the committed value rather than a stale one.
|
||||
bool committed = false;
|
||||
if (Page* field_page = nullptr; Field* field = get_field(opt_key, &field_page)) {
|
||||
field_page->reload_config();
|
||||
field->field_changed();
|
||||
committed = true;
|
||||
} else {
|
||||
for (const PageShp& page : m_pages) {
|
||||
for (const ConfigOptionsGroupShp& group : page->m_optgroups) {
|
||||
if (group->commit_config_value(opt_key)) {
|
||||
committed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (committed)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (committed) {
|
||||
// The commit path already ran update_dirty() (dirty flag, preset combo and, on the printer
|
||||
// tab, the printer-agent switch); refresh the visible page and push the config to the plater.
|
||||
reload_config();
|
||||
update();
|
||||
} else {
|
||||
// The GUI edits this setting through a custom widget rather than a group (e.g.
|
||||
// compatible_printers), so take the same shared no-field commit path the widget uses. Saved
|
||||
// mode skips the value write (the strict parse above already did it) and keeps the path's
|
||||
// compatibility recompute, dirty flag, preset-combo and plater updates.
|
||||
load_key_value(opt_key, boost::any(), /*saved_value=*/true);
|
||||
// The widget itself normally re-reads its value after load_key_value(); with no widget to do
|
||||
// that, sync the visible page so a custom widget (the compatible-printers row) reflects the
|
||||
// new value.
|
||||
reload_config();
|
||||
}
|
||||
}
|
||||
|
||||
// Reload current $self->{config} (aka $self->{presets}->edited_preset->config) into the UI fields.
|
||||
void Tab::reload_config()
|
||||
{
|
||||
|
||||
@@ -424,17 +424,6 @@ public:
|
||||
//BBS update plater presets if update_plater_presets = true
|
||||
void update_tab_ui(bool update_plater_presets = false);
|
||||
void load_config(const DynamicPrintConfig& config);
|
||||
// Host/plugin API: set one option of this tab's currently edited preset from its serialized
|
||||
// string value, then run the same committed-change path a field edit runs. `opt_key` must be a
|
||||
// setting of this tab's preset type, so a key of another preset type (e.g. a printer setting
|
||||
// passed to the process tab) is rejected instead of written silently. `value` is parsed by the
|
||||
// strict config deserializer, which raises std::invalid_argument for an unknown key or a value
|
||||
// the option cannot parse, and leaves the preset unchanged. The key-specific behaviour runs
|
||||
// through its ConfigOptionsGroup::on_change_OG(): with the option's built field it uses the
|
||||
// field's typed value, otherwise the owning group builds the value and dispatches the same call,
|
||||
// so a not-yet-built field still gets it. A key the GUI edits through a custom widget instead
|
||||
// takes the shared no-field commit path, Tab::load_key_value() in saved mode.
|
||||
void apply_preset_config_value(const std::string& opt_key, const std::string& value);
|
||||
virtual void reload_config();
|
||||
void update_mode();
|
||||
void update_visibility();
|
||||
|
||||
@@ -297,9 +297,12 @@ void UVEditorCanvas::set_island_fill_colors(std::vector<ColorRGBA> colors)
|
||||
Refresh();
|
||||
}
|
||||
|
||||
void UVEditorCanvas::set_uv_transform(float tiling_scale, float rotation_deg, bool tile_enabled, bool tile_mirrored)
|
||||
void UVEditorCanvas::set_uv_transform(float tiling_scale, float tiling_scale_v, const Vec2f &mirror_sign,
|
||||
float rotation_deg, bool tile_enabled, bool tile_mirrored)
|
||||
{
|
||||
m_tiling_scale = (std::abs(tiling_scale) > 1e-6f) ? tiling_scale : 1.f;
|
||||
m_tiling_scale_v = (tiling_scale_v > 1e-6f) ? tiling_scale_v : m_tiling_scale;
|
||||
m_mirror_sign = mirror_sign;
|
||||
m_rotation_deg = rotation_deg;
|
||||
m_tile_enabled = tile_enabled;
|
||||
m_tile_mirrored = tile_mirrored;
|
||||
@@ -661,11 +664,18 @@ Vec2f UVEditorCanvas::island_centroid(int island) const
|
||||
|
||||
Vec2f UVEditorCanvas::uv_delta_to_unwrap(const Vec2f &delta_uv) const
|
||||
{
|
||||
// apply_uv_transform() maps unwrap -> uv as uv = R(unwrap / tiling) + offset, so the inverse of
|
||||
// a *delta* (the translation drops out) is unwrap = tiling * R^-1(delta_uv).
|
||||
// apply_uv_transform() maps unwrap -> uv as uv = S^-1 R(unwrap) + offset, with S the tile
|
||||
// extents, so the inverse of a *delta* (the translation drops out) is unwrap = R^-1(S delta_uv).
|
||||
// The extents therefore multiply *before* the inverse rotation, which is the mirror image of the
|
||||
// forward order; with a square tile the two are interchangeable, with a rectangular one they are
|
||||
// not.
|
||||
const float rad = m_rotation_deg * float(M_PI) / 180.f;
|
||||
const float cs = std::cos(rad), sn = std::sin(rad);
|
||||
return Vec2f(delta_uv.x() * cs + delta_uv.y() * sn, -delta_uv.x() * sn + delta_uv.y() * cs) * m_tiling_scale;
|
||||
// The mirror is its own sign on the same axis, and a negation is its own inverse, so it comes
|
||||
// back out here exactly as it went in.
|
||||
const Vec2f scaled(delta_uv.x() * m_tiling_scale * m_mirror_sign.x(),
|
||||
delta_uv.y() * m_tiling_scale_v * m_mirror_sign.y());
|
||||
return Vec2f(scaled.x() * cs + scaled.y() * sn, -scaled.x() * sn + scaled.y() * cs);
|
||||
}
|
||||
|
||||
Vec2f UVEditorCanvas::snap_correction(int island) const
|
||||
@@ -2180,7 +2190,10 @@ UVEditorPanel::UVEditorPanel(wxWindow *parent) : wxPanel(parent, wxID_ANY)
|
||||
void UVEditorPanel::apply_state(const UVEditorCanvas::PaneState &s)
|
||||
{
|
||||
const wxString name = s.has_layer ? s.layer_name : _L("No layer mapped with Unwrap");
|
||||
const wxString tile = s.has_layer ? wxString::Format(_L("%.1f mm tile"), s.tile_mm) : wxString();
|
||||
// A square tile reads as one number; only a genuinely rectangular one is worth two.
|
||||
const wxString tile = !s.has_layer ? wxString() :
|
||||
std::abs(s.tile_mm_v - s.tile_mm) < 0.005f ? wxString::Format(_L("%.1f mm tile"), s.tile_mm) :
|
||||
wxString::Format(_L("%.1f x %.1f mm tile"), s.tile_mm, s.tile_mm_v);
|
||||
const bool relayout = m_layer_name->GetLabel() != name || m_tile->GetLabel() != tile;
|
||||
m_layer_name->SetLabel(name);
|
||||
m_tile->SetLabel(tile);
|
||||
|
||||
@@ -91,7 +91,10 @@ public:
|
||||
// space, back into the unwrap's mm space - which is where a TextureIsland's offset actually
|
||||
// lives (see apply_uv_transform()). The tile settings come along because the background has to
|
||||
// repeat exactly the way the height sampler does, or the pane would stop showing what gets baked.
|
||||
void set_uv_transform(float tiling_scale, float rotation_deg, bool tile_enabled, bool tile_mirrored);
|
||||
// `tiling_scale_v` is the tile's extent along v; pass <= 0 for a square tile, matching
|
||||
// layer_tile_mm().
|
||||
void set_uv_transform(float tiling_scale, float tiling_scale_v, const Vec2f &mirror_sign,
|
||||
float rotation_deg, bool tile_enabled, bool tile_mirrored);
|
||||
|
||||
// Same 8-bit grayscale pixels build_texture_displacement()'s height sampling uses, shown
|
||||
// beneath the wireframe (expanded to RGBA on upload) so the unwrap can be checked against the
|
||||
@@ -129,7 +132,8 @@ public:
|
||||
{
|
||||
bool has_layer = false; // an active layer mapped with Unwrap
|
||||
wxString layer_name;
|
||||
float tile_mm = 0.f;
|
||||
float tile_mm = 0.f; // tile extent along u
|
||||
float tile_mm_v = 0.f; // along v; equal to tile_mm for a square tile
|
||||
std::vector<unsigned char> thumbnail_rgb; // thumbnail_px square, 3 bytes per pixel; empty for none
|
||||
int thumbnail_px = 0;
|
||||
float seam_angle_deg = 40.f;
|
||||
@@ -292,8 +296,10 @@ private:
|
||||
GLModel m_grid_glmodel;
|
||||
float m_grid_step = 0.f; // the UV step m_grid_glmodel was built for; 0 = not built
|
||||
|
||||
float m_tiling_scale = 1.f;
|
||||
float m_rotation_deg = 0.f;
|
||||
float m_tiling_scale = 1.f;
|
||||
float m_tiling_scale_v = 1.f; // already resolved: never the "unset" sentinel, see set_uv_transform()
|
||||
Vec2f m_mirror_sign = Vec2f::Ones(); // +1 or -1 per axis, see layer_mirror_sign()
|
||||
float m_rotation_deg = 0.f;
|
||||
bool m_tile_enabled = true;
|
||||
bool m_tile_mirrored = false;
|
||||
bool m_snap_enabled = false;
|
||||
|
||||
@@ -38,8 +38,6 @@
|
||||
#include <cstddef>
|
||||
#include <wx/simplebook.h>
|
||||
#include <wx/dcgraph.h>
|
||||
#include <wx/graphics.h>
|
||||
#include <memory>
|
||||
|
||||
#include <boost/log/trivial.hpp>
|
||||
#include <wx/timer.h>
|
||||
@@ -1411,17 +1409,22 @@ void AMSLib::render_lite_lib(wxDC& dc)
|
||||
|
||||
dc.SetPen(wxPen(*wxTRANSPARENT_PEN));
|
||||
if (m_info.material_cols.size() > 1) {
|
||||
wxRect color_rect(FromDIP(10), FromDIP(10), libsize.x - FromDIP(18), libsize.y - FromDIP(18));
|
||||
int left = FromDIP(10);
|
||||
int gwidth = std::round(libsize.x / (m_info.material_cols.size() - 1));
|
||||
//gradient
|
||||
if (m_info.ctype == 0) {
|
||||
fill_gradient_rect_east(dc, color_rect, m_info.material_cols.front(), m_info.material_cols.back());
|
||||
for (int i = 0; i < m_info.material_cols.size() - 1; i++) {
|
||||
auto rect = wxRect(left, FromDIP(10), libsize.x - FromDIP(18), libsize.y - FromDIP(18));
|
||||
fill_gradient_rect_east(dc, rect, m_info.material_cols[i], m_info.material_cols[i + 1]);
|
||||
left += gwidth;
|
||||
}
|
||||
}
|
||||
else {
|
||||
int cols_size = m_info.material_cols.size();
|
||||
for (int i = 0; i < cols_size; i++) {
|
||||
dc.SetBrush(wxBrush(m_info.material_cols[i]));
|
||||
int x = color_rect.x + color_rect.width * i / cols_size;
|
||||
int next_x = color_rect.x + color_rect.width * (i + 1) / cols_size;
|
||||
dc.DrawRectangle(x, color_rect.y, next_x - x, color_rect.height);
|
||||
float x = FromDIP(10) + ((float)libsize.x - FromDIP(18)) * i / cols_size;
|
||||
dc.DrawRoundedRectangle(x, FromDIP(10), ((float)libsize.x - FromDIP(17)) / cols_size, libsize.y - FromDIP(20), 0);
|
||||
}
|
||||
dc.SetBrush(wxBrush(tmp_lib_colour));
|
||||
}
|
||||
@@ -1513,27 +1516,7 @@ void AMSLib::render_generic_lib(wxDC &dc)
|
||||
int top = height - curr_height;
|
||||
|
||||
if (m_ams_model == EXT_AMS){
|
||||
wxRect color_rect(FromDIP(1), FromDIP(1), size.x - FromDIP(2), size.y - FromDIP(1));
|
||||
if ((m_info.ctype == 0 || m_info.ctype == 1) && m_info.material_cols.size() > 1 && alpha != 0) {
|
||||
if (m_info.ctype == 0) {
|
||||
fill_gradient_rect_east(dc, color_rect, m_info.material_cols.front(), m_info.material_cols.back());
|
||||
}
|
||||
else {
|
||||
int cols_size = static_cast<int>(m_info.material_cols.size());
|
||||
for (int i = 0; i < cols_size; i++) {
|
||||
dc.SetPen(wxPen(*wxTRANSPARENT_PEN));
|
||||
dc.SetBrush(wxBrush(m_info.material_cols[i]));
|
||||
int x = color_rect.x + color_rect.width * i / cols_size;
|
||||
int next_x = color_rect.x + color_rect.width * (i + 1) / cols_size;
|
||||
dc.DrawRectangle(x, color_rect.y, next_x - x, color_rect.height);
|
||||
}
|
||||
}
|
||||
dc.SetPen(wxPen(*wxTRANSPARENT_PEN));
|
||||
dc.SetBrush(wxBrush(tmp_lib_colour));
|
||||
}
|
||||
else {
|
||||
dc.DrawRoundedRectangle(color_rect.x, color_rect.y, color_rect.width, color_rect.height, m_radius - 1);
|
||||
}
|
||||
dc.DrawRoundedRectangle(FromDIP(1), FromDIP(1), size.x - FromDIP(2), size.y - FromDIP(1), m_radius - 1);
|
||||
if (alpha == 0) {
|
||||
dc.DrawBitmap(m_bitmap_transparent_def.bmp(), FromDIP(2), FromDIP(2));
|
||||
}
|
||||
@@ -2861,38 +2844,30 @@ void AMSPreview::doRender(wxDC &dc)
|
||||
//dc.DrawRoundedRectangle((size.x - rec_size.x) / 2, (size.y - rec_size.y) / 2, rec_size.x, rec_size.y, FromDIP(2));
|
||||
if (iter.material_cols.size() > 1)
|
||||
{
|
||||
const wxSize swatch_size = m_ams_item_type == AMSModel::N3S_AMS ? AMS_ITEM_CUBE_SIZE : AMS_ITEM_CUBE_SIZE2;
|
||||
const wxRect rect((size.x - swatch_size.x) / 2, (size.y - swatch_size.y) / 2, swatch_size.x, swatch_size.y);
|
||||
const int radius = m_ams_item_type == AMSModel::N3S_AMS ? 0 : FromDIP(3);
|
||||
std::unique_ptr<wxGraphicsContext> owned_gc;
|
||||
wxGraphicsContext* gc = dc.GetGraphicsContext();
|
||||
if (!gc) {
|
||||
owned_gc.reset(wxGraphicsContext::CreateFromUnknownDC(dc));
|
||||
gc = owned_gc.get();
|
||||
}
|
||||
if (gc) {
|
||||
wxGraphicsGradientStops stops(iter.material_cols.front(), iter.material_cols.back());
|
||||
const size_t count = iter.material_cols.size();
|
||||
if (iter.ctype == 0) {
|
||||
for (size_t i = 1; i + 1 < count; ++i)
|
||||
stops.Add(iter.material_cols[i], static_cast<float>(i) / (count - 1));
|
||||
} else {
|
||||
// Two stops at each boundary keep multicolour bands distinct.
|
||||
for (size_t i = 1; i < count; ++i) {
|
||||
const float position = static_cast<float>(i) / count;
|
||||
stops.Add(iter.material_cols[i - 1], position);
|
||||
stops.Add(iter.material_cols[i], position);
|
||||
int fleft = (size.x - AMS_ITEM_CUBE_SIZE.x) / 2;
|
||||
|
||||
float total_width = AMS_ITEM_CUBE_SIZE.x;
|
||||
int gwidth = (total_width / (iter.material_cols.size()));
|
||||
if (iter.ctype == 0) {
|
||||
for (int i = 0; i < iter.material_cols.size() - 1; i++) {
|
||||
|
||||
if ((fleft + gwidth) > (AMS_ITEM_CUBE_SIZE.x)) {
|
||||
gwidth = (fleft + AMS_ITEM_CUBE_SIZE.x) - fleft;
|
||||
}
|
||||
|
||||
auto rect = wxRect(fleft, (size.y - AMS_ITEM_CUBE_SIZE.y) / 2, gwidth, AMS_ITEM_CUBE_SIZE.y);
|
||||
fill_gradient_rect_east(dc, rect, iter.material_cols[i], iter.material_cols[i + 1]);
|
||||
fleft += gwidth;
|
||||
}
|
||||
}
|
||||
else {
|
||||
int cols_size = iter.material_cols.size();
|
||||
for (int i = 0; i < cols_size; i++) {
|
||||
dc.SetPen(wxPen(*wxTRANSPARENT_PEN));
|
||||
dc.SetBrush(wxBrush(iter.material_cols[i]));
|
||||
float x = (size.x - AMS_ITEM_CUBE_SIZE.x) / 2 + total_width * i / cols_size;
|
||||
dc.DrawRectangle(x, (size.y - AMS_ITEM_CUBE_SIZE.y) / 2, total_width / cols_size, AMS_ITEM_CUBE_SIZE.y);
|
||||
}
|
||||
gc->PushState();
|
||||
gc->SetPen(*wxTRANSPARENT_PEN);
|
||||
gc->SetBrush(gc->CreateLinearGradientBrush(rect.x, rect.y, rect.x + rect.width, rect.y, stops));
|
||||
gc->DrawRoundedRectangle(rect.x, rect.y, rect.width, rect.height, radius);
|
||||
gc->PopState();
|
||||
} else {
|
||||
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||
dc.SetBrush(iter.material_colour);
|
||||
dc.DrawRoundedRectangle(rect, radius);
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
#include <slic3r/GUI/GUI_App.hpp>
|
||||
#include <slic3r/GUI/MainFrame.hpp>
|
||||
#include <slic3r/GUI/Plater.hpp>
|
||||
#include <slic3r/GUI/PresetComboBoxes.hpp> // TabPresetComboBox::set_filament_idx
|
||||
#include <slic3r/GUI/Tab.hpp>
|
||||
|
||||
#include <pybind11/stl.h>
|
||||
@@ -49,110 +48,6 @@ PresetCollection& printer_presets(PresetBundle& bundle)
|
||||
return static_cast<PresetCollection&>(bundle.printers);
|
||||
}
|
||||
|
||||
// Handle to one category of currently edited preset. It is a category, not a snapshot: it always
|
||||
// resolves the preset the matching tab edits now, so it stays valid across preset switches.
|
||||
struct PresetEditor
|
||||
{
|
||||
Preset::Type type = Preset::TYPE_INVALID;
|
||||
};
|
||||
|
||||
const char* preset_tab_name(Preset::Type type)
|
||||
{
|
||||
switch (type) {
|
||||
case Preset::TYPE_PRINT: return "process";
|
||||
case Preset::TYPE_FILAMENT: return "filament";
|
||||
case Preset::TYPE_PRINTER: return "printer";
|
||||
default: return "preset";
|
||||
}
|
||||
}
|
||||
|
||||
void update_edited_preset(Preset::Type type, const std::string& key, const std::string& value)
|
||||
{
|
||||
PluginHostUi::run_on_ui_thread([&] {
|
||||
GUI::GUI_App& app = GUI::wxGetApp();
|
||||
if (app.is_closing())
|
||||
throw std::runtime_error("OrcaSlicer is closing");
|
||||
if (app.preset_bundle == nullptr)
|
||||
throw std::runtime_error("Preset bundle is not available");
|
||||
if (app.plater() == nullptr)
|
||||
throw std::runtime_error("OrcaSlicer main window is not available");
|
||||
|
||||
GUI::Tab* tab = app.get_tab(type);
|
||||
if (tab == nullptr)
|
||||
throw std::runtime_error(std::string("The ") + preset_tab_name(type) + " preset tab is not available");
|
||||
// The tab for this preset type exists, but the selected printer's technology may not use it
|
||||
// (the print tab no-ops under SLA); refuse instead of writing a value the UI cannot apply.
|
||||
if (!tab->supports_printer_technology(app.preset_bundle->printers.get_selected_preset().printer_technology()))
|
||||
throw std::runtime_error(std::string("The ") + preset_tab_name(type)
|
||||
+ " preset is not available for the selected printer technology");
|
||||
|
||||
tab->apply_preset_config_value(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
// Slot-addressed filament update. `slot_number` is 1-based, matching the GUI's filament slots and
|
||||
// the values PresetBundle::filament_presets holds. The slot's assigned preset is resolved to its
|
||||
// internal name; when that is not the preset the filament tab edits, the tab is switched through
|
||||
// Tab::select_preset() - the normal UI preselection flow, so a dirty edited preset still prompts
|
||||
// the user and a declined prompt cancels the call with a clear error and no change.
|
||||
void update_filament_slot(int slot_number, const std::string& key, const std::string& value)
|
||||
{
|
||||
PluginHostUi::run_on_ui_thread([&] {
|
||||
GUI::GUI_App& app = GUI::wxGetApp();
|
||||
if (app.is_closing())
|
||||
throw std::runtime_error("OrcaSlicer is closing");
|
||||
if (app.preset_bundle == nullptr)
|
||||
throw std::runtime_error("Preset bundle is not available");
|
||||
if (app.plater() == nullptr)
|
||||
throw std::runtime_error("OrcaSlicer main window is not available");
|
||||
|
||||
PresetBundle& bundle = *app.preset_bundle;
|
||||
if (slot_number < 1 || static_cast<size_t>(slot_number) > bundle.filament_presets.size())
|
||||
throw std::out_of_range("Filament slot " + std::to_string(slot_number) + " is out of range; slot numbers are 1-based and this printer has "
|
||||
+ std::to_string(bundle.filament_presets.size()) + " filament slot(s)");
|
||||
|
||||
const std::string& assigned_name = bundle.filament_presets[static_cast<size_t>(slot_number) - 1];
|
||||
if (assigned_name.empty())
|
||||
throw std::runtime_error("Filament slot " + std::to_string(slot_number) + " has no assigned filament preset");
|
||||
|
||||
// Resolve the assigned name the way the app does, so a renamed/aliased entry maps to the
|
||||
// internal preset it now identifies. Copy the name out before selecting anything: the
|
||||
// collection can be reallocated while switching.
|
||||
Preset* slot_preset = bundle.filaments.find_preset(assigned_name);
|
||||
if (slot_preset == nullptr)
|
||||
throw std::runtime_error("Filament slot " + std::to_string(slot_number) + " is assigned \"" + assigned_name
|
||||
+ "\", which does not resolve to a filament preset");
|
||||
const std::string target_name = slot_preset->name;
|
||||
|
||||
GUI::Tab* tab = app.get_tab(Preset::TYPE_FILAMENT);
|
||||
if (tab == nullptr)
|
||||
throw std::runtime_error("The filament preset tab is not available");
|
||||
if (!tab->supports_printer_technology(bundle.printers.get_selected_preset().printer_technology()))
|
||||
throw std::runtime_error("The filament preset is not available for the selected printer technology");
|
||||
|
||||
// Switch to the slot's preset through the normal preset selection, never a forced one, so the
|
||||
// user's unsaved-changes prompt is honoured. A declined prompt returns false here.
|
||||
if (bundle.filaments.get_edited_preset().name != target_name) {
|
||||
if (!tab->select_preset(target_name))
|
||||
throw std::runtime_error("Switching the edited filament preset to \"" + target_name + "\" for filament slot "
|
||||
+ std::to_string(slot_number) + " was canceled; the setting was not changed");
|
||||
}
|
||||
|
||||
// select_preset() can still leave a different preset edited (e.g. the target disappeared), so
|
||||
// re-check before writing anything.
|
||||
if (bundle.filaments.get_edited_preset().name != target_name)
|
||||
throw std::runtime_error("The edited filament preset is \"" + bundle.filaments.get_edited_preset().name
|
||||
+ "\", not \"" + target_name + "\"; the setting was not changed");
|
||||
|
||||
// Match PlaterPresetComboBox::switch_to_tab(): record which 0-based filament slot this tab
|
||||
// combobox now edits, so the GUI shows the slot the value was written for.
|
||||
if (GUI::TabPresetComboBox* combo = tab->get_combo_box())
|
||||
combo->set_filament_idx(slot_number - 1);
|
||||
|
||||
tab->apply_preset_config_value(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
void reload_local_bundle(const std::string& bundle_id)
|
||||
{
|
||||
bool reloaded = false;
|
||||
@@ -224,47 +119,6 @@ void host_bindings::register_presets(py::module_& host)
|
||||
.value("Plate", Preset::TYPE_PLATE)
|
||||
.value("Model", Preset::TYPE_MODEL);
|
||||
|
||||
py::class_<PresetEditor>(preset, "PresetEditor",
|
||||
"Handle to a category of currently edited preset. It is not a snapshot: it always "
|
||||
"resolves the preset the matching tab edits now, so it stays usable across preset "
|
||||
"switches. update() edits the preset in place; the change is not saved to disk.")
|
||||
.def_readonly("type", &PresetEditor::type)
|
||||
.def(
|
||||
"update",
|
||||
[](const PresetEditor& editor, const std::string& key, const std::string& value) {
|
||||
update_edited_preset(editor.type, key, value);
|
||||
},
|
||||
py::arg("key"), py::arg("value"),
|
||||
"Set one setting of the currently edited preset and run the same committed-change path as "
|
||||
"editing the setting in the GUI; the edited preset becomes dirty until saved/exported. "
|
||||
"key must be a setting of this preset category and value its serialized string form, "
|
||||
"otherwise ValueError is raised and nothing is written. A setting edited through a field "
|
||||
"runs that field's validation even before its page has been opened; one the GUI edits "
|
||||
"outside a field (such as compatible printers) also recomputes preset compatibility.")
|
||||
.def(
|
||||
"update",
|
||||
[](const PresetEditor& editor, int slot_number, const std::string& key, const std::string& value) {
|
||||
if (editor.type != Preset::TYPE_FILAMENT)
|
||||
throw std::invalid_argument("The slot_number form of update() is only available on "
|
||||
"orca.host.preset.filament; call update(key, value) on this editor");
|
||||
update_filament_slot(slot_number, key, value);
|
||||
},
|
||||
py::arg("slot_number"), py::arg("key"), py::arg("value"),
|
||||
"Set one setting of the filament preset assigned to a 1-based filament slot. When that preset "
|
||||
"is not the one currently edited the call switches to it through the normal preset-selection "
|
||||
"flow, so an unsaved edited preset still prompts the user and a declined prompt cancels the "
|
||||
"call with an error and no change. An out-of-range, empty or unresolved slot is rejected with "
|
||||
"a clear error. Other preset editors accept only update(key, value); this form raises "
|
||||
"ValueError on them.");
|
||||
|
||||
// The three preset categories edited by the main tabs. `process` is the print/process preset,
|
||||
// `printer` the machine preset and `filament` the currently edited filament preset (its
|
||||
// 3-argument form addresses a 1-based filament slot); each only accepts its own settings, so a
|
||||
// printer setting passed to `process` is rejected instead of written silently.
|
||||
preset.attr("process") = py::cast(PresetEditor{Preset::TYPE_PRINT});
|
||||
preset.attr("printer") = py::cast(PresetEditor{Preset::TYPE_PRINTER});
|
||||
preset.attr("filament") = py::cast(PresetEditor{Preset::TYPE_FILAMENT});
|
||||
|
||||
py::class_<Preset, std::unique_ptr<Preset, py::nodelete>>(host, "Preset")
|
||||
.def_readonly("type", &Preset::type)
|
||||
.def_readonly("name", &Preset::name)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/catch_all.hpp>
|
||||
#include <catch2/generators/catch_generators.hpp>
|
||||
|
||||
#include <slic3r/GUI/DeviceCore/DevManager.h>
|
||||
#include <slic3r/GUI/DeviceManager.hpp>
|
||||
@@ -17,7 +16,6 @@
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using namespace Slic3r;
|
||||
using json = nlohmann::json;
|
||||
@@ -51,21 +49,6 @@ public:
|
||||
|
||||
AgentInfo get_agent_info() override { return m_info; }
|
||||
|
||||
int send_message(std::string dev_id, std::string json_str, int, int) override
|
||||
{
|
||||
published_device = std::move(dev_id);
|
||||
published_message = json::parse(json_str);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int send_message_to_printer(std::string dev_id, std::string json_str, int qos, int flag) override
|
||||
{
|
||||
return send_message(std::move(dev_id), std::move(json_str), qos, flag);
|
||||
}
|
||||
|
||||
std::string published_device;
|
||||
json published_message;
|
||||
|
||||
private:
|
||||
AgentInfo m_info;
|
||||
};
|
||||
@@ -177,62 +160,3 @@ TEST_CASE("Device manager filters and rehomes devices by printer-agent ownership
|
||||
CHECK(manager.get_my_machine_list("integration-agent-a").empty());
|
||||
CHECK(manager.get_my_machine_list("integration-agent-b").count(machine.dev_id) == 1);
|
||||
}
|
||||
|
||||
TEST_CASE("AMS filament settings keep legacy single-color commands compatible", "[DeviceManager][AMSMaterialsSetting][integration]")
|
||||
{
|
||||
ScopedAppConfig app_config;
|
||||
auto agent = std::make_shared<TestPrinterAgent>("integration-agent");
|
||||
NetworkAgent network(nullptr, agent);
|
||||
DeviceManager manager(&network, false, &app_config.config);
|
||||
MachineObject object(&manager, &network, "Test printer", "integration-device", "192.0.2.10");
|
||||
object.dev_connection_type = GENERATE("lan", "cloud");
|
||||
|
||||
REQUIRE(object.command_ams_filament_settings(1, 2, "filament-id", "setting-id", "FF000080", "PLA", 190, 220) == 0);
|
||||
|
||||
const json& payload = agent->published_message.at("print");
|
||||
CHECK(agent->published_device == object.get_dev_id());
|
||||
CHECK(payload.at("command") == "ams_filament_setting");
|
||||
CHECK(payload.at("ams_id") == 1);
|
||||
CHECK(payload.at("slot_id") == 2);
|
||||
CHECK(payload.at("tray_color") == "FF000080");
|
||||
CHECK_FALSE(payload.contains("cols"));
|
||||
CHECK_FALSE(payload.contains("ctype"));
|
||||
}
|
||||
|
||||
TEST_CASE("AMS filament settings preserve ordered gradient and multicolor RGBA components", "[DeviceManager][AMSMaterialsSetting][integration]")
|
||||
{
|
||||
ScopedAppConfig app_config;
|
||||
auto agent = std::make_shared<TestPrinterAgent>("integration-agent");
|
||||
NetworkAgent network(nullptr, agent);
|
||||
DeviceManager manager(&network, false, &app_config.config);
|
||||
MachineObject object(&manager, &network, "Test printer", "integration-device", "192.0.2.10");
|
||||
object.dev_connection_type = GENERATE("lan", "cloud");
|
||||
const int color_type = GENERATE(0, 1);
|
||||
const std::vector<std::string> colors{"FF000080", "0000FF40", "00FF00FF"};
|
||||
|
||||
REQUIRE(object.command_ams_filament_settings(1, 2, "filament-id", "setting-id", colors.front(), "PLA", 190, 220,
|
||||
colors, color_type) == 0);
|
||||
|
||||
const json& payload = agent->published_message.at("print");
|
||||
CHECK(payload.at("tray_color") == colors.front());
|
||||
CHECK(payload.at("cols").get<std::vector<std::string>>() == colors);
|
||||
CHECK(payload.at("ctype") == color_type);
|
||||
}
|
||||
|
||||
TEST_CASE("AMS filament settings default a supplied single-color list to type two", "[DeviceManager][AMSMaterialsSetting][integration]")
|
||||
{
|
||||
ScopedAppConfig app_config;
|
||||
auto agent = std::make_shared<TestPrinterAgent>("integration-agent");
|
||||
NetworkAgent network(nullptr, agent);
|
||||
DeviceManager manager(&network, false, &app_config.config);
|
||||
MachineObject object(&manager, &network, "Test printer", "integration-device", "192.0.2.10");
|
||||
object.dev_connection_type = GENERATE("lan", "cloud");
|
||||
const std::vector<std::string> colors{"FF000080"};
|
||||
|
||||
REQUIRE(object.command_ams_filament_settings(1, 2, "filament-id", "setting-id", colors.front(), "PLA", 190, 220,
|
||||
colors) == 0);
|
||||
|
||||
const json& payload = agent->published_message.at("print");
|
||||
CHECK(payload.at("cols").get<std::vector<std::string>>() == colors);
|
||||
CHECK(payload.at("ctype") == 2);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,3 @@
|
||||
// why: keep the Windows headers ahead of libslic3r. Model.hpp pulls in Format/STEP.hpp, and
|
||||
// OCCT's Standard_Macro.hxx defines NOUSER and NOGDI on Windows so that windows.h does not
|
||||
// collide with OCCT's identifiers. Defined that early they also hide the USER and GDI
|
||||
// declarations, and the wx MSW headers Field.hpp reaches (wx/wxprec.h -> wx/msw/private.h)
|
||||
// need WNDPROC, RGB and the ::GetWindowRect family. Including windows.h first, as
|
||||
// libslic3r_gui does through pchheader.hpp or win_platform.hpp, keeps them.
|
||||
#ifdef WIN32
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
@@ -22,12 +6,8 @@
|
||||
#include <libslic3r/TriangleMesh.hpp>
|
||||
#include <slic3r/GUI/DockPanel.hpp>
|
||||
#include <slic3r/GUI/AuiPaneLayout.hpp>
|
||||
#include <slic3r/GUI/OptionsGroup.hpp>
|
||||
#include <slic3r/GUI/Widgets/WebHosting.hpp>
|
||||
|
||||
#include <map>
|
||||
#include <utility>
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/catch_message.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
@@ -157,106 +137,6 @@ TEST_CASE("Plugin host API exposes host-owned bundle and preset surface to Pytho
|
||||
CHECK(printers.attr("find_preset")(printer_preset.name).attr("name").cast<std::string>() == printer_preset.name);
|
||||
}
|
||||
|
||||
TEST_CASE("Plugin host API exposes the editable preset category handles and guards them before Orca app initialization",
|
||||
"[PluginHost][Python]")
|
||||
{
|
||||
py::object host = import_orca_module().attr("host");
|
||||
py::object preset = host.attr("preset");
|
||||
|
||||
REQUIRE(has_attr(preset, "PresetEditor"));
|
||||
REQUIRE(has_attr(preset, "process"));
|
||||
REQUIRE(has_attr(preset, "printer"));
|
||||
REQUIRE(has_attr(preset, "filament"));
|
||||
|
||||
py::object process = preset.attr("process");
|
||||
py::object printer = preset.attr("printer");
|
||||
py::object filament = preset.attr("filament");
|
||||
|
||||
CHECK(process.attr("type").cast<Slic3r::Preset::Type>() == Slic3r::Preset::TYPE_PRINT);
|
||||
CHECK(printer.attr("type").cast<Slic3r::Preset::Type>() == Slic3r::Preset::TYPE_PRINTER);
|
||||
CHECK(filament.attr("type").cast<Slic3r::Preset::Type>() == Slic3r::Preset::TYPE_FILAMENT);
|
||||
for (const py::object& editor : { process, printer, filament })
|
||||
CHECK(has_attr(editor, "update"));
|
||||
|
||||
// With no wx application the call must fail cleanly, like the rest of host.preset, and must not
|
||||
// be mistaken for a missing attribute or an argument error.
|
||||
for (const py::object& editor : { process, printer, filament }) {
|
||||
try {
|
||||
editor.attr("update")("layer_height", "0.25");
|
||||
FAIL("preset editor update unexpectedly succeeded without a wx application");
|
||||
} catch (const py::error_already_set& error) {
|
||||
CHECK(error.matches(PyExc_RuntimeError));
|
||||
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Plugin host API exposes the filament slot form of preset update and guards it", "[PluginHost][Python]")
|
||||
{
|
||||
py::object host = import_orca_module().attr("host");
|
||||
py::object preset = host.attr("preset");
|
||||
|
||||
// The filament editor accepts update(slot_number, key, value) as well as update(key, value); with
|
||||
// no wx application it reaches the same clean failure as the rest of host.preset. The slot
|
||||
// validation and the switch/cancel path need a running app, so they are not exercised here.
|
||||
try {
|
||||
preset.attr("filament").attr("update")(1, "layer_height", "0.25");
|
||||
FAIL("slot-addressed filament update unexpectedly succeeded without a wx application");
|
||||
} catch (const py::error_already_set& error) {
|
||||
CHECK(error.matches(PyExc_RuntimeError));
|
||||
CHECK(std::string(error.what()).find("OrcaSlicer application is not initialized") != std::string::npos);
|
||||
}
|
||||
|
||||
// The process and printer editors accept only update(key, value); the slot form is rejected
|
||||
// before any application access, naming the editor that does accept it.
|
||||
for (const char* name : { "process", "printer" }) {
|
||||
CAPTURE(name);
|
||||
try {
|
||||
preset.attr(name).attr("update")(1, "layer_height", "0.25");
|
||||
FAIL("slot-addressed update on a non-filament editor unexpectedly succeeded");
|
||||
} catch (const py::error_already_set& error) {
|
||||
CHECK(error.matches(PyExc_ValueError));
|
||||
CHECK(std::string(error.what()).find("only available on") != std::string::npos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("A settings group resolves an option's id from its option map", "[PluginHost]")
|
||||
{
|
||||
using Slic3r::GUI::option_id_in_map;
|
||||
using Slic3r::GUI::t_opt_map;
|
||||
|
||||
// The map ConfigOptionsGroup::get_option leaves: a scalar option, and a per-extruder option
|
||||
// whose entries carry their index.
|
||||
t_opt_map options;
|
||||
options.emplace("layer_height", std::make_pair(std::string("layer_height"), -1));
|
||||
options.emplace("nozzle_diameter#0", std::make_pair(std::string("nozzle_diameter"), 0));
|
||||
options.emplace("nozzle_diameter#1", std::make_pair(std::string("nozzle_diameter"), 1));
|
||||
|
||||
// A negative index resolves a scalar option and yields its own (negative) index.
|
||||
int index = -1;
|
||||
CHECK(option_id_in_map(options, "layer_height", index) == "layer_height");
|
||||
CHECK(index == -1);
|
||||
|
||||
// A negative index on a vector option matches several entries, so it resolves to nothing.
|
||||
index = -1;
|
||||
CHECK(option_id_in_map(options, "nozzle_diameter", index).empty());
|
||||
|
||||
// An explicit index selects that entry.
|
||||
index = 1;
|
||||
CHECK(option_id_in_map(options, "nozzle_diameter", index) == "nozzle_diameter#1");
|
||||
CHECK(index == 1);
|
||||
|
||||
// An index no entry carries, and a key the map does not own, resolve to nothing.
|
||||
index = 7;
|
||||
CHECK(option_id_in_map(options, "nozzle_diameter", index).empty());
|
||||
index = -1;
|
||||
CHECK(option_id_in_map(options, "wall_loops", index).empty());
|
||||
|
||||
const t_opt_map empty;
|
||||
CHECK(option_id_in_map(empty, "layer_height", index).empty());
|
||||
}
|
||||
|
||||
TEST_CASE("Plugin host API reports unavailable GUI objects before Orca app initialization", "[PluginHost][Python]")
|
||||
{
|
||||
py::object host = import_orca_module().attr("host");
|
||||
|
||||
Reference in New Issue
Block a user