← Natura·Docs/Modules/Source

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 };
});