{"version":3,"sources":["cabinet-doors.mjs"],"sourcesContent":["// CABINET DOORS (the owner's picks, Oct 21, on the cabinet doors proposal page; and the owner, Oct 21: \"the entire texture\n// should be twin side by side doors and they should not extend beyond 90 degrees. The inside of the cabinet should be\n// black without textures. It should be possible to make cabinets out of wood or metal\"). Pure rules, byte-identical in\n// three twins: public/cabinet-doors.mjs, functions/lib/cabinet-doors.mjs, zone-server/lib/cabinet-doors.mjs.\n//\n// THE PICKS:\n//   animation 'soft'  soft-close hinges. Each door pivots on its OUTER FRONT edge: the left door about the vertical edge\n//                     at the front face's left, the right door about its right. Open: 350 ms to 90°, easing out\n//                     (a = easeOutCubic(p)·90°). Close: 650 ms, slowing right down over the last few degrees so the doors\n//                     meet without a sound: the closed fraction eases out as a quartic, 1 − (1 − s)^4 for s 0 → 1 over the\n//                     650 ms, so a = (1 − s)^4·90°. (Read literally, \"(1 − (1 − p)^4)·90° as p runs 1 → 0\" runs that curve\n//                     backwards and ends at its FASTEST: a slam, which the pick's own words rule out. This is the same\n//                     quartic, turned the way the words say.) Never past 90°.\n//   speed             350 open, 650 close; the bag opens at once (the doors never hold the slots back).\n//   close             the doors close when the bag (the cabinet's storage panel) closes.\n//   seen              everyone nearby sees them, as a door: the zone relays { t: 'cab', cells, o } to the world's others\n//                     (zone-server/lib/zones.js onCabinet), and closes a cabinet its opener left open when they leave.\n//   sound             the crate's sound by material, timed to the doors: the open at the start; the close only a very\n//                     soft touch as the doors meet (no slam).\n//   aim               any face opens it; the doors face the player who placed it (facing f, below).\n//   inside            plain black, unlit and untextured (#000000), nothing in it.\n//\n// THE FACING: a cabinet's doors look the way its placer stood (doors.mjs facingToward: 0 +tA, 1 −tA, 2 +tB, 3 −tB, the\n// cell's two tangents). It rides in the cabinet's public line in its chunk, { k: 'c', u, l, r, f }, written by\n// submitEdits with the placement and kept by every crate op since (functions/machines.js). A cabinet placed before (or a\n// town's, which has no line) faces CABINET_DEFAULT_FACING, +tB: the face the world drew its doors on until now; or, when\n// something stands in front of that face, the first open side (cabinetFallbackFacing).\n\nimport { baseItemOf } from './item-ids.mjs';\nimport { doorAxes, isFacing } from './doors.mjs';\n\nexport const CABINET = 'Cabinet';\nexport const CABINET_OPEN_MS = 350;\nexport const CABINET_CLOSE_MS = 650;\nexport const CABINET_MAX_DEG = 90;\nexport const CABINET_DEFAULT_FACING = 2;\n// A cabinet another player opened and never closed (their page went quiet without the zone noticing) shuts after this.\nexport const CABINET_PEER_MAX_MS = 10 * 60e3;\n\nexport const isCabinetName = (n) => typeof n === 'string' && baseItemOf(n) === CABINET;\n// Of wood or of metal: its sound is the crate's by material (metal clanks, wood creaks).\nexport const isMetalCabinet = (n) => isCabinetName(n) && /\\((Iron|Aluminum|Copper|Titanium|Steel)\\)$/.test(n);\n\n// ---- THE ANGLE ---------------------------------------------------------------------------------------------------------\nconst clamp01 = (p) => (p > 0 ? (p < 1 ? p : 1) : 0);\nexport const clampDeg = (a) => (a > 0 ? (a < CABINET_MAX_DEG ? a : CABINET_MAX_DEG) : 0);   // never past 90°, never inside\nexport const easeOutCubic = (p) => { const q = 1 - clamp01(p); return 1 - q * q * q; };\n// Opening: p runs 0 → 1 over CABINET_OPEN_MS.\nexport const openAngle = (p) => clampDeg(easeOutCubic(p) * CABINET_MAX_DEG);\n// Closing: s runs 0 → 1 over CABINET_CLOSE_MS; the closed fraction is 1 − (1 − s)^4, so the doors slow right down into the shut.\nexport const closeAngle = (s) => { const q = 1 - clamp01(s); return clampDeg(q * q * q * q * CABINET_MAX_DEG); };\n// Where on each curve an angle already stands, so a door turned back halfway carries on from where it is.\nconst openP = (a) => 1 - Math.cbrt(1 - clampDeg(a) / CABINET_MAX_DEG);\nconst closeS = (a) => 1 - Math.pow(clampDeg(a) / CABINET_MAX_DEG, 0.25);\n// THE DOORS' ANGLE NOW, in degrees, for a cabinet told `o` (1 open, 0 shut) at time `at` while its doors stood at `from`\n// degrees. `snap` (reduced motion) goes straight to the end. Returns { a, done }.\nexport function cabinetAngle(st, now, snap = false) {\n  const end = st && st.o ? CABINET_MAX_DEG : 0;\n  if (!st || snap) return { a: end, done: true };\n  const t = Math.max(0, now - (st.at || 0)), from = clampDeg(st.from || 0);\n  if (st.o) { const p = clamp01(openP(from) + t / CABINET_OPEN_MS); return { a: openAngle(p), done: p >= 1 }; }\n  const q = clamp01(closeS(from) + t / CABINET_CLOSE_MS);\n  return { a: closeAngle(q), done: q >= 1 };\n}\n// How long from `from` degrees to the end, in ms (the soft close's touch is timed to the doors meeting).\nexport function cabinetMsToEnd(o, from) {\n  return o ? Math.round((1 - openP(from)) * CABINET_OPEN_MS) : Math.round((1 - closeS(from)) * CABINET_CLOSE_MS);\n}\n\n// ---- THE DOORS IN THE CABINET'S OWN FRAME ------------------------------------------------------------------------------\n// X runs to the right as seen by someone facing the doors, Y up, Z out of the front face; (0, 0, 0) is the front face's\n// bottom-left corner as seen, so the face is x 0..1, y 0..1 at z 0. Each door is half the face wide. The left door's hinge\n// is its outer edge, x 0; the right's x 1. At angle a the left door turns by −a about Y and the right by +a (so each free\n// edge swings out toward the viewer): its free edge then stands at (½ cos a, ·, ½ sin a) and (1 − ½ cos a, ·, ½ sin a).\nexport function cabinetLeaves(aDeg) {\n  const a = clampDeg(aDeg) * Math.PI / 180, c = Math.cos(a), s = Math.sin(a);\n  return [\n    { side: 'left', hinge: [0, 0, 0], rotY: -a, free: [0.5 * c, 0, 0.5 * s], u0: 0, u1: 0.5 },\n    { side: 'right', hinge: [1, 0, 0], rotY: a, free: [1 - 0.5 * c, 0, 0.5 * s], u0: 0.5, u1: 1 },\n  ];\n}\n// THE FRAME IN THE GRID for a cabinet in a cell whose up is `up` ({ a, s }: doors.mjs cellUp) facing f: X, Y and Z as grid\n// unit vectors and the origin's offset from the cell's min corner. Right = Up × Out, so the frame is right-handed.\nexport function cabinetFrame(up, f) {\n  const ax = doorAxes(up, isFacing(f) ? f : CABINET_DEFAULT_FACING);\n  const Y = [0, 0, 0], Z = [0, 0, 0]; Y[ax.up] = ax.upS; Z[ax.nrm] = ax.nrmS;\n  const X = [Y[1] * Z[2] - Y[2] * Z[1], Y[2] * Z[0] - Y[0] * Z[2], Y[0] * Z[1] - Y[1] * Z[0]];\n  const origin = [0, 1, 2].map((k) => 0.5 - 0.5 * X[k] - 0.5 * Y[k] + 0.5 * Z[k]);\n  return { X, Y, Z, origin };\n}\n// The facing a cabinet's line gives it, or the default (a line before facings, a town's cabinet with none).\nexport const cabinetFacingOf = (line) => (line && isFacing(line.f) ? line.f : CABINET_DEFAULT_FACING);\n// A CABINET WITH NO FACING OF ITS OWN (one placed before facings, or a town's, which has no line): its doors on +tB, the\n// face the world first drew them on, unless something stands there and another side is open; then the first open side of\n// +tB, −tB, +tA, −tA (so a town's cabinet set against a wall shows its doors to the room). `isOpen(f)`: is the cell in\n// front of that face free? Every page asks the same world, so everyone sees the doors on the same face.\nexport function cabinetFallbackFacing(isOpen) {\n  for (const f of [CABINET_DEFAULT_FACING, 3, 0, 1]) if (isOpen(f)) return f;\n  return CABINET_DEFAULT_FACING;\n}\n// Does the world draw its front on the cell's first tangent (tA) rather than the second (tB, the atlas's own)? Then the\n// atlas's front and side tiles trade places (machine-icons.mjs machineAtlasCanvas st.swap). Since the doors became real\n// boards (below) the world's cabinet wears its body on every side, so it no longer asks.\nexport const cabinetFrontOnTA = (f) => isFacing(f) && f < 2;\n\n// ---- THE DOORS ARE ALWAYS 3D (the owner, Oct 21: Framed, doors always 3D) -----------------------------------------------\n// On the Cabinet Looks page: \"I'd like the cabinet texture to be more solid … without the bulging 3d elements. I would\n// like the doors to be 3d though\"; picks: the look 'Framed' (machine-icons.mjs), the doors a SIXTEENTH of a kube thick,\n// their edges and backs in the material's DARK tone, the body's front behind them plain black (the inside). Each door is\n// a board half the face wide and the face's full height, hinged on its outer front corner. Shut, it lies flat on the face;\n// open, it swings about that corner (cabinetLeaves) and the black shows.\n// …INSIDE ITS OWN CELL (the owner, Oct 22: \"the door/drawer parts should be within their designated kube-space sizes when\n// closed, right now the door/drawer parts are in adjacent kube spaces\"): a shut board fills the OUTERMOST sixteenth of\n// its cell, z −T..0 in the cabinet's frame (it stood proud of the face, z 0..T, in the cell in front, until Oct 22), and\n// the body is drawn recessed at the front by T to make room (system-view.js, the 'm:' cell's aRecess). The hinge is still\n// the cell's outer front corner (x 0 or 1, z 0), so a door swung to 90° turns about the cell's outer front edge and stands\n// flush with its side, never into the body (every point of a board keeps z ≥ −T as it turns).\nexport const CABINET_DOOR_THICK = 1 / 16;\n// THE SHUT BOARDS IN THE GRID for a cabinet whose cell's up is `up`, facing f: each door's box from the cell's min corner\n// (min, max on the grid's axes), and its face's axes as grid unit vectors: U along the face from the hinge to the seam (the\n// door's look runs x 0 at the hinge to x 9 at the seam), V down it (the look's y), N its thin axis, out of the front.\n// Each lies in the cell's outermost T on the front axis (z −T..0: within [0, 1] of the cell on every axis).\n// The world merges these into one instanced layer (system-view.js cabinetLayerBuild); an opening door is a live mesh.\nexport function cabinetShutBoards(up, f, T = CABINET_DOOR_THICK) {\n  const fr = cabinetFrame(up, f);\n  const at = (x, y, z) => [0, 1, 2].map((k) => fr.origin[k] + fr.X[k] * x + fr.Y[k] * y + fr.Z[k] * z);\n  return cabinetLeaves(0).map((L) => {\n    const a = at(L.u0, 0, -T), b = at(L.u1, 1, 0);\n    return { side: L.side, min: a.map((v, k) => Math.min(v, b[k])), max: a.map((v, k) => Math.max(v, b[k])),\n      U: fr.X.map((v) => (L.side === 'left' ? v : -v) || 0), V: fr.Y.map((v) => -v || 0), N: fr.Z.slice() };\n  });\n}\n\n// ---- THE RELAY ---------------------------------------------------------------------------------------------------------\n// A cabinet opened or shut on this page goes to the zone as { t: 'cab', id, cells: [{ c, k }], o }, and the zone hands\n// the world's others { t: 'cab', addr, cells, o }. A cell is its chunk id and its whole-cell key (persist.js's shapes).\nconst CHUNK_RE = /^-?\\d{1,3}_-?\\d{1,3}_-?\\d{1,3}$/, CELL_KEY_RE = /^\\d{1,3}_\\d{1,3}_\\d{1,3}$/;\nexport const CABINET_RELAY_MAX = 4;\nexport function cabinetRelayMsg(id, cells, o) { return { t: 'cab', id, cells: (cells || []).map((x) => ({ c: x.c, k: x.k })), o: o ? 1 : 0 }; }\nexport function validCabinetRelay(msg) {\n  return !!(msg && msg.t === 'cab' && Array.isArray(msg.cells) && msg.cells.length >= 1 && msg.cells.length <= CABINET_RELAY_MAX\n    && msg.cells.every((x) => x && typeof x.c === 'string' && CHUNK_RE.test(x.c) && typeof x.k === 'string' && CELL_KEY_RE.test(x.k))\n    && (msg.o === 0 || msg.o === 1));\n}\nexport const cabinetCellKey = (x) => x.c + '~' + x.k;\n"],"mappings":"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","names":[],"file":"cabinet-doors.mjs"}