Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com Patches, unit tests, benchmarks, whitepaper, and outreach briefs. Public domain — no copyright claimed. Use freely.
156 lines
5.9 KiB
Java
156 lines
5.9 KiB
Java
package workbench;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.HashSet;
|
|
import java.util.List;
|
|
import java.util.Set;
|
|
|
|
/**
|
|
* Sym² manifold — pure geometry + heat diffusion.
|
|
*
|
|
* Given m seed points, maps every pair (u,v) to a 3D vertex:
|
|
* x,y = midpoint of p_u and p_v (centered)
|
|
* z = distance between p_u and p_v * 8
|
|
*
|
|
* This is the symmetric product Sym²(curve). The seam (u==v diagonal)
|
|
* re-embeds the original curve at z=0.
|
|
*
|
|
* No Swing dependencies — pure data structure.
|
|
*/
|
|
public class Manifold {
|
|
|
|
public static final int M = 32; // grid resolution
|
|
public static final float DECAY = 0.96f; // heat decay per frame
|
|
|
|
// flat float arrays: vertex i → positions[i*3 .. i*3+2]
|
|
public float[] positions; // current positions (displaced by heat + wobble)
|
|
public float[] originalPositions;// rest positions (no displacement)
|
|
public int[] indices; // triangle index list
|
|
|
|
// seam: diagonal u==v re-embeds original curve at z=0
|
|
public float[] seamPositions; // M points, flat x,y,z
|
|
|
|
// adjacency[i] = list of vertex indices adjacent to i
|
|
public List<List<Integer>> adjacency;
|
|
|
|
public float[] heat; // heat per vertex
|
|
|
|
public int vertexCount;
|
|
public boolean built = false;
|
|
|
|
// ── Sym² computation ──────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Pure math: compute M*M vertex positions from seed points.
|
|
* Returns flat float array: [x0,y0,z0, x1,y1,z1, ...]
|
|
*/
|
|
public static float[] computePositions(float[] seedX, float[] seedY, int n) {
|
|
// sample M evenly-spaced seeds
|
|
float[] sx = new float[M], sy = new float[M];
|
|
for (int i = 0; i < M; i++) {
|
|
int k = (int)(i * n / (double)M);
|
|
sx[i] = seedX[k];
|
|
sy[i] = seedY[k];
|
|
}
|
|
// center
|
|
float cx = 0, cy = 0;
|
|
for (int i = 0; i < M; i++) { cx += sx[i]; cy += sy[i]; }
|
|
cx /= M; cy /= M;
|
|
|
|
float[] pos = new float[M * M * 3];
|
|
for (int u = 0; u < M; u++) {
|
|
for (int v = 0; v < M; v++) {
|
|
int base = (u * M + v) * 3;
|
|
float midX = (sx[u] + sx[v]) / 2f - cx;
|
|
float midY = (sy[u] + sy[v]) / 2f - cy;
|
|
float dx = sx[u] - sx[v];
|
|
float dy = sy[u] - sy[v];
|
|
pos[base] = midX * 12f;
|
|
pos[base + 1] = -midY * 12f;
|
|
pos[base + 2] = (float)Math.sqrt(dx*dx + dy*dy) * 8f;
|
|
}
|
|
}
|
|
return pos;
|
|
}
|
|
|
|
/**
|
|
* Build manifold from seed points. Call when seeds change.
|
|
* Resets heat to zero.
|
|
*/
|
|
public void build(float[] seedX, float[] seedY, int n) {
|
|
if (n < 5) return;
|
|
|
|
vertexCount = M * M;
|
|
positions = computePositions(seedX, seedY, n);
|
|
originalPositions = Arrays.copyOf(positions, positions.length);
|
|
|
|
// triangle indices: quad (u,v) split into 2 triangles
|
|
int quadCount = (M - 1) * (M - 1);
|
|
indices = new int[quadCount * 6];
|
|
int idx = 0;
|
|
for (int u = 0; u < M - 1; u++) {
|
|
for (int v = 0; v < M - 1; v++) {
|
|
int a = u*M+v, b = (u+1)*M+v, c = u*M+(v+1), d = (u+1)*M+(v+1);
|
|
indices[idx++] = a; indices[idx++] = b; indices[idx++] = c;
|
|
indices[idx++] = b; indices[idx++] = d; indices[idx++] = c;
|
|
}
|
|
}
|
|
|
|
// adjacency from triangle index list
|
|
adjacency = new ArrayList<>(vertexCount);
|
|
for (int i = 0; i < vertexCount; i++) adjacency.add(new ArrayList<>());
|
|
for (int i = 0; i < indices.length; i += 3) {
|
|
int a = indices[i], b = indices[i+1], c = indices[i+2];
|
|
adjacency.get(a).add(b); adjacency.get(b).add(a);
|
|
adjacency.get(b).add(c); adjacency.get(c).add(b);
|
|
adjacency.get(a).add(c); adjacency.get(c).add(a);
|
|
}
|
|
// deduplicate
|
|
for (int i = 0; i < vertexCount; i++) {
|
|
Set<Integer> seen = new HashSet<>(adjacency.get(i));
|
|
adjacency.set(i, new ArrayList<>(seen));
|
|
}
|
|
|
|
// seam: diagonal u==v
|
|
seamPositions = new float[M * 3];
|
|
for (int u = 0; u < M; u++) {
|
|
int base = (u * M + u) * 3;
|
|
seamPositions[u*3] = positions[base];
|
|
seamPositions[u*3 + 1] = positions[base + 1];
|
|
seamPositions[u*3 + 2] = positions[base + 2];
|
|
}
|
|
|
|
heat = new float[vertexCount];
|
|
built = true;
|
|
}
|
|
|
|
// ── Heat diffusion (graph Laplacian, one step) ────────────────────────────
|
|
|
|
public void diffuseHeat() {
|
|
if (!built) return;
|
|
float[] next = new float[heat.length];
|
|
for (int i = 0; i < heat.length; i++) {
|
|
List<Integer> nb = adjacency.get(i);
|
|
float sum = heat[i];
|
|
for (int j : nb) sum += heat[j];
|
|
next[i] = (sum / (nb.size() + 1)) * DECAY;
|
|
}
|
|
heat = next;
|
|
}
|
|
|
|
// ── Bounding box for viewport scaling ────────────────────────────────────
|
|
|
|
public float[] bounds() {
|
|
if (!built) return new float[]{-1,-1,-1,1,1,1};
|
|
float minX = Float.MAX_VALUE, minY = Float.MAX_VALUE, minZ = Float.MAX_VALUE;
|
|
float maxX = -Float.MAX_VALUE, maxY = -Float.MAX_VALUE, maxZ = -Float.MAX_VALUE;
|
|
for (int i = 0; i < vertexCount; i++) {
|
|
float x = originalPositions[i*3], y = originalPositions[i*3+1], z = originalPositions[i*3+2];
|
|
if (x < minX) minX = x; if (x > maxX) maxX = x;
|
|
if (y < minY) minY = y; if (y > maxY) maxY = y;
|
|
if (z < minZ) minZ = z; if (z > maxZ) maxZ = z;
|
|
}
|
|
return new float[]{minX, minY, minZ, maxX, maxY, maxZ};
|
|
}
|
|
}
|