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 <noreply@anthropic.com> 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() {
|
||||
|
||||
Reference in new issue
Block a user