diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 024b6fb..28afaec 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -23,6 +23,7 @@ is committed and listed here with its exports. | docs/media/examples/heated-bracket.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the solved heated-bracket sample (docs/examples.md) | MIT OR Apache-2.0 | | docs/media/examples/bracket-hex.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the solved bracket-hex sample (docs/examples.md) | MIT OR Apache-2.0 | | docs/media/examples/crank-arm.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the solved crank-arm sample (docs/examples.md) | MIT OR Apache-2.0 | +| docs/media/examples/crank-mirror.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the solved crank-mirror sample (docs/examples.md) | MIT OR Apache-2.0 | | crates/decks/tests/golden/nastran/*.bdf | FieldCraft contributors | Written by our Nastran writer from our own test models (crates/decks/tests/nastran.rs) | MIT OR Apache-2.0 | | assets/icons/assembly.instance.svg, assets/icons/assembly.instance.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/assembly.instance.json` (`cargo xtask icons`) | MIT OR Apache-2.0 | | assets/icons/deck.read.svg, assets/icons/deck.read.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/deck.read.json` (`cargo xtask icons`) | MIT OR Apache-2.0 | diff --git a/ROADMAP.md b/ROADMAP.md index 44619b5..c34069a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -31,7 +31,7 @@ pressure) is a degree of freedom of the existing DOF framework, not a separate s | M2 Tet meshing and size field | Size field (global, per face and edge seeds, curvature and proximity; the `fieldcraft_mesh::size::SizeField` trait and edge-division helpers are in); Bowyer–Watson Delaunay (exact, symbolic perturbation, verified; `fieldcraft-mesh::delaunay`, done); robust CDT boundary recovery (Diazzi et al. 2023: implicit LNC Steiner points on segments, gift-wrapping facet recovery, inside/outside labelling, B-rep face tags on boundary tet faces, flip repair of rounding (§5.1) checked by the validity gate; `fieldcraft-mesh::cdt` and `mesh.tet.generate`, done: all 20 valid corpus parts); quality stage (done: `fieldcraft-mesh::tet` — curvature-graded size field, edge-collapse coarsening, constrained Delaunay refinement with boundary points on the B-rep faces and curves, ODT smoothing, Klingner–Shewchuk improvement, tet4 and tet10 with mid-side nodes on the B-rep, quality report; one validity criterion shared by the gate and both solvers; the filleted-bracket fillet stress converges, tests/oracle); mesh verify per set | in progress | | M3 First deck | Sets and surfaces bound to SolidCraft face, edge, vertex and body names, resolved after meshing; materials (isotropic and orthotropic elastic, density) with a unit-system label and consistency check; sections (solid, shell, beam profiles) assigned by set; element formulation per region mapped to each solver; BCs (fixed, displacement, symmetry); loads (force and moment on reference points, pressure, gravity); reference points with kinematic and distributing coupling; steps (linear static, frequency) with explicit per-step load and BC tables and output presets; `model.check` naming every missing or bad item; `model.*` and `mesh.tag` commands (model definition: done); writers for Abaqus input (done: `deck.write`), Nastran SOL 101/103 (done: `deck.write`) and LS-DYNA implicit (done); the `.fieldcraft` file (text definition, binary mesh, geometry references reopened on load, journal; atomic saves, autosave and recovery; undo/redo on the journal: done) | in progress | | M4 Own solver and first results | Element library per docs/elements.md (1D and 2D first); Linear static and modal solver (tet4/10, wedge6, hex8, hex20; sparse direct, Lanczos), verified against analytic solutions (solver: done — `fieldcraft-fem` kernels, `fieldcraft-solver` with nested dissection, sparse Cholesky and block Lanczos, `solve.static`/`solve.frequency`/`solve.probe`, patch, cantilever, Lamé, Kirsch and beam-frequency tests; reference points with kinematic and distributing couplings and moments by master–slave constraint elimination, verified by rigid motion, clamping, Saint-Venant tension, bending and tube torsion; 1D elements I: rod2/rod3 trusses, point masses, rotary inertia, springs and dashpots in both solvers and all three decks, verified by the method of joints, the three-bar truss, spring–mass, two-mass chain and torsional pendulum frequencies, and damped decay; 2D elements: plane stress, plane strain and axisymmetric tri3, tri6, quad4 (incompatible modes QM6), quad8 with two degrees of freedom per node, centrifugal loads, `mesh.planar` 2D meshes of polygons and planar faces, verified by patch tests, Kirsch, Lamé (plane strain and axisymmetric), rotating discs and cylinders, and a plane stress cantilever; deck mapping to Abaqus CPS/CPE/CAX, Nastran membranes, LS-DYNA ELFORM 12/13); shells and beams (done: quad4 MITC4 and tri3 MITC3 flat shells with a Hughes–Brezzi drilling rotation, two-node Timoshenko beams with profile properties and orientations, six degrees of freedom at shell and beam nodes shared with solids and trusses, section forces and surface stresses, verified by shell patch tests, Navier's plate, cantilever and portal-frame beams, and the Scordelis–Lo and pinched-cylinder benchmarks); results display (done: `results.*` commands, deformed shape to scale, banded contours of displacement, von Mises, principal and component stresses with a legend, mode shapes with their frequencies and animation, probing a node by clicking it, reaction force and moment sums per set, stale results marked after an edit; explicit steps: field frames by slider or animation with contours per frame, energy histories plotted) | started | -| M5 Structured hex | 2D quad meshing of faces (`fieldcraft-mesh::quad`: transfinite mapped meshes, free all-quad meshes of polygons with holes on prescribed boundary nodes, exact planar quad validity, shared `SizeField` and even edge divisions; done for planar faces); mapped hex of six-sided regions (trivariate TFI, curved faces) and sweep meshing (multi-level extrusions with imprinted cross-sections, revolutions, one-to-one sweeps through linking faces; interval assignment, hex8/hex20 on the geometry, B-rep face tags, `mesh.hex.generate`/`sweep`/`mapped`: done, 16 of 21 corpus parts all-hex); conforming interfaces between cells (done: `mesh.hex.blocks` meshes a part split with SolidCraft's Split Body into mapped blocks as one conforming mesh — composite faces for seams and tangent faces, interval assignment over all cells, shared faces meshed once, grading off holes and fillets, free-internal-face check; the filleted bracket in 30 cells, hex20 within 0.1 % of a fine tet10 tip deflection); partitions inside FieldCraft (done: datum planes and axes, `geometry.partition` cuts the open design by a datum plane, kept in the model and replayed when the design opens, the two faces of each cut paired by name); cells meshed each with its own method into one mesh (done: `mesh.cells` — hex where a method fits, else tet; matching interfaces merged node for node, others, such as hex next to tet, tied by dual mortar, verified by a tied hex–tet bar in tension to 1e-6); meshing half a symmetric part and mirroring it (done: `mesh.mirror` reflects hex8/hex20/tet4/tet10/wedge6 across a datum plane, right-handed, nodes on the plane merged, tags carried to the image half; a mirrored half bar equals the whole bar's mesh and bending deflection to 1e-12; or solving the half: `model.boundary_condition.symmetry` on a datum plane, a half bar with half the load bends as the whole to 1e-11); pyramid transitions; automatic decomposition into sweepable and mappable regions, proposed as SolidCraft splits | started | +| M5 Structured hex | 2D quad meshing of faces (`fieldcraft-mesh::quad`: transfinite mapped meshes, free all-quad meshes of polygons with holes on prescribed boundary nodes, exact planar quad validity, shared `SizeField` and even edge divisions; done for planar faces); mapped hex of six-sided regions (trivariate TFI, curved faces) and sweep meshing (multi-level extrusions with imprinted cross-sections, revolutions, one-to-one sweeps through linking faces; interval assignment, hex8/hex20 on the geometry, B-rep face tags, `mesh.hex.generate`/`sweep`/`mapped`: done, 16 of 21 corpus parts all-hex); conforming interfaces between cells (done: `mesh.hex.blocks` meshes a part split with SolidCraft's Split Body into mapped blocks as one conforming mesh — composite faces for seams and tangent faces, interval assignment over all cells, shared faces meshed once, grading off holes and fillets, free-internal-face check; the filleted bracket in 30 cells, hex20 within 0.1 % of a fine tet10 tip deflection); partitions inside FieldCraft (done: datum planes and axes, `geometry.partition` cuts the open design by a datum plane, kept in the model and replayed when the design opens, the two faces of each cut paired by name); cells meshed each with its own method into one mesh (done: `mesh.cells` — hex where a method fits, else tet; matching interfaces merged node for node, others, such as hex next to tet, tied by dual mortar, verified by a tied hex–tet bar in tension to 1e-6); meshing half a symmetric part and mirroring it (done: `mesh.mirror` reflects hex8/hex20/tet4/tet10/wedge6 across a datum plane, right-handed, nodes on the plane merged, tags carried to the image half; a mirrored half bar equals the whole bar's mesh and bending deflection to 1e-12; sample `crank-mirror`; or solving the half: `model.boundary_condition.symmetry` on a datum plane, a half bar with half the load bends as the whole to 1e-11); pyramid transitions; automatic decomposition into sweepable and mappable regions, proposed as SolidCraft splits | started | | M6 Automatic hex | Octree/grid all-hex for any part (first version: done — `fieldcraft-mesh::octree`, `mesh.hex.octree`: 3-refinement grid with conforming templates refined by curvature, thickness and narrow faces, a core cleared to a manifold boundary, feature map onto it, padding and a pillow per patch capturing every sharp curve, foldover-free fitting, certified validity, B-rep face tags; the corpus hex column and the filleted-bracket oracle case; next: transitions for concave refined regions so thin and curved parts stop over-refining); hex-dominant by tet recombination; polycube for blocky parts; layers through thin walls; mesh editing and orphan-mesh import | started | | M7 Assemblies, contact and friction | Needs M15. Part instances from SolidCraft occurrences; tie between non-matching meshes (mortar: done — `interaction.tie`, dual mortar constraints in the implicit analyses, Abaqus `*TIE` and LS-DYNA tied contact, the tie patch test); implicit contact: surface-to-surface mortar and node-to-surface, penalty and augmented Lagrange, self-contact, automatic pair detection (mortar contact pairs with Coulomb friction in nonlinear static steps: done — segment-to-segment mortar with the consistent tangent by automatic differentiation of the segmentation, augmented Lagrangian with Uzawa updates to a gap tolerance, semismooth active set with a chattering cutback, small sliding without nonlinear geometry and finite sliding with it, overclosure adjusted or refused, Coulomb friction (stick/slip return map, static and kinetic coefficients, symmetrised tangent), pressure, gap, status, shear and slip output; verified by the contact patch test, Hertz cylinder and sphere, Newton's quadratic rate and sliding blocks; the linear analyses refuse contact by name); friction: Coulomb with stick–slip, static/kinetic, anisotropic, pressure-, rate- and temperature-dependent coefficients; contact output (pressure, slip, status); thermal contact conductance and frictional heating; wear (Archard); bolt pretension, connectors with behaviour (elastic, damped, stops, locks); amplitudes, multiple steps, restart; nonlinear steps in all three decks. Verified by Hertz (sphere and cylinder), the Cattaneo–Mindlin partial-slip problem, the sliding block, NAFEMS contact benchmarks | started | | M8 Materials, explicit, shells, composites | Plasticity, hyperelasticity, damage data per solver; explicit dynamics decks (Abaqus/Explicit, LS-DYNA); midsurface shells; composite layups and material orientations, continuum shells on swept hex; hybrid (mixed u–p) hex8/hex20/tet10 for nearly incompressible materials (done); heat transfer (solids done: steady and θ-method transient conduction with flux, convection and sources, thermal strain in static steps (solids and 2D) and explicit dynamics (with plasticity), coupled temperature–displacement, `solve.heat`; deck output, 2D/shell/beam conduction planned) | started | diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index f7cbaf6..7453c77 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -97,6 +97,9 @@ fn open_sample(s: &mut Session, p: &Value) -> Result { if let Some(recipe) = sample.design { opened.design = Some(Arc::new(open_design(&fieldcraft_geometry::script_design(recipe)?)?)); } + if let Some(prepare) = sample.prepare { + opened.run_script(&prepare())?; + } let bodies = opened.design.as_ref().map_or(&[][..], |d| d.bodies.as_slice()); let script = (sample.script)(bodies); opened.run_script(&script)?; diff --git a/crates/engine/src/cmd/forms.rs b/crates/engine/src/cmd/forms.rs index fad8f90..9c255fe 100644 --- a/crates/engine/src/cmd/forms.rs +++ b/crates/engine/src/cmd/forms.rs @@ -895,6 +895,7 @@ pub const SAMPLE: &[Field] = &[choice( ("heated-bracket", "Heated bracket"), ("bracket-hex", "Hex-meshed bracket"), ("crank-arm", "Bicycle crank arm"), + ("crank-mirror", "Mirrored crank arm"), ], ) .req() diff --git a/crates/engine/src/sample.rs b/crates/engine/src/sample.rs index 1231d8f..ca1f72b 100644 --- a/crates/engine/src/sample.rs +++ b/crates/engine/src/sample.rs @@ -27,6 +27,9 @@ pub struct Sample { /// The SolidCraft script (`{"commands": [...]}`, the corpus recipe form) that builds the /// sample's geometry, opened as its design before [`Sample::script`] runs. pub design: Option<&'static str>, + /// Commands run on the opened design before [`Sample::script`] (datums and partitions), so + /// the script is given the bodies as they cut it. + pub prepare: Option Value>, /// The model's commands, given the design's bodies (none without a design). pub script: fn(&[Shape]) -> Value, } @@ -38,6 +41,7 @@ pub const SAMPLES: &[Sample] = &[ label: "Hex and Tet Parts", about: "A hex8 block beside a tet4 cube: meshes to look at, nothing to solve", design: None, + prepare: None, script: |_| script(), }, Sample { @@ -45,6 +49,7 @@ pub const SAMPLES: &[Sample] = &[ label: "Cantilever", about: "A clamped steel beam with an end load: a static and a frequency step, ready to solve", design: None, + prepare: None, script: |_| cantilever(), }, Sample { @@ -52,6 +57,7 @@ pub const SAMPLES: &[Sample] = &[ label: "Bar Impact", about: "The steel beam flying onto a rigid wall: an explicit dynamic step, ready to solve", design: None, + prepare: None, script: |_| impact(), }, Sample { @@ -59,6 +65,7 @@ pub const SAMPLES: &[Sample] = &[ label: "Drop Test", about: "An aluminium L-bracket dropped 2 m onto a tilted rigid floor: explicit dynamics, the strike and its stress waves", design: Some(UPRIGHT_BRACKET), + prepare: None, script: |_| drop_test(), }, Sample { @@ -66,6 +73,7 @@ pub const SAMPLES: &[Sample] = &[ label: "Rubber Seal", about: "A rubber gasket ring squeezed 25 % between bonded plates: nonlinear static, Mooney–Rivlin, large strain", design: None, + prepare: None, script: |_| rubber_seal(), }, Sample { @@ -73,6 +81,7 @@ pub const SAMPLES: &[Sample] = &[ label: "Flow Past a Bracket", about: "Air flowing past an L-bracket on the floor: lattice Boltzmann flow and the force on each face", design: Some(UPRIGHT_BRACKET), + prepare: None, script: |_| flow_past(), }, Sample { @@ -80,6 +89,7 @@ pub const SAMPLES: &[Sample] = &[ label: "Heated Bracket", about: "A steel bracket clamped at its holes and heated at one end: steady heat conduction and the thermal stress", design: Some(FILLETED_BRACKET), + prepare: None, script: |_| heated_bracket(), }, Sample { @@ -87,13 +97,23 @@ pub const SAMPLES: &[Sample] = &[ label: "Hex-Meshed Bracket", about: "The filleted bracket split into 30 cells and meshed all-hex (hex20): clamped, loaded, von Mises", design: Some(PARTITIONED_BRACKET), + prepare: None, script: bracket_hex, }, + Sample { + name: "crank-mirror", + label: "Mirrored Crank Arm", + about: "The crank arm cut on its symmetry plane, one half swept all-hex (hex20) and mirrored into one conforming mesh; loaded as crank-arm", + design: Some(CRANK_ARM), + prepare: Some(crank_mirror_prepare), + script: crank_mirror, + }, Sample { name: "crank-arm", label: "Bicycle Crank Arm", about: "An aluminium bicycle crank swept all-hex (hex20): square bore fixed, a rider's weight on the pedal, von Mises", design: Some(CRANK_ARM), + prepare: None, script: crank_arm, }, ]; @@ -484,10 +504,37 @@ pub const PEDAL_LOAD: f64 = 1500.0; /// spindle axis and the R1.5 cylinders beside it, the pedal hole is the Ø14 cylinder on the /// pedal axis. pub fn crank_arm(bodies: &[Shape]) -> Value { - let [spindle, pedal] = crank_faces(bodies); + crank_model(bodies, vec![json!({"command": "mesh.hex.generate", "params": {"size": 3, "order": 2, "method": "extrude"}})]) +} + +/// [`CRANK_ARM`] cut on its symmetry plane (`Sym`, y = 0; partition `Half`) by +/// [`crank_mirror_prepare`]: the half `CrankArm/b` (y > 0) swept all-hex (hex20 of 3 mm) and +/// mirrored onto `CrankArm/a`, one conforming mesh; then the model of [`crank_arm`]. +pub fn crank_mirror(bodies: &[Shape]) -> Value { + crank_model( + bodies, + vec![ + json!({"command": "mesh.hex.generate", "params": {"body": "CrankArm/b", "size": 3, "order": 2, "method": "extrude"}}), + json!({"command": "mesh.mirror", "params": {"plane": "Sym", "bodies": ["CrankArm/b"]}}), + ], + ) +} + +/// The crank arm's symmetry plane and the partition by it. +fn crank_mirror_prepare() -> Value { json!([ - {"command": "model.units", "params": {"system": "mm-t-s-N-MPa"}}, - {"command": "mesh.hex.generate", "params": {"size": 3, "order": 2, "method": "extrude"}}, + {"command": "geometry.datum", "params": {"name": "Sym", "by": {"type": "offset", "plane": "zx", "offset": 0}}}, + {"command": "geometry.partition", "params": {"name": "Half", "datum": "Sym"}}, + ]) +} + +/// The crank's model after the `mesh` commands: material, the spindle fixed, the rider on the +/// pedal, step `Pedal`. +fn crank_model(bodies: &[Shape], mesh: Vec) -> Value { + let [spindle, pedal] = crank_faces(bodies); + let mut script = vec![json!({"command": "model.units", "params": {"system": "mm-t-s-N-MPa"}})]; + script.extend(mesh); + script.extend(json_array(json!([ {"command": "model.set.create", "params": {"name": "crank", "geometry": bodies.iter().map(|b| json!({"body": b.name})).collect::>()}}, {"command": "model.set.create", "params": {"name": "spindle", "geometry": spindle}}, {"command": "model.set.create", "params": {"name": "pedal", "geometry": pedal}}, @@ -496,7 +543,15 @@ pub fn crank_arm(bodies: &[Shape]) -> Value { {"command": "model.boundary_condition.create", "params": {"name": "Spindle", "type": "fixed", "target": {"set": "spindle"}}}, {"command": "model.load.create", "params": {"name": "Rider", "type": "force", "target": {"set": "pedal"}, "vector": [0, -PEDAL_LOAD, 0]}}, {"command": "model.step.create", "params": {"name": "Pedal", "procedure": {"type": "static"}, "boundary_conditions": ["Spindle"], "loads": ["Rider"]}}, - ]) + ]))); + Value::Array(script) +} + +fn json_array(v: Value) -> Vec { + match v { + Value::Array(a) => a, + other => vec![other], + } } /// The crank's square-bore faces and pedal-hole faces as set members, found by their surfaces. @@ -762,6 +817,57 @@ mod tests { assert!((reaction["sets"][0]["force"][1].as_f64().unwrap() - PEDAL_LOAD).abs() < 1e-6, "{reaction}"); } + /// The mirrored crank opens as one conforming all-hex20 mesh: its two halves alike, joined + /// on the symmetry plane, every element valid, and the spindle and pedal faces found on both + /// halves (the image's faces carry the names of the half they are the image of). + #[test] + fn the_mirrored_crank_opens_as_one_mesh_of_two_alike_halves() { + let mut s = open("crank-mirror"); + let elements = s.model.elements().values().filter(|e| e.kind == fieldcraft_model::ElementKind::Hex20).count(); + assert_eq!(elements, s.model.elements().len()); + let mut half = |body: &str| { + let set = format!("half_{}", body.replace('/', "_")); + s.execute("model.set.create", &json!({"name": set, "geometry": [{"body": body}]})).unwrap(); + s.model.resolve_set(&set).unwrap() + }; + let (a, b) = (half("CrankArm/a"), half("CrankArm/b")); + assert_eq!(a.elements.len(), b.elements.len()); + assert_eq!(a.elements.len() + b.elements.len(), elements); + assert_eq!(a.faces.len(), b.faces.len(), "the halves' skins are alike"); + // Joined on the plane: nodes shared by both halves, none duplicated there. + let shared = a.nodes.iter().filter(|n| b.nodes.binary_search(n).is_ok()).count(); + assert!(shared > 100, "{shared}"); + assert_eq!(a.nodes.len() + b.nodes.len() - shared, s.model.nodes().len()); + let q = s.execute("mesh.quality", &json!({})).unwrap(); + assert_eq!(q["valid"].as_u64().unwrap() as usize, elements, "{q}"); + let members = |set: &str| s.model.sets().get(set).map_or(Vec::new(), |x| x.geometry.iter().map(|g| g.body.clone()).collect::>()); + for set in ["spindle", "pedal"] { + let bodies = members(set); + assert!(bodies.iter().any(|b| b == "CrankArm/a") && bodies.iter().any(|b| b == "CrankArm/b"), "{set}: {bodies:?}"); + assert!(!s.model.resolve_set(set).unwrap().faces.is_empty(), "{set}"); + } + } + + /// Solved, the mirrored crank bends as the swept whole one does (σxx at the root within 3 % of + /// M/Z) and the spindle carries the whole load. + #[test] + #[ignore = "heavy: cargo xtask test --heavy (CI heavy job)"] + fn the_mirrored_crank_bends_as_the_beam_formula_says() { + let mut s = open("crank-mirror"); + s.execute("solve.run", &json!({})).unwrap(); + let half_width: f64 = 19.0 - 7.0 * 29.0 / 170.0; + let probe = s.execute("solve.probe", &json!({"step": "Pedal", "at": [[29.0, -half_width, 8.0], [29.0, half_width, 8.0]]})).unwrap(); + let sxx = |i: usize| probe["nodes"][i]["stress"][0].as_f64().unwrap(); + let hand = PEDAL_LOAD * (170.0 - 29.0) / (16.0 * (2.0 * half_width).powi(2) / 6.0); + for (i, sign) in [(0, -1.0), (1, 1.0)] { + assert!((sign * sxx(i) - hand).abs() < 0.03 * hand, "{} against {hand} MPa: {probe}", sxx(i)); + } + // Mirror images: the two sides' bending stresses are equal and opposite. + assert!((sxx(0) + sxx(1)).abs() < 0.01 * hand, "{probe}"); + let reaction = s.execute("results.reactions", &json!({"step": "Pedal", "sets": ["spindle"]})).unwrap(); + assert!((reaction["sets"][0]["force"][1].as_f64().unwrap() - PEDAL_LOAD).abs() < 1e-6, "{reaction}"); + } + #[test] fn the_drop_test_opens_ready_to_solve() { let s = open("drop-test"); diff --git a/docs/examples.md b/docs/examples.md index d0c56a3..6de3373 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -112,6 +112,24 @@ peaks at 295 MPa at the upright's hole. solution of the unpartitioned part (0.5093 mm, `crates/testkit/tests/bracket_blocks.rs`), and the clamp carries the whole 1 kN. +## Mirrored crank arm (`crank-mirror`) + +![Von Mises stress on the mirrored crank arm: one swept half and its mirror image, one mesh](media/examples/crank-mirror.png) + +The crank arm of `crank-arm`, built from a half. A datum plane on its symmetry plane (`Sym`, +y = 0) partitions it (`Half`) into `CrankArm/a` and `CrankArm/b`; the half `CrankArm/b` is swept +all-hex (hex20 of 3 mm, 3 330 elements) and `mesh.mirror` reflects it onto `CrankArm/a`, joined +on the plane: 6 660 hex20 elements, 32 933 nodes, all valid (scaled Jacobian min 0.009, mean 0.91). +The image's faces carry `CrankArm/a`'s names, so the sets pick the square bore and the pedal hole +on both halves. Material, supports and load as `crank-arm`. + +**Look at:** the two halves' meshes, mirror images of each other across the arm's middle (Mesh +workspace), and the datum plane `Sym` before solving. + +**Check:** σxx on the arm's sides at x = 29 mm is −62.43 and +62.43 MPa, equal and opposite as +the halves are, against M/Z = 62.5 MPa (0.2 %); the pedal bore moves 0.75 mm down, as on the whole +swept crank; the spindle carries the whole 1500 N. + ## Bicycle crank arm (`crank-arm`) ![Von Mises stress on the all-hex crank arm, the arm root and the square bore carrying the most](media/examples/crank-arm.png) diff --git a/docs/media/examples/crank-mirror.png b/docs/media/examples/crank-mirror.png new file mode 100644 index 0000000..857756f Binary files /dev/null and b/docs/media/examples/crank-mirror.png differ