Working but slow

This commit is contained in:
2026-07-06 23:11:42 +02:00
parent 09c1fba8e6
commit f93bcfb8d4
15 changed files with 4443 additions and 4205 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
import { resolveBoundaryCondition, resolveBoundaryIndex, seededRandom } from '../caRuntime.js' import { resolveBoundaryCondition, resolveBoundaryIndex, resolveRngSeed, seededRandom } from '../caRuntime.js'
import type { BoundaryCondition, Cells, JsonObject, SceneParams } from '../types.js' import type { BoundaryCondition, Cells, JsonObject, SceneParams } from '../types.js'
export type NagaArrow = 1 | 2 | 3 | 4 | 5 | 6 export type NagaArrow = 1 | 2 | 3 | 4 | 5 | 6
@@ -389,7 +389,7 @@ export function stepNaga3d(cells: Cells, settings: SceneParams) {
? universeFromCoordinates(coordinates) ? universeFromCoordinates(coordinates)
: seedUniverseFromCells(cells) : seedUniverseFromCells(cells)
const tick = typeof simulation.nagaTick === 'number' ? simulation.nagaTick : 0 const tick = typeof simulation.nagaTick === 'number' ? simulation.nagaTick : 0
const seed = typeof simulation.seed === 'string' ? simulation.seed : 'naga' const seed = resolveRngSeed(simulation, 'naga')
const topology: NagaTopology = { const topology: NagaTopology = {
boundary: resolveBoundaryCondition(simulation.grid), boundary: resolveBoundaryCondition(simulation.grid),
size size
@@ -1,4 +1,4 @@
import { resolveBoundaryCondition, resolveBoundaryIndex, seededRandom } from '../caRuntime.js' import { resolveBoundaryCondition, resolveBoundaryIndex, resolveRngSeed, seededRandom } from '../caRuntime.js'
import type { Cells, SceneParams } from '../types.js' import type { Cells, SceneParams } from '../types.js'
import { emptyCellsLike } from './cellGrid.js' import { emptyCellsLike } from './cellGrid.js'
@@ -17,7 +17,7 @@ export function stepWildfire2d(cells: Cells, settings: SceneParams) {
const width = cells[0]?.length ?? 0 const width = cells[0]?.length ?? 0
const boundary = resolveBoundaryCondition(settings.simulation?.grid) const boundary = resolveBoundaryCondition(settings.simulation?.grid)
const spreadProbability = parseProbability(settings.simulation?.spreadProbability, 1) const spreadProbability = parseProbability(settings.simulation?.spreadProbability, 1)
const seed = typeof settings.simulation?.seed === 'string' ? settings.simulation.seed : 'wildfire' const seed = resolveRngSeed(settings.simulation, 'wildfire')
for (let y = 0; y < height; y += 1) { for (let y = 0; y < height; y += 1) {
for (let x = 0; x < width; x += 1) { for (let x = 0; x < width; x += 1) {
+1 -1
View File
@@ -95,7 +95,7 @@ registerCaEngineRuntime(
id: 'naga-3d', id: 'naga-3d',
label: 'Naga 3D', label: 'Naga 3D',
supportedClasses: [{ dimensions: 3, states: 7 }], supportedClasses: [{ dimensions: 3, states: 7 }],
defaultParams: { ruleId: 'naga-3d', rendererId: 'voxel-3d', seed: 'naga', edgeWrap: true }, defaultParams: { ruleId: 'naga-3d', rendererId: 'voxel-3d', rng_seed: 'naga', edgeWrap: true },
step: stepNaga3d step: stepNaga3d
}, },
{ replace: true } { replace: true }
@@ -36,10 +36,27 @@ function clampProbability(value: unknown, fallback: number) {
return Math.max(0, Math.min(1, typeof value === 'number' && Number.isFinite(value) ? value : fallback)) return Math.max(0, Math.min(1, typeof value === 'number' && Number.isFinite(value) ? value : fallback))
} }
function resolveIcSeed(params: JsonObject, fallback: string) {
if (typeof params.ic_seed === 'string' && params.ic_seed.trim()) return params.ic_seed
if (typeof params.seed === 'string' && params.seed.trim()) return params.seed
return fallback
}
function randomNormal(random: () => number) {
const left = Math.max(Number.EPSILON, random())
const right = Math.max(Number.EPSILON, random())
return Math.sqrt(-2 * Math.log(left)) * Math.cos(2 * Math.PI * right)
}
function sampleNagaLength(meanLength: number, lengthStdDev: number, maxLength: number, random: () => number) {
if (lengthStdDev <= 0) return clampInteger(meanLength, 5, 1, maxLength)
return clampInteger(meanLength + randomNormal(random) * lengthStdDev, meanLength, 1, maxLength)
}
function randomSoup({ settings, caClass, params }: InitialConditionRuntimeInput): InitialConditionResult { function randomSoup({ settings, caClass, params }: InitialConditionRuntimeInput): InitialConditionResult {
const density = clampDensity(params.density) const density = clampDensity(params.density)
const seed = typeof params.seed === 'string' ? params.seed : 'studio-seed' const icSeed = resolveIcSeed(params, 'studio-seed')
const random = seededRandom(seed) const random = seededRandom(icSeed)
const [width, height, depth] = numericGridSize(settings.simulation?.grid, caClass.dimensions) const [width, height, depth] = numericGridSize(settings.simulation?.grid, caClass.dimensions)
const cells: number[][] = [] const cells: number[][] = []
@@ -57,7 +74,7 @@ function randomSoup({ settings, caClass, params }: InitialConditionRuntimeInput)
generator: { generator: {
kind: 'random-soup', kind: 'random-soup',
density, density,
seed ic_seed: icSeed
} }
} }
} }
@@ -83,11 +100,13 @@ function inBounds(x: number, y: number, z: number, width: number, height: number
function nagaMarkov({ settings, caClass, params }: InitialConditionRuntimeInput): InitialConditionResult { function nagaMarkov({ settings, caClass, params }: InitialConditionRuntimeInput): InitialConditionResult {
const [width, height, depth] = numericGridSize(settings.simulation?.grid, caClass.dimensions) const [width, height, depth] = numericGridSize(settings.simulation?.grid, caClass.dimensions)
const maxLength = Math.max(1, width * height * depth)
const count = clampInteger(params.count, 3, 1, Math.max(1, width * height * depth)) const count = clampInteger(params.count, 3, 1, Math.max(1, width * height * depth))
const length = clampInteger(params.length, 5, 1, Math.max(1, width * height * depth)) const meanLength = clampInteger(params.meanLength ?? params.length, 5, 1, maxLength)
const lengthStdDev = Math.max(0, Math.min(maxLength, typeof params.lengthStdDev === 'number' && Number.isFinite(params.lengthStdDev) ? params.lengthStdDev : 0))
const sameTypeProbability = clampProbability(params.sameTypeProbability, 0.8) const sameTypeProbability = clampProbability(params.sameTypeProbability, 0.8)
const seed = typeof params.seed === 'string' ? params.seed : 'naga-markov' const icSeed = resolveIcSeed(params, 'naga-markov')
const random = seededRandom(seed) const random = seededRandom(icSeed)
const occupied = new Set<string>() const occupied = new Set<string>()
const cells: number[][] = [] const cells: number[][] = []
const maxAttempts = count * 300 const maxAttempts = count * 300
@@ -100,6 +119,7 @@ function nagaMarkov({ settings, caClass, params }: InitialConditionRuntimeInput)
let y = Math.floor(random() * height) let y = Math.floor(random() * height)
let z = Math.floor(random() * depth) let z = Math.floor(random() * depth)
let arrow = randomNagaArrow(random) let arrow = randomNagaArrow(random)
const length = sampleNagaLength(meanLength, lengthStdDev, maxLength, random)
const proposed: number[][] = [] const proposed: number[][] = []
const proposedKeys = new Set<string>() const proposedKeys = new Set<string>()
let valid = true let valid = true
@@ -164,9 +184,10 @@ function nagaMarkov({ settings, caClass, params }: InitialConditionRuntimeInput)
generator: { generator: {
kind: 'naga-markov', kind: 'naga-markov',
count, count,
length, meanLength,
lengthStdDev,
sameTypeProbability, sameTypeProbability,
seed, ic_seed: icSeed,
created created
} }
} }
+6
View File
@@ -72,6 +72,12 @@ export function seededRandom(seed: string) {
} }
} }
export function resolveRngSeed(simulation: JsonObject | undefined, fallback: string) {
if (typeof simulation?.rng_seed === 'string' && simulation.rng_seed.trim()) return simulation.rng_seed
if (typeof simulation?.seed === 'string' && simulation.seed.trim()) return simulation.seed
return fallback
}
export function isBoundaryCondition(value: unknown): value is BoundaryCondition { export function isBoundaryCondition(value: unknown): value is BoundaryCondition {
return value === 'wrap' || value === 'mirror' || value === 'fixed' return value === 'wrap' || value === 'mirror' || value === 'fixed'
} }
+39 -22
View File
@@ -72,7 +72,7 @@ interface IcgConfig {
dimensions: number dimensions: number
states: number states: number
density: number density: number
seed: string icSeed: string
} }
interface EngineConfig { interface EngineConfig {
@@ -324,7 +324,7 @@ function defaultIcgConfig(): IcgConfig {
dimensions: 2, dimensions: 2,
states: 2, states: 2,
density: 0.28, density: 0.28,
seed: 'studio-seed' icSeed: 'studio-seed'
} }
} }
@@ -353,16 +353,16 @@ function icgParamsSchema(config: IcgConfig): JsonObject {
max: 1, max: 1,
step: 0.01 step: 0.01
}, },
seed: { ic_seed: {
type: 'string', type: 'string',
label: 'Seed', label: 'IC seed',
default: config.seed default: config.icSeed
} }
} }
} }
function icgDefaultParams(config: IcgConfig): JsonObject { function icgDefaultParams(config: IcgConfig): JsonObject {
return { density: config.density, seed: config.seed } return { density: config.density, ic_seed: config.icSeed }
} }
function ruleLabel(ruleId: string) { function ruleLabel(ruleId: string) {
@@ -1644,8 +1644,8 @@ function IcgConfigFields({
</label> </label>
</div> </div>
<label> <label>
Seed IC seed
<input value={config.seed} onChange={(event) => onChange((current) => ({ ...current, seed: event.target.value }))} /> <input value={config.icSeed} onChange={(event) => onChange((current) => ({ ...current, icSeed: event.target.value }))} />
</label> </label>
</div> </div>
) )
@@ -1817,7 +1817,11 @@ function configFromIcg(generator: InitialConditionGenerator): IcgConfig {
dimensions: supportedClass?.dimensions ?? 2, dimensions: supportedClass?.dimensions ?? 2,
states: typeof supportedClass?.states === 'number' ? supportedClass.states : 2, states: typeof supportedClass?.states === 'number' ? supportedClass.states : 2,
density: typeof generator.default_params.density === 'number' ? generator.default_params.density : 0.28, density: typeof generator.default_params.density === 'number' ? generator.default_params.density : 0.28,
seed: typeof generator.default_params.seed === 'string' ? generator.default_params.seed : 'studio-seed' icSeed: typeof generator.default_params.ic_seed === 'string'
? generator.default_params.ic_seed
: typeof generator.default_params.seed === 'string'
? generator.default_params.seed
: 'studio-seed'
} }
} }
@@ -2168,7 +2172,8 @@ function LibraryEditor({
const [selectedEngineId, setSelectedEngineId] = React.useState('') const [selectedEngineId, setSelectedEngineId] = React.useState('')
const [icgDensity, setIcgDensity] = React.useState(0.28) const [icgDensity, setIcgDensity] = React.useState(0.28)
const [icgNagaCount, setIcgNagaCount] = React.useState(3) const [icgNagaCount, setIcgNagaCount] = React.useState(3)
const [icgNagaLength, setIcgNagaLength] = React.useState(5) const [icgNagaMeanLength, setIcgNagaMeanLength] = React.useState(5)
const [icgNagaLengthStdDev, setIcgNagaLengthStdDev] = React.useState(0)
const [icgNagaSameTypeProbability, setIcgNagaSameTypeProbability] = React.useState(0.8) const [icgNagaSameTypeProbability, setIcgNagaSameTypeProbability] = React.useState(0.8)
const [icgSeed, setIcgSeed] = React.useState('studio-seed') const [icgSeed, setIcgSeed] = React.useState('studio-seed')
const [nodeName, setNodeName] = React.useState('Untitled preset') const [nodeName, setNodeName] = React.useState('Untitled preset')
@@ -2664,24 +2669,28 @@ function LibraryEditor({
function applyIcgDefaults(generator: InitialConditionGenerator | undefined) { function applyIcgDefaults(generator: InitialConditionGenerator | undefined) {
if (typeof generator?.default_params.density === 'number') setIcgDensity(generator.default_params.density) if (typeof generator?.default_params.density === 'number') setIcgDensity(generator.default_params.density)
if (typeof generator?.default_params.count === 'number') setIcgNagaCount(generator.default_params.count) if (typeof generator?.default_params.count === 'number') setIcgNagaCount(generator.default_params.count)
if (typeof generator?.default_params.length === 'number') setIcgNagaLength(generator.default_params.length) if (typeof generator?.default_params.meanLength === 'number') setIcgNagaMeanLength(generator.default_params.meanLength)
else if (typeof generator?.default_params.length === 'number') setIcgNagaMeanLength(generator.default_params.length)
if (typeof generator?.default_params.lengthStdDev === 'number') setIcgNagaLengthStdDev(generator.default_params.lengthStdDev)
if (typeof generator?.default_params.sameTypeProbability === 'number') { if (typeof generator?.default_params.sameTypeProbability === 'number') {
setIcgNagaSameTypeProbability(generator.default_params.sameTypeProbability) setIcgNagaSameTypeProbability(generator.default_params.sameTypeProbability)
} }
if (typeof generator?.default_params.seed === 'string') setIcgSeed(generator.default_params.seed) if (typeof generator?.default_params.ic_seed === 'string') setIcgSeed(generator.default_params.ic_seed)
else if (typeof generator?.default_params.seed === 'string') setIcgSeed(generator.default_params.seed)
} }
function selectedIcgParams(generator: InitialConditionGenerator) { function selectedIcgParams(generator: InitialConditionGenerator) {
if (generator.generator_kind === 'naga-markov') { if (generator.generator_kind === 'naga-markov') {
return { return {
count: icgNagaCount, count: icgNagaCount,
length: icgNagaLength, meanLength: icgNagaMeanLength,
lengthStdDev: icgNagaLengthStdDev,
sameTypeProbability: icgNagaSameTypeProbability, sameTypeProbability: icgNagaSameTypeProbability,
seed: icgSeed ic_seed: icgSeed
} }
} }
return { density: Math.max(0, Math.min(1, icgDensity)), seed: icgSeed } return { density: Math.max(0, Math.min(1, icgDensity)), ic_seed: icgSeed }
} }
function applySelectedIcg() { function applySelectedIcg() {
@@ -3204,12 +3213,6 @@ function LibraryEditor({
</label> </label>
{selectedIcg?.generator_kind === 'naga-markov' ? ( {selectedIcg?.generator_kind === 'naga-markov' ? (
<> <>
<div className="root-config-grid">
<label>
Length
<input min={1} step={1} type="number" value={icgNagaLength} onChange={(event) => setIcgNagaLength(Math.max(1, Number(event.target.value) || 1))} />
</label>
</div>
<div className="icg-controls"> <div className="icg-controls">
<label> <label>
Naga count Naga count
@@ -3217,6 +3220,20 @@ function LibraryEditor({
</label> </label>
<strong>{icgNagaCount}</strong> <strong>{icgNagaCount}</strong>
</div> </div>
<div className="icg-controls">
<label>
Mean length
<input max={64} min={1} step={1} type="range" value={icgNagaMeanLength} onChange={(event) => setIcgNagaMeanLength(Math.max(1, Number(event.target.value) || 1))} />
</label>
<strong>{icgNagaMeanLength}</strong>
</div>
<div className="icg-controls">
<label>
Length std. dev.
<input max={32} min={0} step={1} type="range" value={icgNagaLengthStdDev} onChange={(event) => setIcgNagaLengthStdDev(Math.max(0, Number(event.target.value) || 0))} />
</label>
<strong>{icgNagaLengthStdDev}</strong>
</div>
<div className="icg-controls"> <div className="icg-controls">
<label> <label>
Same type probability Same type probability
@@ -3235,7 +3252,7 @@ function LibraryEditor({
</div> </div>
)} )}
<label> <label>
Seed IC seed
<input value={icgSeed} onChange={(event) => setIcgSeed(event.target.value)} /> <input value={icgSeed} onChange={(event) => setIcgSeed(event.target.value)} />
</label> </label>
<button className="primary" disabled={!selectedNode || !selectedIcgId} type="button" onClick={applySelectedIcg}> <button className="primary" disabled={!selectedNode || !selectedIcgId} type="button" onClick={applySelectedIcg}>
+52 -44
View File
@@ -30,7 +30,6 @@ interface SceneBundle {
bounds: THREE.LineSegments bounds: THREE.LineSegments
camera: THREE.PerspectiveCamera camera: THREE.PerspectiveCamera
controls: OrbitControls controls: OrbitControls
glowMesh: THREE.InstancedMesh | null
grid: THREE.GridHelper grid: THREE.GridHelper
mesh: THREE.InstancedMesh | null mesh: THREE.InstancedMesh | null
mountNode: HTMLDivElement mountNode: HTMLDivElement
@@ -60,23 +59,59 @@ function disposeMesh(scene: THREE.Scene, mesh: THREE.InstancedMesh | null) {
} }
function createVoxelMaterial(primaryColor: THREE.Color) { function createVoxelMaterial(primaryColor: THREE.Color) {
return new THREE.MeshStandardMaterial({ return new THREE.ShaderMaterial({
color: primaryColor, defines: {
emissive: primaryColor.clone().multiplyScalar(0.58), USE_INSTANCING_COLOR: ''
emissiveIntensity: 0.48, },
metalness: 0.04, uniforms: {
roughness: 0.68, ambientStrength: { value: 0.56 },
vertexColors: true diffuseStrength: { value: 0.58 },
}) glowStrength: { value: 0.34 },
} lightDirection: { value: new THREE.Vector3(0.45, 0.82, 0.36).normalize() },
primaryColor: { value: primaryColor }
},
vertexShader: `
varying vec3 vColor;
varying vec3 vNormal;
varying vec3 vWorldPosition;
function createVoxelGlowMaterial() { void main() {
return new THREE.MeshBasicMaterial({ vColor = instanceColor;
blending: THREE.AdditiveBlending, vNormal = normalize(mat3(modelMatrix * instanceMatrix) * normal);
depthWrite: false,
opacity: 0.26, vec4 worldPosition = modelMatrix * instanceMatrix * vec4(position, 1.0);
transparent: true, vWorldPosition = worldPosition.xyz;
vertexColors: true gl_Position = projectionMatrix * viewMatrix * worldPosition;
}
`,
fragmentShader: `
uniform float ambientStrength;
uniform float diffuseStrength;
uniform float glowStrength;
uniform vec3 lightDirection;
uniform vec3 primaryColor;
varying vec3 vColor;
varying vec3 vNormal;
varying vec3 vWorldPosition;
void main() {
vec3 normal = normalize(vNormal);
vec3 viewDirection = normalize(cameraPosition - vWorldPosition);
float diffuse = max(dot(normal, normalize(lightDirection)), 0.0);
float reverseFill = max(dot(normal, normalize(vec3(-0.45, 0.42, -0.68))), 0.0);
float rim = pow(1.0 - max(dot(normal, viewDirection), 0.0), 2.15);
float faceLift = pow(max(abs(normal.y), 0.0), 0.7) * 0.08;
vec3 color = max(vColor, primaryColor * 0.12);
vec3 lit = color * (ambientStrength + diffuse * diffuseStrength + reverseFill * 0.16 + faceLift);
vec3 glow = color * (0.18 + rim * glowStrength);
vec3 finalColor = lit + glow;
gl_FragColor = vec4(finalColor, 1.0);
}
`,
toneMapped: true
}) })
} }
@@ -123,18 +158,6 @@ function createVoxelMesh(size: number, options: VoxelRendererOptions) {
return mesh return mesh
} }
function createVoxelGlowMesh(size: number, options: VoxelRendererOptions) {
const spacing = WORLD_SIZE / size
const edge = spacing * (1 - clamp(options.cellGap, 0.02, 0.45)) * 1.18
const geometry = new THREE.BoxGeometry(edge, edge, edge)
const material = createVoxelGlowMaterial()
const mesh = new THREE.InstancedMesh(geometry, material, Math.max(1, size ** 3))
mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage)
mesh.castShadow = false
mesh.receiveShadow = false
return mesh
}
const CATEGORICAL_STATE_COLORS = [ const CATEGORICAL_STATE_COLORS = [
'#70f45f', '#70f45f',
'#f0c94a', '#f0c94a',
@@ -218,17 +241,12 @@ function colorForCell(
function updateVoxelMesh(bundle: SceneBundle, cells: VoxelCell[], size: number, ruleId: string, options: VoxelRendererOptions) { function updateVoxelMesh(bundle: SceneBundle, cells: VoxelCell[], size: number, ruleId: string, options: VoxelRendererOptions) {
disposeMesh(bundle.scene, bundle.mesh) disposeMesh(bundle.scene, bundle.mesh)
disposeMesh(bundle.scene, bundle.glowMesh)
bundle.mesh = createVoxelMesh(size, options) bundle.mesh = createVoxelMesh(size, options)
bundle.glowMesh = createVoxelGlowMesh(size, options)
bundle.scene.add(bundle.glowMesh)
bundle.scene.add(bundle.mesh) bundle.scene.add(bundle.mesh)
const mesh = bundle.mesh const mesh = bundle.mesh
const glowMesh = bundle.glowMesh
const dummy = new THREE.Object3D() const dummy = new THREE.Object3D()
const color = new THREE.Color() const color = new THREE.Color()
const glowColor = new THREE.Color()
const spacing = WORLD_SIZE / size const spacing = WORLD_SIZE / size
const origin = -WORLD_SIZE / 2 + spacing / 2 const origin = -WORLD_SIZE / 2 + spacing / 2
const statePalette = createStatePalette(cells, options) const statePalette = createStatePalette(cells, options)
@@ -244,22 +262,14 @@ function updateVoxelMesh(bundle: SceneBundle, cells: VoxelCell[], size: number,
colorForCell(bundle, cell, size, ruleId, statePalette, color) colorForCell(bundle, cell, size, ruleId, statePalette, color)
mesh.setMatrixAt(instanceIndex, dummy.matrix) mesh.setMatrixAt(instanceIndex, dummy.matrix)
mesh.setColorAt(instanceIndex, color) mesh.setColorAt(instanceIndex, color)
glowColor.copy(color).lerp(new THREE.Color('#ffffff'), 0.16).multiplyScalar(1.18)
glowMesh.setMatrixAt(instanceIndex, dummy.matrix)
glowMesh.setColorAt(instanceIndex, glowColor)
instanceIndex += 1 instanceIndex += 1
} }
mesh.count = instanceIndex mesh.count = instanceIndex
glowMesh.count = instanceIndex
mesh.instanceMatrix.needsUpdate = true mesh.instanceMatrix.needsUpdate = true
glowMesh.instanceMatrix.needsUpdate = true
if (mesh.instanceColor) { if (mesh.instanceColor) {
mesh.instanceColor.needsUpdate = true mesh.instanceColor.needsUpdate = true
} }
if (glowMesh.instanceColor) {
glowMesh.instanceColor.needsUpdate = true
}
} }
export function createVoxelRendererCore(targetNode: HTMLDivElement, options: VoxelRendererOptions): VoxelRendererCore { export function createVoxelRendererCore(targetNode: HTMLDivElement, options: VoxelRendererOptions): VoxelRendererCore {
@@ -309,7 +319,6 @@ export function createVoxelRendererCore(targetNode: HTMLDivElement, options: Vox
bounds, bounds,
camera, camera,
controls, controls,
glowMesh: null,
grid, grid,
mesh: null, mesh: null,
mountNode: targetNode, mountNode: targetNode,
@@ -347,7 +356,6 @@ export function createVoxelRendererCore(targetNode: HTMLDivElement, options: Vox
window.cancelAnimationFrame(bundle.animationFrame) window.cancelAnimationFrame(bundle.animationFrame)
bundle.resizeObserver.disconnect() bundle.resizeObserver.disconnect()
disposeMesh(bundle.scene, bundle.mesh) disposeMesh(bundle.scene, bundle.mesh)
disposeMesh(bundle.scene, bundle.glowMesh)
bundle.bounds.geometry.dispose() bundle.bounds.geometry.dispose()
disposeMaterial(bundle.bounds.material) disposeMaterial(bundle.bounds.material)
bundle.grid.geometry.dispose() bundle.grid.geometry.dispose()
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>CA Studio Admin</title> <title>CA Studio Admin</title>
<script type="module" crossorigin src="/admin/assets/index-COp4VUl7.js"></script> <script type="module" crossorigin src="/admin/assets/index-DowkWVYj.js"></script>
<link rel="stylesheet" crossorigin href="/admin/assets/index-D7EhCe8j.css"> <link rel="stylesheet" crossorigin href="/admin/assets/index-D7EhCe8j.css">
</head> </head>
<body> <body>
@@ -0,0 +1,17 @@
UPDATE ca_engines
SET
params_schema =
(params_schema - 'seed') ||
jsonb_build_object(
'rng_seed',
jsonb_build_object(
'type', 'string',
'label', 'RNG seed',
'default', COALESCE(default_params->>'rng_seed', default_params->>'seed', 'naga'),
'group', 'Randomness'
)
),
default_params =
(default_params - 'seed') ||
jsonb_build_object('rng_seed', COALESCE(default_params->>'rng_seed', default_params->>'seed', 'naga'))
WHERE slug = 'naga-3d';
@@ -0,0 +1,55 @@
UPDATE ca_initial_condition_generators
SET
params_schema =
(params_schema - 'seed' - 'length') ||
jsonb_build_object(
'ic_seed',
jsonb_build_object(
'type', 'string',
'label', 'IC seed',
'default', COALESCE(default_params->>'ic_seed', default_params->>'seed', 'naga-markov')
),
'meanLength',
jsonb_build_object(
'type', 'range',
'label', 'Mean length',
'default', COALESCE(default_params->'meanLength', default_params->'length', '5'::jsonb),
'min', 1,
'max', 64,
'step', 1
),
'lengthStdDev',
jsonb_build_object(
'type', 'range',
'label', 'Length std. dev.',
'default', COALESCE(default_params->'lengthStdDev', '0'::jsonb),
'min', 0,
'max', 32,
'step', 1
)
),
default_params =
(default_params - 'seed' - 'length') ||
jsonb_build_object(
'ic_seed', COALESCE(default_params->>'ic_seed', default_params->>'seed', 'naga-markov'),
'meanLength', COALESCE(default_params->'meanLength', default_params->'length', '5'::jsonb),
'lengthStdDev', COALESCE(default_params->'lengthStdDev', '0'::jsonb)
)
WHERE slug = 'naga-markov-chains';
UPDATE ca_initial_condition_generators
SET
params_schema =
(params_schema - 'seed') ||
jsonb_build_object(
'ic_seed',
jsonb_build_object(
'type', 'string',
'label', 'IC seed',
'default', COALESCE(default_params->>'ic_seed', default_params->>'seed', 'studio-seed')
)
),
default_params =
(default_params - 'seed') ||
jsonb_build_object('ic_seed', COALESCE(default_params->>'ic_seed', default_params->>'seed', 'studio-seed'))
WHERE slug <> 'naga-markov-chains';
+29
View File
@@ -188,6 +188,35 @@ describe('CA engine runtimes', () => {
expect(stepped.cells).toEqual(cells(5, 5, [[0, 2]])) expect(stepped.cells).toEqual(cells(5, 5, [[0, 2]]))
}) })
it('uses simulation rng_seed to choose deterministic Naga heads', () => {
const current = cells(6, 6)
const baseParams = {
caClass: { dimensions: 3, states: 7 },
renderer: { id: 'voxel-3d' },
simulation: {
engineId: 'naga-3d',
ruleId: 'naga-3d',
grid: { size: [6, 6, 6], boundary: 'wrap' },
initialCondition: {
type: 'cells',
cells: [[1, 2, 2, 1], [4, 2, 2, 1]]
}
}
} satisfies SceneParams
const leftSeed = stepCaSimulation(current, settings({
...baseParams,
simulation: { ...baseParams.simulation, rng_seed: 'bar' }
}))
const rightSeed = stepCaSimulation(current, settings({
...baseParams,
simulation: { ...baseParams.simulation, rng_seed: 'alpha' }
}))
expect(leftSeed.settings.simulation?.initialCondition?.cells).toEqual([[4, 2, 2, 1], [0, 2, 2, 1]])
expect(rightSeed.settings.simulation?.initialCondition?.cells).toEqual([[1, 2, 2, 1], [3, 2, 2, 1]])
})
it('keeps Naga 3D sparse coordinates in place when fixed boundaries would leave the grid', () => { it('keeps Naga 3D sparse coordinates in place when fixed boundaries would leave the grid', () => {
const current = cells(5, 5) const current = cells(5, 5)
const params = settings({ const params = settings({
@@ -19,7 +19,7 @@ describe('CA initial condition runtimes', () => {
const input = { const input = {
caClass: { dimensions: 2, states: 2 }, caClass: { dimensions: 2, states: 2 },
settings: { simulation: { grid: { size: [5, 5, 1] } } }, settings: { simulation: { grid: { size: [5, 5, 1] } } },
params: { density: 0.35, seed: 'repeatable' } params: { density: 0.35, ic_seed: 'repeatable' }
} }
expect(generateInitialCondition('random-soup', input)).toEqual(generateInitialCondition('random-soup', input)) expect(generateInitialCondition('random-soup', input)).toEqual(generateInitialCondition('random-soup', input))
@@ -29,11 +29,11 @@ describe('CA initial condition runtimes', () => {
const generated = generateInitialCondition('random-soup', { const generated = generateInitialCondition('random-soup', {
caClass: { dimensions: 2, states: 2 }, caClass: { dimensions: 2, states: 2 },
settings: { simulation: { grid: { size: [3, 3, 1] } } }, settings: { simulation: { grid: { size: [3, 3, 1] } } },
params: { density: 2, seed: 'full' } params: { density: 2, ic_seed: 'full' }
}) })
expect(generated.cells).toHaveLength(9) expect(generated.cells).toHaveLength(9)
expect(generated.generator).toEqual({ kind: 'random-soup', density: 1, seed: 'full' }) expect(generated.generator).toEqual({ kind: 'random-soup', density: 1, ic_seed: 'full' })
}) })
it('lets custom initial condition runtimes plug into the same contract', () => { it('lets custom initial condition runtimes plug into the same contract', () => {
@@ -54,7 +54,7 @@ describe('CA initial condition runtimes', () => {
const generated = generateInitialCondition('naga-markov', { const generated = generateInitialCondition('naga-markov', {
caClass: { dimensions: 3, states: 7 }, caClass: { dimensions: 3, states: 7 },
settings: { simulation: { grid: { size: [12, 12, 12] } } }, settings: { simulation: { grid: { size: [12, 12, 12] } } },
params: { count: 2, length: 5, sameTypeProbability: 1, seed: 'straight-nagas' } params: { count: 2, meanLength: 5, lengthStdDev: 0, sameTypeProbability: 1, ic_seed: 'straight-nagas' }
}) })
expect(generated.cells).toHaveLength(10) expect(generated.cells).toHaveLength(10)
@@ -62,9 +62,10 @@ describe('CA initial condition runtimes', () => {
expect(generated.generator).toMatchObject({ expect(generated.generator).toMatchObject({
kind: 'naga-markov', kind: 'naga-markov',
count: 2, count: 2,
length: 5, meanLength: 5,
lengthStdDev: 0,
sameTypeProbability: 1, sameTypeProbability: 1,
seed: 'straight-nagas', ic_seed: 'straight-nagas',
created: 2 created: 2
}) })
@@ -93,6 +94,31 @@ describe('CA initial condition runtimes', () => {
} }
}) })
it('samples Naga Markov chain lengths from deterministic mean and deviation settings', () => {
const generated = generateInitialCondition('naga-markov', {
caClass: { dimensions: 3, states: 7 },
settings: { simulation: { grid: { size: [30, 30, 30] } } },
params: { count: 8, meanLength: 7, lengthStdDev: 3, sameTypeProbability: 0.8, ic_seed: 'variable-nagas' }
})
const repeated = generateInitialCondition('naga-markov', {
caClass: { dimensions: 3, states: 7 },
settings: { simulation: { grid: { size: [30, 30, 30] } } },
params: { count: 8, meanLength: 7, lengthStdDev: 3, sameTypeProbability: 0.8, ic_seed: 'variable-nagas' }
})
expect(generated).toEqual(repeated)
expect(generated.generator).toMatchObject({
kind: 'naga-markov',
count: 8,
meanLength: 7,
lengthStdDev: 3,
ic_seed: 'variable-nagas',
created: 8
})
expect(generated.cells.length).not.toBe(56)
})
it('rejects duplicate runtime ids and missing implementations', () => { it('rejects duplicate runtime ids and missing implementations', () => {
expect(() => expect(() =>
registerInitialConditionRuntime({ registerInitialConditionRuntime({
+23 -3
View File
@@ -56,11 +56,17 @@ describe('CA Studio API', () => {
default_params: { default_params: {
ruleId: 'naga-3d', ruleId: 'naga-3d',
rendererId: 'voxel-3d', rendererId: 'voxel-3d',
seed: 'naga', rng_seed: 'naga',
edgeWrap: true edgeWrap: true
} }
}) })
expect(naga.params_schema.ruleId.options).toEqual([{ value: 'naga-3d', label: '3D Naga' }]) expect(naga.params_schema.ruleId.options).toEqual([{ value: 'naga-3d', label: '3D Naga' }])
expect(naga.params_schema.rng_seed).toMatchObject({
type: 'string',
label: 'RNG seed',
default: 'naga',
group: 'Randomness'
})
expect(naga.params_schema.edgeWrap).toMatchObject({ expect(naga.params_schema.edgeWrap).toMatchObject({
type: 'boolean', type: 'boolean',
label: 'Edge wrap', label: 'Edge wrap',
@@ -84,15 +90,29 @@ describe('CA Studio API', () => {
generator_kind: 'naga-markov', generator_kind: 'naga-markov',
default_params: { default_params: {
count: 3, count: 3,
length: 5, meanLength: 5,
lengthStdDev: 0,
sameTypeProbability: 0.8, sameTypeProbability: 0.8,
seed: 'naga-markov' ic_seed: 'naga-markov'
} }
}) })
expect(generator.params_schema.meanLength).toMatchObject({
type: 'range',
default: 5
})
expect(generator.params_schema.lengthStdDev).toMatchObject({
type: 'range',
default: 0
})
expect(generator.params_schema.sameTypeProbability).toMatchObject({ expect(generator.params_schema.sameTypeProbability).toMatchObject({
type: 'range', type: 'range',
default: 0.8 default: 0.8
}) })
expect(generator.params_schema.ic_seed).toMatchObject({
type: 'string',
label: 'IC seed',
default: 'naga-markov'
})
}) })
}) })