Accessibility Analysis: faces a tool coming along a direction reaches (facing it, nothing above: a depth map of the model seen from that direction, tested per pixel on the GPU) shade green, the rest red; the analyses (zebra, draft, curvature map, environment map, accessibility, minimum radius, isocurves, curvature comb, center of mass) also on SOLID INSPECT, and Interference on sketch INSPECT; path patterns follow 3D sketch curves through the same path chain as sweeps (path_segments_3d folded into path_points)
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
a7fc1fe532
commit
940c425316
10 files changed
+316
-65
No files matched your search
+2
-60
@@ -1181,8 +1181,8 @@ fn path_segments(ps: &SolvedSketch, ids: &[String], start: Vec3) -> Result<Vec<k
|
||||
return Err(DocError::Invalid("the path needs 1…1000 curves".into()));
|
||||
}
|
||||
// A path through 3D sketch curves is followed as a 3D polyline.
|
||||
if ids.iter().any(|id| ps.sketch.curve_index(id).is_none() && ps.sketch.wire_index(id).is_some()) {
|
||||
return path_segments_3d(ps, ids, start);
|
||||
if let Some(pts) = path_points(ps, ids, start)? {
|
||||
return Ok(pts.windows(2).map(|w| kernel::PathSeg::Line { a: w[0], b: w[1] }).collect());
|
||||
}
|
||||
let mut segs: Vec<solvecraft_geom::Seg2> = Vec::new();
|
||||
for id in ids {
|
||||
@@ -1230,64 +1230,6 @@ fn path_segments(ps: &SolvedSketch, ids: &[String], start: Vec3) -> Result<Vec<k
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// A chain of sketch curves and 3D sketch curves (wires) as line segments in world space,
|
||||
/// joined end to end, starting at the end nearest `start` (no start: the first curve runs toward
|
||||
/// the second).
|
||||
fn path_segments_3d(ps: &SolvedSketch, ids: &[String], start: Vec3) -> Result<Vec<kernel::PathSeg>> {
|
||||
let mut pieces: Vec<Vec<Vec3>> = Vec::new();
|
||||
for id in ids {
|
||||
let pts: Vec<Vec3> = if let Some(ci) = ps.sketch.curve_index(id) {
|
||||
let mut v: Vec<Vec3> = Vec::new();
|
||||
for s in ps.sketch.segs(ci) {
|
||||
for q in s.polyline(1e-2) {
|
||||
let w = ps.plane.to_world(q);
|
||||
if v.last().is_none_or(|l| l.dist(w) > 1e-12) {
|
||||
v.push(w);
|
||||
}
|
||||
}
|
||||
}
|
||||
v
|
||||
} else {
|
||||
let w = ps.sketch.wire_index(id).and_then(|i| ps.sketch.wires.get(i)).ok_or_else(|| DocError::Unknown(format!("path curve `{id}`")))?;
|
||||
w.pts.clone()
|
||||
};
|
||||
if pts.len() < 2 {
|
||||
return Err(DocError::Invalid(format!("`{id}` is not a curve")));
|
||||
}
|
||||
pieces.push(pts);
|
||||
}
|
||||
let ends = |p: &Vec<Vec3>| (p.first().copied().unwrap_or_default(), p.last().copied().unwrap_or_default());
|
||||
let tol = 1e-5;
|
||||
let flip_first = match (pieces.first().map(ends), pieces.get(1).map(ends)) {
|
||||
(Some((a, b)), _) if start.is_finite() => b.dist(start) < a.dist(start),
|
||||
(Some((a, _)), Some((c, d))) => c.dist(a) < tol || d.dist(a) < tol,
|
||||
_ => false,
|
||||
};
|
||||
if flip_first && let Some(f) = pieces.first_mut() {
|
||||
f.reverse();
|
||||
}
|
||||
for i in 1..pieces.len() {
|
||||
let prev_end = pieces.get(i - 1).map(|p| ends(p).1).unwrap_or_default();
|
||||
if let Some(p) = pieces.get_mut(i) {
|
||||
let (a, b) = ends(p);
|
||||
if b.dist(prev_end) < a.dist(prev_end) {
|
||||
p.reverse();
|
||||
}
|
||||
if ends(p).0.dist(prev_end) > tol {
|
||||
return Err(DocError::Invalid("the path curves are not connected end to end".into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
let segs: Vec<kernel::PathSeg> = pieces
|
||||
.iter()
|
||||
.flat_map(|p| p.windows(2).filter(|w| w[0].dist(w[1]) > 1e-9).map(|w| kernel::PathSeg::Line { a: w[0], b: w[1] }).collect::<Vec<_>>())
|
||||
.collect();
|
||||
if segs.len() > 1000 {
|
||||
return Err(DocError::Invalid("the path is too detailed (over 1000 segments)".into()));
|
||||
}
|
||||
Ok(segs)
|
||||
}
|
||||
|
||||
/// Operation and targets of a feature that makes tools.
|
||||
fn feature_op(f: &Feature) -> (Operation, Vec<String>) {
|
||||
match &f.kind {
|
||||
|
||||
@@ -24,6 +24,11 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.icon("environment_map")
|
||||
.noundo()
|
||||
.params("clear?: true turns the analysis off"),
|
||||
CommandSpec::new("FusionAccessibilityAnalysisCommand", "Accessibility Analysis", accessibility)
|
||||
.at("SKETCH", "INSPECT")
|
||||
.icon("accessibility")
|
||||
.noundo()
|
||||
.params("direction?: [x,y,z] the tool comes from (default Z, from above); clear?: true turns the analysis off"),
|
||||
CommandSpec::new("FusionCurvatureMapAnalysisCommand", "Curvature Map Analysis", curvature_map)
|
||||
.at("SKETCH", "INSPECT")
|
||||
.icon("curvature_map")
|
||||
@@ -62,6 +67,16 @@ fn draft(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
set(s, p, SurfaceAnalysis::Draft { pull, angle })
|
||||
}
|
||||
|
||||
fn accessibility(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let dir = match p.get("direction") {
|
||||
Some(v) => {
|
||||
vec3(v).and_then(Vec3::normalized).ok_or_else(|| bad("FusionAccessibilityAnalysisCommand", "`direction` must be a non-zero [x,y,z]"))?
|
||||
}
|
||||
None => Vec3::Z,
|
||||
};
|
||||
set(s, p, SurfaceAnalysis::Access { dir })
|
||||
}
|
||||
|
||||
fn curvature_map(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "FusionCurvatureMapAnalysisCommand";
|
||||
let radius = match num(p, "radius") {
|
||||
@@ -95,6 +110,8 @@ mod tests {
|
||||
assert!(matches!(s.analysis, Some(SurfaceAnalysis::Curvature { radius }) if (radius - 300f64.sqrt() / 10.0).abs() < 1e-6));
|
||||
s.execute("FusionEnvironmentMapAnalysisCommand", &json!({})).unwrap();
|
||||
assert_eq!(s.analysis, Some(SurfaceAnalysis::Environment));
|
||||
s.execute("FusionAccessibilityAnalysisCommand", &json!({"direction": [0, 0, -3]})).unwrap();
|
||||
assert_eq!(s.analysis, Some(SurfaceAnalysis::Access { dir: solvecraft_geom::Vec3::new(0.0, 0.0, -1.0) }));
|
||||
s.execute("FusionCurvatureMapAnalysisCommand", &json!({"clear": true})).unwrap();
|
||||
assert_eq!(s.analysis, None);
|
||||
assert_eq!(s.undo.len(), undo, "no undo steps");
|
||||
|
||||
@@ -178,6 +178,9 @@ pub enum SurfaceAnalysis {
|
||||
Curvature { radius: f64 },
|
||||
/// Mirror finish reflecting a studio (sky, horizon, floor, light panels).
|
||||
Environment,
|
||||
/// Green where a tool coming along `dir` reaches the face (facing it, nothing above), red
|
||||
/// elsewhere.
|
||||
Access { dir: Vec3 },
|
||||
}
|
||||
|
||||
const MAX_UNDO: usize = 200;
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
//! Accessibility Analysis: the model's height map seen from the access direction, for the GPU
|
||||
//! to tell reachable surface from surface under something else.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::sync::Arc;
|
||||
|
||||
use solvecraft_engine::SurfaceAnalysis;
|
||||
use solvecraft_engine::geom::Vec3;
|
||||
|
||||
use crate::SolveApp;
|
||||
use crate::gpu::AccessMap;
|
||||
|
||||
/// Map resolution (pixels per side).
|
||||
const SIZE: u32 = 1024;
|
||||
|
||||
thread_local! {
|
||||
/// The last map, by (model revision, direction).
|
||||
static CACHE: RefCell<Option<((u64, [u64; 3]), AccessMap)>> = const { RefCell::new(None) };
|
||||
static VERSION: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// The depth map for the active Accessibility Analysis, if any.
|
||||
pub fn map(app: &SolveApp) -> Option<AccessMap> {
|
||||
let Some(SurfaceAnalysis::Access { dir }) = app.session.analysis else { return None };
|
||||
let key = (app.session.revision, [dir.x.to_bits(), dir.y.to_bits(), dir.z.to_bits()]);
|
||||
if let Some(m) = CACHE.with(|c| c.borrow().as_ref().filter(|(k, _)| *k == key).map(|(_, m)| m.clone())) {
|
||||
return Some(m);
|
||||
}
|
||||
let st = app.session.world_state();
|
||||
let meshes: Vec<_> = st.bodies.iter().filter(|b| !app.ui.hidden_bodies.contains(&b.name)).map(|b| b.mesh()).collect();
|
||||
// Every new map gets a new version (the GPU uploads it once).
|
||||
let version = VERSION.with(|v| {
|
||||
v.set(v.get() + 1);
|
||||
v.get()
|
||||
});
|
||||
let m = build(&meshes.iter().map(|m| m.as_ref()).collect::<Vec<_>>(), dir, version)?;
|
||||
CACHE.with(|c| *c.borrow_mut() = Some((key, m.clone())));
|
||||
Some(m)
|
||||
}
|
||||
|
||||
/// Rasterise the triangles seen along `dir` (orthographic), keeping the highest point per pixel.
|
||||
fn build(meshes: &[&solvecraft_engine::geom::Mesh], dir: Vec3, version: u64) -> Option<AccessMap> {
|
||||
let d = dir.normalized()?;
|
||||
let u = d.cross(if d.x.abs() < 0.9 { Vec3::X } else { Vec3::Y }).normalized()?;
|
||||
let v = d.cross(u);
|
||||
let (mut lo, mut hi) = ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]);
|
||||
for m in meshes {
|
||||
for p in &m.positions {
|
||||
let (a, b) = (p.dot(u), p.dot(v));
|
||||
lo = [lo[0].min(a), lo[1].min(b)];
|
||||
hi = [hi[0].max(a), hi[1].max(b)];
|
||||
}
|
||||
}
|
||||
let ext = (hi[0] - lo[0]).max(hi[1] - lo[1]);
|
||||
if !(ext.is_finite() && ext > 1e-9) {
|
||||
return None;
|
||||
}
|
||||
let n = SIZE as usize;
|
||||
let scale = (SIZE - 2) as f64 / ext;
|
||||
// One pixel of margin on each side.
|
||||
let (ou, ov) = (-lo[0] + 1.0 / scale, -lo[1] + 1.0 / scale);
|
||||
let mut depth = vec![f32::NEG_INFINITY; n * n];
|
||||
for m in meshes {
|
||||
for t in &m.triangles {
|
||||
let q: Vec<(f64, f64, f64)> =
|
||||
t.iter().filter_map(|i| m.positions.get(*i as usize)).map(|p| ((p.dot(u) + ou) * scale, (p.dot(v) + ov) * scale, p.dot(d))).collect();
|
||||
let [a, b, c] = q[..] else { continue };
|
||||
let area = (b.0 - a.0) * (c.1 - a.1) - (b.1 - a.1) * (c.0 - a.0);
|
||||
let (x0, x1) = (a.0.min(b.0).min(c.0).floor().max(0.0) as usize, (a.0.max(b.0).max(c.0).ceil() as usize).min(n - 1));
|
||||
let (y0, y1) = (a.1.min(b.1).min(c.1).floor().max(0.0) as usize, (a.1.max(b.1).max(c.1).ceil() as usize).min(n - 1));
|
||||
// Edge-on triangles: their vertices still count.
|
||||
if area.abs() < 1e-12 {
|
||||
for p in [a, b, c] {
|
||||
let (x, y) = (p.0 as usize, p.1 as usize);
|
||||
if let Some(z) = depth.get_mut(y * n + x) {
|
||||
*z = z.max(p.2 as f32);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
for y in y0..=y1 {
|
||||
for x in x0..=x1 {
|
||||
let (px, py) = (x as f64 + 0.5, y as f64 + 0.5);
|
||||
let w0 = ((b.0 - px) * (c.1 - py) - (b.1 - py) * (c.0 - px)) / area;
|
||||
let w1 = ((c.0 - px) * (a.1 - py) - (c.1 - py) * (a.0 - px)) / area;
|
||||
let w2 = 1.0 - w0 - w1;
|
||||
if w0 < -1e-9 || w1 < -1e-9 || w2 < -1e-9 {
|
||||
continue;
|
||||
}
|
||||
let z = (w0 * a.2 + w1 * b.2 + w2 * c.2) as f32;
|
||||
if let Some(cell) = depth.get_mut(y * n + x) {
|
||||
*cell = cell.max(z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(AccessMap {
|
||||
version,
|
||||
size: SIZE,
|
||||
depth: Arc::new(depth),
|
||||
u: [u.x as f32, u.y as f32, u.z as f32, ou as f32],
|
||||
v: [v.x as f32, v.y as f32, v.z as f32, ov as f32],
|
||||
scale: scale as f32,
|
||||
})
|
||||
}
|
||||
+134
-4
@@ -11,7 +11,7 @@ use egui_wgpu::wgpu::util::DeviceExt;
|
||||
pub const TRI_SIZE: usize = 28;
|
||||
/// Line instance: a, b (3 × f32 each), colour (4 × u8), width in pixels (f32).
|
||||
pub const LINE_SIZE: usize = 32;
|
||||
const UNIFORM_SIZE: u64 = 160;
|
||||
const UNIFORM_SIZE: u64 = 192;
|
||||
|
||||
/// CPU-side geometry waiting to be uploaded. The model scene changes with the design; the
|
||||
/// highlight scene (hover, selection, the origin widget) changes often and is small.
|
||||
@@ -120,9 +120,25 @@ pub struct ViewportCallback {
|
||||
pub cap: [f32; 4],
|
||||
/// Canvases and decals.
|
||||
pub images: Vec<GpuImage>,
|
||||
/// Surface analysis: (mode: 0 none, 1 zebra, 2 draft, 3 curvature; parameter; 0; 0) and
|
||||
/// the draft pull direction.
|
||||
/// Surface analysis: (mode: 0 none, 1 zebra, 2 draft, 3 curvature, 4 environment,
|
||||
/// 5 accessibility; parameter; scale; size) and the pull / access direction.
|
||||
pub analysis: [f32; 8],
|
||||
/// Accessibility: the depth map seen from the access direction.
|
||||
pub access: Option<AccessMap>,
|
||||
}
|
||||
|
||||
/// Heights of the model seen from a direction (an orthographic depth map): a point is reachable
|
||||
/// from that direction when nothing lies above it.
|
||||
#[derive(Clone)]
|
||||
pub struct AccessMap {
|
||||
pub version: u64,
|
||||
pub size: u32,
|
||||
/// Row-major heights along the direction (−∞ where nothing is).
|
||||
pub depth: Arc<Vec<f32>>,
|
||||
/// Map axes: pixel = (p·u + u.w, p·v + v.w) × scale.
|
||||
pub u: [f32; 4],
|
||||
pub v: [f32; 4],
|
||||
pub scale: f32,
|
||||
}
|
||||
|
||||
struct Batch {
|
||||
@@ -182,6 +198,41 @@ struct Resources {
|
||||
textures: std::collections::HashMap<u64, (u64, wgpu::BindGroup)>,
|
||||
/// This frame's quads: (image id, vertices).
|
||||
quads: Vec<(u64, Batch)>,
|
||||
access_bgl: wgpu::BindGroupLayout,
|
||||
/// The accessibility depth map (a 1 × 1 stand-in when there is none) and its version.
|
||||
access_bind: wgpu::BindGroup,
|
||||
access_version: u64,
|
||||
}
|
||||
|
||||
fn access_texture(device: &wgpu::Device, queue: Option<&wgpu::Queue>, bgl: &wgpu::BindGroupLayout, size: u32, depth: &[f32]) -> wgpu::BindGroup {
|
||||
let extent = wgpu::Extent3d { width: size.max(1), height: size.max(1), depth_or_array_layers: 1 };
|
||||
let tex = device.create_texture(&wgpu::TextureDescriptor {
|
||||
label: Some("sc_access"),
|
||||
size: extent,
|
||||
mip_level_count: 1,
|
||||
sample_count: 1,
|
||||
dimension: wgpu::TextureDimension::D2,
|
||||
format: wgpu::TextureFormat::R32Float,
|
||||
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
|
||||
view_formats: &[],
|
||||
});
|
||||
if let Some(q) = queue
|
||||
&& depth.len() == (size as usize) * (size as usize)
|
||||
{
|
||||
let bytes: Vec<u8> = depth.iter().flat_map(|x| x.to_le_bytes()).collect();
|
||||
q.write_texture(
|
||||
wgpu::TexelCopyTextureInfo { texture: &tex, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All },
|
||||
&bytes,
|
||||
wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(4 * size), rows_per_image: Some(size) },
|
||||
extent,
|
||||
);
|
||||
}
|
||||
let view = tex.create_view(&Default::default());
|
||||
device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("sc_access"),
|
||||
layout: bgl,
|
||||
entries: &[wgpu::BindGroupEntry { binding: 2, resource: wgpu::BindingResource::TextureView(&view) }],
|
||||
})
|
||||
}
|
||||
|
||||
const SHADER: &str = r#"
|
||||
@@ -193,8 +244,12 @@ struct U {
|
||||
cap: vec4<f32>, // colour of the inside of cut bodies
|
||||
ana: vec4<f32>, // surface analysis: mode (1 zebra, 2 draft, 3 curvature), parameter
|
||||
pull: vec4<f32>, // draft pull direction
|
||||
acc_u: vec4<f32>, // accessibility map axes (xyz, offset)
|
||||
acc_v: vec4<f32>,
|
||||
};
|
||||
|
||||
@group(1) @binding(2) var acc_t: texture_2d<f32>;
|
||||
|
||||
fn clipped(p: vec3<f32>) -> bool {
|
||||
return length(u.clip.xyz) > 0.5 && dot(u.clip.xyz, p) > u.clip.w;
|
||||
}
|
||||
@@ -257,6 +312,24 @@ fn analysis(i: TOut) -> vec4<f32> {
|
||||
let c = select(0.08, 0.95, s > 0.0);
|
||||
return out_color(vec4<f32>(vec3<f32>(c), 1.0));
|
||||
}
|
||||
if (u.ana.x > 4.5) {
|
||||
// Reachable from the access direction: facing it and nothing above.
|
||||
let d = normalize(u.pull.xyz);
|
||||
let size = i32(u.ana.w);
|
||||
let px = vec2<i32>(floor(vec2<f32>(dot(i.wp, u.acc_u.xyz) + u.acc_u.w, dot(i.wp, u.acc_v.xyz) + u.acc_v.w) * u.ana.z));
|
||||
var top = -1e30;
|
||||
if (px.x >= 0 && px.y >= 0 && px.x < size && px.y < size) {
|
||||
top = textureLoad(acc_t, px, 0).x;
|
||||
}
|
||||
let cn = dot(normalize(i.n), d);
|
||||
let facing = cn > 0.02;
|
||||
// Slack of a pixel and a half, more on slopes (the map holds the pixel's highest point).
|
||||
let slack = u.ana.y * (1.0 + sqrt(max(1.0 - cn * cn, 0.0)) / max(cn, 0.05));
|
||||
let clear = dot(i.wp, d) >= top - slack;
|
||||
var c = vec3<f32>(0.85, 0.22, 0.20);
|
||||
if (facing && clear) { c = vec3<f32>(0.20, 0.75, 0.30); }
|
||||
return out_color(vec4<f32>(c * lit, 1.0));
|
||||
}
|
||||
if (u.ana.x > 3.5) {
|
||||
// A studio around the model: sky above, a dark floor below, a bright horizon band and
|
||||
// two soft light panels, seen in a mirror.
|
||||
@@ -528,6 +601,46 @@ impl Resources {
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
let access_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("sc_access"),
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 2,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Texture {
|
||||
sample_type: wgpu::TextureSampleType::Float { filterable: false },
|
||||
view_dimension: wgpu::TextureViewDimension::D2,
|
||||
multisampled: false,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
});
|
||||
let solid_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("sc_solid"),
|
||||
bind_group_layouts: &[Some(&bgl), Some(&access_bgl)],
|
||||
immediate_size: 0,
|
||||
});
|
||||
let solid = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("sc_tris"),
|
||||
layout: Some(&solid_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module: &module,
|
||||
entry_point: Some("vs_tri"),
|
||||
buffers: &[Some(tri_layout())],
|
||||
compilation_options: Default::default(),
|
||||
},
|
||||
primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
|
||||
depth_stencil: depth(wgpu::CompareFunction::Less, true),
|
||||
multisample: wgpu::MultisampleState { count: t.samples.max(1), mask: !0, alpha_to_coverage_enabled: false },
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module: &module,
|
||||
entry_point: Some("fs_solid"),
|
||||
targets: &[Some(wgpu::ColorTargetState { format: t.format, blend: None, write_mask: wgpu::ColorWrites::ALL })],
|
||||
compilation_options: Default::default(),
|
||||
}),
|
||||
multiview_mask: None,
|
||||
cache: None,
|
||||
});
|
||||
let access_bind = access_texture(device, None, &access_bgl, 1, &[]);
|
||||
let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
|
||||
label: Some("sc_image"),
|
||||
mag_filter: wgpu::FilterMode::Linear,
|
||||
@@ -540,7 +653,10 @@ impl Resources {
|
||||
sampler,
|
||||
textures: Default::default(),
|
||||
quads: Vec::new(),
|
||||
tri: pipeline("sc_tris", "vs_tri", "fs_solid", tri_layout(), depth(wgpu::CompareFunction::Less, true), None),
|
||||
tri: solid,
|
||||
access_bgl,
|
||||
access_bind,
|
||||
access_version: 0,
|
||||
tri_hl: pipeline("sc_tris_hl", "vs_tri", "fs_tri", tri_layout(), depth(wgpu::CompareFunction::LessEqual, false), alpha),
|
||||
trans: pipeline("sc_trans", "vs_tri", "fs_tri", tri_layout(), depth(wgpu::CompareFunction::LessEqual, false), alpha),
|
||||
ghost: pipeline("sc_ghost", "vs_ghost", "fs_tri", tri_layout(), depth(wgpu::CompareFunction::LessEqual, false), alpha),
|
||||
@@ -593,6 +709,12 @@ impl egui_wgpu::CallbackTrait for ViewportCallback {
|
||||
res.highlight.take(device, self.hl_key, &self.hl_slot);
|
||||
res.preview.take(device, self.pv_key, &self.pv_slot);
|
||||
queue.write_buffer(&res.uniform, 0, &uniform_bytes(self, self.size_px[0], self.size_px[1], res.linear_out));
|
||||
if let Some(a) = &self.access
|
||||
&& a.version != res.access_version
|
||||
{
|
||||
res.access_bind = access_texture(device, Some(queue), &res.access_bgl, a.size, &a.depth);
|
||||
res.access_version = a.version;
|
||||
}
|
||||
// Images: upload new or changed textures, drop removed ones, rebuild the quads.
|
||||
res.textures.retain(|id, _| self.images.iter().any(|i| i.id == *id));
|
||||
res.quads.clear();
|
||||
@@ -669,6 +791,7 @@ impl egui_wgpu::CallbackTrait for ViewportCallback {
|
||||
};
|
||||
let (m, h, pv) = (&res.model, &res.highlight, &res.preview);
|
||||
lines(pass, &res.overlay, &m.under);
|
||||
pass.set_bind_group(1, &res.access_bind, &[]);
|
||||
tris(pass, &res.tri, &m.tris);
|
||||
tris(pass, &res.tri, &pv.tris);
|
||||
tris(pass, &res.tri_hl, &h.tris);
|
||||
@@ -700,6 +823,13 @@ pub fn uniform_bytes(cb: &ViewportCallback, w: f32, h: f32, linear: bool) -> Vec
|
||||
v.extend(cb.clip);
|
||||
v.extend(cb.cap);
|
||||
v.extend(cb.analysis);
|
||||
match &cb.access {
|
||||
Some(a) => {
|
||||
v.extend(a.u);
|
||||
v.extend(a.v);
|
||||
}
|
||||
None => v.extend([0.0; 8]),
|
||||
}
|
||||
v.iter().flat_map(|x| x.to_le_bytes()).collect()
|
||||
}
|
||||
|
||||
|
||||
@@ -652,6 +652,13 @@ pub fn paint(p: &Painter, r: Rect, name: &str, ink: Color32, fill: Color32, acce
|
||||
pen.line(&[(3.0 + 12.0 * t, 17.0 + 2.0 * t), (9.0 + 12.0 * t, 5.0 + 2.0 * t)], a);
|
||||
}
|
||||
}
|
||||
"accessibility" => {
|
||||
pen.poly(&[(2.0, 20.0), (22.0, 20.0), (22.0, 15.0), (2.0, 15.0)], Color32::from_rgb(70, 180, 90), ink);
|
||||
pen.poly(&[(8.0, 15.0), (16.0, 15.0), (16.0, 17.5), (8.0, 17.5)], Color32::from_rgb(220, 70, 60), Color32::TRANSPARENT);
|
||||
pen.poly(&[(6.0, 10.0), (18.0, 10.0), (18.0, 12.5), (6.0, 12.5)], pen.fill, ink);
|
||||
pen.arrow((4.0, 2.0), (4.0, 13.0), a);
|
||||
pen.arrow((20.0, 2.0), (20.0, 13.0), a);
|
||||
}
|
||||
"environment_map" => {
|
||||
let pts: Vec<Pos2> =
|
||||
(0..32).map(|i| i as f32 / 32.0 * std::f32::consts::TAU).map(|t| pen.at(12.0 + 9.0 * t.cos(), 12.0 + 9.0 * t.sin())).collect();
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod access;
|
||||
pub mod browser;
|
||||
pub mod canvas;
|
||||
pub mod context_menu;
|
||||
|
||||
@@ -40,6 +40,40 @@ thread_local! {
|
||||
static OPACITY: Cell<Option<(u64, f64)>> = const { Cell::new(None) };
|
||||
}
|
||||
|
||||
/// Commands registered on one panel that Fusion also lists on another: the analyses (sketch
|
||||
/// INSPECT) on SOLID INSPECT, and Interference (SOLID INSPECT) on sketch INSPECT.
|
||||
const ALSO_IN: &[(&str, &str, &str)] = &[
|
||||
("SOLID", "INSPECT", "FusionZebraAnalysisCommand"),
|
||||
("SOLID", "INSPECT", "FusionDraftAnalysisCommand"),
|
||||
("SOLID", "INSPECT", "FusionCurvatureMapAnalysisCommand"),
|
||||
("SOLID", "INSPECT", "FusionEnvironmentMapAnalysisCommand"),
|
||||
("SOLID", "INSPECT", "FusionAccessibilityAnalysisCommand"),
|
||||
("SOLID", "INSPECT", "FusionMinimumRadiusAnalysisCommand"),
|
||||
("SOLID", "INSPECT", "FusionIsoCurveAnalysisCommand"),
|
||||
("SOLID", "INSPECT", "FusionCurvatureCombAnalysisCommand"),
|
||||
("SOLID", "INSPECT", "FusionCenterOfMassCommand"),
|
||||
("SKETCH", "INSPECT", "InterferenceCheckCommand"),
|
||||
];
|
||||
|
||||
/// A panel's commands plus the ones it shares with another panel (see [`ALSO_IN`]).
|
||||
pub fn also_in<'a>(
|
||||
tab: &str,
|
||||
panel: &str,
|
||||
mut cmds: Vec<&'a solvecraft_engine::CommandSpec>,
|
||||
specs: &[&'a solvecraft_engine::CommandSpec],
|
||||
) -> Vec<&'a solvecraft_engine::CommandSpec> {
|
||||
for (t, p, id) in ALSO_IN {
|
||||
if *t == tab
|
||||
&& *p == panel
|
||||
&& !cmds.iter().any(|c| c.id == *id)
|
||||
&& let Some(c) = specs.iter().find(|c| c.id == *id)
|
||||
{
|
||||
cmds.push(c);
|
||||
}
|
||||
}
|
||||
cmds
|
||||
}
|
||||
|
||||
/// Run an analysis and keep its result as a marker.
|
||||
fn analysis(app: &mut SolveApp, cmd: &str, p: Value) {
|
||||
let Ok(v) = app.run(cmd, p) else { return };
|
||||
@@ -102,8 +136,13 @@ pub fn start_hook(app: &mut SolveApp, id: &str) -> bool {
|
||||
("FusionDraftAnalysisCommand", Some(A::Draft { .. })) => Some(true),
|
||||
("FusionCurvatureMapAnalysisCommand", Some(A::Curvature { .. })) => Some(true),
|
||||
("FusionEnvironmentMapAnalysisCommand", Some(A::Environment)) => Some(true),
|
||||
("FusionAccessibilityAnalysisCommand", Some(A::Access { .. })) => Some(true),
|
||||
(
|
||||
"FusionZebraAnalysisCommand" | "FusionDraftAnalysisCommand" | "FusionCurvatureMapAnalysisCommand" | "FusionEnvironmentMapAnalysisCommand",
|
||||
"FusionZebraAnalysisCommand"
|
||||
| "FusionDraftAnalysisCommand"
|
||||
| "FusionCurvatureMapAnalysisCommand"
|
||||
| "FusionEnvironmentMapAnalysisCommand"
|
||||
| "FusionAccessibilityAnalysisCommand",
|
||||
_,
|
||||
) => Some(false),
|
||||
_ => None,
|
||||
|
||||
@@ -272,6 +272,7 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
Some(l) => l,
|
||||
None => (specs.iter().filter(|c| c.tab == tab && c.panel == *panel).copied().collect(), promoted(&tab, panel)),
|
||||
};
|
||||
let cmds = crate::sketch_tools::also_in(&tab, panel, cmds, &specs);
|
||||
let n = promote.min(cmds.len()).max(if cmds.is_empty() { 1 } else { 0 });
|
||||
let width = (n as f32 * 40.0).max(64.0) + 8.0;
|
||||
let enabled_panel = !cmds.is_empty() || *panel == "SELECT";
|
||||
|
||||
@@ -654,6 +654,10 @@ fn analysis_uniform(app: &SolveApp) -> [f32; 8] {
|
||||
Some(A::Draft { pull, angle }) => [2.0, angle as f32, 0.0, 0.0, pull.x as f32, pull.y as f32, pull.z as f32, 0.0],
|
||||
Some(A::Curvature { radius }) => [3.0, radius as f32, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0],
|
||||
Some(A::Environment) => [4.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0],
|
||||
Some(A::Access { dir }) => {
|
||||
let (scale, size, eps) = crate::access::map(app).map(|m| (m.scale, m.size as f32, 1.5 / m.scale.max(1e-9))).unwrap_or((0.0, 0.0, 0.0));
|
||||
[5.0, eps, scale, size, dir.x as f32, dir.y as f32, dir.z as f32, 0.0]
|
||||
}
|
||||
None => [0.0; 8],
|
||||
}
|
||||
}
|
||||
@@ -1139,6 +1143,7 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
size_px: [rect.width() * ppp, rect.height() * ppp],
|
||||
images: crate::ref_images::gpu_images(app),
|
||||
analysis: analysis_uniform(app),
|
||||
access: crate::access::map(app),
|
||||
};
|
||||
painter.add(egui_wgpu::Callback::new_paint_callback(rect, cb));
|
||||
} else {
|
||||
|
||||
Reference in new issue
Block a user