forked from 1b-agent/fieldcraft-upstream
render, ui: prescribed motion and thermal conditions get their own symbols and tree icons (#354, #355)
A non-zero prescribed displacement is an arrow leaving the nodes along the motion (its held degrees of freedom stay supports); a prescribed temperature is a thermometer, convection a wave off the faces, a heat flux an arrow onto them. In the tree, thermal conditions carry the heat icon and a moving boundary condition a motion arrow. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
27af5e19b7
commit
b89efb55c1
2 files changed
+152
-13
No files matched your search
+104
-11
@@ -244,7 +244,18 @@ enum Shape {
|
||||
Constraint,
|
||||
Force(Vec3),
|
||||
Moment(Vec3),
|
||||
/// Heat generated in a set's elements: a dot at each node.
|
||||
Thermal,
|
||||
/// A prescribed temperature (a boundary condition or a temperature field): a stem off the
|
||||
/// part ending in a bulb, a thermometer.
|
||||
Temperature,
|
||||
/// A prescribed motion along a unit direction: an arrow leaving the node from a short bar,
|
||||
/// telling it apart from a force (an arrow onto the node) and a support (triangles).
|
||||
Prescribed(Vec3),
|
||||
/// Convection to the surroundings: a wavy line off the face.
|
||||
Convection,
|
||||
/// A heat flux into the face: an arrow onto it.
|
||||
HeatFlux,
|
||||
/// A pressure: onto the face (positive) or away from it.
|
||||
Pressure {
|
||||
onto: bool,
|
||||
@@ -474,10 +485,11 @@ impl Builder<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Dots at a thermal condition's nodes.
|
||||
fn thermal(&mut self, owner: usize, target: &Target) {
|
||||
/// A thermal condition's symbol (`shape`, at nodes) at its target's nodes.
|
||||
fn thermal(&mut self, owner: usize, target: &Target, shape: Shape) {
|
||||
let (spots, total) = self.spots(target);
|
||||
self.group(owner, Shape::Thermal, spots, Vec::new(), total, 1.5);
|
||||
let cell = if shape == Shape::Temperature { 2.4 } else { 1.5 };
|
||||
self.group(owner, shape, spots, Vec::new(), total, cell);
|
||||
}
|
||||
|
||||
/// The faces of a set (its element faces, else the edges of its 2D elements) with their
|
||||
@@ -543,9 +555,22 @@ impl Builder<'_> {
|
||||
fn boundary_condition(&mut self, name: &str, bc: &BoundaryCondition) {
|
||||
let owner = self.owner(Definition::BoundaryCondition, name);
|
||||
if bc.is_thermal() {
|
||||
return self.thermal(owner, bc.target());
|
||||
return self.thermal(owner, bc.target(), Shape::Temperature);
|
||||
}
|
||||
let mut dofs = bc.dofs().bits();
|
||||
// A prescribed non-zero translation moves the nodes: an arrow along the motion; the
|
||||
// degrees of freedom it drives are no supports.
|
||||
if let BoundaryCondition::Displacement { values, .. } = bc {
|
||||
let moved = |i: usize| values.get(i).copied().flatten().filter(|v| v.is_finite() && *v != 0.0);
|
||||
let motion = Vec3::new(moved(0).unwrap_or(0.0), moved(1).unwrap_or(0.0), moved(2).unwrap_or(0.0));
|
||||
for (i, _) in values.iter().enumerate().filter(|(i, _)| moved(*i).is_some()) {
|
||||
dofs &= !(1u8 << i);
|
||||
}
|
||||
if let Some(dir) = motion.normalized() {
|
||||
let (spots, total) = self.spots(bc.target());
|
||||
self.group(owner, Shape::Prescribed(dir), spots, Vec::new(), total, 2.8);
|
||||
}
|
||||
}
|
||||
// A symmetry plane shows its normal only: the rotations it holds follow from it.
|
||||
if matches!(bc, BoundaryCondition::Symmetry { .. }) {
|
||||
dofs &= 0b000_111;
|
||||
@@ -606,10 +631,17 @@ impl Builder<'_> {
|
||||
let centre = point + axis * axis.dot(c - point);
|
||||
self.marks.push((owner, Mark::Spin { centre, axis, half_length: 0.5 * self.bounds.diagonal() }));
|
||||
}
|
||||
Load::Temperature { target, .. } => self.thermal(owner, target),
|
||||
Load::SurfaceFlux { set, .. } | Load::Convection { set, .. } | Load::HeatSource { set, .. } => {
|
||||
self.thermal(owner, &Target::Set(set.clone()))
|
||||
Load::Temperature { target, .. } => self.thermal(owner, target, Shape::Temperature),
|
||||
Load::Convection { set, .. } | Load::SurfaceFlux { set, .. } => {
|
||||
let shape = if matches!(load, Load::Convection { .. }) { Shape::Convection } else { Shape::HeatFlux };
|
||||
let (faces, total) = self.faces(set);
|
||||
if faces.is_empty() {
|
||||
self.thermal(owner, &Target::Set(set.clone()), Shape::Thermal);
|
||||
} else {
|
||||
self.group(owner, shape, Vec::new(), faces, total, 2.2);
|
||||
}
|
||||
}
|
||||
Load::HeatSource { set, .. } => self.thermal(owner, &Target::Set(set.clone()), Shape::Thermal),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -761,8 +793,8 @@ impl Glyphs {
|
||||
continue;
|
||||
}
|
||||
let color = match g.shape {
|
||||
Shape::Constraint => colors.constraint,
|
||||
Shape::Thermal => colors.thermal,
|
||||
Shape::Constraint | Shape::Prescribed(_) => colors.constraint,
|
||||
Shape::Thermal | Shape::Temperature | Shape::Convection | Shape::HeatFlux => colors.thermal,
|
||||
Shape::Surface => colors.contact,
|
||||
_ => colors.load,
|
||||
};
|
||||
@@ -1087,7 +1119,29 @@ impl Draw<'_> {
|
||||
self.curl(p, dir, radius, head);
|
||||
}
|
||||
Shape::Thermal => self.point(p, THERMAL_SIZE),
|
||||
Shape::Pressure { .. } | Shape::Traction(_) | Shape::Surface => {}
|
||||
Shape::Temperature => {
|
||||
// Off the part along its faces' outward direction (up, away from faces).
|
||||
let dir = s.normal.unwrap_or(Vec3::Z);
|
||||
let (stem, bulb) = if spread { (1.2 * u, 0.32 * u) } else { (1.8 * u, 0.45 * u) };
|
||||
self.line(p, p + dir * stem);
|
||||
let (e1, _) = perpendiculars(dir);
|
||||
let c = p + dir * (stem + bulb);
|
||||
for k in 0..ARC_SEGMENTS {
|
||||
let at = |k: usize| {
|
||||
let a = std::f64::consts::TAU * k as f64 / ARC_SEGMENTS as f64;
|
||||
c + (dir * a.cos() + e1 * a.sin()) * bulb
|
||||
};
|
||||
self.line(at(k), at(k + 1));
|
||||
}
|
||||
self.point(p, THERMAL_SIZE * 0.7);
|
||||
}
|
||||
Shape::Prescribed(dir) => {
|
||||
let (len, head) = if spread { (2.2 * u, 0.7 * u) } else { (3.5 * u, 0.9 * u) };
|
||||
let (e1, _) = perpendiculars(dir);
|
||||
self.line(p - e1 * (0.45 * u), p + e1 * (0.45 * u));
|
||||
self.arrow(p, dir, len, head);
|
||||
}
|
||||
Shape::Pressure { .. } | Shape::Traction(_) | Shape::Surface | Shape::Convection | Shape::HeatFlux => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1108,6 +1162,21 @@ impl Draw<'_> {
|
||||
Shape::Pressure { onto: true } => self.arrow(c + outward * len, -outward, len, head),
|
||||
Shape::Pressure { onto: false } => self.arrow(c, outward, len, head),
|
||||
Shape::Traction(dir) => self.arrow(c - dir * len, dir, len, head),
|
||||
Shape::HeatFlux => self.arrow(c + outward * len, -outward, len, head),
|
||||
Shape::Convection => {
|
||||
// A wave leaving the face.
|
||||
let (e1, _) = perpendiculars(outward);
|
||||
let steps = 6;
|
||||
let at = |k: usize| {
|
||||
let along = len * k as f64 / steps as f64;
|
||||
let side = if k == 0 { 0.0 } else { (k as f64 * std::f64::consts::FRAC_PI_2).sin() * 0.25 * u };
|
||||
c + outward * along + e1 * side
|
||||
};
|
||||
for k in 0..steps {
|
||||
self.line(at(k), at(k + 1));
|
||||
}
|
||||
self.head(at(steps), outward, head);
|
||||
}
|
||||
Shape::Surface => {
|
||||
// A small square on the face, lifted off it a little.
|
||||
let (e1, e2) = perpendiculars(outward);
|
||||
@@ -1117,7 +1186,7 @@ impl Draw<'_> {
|
||||
self.line(*a, ring.get((k + 1) % 4).copied().unwrap_or(*a));
|
||||
}
|
||||
}
|
||||
Shape::Constraint | Shape::Force(_) | Shape::Moment(_) | Shape::Thermal => {}
|
||||
Shape::Constraint | Shape::Force(_) | Shape::Moment(_) | Shape::Thermal | Shape::Temperature | Shape::Prescribed(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1244,6 +1313,30 @@ mod tests {
|
||||
assert_eq!(near(&bases(&s, Vec3::ZERO), -Vec3::X * u), 2);
|
||||
}
|
||||
|
||||
/// A prescribed motion is drawn as an arrow leaving each node along it (a bar, a shaft and a
|
||||
/// head), its held degrees of freedom still as supports (#354); a prescribed
|
||||
/// temperature is a thermometer, never a support (#355).
|
||||
#[test]
|
||||
fn prescribed_motion_and_temperature_have_their_own_symbols() {
|
||||
let u = 0.1;
|
||||
let mut m = bar(4);
|
||||
let values = [Some(0.0), None, Some(-2.5), None, None, None];
|
||||
m.add_boundary_condition("Press", BoundaryCondition::Displacement { target: Target::Set("root".into()), values }).unwrap();
|
||||
let (s, _) = drawn_at(&Glyphs::from_model(&m), &m, &view(GlyphShow::ALL, u));
|
||||
// Per node: one support triangle (x held at 0, six lines), and the motion's bar, shaft and
|
||||
// four-line head pointing down from the node.
|
||||
assert_eq!(s.lines.len(), 4 * (6 + 1 + 1 + 4), "{}", s.lines.len());
|
||||
let down = s.lines.iter().filter(|l| l.a.dist(Vec3::ZERO) < 1e-9 && l.b.dist(Vec3::new(0.0, 0.0, -2.2 * u)) < 1e-9).count();
|
||||
assert_eq!(down, 1, "a shaft leaves the node along the motion");
|
||||
assert!(s.lines.iter().all(|l| l.color == COLORS.constraint));
|
||||
|
||||
let mut m = bar(4);
|
||||
m.add_boundary_condition("Heater", BoundaryCondition::Temperature { target: Target::Set("root".into()), value: 120.0 }).unwrap();
|
||||
let (s, _) = drawn_at(&Glyphs::from_model(&m), &m, &view(GlyphShow::ALL, u));
|
||||
assert!(s.lines.iter().all(|l| l.color == COLORS.thermal) && s.lines.len() >= 4 * (1 + ARC_SEGMENTS), "{}", s.lines.len());
|
||||
assert_eq!(s.points.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rotations_are_double_marks_and_symmetry_shows_its_normal_only() {
|
||||
let u = 0.5;
|
||||
|
||||
@@ -423,6 +423,8 @@ struct Row<'a> {
|
||||
/// Some(visible) for rows with an eye.
|
||||
eye: Option<bool>,
|
||||
icon: Option<&'static str>,
|
||||
/// A motion arrow over the icon's corner: a boundary condition that moves the part.
|
||||
moves: bool,
|
||||
/// A colour swatch instead of an icon (element kinds).
|
||||
swatch: Option<Color32>,
|
||||
label: &'a str,
|
||||
@@ -499,7 +501,17 @@ fn draw_row(app: &mut ModelApp, ui: &mut egui::Ui, id: egui::Id, row: &Row) -> R
|
||||
x += 20.0;
|
||||
}
|
||||
if let Some(icon) = row.icon {
|
||||
icons::paint(app, ui, icon, Rect::from_min_size(pos2(x, cy - 8.0), Vec2::splat(16.0)), Look::of(!row.dim));
|
||||
let ir = Rect::from_min_size(pos2(x, cy - 8.0), Vec2::splat(16.0));
|
||||
icons::paint(app, ui, icon, ir, Look::of(!row.dim));
|
||||
if row.moves {
|
||||
// An arrow out of the icon's lower right corner, on a disc of the row's background.
|
||||
let c = ir.right_bottom() - vec2(3.0, 3.0);
|
||||
ui.painter().circle_filled(c, 4.5, if row.selected { t.accent_soft } else { t.panel });
|
||||
let s = Stroke::new(1.4, t.accent);
|
||||
ui.painter().line_segment([c + vec2(-3.0, -3.0), c + vec2(2.5, 2.5)], s);
|
||||
ui.painter().line_segment([c + vec2(2.5, 2.5), c + vec2(-0.5, 2.5)], s);
|
||||
ui.painter().line_segment([c + vec2(2.5, 2.5), c + vec2(2.5, -0.5)], s);
|
||||
}
|
||||
x += 21.0;
|
||||
} else if let Some(c) = row.swatch {
|
||||
ui.painter().rect_filled(Rect::from_center_size(pos2(x + 6.0, cy), Vec2::splat(10.0)), 2.0, c);
|
||||
@@ -651,6 +663,25 @@ enum Act {
|
||||
Menu(Target, Action),
|
||||
}
|
||||
|
||||
/// A definition's icon in the tree, by what it is: thermal boundary conditions and loads carry
|
||||
/// the heat icon (a temperature is no support), and a boundary condition that moves the part
|
||||
/// (a non-zero prescribed displacement) its kind's icon with a motion arrow (`true`).
|
||||
fn item_icon(m: &Model, kind: Def, name: &str) -> (&'static str, bool) {
|
||||
const HEAT: &str = "model.step.heat";
|
||||
match kind {
|
||||
Def::BoundaryCondition => match m.boundary_condition(name) {
|
||||
Some(bc) if bc.is_thermal() => (HEAT, false),
|
||||
Some(bc) => (kind.icon(), bc.drives()),
|
||||
None => (kind.icon(), false),
|
||||
},
|
||||
Def::Load => match m.loads().get(name) {
|
||||
Some(l) if l.is_thermal() || matches!(l, fieldcraft_model::Load::Temperature { .. }) => (HEAT, false),
|
||||
_ => (kind.icon(), false),
|
||||
},
|
||||
_ => (kind.icon(), false),
|
||||
}
|
||||
}
|
||||
|
||||
/// A seed in a few words: `8 exact`, `2.5 min, bias 4 end`.
|
||||
fn seed_note(s: &fieldcraft_model::Seed) -> String {
|
||||
let what = match s.divisions {
|
||||
@@ -936,7 +967,8 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
|
||||
if open(app, key) {
|
||||
for (kind, name) in items {
|
||||
let id = egui::Id::new(("fc_row_def", kind.stem(), &name));
|
||||
let row = Row { depth: 2, icon: Some(kind.icon()), label: &name, ..Default::default() };
|
||||
let (icon, moves) = item_icon(&app.session.model, kind, &name);
|
||||
let row = Row { depth: 2, icon: Some(icon), moves, label: &name, ..Default::default() };
|
||||
item_row(app, ui, id, row, Target::Def(kind, name.clone()), acts);
|
||||
}
|
||||
}
|
||||
@@ -1055,6 +1087,20 @@ mod tests {
|
||||
assert_eq!(v[2], json!({"node": "sets", "state": "done", "tip": "Done", "add": []}));
|
||||
}
|
||||
|
||||
/// A prescribed displacement and a temperature read apart from a support in the tree (#354,
|
||||
/// #355).
|
||||
#[test]
|
||||
fn driving_and_thermal_conditions_have_their_own_icons() {
|
||||
let mut s = Session::default();
|
||||
s.run_script(&fieldcraft_engine::sample::rubber_seal()).unwrap();
|
||||
assert_eq!(item_icon(&s.model, Def::BoundaryCondition, "Press"), ("model.boundary_condition.create", true));
|
||||
assert_eq!(item_icon(&s.model, Def::BoundaryCondition, "Base"), ("model.boundary_condition.create", false));
|
||||
let mut s = Session::default();
|
||||
s.execute("file.sample", &json!({"name": "heated-bracket"})).unwrap();
|
||||
assert_eq!(item_icon(&s.model, Def::BoundaryCondition, "Heater"), ("model.step.heat", false));
|
||||
assert_eq!(item_icon(&s.model, Def::Load, "Air").0, "model.step.heat");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn definitions_read_back_as_their_parameters() {
|
||||
let mut app = ModelApp::new(Session::default());
|
||||
|
||||
Reference in new issue
Block a user