diff --git a/ROADMAP.md b/ROADMAP.md index 78fa652..bb965ca 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, few-ulp rounding repair checked by the validity gate; `fieldcraft-mesh::cdt` and `mesh.tet.generate`, done: all 20 valid corpus parts); Delaunay refinement, ODT smoothing, sliver removal; tet4 and tet10 with mid-side nodes on the B-rep; 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 and tet–hex transition; 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); sweepable cells in a conforming mesh and tet–hex transition; automatic decomposition into sweepable and mappable regions, proposed as SolidCraft splits | started | | M6 Automatic hex | Octree/grid-dual all-hex for any part; hex-dominant by tet recombination; polycube for blocky parts; smoothing, untangling, boundary padding, layers through thin walls; mesh editing and orphan-mesh import | planned | | M7 Assemblies, contact and friction | Needs M15. Part instances from SolidCraft occurrences; tie between non-matching meshes (mortar); implicit contact: surface-to-surface mortar and node-to-surface, penalty and augmented Lagrange, self-contact, automatic pair detection with a bounding-volume search shared with explicit contact; 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 | planned | | 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/testkit/src/corpus.rs b/crates/testkit/src/corpus.rs index 07943ac..b9b705b 100644 --- a/crates/testkit/src/corpus.rs +++ b/crates/testkit/src/corpus.rs @@ -40,6 +40,7 @@ pub const CATEGORIES: &[(&str, &str)] = &[ ("blocky", "axis-aligned blocks (polycube-friendly)"), ("organic", "free-form, lofted or revolved spline surfaces"), ("assembly", "several bodies placed by component transforms"), + ("partitioned", "split into cells that each mesh as a mapped block (conforming multi-block hex)"), ("analytic", "closed-form geometry for solver verification"), ]; diff --git a/crates/testkit/src/runner.rs b/crates/testkit/src/runner.rs index f02827c..513e727 100644 --- a/crates/testkit/src/runner.rs +++ b/crates/testkit/src/runner.rs @@ -15,7 +15,7 @@ use std::time::Instant; use crate::corpus::{self, CATEGORIES, Part}; use fieldcraft_geometry::{Design, FaceSurface, Measure, SurfaceMesh}; use fieldcraft_mesh::cdt::{CdtOptions, tetrahedralize}; -use fieldcraft_mesh::hex::{HexOptions, Surface, hex_mesh}; +use fieldcraft_mesh::hex::{Cell, HexOptions, Surface, hex_mesh, hex_mesh_cells}; use fieldcraft_mesh::size::UniformSize; use fieldcraft_mesh::surface::{BoundarySurface, SurfaceOptions}; @@ -123,7 +123,8 @@ const REASON_CHARS: usize = 240; /// Structured all-hex meshing (`fieldcraft_mesh::hex`): the method that meshed each body, the /// element count and the smallest scaled Jacobian of the hex8 mesh and of the hex20 mesh (mid-edge -/// nodes on the geometry), or why no structured method fits. +/// nodes on the geometry), or why no structured method fits. A `partitioned` part's bodies are +/// the cells of one conforming mesh (`blocks`). struct StructuredHex; impl Stage for StructuredHex { @@ -144,6 +145,12 @@ impl Stage for StructuredHex { let size = UniformSize(HEX_RELATIVE_SIZE * diagonal); let linear = HexOptions { chordal_tolerance: l.tolerance, ..HexOptions::default() }; let quadratic = HexOptions { quadratic: true, ..linear.clone() }; + if l.part.categories.iter().any(|c| c == "partitioned") { + return blocks(l, &size, &linear, &quadratic).map(|mut cells| { + cells.push(format!("{:.0}", start.elapsed().as_secs_f64() * 1e3)); + cells + }); + } let (mut methods, mut elements, mut sj8, mut sj20) = (Vec::new(), 0usize, f64::INFINITY, Ok(f64::INFINITY)); for (body, mesh) in l.design.bodies.iter().zip(&l.surfaces) { let surface_options = SurfaceOptions { weld_distance: mesh.weld_distance, ..SurfaceOptions::default() }; @@ -180,6 +187,39 @@ impl Stage for StructuredHex { } } +/// The bodies of a partitioned part as the cells of one conforming mesh: method, elements and +/// the hex8 and hex20 smallest scaled Jacobians. +fn blocks(l: &Loaded, size: &UniformSize, linear: &HexOptions, quadratic: &HexOptions) -> Result, String> { + let mut surfaces = Vec::with_capacity(l.surfaces.len()); + let mut faces: Vec>> = Vec::with_capacity(l.surfaces.len()); + for (body, mesh) in l.design.bodies.iter().zip(&l.surfaces) { + let options = SurfaceOptions { weld_distance: mesh.weld_distance, ..SurfaceOptions::default() }; + surfaces.push(BoundarySurface::new(&mesh.positions, &mesh.triangles, &mesh.tri_face, &options).map_err(|e| format!("{}: {e}", body.name))?); + faces.push(body.face_surfaces().map_err(|e| format!("{}: {e}", body.name))?.into_iter().map(|f| f.map(analytic)).collect()); + } + let cells: Vec = + l.design.bodies.iter().zip(&surfaces).zip(&faces).map(|((b, surface), faces)| Cell { name: &b.name, surface, faces }).collect(); + let m = match hex_mesh_cells(&cells, size, linear) { + Ok(m) => m, + Err(e) => { + return Ok(vec![ + format!("no: {}", e.to_string().chars().take(REASON_CHARS).collect::()), + String::new(), + String::new(), + String::new(), + ]); + } + }; + let sj20 = + hex_mesh_cells(&cells, size, quadratic).map_or_else(|e| format!("error: {e}"), |q| format!("{:.3}", q.mesh.report.min_scaled_jacobian)); + Ok(vec![ + format!("blocks ({} cells, {} shared faces)", cells.len(), m.interfaces), + m.mesh.element_count().to_string(), + format!("{:.3}", m.mesh.report.min_scaled_jacobian), + sj20, + ]) +} + /// The geometry's analytic face surface in the mesher's terms. pub fn analytic(s: FaceSurface) -> Surface { match s { @@ -330,6 +370,17 @@ mod tests { } } + /// The partitioned bracket's 30 cells mesh into one conforming hex mesh. + #[test] + fn partitioned_bracket_meshes_as_blocks() { + let part = corpus::read_recipe(&corpus::corpus_dir().join("filleted-bracket-blocks")).unwrap(); + let (loaded, _) = load(&part).unwrap(); + assert_eq!(loaded.design.bodies.len(), 30); + let cells = StructuredHex.run(&loaded).unwrap(); + assert!(cells[0].starts_with("blocks (30 cells"), "{cells:?}"); + assert!(cells[2].parse::().unwrap() > 0.5, "{cells:?}"); + } + /// The constrained Delaunay tetrahedralization passes the gate on parts that exercise each /// of its stages: an assembly whose rounding needs a few-ulp repair, cavities refilled by /// gift-wrapping, and a holed plate. diff --git a/crates/testkit/tests/bracket_blocks.rs b/crates/testkit/tests/bracket_blocks.rs new file mode 100644 index 0000000..3e82338 --- /dev/null +++ b/crates/testkit/tests/bracket_blocks.rs @@ -0,0 +1,96 @@ +//! The filleted bracket meshed all-hex by partitioning (`tests/corpus/filleted-bracket-blocks`): +//! the upright's end clamped, the foot's end pushed down by 1 kN (steel). The conforming hex8 and +//! hex20 meshes converge: the tip deflection, the von Mises stress at the middle of the fillet and +//! the peak (at the upright's hole) agree between hex20 at 3 mm and 2 mm, and hex8 approaches them. +//! For reference, a 48 016-element tet10 mesh of the unpartitioned part (curvature-refined, from +//! the tet quality pipeline) gives a tip deflection of 0.5093 mm, 133.2 MPa at the fillet and a +//! peak of 291.8 MPa; hex20 at 2 mm gives 0.5089 mm, 137.4 MPa and 297.0 MPa. +#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] + +use fieldcraft_engine::Session; +use fieldcraft_testkit::corpus; +use serde_json::{Value, json}; + +fn open(part: &str) -> (Session, std::path::PathBuf) { + let parts = corpus::parts().unwrap(); + let p = parts.iter().find(|p| p.name == part).unwrap(); + let dir = std::env::temp_dir().join(format!("fieldcraft-bracket-{}-{part}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("part.solidcraft"); + std::fs::write(&path, corpus::design_bytes(p).unwrap()).unwrap(); + let mut s = Session::default(); + s.execute("geometry.open", &json!({"path": path})).unwrap(); + (s, dir) +} + +/// Every (body, face) whose name is `face` (a face split into cells keeps its name in each). +fn faces_named(s: &mut Session, face: &str) -> Vec { + let bodies = s.execute("geometry.bodies", &json!({})).unwrap(); + let mut out = Vec::new(); + for b in bodies.as_array().unwrap() { + for f in b["faces"].as_array().unwrap() { + if f["name"] == face { + out.push(json!({"body": b["name"], "face": face})); + } + } + } + out +} + +/// Clamp the upright's top (y = 50), push the foot's end (x = 60) down by 1 kN; tip deflection, +/// von Mises at the middle of the fillet and the largest von Mises. +fn analyse(s: &mut Session) -> (f64, f64, f64) { + let top = faces_named(s, "F2:side:l5"); + let tip = faces_named(s, "F2:side:l2"); + assert!(!top.is_empty() && !tip.is_empty()); + for (name, geometry) in [ + ( + "all", + json!( + s.execute("geometry.bodies", &json!({})).unwrap().as_array().unwrap().iter().map(|b| json!({"body": b["name"]})).collect::>() + ), + ), + ("top", json!(top)), + ("tip", json!(tip)), + ] { + s.execute("model.set.create", &json!({"name": name, "geometry": geometry})).unwrap(); + } + s.execute("model.material.create", &json!({"name": "Steel", "elastic": {"type": "isotropic", "young": 210000.0, "poisson": 0.3}})).unwrap(); + s.execute("model.section.create", &json!({"name": "solid", "set": "all", "material": "Steel", "kind": {"type": "solid"}})).unwrap(); + s.execute("model.boundary_condition.create", &json!({"name": "clamp", "type": "fixed", "target": {"set": "top"}})).unwrap(); + s.execute("model.load.create", &json!({"name": "push", "type": "force", "target": {"set": "tip"}, "vector": [0.0, -1000.0, 0.0]})).unwrap(); + s.execute("model.step.create", &json!({"name": "Load", "procedure": {"type": "static"}, "boundary_conditions": ["clamp"], "loads": ["push"]})) + .unwrap(); + let r = s.execute("solve.static", &json!({})).unwrap(); + let probe = s.execute("solve.probe", &json!({"step": "Load", "at": [[60.0, 4.0, 20.0], [9.757359312880714, 9.757359312880714, 20.0]]})).unwrap(); + let nodes = probe["nodes"].as_array().unwrap(); + let tip = nodes[0]["displacement"][1].as_f64().unwrap(); + let fillet = nodes[1]["von_mises"].as_f64().unwrap(); + (tip, fillet, r["max_von_mises"]["value"].as_f64().unwrap()) +} + +#[test] +#[ignore = "heavy: cargo xtask test --heavy (CI heavy job)"] +fn partitioned_bracket_converges_with_hex8_and_hex20() { + let mut results = Vec::new(); + for (order, size) in [(1, 2.0), (2, 3.0), (2, 2.0)] { + let (mut s, dir) = open("filleted-bracket-blocks"); + let m = s.execute("mesh.hex.blocks", &json!({"size": size, "order": order})).unwrap(); + assert_eq!(m["cells"], 30, "{m}"); + assert!(m["quality"]["min_scaled_jacobian"].as_f64().unwrap() > 0.6, "{m}"); + let r = analyse(&mut s); + eprintln!("order {order}, size {size}: {} elements; tip {:.5} mm, fillet {:.2} MPa, peak {:.2} MPa", m["elements"], r.0, r.1, r.2); + results.push(r); + std::fs::remove_dir_all(dir).ok(); + } + let close = |a: f64, b: f64, rel: f64| (a - b).abs() <= rel * b.abs(); + let [hex8, coarse, fine] = [results[0], results[1], results[2]]; + // The tip moves down; hex20 has converged on it, hex8 is within a few percent (stiffer). + assert!(fine.0 < 0.0); + assert!(close(coarse.0, fine.0, 0.005), "{coarse:?} {fine:?}"); + assert!(close(hex8.0, fine.0, 0.04) && hex8.0.abs() < fine.0.abs(), "{hex8:?} {fine:?}"); + // Stresses at the fillet and the peak. + assert!(close(coarse.1, fine.1, 0.06), "{coarse:?} {fine:?}"); + assert!(close(coarse.2, fine.2, 0.02), "{coarse:?} {fine:?}"); + assert!(close(hex8.1, fine.1, 0.15) && close(hex8.2, fine.2, 0.10), "{hex8:?} {fine:?}"); +} diff --git a/docs/meshing/corpus.md b/docs/meshing/corpus.md index fb1ea2e..5a4cfc1 100644 --- a/docs/meshing/corpus.md +++ b/docs/meshing/corpus.md @@ -10,28 +10,29 @@ clock on the machine that ran it; every other column is deterministic. | part | categories | bodies | faces | triangles | tol. mm | build ms | enclosed mm³ | SolidCraft mm³ | rel. diff | exact mm³ | boundary | genus | feature edges | check ms | CDT tets | Steiner | cavities | tet gate | CDT ms | hex | hex elements | hex8 min SJ | hex20 min SJ | hex ms | |---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| -| bent-tube | sweepable, thin-wall, genus | 1 | 18 | 4296 | 0.121 | 2726 | 4213.1 | 4159.4 | 1.3e-2 | 4159.8 | ok, 2148 vertices | 1 | 268 | 19.8 | 15550 | 2338 | 0 | pass | 4070 | sweep | 192 | 0.859 | 0.997 | 535 | -| bolted-assembly | assembly, blocky | 3 | 22 | 284 | 0.117 | 1141 | 33004.7 | 32999.0 | 1.7e-4 | 32998.9 | ok, 144 vertices | 0, 1, 1 | 104 | 1.6 | 690 | 181 | 0 | pass | 211 | extrude | 357 | 0.398 | 0.222 | 139 | -| cantilever-i-beam | sweepable, thin-wall | 1 | 14 | 44 | 0.208 | 483 | 61600.0 | 61600.0 | 0.0e0 | 61600.0 | ok, 24 vertices | 0 | 36 | 0.2 | 90 | 24 | 0 | pass | 5 | extrude | 336 | 0.707 | 0.707 | 47 | -| cross-cylinders | boolean, sharp | 1 | 10 | 1395 | 0.087 | 59348 | 32197.4 | 32357.8 | -5.0e-3 | 32365.8 | error: Cross: invalid boundary surface: 28 defects; triangles 142 and 184 are oriented inconsistently across edge 0–396 at (-15.488656021315903, 4.577998041166734, 7.853092194999726); triangles 184 and 239 are oriented inconsistently across edge 0–554 at (-18.75000000000002, 5.634596901641276, 7.632255089815496); edge 103–108 at (-0.16007784063084018, 0.03984236936862052, 9.998639270596865) is shared by 3 triangles; … | | | | error: Cross: invalid boundary surface: 28 defects; triangles 142 and 184 are oriented inconsistently across edge 0–396 at (-15.488656021315903, 4.577998041166734, 7.853092194999726); triangles 184 and 239 are oriented inconsistently across edge 0–554 at (-18.75000000000002, 5.634596901641276, 7.632255089815496); edge 103–108 at (-0.16007784063084018, 0.03984236936862052, 9.998639270596865) is shared by 3 triangles; … | | | | | error: Cross: invalid boundary surface: 28 defects; triangles 142 and 184 are oriented inconsistently across edge 0–396 at (-15.488656021315903, 4.577998041166734, 7.853092194999726); triangles 184 and 239 are oriented inconsistently across edge 0–554 at (-18.75000000000002, 5.634596901641276, 7.632255089815496); edge 103–108 at (-0.16007784063084018, 0.03984236936862052, 9.998639270596865) is shared by 3 triangles; … | | | | | -| filleted-bracket | fillet, genus, sweepable | 1 | 13 | 540 | 0.088 | 3706 | 32162.3 | 32145.0 | 5.4e-4 | 32144.8 | ok, 268 vertices | 2 | 135 | 2.1 | 5582 | 1613 | 0 | pass | 1769 | no: Bracket: not supported yet: no structured hex method fits this body (extrude: cylindrical face 8 is not parallel to the direction; sweep: from face 1: linking face 6 has 2 boundary loops (periodic or holed linking faces are not swept); from face 6: | | | | 10 | -| i-beam | sweepable, thin-wall | 1 | 14 | 44 | 0.208 | 280 | 61600.0 | 61600.0 | 0.0e0 | 61600.0 | ok, 24 vertices | 0 | 36 | 0.3 | 94 | 24 | 0 | pass | 7 | extrude | 336 | 0.707 | 0.707 | 52 | -| loft-transition | organic | 1 | 6 | 292 | 0.075 | 397 | 56390.2 | 56450.6 | -1.1e-3 | – | ok, 148 vertices | 0 | 40 | 1.0 | 1329 | 256 | 1 | pass | 172 | sweep | 2873 | 0.189 | 0.189 | 2056 | -| perforated-plate | genus, thin-wall, sweepable | 1 | 54 | 1644 | 0.144 | 2864 | 26822.2 | 26764.6 | 2.2e-3 | 26764.2 | ok, 776 vertices | 24 | 828 | 10.6 | 33611 | 9798 | 0 | pass | 6513 | extrude | 467 | 0.304 | 0.033 | 480 | -| pinhole-block | small-feature, genus | 1 | 38 | 4044 | 0.073 | 2053 | 49919.4 | 49909.6 | 2.0e-4 | 49909.5 | ok, 1992 vertices | 16 | 1548 | 41.8 | 96411 | 30221 | 0 | pass | 28180 | extrude | 5236 | 0.408 | 0.392 | 3838 | -| rounded-box | fillet | 1 | 26 | 2540 | 0.054 | 1069 | 23325.7 | 23340.5 | -6.3e-4 | 23340.8 | ok, 1272 vertices | 0 | 216 | 11.5 | 11653 | 1914 | 0 | pass | 2265 | no: Box: not supported yet: no structured hex method fits this body (extrude: cylindrical face 8 is not parallel to the direction; sweep: from face 0: 12 faces are not on the sweep path; from face 5: 12 faces are not on the sweep path; revolve: plan | | | | 67 | -| shallow-groove | near-tangent, boolean | 1 | 8 | 62 | 0.075 | 218 | 47795.2 | 47782.3 | 2.7e-4 | 47781.5 | ok, 33 vertices | 0 | 24 | 0.2 | 267 | 87 | 0 | pass | 24 | extrude | 1960 | 0.991 | 0.986 | 1078 | -| sharp-wedge | sharp, thin-wall, sweepable | 1 | 5 | 8 | 0.083 | 72 | 3200.0 | 3200.0 | 0.0e0 | 3200.0 | ok, 6 vertices | 0 | 9 | 0.0 | 11 | 3 | 0 | pass | 1 | extrude | 294 | 0.050 | 0.050 | 87 | -| sphere-on-plate | near-tangent, boolean | 1 | 12 | 2826 | 0.098 | 3775 | 69353.3 | 69509.0 | -2.2e-3 | 69509.7 | ok, 1415 vertices | 0 | 124 | 8.3 | 4898 | 309 | 9 | pass | 1265 | no: Plate: not supported yet: no structured hex method fits this body (extrude: face 6 is curved across the direction; sweep: from face 0: 6 faces are not on the sweep path; from face 5: linking face 11 has 2 boundary loops (periodic or holed linking | | | | 75 | -| spline-vase | organic | 1 | 11 | 3890 | 0.106 | 1097 | 94891.2 | 94948.5 | -6.0e-4 | – | ok, 1947 vertices | 0 | 243 | 23.5 | 12461 | 1512 | 0 | pass | 2487 | sweep | 4554 | 0.649 | 0.649 | 3030 | -| stepped-block | blocky, sharp | 1 | 14 | 96 | 0.078 | 438 | 47000.0 | 47000.0 | 0.0e0 | 47000.0 | ok, 48 vertices | 1 | 60 | 0.6 | 223 | 41 | 0 | pass | 11 | extrude | 1416 | 0.999 | 0.999 | 906 | -| thin-ribs | thin-wall, blocky | 1 | 26 | 92 | 0.119 | 1085 | 32000.0 | 32000.0 | -4.5e-16 | 32000.0 | ok, 48 vertices | 0 | 72 | 0.7 | 171 | 33 | 0 | pass | 10 | extrude | 777 | 0.844 | 0.844 | 236 | -| thin-wall-enclosure | thin-wall, blocky | 1 | 11 | 36 | 0.108 | 189 | 15564.0 | 15564.0 | 0.0e0 | 15564.0 | ok, 20 vertices | 0 | 28 | 0.2 | 191 | 47 | 2 | pass | 8 | extrude | 894 | 0.297 | 0.297 | 349 | -| analytic-sphere | analytic | 1 | 6 | 2784 | 0.069 | 1340 | 33353.4 | 33507.9 | -4.6e-3 | 33510.3 | ok, 1394 vertices | 0 | 96 | 8.9 | 4211 | 58 | 0 | pass | 1430 | no: Sphere: not supported yet: no structured hex method fits this body (extrude: no direction to try; sweep: no planar face to sweep from; revolve: no axis to try; mapped: meshing failed: meshing failed: mesh validity gate: 8 inverted or degenerate ele | | | | 159 | -| analytic-torus | analytic, genus | 1 | 6 | 7230 | 0.109 | 1725 | 37644.1 | 37893.7 | -6.6e-3 | 37899.3 | ok, 3615 vertices | 1 | 180 | 31.7 | 19790 | 466 | 0 | pass | 4946 | revolve | 1134 | 0.706 | 0.706 | 1155 | -| analytic-cylinder | analytic, sweepable | 1 | 4 | 1004 | 0.049 | 368 | 12533.1 | 12564.7 | -2.5e-3 | 12566.4 | ok, 504 vertices | 0 | 106 | 9.3 | 7437 | 1568 | 0 | pass | 1797 | extrude | 3200 | 0.707 | 0.707 | 2316 | -| analytic-pipe | analytic, genus, sweepable | 1 | 6 | 1448 | 0.104 | 506 | 94150.9 | 94238.0 | -9.3e-4 | 94247.8 | ok, 724 vertices | 1 | 220 | 9.5 | 6238 | 1340 | 0 | pass | 1734 | extrude | 1288 | 0.998 | 0.998 | 1059 | -| analytic-plate-hole | analytic, genus, sweepable | 1 | 8 | 112 | 0.224 | 524 | 98449.1 | 98429.5 | 2.0e-4 | 98429.2 | ok, 56 vertices | 1 | 62 | 0.6 | 1868 | 543 | 0 | pass | 78 | extrude | 321 | 0.320 | 0.142 | 248 | +| bent-tube | sweepable, thin-wall, genus | 1 | 18 | 4296 | 0.121 | 428 | 4213.1 | 4159.4 | 1.3e-2 | 4159.8 | ok, 2148 vertices | 1 | 268 | 11.8 | 15550 | 2338 | 0 | pass | 598 | sweep | 192 | 0.859 | 0.997 | 62 | +| bolted-assembly | assembly, blocky | 3 | 22 | 284 | 0.117 | 53 | 33004.7 | 32999.0 | 1.7e-4 | 32998.9 | ok, 144 vertices | 0, 1, 1 | 104 | 0.7 | 690 | 181 | 0 | pass | 33 | extrude | 357 | 0.398 | 0.222 | 40 | +| cantilever-i-beam | sweepable, thin-wall | 1 | 14 | 44 | 0.208 | 4 | 61600.0 | 61600.0 | 0.0e0 | 61600.0 | ok, 24 vertices | 0 | 36 | 0.1 | 90 | 24 | 0 | pass | 3 | extrude | 336 | 0.707 | 0.707 | 33 | +| cross-cylinders | boolean, sharp | 1 | 10 | 1395 | 0.087 | 10640 | 32197.4 | 32357.8 | -5.0e-3 | 32365.8 | error: Cross: invalid boundary surface: 28 defects; triangles 142 and 184 are oriented inconsistently across edge 0–396 at (-15.488656021315903, 4.577998041166734, 7.853092194999726); triangles 184 and 239 are oriented inconsistently across edge 0–554 at (-18.75000000000002, 5.634596901641276, 7.632255089815496); edge 103–108 at (-0.16007784063084018, 0.03984236936862052, 9.998639270596865) is shared by 3 triangles; … | | | | error: Cross: invalid boundary surface: 28 defects; triangles 142 and 184 are oriented inconsistently across edge 0–396 at (-15.488656021315903, 4.577998041166734, 7.853092194999726); triangles 184 and 239 are oriented inconsistently across edge 0–554 at (-18.75000000000002, 5.634596901641276, 7.632255089815496); edge 103–108 at (-0.16007784063084018, 0.03984236936862052, 9.998639270596865) is shared by 3 triangles; … | | | | | error: Cross: invalid boundary surface: 28 defects; triangles 142 and 184 are oriented inconsistently across edge 0–396 at (-15.488656021315903, 4.577998041166734, 7.853092194999726); triangles 184 and 239 are oriented inconsistently across edge 0–554 at (-18.75000000000002, 5.634596901641276, 7.632255089815496); edge 103–108 at (-0.16007784063084018, 0.03984236936862052, 9.998639270596865) is shared by 3 triangles; … | | | | | +| filleted-bracket | fillet, genus, sweepable | 1 | 13 | 540 | 0.088 | 494 | 32162.3 | 32145.0 | 5.4e-4 | 32144.8 | ok, 268 vertices | 2 | 135 | 1.7 | 5582 | 1613 | 0 | pass | 233 | no: Bracket: not supported yet: no structured hex method fits this body (extrude: cylindrical face 8 is not parallel to the direction; sweep: from face 1: linking face 6 has 2 boundary loops (periodic or holed linking faces are not swept); from face 6: | | | | 5 | +| filleted-bracket-blocks | fillet, genus, partitioned | 30 | 190 | 712 | 0.088 | 1057 | 32169.8 | 32145.1 | 7.7e-4 | 32144.8 | ok, 416 vertices | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 | 474 | 1.7 | 1423 | 349 | 0 | pass | 64 | blocks (30 cells, 45 shared faces) | 4986 | 0.705 | 0.705 | 1440 | +| i-beam | sweepable, thin-wall | 1 | 14 | 44 | 0.208 | 234 | 61600.0 | 61600.0 | 0.0e0 | 61600.0 | ok, 24 vertices | 0 | 36 | 0.3 | 94 | 24 | 0 | pass | 8 | extrude | 336 | 0.707 | 0.707 | 74 | +| loft-transition | organic | 1 | 6 | 292 | 0.075 | 493 | 56390.2 | 56450.6 | -1.1e-3 | – | ok, 148 vertices | 0 | 40 | 1.5 | 1329 | 256 | 1 | pass | 80 | sweep | 2873 | 0.189 | 0.189 | 892 | +| perforated-plate | genus, thin-wall, sweepable | 1 | 54 | 1644 | 0.144 | 2861 | 26822.2 | 26764.6 | 2.2e-3 | 26764.2 | ok, 776 vertices | 24 | 828 | 10.6 | 33611 | 9798 | 0 | pass | 7636 | extrude | 467 | 0.304 | 0.033 | 555 | +| pinhole-block | small-feature, genus | 1 | 38 | 4044 | 0.073 | 4821 | 49919.4 | 49909.6 | 2.0e-4 | 49909.5 | ok, 1992 vertices | 16 | 1548 | 29.3 | 96411 | 30221 | 0 | pass | 23460 | extrude | 5236 | 0.408 | 0.392 | 1853 | +| rounded-box | fillet | 1 | 26 | 2540 | 0.054 | 1822 | 23325.7 | 23340.5 | -6.3e-4 | 23340.8 | ok, 1272 vertices | 0 | 216 | 15.1 | 11687 | 1921 | 0 | pass | 1081 | no: Box: not supported yet: no structured hex method fits this body (extrude: cylindrical face 6 is not parallel to the direction; sweep: from face 0: 12 faces are not on the sweep path; from face 5: 12 faces are not on the sweep path; revolve: plan | | | | 37 | +| shallow-groove | near-tangent, boolean | 1 | 8 | 62 | 0.075 | 266 | 47795.2 | 47782.3 | 2.7e-4 | 47781.5 | ok, 33 vertices | 0 | 24 | 0.4 | 267 | 87 | 0 | pass | 27 | extrude | 1960 | 0.991 | 0.986 | 500 | +| sharp-wedge | sharp, thin-wall, sweepable | 1 | 5 | 8 | 0.083 | 143 | 3200.0 | 3200.0 | 0.0e0 | 3200.0 | ok, 6 vertices | 0 | 9 | 0.0 | 11 | 3 | 0 | pass | 1 | extrude | 294 | 0.050 | 0.050 | 67 | +| sphere-on-plate | near-tangent, boolean | 1 | 12 | 2826 | 0.098 | 3307 | 69353.3 | 69509.0 | -2.2e-3 | 69509.7 | ok, 1415 vertices | 0 | 124 | 15.4 | 4898 | 309 | 9 | pass | 736 | no: Plate: not supported yet: no structured hex method fits this body (extrude: face 6 is curved across the direction; sweep: from face 0: 6 faces are not on the sweep path; from face 5: linking face 11 has 2 boundary loops (periodic or holed linking | | | | 55 | +| spline-vase | organic | 1 | 11 | 3890 | 0.106 | 1175 | 94891.2 | 94948.5 | -6.0e-4 | – | ok, 1947 vertices | 0 | 243 | 20.5 | 12461 | 1512 | 0 | pass | 2093 | sweep | 4554 | 0.649 | 0.649 | 846 | +| stepped-block | blocky, sharp | 1 | 14 | 96 | 0.078 | 32 | 47000.0 | 47000.0 | 0.0e0 | 47000.0 | ok, 48 vertices | 1 | 60 | 0.5 | 223 | 41 | 0 | pass | 8 | extrude | 1416 | 0.999 | 0.999 | 158 | +| thin-ribs | thin-wall, blocky | 1 | 26 | 92 | 0.119 | 68 | 32000.0 | 32000.0 | -4.5e-16 | 32000.0 | ok, 48 vertices | 0 | 72 | 0.6 | 171 | 33 | 0 | pass | 8 | extrude | 777 | 0.844 | 0.844 | 86 | +| thin-wall-enclosure | thin-wall, blocky | 1 | 11 | 36 | 0.108 | 9 | 15564.0 | 15564.0 | 0.0e0 | 15564.0 | ok, 20 vertices | 0 | 28 | 0.1 | 191 | 47 | 2 | pass | 6 | extrude | 894 | 0.297 | 0.297 | 100 | +| analytic-sphere | analytic | 1 | 6 | 2784 | 0.069 | 284 | 33353.4 | 33507.9 | -4.6e-3 | 33510.3 | ok, 1394 vertices | 0 | 96 | 7.0 | 4211 | 58 | 0 | pass | 358 | no: Sphere: not supported yet: no structured hex method fits this body (extrude: no direction to try; sweep: no planar face to sweep from; revolve: no axis to try; mapped: meshing failed: meshing failed: mesh validity gate: 8 inverted or degenerate ele | | | | 35 | +| analytic-torus | analytic, genus | 1 | 6 | 7230 | 0.109 | 627 | 37644.1 | 37893.7 | -6.6e-3 | 37899.3 | ok, 3615 vertices | 1 | 180 | 30.0 | 19790 | 466 | 0 | pass | 1091 | revolve | 1134 | 0.706 | 0.706 | 210 | +| analytic-cylinder | analytic, sweepable | 1 | 4 | 1004 | 0.049 | 32 | 12533.1 | 12564.7 | -2.5e-3 | 12566.4 | ok, 504 vertices | 0 | 106 | 3.4 | 7437 | 1568 | 0 | pass | 344 | extrude | 3200 | 0.707 | 0.707 | 409 | +| analytic-pipe | analytic, genus, sweepable | 1 | 6 | 1448 | 0.104 | 51 | 94150.9 | 94238.0 | -9.3e-4 | 94247.8 | ok, 724 vertices | 1 | 220 | 4.5 | 6238 | 1340 | 0 | pass | 327 | extrude | 1288 | 0.998 | 0.998 | 176 | +| analytic-plate-hole | analytic, genus, sweepable | 1 | 8 | 112 | 0.224 | 13 | 98449.1 | 98429.5 | 2.0e-4 | 98429.2 | ok, 56 vertices | 1 | 62 | 0.5 | 1868 | 543 | 0 | pass | 69 | extrude | 321 | 0.320 | 0.142 | 49 | ## Categories @@ -39,8 +40,8 @@ clock on the machine that ran it; every other column is deterministic. |---|---|---| | thin-wall | walls thin against the part's size | 7 | | small-feature | features much smaller than the part | 1 | -| fillet | blends between faces | 2 | -| genus | through holes (genus > 0) | 7 | +| fillet | blends between faces | 3 | +| genus | through holes (genus > 0) | 8 | | sharp | sharp edges and acute dihedral angles | 3 | | near-tangent | faces meeting at a shallow angle | 2 | | boolean | intersection curves from booleans | 3 | @@ -48,6 +49,7 @@ clock on the machine that ran it; every other column is deterministic. | blocky | axis-aligned blocks (polycube-friendly) | 4 | | organic | free-form, lofted or revolved spline surfaces | 2 | | assembly | several bodies placed by component transforms | 1 | +| partitioned | split into cells that each mesh as a mapped block (conforming multi-block hex) | 1 | | analytic | closed-form geometry for solver verification | 5 | ## Parts @@ -57,6 +59,7 @@ clock on the machine that ran it; every other column is deterministic. - `cantilever-i-beam`: The 200 mm I-beam of `i-beam` lying along X with its web upright (Z up): a sweep along a horizontal axis, and the cantilever of the README demo. - `cross-cylinders`: Two orthogonal R10 cylinders joined: equal radii, so the intersection curves cross at two singular points. - `filleted-bracket`: An 8 mm L-bracket with an R6 inside fillet and a hole through each leg: a blend beside holes. +- `filleted-bracket-blocks`: The filleted bracket split into 30 cells with SolidCraft's Split Body (three slabs across its width, the corner halved along its diagonal, an O-grid of four cells round each hole) so that every cell is a mapped block: conforming multi-block hex meshing. - `i-beam`: A 200 mm I-beam with 3 mm flanges and a 2 mm web: a long sweepable prism with thin walls. - `loft-transition`: A 40 mm square lofted to an R15 circle 50 mm above: a ruled free-form surface between unlike sections. - `perforated-plate`: A 3 mm plate perforated by a 6 x 4 grid of 6 mm holes: genus 24 in a thin sheet. diff --git a/tests/corpus/filleted-bracket-blocks/README.md b/tests/corpus/filleted-bracket-blocks/README.md new file mode 100644 index 0000000..4f638b8 --- /dev/null +++ b/tests/corpus/filleted-bracket-blocks/README.md @@ -0,0 +1 @@ +The filleted bracket split into 30 cells with SolidCraft's Split Body (three slabs across its width, the corner halved along its diagonal, an O-grid of four cells round each hole) so that every cell is a mapped block: conforming multi-block hex meshing. diff --git a/tests/corpus/filleted-bracket-blocks/recipe.json b/tests/corpus/filleted-bracket-blocks/recipe.json new file mode 100644 index 0000000..4802cf3 --- /dev/null +++ b/tests/corpus/filleted-bracket-blocks/recipe.json @@ -0,0 +1,39 @@ +{"corpus": {"categories": ["fillet", "genus", "partitioned"], "volume_mm3": 32144.778925}, + "commands": [ + {"command": "sketch.create", "params": {"plane": "XY", "name": "Profile"}}, + {"command": "sketch.line", "params": {"points": [[0, 0], [60, 0], [60, 8], [8, 8], [8, 50], [0, 50]], "closed": true}}, + {"command": "sketch.finish", "params": {}}, + {"command": "solid.extrude", "params": {"distance": 40, "body_name": "Bracket"}}, + {"command": "solid.fillet", "params": {"edges": [[8, 8, 20]], "radius": 6}}, + {"command": "solid.hole", "params": {"position": [40, 8, 20], "diameter": 8}}, + {"command": "solid.hole", "params": {"position": [8, 32, 20], "diameter": 8}}, + {"command": "solid.split_body", "params": {"body": "Bracket", "plane": {"origin": [0, 0, 30], "normal": [0, 0, 1]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1)", "plane": {"origin": [0, 0, 10], "normal": [0, 0, 1]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1) (1)", "plane": {"origin": [16, 0, 0], "normal": [1, 0, 0]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1) (1) (1)", "plane": {"origin": [0, 16, 0], "normal": [0, 1, 0]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1) (1) (1) (1)", "plane": {"origin": [0, 0, 0], "normal": [1, -1, 0]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1) (1)", "plane": {"origin": [30, 0, 0], "normal": [1, 0, 0]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1) (1)", "plane": {"origin": [50, 0, 0], "normal": [1, 0, 0]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1) (1) (1)", "plane": {"origin": [0, 22, 0], "normal": [0, 1, 0]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1) (1) (1)", "plane": {"origin": [0, 42, 0], "normal": [0, 1, 0]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1)", "plane": {"origin": [16, 0, 0], "normal": [1, 0, 0]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1) (1)'", "plane": {"origin": [0, 16, 0], "normal": [0, 1, 0]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1) (1)' (1)", "plane": {"origin": [0, 0, 0], "normal": [1, -1, 0]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1)", "plane": {"origin": [30, 0, 0], "normal": [1, 0, 0]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1)", "plane": {"origin": [50, 0, 0], "normal": [1, 0, 0]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1) (1)'", "plane": {"origin": [0, 22, 0], "normal": [0, 1, 0]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1) (1)'", "plane": {"origin": [0, 42, 0], "normal": [0, 1, 0]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket", "plane": {"origin": [16, 0, 0], "normal": [1, 0, 0]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1)'", "plane": {"origin": [0, 16, 0], "normal": [0, 1, 0]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1)' (1)", "plane": {"origin": [0, 0, 0], "normal": [1, -1, 0]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket", "plane": {"origin": [30, 0, 0], "normal": [1, 0, 0]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket", "plane": {"origin": [50, 0, 0], "normal": [1, 0, 0]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1)'", "plane": {"origin": [0, 22, 0], "normal": [0, 1, 0]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1)'", "plane": {"origin": [0, 42, 0], "normal": [0, 1, 0]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1) (1)'''", "plane": {"origin": [40, 0, 20], "normal": [1, 0, 1]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1) (1)'''", "plane": {"origin": [40, 0, 20], "normal": [1, 0, -1]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1) (1)''' (1)", "plane": {"origin": [40, 0, 20], "normal": [1, 0, -1]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1) (1)' (1)''", "plane": {"origin": [0, 32, 20], "normal": [0, 1, 1]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1) (1)' (1)''", "plane": {"origin": [0, 32, 20], "normal": [0, 1, -1]}}}, + {"command": "solid.split_body", "params": {"body": "Bracket (1) (1)' (1)'' (1)", "plane": {"origin": [0, 32, 20], "normal": [0, 1, -1]}}} + ]}