Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion examples/shell-coupling/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ The pipeline is fully automatic from the STEP file:
each to a shell mid-surface facet carrying that wall's own thickness
(`extractThinWallShells`). The holder's walls run 0.6–5.4 mm.
3. **Keep the bulk bodies solid** (pin, hook); **weld** the pin↔hook contact that
otherwise meets at only a couple of coincidental nodes (`tieSolidBodies`).
otherwise meets at only a couple of coincidental nodes. Netgen meshes the two
bodies independently, so the raw mesh joins them at exactly two nodes — a
hinge, not a joint. The tie is *declared* by the caller (`buildCoupledModel`'s
`ties`, naming the hook eye face and the pin body); it is no longer detected
from geometry.
4. **Couple** the shell holder to the solid with a **distributing (RBE3)**
constraint — transmits force *and* moment across the mid-surface offset,
tolerant of the non-conforming interface (`buildCoupledModel`).
Expand Down
33 changes: 28 additions & 5 deletions examples/shell-coupling/crane-holder-shell.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
import { loadEngine, meshStep, extractThinWallShells, shellWallTets, buildCoupledModel, dropCouplingsOnFixedNodes } from "./lib.mjs";
import { loadEngine, meshStep, extractThinWallShells, shellWallTets, buildCoupledModel, dropCouplingsOnFixedNodes, surfaceVertices } from "./lib.mjs";

const here = dirname(fileURLToPath(import.meta.url));
const STEP = join(here, "../../test_files/full-crane-hook.step");
Expand All @@ -30,6 +30,17 @@ const STEEL = { young_modulus: 210000, poisson_ratio: 0.3 }; // MPa
const BC_FIXED_FACE = 7;
const LOAD_FACES = { 66: [0, -1000, 0], 67: [0, -1000, 0] };

// The hook hangs from the pin through a clearance fit: face 65 is the hook eye,
// the pin is body 2. Netgen meshes the two bodies independently, so in the raw
// mesh they touch at exactly TWO coincidental nodes — a hinge free to rotate
// about the line through them, not a joint. Until #417 that tie was detected
// automatically; it is now declared by the caller, so name it here. Without it
// the hook is a mechanism, the load has a component in the null space of K, and
// the solve diverges into a CG breakdown (KOF-214).
const TIE_HOOK_FACE = 65;
const TIE_PIN_BODY = 2;
const TIE_CLEARANCE = 3.0; // mm — measured pin/eye gap is ~2.5 mm

const vtuArg = process.argv.indexOf("--vtu");
const vtuPath = vtuArg >= 0 ? process.argv[vtuArg + 1] : null;

Expand All @@ -52,14 +63,22 @@ console.log(
const wallTets = shellWallTets(mesh, shells);

// ── 4. build the coupled node pool + distributing couplings ────────────────────
// Pin/hook/base stay separate solid bodies joined by distributing couplings; a
// gapped pin/hole interface becomes a force-and-moment tie, not a sparse hinge.
const model = buildCoupledModel(mesh, shells, wallTets);
// The holder and pin are meshed conformally; the hook is its own body across a
// clearance, so it is tied explicitly. A gapped pin/eye interface becomes a
// force-and-moment tie, not a sparse hinge.
const { byBody, byFace } = surfaceVertices(mesh);
const pinVerts = byBody.get(TIE_PIN_BODY), hookVerts = byFace.get(TIE_HOOK_FACE);
if (!pinVerts?.size || !hookVerts?.size)
throw new Error(`pin/hook tie found no surface (pin body ${TIE_PIN_BODY}: ${pinVerts?.size ?? 0} verts, hook face ${TIE_HOOK_FACE}: ${hookVerts?.size ?? 0}) — check the body/face ids`);
const ties = [{ verticesA: [...pinVerts], verticesB: [...hookVerts], maxSeparation: TIE_CLEARANCE }];
const model = buildCoupledModel(mesh, shells, wallTets, { ties });
const nSolid = model.solidPool.size, nShell = model.shellPool.length;
console.log(
`coupled model: ${model.pool.length / 3} nodes (${nSolid} solid + ${nShell} shell), ` +
`${model.tets.length / 4} tets, ${model.triangles.length / 3} shell tris, ` +
`${model.coupling.ref.length} distributing couplings`,
`${model.coupling.ref.length} couplings ` +
`(${model.coupling.mpc.filter((k) => k === 1).length} shell/solid seam, ` +
`${model.coupling.mpc.filter((k) => k === 0).length} pin/eye tie)`,
);

// ── 5. boundary conditions + loads (by CAD face) ───────────────────────────────
Expand Down Expand Up @@ -104,6 +123,10 @@ const r = Module.solve_coupled(
ref: Int32Array.from(coupling.ref),
offsets: Int32Array.from(coupling.offsets),
solid: Int32Array.from(coupling.solid),
// Per-coupling kind: the shell/solid seam is continuous material (relaxed
// MPC), the pin/eye tie spans a clearance (distributing). Dropping this
// silently solved the seam as distributing.
mpc: Int32Array.from(coupling.mpc),
},
{ fixed_dofs: Int32Array.from(fixed), load_dofs: Int32Array.from(load_dofs), load_vals: Float64Array.from(load_vals) },
JSON.stringify({ solid: STEEL, shell: STEEL }),
Expand Down
25 changes: 25 additions & 0 deletions examples/shell-coupling/lib.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,31 @@ export function meshStep(Module, stepPath, { maxElementSize = 6 } = {}) {

const pt = (V, i) => [V[3 * i], V[3 * i + 1], V[3 * i + 2]];
const TET_FACES = [[0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3]];
/** Order-independent key identifying one triangular face by its vertices. */
const faceKey = (a, b, c) => [a, b, c].sort((x, y) => x - y).join(",");

/**
* Surface vertices of a meshed assembly, grouped by the body that owns them and
* by CAD face id — the two ways a tie connection names a surface (a body picked
* in the model tree, a face picked in the viewport). Vertex indices are into
* `mesh.V`, which is what `buildCoupledModel`'s `ties` expect.
*/
export function surfaceVertices(mesh) {
const owner = new Map();
for (let e = 0; e < mesh.tet.length / 4; e++) {
const p = [mesh.tet[4 * e], mesh.tet[4 * e + 1], mesh.tet[4 * e + 2], mesh.tet[4 * e + 3]];
for (const f of TET_FACES) owner.set(faceKey(p[f[0]], p[f[1]], p[f[2]]), mesh.body[e]);
}
const byBody = new Map(), byFace = new Map();
const bucket = (m, k) => m.get(k) ?? m.set(k, new Set()).get(k);
for (let t = 0; t < mesh.surfFace.length; t++) {
const tri = [mesh.surfTri[3 * t], mesh.surfTri[3 * t + 1], mesh.surfTri[3 * t + 2]];
const b = bucket(byBody, owner.get(faceKey(...tri)));
const f = bucket(byFace, mesh.surfFace[t]);
for (const v of tri) { b.add(v); f.add(v); }
}
return { byBody, byFace };
}

// Squared distance from a point to a triangle (Ericson, Real-Time Collision
// Detection §5.1.5). Mirrors web/src/lib/shellize.ts.
Expand Down
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"examples:generate-crane-shell": "bun ../examples/web-examples/generate-crane-shell.mjs",
"examples:generate-plate-hole-shell": "bun ../examples/web-examples/generate-plate-hole-shell.mjs",
"pretest": "bun run wasm:fetch",
"test": "bun ../examples/validation/run.mjs && bun ../examples/validation/dof-constraint.test.mjs && bun ../examples/validation/prescribed-displacement.test.mjs && bun ../examples/validation/shell-prescribed-displacement.test.mjs && bun ../examples/validation/multiple-loads.test.mjs && bun tests/test_wall_bracket.mjs 20.0 && bun tests/test_multibody.mjs && bun tests/test_tie.mjs && bun tests/test_coupling.mjs && bun tests/test_reference_point.mjs && bun tests/test_face_pick.mjs && bun tests/test_edge_pick.mjs && bun tests/test_moment_load.mjs && bun tests/test_multi_load.mjs && bun tests/test_shellize_mpc.mjs && bun tests/test_mesh_sizing.mjs && bun tests/test_body_highlight.mjs && bun tests/test_midsurface_junction.mjs && bun tests/test_coupled_materials.mjs && playwright test",
"test": "bun ../examples/validation/run.mjs && bun ../examples/validation/dof-constraint.test.mjs && bun ../examples/validation/prescribed-displacement.test.mjs && bun ../examples/validation/shell-prescribed-displacement.test.mjs && bun ../examples/validation/multiple-loads.test.mjs && bun tests/test_wall_bracket.mjs 20.0 && bun tests/test_multibody.mjs && bun tests/test_tie.mjs && bun tests/test_coupling.mjs && bun tests/test_reference_point.mjs && bun tests/test_face_pick.mjs && bun tests/test_edge_pick.mjs && bun tests/test_moment_load.mjs && bun tests/test_multi_load.mjs && bun tests/test_shellize_mpc.mjs && bun tests/test_mesh_sizing.mjs && bun tests/test_body_highlight.mjs && bun tests/test_midsurface_junction.mjs && bun tests/test_coupled_materials.mjs && bun tests/test_crane_tie.mjs && bun ../examples/shell-coupling/crane-holder-shell.mjs && playwright test",
"test:coverage": "rm -rf .nyc_output coverage && COVERAGE=1 playwright test && bun run coverage:report",
"coverage:report": "nyc report && bun scripts/coverage-dead-code.ts",
"test:ui": "playwright test --ui",
Expand Down
Loading
Loading