Working but slow
This commit is contained in:
@@ -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'
|
||||
|
||||
export type NagaArrow = 1 | 2 | 3 | 4 | 5 | 6
|
||||
@@ -389,7 +389,7 @@ export function stepNaga3d(cells: Cells, settings: SceneParams) {
|
||||
? universeFromCoordinates(coordinates)
|
||||
: seedUniverseFromCells(cells)
|
||||
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 = {
|
||||
boundary: resolveBoundaryCondition(simulation.grid),
|
||||
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 { emptyCellsLike } from './cellGrid.js'
|
||||
|
||||
@@ -17,7 +17,7 @@ export function stepWildfire2d(cells: Cells, settings: SceneParams) {
|
||||
const width = cells[0]?.length ?? 0
|
||||
const boundary = resolveBoundaryCondition(settings.simulation?.grid)
|
||||
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 x = 0; x < width; x += 1) {
|
||||
|
||||
@@ -95,7 +95,7 @@ registerCaEngineRuntime(
|
||||
id: 'naga-3d',
|
||||
label: 'Naga 3D',
|
||||
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
|
||||
},
|
||||
{ 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))
|
||||
}
|
||||
|
||||
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 {
|
||||
const density = clampDensity(params.density)
|
||||
const seed = typeof params.seed === 'string' ? params.seed : 'studio-seed'
|
||||
const random = seededRandom(seed)
|
||||
const icSeed = resolveIcSeed(params, 'studio-seed')
|
||||
const random = seededRandom(icSeed)
|
||||
const [width, height, depth] = numericGridSize(settings.simulation?.grid, caClass.dimensions)
|
||||
const cells: number[][] = []
|
||||
|
||||
@@ -57,7 +74,7 @@ function randomSoup({ settings, caClass, params }: InitialConditionRuntimeInput)
|
||||
generator: {
|
||||
kind: 'random-soup',
|
||||
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 {
|
||||
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 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 seed = typeof params.seed === 'string' ? params.seed : 'naga-markov'
|
||||
const random = seededRandom(seed)
|
||||
const icSeed = resolveIcSeed(params, 'naga-markov')
|
||||
const random = seededRandom(icSeed)
|
||||
const occupied = new Set<string>()
|
||||
const cells: number[][] = []
|
||||
const maxAttempts = count * 300
|
||||
@@ -100,6 +119,7 @@ function nagaMarkov({ settings, caClass, params }: InitialConditionRuntimeInput)
|
||||
let y = Math.floor(random() * height)
|
||||
let z = Math.floor(random() * depth)
|
||||
let arrow = randomNagaArrow(random)
|
||||
const length = sampleNagaLength(meanLength, lengthStdDev, maxLength, random)
|
||||
const proposed: number[][] = []
|
||||
const proposedKeys = new Set<string>()
|
||||
let valid = true
|
||||
@@ -164,9 +184,10 @@ function nagaMarkov({ settings, caClass, params }: InitialConditionRuntimeInput)
|
||||
generator: {
|
||||
kind: 'naga-markov',
|
||||
count,
|
||||
length,
|
||||
meanLength,
|
||||
lengthStdDev,
|
||||
sameTypeProbability,
|
||||
seed,
|
||||
ic_seed: icSeed,
|
||||
created
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
return value === 'wrap' || value === 'mirror' || value === 'fixed'
|
||||
}
|
||||
|
||||
+39
-22
@@ -72,7 +72,7 @@ interface IcgConfig {
|
||||
dimensions: number
|
||||
states: number
|
||||
density: number
|
||||
seed: string
|
||||
icSeed: string
|
||||
}
|
||||
|
||||
interface EngineConfig {
|
||||
@@ -324,7 +324,7 @@ function defaultIcgConfig(): IcgConfig {
|
||||
dimensions: 2,
|
||||
states: 2,
|
||||
density: 0.28,
|
||||
seed: 'studio-seed'
|
||||
icSeed: 'studio-seed'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,16 +353,16 @@ function icgParamsSchema(config: IcgConfig): JsonObject {
|
||||
max: 1,
|
||||
step: 0.01
|
||||
},
|
||||
seed: {
|
||||
ic_seed: {
|
||||
type: 'string',
|
||||
label: 'Seed',
|
||||
default: config.seed
|
||||
label: 'IC seed',
|
||||
default: config.icSeed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function icgDefaultParams(config: IcgConfig): JsonObject {
|
||||
return { density: config.density, seed: config.seed }
|
||||
return { density: config.density, ic_seed: config.icSeed }
|
||||
}
|
||||
|
||||
function ruleLabel(ruleId: string) {
|
||||
@@ -1644,8 +1644,8 @@ function IcgConfigFields({
|
||||
</label>
|
||||
</div>
|
||||
<label>
|
||||
Seed
|
||||
<input value={config.seed} onChange={(event) => onChange((current) => ({ ...current, seed: event.target.value }))} />
|
||||
IC seed
|
||||
<input value={config.icSeed} onChange={(event) => onChange((current) => ({ ...current, icSeed: event.target.value }))} />
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
@@ -1817,7 +1817,11 @@ function configFromIcg(generator: InitialConditionGenerator): IcgConfig {
|
||||
dimensions: supportedClass?.dimensions ?? 2,
|
||||
states: typeof supportedClass?.states === 'number' ? supportedClass.states : 2,
|
||||
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 [icgDensity, setIcgDensity] = React.useState(0.28)
|
||||
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 [icgSeed, setIcgSeed] = React.useState('studio-seed')
|
||||
const [nodeName, setNodeName] = React.useState('Untitled preset')
|
||||
@@ -2664,24 +2669,28 @@ function LibraryEditor({
|
||||
function applyIcgDefaults(generator: InitialConditionGenerator | undefined) {
|
||||
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.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') {
|
||||
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) {
|
||||
if (generator.generator_kind === 'naga-markov') {
|
||||
return {
|
||||
count: icgNagaCount,
|
||||
length: icgNagaLength,
|
||||
meanLength: icgNagaMeanLength,
|
||||
lengthStdDev: icgNagaLengthStdDev,
|
||||
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() {
|
||||
@@ -3204,12 +3213,6 @@ function LibraryEditor({
|
||||
</label>
|
||||
{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">
|
||||
<label>
|
||||
Naga count
|
||||
@@ -3217,6 +3220,20 @@ function LibraryEditor({
|
||||
</label>
|
||||
<strong>{icgNagaCount}</strong>
|
||||
</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">
|
||||
<label>
|
||||
Same type probability
|
||||
@@ -3235,7 +3252,7 @@ function LibraryEditor({
|
||||
</div>
|
||||
)}
|
||||
<label>
|
||||
Seed
|
||||
IC seed
|
||||
<input value={icgSeed} onChange={(event) => setIcgSeed(event.target.value)} />
|
||||
</label>
|
||||
<button className="primary" disabled={!selectedNode || !selectedIcgId} type="button" onClick={applySelectedIcg}>
|
||||
|
||||
@@ -30,7 +30,6 @@ interface SceneBundle {
|
||||
bounds: THREE.LineSegments
|
||||
camera: THREE.PerspectiveCamera
|
||||
controls: OrbitControls
|
||||
glowMesh: THREE.InstancedMesh | null
|
||||
grid: THREE.GridHelper
|
||||
mesh: THREE.InstancedMesh | null
|
||||
mountNode: HTMLDivElement
|
||||
@@ -60,23 +59,59 @@ function disposeMesh(scene: THREE.Scene, mesh: THREE.InstancedMesh | null) {
|
||||
}
|
||||
|
||||
function createVoxelMaterial(primaryColor: THREE.Color) {
|
||||
return new THREE.MeshStandardMaterial({
|
||||
color: primaryColor,
|
||||
emissive: primaryColor.clone().multiplyScalar(0.58),
|
||||
emissiveIntensity: 0.48,
|
||||
metalness: 0.04,
|
||||
roughness: 0.68,
|
||||
vertexColors: true
|
||||
})
|
||||
}
|
||||
return new THREE.ShaderMaterial({
|
||||
defines: {
|
||||
USE_INSTANCING_COLOR: ''
|
||||
},
|
||||
uniforms: {
|
||||
ambientStrength: { value: 0.56 },
|
||||
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() {
|
||||
return new THREE.MeshBasicMaterial({
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false,
|
||||
opacity: 0.26,
|
||||
transparent: true,
|
||||
vertexColors: true
|
||||
void main() {
|
||||
vColor = instanceColor;
|
||||
vNormal = normalize(mat3(modelMatrix * instanceMatrix) * normal);
|
||||
|
||||
vec4 worldPosition = modelMatrix * instanceMatrix * vec4(position, 1.0);
|
||||
vWorldPosition = worldPosition.xyz;
|
||||
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
|
||||
}
|
||||
|
||||
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 = [
|
||||
'#70f45f',
|
||||
'#f0c94a',
|
||||
@@ -218,17 +241,12 @@ function colorForCell(
|
||||
|
||||
function updateVoxelMesh(bundle: SceneBundle, cells: VoxelCell[], size: number, ruleId: string, options: VoxelRendererOptions) {
|
||||
disposeMesh(bundle.scene, bundle.mesh)
|
||||
disposeMesh(bundle.scene, bundle.glowMesh)
|
||||
bundle.mesh = createVoxelMesh(size, options)
|
||||
bundle.glowMesh = createVoxelGlowMesh(size, options)
|
||||
bundle.scene.add(bundle.glowMesh)
|
||||
bundle.scene.add(bundle.mesh)
|
||||
|
||||
const mesh = bundle.mesh
|
||||
const glowMesh = bundle.glowMesh
|
||||
const dummy = new THREE.Object3D()
|
||||
const color = new THREE.Color()
|
||||
const glowColor = new THREE.Color()
|
||||
const spacing = WORLD_SIZE / size
|
||||
const origin = -WORLD_SIZE / 2 + spacing / 2
|
||||
const statePalette = createStatePalette(cells, options)
|
||||
@@ -244,22 +262,14 @@ function updateVoxelMesh(bundle: SceneBundle, cells: VoxelCell[], size: number,
|
||||
colorForCell(bundle, cell, size, ruleId, statePalette, color)
|
||||
mesh.setMatrixAt(instanceIndex, dummy.matrix)
|
||||
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
|
||||
}
|
||||
|
||||
mesh.count = instanceIndex
|
||||
glowMesh.count = instanceIndex
|
||||
mesh.instanceMatrix.needsUpdate = true
|
||||
glowMesh.instanceMatrix.needsUpdate = true
|
||||
if (mesh.instanceColor) {
|
||||
mesh.instanceColor.needsUpdate = true
|
||||
}
|
||||
if (glowMesh.instanceColor) {
|
||||
glowMesh.instanceColor.needsUpdate = true
|
||||
}
|
||||
}
|
||||
|
||||
export function createVoxelRendererCore(targetNode: HTMLDivElement, options: VoxelRendererOptions): VoxelRendererCore {
|
||||
@@ -309,7 +319,6 @@ export function createVoxelRendererCore(targetNode: HTMLDivElement, options: Vox
|
||||
bounds,
|
||||
camera,
|
||||
controls,
|
||||
glowMesh: null,
|
||||
grid,
|
||||
mesh: null,
|
||||
mountNode: targetNode,
|
||||
@@ -347,7 +356,6 @@ export function createVoxelRendererCore(targetNode: HTMLDivElement, options: Vox
|
||||
window.cancelAnimationFrame(bundle.animationFrame)
|
||||
bundle.resizeObserver.disconnect()
|
||||
disposeMesh(bundle.scene, bundle.mesh)
|
||||
disposeMesh(bundle.scene, bundle.glowMesh)
|
||||
bundle.bounds.geometry.dispose()
|
||||
disposeMaterial(bundle.bounds.material)
|
||||
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
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<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">
|
||||
</head>
|
||||
<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';
|
||||
@@ -188,6 +188,35 @@ describe('CA engine runtimes', () => {
|
||||
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', () => {
|
||||
const current = cells(5, 5)
|
||||
const params = settings({
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('CA initial condition runtimes', () => {
|
||||
const input = {
|
||||
caClass: { dimensions: 2, states: 2 },
|
||||
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))
|
||||
@@ -29,11 +29,11 @@ describe('CA initial condition runtimes', () => {
|
||||
const generated = generateInitialCondition('random-soup', {
|
||||
caClass: { dimensions: 2, states: 2 },
|
||||
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.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', () => {
|
||||
@@ -54,7 +54,7 @@ describe('CA initial condition runtimes', () => {
|
||||
const generated = generateInitialCondition('naga-markov', {
|
||||
caClass: { dimensions: 3, states: 7 },
|
||||
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)
|
||||
@@ -62,9 +62,10 @@ describe('CA initial condition runtimes', () => {
|
||||
expect(generated.generator).toMatchObject({
|
||||
kind: 'naga-markov',
|
||||
count: 2,
|
||||
length: 5,
|
||||
meanLength: 5,
|
||||
lengthStdDev: 0,
|
||||
sameTypeProbability: 1,
|
||||
seed: 'straight-nagas',
|
||||
ic_seed: 'straight-nagas',
|
||||
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', () => {
|
||||
expect(() =>
|
||||
registerInitialConditionRuntime({
|
||||
|
||||
@@ -56,11 +56,17 @@ describe('CA Studio API', () => {
|
||||
default_params: {
|
||||
ruleId: 'naga-3d',
|
||||
rendererId: 'voxel-3d',
|
||||
seed: 'naga',
|
||||
rng_seed: 'naga',
|
||||
edgeWrap: true
|
||||
}
|
||||
})
|
||||
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({
|
||||
type: 'boolean',
|
||||
label: 'Edge wrap',
|
||||
@@ -84,15 +90,29 @@ describe('CA Studio API', () => {
|
||||
generator_kind: 'naga-markov',
|
||||
default_params: {
|
||||
count: 3,
|
||||
length: 5,
|
||||
meanLength: 5,
|
||||
lengthStdDev: 0,
|
||||
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({
|
||||
type: 'range',
|
||||
default: 0.8
|
||||
})
|
||||
expect(generator.params_schema.ic_seed).toMatchObject({
|
||||
type: 'string',
|
||||
label: 'IC seed',
|
||||
default: 'naga-markov'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user