Datums: drawn at the model's extent, never framed, the selected one shown

A datum plane is drawn as the model's extent within it (where it cuts the
model's box, in the plane's axes) plus a 5 % margin, a datum axis along the
model's extent plus the margin, instead of squares 1.2 diagonals wide.
Fit, Home, the standard views and headless pictures frame the model
without the datums (they stay inside the clipping radius). The datum
selected in the browser is drawn highlighted and on top, even when hidden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-15 01:01:43 -04:00
1 parent b19277ae7a
commit dddb2d0584
6 files changed
+239 -56

No files matched your search

+44 -6
View File
@@ -883,7 +883,7 @@ impl ModelApp {
pub fn scene_key(&self) -> u64 {
let mut h = std::hash::DefaultHasher::new();
let plot = self.plot.1.as_ref().map(|p| Arc::as_ptr(p) as usize);
(self.revision, self.ui.style, self.ui.show(), self.ui.dark, plot).hash(&mut h);
(self.revision, self.ui.style, self.ui.show(), self.ui.dark, plot, self.selected_datum()).hash(&mut h);
h.finish()
}
@@ -929,19 +929,28 @@ impl ModelApp {
};
let mut datums = Aabb3::EMPTY;
if show.geometry && self.plot.1.is_none() {
datums = scene::datums(&mut s, &self.session, &self.display.1.bounds, &t);
datums = scene::datums(&mut s, &self.session, &self.display.1.bounds, self.selected_datum(), &t);
scene::seeds(&mut s, &self.session.model, &self.pick.names, &self.display.1, &t);
}
self.scene = (Some(key), s, datums);
}
// The clipping radius holds the symbols too (framing leaves them out: their size in the
// model follows the zoom).
// The clipping radius holds the symbols and the datums too (framing leaves them out: the
// symbols' size in the model follows the zoom, the datums are drawn at the model).
let mut bounds = self.drawn_bounds();
bounds.union(self.symbols.2.bounds);
bounds.union(self.scene.2);
self.scene.1.radius = scene::radius(&bounds, &self.cam);
(key, &self.scene.1)
}
/// The datum selected in the browser: drawn highlighted, shown or hidden.
fn selected_datum(&self) -> Option<&str> {
match &self.selected {
Some((browser::Def::Datum, name)) => Some(name.as_str()),
_ => None,
}
}
/// The definition selected in the browser, when it has symbols.
fn highlighted(&self) -> Option<(Definition, &str)> {
let (def, name) = self.selected.as_ref()?;
@@ -981,13 +990,13 @@ impl ModelApp {
}
/// What the scene built last draws, as framing sees it: the model, a results plot's
/// (deformed) shape, the datums and the reference points.
/// (deformed) shape and the reference points. Datums are never framed (#295): they are drawn
/// at the model's extent, so framing them would only shrink the part.
fn drawn_bounds(&self) -> Aabb3 {
let mut bounds = self.display.1.bounds;
if let Some(p) = &self.plot.1 {
bounds.union(p.display.bounds);
}
bounds.union(self.scene.2);
// Reference points are places in the model, framed with it (their symbols are not).
for p in self.session.model.reference_points().values() {
let p = Vec3::from(p.xyz);
@@ -2038,6 +2047,35 @@ mod tests {
}
}
/// Fit and home never frame datums (#295); a hidden datum selected in the browser is drawn,
/// highlighted, until the selection moves on.
#[test]
fn datums_are_not_framed_and_a_selected_one_shows() {
let mut app = ModelApp::new(Session::default());
let ctx = egui::Context::default();
app.run("file.sample", json!({"name": "cantilever"})).unwrap();
draw(&mut app, &ctx, 1280.0, 800.0);
app.home_view();
let home = app.cam;
app.run("geometry.datum", json!({"name": "Far", "by": {"type": "offset", "plane": "xy", "offset": 5000}})).unwrap();
draw(&mut app, &ctx, 1280.0, 800.0);
app.home_view();
// What framing sees is the model alone; the view stays as close as before (the overlays
// may differ a little after a command).
assert_eq!(app.drawn_bounds(), app.display.1.bounds, "a datum far off is not framed");
assert!(app.cam.distance < home.distance * 1.2, "{:?} vs {:?}", app.cam, home);
app.fit_view();
assert!(app.cam.distance < home.distance * 1.2);
let highlight = theme::Tokens::get(app.ui.dark).glyph_colors().highlight;
let lit = |app: &mut ModelApp| app.scene().1.on_top.iter().filter(|l| l.color == highlight).count();
app.run("view.hide", json!({"items": [{"kind": "datum", "name": "Far"}]})).unwrap();
assert_eq!(lit(&mut app), 0);
app.selected = Some((browser::Def::Datum, "Far".into()));
assert!(lit(&mut app) > 0, "the selected datum is drawn though hidden");
app.selected = None;
assert_eq!(lit(&mut app), 0);
}
/// Opening a sample, then another, frames each in the home view; an edit keeps the camera
/// (#233, #223).
#[test]
+1 -1
View File
@@ -233,7 +233,7 @@ mod tests {
fn on_datum(app: &mut ModelApp, name: &str) -> egui::Pos2 {
let bounds = app.display().bounds;
let mut s = fieldcraft_render::Scene::default();
fieldcraft_render::glyphs::datums(&app.session.model, &bounds, &|n| n == name, &crate::theme::Tokens::dark().glyph_colors(), &mut s);
fieldcraft_render::glyphs::datums(&app.session.model, &bounds, &|n| n == name, None, &crate::theme::Tokens::dark().glyph_colors(), &mut s);
let l = s.lines[0];
viewport::to_screen(app, (l.a + l.b) * 0.5).unwrap()
}
+4 -3
View File
@@ -82,10 +82,11 @@ pub fn symbols(
glyphs.draw(pos, &view, s)
}
/// Draw the datums the session's view shows around `bounds` into `s`; returns the box around them.
pub fn datums(s: &mut Scene, session: &fieldcraft_engine::Session, bounds: &Aabb3, t: &Tokens) -> Aabb3 {
/// Draw the datums the session's view shows at the model (`bounds`) into `s`, and `selected` (the
/// datum selected in the browser), shown or hidden, highlighted; returns the box around them.
pub fn datums(s: &mut Scene, session: &fieldcraft_engine::Session, bounds: &Aabb3, selected: Option<&str>, t: &Tokens) -> Aabb3 {
let shown = |name: &str| session.shows(fieldcraft_engine::ItemKind::Datum, name);
fieldcraft_render::glyphs::datums(&session.model, bounds, &shown, &t.glyph_colors(), s)
fieldcraft_render::glyphs::datums(&session.model, bounds, &shown, selected, &t.glyph_colors(), s)
}
/// The scene for `display` drawn in `style`.
+1 -1
View File
@@ -209,7 +209,7 @@ fn datum_at(app: &mut ModelApp, at: Pos2) -> Option<String> {
let mut best: Option<(f32, String)> = None;
for name in names {
let mut drawn = fieldcraft_render::Scene::default();
fieldcraft_render::glyphs::datums(&app.session.model, &bounds, &|n| n == name, &colors, &mut drawn);
fieldcraft_render::glyphs::datums(&app.session.model, &bounds, &|n| n == name, None, &colors, &mut drawn);
for l in &drawn.lines {
let (Some(a), Some(b)) = (to_screen(app, l.a), to_screen(app, l.b)) else { continue };
let xy = |p: Pos2| [f64::from(p.x), f64::from(p.y)];