Files
misode.github.io/src/app/components/previews/NoiseSettingsPreview.tsx
2024-11-27 06:01:05 +01:00

135 lines
5.8 KiB
TypeScript

import { clampedMap } from 'deepslate'
import type { mat3 } from 'gl-matrix'
import { vec2 } from 'gl-matrix'
import { useCallback, useRef, useState } from 'preact/hooks'
import { getWorldgenProjectData, useLocale, useProject, useVersion } from '../../contexts/index.js'
import { useAsync } from '../../hooks/index.js'
import { fetchRegistries } from '../../services/index.js'
import { Store } from '../../Store.js'
import { iterateWorld2D, randomSeed, safeJsonParse } from '../../Utils.js'
import { Btn, BtnInput, BtnMenu, ErrorPanel } from '../index.js'
import type { ColormapType } from './Colormap.js'
import { getColormap } from './Colormap.js'
import { ColormapSelector } from './ColormapSelector.jsx'
import { DEEPSLATE } from './Deepslate.js'
import type { PreviewProps } from './index.js'
import { InteractiveCanvas2D } from './InteractiveCanvas2D.jsx'
export const NoiseSettingsPreview = ({ docAndNode, shown }: PreviewProps) => {
const { locale } = useLocale()
const { version } = useVersion()
const { project } = useProject()
const [seed, setSeed] = useState(randomSeed())
const [biome, setBiome] = useState('minecraft:plains')
const [layer, setLayer] = useState('terrain')
const text = docAndNode.doc.getText()
const { value, error } = useAsync(async () => {
const data = safeJsonParse(text) ?? {}
const projectData = await getWorldgenProjectData(project)
await DEEPSLATE.loadVersion(version, projectData)
const biomeSource = { type: 'fixed', biome }
await DEEPSLATE.loadChunkGenerator(data, biomeSource, seed)
const noiseSettings = DEEPSLATE.getNoiseSettings()
const finalDensity = DEEPSLATE.loadDensityFunction(data?.noise_router?.final_density, noiseSettings.minY, noiseSettings.height, seed)
return { noiseSettings, finalDensity }
}, [text, seed, version, project, biome])
const { noiseSettings, finalDensity } = value ?? {}
const imageData = useRef<ImageData>()
const ctx = useRef<CanvasRenderingContext2D>()
const [focused, setFocused] = useState<string[]>([])
const [colormap, setColormap] = useState<ColormapType>(Store.getColormap() ?? 'viridis')
const onSetup = useCallback((canvas: HTMLCanvasElement) => {
const ctx2D = canvas.getContext('2d')
if (!ctx2D) return
ctx.current = ctx2D
}, [])
const onResize = useCallback((width: number, height: number) => {
if (!ctx.current) return
imageData.current = ctx.current.getImageData(0, 0, width, height)
}, [])
const onDraw = useCallback((transform: mat3) => {
if (!ctx.current || !imageData.current || !shown) return
if (layer === 'terrain') {
const pos = vec2.create()
const minX = vec2.transformMat3(pos, vec2.fromValues(0, 0), transform)[0]
const maxX = vec2.transformMat3(pos, vec2.fromValues(imageData.current.width-1, 0), transform)[0]
DEEPSLATE.generateChunks(minX, maxX - minX + 1, biome)
iterateWorld2D(imageData.current, transform, (x, y) => {
return DEEPSLATE.getBlockState(x, y)?.getName().toString()
}, (block) => {
return BlockColors[block ?? 'minecraft:air'] ?? [0, 0, 0]
})
} else if (layer === 'final_density') {
const colormapFn = getColormap(colormap)
const colorPicker = (t: number) => colormapFn(t <= 0.5 ? t - 0.08 : t + 0.08)
iterateWorld2D(imageData.current, transform, (x, y) => {
return finalDensity?.compute({ x, y, z: 0 }) ?? 0
}, (density) => {
const color = colorPicker(clampedMap(density, -1, 1, 1, 0))
return [color[0] * 256, color[1] * 256, color[2] * 256]
})
}
ctx.current.putImageData(imageData.current, 0, 0)
}, [noiseSettings, finalDensity, layer, colormap, biome, shown])
const onHover = useCallback((pos: [number, number] | undefined) => {
if (!pos || !noiseSettings || !finalDensity) {
setFocused([])
} else {
const [x, y] = pos
const inVoid = -y < noiseSettings.minY || -y >= noiseSettings.minY + noiseSettings.height
const density = finalDensity.compute({ x, y: -y, z: 0})
const block = inVoid ? 'void' : DEEPSLATE.getBlockState(x, -y)?.getName().path ?? 'unknown'
setFocused([`${block} D=${density.toPrecision(3)}`, `X=${x} Y=${-y}`])
}
}, [noiseSettings, finalDensity])
const { value: allBiomes } = useAsync(async () => {
const registries = await fetchRegistries(version)
return registries.get('worldgen/biome')
}, [version])
if (error) {
return <ErrorPanel error={error} prefix="Failed to initialize preview: " />
}
return <>
<div class="controls preview-controls">
{focused.map(s => <Btn label={s} class="no-pointer" /> )}
{layer === 'final_density' && <ColormapSelector value={colormap} onChange={setColormap} />}
<BtnMenu icon="gear" tooltip={locale('terrain_settings')}>
<BtnInput label={locale('preview.biome')} value={biome} onChange={setBiome} dataList={allBiomes} larger />
<Btn icon={layer === 'final_density' ? 'square_fill' : 'square'} label={locale('preview.final_density')} onClick={() => setLayer(layer === 'final_density' ? 'terrain' : 'final_density')} />
</BtnMenu>
<Btn icon="sync" tooltip={locale('generate_new_seed')}
onClick={() => setSeed(randomSeed())} />
</div>
<div class="full-preview">
<InteractiveCanvas2D onSetup={onSetup} onResize={onResize} onDraw={onDraw} onHover={onHover} pixelSize={4} startScale={0.5} startPosition={[0, 64]} />
</div>
</>
}
const BlockColors: Record<string, [number, number, number]> = {
'minecraft:air': [150, 160, 170],
'minecraft:water': [20, 80, 170],
'minecraft:lava': [200, 100, 0],
'minecraft:stone': [55, 55, 55],
'minecraft:deepslate': [34, 34, 36],
'minecraft:bedrock': [10, 10, 10],
'minecraft:grass_block': [47, 120, 23],
'minecraft:dirt': [64, 40, 8],
'minecraft:gravel': [70, 70, 70],
'minecraft:sand': [196, 180, 77],
'minecraft:sandstone': [148, 135, 52],
'minecraft:netherrack': [100, 40, 40],
'minecraft:crimson_nylium': [144, 22, 22],
'minecraft:warped_nylium': [28, 115, 113],
'minecraft:basalt': [73, 74, 85],
'minecraft:end_stone': [200, 200, 140],
}