import { useMemo, useRef, useState } from 'react'; import { ready, ink } from '../../lib/mrly.js'; import { cubes } from '../../lib/stage.js'; import { mount, Page, Row, Pick, Slider, Check, Btn, Stats, Stat, Note, Group } from '../../lib/app.jsx'; import { Grid, Markup } from '../../lib/draw.jsx'; import { Stage } from '../../lib/stage.jsx'; import { useQuery } from '../../lib/query.js'; import { useSeeds, seeded, Picker } from '../../lib/select.jsx'; const m = await ready(); const DIMS = [[2, 'the plane'], [3, 'the cube'], [6, 'the hexagon']]; const PROJECTIONS = [['cut', 'the middle slice'], ['pro', 'three facing sides'], ['iso', 'the isometric skin']]; const PRESETS = [[5, 5], [3, 9]]; const PLANE_CELLS = 262144; const SOLID_CELLS = 1000000; const HEX_DRAWN = 24000; const SOLID_FILLS = 150000; const SEED = { 2: ['495', 3], 3: ['23', 2], 6: ['23', 2] }; function capOf(q, reps) { const copies = reps.reduce((a, b) => a * b, 1); try { if (q.dim === 2) return m.level_cap(q.number, 2, Math.floor(PLANE_CELLS / copies)); if (q.dim === 3) { const room = m.level_cap(q.number, 3, Math.floor(SOLID_CELLS / copies)); return Math.min(room, m.fill_cap(q.code.trim(), q.number, 3, q.base, Math.floor(SOLID_FILLS / copies))); } return Math.min(m.level_cap(q.number, 1, 81), m.level_cap(q.number, 2, Math.floor(HEX_DRAWN / (8 * copies)))); } catch { return 1; } } function App() { const s = useSeeds(); const [q, set] = useQuery({ dim: 2, code: seeded(s, 2, 3, '495'), base: 3, number: 3, level: 2, x: 5, y: 5, z: 5, proj: 'cut', overhang: true }); const [spin, setSpin] = useState(true); const live = useRef(null); const reps = q.dim === 3 ? [q.x, q.y, q.z] : [q.x, q.y]; const top = capOf(q, reps); const level = Math.min(q.level, top); const code = q.code.trim(); const solid = q.dim !== 2; const crop = !q.overhang && q.x >= 2 && q.y >= 2; const data = useMemo(() => { const out = {}; try { out.name = m.name_of(code, solid ? 3 : 2, q.base); out.census = JSON.parse(m.tile_census(code, q.number, level, q.base, q.dim, q.proj, reps, crop)); if (q.dim === 2) out.grid = m.tile_grid(code, q.number, level, q.base, q.x, q.y); else if (q.dim === 3) out.cells = m.tile_cells(code, q.number, level, q.base, q.x, q.y, q.z); else { if (out.census.triangles > HEX_DRAWN) throw new Error(`${out.census.triangles} triangles is more than this page draws; lower the level or the repeats.`); const scale = Math.max(1, Math.round(900 / out.census.sheet[0])); const art = m.tile_svg(code, q.number, level, q.base, q.proj, q.x, q.y, crop, scale); if (art.length > 4000000) throw new Error('that drawing is larger than this page serves; lower the level or the repeats.'); out.art = art; } } catch (error) { out.error = error; } return out; }, [code, q.base, q.number, level, q.dim, q.proj, q.x, q.y, q.z, crop]); const census = data.census ?? {}; const sheet = `${data.name ?? ''} repeated into a sheet`; const shift = (value) => { const dim = +value; const [code, base] = SEED[dim]; s.drop(); set({ dim, code, base, number: 3, level: dim === 2 ? 2 : 1 }); }; const preset = ([a, b]) => set({ x: a, y: b, z: a }); const turn = (on) => { setSpin(on); if (live.current) live.current.spin = on ? 0.004 : 0; }; const onStage = (stage) => { live.current = stage; stage.spin = spin ? 0.004 : 0; if (!data.cells) { stage.clear(); return; } const span = [census.sheet[1], census.sheet[0], census.sheet[2]]; const side = Math.max(...span); const mesh = cubes(data.cells, side, ink.orange); mesh.position.set(...span.map((n) => 1 - n / side)); stage.show(mesh); }; const controls = ( <> set({ number: +v })} /> set({ level: v })} /> set({ x: v })} /> set({ y: v })} /> {q.dim === 3 && set({ z: v })} />} {PRESETS.map(([a, b]) => preset([a, b])}>tile({a},{b}))} {q.dim === 6 && ( set({ proj: v })} /> set({ overhang: v })} /> )} {q.dim === 3 && ( )} ); return ( The tile is the design at its level and the sheet is that tile repeated, both built in Rust before a pixel is drawn. The hexagon interlocks rather than butts: the crop trims the jagged overhang to a clean rectangle and slides the whole triangle grid by one interlocking step, so the crate flips the start parity to keep every triangle pointing the way the tile's do, and backs a tall sheet of wide hexagons onto a wider frame so the renderer still reads the hexagon the right way up. Exposed means perimeter in the plane, surface in the cube and the boundary edges of the filled sub-mesh on the hexagon; buried is the difference between the copies' own exposure and the sheet's, and under the crop that difference also counts what the trim removed. Why a design's symmetries are symmetries of the infinite tiling and not of a truncation to a finite side is in the core. Nearby: the words nests one design inside another instead of repeating it, the slices counts the single hexagon, the sponge grows the cube.}> {q.dim === 2 && data.grid && } ); } mount();