structure.js
83 lines · in natura-src-1.0.0.zip as natura/structure.js · a module · its entry in the module reference · its test: structure-test.js
// structure.js -- Natura STRUCTURE (artifice / material, EXISTA): load, stability, and failure. artifact.js
// wears things out statistically; this resolves real FORCE through a structure. Load-bearing members share a
// weight; each has an Euler buckling limit Pcrit ~ stiffness/L^2; loaded past it a member BOWS (deflection grows
// as sqrt(P/Pcrit - 1) -- the pitchfork bifurcation of stability), and past a failure margin it gives way and
// sheds its load onto the rest -- which can push THEM over, cascading to collapse. Bracing (shorter effective L)
// raises Pcrit and buys capacity. Three September pieces converge here (The Load It Cannot Bear = the buckling
// bifurcation, As High As It Holds = truss + cascade, Hold Fast = joints). Deterministic mean-field. UMD.
(function (root, factory) {
if (typeof module !== 'undefined' && module.exports) module.exports = factory();
else root.Structure = factory();
})(typeof self !== 'undefined' ? self : this, function () {
'use strict';
const clamp = (v, a, b) => v < a ? a : v > b ? b : v;
// config: { members:[{L,section}] | {N,L,section,spread}, stiffness, load, loadRate, failMargin, braceFactor, maxDtDays, name }
function makeStructure(config) {
config = config || {};
const stiffness = config.stiffness != null ? config.stiffness : 1.0;
const failMargin = config.failMargin != null ? config.failMargin : 1.6; // buckles at Pcrit, fails at failMargin*Pcrit
const brace = config.braceFactor != null ? config.braceFactor : 1.0; // <1 = shorter effective length (braced) -> higher Pcrit
const loadRate = config.loadRate != null ? config.loadRate : 0; // load added per day (ramp); 0 = static `load`
const maxDd = config.maxDtDays != null ? config.maxDtDays : 4;
const pcritOf = (L, section) => stiffness * section / ((L * brace) * (L * brace)); // Euler: ~ EI / (kL)^2
function buildMembers() {
if (Array.isArray(config.members)) return config.members.map(m => ({ L: m.L, section: m.section != null ? m.section : 1, pcrit: pcritOf(m.L, m.section != null ? m.section : 1), load: 0, deflect: 0, failed: false }));
const N = (config.members && config.members.N) || config.N || 8;
const L = (config.members && config.members.L) || config.L || 1;
const section = (config.members && config.members.section) || config.section || 1;
const spread = (config.members && config.members.spread) || config.spread || 0; // vary L so failure is progressive
const out = [];
for (let i = 0; i < N; i++) { const Li = L * (1 + spread * ((i / Math.max(1, N - 1)) - 0.5) * 2); out.push({ L: Li, section, pcrit: pcritOf(Li, section), load: 0, deflect: 0, failed: false }); }
return out;
}
// resolve the equilibrium at a given total load: share equally among standing members, fail the overloaded
// (weakest first), redistribute, repeat until stable -- a cascade. Returns members updated in place.
function resolve(members, W) {
for (let iter = 0; iter < members.length + 2; iter++) {
const standing = members.filter(m => !m.failed);
if (!standing.length) break;
const share = W / standing.length;
// find the standing member most over its failure limit
let worst = null; for (const m of standing) { const over = share - m.pcrit * failMargin; if (over > 0 && (!worst || over > worst._over)) { worst = m; worst._over = over; } }
if (!worst) { for (const m of standing) { m.load = share; m.deflect = share > m.pcrit ? Math.sqrt(share / m.pcrit - 1) : 0; } break; }
worst.failed = true; worst.load = 0; worst.deflect = 0; // it gives way, shedding its load onto the rest
}
return members;
}
return {
name: config.name || 'structure',
domain: 'artifice',
scale: 'local',
params: config,
init() { const members = buildMembers(); const W0 = config.load != null ? config.load : 0; resolve(members, W0); return { members, W: W0 }; },
step(st, ctx, dt) {
const dd = clamp(dt / 86400, 0, maxDd); if (dd <= 0) return st;
st.W += loadRate * dd;
resolve(st.members, st.W);
return st;
},
// OUT: per-member load/deflect/failed; standing count, failedFraction, maxDeflect, carried load, collapsed
fields(st) {
let standing = 0, maxD = 0, carried = 0;
for (const m of st.members) { if (!m.failed) { standing++; carried += m.load; if (m.deflect > maxD) maxD = m.deflect; } }
return { N: st.members.length, members: st.members, load: +st.W.toFixed(4), standing,
failedFraction: +(1 - standing / st.members.length).toFixed(3), maxDeflect: +maxD.toFixed(4),
carried: +carried.toFixed(4), collapsed: standing === 0 };
},
events(st) {
const f = this.fields(st), out = [];
if (f.collapsed) out.push({ type: 'collapse', load: f.load });
else if (f.maxDeflect > 0) out.push({ type: 'buckling', maxDeflect: f.maxDeflect });
return out;
}
};
}
return { makeStructure };
});