forked from 1b-agent/fieldcraft-upstream
engine, render: symmetry plane command on any datum plane (#242)
model.boundary_condition.symmetry takes a datum plane of any orientation, finds the faces on it and reports the plane's unit normal when it is inclined; the glyph draws a support along that normal. Verified: half a plate cut on its diagonal with the plane deflects as the half mirrored across it (6.7e-13), the mirror conforming on the plane. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
a226b7dcac
commit
992bc71b6b
3 files changed
+150
-18
No files matched your search
@@ -252,7 +252,7 @@ definition_commands!(
|
|||||||
BoundaryCondition,
|
BoundaryCondition,
|
||||||
"boundary_condition",
|
"boundary_condition",
|
||||||
"Boundary Condition",
|
"Boundary Condition",
|
||||||
r#"{"name": "clamp", "type": "fixed", "target": {"set": "root"} | {"reference_point": "RP"}} | {"name", "type": "displacement", "target", "values": [u1, u2, u3, ur1, ur2, ur3] (null leaves a degree of freedom free)} | {"name", "type": "symmetry", "target", "plane": "x|y|z" (the plane's normal)} — active in the steps that list it (model.step.activate)"#,
|
r#"{"name": "clamp", "type": "fixed", "target": {"set": "root"} | {"reference_point": "RP"}} | {"name", "type": "displacement", "target", "values": [u1, u2, u3, ur1, ur2, ur3] (null leaves a degree of freedom free)} | {"name", "type": "symmetry", "target", "plane": "x|y|z" | {"normal": [nx, ny, nz]} (the plane's normal; an inclined plane holds n · u = 0 and the rotations about its in-plane axes)} — active in the steps that list it (model.step.activate)"#,
|
||||||
super::forms::BOUNDARY_CONDITION,
|
super::forms::BOUNDARY_CONDITION,
|
||||||
super::forms::BOUNDARY_CONDITION_DELETE,
|
super::forms::BOUNDARY_CONDITION_DELETE,
|
||||||
add_boundary_condition,
|
add_boundary_condition,
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
//! symmetric part can be solved as a half.
|
//! symmetric part can be solved as a half.
|
||||||
//!
|
//!
|
||||||
//! The condition holds the displacement normal to the plane, and the rotations about the two
|
//! The condition holds the displacement normal to the plane, and the rotations about the two
|
||||||
//! axes in it, at zero (`type: "symmetry"`). Its set is given, or made of the faces of the open
|
//! axes in it, at zero (`type: "symmetry"`): on a plane normal to X, Y or Z those are global
|
||||||
|
//! degrees of freedom; on any other plane they are linear constraints along its normal. Its set is given, or made of the faces of the open
|
||||||
//! design's bodies that lie on the plane (the cut faces of a partition by that datum, `partition:
|
//! design's bodies that lie on the plane (the cut faces of a partition by that datum, `partition:
|
||||||
//! <name>:a|b`), so it follows the geometry through a remesh.
|
//! <name>:a|b`), so it follows the geometry through a remesh.
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
|
|||||||
pub const SYMMETRY: CommandSpec = CommandSpec {
|
pub const SYMMETRY: CommandSpec = CommandSpec {
|
||||||
id: "model.boundary_condition.symmetry",
|
id: "model.boundary_condition.symmetry",
|
||||||
label: "Symmetry Plane",
|
label: "Symmetry Plane",
|
||||||
params: r#"{"name": "Sym", "datum": "Mid" (a datum plane normal to X, Y or Z), "set"?: "cut" (default: a new set of the same name holding every face of the open design's bodies on the plane, e.g. a partition's cut faces), "bodies"?: ["Part/a"] (only their faces)} — a symmetry boundary condition on the plane: zero displacement normal to it and zero rotation about the axes in it, so half a symmetric part (loaded symmetrically, with half the load) is solved as a half. Add it to the steps (model.step.activate). Planes not normal to X, Y or Z are not supported yet"#,
|
params: r#"{"name": "Sym", "datum": "Mid" (a datum plane, any orientation), "set"?: "cut" (default: a new set of the same name holding every face of the open design's bodies on the plane, e.g. a partition's cut faces), "bodies"?: ["Part/a"] (only their faces)} — a symmetry boundary condition on the plane: zero displacement normal to it and zero rotation about the axes in it, so half a symmetric part (loaded symmetrically, with half the load) is solved as a half. Add it to the steps (model.step.activate). Reports the plane as "x", "y", "z" or its unit normal"#,
|
||||||
form: Some(super::forms::SYMMETRY),
|
form: Some(super::forms::SYMMETRY),
|
||||||
kind: CommandKind::Model,
|
kind: CommandKind::Model,
|
||||||
requires: &[Requirement::DatumPlane],
|
requires: &[Requirement::DatumPlane],
|
||||||
@@ -30,10 +31,10 @@ fn symmetry(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
let name = super::name(p)?.to_string();
|
let name = super::name(p)?.to_string();
|
||||||
let datum: String = super::req(p, "datum")?;
|
let datum: String = super::req(p, "datum")?;
|
||||||
let (point, normal) = super::datum::datum_plane_of(&s.model, &datum)?;
|
let (point, normal) = super::datum::datum_plane_of(&s.model, &datum)?;
|
||||||
let axis = (0..3).find(|&k| (normal[k].abs() - 1.0).abs() < 1e-9).ok_or_else(|| {
|
let unusable = || EngineError::Params(format!("datum `{datum}` has no usable normal ({normal:?})"));
|
||||||
EngineError::Params(format!("not supported yet: symmetry about a plane not normal to X, Y or Z (datum `{datum}` has normal {normal:?})"))
|
let plane = Symmetry::from_normal(normal).ok_or_else(unusable)?;
|
||||||
})?;
|
let n = plane.unit_normal().ok_or_else(unusable)?;
|
||||||
let plane = [Symmetry::X, Symmetry::Y, Symmetry::Z][axis];
|
let along = |x: [f64; 3]| (0..3).map(|k| x[k] * n[k]).sum::<f64>();
|
||||||
let set = match p.get("set") {
|
let set = match p.get("set") {
|
||||||
Some(_) => super::req::<String>(p, "set")?,
|
Some(_) => super::req::<String>(p, "set")?,
|
||||||
None => {
|
None => {
|
||||||
@@ -48,8 +49,9 @@ fn symmetry(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
let tol = ON_PLANE * b.size()?.max(1.0);
|
let tol = ON_PLANE * b.size()?.max(1.0);
|
||||||
for (f, surface) in b.faces().iter().zip(b.face_surfaces()?) {
|
for (f, surface) in b.faces().iter().zip(b.face_surfaces()?) {
|
||||||
if let Some(FaceSurface::Plane { normal: m, point: q }) = surface {
|
if let Some(FaceSurface::Plane { normal: m, point: q }) = surface {
|
||||||
let parallel = (m[axis].abs() - 1.0).abs() < 1e-9;
|
let length = m.iter().map(|c| c * c).sum::<f64>().sqrt();
|
||||||
if parallel && (q[axis] - point[axis]).abs() < tol {
|
let parallel = (along(m).abs() - length).abs() < 1e-9 * length;
|
||||||
|
if parallel && (along(q) - along(point)).abs() < tol {
|
||||||
faces.push(GeometryRef::face(b.name.as_str(), f.name.as_str())?);
|
faces.push(GeometryRef::face(b.name.as_str(), f.name.as_str())?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,8 +66,11 @@ fn symmetry(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
};
|
};
|
||||||
s.model.add_boundary_condition(&name, BoundaryCondition::Symmetry { target: Target::Set(set.clone()), plane })?;
|
s.model.add_boundary_condition(&name, BoundaryCondition::Symmetry { target: Target::Set(set.clone()), plane })?;
|
||||||
let faces = s.model.set(&set).map_or(0, |x| x.geometry.len());
|
let faces = s.model.set(&set).map_or(0, |x| x.geometry.len());
|
||||||
let axis_name = ["x", "y", "z"].get(axis).copied().unwrap_or("x");
|
let reported = match plane.axis() {
|
||||||
Ok(json!({"name": name, "set": set, "plane": axis_name, "faces": faces}))
|
Some(k) => json!(["x", "y", "z"].get(k).copied().unwrap_or("x")),
|
||||||
|
None => json!(n),
|
||||||
|
};
|
||||||
|
Ok(json!({"name": name, "set": set, "plane": reported, "faces": faces}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -137,17 +142,117 @@ mod tests {
|
|||||||
assert!((2.0 * halved.1 - full.1).abs() < 1e-9 * full.1.abs(), "reaction {} vs {}", halved.1, full.1);
|
assert!((2.0 * halved.1 - full.1).abs() < 1e-9 * full.1.abs(), "reaction {} vs {}", halved.1, full.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A 20 × 20 × 2 plate (0..20 in x and y), cut on its diagonal plane x = y (datum `Diag`,
|
||||||
|
/// normal (1, −1, 0)) into `Plate/a` and `Plate/b`, the half below the diagonal (y < x) meshed
|
||||||
|
/// with quadratic tets.
|
||||||
|
fn plate() -> (crate::tests::TempDir, Session, String) {
|
||||||
|
let commands = json!({"commands": [
|
||||||
|
{"command": "solid.box", "params": {"length": 20, "width": 20, "height": 2, "body_name": "Plate"}},
|
||||||
|
]});
|
||||||
|
let bytes = fieldcraft_geometry::script_design(&commands.to_string()).unwrap();
|
||||||
|
let (dir, path) = crate::tests::design_file(&bytes);
|
||||||
|
let mut s = Session::default();
|
||||||
|
s.execute("geometry.open", &json!({"path": path})).unwrap();
|
||||||
|
s.execute("geometry.datum", &json!({"name": "Diag", "by": {"type": "point_normal", "point": [0, 0, 0], "normal": [1, -1, 0]}})).unwrap();
|
||||||
|
s.execute("geometry.partition", &json!({"name": "Cut", "datum": "Diag"})).unwrap();
|
||||||
|
let design = s.design.as_ref().unwrap();
|
||||||
|
let below = design
|
||||||
|
.bodies
|
||||||
|
.iter()
|
||||||
|
.find(|b| {
|
||||||
|
let c = b.measure().unwrap().centroid;
|
||||||
|
c[0] > c[1]
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
.name
|
||||||
|
.clone();
|
||||||
|
s.execute("mesh.tet.generate", &json!({"body": below, "size": 2, "order": "quadratic"})).unwrap();
|
||||||
|
(dir, s, below)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clamp the faces at y = 0 (and x = 0 when `whole`), press the top faces, solve; the
|
||||||
|
/// displacement of every node of the half below the diagonal, by label.
|
||||||
|
fn press(s: &mut Session, below: &str, whole: bool, extra: &[&str]) -> std::collections::BTreeMap<u64, [f64; 3]> {
|
||||||
|
let bodies: Vec<String> = s.design.as_ref().unwrap().bodies.iter().map(|b| b.name.clone()).collect();
|
||||||
|
let mut clamp = vec![json!({"body": below, "face": face(s, below, [10.0, 0.0, 1.0])})];
|
||||||
|
let mut top = vec![json!({"body": below, "face": face(s, below, [15.0, 5.0, 2.0])})];
|
||||||
|
if whole {
|
||||||
|
let above = bodies.iter().find(|b| b.as_str() != below).unwrap();
|
||||||
|
clamp.push(json!({"body": above, "face": face(s, above, [0.0, 10.0, 1.0])}));
|
||||||
|
top.push(json!({"body": above, "face": face(s, above, [5.0, 15.0, 2.0])}));
|
||||||
|
}
|
||||||
|
let all: Vec<Value> = bodies.iter().filter(|b| whole || b.as_str() == below).map(|b| json!({"body": b})).collect();
|
||||||
|
let mut bcs = vec!["Clamp".to_string()];
|
||||||
|
bcs.extend(extra.iter().map(|x| x.to_string()));
|
||||||
|
let script = json!([
|
||||||
|
{"command": "model.set.create", "params": {"name": "all", "geometry": all}},
|
||||||
|
{"command": "model.set.create", "params": {"name": "clamped", "geometry": clamp}},
|
||||||
|
{"command": "model.set.create", "params": {"name": "top", "geometry": top}},
|
||||||
|
{"command": "model.set.create", "params": {"name": "half", "geometry": [{"body": below}]}},
|
||||||
|
{"command": "model.material.create", "params": {"name": "Steel", "elastic": {"type": "isotropic", "young": 210000, "poisson": 0.3}}},
|
||||||
|
{"command": "model.section.create", "params": {"name": "Plate", "set": "all", "material": "Steel", "kind": {"type": "solid"}}},
|
||||||
|
{"command": "model.boundary_condition.create", "params": {"name": "Clamp", "type": "fixed", "target": {"set": "clamped"}}},
|
||||||
|
{"command": "model.load.create", "params": {"name": "Press", "type": "pressure", "set": "top", "magnitude": 0.1}},
|
||||||
|
{"command": "model.step.create", "params": {"name": "Load", "procedure": {"type": "static"}, "boundary_conditions": bcs, "loads": ["Press"]}},
|
||||||
|
]);
|
||||||
|
s.run_script(&script).unwrap();
|
||||||
|
s.execute("solve.static", &json!({})).unwrap();
|
||||||
|
let nodes = s.model.resolve_set("half").unwrap().nodes;
|
||||||
|
let probe = s.execute("solve.probe", &json!({"step": "Load", "nodes": nodes})).unwrap();
|
||||||
|
probe["nodes"]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|n| (n["node"].as_u64().unwrap(), [0, 1, 2].map(|k| n["displacement"][k].as_f64().unwrap())))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The half of the plate below its diagonal, with the inclined symmetry plane (found on the
|
||||||
|
/// cut face) and its half of the pressure, deflects as the whole plate, meshed as that half
|
||||||
|
/// mirrored across the diagonal (conforming there, so the whole is the same mesh twice).
|
||||||
#[test]
|
#[test]
|
||||||
fn inclined_planes_and_hostile_params_change_nothing() {
|
fn a_half_cut_on_an_inclined_plane_matches_the_whole() {
|
||||||
|
let (_d1, mut whole, below) = plate();
|
||||||
|
let r = whole.execute("mesh.mirror", &json!({"plane": "Diag", "bodies": [below]})).unwrap();
|
||||||
|
let on_plane = r["on_plane"].as_u64().unwrap();
|
||||||
|
let q = whole.execute("mesh.quality", &json!({})).unwrap();
|
||||||
|
assert_eq!(q["valid"].as_u64().unwrap() as usize, whole.model.elements().len(), "{q}");
|
||||||
|
assert!((q["volume"].as_f64().unwrap() - 800.0).abs() < 1e-9 * 800.0, "{q}");
|
||||||
|
let full = press(&mut whole, &below, true, &[]);
|
||||||
|
|
||||||
|
let (_d2, mut half, below) = plate();
|
||||||
|
let nodes = half.model.nodes().len() as u64;
|
||||||
|
assert_eq!(whole.model.nodes().len() as u64, 2 * nodes - on_plane, "the mirror shares the plane's nodes");
|
||||||
|
let r = half.execute("model.boundary_condition.symmetry", &json!({"name": "Sym", "datum": "Diag", "bodies": [below]})).unwrap();
|
||||||
|
let h = 1.0 / 2f64.sqrt();
|
||||||
|
assert_eq!(r["faces"], json!(1), "{r}");
|
||||||
|
let n: Vec<f64> = r["plane"].as_array().unwrap().iter().map(|v| v.as_f64().unwrap()).collect();
|
||||||
|
assert!((n[0] - h).abs() < 1e-15 && (n[1] + h).abs() < 1e-15 && n[2] == 0.0, "{r}");
|
||||||
|
assert_eq!(half.model.resolve_set("Sym").unwrap().nodes.len() as u64, on_plane);
|
||||||
|
let halved = press(&mut half, &below, false, &["Sym"]);
|
||||||
|
|
||||||
|
assert_eq!(full.len(), halved.len());
|
||||||
|
let size = full.values().fold(0.0f64, |m, u| m.max(u.iter().map(|c| c * c).sum::<f64>().sqrt()));
|
||||||
|
let worst = halved.iter().map(|(l, u)| (0..3).map(|k| (u[k] - full[l][k]).powi(2)).sum::<f64>().sqrt()).fold(0.0f64, f64::max);
|
||||||
|
eprintln!("half vs whole plate on the inclined plane: largest deflection {size:.6e}, difference {:.1e}", worst / size);
|
||||||
|
assert!(size > 1e-4 && worst < 1e-9 * size, "{worst} of {size}");
|
||||||
|
// On the plane the nodes move along it: n · u = 0.
|
||||||
|
for l in half.model.resolve_set("Sym").unwrap().nodes {
|
||||||
|
let u = halved[&u64::from(l)];
|
||||||
|
assert!((u[0] - u[1]).abs() * h < 1e-12 * size, "node {l}: {u:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hostile_params_change_nothing() {
|
||||||
let (_d, mut s) = bar(true);
|
let (_d, mut s) = bar(true);
|
||||||
s.execute("geometry.datum", &json!({"name": "Tilt", "by": {"type": "point_normal", "point": [0, 0, 0], "normal": [1, 1, 0]}})).unwrap();
|
|
||||||
s.execute("geometry.datum", &json!({"name": "Out", "by": {"type": "offset", "plane": "xy", "offset": 100}})).unwrap();
|
s.execute("geometry.datum", &json!({"name": "Out", "by": {"type": "offset", "plane": "xy", "offset": 100}})).unwrap();
|
||||||
|
s.execute("geometry.datum", &json!({"name": "Tilt", "by": {"type": "point_normal", "point": [100, 0, 0], "normal": [1, 1, 0]}})).unwrap();
|
||||||
s.execute("geometry.datum.axis", &json!({"name": "Ax", "by": {"type": "principal", "axis": "x"}})).unwrap();
|
s.execute("geometry.datum.axis", &json!({"name": "Ax", "by": {"type": "principal", "axis": "x"}})).unwrap();
|
||||||
let model = s.model.clone();
|
let model = s.model.clone();
|
||||||
let e = s.execute("model.boundary_condition.symmetry", &json!({"name": "S", "datum": "Tilt"})).unwrap_err();
|
|
||||||
assert!(e.to_string().contains("not supported yet"), "{e}");
|
|
||||||
for p in [
|
for p in [
|
||||||
json!({"name": "S", "datum": "Out"}),
|
json!({"name": "S", "datum": "Out"}),
|
||||||
|
json!({"name": "S", "datum": "Tilt"}),
|
||||||
json!({"name": "S", "datum": "Ax"}),
|
json!({"name": "S", "datum": "Ax"}),
|
||||||
json!({"name": "S", "datum": "Nope"}),
|
json!({"name": "S", "datum": "Nope"}),
|
||||||
json!({"name": "S", "datum": "Mid", "set": "nope"}),
|
json!({"name": "S", "datum": "Mid", "set": "nope"}),
|
||||||
|
|||||||
@@ -258,6 +258,9 @@ enum Shape {
|
|||||||
/// A prescribed temperature (a boundary condition or a temperature field): a stem off the
|
/// A prescribed temperature (a boundary condition or a temperature field): a stem off the
|
||||||
/// part ending in a bulb, a thermometer.
|
/// part ending in a bulb, a thermometer.
|
||||||
Temperature,
|
Temperature,
|
||||||
|
/// A support along a unit direction that is no axis (a symmetry plane's normal): a
|
||||||
|
/// constraint triangle along it, from outside the part.
|
||||||
|
Support(Vec3),
|
||||||
/// A prescribed motion along a unit direction: an arrow leaving the node from a short bar,
|
/// 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).
|
/// telling it apart from a force (an arrow onto the node) and a support (triangles).
|
||||||
Prescribed(Vec3),
|
Prescribed(Vec3),
|
||||||
@@ -568,7 +571,11 @@ impl Builder<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// A symmetry plane shows its normal only: the rotations it holds follow from it.
|
// A symmetry plane shows its normal only: the rotations it holds follow from it.
|
||||||
if matches!(bc, BoundaryCondition::Symmetry { .. }) {
|
if let BoundaryCondition::Symmetry { plane, .. } = bc {
|
||||||
|
if let Some(n) = plane.inclined().and_then(|n| Vec3::from(n).normalized()) {
|
||||||
|
let (spots, total) = self.spots(bc.target());
|
||||||
|
return self.group(owner, Shape::Support(n), spots, Vec::new(), total, 2.4);
|
||||||
|
}
|
||||||
dofs &= 0b000_111;
|
dofs &= 0b000_111;
|
||||||
}
|
}
|
||||||
if dofs == 0 {
|
if dofs == 0 {
|
||||||
@@ -789,7 +796,7 @@ impl Glyphs {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let color = match g.shape {
|
let color = match g.shape {
|
||||||
Shape::Constraint | Shape::Prescribed(_) => colors.constraint,
|
Shape::Constraint | Shape::Support(_) | Shape::Prescribed(_) => colors.constraint,
|
||||||
Shape::Thermal | Shape::Temperature | Shape::Convection | Shape::HeatFlux => colors.thermal,
|
Shape::Thermal | Shape::Temperature | Shape::Convection | Shape::HeatFlux => colors.thermal,
|
||||||
Shape::Surface => colors.contact,
|
Shape::Surface => colors.contact,
|
||||||
_ => colors.load,
|
_ => colors.load,
|
||||||
@@ -1105,6 +1112,14 @@ impl Draw<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Shape::Support(n) => {
|
||||||
|
let dir = match s.normal {
|
||||||
|
Some(f) if f.dot(n) > 0.1 => -n,
|
||||||
|
_ => n,
|
||||||
|
};
|
||||||
|
let (e1, e2) = perpendiculars(n);
|
||||||
|
self.triangle(p, dir, u, 0.45 * u, e1, e2);
|
||||||
|
}
|
||||||
Shape::Force(dir) => {
|
Shape::Force(dir) => {
|
||||||
let (len, head) = if spread { (2.5 * u, 0.8 * u) } else { (4.0 * u, u) };
|
let (len, head) = if spread { (2.5 * u, 0.8 * u) } else { (4.0 * u, u) };
|
||||||
self.arrow(p - dir * len, dir, len, head);
|
self.arrow(p - dir * len, dir, len, head);
|
||||||
@@ -1182,7 +1197,13 @@ impl Draw<'_> {
|
|||||||
self.line(*a, ring.get((k + 1) % 4).copied().unwrap_or(*a));
|
self.line(*a, ring.get((k + 1) % 4).copied().unwrap_or(*a));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Shape::Constraint | Shape::Force(_) | Shape::Moment(_) | Shape::Thermal | Shape::Temperature | Shape::Prescribed(_) => {}
|
Shape::Constraint
|
||||||
|
| Shape::Support(_)
|
||||||
|
| Shape::Force(_)
|
||||||
|
| Shape::Moment(_)
|
||||||
|
| Shape::Thermal
|
||||||
|
| Shape::Temperature
|
||||||
|
| Shape::Prescribed(_) => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1356,6 +1377,12 @@ mod tests {
|
|||||||
let s = beam(BoundaryCondition::Symmetry { target: end(), plane: fieldcraft_model::Symmetry::Y });
|
let s = beam(BoundaryCondition::Symmetry { target: end(), plane: fieldcraft_model::Symmetry::Y });
|
||||||
assert_eq!(s.lines.len(), 6);
|
assert_eq!(s.lines.len(), 6);
|
||||||
assert_eq!(near(&bases(&s, Vec3::ZERO), -Vec3::Y * u), 2);
|
assert_eq!(near(&bases(&s, Vec3::ZERO), -Vec3::Y * u), 2);
|
||||||
|
// An inclined one shows its normal: one triangle along it.
|
||||||
|
let plane = fieldcraft_model::Symmetry::from_normal([1.0, 1.0, 0.0]).unwrap();
|
||||||
|
let s = beam(BoundaryCondition::Symmetry { target: end(), plane });
|
||||||
|
assert_eq!(s.lines.len(), 6);
|
||||||
|
let n = Vec3::new(1.0, 1.0, 0.0).normalized().unwrap();
|
||||||
|
assert_eq!(near(&bases(&s, Vec3::ZERO), -n * u), 2);
|
||||||
// A rotation alone keeps its place behind the node, apart from it.
|
// A rotation alone keeps its place behind the node, apart from it.
|
||||||
let s = beam(BoundaryCondition::Displacement { target: end(), values: [None, None, None, Some(0.0), None, None] });
|
let s = beam(BoundaryCondition::Displacement { target: end(), values: [None, None, None, Some(0.0), None, None] });
|
||||||
assert_eq!(s.lines.len(), 6);
|
assert_eq!(s.lines.len(), 6);
|
||||||
|
|||||||
Reference in new issue
Block a user