Section Analysis (FusionHalfSectionViewCommand, INSPECT): cut the view by an origin plane, construction plane or planar face with an offset (drag arrow) and flip; the inside of cut bodies shows a flat cap colour; live while the dialog is open, Remove Section on the right-click menu; viewport shader validated in a test
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
24627d4f0a
commit
1988594a13
11 files changed
+206
-6
No files matched your search
@@ -12,6 +12,7 @@ pub use features::auto_operation;
|
||||
mod file;
|
||||
mod inspect;
|
||||
mod measure_sel;
|
||||
mod section;
|
||||
pub(crate) use measure_sel::measure_items;
|
||||
mod parameters;
|
||||
mod sketch;
|
||||
@@ -129,6 +130,7 @@ pub fn command_specs() -> Vec<&'static CommandSpec> {
|
||||
v.extend(parameters::COMMANDS.iter());
|
||||
v.extend(file::COMMANDS.iter());
|
||||
v.extend(inspect::COMMANDS.iter());
|
||||
v.extend(section::COMMANDS.iter());
|
||||
v
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
//! Section Analysis: a view-only cut through the model by a plane (what is on the plane's
|
||||
//! positive side is hidden). It changes no geometry and is not an undo step.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solvecraft_geom::{Plane, Vec3};
|
||||
|
||||
use super::CommandSpec;
|
||||
use crate::params::{bad, bool_, expr, vec3};
|
||||
use crate::{Result, Session};
|
||||
|
||||
pub static COMMANDS: &[CommandSpec] = &[CommandSpec::new("FusionHalfSectionViewCommand", "Section Analysis", section)
|
||||
.at("SOLID", "INSPECT")
|
||||
.icon("section")
|
||||
.noundo()
|
||||
.params("plane: XY|XZ|YZ | {origin, normal}; offset?: expr along the normal; flip?: bool; or clear: true")];
|
||||
|
||||
fn section(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "FusionHalfSectionViewCommand";
|
||||
if bool_(p, "clear").unwrap_or(false) {
|
||||
s.section = None;
|
||||
s.revision += 1;
|
||||
return Ok(json!({"section": null}));
|
||||
}
|
||||
let (origin, normal) = match p.get("plane") {
|
||||
Some(Value::String(n)) => {
|
||||
let pl = Plane::named(n).ok_or_else(|| bad(cmd, format!("unknown plane `{n}` (XY, XZ, YZ)")))?;
|
||||
(pl.origin, pl.normal())
|
||||
}
|
||||
Some(o @ Value::Object(_)) => {
|
||||
let origin = o.get("origin").and_then(vec3).unwrap_or(Vec3::ZERO);
|
||||
let n = o.get("normal").and_then(vec3).and_then(|n| n.normalized()).ok_or_else(|| bad(cmd, "the plane needs a non-zero `normal`"))?;
|
||||
(origin, n)
|
||||
}
|
||||
_ => return Err(bad(cmd, "`plane` must be XY, XZ, YZ or {origin, normal}")),
|
||||
};
|
||||
let offset = match expr(p, "offset") {
|
||||
Some(e) => s.doc.eval(&e, solvecraft_doc::expr::Kind::Length).map_err(|e| bad(cmd, format!("offset: {e}")))?,
|
||||
None => 0.0,
|
||||
};
|
||||
if !origin.is_finite() || !offset.is_finite() {
|
||||
return Err(bad(cmd, "the plane must be finite"));
|
||||
}
|
||||
let normal = if bool_(p, "flip").unwrap_or(false) { -normal } else { normal };
|
||||
let origin = origin + normal * offset * if bool_(p, "flip").unwrap_or(false) { -1.0 } else { 1.0 };
|
||||
s.section = Some((origin, normal));
|
||||
s.revision += 1;
|
||||
Ok(json!({"section": {"origin": origin, "normal": normal}}))
|
||||
}
|
||||
@@ -156,6 +156,8 @@ pub struct Session {
|
||||
world_cache: std::sync::Mutex<Option<(u64, Arc<solvecraft_doc::ModelState>)>>,
|
||||
/// Features copied with `timeline.copy` (pasted by `timeline.paste`).
|
||||
pub clipboard: Vec<solvecraft_doc::Feature>,
|
||||
/// Section Analysis: a view cut by a plane (origin, unit normal; the normal side is hidden).
|
||||
pub section: Option<(Vec3, Vec3)>,
|
||||
}
|
||||
|
||||
const MAX_UNDO: usize = 200;
|
||||
@@ -188,6 +190,7 @@ impl Session {
|
||||
pending_moves: Default::default(),
|
||||
world_cache: Default::default(),
|
||||
clipboard: Vec::new(),
|
||||
section: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,6 +317,7 @@ impl Session {
|
||||
pending_moves: self.pending_moves.clone(),
|
||||
world_cache: Default::default(),
|
||||
clipboard: self.clipboard.clone(),
|
||||
section: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -626,6 +626,25 @@ fn measure_items() {
|
||||
assert!(s.execute("MeasureCommand", &json!({"items": [{"type": "nonsense"}]})).is_err());
|
||||
}
|
||||
|
||||
/// Section Analysis sets and clears the view cut without touching the design or history.
|
||||
#[test]
|
||||
fn section_analysis() {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "PrimitiveBox", json!({"length": 40, "width": 30, "height": 20}));
|
||||
let (doc, undo) = (s.doc.clone(), s.undo.len());
|
||||
run(&mut s, "FusionHalfSectionViewCommand", json!({"plane": "XZ", "offset": 15}));
|
||||
let (o, n) = s.section.unwrap();
|
||||
assert!((o - n * 15.0).len() < 1e-12 && (n.y.abs() - 1.0).abs() < 1e-12);
|
||||
run(&mut s, "FusionHalfSectionViewCommand", json!({"plane": {"origin": [0, 0, 5], "normal": [0, 0, 2]}, "flip": true}));
|
||||
let (o, n) = s.section.unwrap();
|
||||
assert!((o.z - 5.0).abs() < 1e-12 && (n.z + 1.0).abs() < 1e-12);
|
||||
assert!(s.execute("FusionHalfSectionViewCommand", &json!({"plane": {"normal": [0, 0, 0]}})).is_err());
|
||||
run(&mut s, "FusionHalfSectionViewCommand", json!({"clear": true}));
|
||||
assert!(s.section.is_none());
|
||||
assert!(Arc::ptr_eq(&doc, &s.doc));
|
||||
assert_eq!(s.undo.len(), undo);
|
||||
}
|
||||
|
||||
/// Press Pull: positive pulls a face out, negative pushes it in (a cut), edges get a fillet.
|
||||
#[test]
|
||||
fn press_pull() {
|
||||
|
||||
@@ -30,10 +30,14 @@ pub fn anchor(app: &SolveApp, d: &Dialog) -> Option<(Vec3, Option<Vec3>)> {
|
||||
Sel::Edge { body, index, point } => (*point, st.body(body).and_then(|b| edge_bisector(&b.mesh(), *index, *point))),
|
||||
Sel::Vertex { point, .. } => (*point, None),
|
||||
Sel::Body { name } => (st.body(name)?.mesh().bounds().center(), None),
|
||||
Sel::Plane { name } => {
|
||||
let pl = solvecraft_engine::geom::Plane::named(name)?;
|
||||
(pl.origin, Some(pl.normal()))
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
Some(match d.kind {
|
||||
Kind::Extrude { .. } | Kind::Fillet { .. } => (at, normal),
|
||||
Kind::Extrude { .. } | Kind::Fillet { .. } | Kind::Section { .. } => (at, normal),
|
||||
// Shell thickness grows into the body.
|
||||
Kind::Shell { .. } => (at, normal.map(|n| -n)),
|
||||
Kind::Move { .. } => (at, Some(Vec3::Z)),
|
||||
@@ -118,7 +122,7 @@ fn draw(app: &SolveApp, ui: &mut egui::Ui, painter: &egui::Painter, proj: &Proj,
|
||||
};
|
||||
let symmetric = matches!(d.kind, Kind::Extrude { direction: 2, .. });
|
||||
// Extrudes and moves go either way; radii and thicknesses stay positive.
|
||||
let signed = matches!(d.kind, Kind::Extrude { .. } | Kind::Move { .. });
|
||||
let signed = matches!(d.kind, Kind::Extrude { .. } | Kind::Move { .. } | Kind::Section { .. });
|
||||
let focus = std::mem::take(&mut d.focus);
|
||||
let half_height = app.cam.half_height();
|
||||
let axis = revolve_axis(app, d);
|
||||
|
||||
@@ -73,6 +73,9 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
ui.separator();
|
||||
if app.session.section.is_some() && item(ui, "Remove Section") {
|
||||
other = Some("unsection");
|
||||
}
|
||||
if !app.session.selection.is_empty() && item(ui, "Clear Selection") {
|
||||
other = Some("clear");
|
||||
}
|
||||
@@ -97,6 +100,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
}
|
||||
match other {
|
||||
Some("clear") => drop(app.run("select.clear", json!({}))),
|
||||
Some("unsection") => drop(app.run("FusionHalfSectionViewCommand", json!({"clear": true}))),
|
||||
Some("fit") => app.animate_view("fit"),
|
||||
Some("home") => app.animate_view("home"),
|
||||
Some("undo") => drop(app.run("UndoCommand", json!({}))),
|
||||
|
||||
@@ -78,6 +78,11 @@ pub enum Kind {
|
||||
Sweep {
|
||||
operation: usize,
|
||||
},
|
||||
/// Section Analysis: cut the view by a plane, moved along its normal.
|
||||
Section {
|
||||
offset: String,
|
||||
flip: bool,
|
||||
},
|
||||
/// Measure one or two picked items (vertices, edges, faces): the result of the last
|
||||
/// measurement and the items it was for.
|
||||
Measure {
|
||||
@@ -197,6 +202,9 @@ impl Dialog {
|
||||
"MirrorCommand" => {
|
||||
Dialog::new(Kind::Mirror, vec![SelInput::new("Bodies", BODIES, true), SelInput::new("Mirror plane", PLANES | PLANAR_FACES, false)])
|
||||
}
|
||||
"FusionHalfSectionViewCommand" => {
|
||||
Dialog::new(Kind::Section { offset: "0 mm".into(), flip: false }, vec![SelInput::new("Plane", PLANES | PLANAR_FACES, false)])
|
||||
}
|
||||
"MeasureCommand" => {
|
||||
Dialog::new(Kind::Measure { result: None, of: Vec::new() }, vec![SelInput::new("Items", FACES | EDGES | selection::VERTICES, true)])
|
||||
}
|
||||
@@ -327,6 +335,7 @@ impl Dialog {
|
||||
Kind::Move { z, .. } => ("Z", ValueKind::Length, z),
|
||||
Kind::PatternRect { spacing, .. } => ("Spacing", ValueKind::Length, spacing),
|
||||
Kind::PatternCirc { angle, .. } => ("Angle", ValueKind::Angle, angle),
|
||||
Kind::Section { offset, .. } => ("Distance", ValueKind::Length, offset),
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
@@ -480,6 +489,7 @@ fn title(k: &Kind) -> &'static str {
|
||||
Kind::Draft { .. } => "DRAFT",
|
||||
Kind::Mirror => "MIRROR",
|
||||
Kind::Measure { .. } => "MEASURE",
|
||||
Kind::Section { .. } => "SECTION ANALYSIS",
|
||||
Kind::PatternRect { .. } => "RECTANGULAR PATTERN",
|
||||
Kind::PatternCirc { .. } => "CIRCULAR PATTERN",
|
||||
Kind::Loft { .. } => "LOFT",
|
||||
@@ -739,6 +749,14 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
}
|
||||
Kind::Mirror => {}
|
||||
Kind::Measure { result, .. } => measure_rows(ui, result.as_ref()),
|
||||
Kind::Section { offset, flip } => {
|
||||
ui.label("Distance");
|
||||
enter |= field(ui, offset);
|
||||
ui.end_row();
|
||||
ui.label("Flip");
|
||||
ui.checkbox(flip, "");
|
||||
ui.end_row();
|
||||
}
|
||||
Kind::PatternRect { count, spacing, count2, spacing2 } => {
|
||||
for (l, v) in [("Quantity", count), ("Spacing", spacing), ("Quantity 2", count2), ("Spacing 2", spacing2)] {
|
||||
ui.label(l);
|
||||
@@ -1194,6 +1212,26 @@ fn dialog_commands(app: &SolveApp, d: &Dialog) -> Result<Vec<(String, Value)>, S
|
||||
json!({"sketch": sketch, "profiles": idx, "path_sketch": path_sketch, "path": path, "operation": OPS.get(*operation).copied().unwrap_or("new")}),
|
||||
)
|
||||
}
|
||||
Kind::Section { offset, flip } => {
|
||||
need(0, "a plane or planar face")?;
|
||||
let plane = match sels(d, 0).first() {
|
||||
Some(Sel::Plane { name }) if matches!(name.as_str(), "XY" | "XZ" | "YZ") => json!(name),
|
||||
Some(Sel::Face { body, index, point }) => {
|
||||
let (_, n) = planar_face(s, body, *index).ok_or("the face must be planar")?;
|
||||
json!({"origin": pt(*point), "normal": pt(n)})
|
||||
}
|
||||
Some(Sel::Plane { name }) => {
|
||||
let pl = solvecraft_engine::view::construction_planes(s)
|
||||
.into_iter()
|
||||
.find(|(_, n, _)| n == name)
|
||||
.map(|(_, _, pl)| pl)
|
||||
.ok_or("unknown plane")?;
|
||||
json!({"origin": pt(pl.origin), "normal": pt(pl.normal())})
|
||||
}
|
||||
_ => return Err("pick a plane or a planar face".into()),
|
||||
};
|
||||
("FusionHalfSectionViewCommand", json!({"plane": plane, "offset": offset, "flip": flip}))
|
||||
}
|
||||
Kind::Move { x, y, z } => {
|
||||
need(0, "bodies")?;
|
||||
("FusionMoveCommand", json!({"bodies": body_names(0), "translate": [x, y, z]}))
|
||||
|
||||
@@ -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 = 112;
|
||||
const UNIFORM_SIZE: u64 = 128;
|
||||
|
||||
/// 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.
|
||||
@@ -97,6 +97,10 @@ pub struct ViewportCallback {
|
||||
pub back: [f32; 3],
|
||||
/// Viewport size in physical pixels.
|
||||
pub size_px: [f32; 2],
|
||||
/// Section cut: (normal, d); fragments with normal·p > d are hidden. A zero normal: none.
|
||||
pub clip: [f32; 4],
|
||||
/// Colour of the inside of cut bodies (straight sRGBA, 0..1).
|
||||
pub cap: [f32; 4],
|
||||
}
|
||||
|
||||
struct Batch {
|
||||
@@ -156,7 +160,13 @@ struct U {
|
||||
vp: mat4x4<f32>,
|
||||
back: vec4<f32>, // xyz = toward the eye, w = 1 for linear output
|
||||
screen: vec4<f32>, // viewport width, height (px), depth bias
|
||||
clip: vec4<f32>, // section plane: normal, d (zero normal: no section)
|
||||
cap: vec4<f32>, // colour of the inside of cut bodies
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
fn to_linear(c: vec3<f32>) -> vec3<f32> {
|
||||
@@ -174,6 +184,7 @@ struct TOut {
|
||||
@builtin(position) pos: vec4<f32>,
|
||||
@location(0) n: vec3<f32>,
|
||||
@location(1) c: vec4<f32>,
|
||||
@location(2) wp: vec3<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
@@ -182,11 +193,27 @@ fn vs_tri(@location(0) p: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c:
|
||||
o.pos = u.vp * vec4<f32>(p, 1.0);
|
||||
o.n = n;
|
||||
o.c = c;
|
||||
o.wp = p;
|
||||
return o;
|
||||
}
|
||||
|
||||
/// Opaque model surfaces: cut by the section plane, the inside shows as a flat cap colour.
|
||||
@fragment
|
||||
fn fs_tri(i: TOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
|
||||
fn fs_solid(i: TOut, @builtin(front_facing) front: bool) -> @location(0) vec4<f32> {
|
||||
if (clipped(i.wp)) { discard; }
|
||||
if (!front && length(u.clip.xyz) > 0.5) {
|
||||
return out_color(u.cap);
|
||||
}
|
||||
return shade(i);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_tri(i: TOut) -> @location(0) vec4<f32> {
|
||||
if (clipped(i.wp)) { discard; }
|
||||
return shade(i);
|
||||
}
|
||||
|
||||
fn shade(i: TOut) -> vec4<f32> {
|
||||
// A zero normal marks flat colour (selection fills, translucent planes).
|
||||
if (length(i.n) < 0.5) {
|
||||
return out_color(i.c);
|
||||
@@ -210,12 +237,14 @@ fn vs_ghost(@location(0) p: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c
|
||||
o.pos.z = o.pos.z + u.screen.z * 4.0 * o.pos.w;
|
||||
o.n = n;
|
||||
o.c = c;
|
||||
o.wp = p;
|
||||
return o;
|
||||
}
|
||||
|
||||
struct LOut {
|
||||
@builtin(position) pos: vec4<f32>,
|
||||
@location(0) c: vec4<f32>,
|
||||
@location(1) wp: vec3<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
@@ -239,11 +268,19 @@ fn vs_line(@builtin(vertex_index) vi: u32, @location(0) a: vec3<f32>, @location(
|
||||
var o: LOut;
|
||||
o.pos = vec4<f32>(base.xy + off / half * base.w, base.z - u.screen.z * base.w, base.w);
|
||||
o.c = c;
|
||||
o.wp = select(a, b, t > 0.5);
|
||||
return o;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_line(i: LOut) -> @location(0) vec4<f32> {
|
||||
if (clipped(i.wp)) { discard; }
|
||||
return out_color(i.c);
|
||||
}
|
||||
|
||||
/// Lines the section never cuts (the grid, highlights drawn on top).
|
||||
@fragment
|
||||
fn fs_line_all(i: LOut) -> @location(0) vec4<f32> {
|
||||
return out_color(i.c);
|
||||
}
|
||||
"#;
|
||||
@@ -329,13 +366,13 @@ impl Resources {
|
||||
};
|
||||
let alpha = Some(wgpu::BlendState::ALPHA_BLENDING);
|
||||
Resources {
|
||||
tri: pipeline("sc_tris", "vs_tri", "fs_tri", tri_layout(), depth(wgpu::CompareFunction::Less, true), None),
|
||||
tri: pipeline("sc_tris", "vs_tri", "fs_solid", tri_layout(), depth(wgpu::CompareFunction::Less, true), None),
|
||||
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),
|
||||
xray: pipeline("sc_xray", "vs_tri", "fs_tri", tri_layout(), depth(wgpu::CompareFunction::Always, false), alpha),
|
||||
line: pipeline("sc_lines", "vs_line", "fs_line", line_layout(), depth(wgpu::CompareFunction::LessEqual, false), alpha),
|
||||
overlay: pipeline("sc_overlay", "vs_line", "fs_line", line_layout(), depth(wgpu::CompareFunction::Always, false), alpha),
|
||||
overlay: pipeline("sc_overlay", "vs_line", "fs_line_all", line_layout(), depth(wgpu::CompareFunction::Always, false), alpha),
|
||||
uniform,
|
||||
bind,
|
||||
linear_out: t.format.is_srgb(),
|
||||
@@ -429,5 +466,19 @@ pub fn uniform_bytes(cb: &ViewportCallback, w: f32, h: f32, linear: bool) -> Vec
|
||||
let mut v: Vec<f32> = cb.view_proj.iter().flatten().copied().collect();
|
||||
v.extend([cb.back[0], cb.back[1], cb.back[2], if linear { 1.0 } else { 0.0 }]);
|
||||
v.extend([w.max(1.0), h.max(1.0), 2e-4, 0.0]);
|
||||
v.extend(cb.clip);
|
||||
v.extend(cb.cap);
|
||||
v.iter().flat_map(|x| x.to_le_bytes()).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use egui_wgpu::wgpu::naga;
|
||||
|
||||
/// The viewport shader parses and validates (a bad shader would only fail at run time).
|
||||
#[test]
|
||||
fn shader_is_valid() {
|
||||
let module = naga::front::wgsl::parse_str(super::SHADER).unwrap_or_else(|e| panic!("{}", e.emit_to_string(super::SHADER)));
|
||||
naga::valid::Validator::new(naga::valid::ValidationFlags::all(), naga::valid::Capabilities::empty()).validate(&module).unwrap();
|
||||
}
|
||||
}
|
||||
@@ -199,6 +199,11 @@ pub fn paint(p: &Painter, r: Rect, name: &str, ink: Color32, fill: Color32, acce
|
||||
pts.extend(inner);
|
||||
p.add(Shape::closed_line(pts, Stroke::new(pen.w, ink)));
|
||||
}
|
||||
"section" => {
|
||||
pen.poly(&[(4.0, 8.0), (14.0, 4.0), (20.0, 8.0), (10.0, 12.0)], pen.fill, ink);
|
||||
pen.poly(&[(4.0, 8.0), (10.0, 12.0), (10.0, 20.0), (4.0, 16.0)], pen.fill, ink);
|
||||
pen.poly(&[(10.0, 12.0), (20.0, 8.0), (20.0, 16.0), (10.0, 20.0)], a, ink);
|
||||
}
|
||||
"presspull" => {
|
||||
pen.poly(&[(3.0, 15.0), (12.0, 11.0), (21.0, 15.0), (12.0, 19.0)], pen.fill, ink);
|
||||
pen.arrow((12.0, 15.0), (12.0, 3.0), a);
|
||||
|
||||
@@ -83,6 +83,8 @@ pub struct Tokens {
|
||||
/// Sketch points: fully constrained, free.
|
||||
pub sketch_point: Color32,
|
||||
pub sketch_point_free: Color32,
|
||||
/// Inside of bodies cut by a section.
|
||||
pub section_cap: Color32,
|
||||
/// Closed sketch profiles (translucent fill).
|
||||
pub profile_fill: Color32,
|
||||
/// Constraint glyph boxes.
|
||||
@@ -167,6 +169,7 @@ impl Tokens {
|
||||
sketch_fixed: Color32::from_rgb(20, 20, 20),
|
||||
sketch_point: Color32::BLACK,
|
||||
sketch_point_free: Color32::from_rgb(30, 90, 200),
|
||||
section_cap: Color32::from_rgb(214, 128, 72),
|
||||
profile_fill: Color32::from_rgba_unmultiplied_const(255, 196, 120, 70),
|
||||
glyph_bg: Color32::WHITE,
|
||||
glyph_edge: Color32::from_rgb(90, 150, 90),
|
||||
|
||||
@@ -423,6 +423,26 @@ pub fn pick(app: &SolveApp, proj: &Proj, pos: Pos2) -> Vec<Hit> {
|
||||
hits
|
||||
}
|
||||
|
||||
/// The section cut shown now: the open Section Analysis dialog's plane, else the session's.
|
||||
pub fn section_plane(app: &SolveApp) -> Option<(Vec3, Vec3)> {
|
||||
if let Some(d) = app.dialog.as_ref().filter(|d| matches!(d.kind, crate::dialogs::Kind::Section { .. })) {
|
||||
let cmds = crate::dialogs::apply_commands(app, d).ok()?;
|
||||
let (_, p) = cmds.into_iter().next()?;
|
||||
let mut scratch = app.session.scratch();
|
||||
scratch.execute("FusionHalfSectionViewCommand", &p).ok()?;
|
||||
return scratch.section;
|
||||
}
|
||||
app.session.section
|
||||
}
|
||||
|
||||
/// The section as the shader's clip plane (normal, d), zero when there is none.
|
||||
fn section_clip(app: &SolveApp) -> [f32; 4] {
|
||||
match section_plane(app) {
|
||||
Some((o, n)) => [n.x as f32, n.y as f32, n.z as f32, n.dot(o) as f32],
|
||||
None => [0.0; 4],
|
||||
}
|
||||
}
|
||||
|
||||
/// Space kept free at the top right of the viewport for the view cube (docked dialogs start below).
|
||||
pub const VIEW_CUBE_CLEARANCE: f32 = 168.0;
|
||||
|
||||
@@ -873,6 +893,8 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui) {
|
||||
hl_slot: app.viewport.hl_slot.clone(),
|
||||
pv_key: app.preview.key,
|
||||
pv_slot: app.preview.slot.clone(),
|
||||
clip: section_clip(app),
|
||||
cap: t.section_cap.to_normalized_gamma_f32(),
|
||||
view_proj: proj.vp.to_f32(),
|
||||
back: proj.cam.back().to_f32(),
|
||||
size_px: [rect.width() * ppp, rect.height() * ppp],
|
||||
|
||||
Reference in new issue
Block a user