RMA

Anonymous
jsx
02/22/2026 10:49 AM
12.0 KB
13
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