mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
Section hatch by part: touching cut parts never look alike
Each body a section cuts is hatched at 45 or 135 degrees in one of three colours of one family (the cap's clay, a steel and a sage, theme tokens `section_cap` and `section_hatch`). Bodies whose cut outlines touch or overlap get different hatches, the angle changed first, then the colour (solidcraft_render::hatch, a greedy map colouring of the cut); otherwise a body keeps the hatch its name seeds, so it stays the same from frame to frame and as the plane moves. Spacing and line width are unchanged. The hatch rides on the palette (an Rg32Uint texel: colour, 1 + hatch), so the viewport, headless GPU pictures and the CPU raster draw the same. Preference Graphics > Section hatch: By part (default) or One style, the look before this change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DtL5Lzv2n8fyKqD61NCHSF
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"en": {
|
||||
"prefs.section_hatch": "Section hatch",
|
||||
"prefs.section_hatch_by_part": "By part",
|
||||
"prefs.section_hatch_one": "One style"
|
||||
},
|
||||
"ja": {
|
||||
"prefs.section_hatch": "断面ハッチング",
|
||||
"prefs.section_hatch_by_part": "部品ごと",
|
||||
"prefs.section_hatch_one": "単一スタイル"
|
||||
},
|
||||
"zh": {
|
||||
"prefs.section_hatch": "剖面线",
|
||||
"prefs.section_hatch_by_part": "按零件",
|
||||
"prefs.section_hatch_one": "单一样式"
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ struct U {
|
||||
depth: vec4<f32>, // x = 1: reversed depth (1 near, 0 far); y, z: the faces' push (`FACE_BIAS`)
|
||||
tint: vec4<f32>, // xyz added to a whole hovered body's colour before lighting
|
||||
sel: vec4<f32>, // xyz a whole selected body's colour before lighting
|
||||
// the section hatch colours by part (`solidcraft_render::hatch`), the first the cap's own
|
||||
hatch: array<vec4<f32>, 3>,
|
||||
};
|
||||
|
||||
fn reversed() -> bool {
|
||||
@@ -37,16 +39,25 @@ fn clipped(p: vec3<f32>) -> bool {
|
||||
return length(u.clip.xyz) > 0.5 && dot(u.clip.xyz, p) > u.clip.w;
|
||||
}
|
||||
@group(0) @binding(0) var<uniform> u: U;
|
||||
// The scene's colours, 2048 entries to a row: an R32Uint texture rather than a storage buffer,
|
||||
// so the viewport draws on WebGL2 (which has no storage buffers) as well.
|
||||
// The scene's colours, 2048 entries to a row: an Rg32Uint texture rather than a storage buffer,
|
||||
// so the viewport draws on WebGL2 (which has no storage buffers) as well. Each entry is the
|
||||
// colour's bytes, then 1 + the section hatch of the body it colours (0: the cap's own).
|
||||
@group(0) @binding(1) var pal: texture_2d<u32>;
|
||||
|
||||
/// The colour at palette entry `i`, as byte/255 (the old Unorm8x4 conversion, bit for bit).
|
||||
fn palette(i: u32) -> vec4<f32> {
|
||||
let v = textureLoad(pal, vec2<i32>(i32(i & 2047u), i32(i >> 11u)), 0).x;
|
||||
fn palette_entry(i: u32) -> vec2<u32> {
|
||||
return textureLoad(pal, vec2<i32>(i32(i & 2047u), i32(i >> 11u)), 0).xy;
|
||||
}
|
||||
|
||||
/// A palette entry's colour bytes as byte/255 (the old Unorm8x4 conversion, bit for bit).
|
||||
fn unpack_color(v: u32) -> vec4<f32> {
|
||||
return vec4<f32>(f32(v & 255u), f32((v >> 8u) & 255u), f32((v >> 16u) & 255u), f32((v >> 24u) & 255u)) / 255.0;
|
||||
}
|
||||
|
||||
/// The colour at palette entry `i`.
|
||||
fn palette(i: u32) -> vec4<f32> {
|
||||
return unpack_color(palette_entry(i).x);
|
||||
}
|
||||
|
||||
fn to_linear(c: vec3<f32>) -> vec3<f32> {
|
||||
return select(c, pow((c + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4)), c > vec3<f32>(0.04045));
|
||||
}
|
||||
@@ -63,6 +74,8 @@ struct TOut {
|
||||
@location(0) n: vec3<f32>,
|
||||
@location(1) c: vec4<f32>,
|
||||
@location(2) wp: vec3<f32>,
|
||||
// 1 + the section hatch of the body (0: the cap's own), from the palette.
|
||||
@location(3) @interpolate(flat) h: u32,
|
||||
};
|
||||
|
||||
@vertex
|
||||
@@ -70,7 +83,9 @@ fn vs_tri(@location(0) p: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c:
|
||||
var o: TOut;
|
||||
o.pos = u.vp * vec4<f32>(p, 1.0);
|
||||
o.n = n;
|
||||
o.c = palette(c);
|
||||
let e = palette_entry(c);
|
||||
o.c = unpack_color(e.x);
|
||||
o.h = e.y;
|
||||
o.wp = p;
|
||||
return o;
|
||||
}
|
||||
@@ -92,7 +107,9 @@ fn vs_tri_inst(
|
||||
var o: TOut;
|
||||
o.pos = u.vp * vec4<f32>(wp, 1.0);
|
||||
o.n = vec3<f32>(dot(r0.xyz, n), dot(r1.xyz, n), dot(r2.xyz, n));
|
||||
o.c = palette(c);
|
||||
let e = palette_entry(c);
|
||||
o.c = unpack_color(e.x);
|
||||
o.h = e.y;
|
||||
o.wp = wp;
|
||||
return o;
|
||||
}
|
||||
@@ -107,6 +124,7 @@ struct FOut {
|
||||
@builtin(position) pos: vec4<f32>,
|
||||
@location(0) rp: vec3<f32>,
|
||||
@location(1) c: vec4<f32>,
|
||||
@location(2) @interpolate(flat) h: u32,
|
||||
};
|
||||
|
||||
@vertex
|
||||
@@ -122,7 +140,9 @@ fn vs_tri_inst_flat(
|
||||
var o: FOut;
|
||||
o.pos = u.vp * vec4<f32>(wp, 1.0);
|
||||
o.rp = wp - u.eye.xyz;
|
||||
o.c = palette(c);
|
||||
let e = palette_entry(c);
|
||||
o.c = unpack_color(e.x);
|
||||
o.h = e.y;
|
||||
return o;
|
||||
}
|
||||
|
||||
@@ -138,6 +158,7 @@ fn flat_fragment(i: FOut, front: bool) -> TOut {
|
||||
o.pos = i.pos;
|
||||
o.n = n;
|
||||
o.c = i.c;
|
||||
o.h = i.h;
|
||||
o.wp = i.rp + u.eye.xyz;
|
||||
return o;
|
||||
}
|
||||
@@ -294,11 +315,19 @@ fn solid(i: TOut, front: bool) -> vec4<f32> {
|
||||
if (clipped(i.wp)) { discard; }
|
||||
if (!front && length(u.clip.xyz) > 0.5) {
|
||||
// The cut face in its body's colour ("from component" colouring), tinted toward the
|
||||
// cap colour so it reads as a cut, hatched at 45 degrees on screen; with the hatch off
|
||||
// cap colour so it reads as a cut, hatched at 45 degrees on screen; by part, toward the
|
||||
// body's own hatch colour and at its angle (45 or 135 degrees). With the hatch off
|
||||
// (`u.cap.a` 0), plain in the body's own colour.
|
||||
if (u.cap.a < 0.5) { return out_color(vec4<f32>(i.c.rgb, 1.0)); }
|
||||
let base = mix(i.c.rgb, u.cap.rgb, 0.35);
|
||||
let h = fract((i.pos.x + i.pos.y) / 9.0);
|
||||
var tint = u.cap.rgb;
|
||||
var turn = 1.0;
|
||||
if (i.h > 0u) {
|
||||
let k = i.h - 1u;
|
||||
tint = u.hatch[min(k >> 1u, 2u)].rgb;
|
||||
turn = select(1.0, -1.0, (k & 1u) == 1u);
|
||||
}
|
||||
let base = mix(i.c.rgb, tint, 0.35);
|
||||
let h = fract((i.pos.x + turn * i.pos.y) / 9.0);
|
||||
return out_color(vec4<f32>(select(base, base * 0.6, h < 0.22), 1.0));
|
||||
}
|
||||
if (u.ana.x > 0.5 && length(i.n) > 0.5) {
|
||||
|
||||
@@ -187,6 +187,11 @@ impl Tokens {
|
||||
dimension_driven: Color32::from_rgb(120, 126, 138),
|
||||
sketch_point_free: Color32::from_rgb(30, 90, 200),
|
||||
section_cap: Color32::from_rgb(214, 128, 72),
|
||||
// A steel and a sage that tint a body about as strongly as the cap does and to within
|
||||
// a few points of its lightness, so the cut stays calm, while staying apart from the
|
||||
// cap, each other and the body in every colour vision mode (dark keeps them, as the
|
||||
// cap).
|
||||
section_hatch: [Color32::from_rgb(84, 138, 168), Color32::from_rgb(108, 180, 126)],
|
||||
dialog_bg: Color32::from_rgb(250, 251, 252),
|
||||
dialog_border: Color32::from_rgb(213, 219, 227),
|
||||
chip: Color32::from_rgb(38, 120, 218),
|
||||
|
||||
@@ -37,7 +37,7 @@ fn scene(s: &Session) -> Option<(Scene, Camera)> {
|
||||
let face_colors =
|
||||
s.doc.face_colors(body).into_iter().map(|(face, look)| (face as u32, Rgb(look.color[0], look.color[1], look.color[2]))).collect();
|
||||
let opacity = s.doc.model_body_opacity(body) as f32;
|
||||
scene.meshes.push(SceneMesh { mesh, color, face_colors, opacity, closed: body.is_closed() });
|
||||
scene.meshes.push(SceneMesh { mesh, color, face_colors, opacity, closed: body.is_closed(), hatch: Default::default() });
|
||||
}
|
||||
if bounds.is_empty() {
|
||||
return None;
|
||||
|
||||
@@ -72,6 +72,8 @@ pub struct Palette {
|
||||
pub axes: [Rgb; 3],
|
||||
/// What the section cap is tinted toward.
|
||||
pub cap: Rgb,
|
||||
/// The section hatch's other colours, after the cap's (`Scene::part_hatch`).
|
||||
pub hatch: [Rgb; 2],
|
||||
/// Isocurve Analysis: the faces' isoparametric curves.
|
||||
pub isocurve: Rgb,
|
||||
/// The surface analysis colours.
|
||||
@@ -97,6 +99,7 @@ impl Palette {
|
||||
sketch_centerline: colors::SKETCH_CENTERLINE,
|
||||
axes: [colors::AXIS_X, colors::AXIS_Y, colors::AXIS_Z],
|
||||
cap: colors::SECTION_CAP,
|
||||
hatch: solidcraft_render::SECTION_HATCH,
|
||||
isocurve: colors::ISOCURVE,
|
||||
analysis: AnalysisColors::DEFAULT,
|
||||
};
|
||||
@@ -589,6 +592,7 @@ fn build_scene(
|
||||
clip: section.map(|(o, n)| (n, n.dot(o))),
|
||||
cap: Some(p.cap),
|
||||
plain_cap: !s.section_hatch,
|
||||
part_hatch: Some([p.cap, p.hatch[0], p.hatch[1]]),
|
||||
analysis: s.analysis.map(|a| SceneAnalysis { mode: a.mode(), colors: p.analysis }),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -655,6 +659,8 @@ fn build_scene(
|
||||
.flatten()
|
||||
.collect();
|
||||
let mut thread_budget = crate::cosmetic_threads::Budget::default();
|
||||
// Where the section cuts each closed solid body, by its mesh in `sc.meshes`: their hatches.
|
||||
let mut cuts: Vec<(usize, u64, Vec<(Vec3, Vec3)>)> = Vec::new();
|
||||
for body in st.bodies.iter().filter(|b| s.body_visible(&b.name)) {
|
||||
let selected = selected_occurrences.contains(&body.name) || s.selection.iter().any(|x| matches!(x, Sel::Body { name } if *name == body.name));
|
||||
let mesh = body.mesh();
|
||||
@@ -676,7 +682,9 @@ fn build_scene(
|
||||
// A see-through appearance is drawn see-through, as the viewport draws it; a selected
|
||||
// body is drawn solid, so the selection colour reads.
|
||||
let opacity = if selected { 1.0 } else { s.doc.model_body_opacity(body) as f32 };
|
||||
sc.meshes.push(SceneMesh { mesh: Arc::clone(&mesh), color, face_colors, opacity, closed: body.is_closed() });
|
||||
let cut_solid = section.is_some() && body.is_closed() && opacity >= solidcraft_render::SEE_THROUGH;
|
||||
sc.meshes.push(SceneMesh { mesh: Arc::clone(&mesh), color, face_colors, opacity, closed: body.is_closed(), hatch: Default::default() });
|
||||
let at = sc.meshes.len() - 1;
|
||||
if cosmetic_threads
|
||||
&& thread_budget.available()
|
||||
&& s.doc.model_body_opacity(body) >= 0.999
|
||||
@@ -694,8 +702,21 @@ fn build_scene(
|
||||
}
|
||||
// The outline of the body where the section cuts it.
|
||||
if let Some((o, n)) = section {
|
||||
for (a, b) in section_segments(&mesh, o, n) {
|
||||
sc.lines.push(SceneLine { points: vec![a, b], color: p.edge, alpha: 1.0, width: 1.6, on_top: false });
|
||||
let segments = section_segments(&mesh, o, n);
|
||||
for (a, b) in &segments {
|
||||
sc.lines.push(SceneLine { points: vec![*a, *b], color: p.edge, alpha: 1.0, width: 1.6, on_top: false });
|
||||
}
|
||||
if cut_solid && !segments.is_empty() {
|
||||
cuts.push((at, solidcraft_render::hatch::seed(&body.name), segments));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((_, n)) = section {
|
||||
let parts: Vec<solidcraft_render::hatch::CutPart> =
|
||||
cuts.iter().map(|(_, seed, segments)| solidcraft_render::hatch::CutPart { seed: *seed, segments }).collect();
|
||||
for ((at, _, _), hatch) in cuts.iter().zip(solidcraft_render::hatch::hatches(&parts, n)) {
|
||||
if let Some(m) = sc.meshes.get_mut(*at) {
|
||||
m.hatch = hatch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ const MARK: Palette = Palette {
|
||||
sketch_centerline: Rgb(43, 44, 45),
|
||||
axes: [Rgb(46, 47, 48), Rgb(49, 50, 51), Rgb(52, 53, 54)],
|
||||
cap: Rgb(55, 56, 57),
|
||||
hatch: [Rgb(61, 62, 63), Rgb(64, 65, 66)],
|
||||
isocurve: Rgb(58, 59, 60),
|
||||
analysis: solidcraft_render::AnalysisColors {
|
||||
draft: [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.7, 0.8, 0.9]],
|
||||
|
||||
+54
-11
@@ -12,10 +12,11 @@ use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use solidcraft_geom::Vec3;
|
||||
use solidcraft_render::{AnalysisMode, Camera, Rgb, SECTION_CAP, Scene};
|
||||
use solidcraft_render::hatch::Hatch;
|
||||
use solidcraft_render::{AnalysisMode, Camera, Rgb, SECTION_CAP, Scene, SceneMesh};
|
||||
use wgpu::util::DeviceExt;
|
||||
|
||||
use crate::view::{AccessMap, Frame, GpuScene, GpuTarget, Renderer, analysis_colors, analysis_uniform};
|
||||
use crate::view::{AccessMap, Frame, GpuScene, GpuTarget, Palette, PaletteIndex, Renderer, analysis_colors, analysis_uniform};
|
||||
use crate::{GpuError, Result, popped};
|
||||
|
||||
/// The colour format pictures are drawn in: sRGB bytes as they are, like the viewport's usual
|
||||
@@ -401,6 +402,7 @@ fn frame(scene: &Scene, cam: &Camera, [width, height]: [u32; 2], key: u64, acces
|
||||
ssaa: 1.0,
|
||||
hover_tint: [0.0; 3],
|
||||
selection_tint: [0.0; 3],
|
||||
hatch: scene.part_hatch.map_or_else(Default::default, |c| c.map(|Rgb(r, g, b)| [r, g, b].map(|x| f32::from(x) / 255.0))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,16 +410,44 @@ fn frame(scene: &Scene, cam: &Camera, [width, height]: [u32; 2], key: u64, acces
|
||||
/// normals; a face normal where a vertex has none), lines as wide lines (over everything when
|
||||
/// `on_top`), the grid under everything. A body whose appearance is see-through
|
||||
/// ([`solidcraft_render::SceneMesh::glass`]) goes into the scene's glass, drawn as the viewport
|
||||
/// draws glass: far sides first, then near sides, so what lies inside it shows through.
|
||||
/// draws glass: far sides first, then near sides, so what lies inside it shows through. When the
|
||||
/// scene hatches its cut by part ([`Scene::part_hatch`]), each body's colours are palette entries
|
||||
/// of its own, carrying its hatch.
|
||||
pub fn scene_geometry(scene: &Scene) -> GpuScene {
|
||||
let mut sc = GpuScene::default();
|
||||
for m in &scene.meshes {
|
||||
let alpha = (m.alpha() * 255.0).round().clamp(0.0, 255.0) as u8;
|
||||
let c4 = |c: Rgb| [c.0, c.1, c.2, alpha];
|
||||
let by_part = scene.part_hatch.is_some() && scene.clip.is_some();
|
||||
// Per body, its colours' stable entries (by part), and the stable palette they make.
|
||||
let mut own: Vec<HashMap<[u8; 4], PaletteIndex>> = Vec::new();
|
||||
let mut stable: Vec<[u8; 4]> = Vec::new();
|
||||
let mut hatches: Vec<(PaletteIndex, Hatch)> = Vec::new();
|
||||
let c4 = |m: &SceneMesh, c: Rgb| [c.0, c.1, c.2, (m.alpha() * 255.0).round().clamp(0.0, 255.0) as u8];
|
||||
if by_part {
|
||||
for m in &scene.meshes {
|
||||
let mut map = HashMap::new();
|
||||
for c in std::iter::once(m.color).chain(m.face_colors.iter().map(|(_, c)| *c)) {
|
||||
map.entry(c4(m, c)).or_insert_with(|| {
|
||||
let i = PaletteIndex(u32::try_from(stable.len()).unwrap_or(0));
|
||||
stable.push(c4(m, c));
|
||||
hatches.push((i, m.hatch));
|
||||
i
|
||||
});
|
||||
}
|
||||
own.push(map);
|
||||
}
|
||||
}
|
||||
let mut palette = Palette::with_stable(stable);
|
||||
for (i, h) in hatches {
|
||||
palette.set_hatch(i, h);
|
||||
}
|
||||
let mut sc = GpuScene::with_palette(palette);
|
||||
for (mi, m) in scene.meshes.iter().enumerate() {
|
||||
let faces: HashMap<u32, Rgb> = m.face_colors.iter().copied().collect();
|
||||
let mesh = &m.mesh;
|
||||
for (ti, t) in mesh.triangles.iter().enumerate() {
|
||||
let col = sc.color(c4(mesh.tri_face.get(ti).and_then(|f| faces.get(f)).copied().unwrap_or(m.color)));
|
||||
let rgba = c4(m, mesh.tri_face.get(ti).and_then(|f| faces.get(f)).copied().unwrap_or(m.color));
|
||||
let col = match own.get(mi).and_then(|o| o.get(&rgba)) {
|
||||
Some(i) => *i,
|
||||
None => sc.color(rgba),
|
||||
};
|
||||
let [Some(a), Some(b), Some(c)] = t.map(|k| mesh.positions.get(k as usize).copied()) else { continue };
|
||||
let flat = (b - a).cross(c - a).normalized().unwrap_or(Vec3::Z);
|
||||
for (k, p) in t.iter().zip([a, b, c]) {
|
||||
@@ -557,7 +587,6 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::view::TRI_SIZE;
|
||||
use solidcraft_geom::Mesh;
|
||||
use solidcraft_render::SceneMesh;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// One triangle facing the camera.
|
||||
@@ -582,7 +611,14 @@ mod tests {
|
||||
/// headless picture of a translucent target shows the tool inside it.
|
||||
#[test]
|
||||
fn a_see_through_body_is_glass_at_its_own_alpha_and_a_solid_one_is_not() {
|
||||
let mesh = |opacity: f32| SceneMesh { mesh: sheet(), color: Rgb(200, 30, 30), face_colors: Vec::new(), opacity, closed: true };
|
||||
let mesh = |opacity: f32| SceneMesh {
|
||||
mesh: sheet(),
|
||||
color: Rgb(200, 30, 30),
|
||||
face_colors: Vec::new(),
|
||||
opacity,
|
||||
closed: true,
|
||||
hatch: Default::default(),
|
||||
};
|
||||
let geometry = |opacity: f32| scene_geometry(&Scene { meshes: vec![mesh(opacity)], ..Default::default() });
|
||||
|
||||
let solid = geometry(1.0);
|
||||
@@ -606,7 +642,14 @@ mod tests {
|
||||
/// painting one face does not make the body solid again.
|
||||
#[test]
|
||||
fn a_painted_face_keeps_its_bodys_opacity() {
|
||||
let mesh = SceneMesh { mesh: sheet(), color: Rgb(200, 30, 30), face_colors: vec![(0, Rgb(0, 255, 0))], opacity: 0.5, closed: true };
|
||||
let mesh = SceneMesh {
|
||||
mesh: sheet(),
|
||||
color: Rgb(200, 30, 30),
|
||||
face_colors: vec![(0, Rgb(0, 255, 0))],
|
||||
opacity: 0.5,
|
||||
closed: true,
|
||||
hatch: Default::default(),
|
||||
};
|
||||
let sc = scene_geometry(&Scene { meshes: vec![mesh], ..Default::default() });
|
||||
assert_eq!(sc.tris.len(), 0);
|
||||
assert_eq!(color(&sc, &sc.glass), Some([0, 255, 0, 128]), "the face's own colour at the body's alpha");
|
||||
|
||||
+70
-29
@@ -41,7 +41,7 @@ pub struct PaletteIndex(pub u32);
|
||||
/// How many palette entries fit on one texel row: the shader reads `x = i & 2047`,
|
||||
/// `y = i >> 11`. A power of two keeps the lookup a shift and a mask.
|
||||
pub const PALETTE_WIDTH: u32 = 2048;
|
||||
/// Most palette entries a scene may carry (2048 × 512): the palette is an `R32Uint` texture
|
||||
/// Most palette entries a scene may carry (2048 × 512): the palette is an `Rg32Uint` texture
|
||||
/// (`downlevel_defaults`/WebGL2 have no storage buffers in the vertex stage).
|
||||
pub const MAX_PALETTE: usize = 1 << 20;
|
||||
|
||||
@@ -50,17 +50,22 @@ pub const MAX_PALETTE: usize = 1 << 20;
|
||||
/// The first [`Palette::stable_len`] entries are stable for a scene: cached block bytes keep
|
||||
/// referring to them, so appearance edits only change their values. Entries after that are
|
||||
/// interned per build for ephemeral geometry (grids, highlights, sketch, previews).
|
||||
///
|
||||
/// A stable entry also carries the section hatch of the body it colours
|
||||
/// (`solidcraft_render::hatch`): none, the cap's own hatch, unless the scene hatches by part.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Palette {
|
||||
entries: Vec<[u8; 4]>,
|
||||
lookup: std::collections::HashMap<[u8; 4], u32>,
|
||||
stable_len: usize,
|
||||
/// Per entry, 1 + its body's hatch, or 0 for the cap's own (empty: 0 for every entry).
|
||||
hatches: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Palette {
|
||||
/// A palette whose entries are all stable (the model scene's body/face slots).
|
||||
pub fn with_stable(entries: Vec<[u8; 4]>) -> Palette {
|
||||
Palette { stable_len: entries.len(), entries, lookup: std::collections::HashMap::new() }
|
||||
Palette { stable_len: entries.len(), entries, lookup: std::collections::HashMap::new(), hatches: Vec::new() }
|
||||
}
|
||||
|
||||
/// Where the stable entries end and the per-build interned ones begin.
|
||||
@@ -116,9 +121,35 @@ impl Palette {
|
||||
PaletteIndex(i)
|
||||
}
|
||||
|
||||
/// The palette as the shader's `R32Uint` texels (rgba bytes packed little-endian).
|
||||
pub fn texels(&self) -> Vec<u32> {
|
||||
self.entries.iter().map(|[r, g, b, a]| u32::from_le_bytes([*r, *g, *b, *a])).collect()
|
||||
/// Give stable entry `i` (a body's colour) that body's section hatch; `false` when `i` is not
|
||||
/// a stable entry.
|
||||
pub fn set_hatch(&mut self, i: PaletteIndex, hatch: solidcraft_render::hatch::Hatch) -> bool {
|
||||
let at = i.0 as usize;
|
||||
if at >= self.stable_len {
|
||||
return false;
|
||||
}
|
||||
if self.hatches.len() < self.stable_len {
|
||||
self.hatches.resize(self.stable_len, 0);
|
||||
}
|
||||
match self.hatches.get_mut(at) {
|
||||
Some(slot) => {
|
||||
*slot = hatch.0.saturating_add(1);
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The section hatch entry `i` was given ([`Palette::set_hatch`]), if any.
|
||||
pub fn hatch(&self, i: PaletteIndex) -> Option<solidcraft_render::hatch::Hatch> {
|
||||
self.hatches.get(i.0 as usize).and_then(|h| h.checked_sub(1)).map(solidcraft_render::hatch::Hatch)
|
||||
}
|
||||
|
||||
/// Entry `i` as the shader's `Rg32Uint` texel: the rgba bytes packed little-endian, then
|
||||
/// 1 + its hatch (0: the cap's own).
|
||||
fn texel(&self, i: usize) -> [u32; 2] {
|
||||
let colour = self.entries.get(i).map_or(0, |[r, g, b, a]| u32::from_le_bytes([*r, *g, *b, *a]));
|
||||
[colour, u32::from(self.hatches.get(i).copied().unwrap_or(0))]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,8 +182,8 @@ fn octahedral6(n: [f32; 3]) -> u32 {
|
||||
}
|
||||
/// The shader's `U`: the view-projection matrix, 10 `vec4`s (the tenth carries the ambient
|
||||
/// occlusion radius), the 8 analysis colours, their flags, the depth direction and the two
|
||||
/// whole-body highlight colours (`uniform_bytes`).
|
||||
const UNIFORM_SIZE: u64 = 416;
|
||||
/// whole-body highlight colours, then the section hatch colours (`uniform_bytes`).
|
||||
const UNIFORM_SIZE: u64 = 416 + 16 * solidcraft_render::hatch::HATCH_COLOURS as u64;
|
||||
|
||||
/// A body drawn from its display plan, evaluated on the GPU: the plan, and a palette index per
|
||||
/// face (the plan's face numbering; faces past the end take the last index).
|
||||
@@ -914,6 +945,9 @@ pub struct Frame {
|
||||
pub hover_tint: [f32; 3],
|
||||
/// Whole-body selection highlight: a selected body's colour before lighting (sRGB 0..1).
|
||||
pub selection_tint: [f32; 3],
|
||||
/// The section hatch colours by part (straight sRGB, 0..1; the first the cap's own): what a
|
||||
/// cut body whose palette entries carry a hatch ([`Palette::set_hatch`]) is tinted toward.
|
||||
pub hatch: [[f32; 3]; solidcraft_render::hatch::HATCH_COLOURS],
|
||||
}
|
||||
|
||||
/// Heights of the model seen from a direction (an orthographic depth map): a point is reachable
|
||||
@@ -1393,16 +1427,19 @@ fn joined<'a>(blocks: impl Iterator<Item = &'a Option<Batch>>, own: Option<Batch
|
||||
(!parts.is_empty()).then_some(Batch { parts, chunk })
|
||||
}
|
||||
|
||||
/// A scene's palette on the GPU: an `R32Uint` texture, [`PALETTE_WIDTH`] entries to a row.
|
||||
/// A scene's palette on the GPU: an `Rg32Uint` texture (colour, hatch), [`PALETTE_WIDTH`]
|
||||
/// entries to a row.
|
||||
struct PaletteGpu {
|
||||
palette: Arc<Palette>,
|
||||
/// Held for the bind group, which views it.
|
||||
_texture: wgpu::Texture,
|
||||
bind: wgpu::BindGroup,
|
||||
rows: u32,
|
||||
entries: Vec<[u8; 4]>,
|
||||
}
|
||||
|
||||
/// Bytes of one palette texel.
|
||||
const PALETTE_TEXEL: u32 = 8;
|
||||
|
||||
fn palette_texture(device: &wgpu::Device, rows: u32) -> wgpu::Texture {
|
||||
device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("sc_palette"),
|
||||
@@ -1410,28 +1447,28 @@ fn palette_texture(device: &wgpu::Device, rows: u32) -> wgpu::Texture {
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::R32Uint,
|
||||
format: wgpu::TextureFormat::Rg32Uint,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
})
|
||||
}
|
||||
|
||||
/// Rows `first..last` of `entries` as the texture's texels (zero past the end).
|
||||
fn palette_bytes(entries: &[[u8; 4]], first: u32, last: u32) -> Vec<u8> {
|
||||
/// Rows `first..last` of `palette` as the texture's texels (zero past the end).
|
||||
fn palette_bytes(palette: &Palette, first: u32, last: u32) -> Vec<u8> {
|
||||
let width = PALETTE_WIDTH as usize;
|
||||
let mut out = Vec::with_capacity((last - first) as usize * width * 4);
|
||||
let mut out = Vec::with_capacity((last - first) as usize * width * PALETTE_TEXEL as usize);
|
||||
for row in first..last {
|
||||
for col in 0..PALETTE_WIDTH {
|
||||
let i = row as usize * width + col as usize;
|
||||
let texel = entries.get(i).map_or(0, |[r, g, b, a]| u32::from_le_bytes([*r, *g, *b, *a]));
|
||||
out.extend_from_slice(&texel.to_le_bytes());
|
||||
for word in palette.texel(row as usize * width + col as usize) {
|
||||
out.extend_from_slice(&word.to_le_bytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn palette_rows(entries: &[[u8; 4]]) -> u32 {
|
||||
u32::try_from(entries.len().div_ceil(PALETTE_WIDTH as usize)).unwrap_or(1).max(1)
|
||||
fn palette_rows(palette: &Palette) -> u32 {
|
||||
u32::try_from(palette.len().div_ceil(PALETTE_WIDTH as usize)).unwrap_or(1).max(1)
|
||||
}
|
||||
|
||||
impl PaletteGpu {
|
||||
@@ -1443,15 +1480,14 @@ impl PaletteGpu {
|
||||
palette: Arc<Palette>,
|
||||
written: &mut u64,
|
||||
) -> PaletteGpu {
|
||||
let entries = palette.entries().to_vec();
|
||||
let rows = palette_rows(&entries);
|
||||
let rows = palette_rows(&palette);
|
||||
let texture = palette_texture(device, rows);
|
||||
let bytes = palette_bytes(&entries, 0, rows);
|
||||
let bytes = palette_bytes(&palette, 0, rows);
|
||||
*written += bytes.len() as u64;
|
||||
queue.write_texture(
|
||||
wgpu::TexelCopyTextureInfo { texture: &texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
|
||||
&bytes,
|
||||
wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(PALETTE_WIDTH * 4), rows_per_image: Some(rows) },
|
||||
wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(PALETTE_WIDTH * PALETTE_TEXEL), rows_per_image: Some(rows) },
|
||||
wgpu::Extent3d { width: PALETTE_WIDTH, height: rows, depth_or_array_layers: 1 },
|
||||
);
|
||||
let view = texture.create_view(&Default::default());
|
||||
@@ -1463,7 +1499,7 @@ impl PaletteGpu {
|
||||
wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::TextureView(&view) },
|
||||
],
|
||||
});
|
||||
PaletteGpu { palette, _texture: texture, bind, rows, entries }
|
||||
PaletteGpu { palette, _texture: texture, bind, rows }
|
||||
}
|
||||
|
||||
/// Bring the texture up to `palette`, writing only the rows whose entries changed (or the
|
||||
@@ -1480,8 +1516,7 @@ impl PaletteGpu {
|
||||
if Arc::ptr_eq(&self.palette, &palette) {
|
||||
return;
|
||||
}
|
||||
let entries = palette.entries().to_vec();
|
||||
let rows = palette_rows(&entries);
|
||||
let rows = palette_rows(&palette);
|
||||
if rows != self.rows {
|
||||
*self = PaletteGpu::new(device, queue, uniform, bgl, palette, written);
|
||||
return;
|
||||
@@ -1489,7 +1524,7 @@ impl PaletteGpu {
|
||||
let width = PALETTE_WIDTH as usize;
|
||||
let same = |row: u32| {
|
||||
let base = row as usize * width;
|
||||
(0..width).all(|c| self.entries.get(base + c).copied().unwrap_or_default() == entries.get(base + c).copied().unwrap_or_default())
|
||||
(0..width).all(|c| self.palette.texel(base + c) == palette.texel(base + c))
|
||||
};
|
||||
let mut row = 0u32;
|
||||
while row < rows {
|
||||
@@ -1501,7 +1536,7 @@ impl PaletteGpu {
|
||||
while row < rows && !same(row) {
|
||||
row += 1;
|
||||
}
|
||||
let bytes = palette_bytes(&entries, first, row);
|
||||
let bytes = palette_bytes(&palette, first, row);
|
||||
*written += bytes.len() as u64;
|
||||
queue.write_texture(
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
@@ -1511,11 +1546,10 @@ impl PaletteGpu {
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
&bytes,
|
||||
wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(PALETTE_WIDTH * 4), rows_per_image: Some(row - first) },
|
||||
wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(PALETTE_WIDTH * PALETTE_TEXEL), rows_per_image: Some(row - first) },
|
||||
wgpu::Extent3d { width: PALETTE_WIDTH, height: row - first, depth_or_array_layers: 1 },
|
||||
);
|
||||
}
|
||||
self.entries = entries;
|
||||
self.palette = palette;
|
||||
}
|
||||
}
|
||||
@@ -2942,6 +2976,7 @@ fn frame_key(cb: &Frame) -> u64 {
|
||||
// picture cached under the previous theme's colours is not shown (#5802).
|
||||
cb.hover_tint.map(f32::to_bits).hash(&mut h);
|
||||
cb.selection_tint.map(f32::to_bits).hash(&mut h);
|
||||
cb.hatch.iter().flatten().for_each(|v| v.to_bits().hash(&mut h));
|
||||
cb.outline.hash(&mut h);
|
||||
cb.ao_radius.to_bits().hash(&mut h);
|
||||
cb.ssaa.to_bits().hash(&mut h);
|
||||
@@ -3656,6 +3691,9 @@ pub fn uniform_bytes(cb: &Frame, w: f32, h: f32, linear: bool, reversed: bool) -
|
||||
v.extend([if reversed { 1.0 } else { 0.0 }, FACE_STEPS as f32, FACE_SLOPE, 0.0]);
|
||||
v.extend([cb.hover_tint[0], cb.hover_tint[1], cb.hover_tint[2], 0.0]);
|
||||
v.extend([cb.selection_tint[0], cb.selection_tint[1], cb.selection_tint[2], 0.0]);
|
||||
for [r, g, b] in cb.hatch {
|
||||
v.extend([r, g, b, 1.0]);
|
||||
}
|
||||
v.iter().flat_map(|x| x.to_le_bytes()).collect()
|
||||
}
|
||||
|
||||
@@ -3820,6 +3858,7 @@ mod tests {
|
||||
access: None,
|
||||
hover_tint: [0.0; 3],
|
||||
selection_tint: [0.0; 3],
|
||||
hatch: Default::default(),
|
||||
outline: false,
|
||||
eye: [0.0; 4],
|
||||
ao_radius: 0.0,
|
||||
@@ -4106,6 +4145,7 @@ mod glass_tests {
|
||||
ssaa: 1.0,
|
||||
hover_tint: [0.0; 3],
|
||||
selection_tint: [0.0; 3],
|
||||
hatch: Default::default(),
|
||||
};
|
||||
let texture = |format, usage, sample_count| {
|
||||
device.create_texture(&wgpu::TextureDescriptor {
|
||||
@@ -4273,6 +4313,7 @@ mod glass_tests {
|
||||
access: None,
|
||||
hover_tint: [0.0; 3],
|
||||
selection_tint: [0.0; 3],
|
||||
hatch: Default::default(),
|
||||
outline: false,
|
||||
eye: [0.0, 0.0, 100.0, 0.0],
|
||||
ao_radius: 0.0,
|
||||
|
||||
@@ -116,6 +116,7 @@ impl Renderer {
|
||||
ssaa: 1.0,
|
||||
hover_tint: [0.0; 3],
|
||||
selection_tint: [0.0; 3],
|
||||
hatch: Default::default(),
|
||||
};
|
||||
let size = wgpu::Extent3d { width: SIZE, height: SIZE, depth_or_array_layers: 1 };
|
||||
let color = device.create_texture(&wgpu::TextureDescriptor {
|
||||
|
||||
@@ -502,3 +502,76 @@ fn a_cut_hides_the_edges_behind_it() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The hatch a picture shows over the pixels `at`: the colour most of them have (the fill,
|
||||
/// rounded to 8 levels a channel), and whether the darker lines through it run at 135 degrees
|
||||
/// (down to the right on screen) rather than 45.
|
||||
fn hatch_seen(img: &[u8], w: u32, at: &[(i64, i64)]) -> ([u8; 3], bool) {
|
||||
let px = |x: i64, y: i64| {
|
||||
let i = (y * i64::from(w) + x) as usize * 4;
|
||||
img.get(i..i + 3).map(|p| [p[0], p[1], p[2]])
|
||||
};
|
||||
let mut counts = std::collections::HashMap::new();
|
||||
for (x, y) in at {
|
||||
if let Some(p) = px(*x, *y) {
|
||||
*counts.entry(p.map(|c| c / 8)).or_insert(0usize) += 1;
|
||||
}
|
||||
}
|
||||
let fill = counts.into_iter().max_by_key(|(_, n)| *n).map_or([0; 3], |(c, _)| c);
|
||||
let line = |x: i64, y: i64| {
|
||||
px(x, y).is_some_and(|p| p.iter().map(|c| u32::from(*c)).sum::<u32>() * 8 < fill.iter().map(|c| u32::from(*c) * 8 + 4).sum::<u32>() * 7)
|
||||
};
|
||||
// Along a 45 degree line (x + y the same) the pixels agree; across it they alternate.
|
||||
let agree = |dy: i64| at.iter().filter(|(x, y)| line(*x, *y) && line(x + 1, y + dy)).count();
|
||||
(fill, agree(1) > agree(-1))
|
||||
}
|
||||
|
||||
/// Section hatch by part: two blocks side by side, cut through both, touch along the cut, so they
|
||||
/// get different hatches (in angle or colour) and the picture shows them apart; the next frame
|
||||
/// and a plane moved a little keep each block's hatch. With one hatch for every body (the
|
||||
/// preference's old behaviour) both caps are the cap colour at 45 degrees.
|
||||
#[test]
|
||||
fn touching_caps_are_hatched_apart_and_keep_their_hatch() {
|
||||
let left = ("solid.box", json!({"length": 20, "width": 30, "height": 20, "body_name": "left"}));
|
||||
let right = ("solid.box", json!({"length": 20, "width": 30, "height": 20, "corner": [20, 0, 0], "operation": "new", "body_name": "right"}));
|
||||
let cut = |y: f64| ("inspect.section", json!({"plane": {"origin": [0, y, 0], "normal": [0, -1, 0]}}));
|
||||
let s = session(&[left.clone(), right.clone(), cut(15.0)]);
|
||||
let moved = session(&[left, right, cut(15.5)]);
|
||||
let mut cam = view::home_camera(&s).unwrap();
|
||||
(cam.fov, cam.yaw, cam.pitch) = (0.0, -0.3, 0.2);
|
||||
let hatches = |s: &Session| view::scene(s, &cam).unwrap().meshes.iter().map(|m| m.hatch).collect::<Vec<_>>();
|
||||
let first = hatches(&s);
|
||||
assert_eq!(first.len(), 2);
|
||||
assert_ne!(first[0], first[1], "touching caps share a hatch");
|
||||
assert_eq!(hatches(&s), first, "the next frame keeps them");
|
||||
assert_eq!(hatches(&moved), first, "a plane moved a little keeps them");
|
||||
let (w, h) = (360, 280);
|
||||
// Each block's cap on screen, 4 px in: x 0..20 and 20..40, z 0..20 at y = 15.
|
||||
let radius = view::scene(&s, &cam).unwrap().radius;
|
||||
let cap_pixels = |x0: f64| -> Vec<(i64, i64)> {
|
||||
let corner = |x: f64, z: f64| cam.to_screen(Vec3::new(x, 15.0, z), f64::from(w), f64::from(h), radius).map(|(x, y, _)| (x, y));
|
||||
let q: Vec<(f64, f64)> = [(x0, 0.0), (x0 + 20.0, 0.0), (x0 + 20.0, 20.0), (x0, 20.0)].iter().filter_map(|(x, z)| corner(*x, *z)).collect();
|
||||
assert_eq!(q.len(), 4, "the cut is on screen");
|
||||
let turn = (0..4).map(|i| q[i].0 * q[(i + 1) % 4].1 - q[(i + 1) % 4].0 * q[i].1).sum::<f64>().signum();
|
||||
let inside = |x: f64, y: f64| {
|
||||
(0..4).all(|i| {
|
||||
let ((ax, ay), (bx, by)) = (q[i], q[(i + 1) % 4]);
|
||||
((bx - ax) * (y - ay) - (by - ay) * (x - ax)) / (bx - ax).hypot(by - ay) * turn >= 4.0
|
||||
})
|
||||
};
|
||||
(0..i64::from(h)).flat_map(|y| (0..i64::from(w)).map(move |x| (x, y))).filter(|(x, y)| inside(*x as f64 + 0.5, *y as f64 + 0.5)).collect()
|
||||
};
|
||||
let (a, b) = (cap_pixels(0.0), cap_pixels(20.0));
|
||||
assert!(a.len() > 1500 && b.len() > 1500, "the caps ({} and {} px)", a.len(), b.len());
|
||||
for (mode, c) in capturers() {
|
||||
let by_part = c.rgba(&Scene { grid: Vec::new(), ..view::scene(&s, &cam).unwrap() }, &cam, w, h).unwrap();
|
||||
let (la, lb) = (hatch_seen(&by_part, w, &a), hatch_seen(&by_part, w, &b));
|
||||
assert_ne!(la, lb, "{mode} depth: the touching caps look alike");
|
||||
let expect = |hatch: solidcraft_engine::render::hatch::Hatch| hatch.turned();
|
||||
assert_eq!((la.1, lb.1), (expect(first[0]), expect(first[1])), "{mode} depth: the caps' angles");
|
||||
let one = c.rgba(&Scene { grid: Vec::new(), part_hatch: None, ..view::scene(&s, &cam).unwrap() }, &cam, w, h).unwrap();
|
||||
let (oa, ob) = (hatch_seen(&one, w, &a), hatch_seen(&one, w, &b));
|
||||
assert_eq!(oa, ob, "{mode} depth: one hatch for every body");
|
||||
assert!(!oa.1, "{mode} depth: one hatch runs at 45 degrees");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
//! The section hatch by part, as engineering section drawings tell parts apart: each body a
|
||||
//! section cuts is hatched at 45 or 135 degrees in one of a few colours of one family, and bodies
|
||||
//! whose cut faces touch or overlap never share a hatch (a map colouring of the cut, the angle
|
||||
//! changed first, then the colour). Otherwise a body keeps the hatch its name seeds, so it stays
|
||||
//! the same from frame to frame and as the plane moves. Spacing and line width are the same for
|
||||
//! every hatch; only the angle and the colour change.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use solidcraft_geom::{Vec2, Vec3};
|
||||
|
||||
/// The colours a cut is hatched in: the section cap's own, then the others of its family.
|
||||
pub const HATCH_COLOURS: usize = 3;
|
||||
|
||||
/// The hatches a body may get: every colour at 45 and at 135 degrees.
|
||||
pub const HATCHES: usize = 2 * HATCH_COLOURS;
|
||||
|
||||
/// One of the [`HATCHES`]: colour `n / 2`, at 135 degrees when `n` is odd. Hatch 0 is the one a
|
||||
/// section draws every body with when it does not hatch by part: the cap's colour at 45 degrees.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
|
||||
pub struct Hatch(pub u8);
|
||||
|
||||
impl Hatch {
|
||||
/// Which of the [`HATCH_COLOURS`] it is drawn in.
|
||||
pub fn colour(self) -> usize {
|
||||
usize::from(self.0 / 2) % HATCH_COLOURS
|
||||
}
|
||||
|
||||
/// Its lines run at 135 degrees on screen (else 45).
|
||||
pub fn turned(self) -> bool {
|
||||
self.0 % 2 == 1
|
||||
}
|
||||
|
||||
fn of(colour: usize, turned: bool) -> Hatch {
|
||||
Hatch(((colour % HATCH_COLOURS) * 2 + usize::from(turned)) as u8)
|
||||
}
|
||||
}
|
||||
|
||||
/// A body's seed: its persistent name hashed (FNV-1a, the same on every platform and release).
|
||||
pub fn seed(name: &str) -> u64 {
|
||||
name.bytes().fold(0xcbf2_9ce4_8422_2325u64, |h, b| (h ^ u64::from(b)).wrapping_mul(0x0100_0000_01b3))
|
||||
}
|
||||
|
||||
/// A closed body the section cuts: its [`seed`] and where the plane cuts it, as segments in the
|
||||
/// plane (one per crossed triangle, in world space).
|
||||
pub struct CutPart<'a> {
|
||||
pub seed: u64,
|
||||
pub segments: &'a [(Vec3, Vec3)],
|
||||
}
|
||||
|
||||
/// Cut faces closer than this, as a share of the whole cut's size, touch: a pin in its bore,
|
||||
/// tessellated each its own way, still reads as one contact.
|
||||
const TOUCH: f64 = 1e-3;
|
||||
|
||||
/// The hatch of each part (in order) cut by a plane with normal `normal`: the one its seed picks,
|
||||
/// unless a part it touches or overlaps on the cut, taken first, has that one already; then the
|
||||
/// same colour at the other angle, then the next colours. Parts are taken in the order of their
|
||||
/// seeds, so the hatches depend only on which parts are cut and which of them touch.
|
||||
pub fn hatches(parts: &[CutPart], normal: Vec3) -> Vec<Hatch> {
|
||||
let neighbours = touching(parts, normal);
|
||||
let mut order: Vec<usize> = (0..parts.len()).collect();
|
||||
order.sort_by_key(|&i| (parts.get(i).map_or(0, |p| p.seed), i));
|
||||
let mut out: Vec<Option<Hatch>> = vec![None; parts.len()];
|
||||
for i in order {
|
||||
let Some(part) = parts.get(i) else { continue };
|
||||
let taken: Vec<Hatch> = neighbours.get(&i).into_iter().flatten().filter_map(|j| out.get(*j).copied().flatten()).collect();
|
||||
let own = Hatch((mix(part.seed) % HATCHES as u64) as u8);
|
||||
let candidates = (0..HATCH_COLOURS).flat_map(|k| {
|
||||
let colour = own.colour() + k;
|
||||
[Hatch::of(colour, own.turned()), Hatch::of(colour, !own.turned())]
|
||||
});
|
||||
// The first free one; with every one taken, the one the fewest neighbours have.
|
||||
let pick = candidates.clone().find(|h| !taken.contains(h)).or_else(|| candidates.min_by_key(|h| taken.iter().filter(|t| *t == h).count()));
|
||||
if let Some(slot) = out.get_mut(i) {
|
||||
*slot = pick;
|
||||
}
|
||||
}
|
||||
out.into_iter().map(Option::unwrap_or_default).collect()
|
||||
}
|
||||
|
||||
/// The seed spread over all its bits (SplitMix64's finaliser), so names that differ in one
|
||||
/// letter pick unrelated hatches.
|
||||
fn mix(mut z: u64) -> u64 {
|
||||
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
|
||||
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
|
||||
z ^ (z >> 31)
|
||||
}
|
||||
|
||||
/// Which parts touch or overlap on the cut: per part, the others.
|
||||
fn touching(parts: &[CutPart], normal: Vec3) -> HashMap<usize, Vec<usize>> {
|
||||
let u = normal.any_perp();
|
||||
let v = normal.cross(u).normalized().unwrap_or(Vec3::Y);
|
||||
let flat = |p: Vec3| Vec2::new(p.dot(u), p.dot(v));
|
||||
let cuts: Vec<Vec<(Vec2, Vec2)>> = parts
|
||||
.iter()
|
||||
.map(|p| p.segments.iter().map(|(a, b)| (flat(*a), flat(*b))).filter(|(a, b)| a.is_finite() && b.is_finite()).collect())
|
||||
.collect();
|
||||
let boxes: Vec<Option<(Vec2, Vec2)>> = cuts.iter().map(|c| bounds(c)).collect();
|
||||
let Some((lo, hi)) = boxes.iter().flatten().copied().reduce(|(a, b), (c, d)| (min2(a, c), max2(b, d))) else {
|
||||
return HashMap::new();
|
||||
};
|
||||
let extent = (hi - lo).len();
|
||||
if !(extent.is_finite() && extent > 0.0) {
|
||||
return HashMap::new();
|
||||
}
|
||||
let eps = extent * TOUCH;
|
||||
let mut pairs: HashSet<(usize, usize)> = HashSet::new();
|
||||
// Outlines closer than `eps`: through a grid of cells at least `eps` wide, each holding the
|
||||
// pieces of segments (no longer than a cell) whose box, grown by half of `eps`, reaches it.
|
||||
let cell = eps.max(extent / 1024.0);
|
||||
let mut grid: HashMap<(i64, i64), Vec<(usize, usize)>> = HashMap::new();
|
||||
let at = |x: f64, o: f64| ((x - o) / cell).floor() as i64;
|
||||
for (pi, cut) in cuts.iter().enumerate() {
|
||||
for (si, (a, b)) in cut.iter().enumerate() {
|
||||
let pieces = ((*b - *a).len() / cell).ceil().clamp(1.0, 4096.0) as usize;
|
||||
let mut last = None;
|
||||
for k in 0..pieces {
|
||||
let (p, q) = (a.lerp(*b, k as f64 / pieces as f64), a.lerp(*b, (k + 1) as f64 / pieces as f64));
|
||||
let (x0, x1) = (at(p.x.min(q.x) - eps / 2.0, lo.x), at(p.x.max(q.x) + eps / 2.0, lo.x));
|
||||
let (y0, y1) = (at(p.y.min(q.y) - eps / 2.0, lo.y), at(p.y.max(q.y) + eps / 2.0, lo.y));
|
||||
for x in x0..=x1 {
|
||||
for y in y0..=y1 {
|
||||
// Consecutive pieces share cells: the segment goes in each cell once.
|
||||
if last.is_some_and(|(lx0, lx1, ly0, ly1)| (lx0..=lx1).contains(&x) && (ly0..=ly1).contains(&y)) {
|
||||
continue;
|
||||
}
|
||||
grid.entry((x, y)).or_default().push((pi, si));
|
||||
}
|
||||
}
|
||||
last = Some((x0, x1, y0, y1));
|
||||
}
|
||||
}
|
||||
}
|
||||
for list in grid.values() {
|
||||
for (k, &(pa, sa)) in list.iter().enumerate() {
|
||||
for &(pb, sb) in list.iter().skip(k + 1) {
|
||||
let pair = (pa.min(pb), pa.max(pb));
|
||||
if pa == pb || pairs.contains(&pair) {
|
||||
continue;
|
||||
}
|
||||
let seg = |p: usize, s: usize| cuts.get(p).and_then(|c| c.get(s)).copied();
|
||||
if let (Some(x), Some(y)) = (seg(pa, sa), seg(pb, sb))
|
||||
&& segment_distance(x, y) <= eps
|
||||
{
|
||||
pairs.insert(pair);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Overlap without outlines that meet: one part's cut lies inside the other's material.
|
||||
let mut rows: HashMap<usize, Rows> = HashMap::new();
|
||||
for a in 0..cuts.len() {
|
||||
for b in a + 1..cuts.len() {
|
||||
if pairs.contains(&(a, b)) {
|
||||
continue;
|
||||
}
|
||||
let (Some(Some(ba)), Some(Some(bb))) = (boxes.get(a), boxes.get(b)) else { continue };
|
||||
if ba.1.x < bb.0.x || bb.1.x < ba.0.x || ba.1.y < bb.0.y || bb.1.y < ba.0.y {
|
||||
continue;
|
||||
}
|
||||
let inside = |outer: usize, inner: usize, rows: &mut HashMap<usize, Rows>| {
|
||||
let (Some(o), Some(Some(first))) = (cuts.get(outer), cuts.get(inner).map(|c| c.first())) else { return false };
|
||||
let r = rows.entry(outer).or_insert_with(|| Rows::new(o));
|
||||
r.contains(o, first.0.lerp(first.1, 0.5))
|
||||
};
|
||||
if inside(a, b, &mut rows) || inside(b, a, &mut rows) {
|
||||
pairs.insert((a, b));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut out: HashMap<usize, Vec<usize>> = HashMap::new();
|
||||
for (a, b) in pairs {
|
||||
out.entry(a).or_default().push(b);
|
||||
out.entry(b).or_default().push(a);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// A part's cut segments sorted into rows across the plane's second axis, so a point-in-material
|
||||
/// test looks at the segments of one row.
|
||||
struct Rows {
|
||||
lo: f64,
|
||||
step: f64,
|
||||
rows: Vec<Vec<usize>>,
|
||||
}
|
||||
|
||||
/// Rows a [`Rows`] makes, at most.
|
||||
const ROWS: usize = 256;
|
||||
|
||||
impl Rows {
|
||||
fn new(cut: &[(Vec2, Vec2)]) -> Rows {
|
||||
let (lo, hi) = cut.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(l, h), (a, b)| (l.min(a.y).min(b.y), h.max(a.y).max(b.y)));
|
||||
let count = (cut.len() / 16).clamp(1, ROWS);
|
||||
let step = if hi > lo { (hi - lo) / count as f64 } else { 1.0 };
|
||||
let mut rows = vec![Vec::new(); count];
|
||||
let row = |y: f64| (((y - lo) / step).floor().max(0.0) as usize).min(count - 1);
|
||||
for (i, (a, b)) in cut.iter().enumerate() {
|
||||
for r in rows.iter_mut().take(row(a.y.max(b.y)) + 1).skip(row(a.y.min(b.y))) {
|
||||
r.push(i);
|
||||
}
|
||||
}
|
||||
Rows { lo, step, rows }
|
||||
}
|
||||
|
||||
/// Whether `p` lies inside the material `cut` bounds: a ray along the first axis crosses its
|
||||
/// outline an odd number of times.
|
||||
fn contains(&self, cut: &[(Vec2, Vec2)], p: Vec2) -> bool {
|
||||
let r = ((p.y - self.lo) / self.step).floor();
|
||||
let Some(row) = (r >= 0.0).then_some(r as usize).and_then(|r| self.rows.get(r)) else { return false };
|
||||
let crossings = row
|
||||
.iter()
|
||||
.filter_map(|i| cut.get(*i))
|
||||
.filter(|(a, b)| (a.y > p.y) != (b.y > p.y) && a.x + (p.y - a.y) / (b.y - a.y) * (b.x - a.x) > p.x)
|
||||
.count();
|
||||
crossings % 2 == 1
|
||||
}
|
||||
}
|
||||
|
||||
fn min2(a: Vec2, b: Vec2) -> Vec2 {
|
||||
Vec2::new(a.x.min(b.x), a.y.min(b.y))
|
||||
}
|
||||
|
||||
fn max2(a: Vec2, b: Vec2) -> Vec2 {
|
||||
Vec2::new(a.x.max(b.x), a.y.max(b.y))
|
||||
}
|
||||
|
||||
fn bounds(cut: &[(Vec2, Vec2)]) -> Option<(Vec2, Vec2)> {
|
||||
cut.iter().map(|(a, b)| (min2(*a, *b), max2(*a, *b))).reduce(|(a, b), (c, d)| (min2(a, c), max2(b, d)))
|
||||
}
|
||||
|
||||
/// The distance between two segments: zero where they cross, else the nearest end to the other.
|
||||
fn segment_distance((a, b): (Vec2, Vec2), (c, d): (Vec2, Vec2)) -> f64 {
|
||||
let side = |p: Vec2, q: Vec2, r: Vec2| (q - p).cross(r - p);
|
||||
let (d1, d2, d3, d4) = (side(c, d, a), side(c, d, b), side(a, b, c), side(a, b, d));
|
||||
if ((d1 > 0.0 && d2 < 0.0) || (d1 < 0.0 && d2 > 0.0)) && ((d3 > 0.0 && d4 < 0.0) || (d3 < 0.0 && d4 > 0.0)) {
|
||||
return 0.0;
|
||||
}
|
||||
let to = |p: Vec2, s: Vec2, e: Vec2| {
|
||||
let se = e - s;
|
||||
let l2 = se.dot(se);
|
||||
let t = if l2 > 0.0 { ((p - s).dot(se) / l2).clamp(0.0, 1.0) } else { 0.0 };
|
||||
(p - (s + se * t)).len()
|
||||
};
|
||||
to(a, c, d).min(to(b, c, d)).min(to(c, a, b)).min(to(d, a, b))
|
||||
}
|
||||
@@ -12,6 +12,7 @@ pub mod clear_coat;
|
||||
mod curve_display;
|
||||
pub mod film;
|
||||
mod gif;
|
||||
pub mod hatch;
|
||||
pub mod job;
|
||||
pub mod lighting;
|
||||
pub mod mapping;
|
||||
@@ -41,4 +42,4 @@ pub use curve_display::{PixelCurveView, SampledPath, SamplingBudget, SamplingQua
|
||||
pub use gif::gif;
|
||||
pub use product::{FaceAppearance, ProductMesh, ProductScene};
|
||||
pub use raster::{Canvas, render, render_png, render_transparent};
|
||||
pub use scene::{MIN_ALPHA, Rgb, SECTION_CAP, SEE_THROUGH, Scene, SceneLine, SceneMesh, png};
|
||||
pub use scene::{MIN_ALPHA, Rgb, SECTION_CAP, SECTION_HATCH, SEE_THROUGH, Scene, SceneLine, SceneMesh, png};
|
||||
|
||||
@@ -118,6 +118,11 @@ pub fn render(scene: &Scene, cam: &Camera, w: usize, h: usize) -> Canvas {
|
||||
let alpha = sm.alpha();
|
||||
let m = &sm.mesh;
|
||||
let body = sm.color.f();
|
||||
// The hatch of its cut face: its own by part, else the cap's at 45 degrees.
|
||||
let (tint, turned) = match scene.part_hatch {
|
||||
Some(colours) => (colours.get(sm.hatch.colour()).map_or(cap_tint, |c| c.f()), sm.hatch.turned()),
|
||||
None => (cap_tint, false),
|
||||
};
|
||||
let pts: Vec<Option<[f64; 3]>> = m.positions.iter().map(|p| project(*p)).collect();
|
||||
// 1 / clip w per vertex, for perspective-correct interpolation of world positions (so
|
||||
// the section cuts along the plane, not a curve, in perspective views).
|
||||
@@ -194,15 +199,16 @@ pub fn render(scene: &Scene, cam: &Camera, w: usize, h: usize) -> Canvas {
|
||||
};
|
||||
}
|
||||
if cut {
|
||||
// The cut: the body's colour tinted toward the cap colour, hatched
|
||||
// at 45 degrees (as the viewport draws it); a plain cap is the
|
||||
// body's own colour.
|
||||
// The cut: the body's colour tinted toward its hatch colour, hatched
|
||||
// at 45 or 135 degrees (as the viewport draws it); a plain cap is
|
||||
// the body's own colour.
|
||||
if scene.plain_cap {
|
||||
c.put(x, y, base, 1.0);
|
||||
continue;
|
||||
}
|
||||
let tinted: [f64; 3] = std::array::from_fn(|k| base[k] + (cap_tint[k] - base[k]) * 0.35);
|
||||
let k = if ((x + y) / SS) % 9 < 2 { 0.6 } else { 1.0 };
|
||||
let tinted: [f64; 3] = std::array::from_fn(|k| base[k] + (tint[k] - base[k]) * 0.35);
|
||||
let along = if turned { x as i64 - y as i64 } else { (x + y) as i64 };
|
||||
let k = if along.div_euclid(SS as i64).rem_euclid(9) < 2 { 0.6 } else { 1.0 };
|
||||
c.put(x, y, tinted.map(|v| v * k), 1.0);
|
||||
continue;
|
||||
}
|
||||
@@ -424,7 +430,14 @@ mod tests {
|
||||
|
||||
/// A front panel over a back one, with a line between them, seen from the front.
|
||||
fn two_panels(front_opacity: f32) -> (Scene, Camera) {
|
||||
let glass = SceneMesh { mesh: Arc::new(panel(-5.0)), color: Rgb(255, 0, 0), face_colors: Vec::new(), opacity: front_opacity, closed: true };
|
||||
let glass = SceneMesh {
|
||||
mesh: Arc::new(panel(-5.0)),
|
||||
color: Rgb(255, 0, 0),
|
||||
face_colors: Vec::new(),
|
||||
opacity: front_opacity,
|
||||
closed: true,
|
||||
hatch: Default::default(),
|
||||
};
|
||||
let scene = Scene {
|
||||
meshes: vec![SceneMesh::solid(Arc::new(panel(5.0)), Rgb(0, 255, 0)), glass],
|
||||
lines: vec![SceneLine {
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::sync::Arc;
|
||||
use solidcraft_geom::{Mesh, Vec3};
|
||||
|
||||
use crate::SceneAnalysis;
|
||||
use crate::hatch::{HATCH_COLOURS, Hatch};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Rgb(pub u8, pub u8, pub u8);
|
||||
@@ -30,12 +31,14 @@ pub struct SceneMesh {
|
||||
/// The body encloses a volume (a solid, a closed mesh); an open one (a surface body, a solid
|
||||
/// with faces left out) has no inside, so a section gives it no cap.
|
||||
pub closed: bool,
|
||||
/// The hatch of its cut face when the scene hatches by part ([`Scene::part_hatch`]).
|
||||
pub hatch: Hatch,
|
||||
}
|
||||
|
||||
impl SceneMesh {
|
||||
/// A solid body in `color`, with no face of its own look.
|
||||
pub fn solid(mesh: Arc<Mesh>, color: Rgb) -> SceneMesh {
|
||||
SceneMesh { mesh, color, face_colors: Vec::new(), opacity: 1.0, closed: true }
|
||||
SceneMesh { mesh, color, face_colors: Vec::new(), opacity: 1.0, closed: true, hatch: Hatch(0) }
|
||||
}
|
||||
|
||||
/// Is the body drawn see-through? (Its opacity is below [`SEE_THROUGH`].)
|
||||
@@ -87,6 +90,9 @@ pub struct Scene {
|
||||
pub cap: Option<Rgb>,
|
||||
/// The section cap without its hatch: plain, in the body's own colour.
|
||||
pub plain_cap: bool,
|
||||
/// Each cut body hatched its own way ([`SceneMesh::hatch`]), in these colours (the first
|
||||
/// the cap's own); `None`: every cut face in [`Scene::cap`] at 45 degrees.
|
||||
pub part_hatch: Option<[Rgb; HATCH_COLOURS]>,
|
||||
/// A surface analysis shading the meshes (zebra, draft, curvature map, ...).
|
||||
pub analysis: Option<SceneAnalysis>,
|
||||
}
|
||||
@@ -94,6 +100,10 @@ pub struct Scene {
|
||||
/// The section cap's default tint, as in the viewport.
|
||||
pub const SECTION_CAP: Rgb = Rgb(214, 128, 72);
|
||||
|
||||
/// The section hatch's other colours by default, as in the viewport: a steel and a sage that tint
|
||||
/// a cut face about as strongly as the cap does, so the cut stays calm.
|
||||
pub const SECTION_HATCH: [Rgb; HATCH_COLOURS - 1] = [Rgb(84, 138, 168), Rgb(108, 180, 126)];
|
||||
|
||||
/// RGBA pixels (`w × h`) to PNG bytes.
|
||||
pub fn png(w: usize, h: usize, rgba: Vec<u8>) -> Option<Vec<u8>> {
|
||||
let img = image::RgbaImage::from_raw(u32::try_from(w).ok()?, u32::try_from(h).ok()?, rgba)?;
|
||||
|
||||
@@ -306,7 +306,11 @@ fn groups(t: &Tokens) -> Vec<Group> {
|
||||
],
|
||||
&[],
|
||||
),
|
||||
group("section", &[("section_cap", t.section_cap)], &[("body", t.body)]),
|
||||
group(
|
||||
"section",
|
||||
&[("section_cap", t.section_cap), ("section_hatch[0]", t.section_hatch[0]), ("section_hatch[1]", t.section_hatch[1])],
|
||||
&[("body", t.body)],
|
||||
),
|
||||
group("interference", &[("interference", t.interference)], &[("body", t.body)]),
|
||||
group(
|
||||
"continuity seams",
|
||||
|
||||
@@ -389,6 +389,9 @@ pub(crate) fn view_scene(app: &SolveApp, guides: solidcraft_engine::view::Guides
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
scene.plain_cap = !crate::viewport::section_hatch(app);
|
||||
if app.preferences.section_hatch == crate::prefs::SectionHatch::One {
|
||||
scene.part_hatch = None;
|
||||
}
|
||||
Ok(scene)
|
||||
}
|
||||
|
||||
|
||||
@@ -365,6 +365,7 @@ fn offscreen(
|
||||
ssaa,
|
||||
hover_tint: [0.0; 3],
|
||||
selection_tint: [0.0; 3],
|
||||
hatch: Default::default(),
|
||||
});
|
||||
let texture = |format, usage| {
|
||||
device.create_texture(&wgpu::TextureDescriptor {
|
||||
|
||||
@@ -332,6 +332,19 @@ pub enum HistorySketches {
|
||||
UnderFeature,
|
||||
}
|
||||
|
||||
/// How a section hatches the faces it cuts (Preferences: "Section hatch").
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SectionHatch {
|
||||
/// Each body in its own hatch (`solidcraft_render::hatch`): 45 or 135 degrees in one of the
|
||||
/// theme's hatch colours, touching bodies never alike. Also what a value this version doesn't
|
||||
/// know reads as (`lenient`).
|
||||
#[default]
|
||||
ByPart,
|
||||
/// Every cut face in the cap colour at 45 degrees (how it was before by part).
|
||||
One,
|
||||
}
|
||||
|
||||
/// What an orbit turns around (Preferences: "Orbit around").
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -586,6 +599,9 @@ preference_settings! {
|
||||
ambient_occlusion: bool = false, PAGE_GRAPHICS;
|
||||
/// Optional schematic markings on threaded cylindrical walls; off for new and older preferences.
|
||||
cosmetic_threads: bool = false, PAGE_GRAPHICS;
|
||||
/// A section hatches each cut body its own way (the default), or every one alike, in the cap
|
||||
/// colour at 45 degrees, as before.
|
||||
section_hatch: SectionHatch = SectionHatch::ByPart, PAGE_GRAPHICS;
|
||||
/// The grid turns to the upright origin plane facing a side view (#1470). Off by default: the
|
||||
/// grid stays on the ground plane (XY) whatever the view.
|
||||
grid_follows_view: bool = false => check("Grid follows the view", "Side views show the grid on the upright plane facing them instead of the ground");
|
||||
|
||||
@@ -154,6 +154,15 @@ pub(super) fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let response = ui_kit::checkbox(ui, app.tr("ui.cosmetic_thread_markings"), &mut app.preferences.cosmetic_threads)
|
||||
.on_hover_text(app.tr("ui.cosmetic_thread_markings_help"));
|
||||
crate::scenario::publish_handle("prefs:cosmetic_threads", response.rect.center());
|
||||
ui_kit::row(ui, app.tr("prefs.section_hatch"), |ui| {
|
||||
let styles = [crate::prefs::SectionHatch::One, crate::prefs::SectionHatch::ByPart];
|
||||
let mut selected = styles.iter().position(|s| *s == app.preferences.section_hatch).unwrap_or(1);
|
||||
if ui_kit::segmented(ui, &mut selected, &[app.tr("prefs.section_hatch_one"), app.tr("prefs.section_hatch_by_part")], Default::default())
|
||||
&& let Some(style) = styles.get(selected)
|
||||
{
|
||||
app.preferences.section_hatch = *style;
|
||||
}
|
||||
});
|
||||
ui.columns(2, |columns| {
|
||||
if let Some(left) = columns.get_mut(0) {
|
||||
ui_kit::checkbox(left, crate::i18n::tr_label("Grid"), &mut app.ui.show_grid);
|
||||
|
||||
@@ -1637,6 +1637,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
ssaa: if width < full.0 { width as f32 / full.0 as f32 } else { 1.0 },
|
||||
hover_tint: [0.0; 3],
|
||||
selection_tint: [0.0; 3],
|
||||
hatch: Default::default(),
|
||||
});
|
||||
ui.painter().add(egui_wgpu::Callback::new_paint_callback(rect, callback));
|
||||
Ok(())
|
||||
|
||||
@@ -290,6 +290,7 @@ pub(crate) fn view_frame(
|
||||
ssaa: 1.0,
|
||||
hover_tint: [f32::from(t.hover_face_lift) / 255.0; 3],
|
||||
selection_tint: [0.4, 0.6, 0.9],
|
||||
hatch: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -758,6 +758,9 @@ pub struct Tokens {
|
||||
pub chip: Color32,
|
||||
/// Inside of bodies cut by a section.
|
||||
pub section_cap: Color32,
|
||||
/// The section hatch's other colours by part, after `section_cap` (Preferences > Graphics >
|
||||
/// Section hatch): of its family and lightness, so a cut stays calm.
|
||||
pub section_hatch: [Color32; 2],
|
||||
/// Closed sketch profiles (translucent fill).
|
||||
pub profile_fill: Color32,
|
||||
/// Constraint glyph boxes.
|
||||
@@ -902,6 +905,7 @@ impl Tokens {
|
||||
sketch_point: view.sketch_point,
|
||||
sketch_point_free: view.sketch_point_free,
|
||||
section_cap: view.section_cap,
|
||||
section_hatch: view.section_hatch,
|
||||
profile_fill: view.profile_fill,
|
||||
glyph_bg: view.glyph_bg,
|
||||
glyph_edge: view.glyph_edge,
|
||||
|
||||
@@ -315,7 +315,7 @@ theme_keys! {
|
||||
app_bar_hover_button, app_bar_hover_strong, app_bar_tab_active,
|
||||
form_ink, form_dim, form_primary, form_error, form_error_fill, form_success, form_success_fill, dialog_body, dialog_footer, dialog_rule, control_fill, control_border, dialog_label, selected_ink, selected_bg,
|
||||
],
|
||||
many: [continuity, logo],
|
||||
many: [continuity, logo, section_hatch],
|
||||
icons: [highlight, agent, marker_halo],
|
||||
}
|
||||
|
||||
|
||||
@@ -451,6 +451,7 @@ fn scene_key(app: &SolveApp) -> u64 {
|
||||
app.preview.effective_replaced().hash(&mut h);
|
||||
app.preview.rolled_back.hash(&mut h);
|
||||
app.preferences.cosmetic_threads.hash(&mut h);
|
||||
app.preferences.section_hatch.hash(&mut h);
|
||||
crate::theme::is_dark().hash(&mut h);
|
||||
crate::theme::vision().hash(&mut h);
|
||||
// Two themes may share a light/dark base while drawing different geometry colours.
|
||||
@@ -1422,6 +1423,9 @@ pub(crate) fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
let mut kept = app.viewport.section_index.lock().map(|mut k| std::mem::take(&mut *k)).unwrap_or_default();
|
||||
let mut made = std::collections::HashMap::with_capacity(kept.len());
|
||||
let normal = [n.x.to_bits(), n.y.to_bits(), n.z.to_bits()];
|
||||
// The closed solid bodies the plane cuts and where it cuts them: their hatches by part.
|
||||
let by_part = app.preferences.section_hatch == crate::prefs::SectionHatch::ByPart;
|
||||
let mut cuts: Vec<(&str, Vec<(Vec3, Vec3)>)> = Vec::new();
|
||||
for b in st.bodies.iter().filter(|b| app.session.body_visible(&b.name)) {
|
||||
let mesh = b.mesh();
|
||||
let key = (std::sync::Arc::as_ptr(&mesh) as usize, normal);
|
||||
@@ -1429,10 +1433,28 @@ pub(crate) fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
Some((_, index)) => index,
|
||||
None => std::sync::Arc::new(solidcraft_engine::view::SectionIndex::new(&mesh, n)),
|
||||
};
|
||||
for (a, c) in index.segments(&mesh, o) {
|
||||
let segments = index.segments(&mesh, o);
|
||||
for (a, c) in &segments {
|
||||
sc.line(a.to_f32(), c.to_f32(), col, 1.8, false);
|
||||
}
|
||||
made.insert(key, (mesh, index));
|
||||
let solid = b.is_closed() && !body_faded(app, b, &active_bodies) && s.doc.model_body_opacity(b) >= 0.999;
|
||||
if by_part && solid && !segments.is_empty() {
|
||||
cuts.push((b.name.as_str(), segments));
|
||||
}
|
||||
}
|
||||
// Each cut body's hatch on all its palette entries (its colour, faces and placements).
|
||||
let parts: Vec<solidcraft_engine::render::hatch::CutPart> = cuts
|
||||
.iter()
|
||||
.map(|(name, segments)| solidcraft_engine::render::hatch::CutPart { seed: solidcraft_engine::render::hatch::seed(name), segments })
|
||||
.collect();
|
||||
let hatches = solidcraft_engine::render::hatch::hatches(&parts, n);
|
||||
let palette = std::sync::Arc::make_mut(&mut sc.palette);
|
||||
for ((name, _), hatch) in cuts.iter().zip(hatches) {
|
||||
let Some(slots) = arena.slots(name) else { continue };
|
||||
for slot in (0..slots.face_capacity as usize).map(|f| slots.face(f)).chain([slots.body, slots.instance]) {
|
||||
palette.set_hatch(slot, hatch);
|
||||
}
|
||||
}
|
||||
if let Ok(mut k) = app.viewport.section_index.lock() {
|
||||
*k = made;
|
||||
@@ -3935,6 +3957,12 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
let c = t.body_selected;
|
||||
[f32::from(c.r()) / 255.0, f32::from(c.g()) / 255.0, f32::from(c.b()) / 255.0]
|
||||
},
|
||||
// The section hatch colours by part: the cap's, then the theme's others.
|
||||
hatch: std::array::from_fn(|k| {
|
||||
let c = if k == 0 { t.section_cap } else { t.section_hatch.get(k - 1).copied().unwrap_or(t.section_cap) };
|
||||
let [r, g, b, _] = c.to_normalized_gamma_f32();
|
||||
[r, g, b]
|
||||
}),
|
||||
});
|
||||
painter.add(egui_wgpu::Callback::new_paint_callback(rect, cb));
|
||||
}
|
||||
@@ -5350,6 +5378,7 @@ pub(crate) fn scene_palette(dark: bool, mode: crate::theme::ColorVision) -> soli
|
||||
sketch_centerline: rgb(t.sketch_centerline),
|
||||
axes: t.axes().map(rgb),
|
||||
cap: rgb(t.section_cap),
|
||||
hatch: t.section_hatch.map(rgb),
|
||||
isocurve: rgb(t.accent),
|
||||
analysis: t.analysis,
|
||||
}
|
||||
@@ -7161,6 +7190,7 @@ mod tests {
|
||||
ssaa: 1.0,
|
||||
hover_tint: [0.0; 3],
|
||||
selection_tint: [0.0; 3],
|
||||
hatch: Default::default(),
|
||||
};
|
||||
let t0 = std::time::Instant::now();
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
|
||||
Reference in New Issue
Block a user