MorphoLayout3D.kt
src/jvmMain/kotlin/afield/lang/morpho/MorphoLayout3D.kt · 125 lines
package afield.lang.morpho
import org.graalvm.polyglot.Value
import java.io.Closeable
import java.nio.file.Files
Placement for a MorphoHDL circuit: every active cell gets a 3D position, from a force-directed layout running on morpho's own Barnes–Hut kernel.
One of the two things Field does with a MorphoNetlist snapshot, the other being MorphoSim;
MorphoFLines.kt turns the positions that come out of here into FLines. Layout is stepped
alongside growth rather than run to convergence at the end, and a cell born in a growth step is
seeded at its parent's position, so a circuit that is still expanding is drawable on every frame.
The parameters are upstream's defaults, and the only behavioural difference from morpho's own demo is that Field leaves the z axis free.
class MorphoLayout3D
Force-directed 3D layout for a growing MorphoHDL circuit, computed by morpho's own
Barnes–Hut kernel (graphs_engine/main.wasm) on GraalWasm.
The kernel is 3D throughout — stride-3 positions, an octree, and angular-momentum compensation
via a full inertia tensor — but upstream's force_layout.js pins pos[p*3+2] = 0 and clears
z-velocity every frame, so morpho's own demo only ever sees it flat. Field does not pin z, and
the circuits come out as solids: a fully grown 32-bit Brent–Kung adder occupies a z extent
about two thirds of its x extent.
Using it
sync after each growth step, then relax a few iterations per frame. New cells are seeded at their parent's position, so a subtree unfolds from where it came from rather than materialising somewhere random — which is what makes growth legible.
class MorphoLayout3D(
private val session: MorphoSession,
seed: Int = 1337,
maxPoints: Int = 65_536,
) : Closeable {
data class Params
Barnes–Hut and integration parameters. Defaults are upstream's, from force_layout.js's
constructor, so a Field layout and a morpho layout of the same circuit are comparable.
data class Params(
val repulsionForce: Double = 0.05,
val wireForce: Double = 1.0,
val velocityDecay: Double = 0.05,
/** Barnes–Hut opening angle. Larger is faster and coarser. */
val theta: Double = 0.5,
val leafSize: Int = 64,
val maxLevel: Int = 10,
val linkDistance: Double = 2.0,
val maxDist: Double = 80.0,
val maxSpeed: Double = 1.0,
val gravity: Double = 0.1,
)
private val handle: Value
private var closed = false
init
init {
MorphoRuntime.withJs {
val bytes = Files.readAllBytes(MorphoRuntime.root.resolve("morpho/graphs_engine/main.wasm"))
MorphoRuntime.callLayout("initWasm", bytes, maxPoints)
}
handle = MorphoRuntime.callLayout("createLayout", seed)
}
var particleCount: Int
Number of particles, i.e. active cells, after re-mapping onto the current graph.
var particleCount: Int = 0
private set
fun sync(): Int
Re-map particles onto the circuit's currently active cells. Call after each growth step.
fun sync(): Int {
check(!closed) { "layout has been closed" }
particleCount = MorphoRuntime.callLayout("sync", handle, session.state).asInt()
return particleCount
}
fun relax(steps: Int = 1, params: Params = Params()): Int
Advance the simulation by steps Barnes–Hut iterations.
fun relax(steps: Int = 1, params: Params = Params()): Int {
check(!closed) { "layout has been closed" }
if (particleCount == 0) return 0
val opts = MorphoRuntime.callLayout(
"options",
params.repulsionForce, params.wireForce, params.velocityDecay, params.theta,
params.leafSize, params.maxLevel, params.linkDistance, params.maxDist,
params.maxSpeed, params.gravity,
)
return MorphoRuntime.callLayout("relax", handle, session.state, steps, opts).asInt()
}
fun positions(): FloatArray
Particle positions, stride 3 (x, y, z), in particle order.
fun positions(): FloatArray =
MorphoRuntime.callLayout("positions", handle).`as`(FloatArray::class.java)
fun cellIndices(): IntArray
Cell index for each particle, joining positions back to a MorphoNetlist.
fun cellIndices(): IntArray =
MorphoRuntime.callLayout("cellIndices", handle).`as`(IntArray::class.java)
fun bounds(): FloatArray
[minX, minY, minZ, maxX, maxY, maxZ].
fun bounds(): FloatArray =
MorphoRuntime.callLayout("bounds", handle).`as`(FloatArray::class.java)
fun flatness(): Double
How far the layout actually used the third dimension, as z extent over the larger of the x and y extents. Zero would mean it had collapsed to a plane, as upstream's forces it to.
fun flatness(): Double {
val b = bounds()
if (!b[0].isFinite()) return 0.0
val x = (b[3] - b[0]).toDouble()
val y = (b[4] - b[1]).toDouble()
val z = (b[5] - b[2]).toDouble()
val wide = maxOf(x, y)
return if (wide <= 0.0) 0.0 else z / wide
}
override fun close()
override fun close() {
closed = true
}
}