/** * Headless ez-tree geometry generator. * Usage: node --require ./dom_polyfill.cjs ez_tree_generate.mjs '' * * Input JSON: { preset: "Oak Medium", params: { seed, type, bark, branch, leaves, trellis } } * Output JSON: { branches: { positions, normals, uvs, indices }, leaves: { ... } } */ import { Tree, TreePreset } from '@dgreenheck/ez-tree'; const input = JSON.parse(process.argv[2] ?? '{}'); const presetName = input.preset ?? 'Oak Medium'; const params = input.params ?? {}; if (!TreePreset[presetName]) { process.stderr.write('Unknown preset: ' + presetName + '\n'); process.exit(1); } const options = structuredClone(TreePreset[presetName]); applyParams(options, params); const tree = new Tree(); tree.options.copy(options); try { tree.generate(); } catch (e) { // generate() may fail on DOM APIs for materials, but geometry arrays are // already populated at that point — continue. } const branches = tree.branches; const leaves = tree.leaves; if (!branches || !leaves) { process.stderr.write('Generation produced no geometry\n'); process.exit(1); } const leafNormals = leaves.normals?.length > 0 ? leaves.normals : computeFlatNormals(leaves.verts, leaves.indices); const result = { branches: { positions: Array.from(branches.verts), normals: Array.from(branches.normals), uvs: Array.from(branches.uvs), indices: Array.from(branches.indices), }, leaves: { positions: Array.from(leaves.verts), normals: Array.from(leafNormals), uvs: Array.from(leaves.uvs), indices: Array.from(leaves.indices), }, }; process.stdout.write(JSON.stringify(result)); // ── Parameter-Overrides ──────────────────────────────────────────────────── function applyParams(options, params) { if (params.seed !== undefined) options.seed = params.seed; if (params.type !== undefined) options.type = params.type; if (params.bark) { const b = params.bark; if (b.tint !== undefined) options.bark.tint = b.tint; if (b.flatShading !== undefined) options.bark.flatShading = b.flatShading; if (b.textureScale) { if (b.textureScale.x !== undefined) options.bark.textureScale.x = b.textureScale.x; if (b.textureScale.y !== undefined) options.bark.textureScale.y = b.textureScale.y; } } if (params.branch) { const br = params.branch; if (br.levels !== undefined) options.branch.levels = br.levels; if (br.force) { if (br.force.strength !== undefined) options.branch.force.strength = br.force.strength; if (br.force.direction) { if (br.force.direction.x !== undefined) options.branch.force.direction.x = br.force.direction.x; if (br.force.direction.y !== undefined) options.branch.force.direction.y = br.force.direction.y; if (br.force.direction.z !== undefined) options.branch.force.direction.z = br.force.direction.z; } } // Per-level maps: replace entirely when provided (Java sends the full map) for (const key of ['angle','children','gnarliness','length','radius', 'sections','segments','start','taper','twist']) { if (br[key] !== undefined) options.branch[key] = br[key]; } } if (params.leaves) { Object.assign(options.leaves, params.leaves); } if (params.trellis) { Object.assign(options.trellis, params.trellis); } } // ── Helpers ──────────────────────────────────────────────────────────────── function computeFlatNormals(verts, indices) { const normals = new Float32Array(verts.length); for (let i = 0; i < indices.length; i += 3) { const ia = indices[i] * 3, ib = indices[i + 1] * 3, ic = indices[i + 2] * 3; const ax = verts[ib] - verts[ia], ay = verts[ib+1] - verts[ia+1], az = verts[ib+2] - verts[ia+2]; const bx = verts[ic] - verts[ia], by = verts[ic+1] - verts[ia+1], bz = verts[ic+2] - verts[ia+2]; const nx = ay*bz - az*by, ny = az*bx - ax*bz, nz = ax*by - ay*bx; const len = Math.sqrt(nx*nx + ny*ny + nz*nz) || 1; for (const idx of [ia, ib, ic]) { normals[idx] += nx/len; normals[idx+1] += ny/len; normals[idx+2] += nz/len; } } return Array.from(normals); }