index.jsx
5.2 kB · jsx · 106 lines
1import { useMemo, useRef, useState } from 'react';2import { ready, ink } from '../../lib/mrly.js';3import { faces, cubes } from '../../lib/stage.js';4import { mount, Page, Row, Pick, Slider, Check, Stats, Stat, Note, Group } from '../../lib/app.jsx';5import { Stage } from '../../lib/stage.jsx';6import { useSeeds, seeded, Picker, Cropper, cropOf } from '../../lib/select.jsx';7import { useQuery } from '../../lib/query.js';89const m = await ready();10const VIEWS = [['shell', 'exposed faces'], ['cubes', 'instanced cubes']];1112function Num({ label, value, min, max, onChange }) {13 return <label>{label} <input type="number" value={value} min={min} max={max} onChange={(e) => onChange(+e.target.value)} /></label>;14}1516function App() {17 const s = useSeeds();18 const [q, set] = useQuery({ code: seeded(s, 3, 2, '23'), base: 2, number: 3, level: 3, crop: '', 'crop-r': 16, 'crop-anti': false });19 const [view, setView] = useState('shell');20 const [opacity, setOpacity] = useState(1);21 const [spin, setSpin] = useState(true);22 const live = useRef(null);23 const top = m.level_cap(q.number, 1, 128);24 const level = Math.min(q.level, top);2526 const data = useMemo(() => {27 const out = {};28 try {29 const code = q.code.trim(), c = cropOf(q);30 out.name = m.name_of(code, 3, q.base);31 out.side = m.grid_total(q.number, 1, level);32 out.fills = m.fills(code, q.number, 3, level, q.base);33 out.voids = m.voids(code, q.number, 3, level, q.base);34 out.surface = m.three_surface(code, q.number, level, q.base);35 out.ratio = m.ratio(code, q.number, 3, level, q.base).toFixed(4);36 out.dimension = m.dimension(code, q.number, 3, q.base).toFixed(4);37 if (Number(m.grid_total(q.number, 3, level)) <= 30000) out.tally = JSON.parse(m.three_census(code, q.number, level, q.base));38 if (view === 'shell') {39 if (Number(out.surface) > 400000) throw new Error(`${out.surface} faces is more than this page draws; lower the level.`);40 const mesh = c.active41 ? m.crop_faces(code, q.number, level, q.base, c.shape, c.rnum, c.rden, c.anti, 'touching')42 : m.three_faces(code, q.number, level, q.base);43 if (c.active && mesh[0] / 36 > 400000) throw new Error(`${mesh[0] / 36} faces is more than this page draws; lower the level.`);44 out.mesh = mesh;45 } else {46 if (Number(out.fills) > 250000) throw new Error(`${out.fills} cubes is more than this page draws; lower the level.`);47 out.cells = c.active48 ? m.crop_cells(code, q.number, level, q.base, c.shape, c.rnum, c.rden, c.anti, 'touching')49 : m.three_cells(code, q.number, level, q.base);50 }51 } catch (error) {52 out.error = error;53 }54 return out;55 }, [q.code, q.base, q.number, level, q.crop, q['crop-r'], q['crop-anti'], view]);5657 const turn = (on) => {58 setSpin(on);59 if (live.current) live.current.spin = on ? 0.004 : 0;60 };6162 const controls = (63 <>64 <Group name="Design">65 <Picker dimension={3} bases={[2, 3]} code={q.code} base={q.base} seeds={s} onChange={set} />66 <Num label="number" value={q.number} min={2} max={5} onChange={(v) => set({ number: v })} />67 <Slider label="level" value={level} min={1} max={top} onChange={(v) => set({ level: v })} />68 </Group>69 <Group name="View">70 <Pick label="view" value={view} options={VIEWS} onChange={setView} />71 <Slider label="opacity" value={opacity} min={0.05} max={1} step={0.05} onChange={setOpacity} />72 <Check label="spin" checked={spin} onChange={turn} />73 </Group>74 <Group name="Crop">75 <Cropper dimension={3} value={q} onChange={set} />76 </Group>77 </>78 );7980 return (81 <Page crumb="sponge" title="A code on eight corners grows a sponge" controls={controls}82 sub="A code picks the filled corners of a cube, the cube grows level by level, and the exposed faces come out of Rust already packed for the screen. Drag to orbit, scroll to zoom."83 foot={<>Filled, empty and exposed counts come from closed formulas, so they answer before any cube is built; the mesh is the crate's own list of outward quads, six floats per vertex, and the Euler number is read off the edge graph while the cube is small enough to walk. Where those formulas come from is <a href="/research/core/">the core</a>.</>}>84 <Stage role="img" aria-label="The sponge" deps={[data, opacity]} onStage={(st) => {85 live.current = st;86 st.spin = spin ? 0.004 : 0;87 if (data.mesh) st.show(faces(data.mesh, ink.blue, opacity));88 else if (data.cells) st.show(cubes(data.cells, Number(data.side), ink.orange));89 else st.clear();90 }} />91 <Stats>92 <Stat label="name">{data.name}</Stat>93 <Stat label="side">{data.side}</Stat>94 <Stat label="filled">{data.fills}</Stat>95 <Stat label="empty">{data.voids}</Stat>96 <Stat label="exposed faces">{data.surface}</Stat>97 <Stat label="fill ratio">{data.ratio}</Stat>98 <Stat label="dimension">{data.dimension}</Stat>99 {data.tally && <span>vertices <b>{data.tally.vertices}</b> edges <b>{data.tally.edges}</b> faces <b>{data.tally.faces}</b> euler <b>{data.tally.euler}</b></span>}100 </Stats>101 <Note error={data.error} />102 </Page>103 );104}105106mount(<App />);