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:
bherbruckandClaude Opus 5.5 committed 2026-10-08 10:36:53 +00:00
1 parent a7fc1fe532
commit 940c425316
10 files changed
+316 -65

No files matched your search

+2 -60
View File
@@ -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 {
+17
View File
@@ -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");
+3
View File
@@ -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;
+106
View File
@@ -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
View File
@@ -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()
}
+7
View File
@@ -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();
+1
View File
@@ -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 -1
View File
@@ -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,
+1
View File
@@ -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";
+5
View File
@@ -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 {