RMA
Anonymous
jsx
02/22/2026 10:49 AM
12.0 KB
11
Indexable
import { useState, useRef, useEffect, useCallback } from "react";
// --- Core algorithms ported from Python ---
function algorithmRatioApproximation(ratios, d = 7) {
const Lp = 2 ** d;
const L = ratios.reduce((a, b) => a + b, 0);
const approx = ratios.map(a => Math.round((a * Lp) / L));
const diff = Lp - approx.reduce((a, b) => a + b, 0);
approx[approx.length - 1] += diff;
return approx;
}
function expressionPartition(P, L) {
const half = L / 2;
const sorted = Object.entries(P).sort((a, b) => b[1] - a[1]);
const [uF, aU] = sorted[0];
let P1 = {}, P2 = {};
if (aU >= half) {
P1[uF] = half;
const rem = aU - half;
if (rem > 0) P2[uF] = rem;
for (let i = 1; i < sorted.length; i++) P2[sorted[i][0]] = sorted[i][1];
} else {
P1[uF] = aU;
let cur = aU;
for (let i = 1; i < sorted.length; i++) {
const [f, v] = sorted[i];
if (cur + v <= half) { P1[f] = v; cur += v; }
else {
const need = half - cur;
if (need > 0) { P1[f] = need; const r = v - need; if (r > 0) P2[f] = r; cur += need; }
else P2[f] = v;
}
}
}
return [P1, P2];
}
function buildTree(P, L, lv = 0) {
if (Object.keys(P).length === 1) {
const k = Object.keys(P)[0];
return { label: k, volume: P[k], level: lv, leaf: true };
}
const [P1, P2] = expressionPartition(P, L);
return {
label: "Mix", partition: P, volume: L, level: lv, leaf: false,
left: buildTree(P1, L / 2, lv + 1),
right: buildTree(P2, L / 2, lv + 1),
};
}
// --- Layout: assign x,y to each node ---
function layoutTree(root) {
let idx = 0;
const nodes = [], edges = [];
function walk(n, depth) {
if (!n) return;
if (n.leaf) { n._x = idx++; n._y = depth; }
else {
walk(n.left, depth + 1);
walk(n.right, depth + 1);
n._x = (n.left._x + n.right._x) / 2;
n._y = depth;
edges.push({ from: n, to: n.left });
edges.push({ from: n, to: n.right });
}
nodes.push(n);
}
walk(root, 0);
return { nodes, edges };
}
// Color map
const COLORS = [
"#6366f1","#f59e0b","#10b981","#ef4444","#3b82f6","#ec4899","#8b5cf6",
"#14b8a6","#f97316","#84cc16","#06b6d4","#e11d55"
];
function fluidColor(label, allFluids) {
const i = allFluids.indexOf(label);
return i >= 0 ? COLORS[i % COLORS.length] : "#94a3b8";
}
function partitionBar(P, vol, allFluids, w = 80) {
const entries = Object.entries(P).sort((a, b) => allFluids.indexOf(a[0]) - allFluids.indexOf(b[0]));
let offset = 0;
return entries.map(([f, v]) => {
const seg = (v / vol) * w;
const x = offset;
offset += seg;
return { f, x, w: seg, color: fluidColor(f, allFluids) };
});
}
export default function App() {
const [raw, setRaw] = useState("2,3,5,7,11,13,87");
const [depth, setDepth] = useState(7);
const [treeData, setTreeData] = useState(null);
const [allFluids, setAllFluids] = useState([]);
const [adj, setAdj] = useState([]);
const svgRef = useRef();
const [transform, setTransform] = useState({ x: 0, y: 0, k: 1 });
const dragging = useRef(false);
const lastPt = useRef({ x: 0, y: 0 });
const generate = useCallback(() => {
const ratios = raw.split(",").map(Number).filter(n => !isNaN(n) && n > 0);
if (ratios.length < 2 || depth < 2 || depth > 10) return;
const a = algorithmRatioApproximation(ratios, depth);
setAdj(a);
const fluids = a.map((_, i) => `x${i + 1}`);
setAllFluids(fluids);
const dict = {};
fluids.forEach((f, i) => { if (a[i] > 0) dict[f] = a[i]; });
const root = buildTree(dict, 2 ** depth);
const layout = layoutTree(root);
setTreeData(layout);
setTransform({ x: 0, y: 0, k: 1 });
}, [raw, depth]);
useEffect(generate, []);
const nodeW = 96, nodeH = 52, gapX = 12, gapY = 72;
const onWheel = (e) => {
e.preventDefault();
const factor = e.deltaY < 0 ? 1.12 : 0.89;
setTransform(t => ({ ...t, k: Math.min(3, Math.max(0.15, t.k * factor)) }));
};
const onDown = (e) => { dragging.current = true; lastPt.current = { x: e.clientX, y: e.clientY }; };
const onMove = (e) => {
if (!dragging.current) return;
const dx = e.clientX - lastPt.current.x, dy = e.clientY - lastPt.current.y;
lastPt.current = { x: e.clientX, y: e.clientY };
setTransform(t => ({ ...t, x: t.x + dx, y: t.y + dy }));
};
const onUp = () => { dragging.current = false; };
if (!treeData) return <div className="p-4">Loading…</div>;
const { nodes, edges } = treeData;
const maxX = Math.max(...nodes.map(n => n._x));
const maxY = Math.max(...nodes.map(n => n._y));
const svgW = (maxX + 1) * (nodeW + gapX) + 40;
const svgH = (maxY + 1) * (nodeH + gapY) + 40;
const cx = n => n._x * (nodeW + gapX) + nodeW / 2 + 20;
const cy = n => n._y * (nodeH + gapY) + nodeH / 2 + 20;
return (
<div style={{ width: "100%", height: "100vh", display: "flex", flexDirection: "column", background: "#0f172a", color: "#e2e8f0", fontFamily: "system-ui" }}>
{/* Controls */}
<div style={{ padding: "12px 16px", display: "flex", gap: 12, alignItems: "center", flexWrap: "wrap", background: "#1e293b", borderBottom: "1px solid #334155" }}>
<label style={{ fontSize: 13 }}>Ratios:
<input value={raw} onChange={e => setRaw(e.target.value)}
style={{ marginLeft: 6, padding: "4px 8px", width: 220, background: "#0f172a", border: "1px solid #475569", borderRadius: 4, color: "#e2e8f0", fontSize: 13 }} />
</label>
<label style={{ fontSize: 13 }}>Depth (d):
<input type="number" value={depth} min={2} max={10} onChange={e => setDepth(+e.target.value)}
style={{ marginLeft: 6, padding: "4px 8px", width: 50, background: "#0f172a", border: "1px solid #475569", borderRadius: 4, color: "#e2e8f0", fontSize: 13 }} />
</label>
<button onClick={generate}
style={{ padding: "5px 16px", background: "#6366f1", border: "none", borderRadius: 6, color: "#fff", fontWeight: 600, cursor: "pointer", fontSize: 13 }}>
Generate
</button>
<span style={{ fontSize: 12, color: "#94a3b8", marginLeft: 8 }}>
Scaled to 2<sup>{depth}</sup>={2**depth}: [{adj.join(", ")}]
</span>
</div>
{/* Legend */}
<div style={{ padding: "6px 16px", display: "flex", gap: 14, flexWrap: "wrap", background: "#1e293b", borderBottom: "1px solid #334155" }}>
{allFluids.map((f, i) => adj[i] > 0 && (
<span key={f} style={{ display: "flex", alignItems: "center", gap: 4, fontSize: 12 }}>
<span style={{ width: 10, height: 10, borderRadius: 2, background: fluidColor(f, allFluids), display: "inline-block" }} />
{f} ({adj[i]})
</span>
))}
<span style={{ fontSize: 11, color: "#64748b", marginLeft: "auto" }}>Scroll to zoom · Drag to pan</span>
</div>
{/* Tree SVG */}
<div style={{ flex: 1, overflow: "hidden", cursor: dragging.current ? "grabbing" : "grab" }}
onMouseDown={onDown} onMouseMove={onMove} onMouseUp={onUp} onMouseLeave={onUp}
onWheel={onWheel}>
<svg ref={svgRef} width="100%" height="100%" style={{ display: "block" }}>
<g transform={`translate(${transform.x + 200},${transform.y + 20}) scale(${transform.k})`}>
{/* Edges */}
{edges.map((e, i) => (
<line key={i} x1={cx(e.from)} y1={cy(e.from) + nodeH / 2 - 4}
x2={cx(e.to)} y2={cy(e.to) - nodeH / 2 + 4}
stroke="#475569" strokeWidth={1.5} />
))}
{/* Nodes */}
{nodes.map((n, i) => {
const x = cx(n) - nodeW / 2, y = cy(n) - nodeH / 2;
if (n.leaf) {
const c = fluidColor(n.label, allFluids);
return (
<g key={i}>
<rect x={x} y={y} width={nodeW} height={nodeH} rx={8}
fill={c + "22"} stroke={c} strokeWidth={1.5} />
<text x={cx(n)} y={cy(n) - 4} textAnchor="middle" fill={c} fontSize={13} fontWeight={700}>{n.label}</text>
<text x={cx(n)} y={cy(n) + 14} textAnchor="middle" fill="#94a3b8" fontSize={10}>vol={n.volume}</text>
</g>
);
}
const bars = partitionBar(n.partition, n.volume, allFluids, nodeW - 8);
return (
<g key={i}>
<rect x={x} y={y} width={nodeW} height={nodeH} rx={8}
fill="#1e293b" stroke="#475569" strokeWidth={1} />
<text x={cx(n)} y={cy(n) - 8} textAnchor="middle" fill="#e2e8f0" fontSize={11} fontWeight={600}>Mix</text>
<text x={cx(n)} y={cy(n) + 4} textAnchor="middle" fill="#64748b" fontSize={9}>L={n.volume}</text>
{/* mini composition bar */}
<g transform={`translate(${x + 4},${cy(n) + 10})`}>
{bars.map((b, j) => (
<rect key={j} x={b.x} y={0} width={Math.max(b.w - 0.5, 0.5)} height={6} rx={1} fill={b.color} opacity={0.85} />
))}
</g>
</g>
);
})}
</g>
</svg>
</div>
</div>
);
}Editor is loading...
Leave a Comment