mirror of
https://github.com/misode/misode.github.io.git
synced 2026-05-01 21:23:12 +00:00
Voxel rendering + refactor interactive canvas (#322)
* Add voxel rendering to density function preview * InteractiveCanvas component * Use interactive canvas for noise preview * Use interactive canvas for noise settings preview * Extract common iterateWorld2D logic * Use InteractiveCanvas2D for biome source preview * Display final density in noise settings preview hover * Move remaining preview code * Hide noise router info for checkerboard and fixed * Add higher resolution biome map * User interactive canvas for decorator preview
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { ColormapType } from './previews/Colormap.js'
|
||||
import type { ColormapType } from './components/previews/Colormap.js'
|
||||
import type { VersionId } from './services/index.js'
|
||||
|
||||
type Method = 'menu' | 'hotkey'
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import type { ColormapType } from './components/previews/Colormap.js'
|
||||
import { ColormapTypes } from './components/previews/Colormap.js'
|
||||
import type { Project } from './contexts/index.js'
|
||||
import { DRAFT_PROJECT } from './contexts/index.js'
|
||||
import type { ColormapType } from './previews/Colormap.js'
|
||||
import { ColormapTypes } from './previews/Colormap.js'
|
||||
import type { VersionId } from './services/index.js'
|
||||
import { DEFAULT_VERSION, VersionIds } from './services/index.js'
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { DataModel } from '@mcschema/core'
|
||||
import { Path } from '@mcschema/core'
|
||||
import * as zip from '@zip.js/zip.js'
|
||||
import type { Random } from 'deepslate/core'
|
||||
import type { mat3 } from 'gl-matrix'
|
||||
import { vec2 } from 'gl-matrix'
|
||||
import yaml from 'js-yaml'
|
||||
import { route } from 'preact-router'
|
||||
import rfdc from 'rfdc'
|
||||
@@ -356,3 +358,22 @@ export function getWeightedRandom<T>(random: Random, entries: T[], getWeight: (e
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function iterateWorld2D<D>(img: ImageData, transform: mat3, getData: (x: number, y: number) => D, getColor: (d: D) => [number, number, number]) {
|
||||
const pos = vec2.create()
|
||||
const arr = Array(img.width * img.height)
|
||||
for (let x = 0; x < img.width; x += 1) {
|
||||
for (let y = 0; y < img.height; y += 1) {
|
||||
const i = x + y * img.width
|
||||
vec2.transformMat3(pos, vec2.fromValues(x, y), transform)
|
||||
arr[i] = getData(Math.floor(pos[0]), -Math.floor(pos[1]))
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < img.width * img.height; i += 1) {
|
||||
const color = getColor(arr[i])
|
||||
img.data[4 * i] = color[0]
|
||||
img.data[4 * i + 1] = color[1]
|
||||
img.data[4 * i + 2] = color[2]
|
||||
img.data[4 * i + 3] = 255
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ import { Identifier } from 'deepslate/core'
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { useVersion } from '../contexts/Version.jsx'
|
||||
import { useAsync } from '../hooks/useAsync.js'
|
||||
import { itemHasGlint } from '../previews/LootTable.js'
|
||||
import { renderItem } from '../services/Resources.js'
|
||||
import { getCollections } from '../services/Schemas.js'
|
||||
import { ItemTooltip } from './ItemTooltip.jsx'
|
||||
import { Octicon } from './Octicon.jsx'
|
||||
import { itemHasGlint } from './previews/LootTable.js'
|
||||
|
||||
interface Props {
|
||||
item: ItemStack,
|
||||
|
||||
@@ -24,35 +24,31 @@ export function PreviewPanel({ model, version, id, shown }: PreviewPanelProps) {
|
||||
})
|
||||
|
||||
if (!model) return <></>
|
||||
const data = model.get(new Path([]))
|
||||
if (!data) return <></>
|
||||
|
||||
if (id === 'loot_table') {
|
||||
const data = model.get(new Path([]))
|
||||
if (data) return <LootTablePreview {...{ model, version, shown, data }} />
|
||||
return <LootTablePreview {...{ model, version, shown, data }} />
|
||||
}
|
||||
|
||||
if (id === 'dimension' && model.get(new Path(['generator', 'type']))?.endsWith('noise')) {
|
||||
const data = model.get(new Path(['generator', 'biome_source']))
|
||||
if (data) return <BiomeSourcePreview {...{ model, version, shown, data }} />
|
||||
return <BiomeSourcePreview {...{ model, version, shown, data }} />
|
||||
}
|
||||
|
||||
if (id === 'worldgen/density_function') {
|
||||
const data = model.get(new Path([]))
|
||||
if (data) return <DensityFunctionPreview {...{ model, version, shown, data }} />
|
||||
return <DensityFunctionPreview {...{ model, version, shown, data }} />
|
||||
}
|
||||
|
||||
if (id === 'worldgen/noise') {
|
||||
const data = model.get(new Path([]))
|
||||
if (data) return <NoisePreview {...{ model, version, shown, data }} />
|
||||
return <NoisePreview {...{ model, version, shown, data }} />
|
||||
}
|
||||
|
||||
if (id === 'worldgen/noise_settings') {
|
||||
const data = model.get(new Path([]))
|
||||
if (data) return <NoiseSettingsPreview {...{ model, version, shown, data }} />
|
||||
if (id === 'worldgen/noise_settings' && checkVersion(version, '1.18')) {
|
||||
return <NoiseSettingsPreview {...{ model, version, shown, data }} />
|
||||
}
|
||||
|
||||
if ((id === 'worldgen/placed_feature' || (id === 'worldgen/configured_feature' && checkVersion(version, '1.16', '1.17')))) {
|
||||
const data = model.get(new Path([]))
|
||||
if (data) return <DecoratorPreview {...{ model, version, shown, data }} />
|
||||
return <DecoratorPreview {...{ model, version, shown, data }} />
|
||||
}
|
||||
|
||||
return <></>
|
||||
|
||||
@@ -1,99 +1,273 @@
|
||||
import { DataModel, Path } from '@mcschema/core'
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { useLocale, useProject, useStore } from '../../contexts/index.js'
|
||||
import { useCanvas } from '../../hooks/index.js'
|
||||
import { biomeMap, getBiome } from '../../previews/index.js'
|
||||
import { randomSeed } from '../../Utils.js'
|
||||
import { DataModel } from '@mcschema/core'
|
||||
import { clampedMap } from 'deepslate'
|
||||
import { mat3 } from 'gl-matrix'
|
||||
import { useCallback, useRef, useState } from 'preact/hooks'
|
||||
import { getProjectData, useLocale, useProject, useStore } from '../../contexts/index.js'
|
||||
import { useAsync } from '../../hooks/index.js'
|
||||
import { checkVersion } from '../../services/Schemas.js'
|
||||
import { Store } from '../../Store.js'
|
||||
import { iterateWorld2D, randomSeed, stringToColor } from '../../Utils.js'
|
||||
import { Btn, BtnMenu, NumberInput } 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 BiomeSourcePreview = ({ model, data, shown, version }: PreviewProps) => {
|
||||
const LAYERS = ['biomes', 'temperature', 'vegetation', 'continents', 'erosion', 'ridges', 'depth'] as const
|
||||
type Layer = typeof LAYERS[number]
|
||||
|
||||
const DETAIL_DELAY = 300
|
||||
const DETAIL_SCALE = 2
|
||||
|
||||
export const BiomeSourcePreview = ({ data, shown, version }: PreviewProps) => {
|
||||
const { locale } = useLocale()
|
||||
const { project } = useProject()
|
||||
const [seed, setSeed] = useState(randomSeed())
|
||||
const [scale, setScale] = useState(2)
|
||||
const [yOffset, setYOffset] = useState(64)
|
||||
const [focused, setFocused] = useState<{[k: string]: number | string} | undefined>(undefined)
|
||||
const { biomeColors } = useStore()
|
||||
const offset = useRef<[number, number]>([0, 0])
|
||||
const res = useRef(1)
|
||||
const refineTimeout = useRef<number>()
|
||||
const [seed, setSeed] = useState(randomSeed())
|
||||
const [layer, setLayer] = useState<Layer>('biomes')
|
||||
const [yOffset, setYOffset] = useState(64)
|
||||
const [focused, setFocused] = useState<string[]>([])
|
||||
const [focused2, setFocused2] = useState<string[]>([])
|
||||
|
||||
const settings = DataModel.unwrapLists(model.get(new Path(['generator', 'settings'])))
|
||||
const state = JSON.stringify([data, settings])
|
||||
const type: string = data.type?.replace(/^minecraft:/, '')
|
||||
const state = JSON.stringify(data)
|
||||
const type: string = data?.generator?.biome_source?.type?.replace(/^minecraft:/, '') ?? ''
|
||||
const hasRandomness = type === 'multi_noise' || type === 'the_end'
|
||||
|
||||
const { canvas, redraw } = useCanvas({
|
||||
size() {
|
||||
return [200 / res.current, 200 / res.current]
|
||||
},
|
||||
async draw(img) {
|
||||
const options = { settings, biomeColors, offset: offset.current, scale, seed, res: res.current, version, project, y: yOffset }
|
||||
await biomeMap(data, img, options)
|
||||
if (res.current === 4) {
|
||||
clearTimeout(refineTimeout.current)
|
||||
refineTimeout.current = setTimeout(() => {
|
||||
res.current = 1
|
||||
redraw()
|
||||
}, 150) as any
|
||||
}
|
||||
},
|
||||
async onDrag(dx, dy) {
|
||||
offset.current[0] = offset.current[0] + dx * 200
|
||||
offset.current[1] = offset.current[1] + dy * 200
|
||||
clearTimeout(refineTimeout.current)
|
||||
res.current = hasRandomness ? 4 : 1
|
||||
redraw()
|
||||
},
|
||||
async onHover(x, y) {
|
||||
const options = { settings, biomeColors, offset: offset.current, scale, seed: seed, res: 1, version, project, y: yOffset }
|
||||
const biome = await getBiome(data, Math.floor(x * 200), Math.floor(y * 200), options)
|
||||
setFocused(biome)
|
||||
},
|
||||
onLeave() {
|
||||
setFocused(undefined)
|
||||
},
|
||||
}, [version, state, scale, seed, yOffset, biomeColors, project])
|
||||
|
||||
useEffect(() => {
|
||||
if (shown) {
|
||||
res.current = hasRandomness ? 4 : 1
|
||||
redraw()
|
||||
const { value } = useAsync(async function loadBiomeSource() {
|
||||
await DEEPSLATE.loadVersion(version, getProjectData(project))
|
||||
await DEEPSLATE.loadChunkGenerator(DataModel.unwrapLists(data?.generator?.settings), DataModel.unwrapLists(data?.generator?.biome_source), seed)
|
||||
return {
|
||||
biomeSource: { loaded: true },
|
||||
noiseRouter: checkVersion(version, '1.19') ? DEEPSLATE.getNoiseRouter() : undefined,
|
||||
}
|
||||
}, [version, state, scale, seed, yOffset, shown, biomeColors, project])
|
||||
}, [state, seed, project, version])
|
||||
const { biomeSource, noiseRouter } = value ?? {}
|
||||
|
||||
const changeScale = (newScale: number) => {
|
||||
newScale = Math.max(1, Math.round(newScale))
|
||||
offset.current[0] = offset.current[0] * scale / newScale
|
||||
offset.current[1] = offset.current[1] * scale / newScale
|
||||
setScale(newScale)
|
||||
}
|
||||
const actualLayer = noiseRouter ? layer : 'biomes'
|
||||
|
||||
const ctx = useRef<CanvasRenderingContext2D>()
|
||||
const imageData = useRef<ImageData>()
|
||||
const [colormap, setColormap] = useState<ColormapType>(Store.getColormap() ?? 'viridis')
|
||||
|
||||
const detailCanvas = useRef<HTMLCanvasElement>(null)
|
||||
const detailCtx = useRef<CanvasRenderingContext2D>()
|
||||
const detailImageData = useRef<ImageData>()
|
||||
const detailTimeout = useRef<number>()
|
||||
|
||||
const onSetup = useCallback(function onSetup(canvas: HTMLCanvasElement) {
|
||||
ctx.current = canvas.getContext('2d') ?? undefined
|
||||
detailCtx.current = detailCanvas.current?.getContext('2d') ?? undefined
|
||||
}, [])
|
||||
const onResize = useCallback(function onResize(width: number, height: number) {
|
||||
if (ctx.current) {
|
||||
imageData.current = ctx.current.getImageData(0, 0, width, height)
|
||||
}
|
||||
if (detailCtx.current && detailCanvas.current) {
|
||||
detailCanvas.current.width = width * DETAIL_SCALE
|
||||
detailCanvas.current.height = height * DETAIL_SCALE
|
||||
detailImageData.current = detailCtx.current.getImageData(0, 0, width * DETAIL_SCALE, height * DETAIL_SCALE)
|
||||
}
|
||||
}, [])
|
||||
const onDraw = useCallback(function onDraw(transform: mat3) {
|
||||
if (!ctx.current || !imageData.current || !shown) return
|
||||
|
||||
function actualDraw(ctx: CanvasRenderingContext2D, img: ImageData, transform: mat3) {
|
||||
if (actualLayer === 'biomes' && biomeSource) {
|
||||
iterateWorld2D(img, transform, (x, y) => {
|
||||
return DEEPSLATE.getBiome(x, yOffset, y)
|
||||
}, (biome) => {
|
||||
return getBiomeColor(biome, biomeColors)
|
||||
})
|
||||
} else if (actualLayer !== 'biomes' && noiseRouter) {
|
||||
const df = noiseRouter[actualLayer]
|
||||
const colorPicker = getColormap(colormap)
|
||||
iterateWorld2D(img, transform, (x, y) => {
|
||||
return df.compute({ x: x*4, y: yOffset, z: y*4 }) ?? 0
|
||||
}, (density) => {
|
||||
const color = colorPicker(clampedMap(density, -1, 1, 0, 1))
|
||||
return [color[0] * 256, color[1] * 256, color[2] * 256]
|
||||
})
|
||||
}
|
||||
ctx.putImageData(img, 0, 0)
|
||||
}
|
||||
|
||||
actualDraw(ctx.current, imageData.current, transform)
|
||||
detailCanvas.current?.classList.remove('visible')
|
||||
|
||||
clearTimeout(detailTimeout.current)
|
||||
if (hasRandomness) {
|
||||
detailTimeout.current = setTimeout(function detailTimout() {
|
||||
if (!detailCtx.current || !detailImageData.current || !detailCanvas.current) return
|
||||
const detailTransform = mat3.create()
|
||||
mat3.scale(detailTransform, transform, [1/DETAIL_SCALE, 1/DETAIL_SCALE])
|
||||
actualDraw(detailCtx.current, detailImageData.current, detailTransform)
|
||||
detailCanvas.current.classList.add('visible')
|
||||
}, DETAIL_DELAY) as unknown as number
|
||||
}
|
||||
}, [biomeSource, noiseRouter, actualLayer, colormap, shown, biomeColors, yOffset])
|
||||
const onHover = useCallback(function onHover(pos: [number, number] | undefined) {
|
||||
const [x, y] = pos ?? [0, 0]
|
||||
if (!pos || !biomeSource) {
|
||||
setFocused([])
|
||||
} else {
|
||||
const biome = DEEPSLATE.getBiome(x, yOffset, -y)
|
||||
setFocused([biome.replace(/^minecraft:/, ''), `X=${x*4} Z=${-y*4}`])
|
||||
}
|
||||
if (!pos || !noiseRouter) {
|
||||
setFocused2([])
|
||||
} else {
|
||||
setFocused2([LAYERS.flatMap(l => {
|
||||
if (l === 'biomes') return []
|
||||
const value = noiseRouter[l].compute({ x: x*4, y: yOffset, z: -y*4 })
|
||||
return [`${locale(`layer.${l}`).charAt(0)}=${value.toPrecision(2)}`]
|
||||
}).join(' ')])
|
||||
}
|
||||
}, [biomeSource, noiseRouter, yOffset])
|
||||
|
||||
return <>
|
||||
{(hasRandomness && focused2) && <div class="controls secondary-controls">
|
||||
{focused2.map(s => <Btn label={s} class="no-pointer" /> )}
|
||||
</div>}
|
||||
<div class="controls preview-controls">
|
||||
{focused && <Btn label={focused.biome as string} class="no-pointer" />}
|
||||
<Btn icon="dash" tooltip={locale('zoom_out')}
|
||||
onClick={() => changeScale(scale * 2)} />
|
||||
<Btn icon="plus" tooltip={locale(Math.round(scale) <= 1 ? 'zoom_in_limit' : 'zoom_in')}
|
||||
disabled={Math.round(scale) <= 1}
|
||||
onClick={() => changeScale(scale / 2)} />
|
||||
{focused.map(s => <Btn label={s} class="no-pointer" /> )}
|
||||
{actualLayer !== 'biomes' && <ColormapSelector value={colormap} onChange={setColormap} />}
|
||||
{hasRandomness && <>
|
||||
<BtnMenu icon="stack">
|
||||
<BtnMenu icon="stack" tooltip={locale('layer')}>
|
||||
<div class="btn btn-input" onClick={e => e.stopPropagation()}>
|
||||
<span>{locale('y')}</span>
|
||||
<NumberInput value={yOffset} onChange={setYOffset} />
|
||||
</div>
|
||||
{checkVersion(version, '1.19') && LAYERS.map(l => <Btn label={locale(`layer.${l}`)} active={l === actualLayer} onClick={() => setLayer(l)} />)}
|
||||
</BtnMenu>
|
||||
<Btn icon="sync" tooltip={locale('generate_new_seed')}
|
||||
onClick={() => setSeed(randomSeed())} />
|
||||
</>}
|
||||
</div>
|
||||
{focused?.temperature !== undefined && <div class="controls secondary-controls">
|
||||
<Btn class="no-pointer" label={Object.entries(focused)
|
||||
.filter(([k]) => k !== 'biome')
|
||||
.map(([k, v]) => `${k[0].toUpperCase()}: ${(v as number).toFixed(2)}`).join(' ')}/>
|
||||
</div>}
|
||||
<canvas ref={canvas} width="200" height="200"></canvas>
|
||||
<div class="full-preview">
|
||||
<InteractiveCanvas2D onSetup={onSetup} onResize={onResize} onDraw={onDraw} onHover={onHover} pixelSize={hasRandomness ? 8 : 2} />
|
||||
{hasRandomness && <canvas class={'preview-details'} ref={detailCanvas} />}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
type Triple = [number, number, number]
|
||||
type BiomeColors = Record<string, Triple>
|
||||
function getBiomeColor(biome: string, biomeColors: BiomeColors): Triple {
|
||||
if (!biome) {
|
||||
return [128, 128, 128]
|
||||
}
|
||||
const color = biomeColors[biome] ?? VanillaColors[biome]
|
||||
if (color === undefined) {
|
||||
return stringToColor(biome)
|
||||
}
|
||||
return color
|
||||
}
|
||||
|
||||
export const VanillaColors: Record<string, Triple> = {
|
||||
'minecraft:badlands': [217,69,21],
|
||||
'minecraft:badlands_plateau': [202,140,101],
|
||||
'minecraft:bamboo_jungle': [118,142,20],
|
||||
'minecraft:bamboo_jungle_hills': [59,71,10],
|
||||
'minecraft:basalt_deltas': [64,54,54],
|
||||
'minecraft:beach': [250,222,85],
|
||||
'minecraft:birch_forest': [48,116,68],
|
||||
'minecraft:birch_forest_hills': [31,95,50],
|
||||
'minecraft:cold_ocean': [32,32,112],
|
||||
'minecraft:crimson_forest': [221,8,8],
|
||||
'minecraft:dark_forest': [64,81,26],
|
||||
'minecraft:dark_forest_hills': [104,121,66],
|
||||
'minecraft:deep_cold_ocean': [32,32,56],
|
||||
'minecraft:deep_frozen_ocean': [64,64,144],
|
||||
'minecraft:deep_lukewarm_ocean': [0,0,64],
|
||||
'minecraft:deep_ocean': [0,0,48],
|
||||
'minecraft:deep_warm_ocean': [0,0,80],
|
||||
'minecraft:desert': [250,148,24],
|
||||
'minecraft:desert_hills': [210,95,18],
|
||||
'minecraft:desert_lakes': [255,188,64],
|
||||
'minecraft:end_barrens': [39,30,61],
|
||||
'minecraft:end_highlands': [232,244,178],
|
||||
'minecraft:end_midlands': [194,187,136],
|
||||
'minecraft:eroded_badlands': [255,109,61],
|
||||
'minecraft:flower_forest': [45,142,73],
|
||||
'minecraft:forest': [5,102,33],
|
||||
'minecraft:frozen_ocean': [112,112,214],
|
||||
'minecraft:frozen_river': [160,160,255],
|
||||
'minecraft:giant_spruce_taiga': [129,142,121],
|
||||
'minecraft:old_growth_spruce_taiga': [129,142,121],
|
||||
'minecraft:giant_spruce_taiga_hills': [109,119,102],
|
||||
'minecraft:giant_tree_taiga': [89,102,81],
|
||||
'minecraft:old_growth_pine_taiga': [89,102,81],
|
||||
'minecraft:giant_tree_taiga_hills': [69,79,62],
|
||||
'minecraft:gravelly_hills': [136,136,136],
|
||||
'minecraft:gravelly_mountains': [136,136,136],
|
||||
'minecraft:windswept_gravelly_hills': [136,136,136],
|
||||
'minecraft:ice_spikes': [180,220,220],
|
||||
'minecraft:jungle': [83,123,9],
|
||||
'minecraft:jungle_edge': [98,139,23],
|
||||
'minecraft:sparse_jungle': [98,139,23],
|
||||
'minecraft:jungle_hills': [44,66,5],
|
||||
'minecraft:lukewarm_ocean': [0,0,144],
|
||||
'minecraft:modified_badlands_plateau': [242,180,141],
|
||||
'minecraft:modified_gravelly_mountains': [120,152,120],
|
||||
'minecraft:modified_jungle': [123,163,49],
|
||||
'minecraft:modified_jungle_edge': [138,179,63],
|
||||
'minecraft:modified_wooded_badlands_plateau': [216,191,141],
|
||||
'minecraft:mountain_edge': [114,120,154],
|
||||
'minecraft:extreme_hills': [96,96,96],
|
||||
'minecraft:mountains': [96,96,96],
|
||||
'minecraft:windswept_hills': [96,96,96],
|
||||
'minecraft:mushroom_field_shore': [160,0,255],
|
||||
'minecraft:mushroom_fields': [255,0,255],
|
||||
'minecraft:nether_wastes': [191,59,59],
|
||||
'minecraft:ocean': [0,0,112],
|
||||
'minecraft:plains': [141,179,96],
|
||||
'minecraft:river': [0,0,255],
|
||||
'minecraft:savanna': [189,178,95],
|
||||
'minecraft:savanna_plateau': [167,157,100],
|
||||
'minecraft:shattered_savanna': [229,218,135],
|
||||
'minecraft:windswept_savanna': [229,218,135],
|
||||
'minecraft:shattered_savanna_plateau': [207,197,140],
|
||||
'minecraft:small_end_islands': [16,12,28],
|
||||
'minecraft:snowy_beach': [250,240,192],
|
||||
'minecraft:snowy_mountains': [160,160,160],
|
||||
'minecraft:snowy_taiga': [49,85,74],
|
||||
'minecraft:snowy_taiga_hills': [36,63,54],
|
||||
'minecraft:snowy_taiga_mountains': [89,125,114],
|
||||
'minecraft:snowy_tundra': [255,255,255],
|
||||
'minecraft:snowy_plains': [255,255,255],
|
||||
'minecraft:soul_sand_valley': [94,56,48],
|
||||
'minecraft:stone_shore': [162,162,132],
|
||||
'minecraft:stony_shore': [162,162,132],
|
||||
'minecraft:sunflower_plains': [181,219,136],
|
||||
'minecraft:swamp': [7,249,178],
|
||||
'minecraft:swamp_hills': [47,255,218],
|
||||
'minecraft:taiga': [11,102,89],
|
||||
'minecraft:taiga_hills': [22,57,51],
|
||||
'minecraft:taiga_mountains': [51,142,129],
|
||||
'minecraft:tall_birch_forest': [88,156,108],
|
||||
'minecraft:old_growth_birch_forest': [88,156,108],
|
||||
'minecraft:tall_birch_hills': [71,135,90],
|
||||
'minecraft:the_end': [59,39,84],
|
||||
'minecraft:the_void': [0,0,0],
|
||||
'minecraft:warm_ocean': [0,0,172],
|
||||
'minecraft:warped_forest': [73,144,123],
|
||||
'minecraft:wooded_badlands_plateau': [176,151,101],
|
||||
'minecraft:wooded_badlands': [176,151,101],
|
||||
'minecraft:wooded_hills': [34,85,28],
|
||||
'minecraft:wooded_mountains': [80,112,80],
|
||||
'minecraft:windswept_forest': [80,112,80],
|
||||
'minecraft:snowy_slopes': [140, 195, 222],
|
||||
'minecraft:lofty_peaks': [196, 168, 193],
|
||||
'minecraft:jagged_peaks': [196, 168, 193],
|
||||
'minecraft:snowcapped_peaks': [200, 198, 200],
|
||||
'minecraft:frozen_peaks': [200, 198, 200],
|
||||
'minecraft:stony_peaks': [82, 92, 103],
|
||||
'minecraft:grove': [150, 150, 189],
|
||||
'minecraft:meadow': [169, 197, 80],
|
||||
'minecraft:lush_caves': [112, 255, 79],
|
||||
'minecraft:dripstone_caves': [140, 124, 0],
|
||||
'minecraft:deep_dark': [10, 14, 19],
|
||||
'minecraft:mangrove_swamp': [36,196,142],
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { clamp } from '../Utils.js'
|
||||
import { clamp } from '../../Utils.js'
|
||||
import colormaps from './colormaps.json'
|
||||
|
||||
// Implementation based on https://github.com/politiken-journalism/scale-color-perceptual/blob/master/utils/interpolate.js
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useCallback } from 'preact/hooks'
|
||||
import { Analytics } from '../../Analytics.js'
|
||||
import type { ColormapType } from '../../previews/Colormap.js'
|
||||
import { ColormapTypes } from '../../previews/Colormap.js'
|
||||
import { Store } from '../../Store.js'
|
||||
import { Btn } from '../Btn.jsx'
|
||||
import { BtnMenu } from '../BtnMenu.jsx'
|
||||
import type { ColormapType } from './Colormap.js'
|
||||
import { ColormapTypes } from './Colormap.js'
|
||||
|
||||
interface Props {
|
||||
value: ColormapType,
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { DataModel } from '@mcschema/core'
|
||||
import type { Random } from 'deepslate/worldgen'
|
||||
import { LegacyRandom, PerlinNoise } from 'deepslate/worldgen'
|
||||
import type { VersionId } from '../services/index.js'
|
||||
import { checkVersion } from '../services/index.js'
|
||||
import { clamp, isObject, stringToColor } from '../Utils.js'
|
||||
import type { BlockPos, ChunkPos, PerlinNoise, Random } from 'deepslate/worldgen'
|
||||
import type { VersionId } from '../../services/index.js'
|
||||
import { checkVersion } from '../../services/index.js'
|
||||
import type { Color } from '../../Utils.js'
|
||||
import { clamp, isObject, stringToColor } from '../../Utils.js'
|
||||
|
||||
type BlockPos = [number, number, number]
|
||||
type Placement = [BlockPos, number]
|
||||
export type Placement = [BlockPos, number]
|
||||
|
||||
type PlacementContext = {
|
||||
export type PlacementContext = {
|
||||
placements: Placement[],
|
||||
features: string[],
|
||||
random: Random,
|
||||
@@ -18,12 +17,17 @@ type PlacementContext = {
|
||||
nextFloat(): number,
|
||||
nextInt(max: number): number,
|
||||
nextGaussian(): number,
|
||||
sampleInt(provider: any): number,
|
||||
}
|
||||
|
||||
export type PlacedFeature = {
|
||||
pos: BlockPos,
|
||||
feature: string,
|
||||
color: Color,
|
||||
}
|
||||
|
||||
const terrain = [50, 50, 51, 51, 52, 52, 53, 54, 56, 57, 57, 58, 58, 59, 60, 60, 60, 59, 59, 59, 60, 61, 61, 62, 63, 63, 64, 64, 64, 65, 65, 66, 66, 65, 65, 66, 66, 67, 67, 67, 68, 69, 71, 73, 74, 76, 79, 80, 81, 81, 82, 83, 83, 82, 82, 81, 81, 80, 80, 80, 81, 81, 82, 82]
|
||||
|
||||
const featureColors = [
|
||||
const featureColors: Color[] = [
|
||||
[255, 77, 54], // red
|
||||
[59, 118, 255], // blue
|
||||
[91, 207, 25], // green
|
||||
@@ -32,58 +36,22 @@ const featureColors = [
|
||||
[52, 204, 209], // cyan
|
||||
]
|
||||
|
||||
export type DecoratorOptions = {
|
||||
size: [number, number, number],
|
||||
seed: bigint,
|
||||
version: VersionId,
|
||||
}
|
||||
export function decorator(state: any, img: ImageData, options: DecoratorOptions) {
|
||||
const random = new LegacyRandom(options.seed)
|
||||
const ctx: PlacementContext = {
|
||||
placements: [],
|
||||
features: [],
|
||||
random,
|
||||
biomeInfoNoise: new PerlinNoise(random.fork(), 0, [1]),
|
||||
seaLevel: 63,
|
||||
version: options.version,
|
||||
nextFloat: () => random.nextFloat(),
|
||||
nextInt: (max: number) => random.nextInt(max),
|
||||
nextGaussian: () => Math.sqrt(-2 * Math.log(1 - random.nextFloat())) * Math.cos(2 * Math.PI * random.nextFloat()),
|
||||
sampleInt(value) { return sampleInt(value, this) },
|
||||
export function decorateChunk(pos: ChunkPos, state: any, ctx: PlacementContext): PlacedFeature[] {
|
||||
if (checkVersion(ctx.version, undefined, '1.17')) {
|
||||
getPlacements([pos[0] * 16, 0, pos[1] * 16], DataModel.unwrapLists(state), ctx)
|
||||
} else {
|
||||
modifyPlacement([pos[0] * 16, 0, pos[1] * 16], DataModel.unwrapLists(state.placement), ctx)
|
||||
}
|
||||
|
||||
for (let x = 0; x < options.size[0] / 16; x += 1) {
|
||||
for (let z = 0; z < options.size[2] / 16; z += 1) {
|
||||
if (checkVersion(options.version, undefined, '1.17')) {
|
||||
getPlacements([x * 16, 0, z * 16], DataModel.unwrapLists(state), ctx)
|
||||
} else {
|
||||
modifyPlacement([x * 16, 0, z * 16], DataModel.unwrapLists(state.placement), ctx)
|
||||
}
|
||||
return ctx.placements.map(([pos, i]) => {
|
||||
const feature = ctx.features[i]
|
||||
let color = i < featureColors.length ? featureColors[i] : stringToColor(feature)
|
||||
color = [clamp(color[0], 50, 205), clamp(color[1], 50, 205), clamp(color[2], 50, 205)]
|
||||
if ((Math.floor(pos[0] / 16) + Math.floor(pos[2] / 16)) % 2 === 0) {
|
||||
color = [0.85 * color[0], 0.85 * color[1], 0.85 * color[2]]
|
||||
}
|
||||
}
|
||||
|
||||
const data = img.data
|
||||
img.data.fill(255)
|
||||
|
||||
for (const [pos, feature] of ctx.placements) {
|
||||
if (pos[0] < 0 || pos[1] < 0 || pos[2] < 0 || pos[0] >= options.size[0] || pos[1] >= options.size[1] || pos[2] >= options.size[2]) continue
|
||||
const i = (pos[2] * (img.width * 4)) + (pos[0] * 4)
|
||||
const color = feature < featureColors.length ? featureColors[feature] : stringToColor(ctx.features[feature])
|
||||
data[i] = clamp(color[0], 50, 205)
|
||||
data[i + 1] = clamp(color[1], 50, 205)
|
||||
data[i + 2] = clamp(color[2], 50, 205)
|
||||
data[i + 3] = 255
|
||||
}
|
||||
|
||||
for (let x = 0; x < options.size[0]; x += 1) {
|
||||
for (let y = 0; y < options.size[2]; y += 1) {
|
||||
if ((Math.floor(x / 16) + Math.floor(y / 16)) % 2 === 0) continue
|
||||
const i = (y * (img.width * 4)) + (x * 4)
|
||||
for (let j = 0; j < 3; j += 1) {
|
||||
data[i + j] = 0.85 * data[i + j]
|
||||
}
|
||||
}
|
||||
}
|
||||
return { pos, feature, color }
|
||||
})
|
||||
}
|
||||
|
||||
function normalize(id: string) {
|
||||
@@ -94,7 +62,7 @@ function decorateY(pos: BlockPos, y: number): BlockPos[] {
|
||||
return [[ pos[0], y, pos[2] ]]
|
||||
}
|
||||
|
||||
function sampleInt(value: any, ctx: PlacementContext): number {
|
||||
export function sampleInt(value: any, ctx: PlacementContext): number {
|
||||
if (typeof value === 'number') {
|
||||
return value
|
||||
} else if (value.base) {
|
||||
@@ -104,7 +72,7 @@ function sampleInt(value: any, ctx: PlacementContext): number {
|
||||
case 'constant': return value.value
|
||||
case 'uniform': return value.value.min_inclusive + ctx.nextInt(value.value.max_inclusive - value.value.min_inclusive + 1)
|
||||
case 'biased_to_bottom': return value.value.min_inclusive + ctx.nextInt(ctx.nextInt(value.value.max_inclusive - value.value.min_inclusive + 1) + 1)
|
||||
case 'clamped': return clamp(ctx.sampleInt(value.value.source), value.value.min_inclusive, value.value.max_inclusive)
|
||||
case 'clamped': return clamp(sampleInt(value.value.source, ctx), value.value.min_inclusive, value.value.max_inclusive)
|
||||
case 'clamped_normal':
|
||||
const normal = value.value.mean + ctx.nextGaussian() * value.value.deviation
|
||||
return Math.floor(clamp(value.value.min_inclusive, value.value.max_inclusive, normal))
|
||||
@@ -113,7 +81,7 @@ function sampleInt(value: any, ctx: PlacementContext): number {
|
||||
let i = ctx.nextInt(totalWeight)
|
||||
for (const e of value.distribution) {
|
||||
i -= e.weight
|
||||
if (i < 0) return ctx.sampleInt(e.data)
|
||||
if (i < 0) return sampleInt(e.data, ctx)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
@@ -122,11 +90,11 @@ function sampleInt(value: any, ctx: PlacementContext): number {
|
||||
}
|
||||
|
||||
function resolveAnchor(anchor: any, _ctx: PlacementContext): number {
|
||||
if (!isObject(anchor)) throw new Error('Invalid vertical anchor')
|
||||
if (anchor.absolute) return anchor.absolute
|
||||
if (anchor.above_bottom) return anchor.above_bottom
|
||||
if (anchor.below_top) return 256 - anchor.below_top
|
||||
throw new Error('Invalid vertical anchor')
|
||||
if (!isObject(anchor)) return 0
|
||||
if (anchor.absolute !== undefined) return anchor.absolute
|
||||
if (anchor.above_bottom !== undefined) return anchor.above_bottom
|
||||
if (anchor.below_top !== undefined) return 256 - anchor.below_top
|
||||
return 0
|
||||
}
|
||||
|
||||
function sampleHeight(height: any, ctx: PlacementContext): number {
|
||||
@@ -234,7 +202,7 @@ const Decorators: {
|
||||
return ctx.nextFloat() < 1 / (config?.chance ?? 1) ? [pos] : []
|
||||
},
|
||||
count: (config, pos, ctx) => {
|
||||
return new Array(ctx.sampleInt(config?.count ?? 1)).fill(pos)
|
||||
return new Array(sampleInt(config?.count ?? 1, ctx)).fill(pos)
|
||||
},
|
||||
count_extra: (config, pos, ctx) => {
|
||||
let count = config?.count ?? 1
|
||||
@@ -244,7 +212,7 @@ const Decorators: {
|
||||
return new Array(count).fill(pos)
|
||||
},
|
||||
count_multilayer: (config, pos, ctx) => {
|
||||
return new Array(ctx.sampleInt(config?.count ?? 1)).fill(pos)
|
||||
return new Array(sampleInt(config?.count ?? 1, ctx)).fill(pos)
|
||||
.map(p => [
|
||||
p[0] + ctx.nextInt(16),
|
||||
p[1],
|
||||
@@ -288,7 +256,7 @@ const Decorators: {
|
||||
])
|
||||
},
|
||||
fire: (config, pos, ctx) => {
|
||||
const count = 1 + ctx.nextInt(ctx.nextInt(ctx.sampleInt(config?.count)))
|
||||
const count = 1 + ctx.nextInt(ctx.nextInt(sampleInt(config?.count, ctx)))
|
||||
return [...new Array(count)].map(() => [
|
||||
pos[0] + ctx.nextInt(16),
|
||||
ctx.nextInt(128),
|
||||
@@ -296,7 +264,7 @@ const Decorators: {
|
||||
])
|
||||
},
|
||||
glowstone: (config, pos, ctx) => {
|
||||
const count = ctx.nextInt(1 + ctx.nextInt(ctx.sampleInt(config?.count)))
|
||||
const count = ctx.nextInt(1 + ctx.nextInt(sampleInt(config?.count, ctx)))
|
||||
return [...new Array(count)].map(() => [
|
||||
pos[0] + ctx.nextInt(16),
|
||||
ctx.nextInt(128),
|
||||
@@ -404,10 +372,10 @@ const PlacementModifiers: {
|
||||
[key: string]: (config: any, pos: BlockPos, ctx: PlacementContext) => BlockPos[],
|
||||
} = {
|
||||
count: ({ count }, pos, ctx) => {
|
||||
return new Array(ctx.sampleInt(count ?? 1)).fill(pos)
|
||||
return new Array(sampleInt(count ?? 1, ctx)).fill(pos)
|
||||
},
|
||||
count_on_every_layer: ({ count }, pos, ctx) => {
|
||||
return new Array(ctx.sampleInt(count ?? 1)).fill(pos)
|
||||
return new Array(sampleInt(count ?? 1, ctx)).fill(pos)
|
||||
.map(p => [
|
||||
p[0] + ctx.nextInt(16),
|
||||
p[1],
|
||||
@@ -444,9 +412,9 @@ const PlacementModifiers: {
|
||||
},
|
||||
random_offset: ({ xz_spread, y_spread }, pos, ctx) => {
|
||||
return [[
|
||||
pos[0] + ctx.sampleInt(xz_spread),
|
||||
pos[1] + ctx.sampleInt(y_spread),
|
||||
pos[2] + ctx.sampleInt(xz_spread),
|
||||
pos[0] + sampleInt(xz_spread, ctx),
|
||||
pos[1] + sampleInt(y_spread, ctx),
|
||||
pos[2] + sampleInt(xz_spread, ctx),
|
||||
]]
|
||||
},
|
||||
rarity_filter: ({ chance }, pos, ctx) => {
|
||||
@@ -1,42 +1,86 @@
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import { BlockPos, ChunkPos, LegacyRandom, PerlinNoise } from 'deepslate'
|
||||
import type { mat3 } from 'gl-matrix'
|
||||
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'
|
||||
import { useLocale } from '../../contexts/index.js'
|
||||
import { useCanvas } from '../../hooks/index.js'
|
||||
import { decorator } from '../../previews/index.js'
|
||||
import { randomSeed } from '../../Utils.js'
|
||||
import { computeIfAbsent, iterateWorld2D, randomSeed } from '../../Utils.js'
|
||||
import { Btn } from '../index.js'
|
||||
import type { PlacedFeature, PlacementContext } from './Decorator.js'
|
||||
import { decorateChunk } from './Decorator.js'
|
||||
import type { PreviewProps } from './index.js'
|
||||
import { InteractiveCanvas2D } from './InteractiveCanvas2D.jsx'
|
||||
|
||||
export const DecoratorPreview = ({ data, version, shown }: PreviewProps) => {
|
||||
const { locale } = useLocale()
|
||||
const [scale, setScale] = useState(4)
|
||||
const [seed, setSeed] = useState(randomSeed())
|
||||
|
||||
const state = JSON.stringify(data)
|
||||
|
||||
const { canvas, redraw } = useCanvas({
|
||||
size() {
|
||||
return [scale * 16, scale * 16]
|
||||
},
|
||||
async draw(img) {
|
||||
decorator(data, img, { seed, version, size: [scale * 16, 128, scale * 16] })
|
||||
},
|
||||
}, [version, state, seed])
|
||||
|
||||
useEffect(() => {
|
||||
if (shown) {
|
||||
redraw()
|
||||
const { context, chunkFeatures } = useMemo(() => {
|
||||
const random = new LegacyRandom(seed)
|
||||
const context: PlacementContext = {
|
||||
placements: [],
|
||||
features: [],
|
||||
random,
|
||||
biomeInfoNoise: new PerlinNoise(random.fork(), 0, [1]),
|
||||
seaLevel: 63,
|
||||
version: version,
|
||||
nextFloat: () => random.nextFloat(),
|
||||
nextInt: (max: number) => random.nextInt(max),
|
||||
nextGaussian: () => Math.sqrt(-2 * Math.log(1 - random.nextFloat())) * Math.cos(2 * Math.PI * random.nextFloat()),
|
||||
}
|
||||
}, [version, state, scale, seed, shown])
|
||||
return {
|
||||
context,
|
||||
chunkFeatures: new Map<string, PlacedFeature[]>(),
|
||||
}
|
||||
}, [state, version, seed])
|
||||
|
||||
const ctx = useRef<CanvasRenderingContext2D>()
|
||||
const imageData = useRef<ImageData>()
|
||||
const [focused, setFocused] = useState<string[]>([])
|
||||
|
||||
const onSetup = useCallback(function onSetup(canvas: HTMLCanvasElement) {
|
||||
const ctx2D = canvas.getContext('2d')
|
||||
if (!ctx2D) return
|
||||
ctx.current = ctx2D
|
||||
}, [])
|
||||
const onResize = useCallback(function onResize(width: number, height: number) {
|
||||
if (!ctx.current) return
|
||||
imageData.current = ctx.current.getImageData(0, 0, width, height)
|
||||
}, [])
|
||||
const onDraw = useCallback(function onDraw(transform: mat3) {
|
||||
if (!ctx.current || !imageData.current || !shown) return
|
||||
|
||||
iterateWorld2D(imageData.current, transform, (x, y) => {
|
||||
const pos = ChunkPos.create(Math.floor(x / 16), Math.floor(-y / 16))
|
||||
const features = computeIfAbsent(chunkFeatures, `${pos[0]} ${pos[1]}`, () => decorateChunk(pos, data, context))
|
||||
return features.find(f => f.pos[0] === x && f.pos[2] == -y) ?? { pos: BlockPos.create(x, 0, -y) }
|
||||
}, (feature) => {
|
||||
if ('color' in feature) {
|
||||
return feature.color
|
||||
}
|
||||
if ((Math.floor(feature.pos[0] / 16) + Math.floor(feature.pos[2] / 16)) % 2 === 0) {
|
||||
return [0.85 * 256, 0.85 * 256, 0.85 * 256]
|
||||
}
|
||||
return [256, 256, 256]
|
||||
})
|
||||
ctx.current.putImageData(imageData.current, 0, 0)
|
||||
}, [context, chunkFeatures, shown])
|
||||
const onHover = useCallback(function onHover(pos: [number, number] | undefined) {
|
||||
if (!pos) {
|
||||
setFocused([])
|
||||
} else {
|
||||
const [x, y] = pos
|
||||
setFocused([`X=${x} Z=${-y}`])
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <>
|
||||
<div class="controls preview-controls">
|
||||
<Btn icon="dash" tooltip={locale('zoom_out')}
|
||||
onClick={() => setScale(Math.min(16, scale + 1))} />
|
||||
<Btn icon="plus" tooltip={locale('zoom_in')}
|
||||
onClick={() => setScale(Math.max(1, scale - 1))} />
|
||||
{focused.map(s => <Btn label={s} class="no-pointer" /> )}
|
||||
<Btn icon="sync" tooltip={locale('generate_new_seed')}
|
||||
onClick={() => setSeed(randomSeed())} />
|
||||
</div>
|
||||
<canvas ref={canvas} width="64" height="64"></canvas>
|
||||
<div class="full-preview">
|
||||
<InteractiveCanvas2D onSetup={onSetup} onResize={onResize} onDraw={onDraw} onHover={onHover} pixelSize={4} startScale={1/8} minScale={1/32} maxScale={1} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as deepslate19 from 'deepslate/worldgen'
|
||||
import type { VersionId } from '../services/index.js'
|
||||
import { checkVersion, fetchAllPresets, fetchPreset } from '../services/index.js'
|
||||
import { BiMap, clamp, computeIfAbsentAsync, deepClone, deepEqual, isObject, square } from '../Utils.js'
|
||||
import type { VersionId } from '../../services/index.js'
|
||||
import { checkVersion, fetchAllPresets, fetchPreset } from '../../services/index.js'
|
||||
import { clamp, computeIfAbsent, computeIfAbsentAsync, deepClone, deepEqual, isObject, square } from '../../Utils.js'
|
||||
|
||||
export type ProjectData = Record<string, Record<string, unknown>>
|
||||
|
||||
@@ -18,10 +18,10 @@ export class Deepslate {
|
||||
private loadingPromise: Promise<void> | undefined
|
||||
private readonly deepslateCache = new Map<VersionId, typeof deepslate19>()
|
||||
private readonly Z = 0
|
||||
private readonly DEBUG = false
|
||||
|
||||
private cacheState: unknown
|
||||
private settingsCache: NoiseSettings | undefined
|
||||
private routerCache: NoiseRouter | undefined
|
||||
private generatorCache: ChunkGenerator | undefined
|
||||
private biomeSourceCache: BiomeSource | undefined
|
||||
private randomStateCache: deepslate19.RandomState | undefined
|
||||
@@ -106,8 +106,11 @@ export class Deepslate {
|
||||
this.generatorCache = chunkGenerator
|
||||
if (this.isVersion('1.19')) {
|
||||
this.randomStateCache = new this.d.RandomState(noiseSettings, seed)
|
||||
this.routerCache = this.randomStateCache.router
|
||||
} else {
|
||||
this.randomStateCache = undefined
|
||||
|
||||
this.routerCache = undefined
|
||||
}
|
||||
this.biomeSourceCache = {
|
||||
getBiome: (x, y, z) => biomeSource.getBiome(x, y, z, undefined!),
|
||||
@@ -129,7 +132,8 @@ export class Deepslate {
|
||||
biomeState = { type: biomeState.type, biomes }
|
||||
}
|
||||
if (this.isVersion('1.19')) {
|
||||
return this.d.BiomeSource.fromJson(biomeState)
|
||||
const bs = this.d.BiomeSource.fromJson(biomeState)
|
||||
return bs
|
||||
} else {
|
||||
const root = isObject(biomeState) ? biomeState : {}
|
||||
const type = typeof root.type === 'string' ? root.type.replace(/^minecraft:/, '') : undefined
|
||||
@@ -242,61 +246,20 @@ export class Deepslate {
|
||||
})
|
||||
}
|
||||
|
||||
public fillBiomes(minX: number, maxX: number, minZ: number, maxZ: number, step = 1, y = 64) {
|
||||
if (!this.generatorCache || !this.settingsCache) {
|
||||
throw new Error('Tried to fill biomes before generator is loaded')
|
||||
}
|
||||
const quartY = (y - this.settingsCache.minY) >> 2
|
||||
const minQuartX = minX >> 2
|
||||
const maxQuartX = maxX >> 2
|
||||
const minQuartZ = minZ >> 2
|
||||
const maxQuartZ = maxZ >> 2
|
||||
const countX = Math.floor((maxQuartX - minQuartX) / step)
|
||||
const countZ = Math.floor((maxQuartZ - minQuartZ) / step)
|
||||
|
||||
const biomeIds = new BiMap<string, number>()
|
||||
const data = new Int8Array(countX * countZ)
|
||||
let biomeId = 0
|
||||
let i = 0
|
||||
|
||||
for (let x = minQuartX; x < maxQuartX; x += step) {
|
||||
for (let z = minQuartZ; z < maxQuartZ; z += step) {
|
||||
const posKey = `${x}:${quartY}:${z}`
|
||||
let biome = this.biomeCache.get(posKey)
|
||||
if (!biome) {
|
||||
if (this.DEBUG) {
|
||||
biome = this.computeDebugBiome(x, z)
|
||||
} else if (this.isVersion('1.19')) {
|
||||
if (!this.randomStateCache) {
|
||||
throw new Error('Tried to compute biomes before random state is loaded')
|
||||
}
|
||||
biome = this.generatorCache.computeBiome(this.randomStateCache, x, quartY, z).toString()
|
||||
} else {
|
||||
if(!this.biomeSourceCache) {
|
||||
throw new Error('Tried to compute biomes before biome source is loaded')
|
||||
}
|
||||
biome = this.biomeSourceCache.getBiome(x, quartY, z).toString()
|
||||
}
|
||||
this.biomeCache.set(posKey, biome)
|
||||
public getBiome(x: number, y: number, z: number) {
|
||||
return computeIfAbsent(this.biomeCache, `${x}:${y}:${z}`, () => {
|
||||
if (this.isVersion('1.19')) {
|
||||
if (!this.randomStateCache || !this.generatorCache) {
|
||||
throw new Error('Tried to compute biomes before random state is loaded')
|
||||
}
|
||||
data[i++] = biomeIds.computeIfAbsent(biome, () => biomeId++)
|
||||
return this.generatorCache.computeBiome(this.randomStateCache, x, y, z).toString()
|
||||
} else {
|
||||
if(!this.biomeSourceCache) {
|
||||
throw new Error('Tried to compute biomes before biome source is loaded')
|
||||
}
|
||||
return this.biomeSourceCache.getBiome(x, y, z).toString()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
palette: biomeIds.backward,
|
||||
data,
|
||||
width: countX,
|
||||
height: countZ,
|
||||
}
|
||||
}
|
||||
|
||||
private computeDebugBiome(x: number, z: number) {
|
||||
if (x > 0) {
|
||||
return z > 0 ? 'minecraft:plains' : 'minecraft:forest'
|
||||
} else {
|
||||
return z > 0 ? 'minecraft:badlands' : 'minecraft:desert'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public loadDensityFunction(state: unknown, minY: number, height: number, seed: bigint) {
|
||||
@@ -340,6 +303,13 @@ export class Deepslate {
|
||||
return this.settingsCache
|
||||
}
|
||||
|
||||
public getNoiseRouter(): NoiseRouter {
|
||||
if (!this.routerCache) {
|
||||
throw new Error('Tried to access noise router when they are not loaded')
|
||||
}
|
||||
return this.routerCache
|
||||
}
|
||||
|
||||
public getBlockState(x: number, y: number) {
|
||||
x = Math.floor(x)
|
||||
y = Math.floor(y)
|
||||
@@ -362,6 +332,15 @@ interface NoiseSettings {
|
||||
height: number
|
||||
}
|
||||
|
||||
interface NoiseRouter {
|
||||
temperature: deepslate19.DensityFunction
|
||||
vegetation: deepslate19.DensityFunction
|
||||
continents: deepslate19.DensityFunction
|
||||
erosion: deepslate19.DensityFunction
|
||||
ridges: deepslate19.DensityFunction
|
||||
depth: deepslate19.DensityFunction
|
||||
}
|
||||
|
||||
interface ChunkGenerator {
|
||||
fill(randomState: deepslate19.RandomState, chunk: Chunk, onlyFirstZ?: boolean): void
|
||||
buildSurface(randomState: deepslate19.RandomState, chunk: Chunk, biome: string): void
|
||||
@@ -1,77 +1,132 @@
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { useLocale, useProject } from '../../contexts/index.js'
|
||||
import { useCanvas } from '../../hooks/index.js'
|
||||
import type { ColormapType } from '../../previews/Colormap.js'
|
||||
import { densityFunction, densityPoint } from '../../previews/index.js'
|
||||
import { DataModel } from '@mcschema/core'
|
||||
import type { Voxel } from 'deepslate/render'
|
||||
import { clampedMap, VoxelRenderer } from 'deepslate/render'
|
||||
import type { mat3, mat4 } from 'gl-matrix'
|
||||
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { getProjectData, useLocale, useProject, useVersion } from '../../contexts/index.js'
|
||||
import { useAsync } from '../../hooks/useAsync.js'
|
||||
import { Store } from '../../Store.js'
|
||||
import { randomSeed } from '../../Utils.js'
|
||||
import { Btn, BtnMenu } from '../index.js'
|
||||
import { iterateWorld2D, randomSeed } from '../../Utils.js'
|
||||
import { Btn, BtnMenu, NumberInput } 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'
|
||||
import { InteractiveCanvas3D } from './InteractiveCanvas3D.jsx'
|
||||
|
||||
export const DensityFunctionPreview = ({ data, shown, version }: PreviewProps) => {
|
||||
export const DensityFunctionPreview = ({ data, shown }: PreviewProps) => {
|
||||
const { locale } = useLocale()
|
||||
const { project } = useProject()
|
||||
const { version } = useVersion()
|
||||
const [voxelMode, setVoxelMode] = useState(false)
|
||||
const [seed, setSeed] = useState(randomSeed())
|
||||
const [minY] = useState(0)
|
||||
const [height] = useState(256)
|
||||
const [autoScroll, setAutoScroll] = useState(false)
|
||||
const serializedData = JSON.stringify(data)
|
||||
|
||||
const { value: df } = useAsync(async () => {
|
||||
await DEEPSLATE.loadVersion(version, getProjectData(project))
|
||||
const df = DEEPSLATE.loadDensityFunction(DataModel.unwrapLists(data), minY, height, seed)
|
||||
return df
|
||||
}, [version, project, minY, height, seed, serializedData])
|
||||
|
||||
// === 2D ===
|
||||
const imageData = useRef<ImageData>()
|
||||
const ctx = useRef<CanvasRenderingContext2D>()
|
||||
const [focused, setFocused] = useState<string[]>([])
|
||||
const [colormap, setColormap] = useState<ColormapType>(Store.getColormap() ?? 'viridis')
|
||||
const offset = useRef(0)
|
||||
const scrollInterval = useRef<number | undefined>(undefined)
|
||||
const state = JSON.stringify([data])
|
||||
|
||||
const size = 256
|
||||
const { canvas, redraw } = useCanvas({
|
||||
size() {
|
||||
return [size, size]
|
||||
},
|
||||
async draw(img) {
|
||||
const options = { offset: offset.current, width: img.width, seed, version, project, minY, height, colormap }
|
||||
await densityFunction(data, img, options)
|
||||
},
|
||||
async onDrag(dx) {
|
||||
offset.current += dx * size
|
||||
redraw()
|
||||
},
|
||||
async onHover(x, y) {
|
||||
const worldX = Math.floor(x * size)
|
||||
const worldY = Math.floor(y * (height - minY))
|
||||
const options = { offset: offset.current, width: size, seed, version, project, minY, height, colormap }
|
||||
const density = await densityPoint(data, worldX, worldY, options)
|
||||
setFocused([density.toPrecision(3), `X=${Math.floor(worldX - offset.current)} Y=${(height - minY) - worldY}`])
|
||||
},
|
||||
onLeave() {
|
||||
const onSetup2D = useCallback((canvas: HTMLCanvasElement) => {
|
||||
const ctx2D = canvas.getContext('2d')
|
||||
if (!ctx2D) return
|
||||
ctx.current = ctx2D
|
||||
}, [voxelMode])
|
||||
const onResize2D = useCallback((width: number, height: number) => {
|
||||
if (!ctx.current) return
|
||||
imageData.current = ctx.current.getImageData(0, 0, width, height)
|
||||
}, [voxelMode])
|
||||
const onDraw2D = useCallback((transform: mat3) => {
|
||||
if (!ctx.current || !imageData.current || !df) return
|
||||
|
||||
const colormapFn = getColormap(colormap)
|
||||
const colorPicker = (t: number) => colormapFn(t <= 0.5 ? t - 0.08 : t + 0.08)
|
||||
let limit = 0.01
|
||||
iterateWorld2D(imageData.current, transform, (x, y) => {
|
||||
const density = df.compute({ x, y, z: 0 })
|
||||
limit = Math.max(limit, Math.min(1, Math.abs(density)))
|
||||
return density
|
||||
}, (density) => {
|
||||
const color = colorPicker(clampedMap(density, -limit, limit, 1, 0))
|
||||
return [color[0] * 256, color[1] * 256, color[2] * 256]
|
||||
})
|
||||
ctx.current.putImageData(imageData.current, 0, 0)
|
||||
}, [voxelMode, df, colormap])
|
||||
const onHover2D = useCallback((pos: [number, number] | undefined) => {
|
||||
if (!pos || !df) {
|
||||
setFocused([])
|
||||
},
|
||||
}, [version, state, seed, minY, height, colormap, project])
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollInterval.current) {
|
||||
clearInterval(scrollInterval.current)
|
||||
} else {
|
||||
const [x, y] = pos
|
||||
const output = df.compute({ x: x, y: -y, z: 0 })
|
||||
setFocused([output.toPrecision(3), `X=${x} Y=${-y}`])
|
||||
}
|
||||
if (shown) {
|
||||
redraw()
|
||||
if (autoScroll) {
|
||||
scrollInterval.current = setInterval(() => {
|
||||
offset.current -= 8
|
||||
redraw()
|
||||
}, 100) as any
|
||||
}, [voxelMode, df])
|
||||
|
||||
// === 3D ===
|
||||
const renderer = useRef<VoxelRenderer | undefined>(undefined)
|
||||
const [state, setState] = useState(0)
|
||||
const [cutoff, setCutoff] = useState(0)
|
||||
|
||||
const onSetup3D = useCallback((canvas: HTMLCanvasElement) => {
|
||||
const gl = canvas.getContext('webgl')
|
||||
if (!gl) return
|
||||
renderer.current = new VoxelRenderer(gl)
|
||||
}, [voxelMode])
|
||||
const onResize3D = useCallback((width: number, height: number) => {
|
||||
renderer.current?.setViewport(0, 0, width, height)
|
||||
}, [voxelMode])
|
||||
const onDraw3D = useCallback((transform: mat4) => {
|
||||
renderer.current?.draw(transform)
|
||||
}, [voxelMode])
|
||||
useEffect(() => {
|
||||
if (!renderer.current || !shown || !df || !voxelMode) return
|
||||
const voxels: Voxel[] = []
|
||||
const maxY = minY + height
|
||||
for (let x = 0; x < 16; x += 1) {
|
||||
for (let y = minY; y < maxY; y += 1) {
|
||||
for (let z = 0; z < 16; z += 1) {
|
||||
const density = df.compute({ x, y, z })
|
||||
if (density > cutoff) {
|
||||
voxels.push({ x, y, z, color: [200, 200, 200] })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [version, state, seed, minY, height, colormap, project, shown, autoScroll])
|
||||
renderer.current.setVoxels(voxels)
|
||||
setState(state => state + 1)
|
||||
}, [voxelMode, df, cutoff])
|
||||
|
||||
return <>
|
||||
<div class="controls preview-controls">
|
||||
{focused.map(s => <Btn label={s} class="no-pointer" /> )}
|
||||
<ColormapSelector value={colormap} onChange={setColormap} />
|
||||
<BtnMenu icon="gear" tooltip={locale('terrain_settings')}>
|
||||
<Btn icon={autoScroll ? 'square_fill' : 'square'} label={locale('preview.auto_scroll')} onClick={() => setAutoScroll(!autoScroll)} />
|
||||
</BtnMenu>
|
||||
{voxelMode ? <>
|
||||
<BtnMenu icon="gear">
|
||||
<div class="btn btn-input" onClick={e => e.stopPropagation()}>
|
||||
<span>{locale('cutoff')}</span>
|
||||
<NumberInput value={cutoff} onChange={setCutoff} />
|
||||
</div>
|
||||
</BtnMenu>
|
||||
</> : <>
|
||||
<ColormapSelector value={colormap} onChange={setColormap} />
|
||||
</>}
|
||||
<Btn label={voxelMode ? locale('3d') : locale('2d')} onClick={() => setVoxelMode(!voxelMode)} />
|
||||
<Btn icon="sync" tooltip={locale('generate_new_seed')}
|
||||
onClick={() => setSeed(randomSeed())} />
|
||||
</div>
|
||||
<canvas ref={canvas} width={size} height={size}></canvas>
|
||||
<div class="full-preview">{voxelMode
|
||||
? <InteractiveCanvas3D onSetup={onSetup3D} onDraw={onDraw3D} onResize={onResize3D} state={state} startDistance={100} startPosition={[8, 120, 8]} />
|
||||
: <InteractiveCanvas2D onSetup={onSetup2D} onDraw={onDraw2D} onHover={onHover2D} onResize={onResize2D} state={state} pixelSize={4} />
|
||||
}</div>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { mat3, vec2 } from 'gl-matrix'
|
||||
import { useCallback, useEffect, useRef } from 'preact/hooks'
|
||||
|
||||
interface Props {
|
||||
onSetup: (canvas: HTMLCanvasElement) => void,
|
||||
onDraw: (transform: mat3) => void,
|
||||
onHover?: (offset: [number, number] | undefined) => void,
|
||||
onResize: (width: number, height: number) => void,
|
||||
state?: unknown,
|
||||
pixelSize?: number,
|
||||
startPosition?: [number, number],
|
||||
startScale?: number,
|
||||
minScale?: number,
|
||||
maxScale?: number,
|
||||
}
|
||||
export function InteractiveCanvas2D({ onSetup, onDraw, onHover, onResize, state, pixelSize = 1, startPosition, startScale, minScale = 1/16, maxScale = 16 }: Props) {
|
||||
const canvas = useRef<HTMLCanvasElement>(null)
|
||||
const dragStart = useRef<[number, number] | undefined>()
|
||||
const dragButton = useRef<number | undefined>()
|
||||
const centerPos = useRef<[number, number]>(startPosition ? [-startPosition[0], -startPosition[1]] : [0, 0])
|
||||
const viewScale = useRef(startScale ?? 1)
|
||||
const frameRequest = useRef<number>()
|
||||
|
||||
// Transforms screen coordinates to world coordinates
|
||||
const transform = useCallback(() => {
|
||||
const mat = mat3.create()
|
||||
if (!canvas.current) return mat
|
||||
const halfWidth = Math.floor(canvas.current.clientWidth / 2) / pixelSize
|
||||
const halfHeight = Math.floor(canvas.current.clientHeight / 2) / pixelSize
|
||||
const scale = Math.pow(2, Math.floor(Math.log(viewScale.current * pixelSize) /Math.log(2)))
|
||||
const offsetX = Math.floor(centerPos.current[0])
|
||||
const offsetY = Math.floor(centerPos.current[1])
|
||||
mat3.translate(mat, mat, [offsetX, offsetY])
|
||||
mat3.scale(mat, mat, [scale, scale])
|
||||
mat3.translate(mat, mat, [-halfWidth, -halfHeight])
|
||||
return mat
|
||||
}, [pixelSize])
|
||||
|
||||
const redraw = useRef<Function>(() => {})
|
||||
useEffect(() => {
|
||||
redraw.current = function requestRedraw() {
|
||||
if (frameRequest.current !== undefined) {
|
||||
cancelAnimationFrame(frameRequest.current)
|
||||
}
|
||||
frameRequest.current = requestAnimationFrame(function redraw() {
|
||||
onDraw(transform())
|
||||
})
|
||||
}
|
||||
}, [onDraw, transform])
|
||||
|
||||
useEffect(function changeDetected() {
|
||||
redraw.current()
|
||||
}, [state, onDraw, transform])
|
||||
|
||||
useEffect(function setupListeners() {
|
||||
if (!canvas.current) return
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
if (!canvas.current) return
|
||||
dragStart.current = [e.offsetX, e.offsetY]
|
||||
dragButton.current = e.button
|
||||
}
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
if (!canvas.current) return
|
||||
if (dragStart.current === undefined) {
|
||||
const pos = vec2.fromValues(e.offsetX / pixelSize, e.offsetY / pixelSize)
|
||||
vec2.transformMat3(pos, pos, transform())
|
||||
onHover?.([Math.floor(pos[0]), Math.floor(pos[1])])
|
||||
} else {
|
||||
const dragEnd: [number, number] = [e.offsetX, e.offsetY]
|
||||
const dx = (dragEnd[0] - dragStart.current[0]) * (viewScale.current)
|
||||
const dy = (dragEnd[1] - dragStart.current[1]) * (viewScale.current)
|
||||
centerPos.current = [centerPos.current[0] - dx, centerPos.current[1] - dy]
|
||||
dragStart.current = dragEnd
|
||||
redraw.current()
|
||||
}
|
||||
}
|
||||
function onMouseUp () {
|
||||
dragStart.current = undefined
|
||||
}
|
||||
function onMouseLeave () {
|
||||
onHover?.(undefined)
|
||||
}
|
||||
function onWheel (e: WheelEvent) {
|
||||
const newScale = Math.pow(2, Math.log(viewScale.current) / Math.log(2) + e.deltaY / 200)
|
||||
if (newScale > minScale && newScale < maxScale) {
|
||||
viewScale.current = newScale
|
||||
redraw.current()
|
||||
}
|
||||
e.preventDefault()
|
||||
}
|
||||
function onContextMenu(evt: MouseEvent) {
|
||||
evt.preventDefault()
|
||||
}
|
||||
function resizeHandler() {
|
||||
if (!canvas.current) return
|
||||
const width = Math.floor(canvas.current.clientWidth / pixelSize)
|
||||
const height = Math.floor(canvas.current.clientHeight / pixelSize)
|
||||
canvas.current.width = width
|
||||
canvas.current.height = height
|
||||
onResize?.(width, height)
|
||||
redraw.current()
|
||||
}
|
||||
|
||||
onSetup(canvas.current)
|
||||
resizeHandler()
|
||||
|
||||
canvas.current.addEventListener('mousedown', onMouseDown)
|
||||
canvas.current.addEventListener('mousemove', onMouseMove)
|
||||
canvas.current.addEventListener('mouseleave', onMouseLeave)
|
||||
document.body.addEventListener('mouseup', onMouseUp)
|
||||
canvas.current.addEventListener('wheel', onWheel)
|
||||
canvas.current.addEventListener('contextmenu', onContextMenu)
|
||||
window.addEventListener('resize', resizeHandler)
|
||||
|
||||
return () => {
|
||||
canvas.current?.removeEventListener('mousedown', onMouseDown)
|
||||
canvas.current?.removeEventListener('mousemove', onMouseMove)
|
||||
canvas.current?.removeEventListener('mouseleave', onMouseLeave)
|
||||
document.body.removeEventListener('mouseup', onMouseUp)
|
||||
canvas.current?.removeEventListener('wheel', onWheel)
|
||||
canvas.current?.removeEventListener('contextmenu', onContextMenu)
|
||||
window.removeEventListener('resize', resizeHandler)
|
||||
}
|
||||
}, [onSetup, onResize, onHover, transform, pixelSize, minScale, maxScale])
|
||||
|
||||
return <canvas ref={canvas}></canvas>
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { mat4, vec3 } from 'gl-matrix'
|
||||
import { useEffect, useRef } from 'preact/hooks'
|
||||
|
||||
interface Props {
|
||||
onSetup: (canvas: HTMLCanvasElement) => void,
|
||||
onDraw: (transform: mat4) => void,
|
||||
onResize: (width: number, height: number) => void,
|
||||
state?: unknown,
|
||||
startPosition?: [number, number, number],
|
||||
startDistance?: number,
|
||||
startYRotation?: number,
|
||||
startXRotation?: number,
|
||||
}
|
||||
export function InteractiveCanvas3D({ onSetup, onDraw, onResize, state, startPosition, startDistance, startYRotation, startXRotation }: Props) {
|
||||
const canvas = useRef<HTMLCanvasElement>(null)
|
||||
const dragStart = useRef<[number, number] | undefined>()
|
||||
const dragButton = useRef<number | undefined>()
|
||||
const centerPos = useRef<[number, number, number]>(startPosition ? [-startPosition[0], -startPosition[1], -startPosition[2]] : [0, 0, 0])
|
||||
const viewDist = useRef(startDistance ?? 4)
|
||||
const yRotation = useRef(startYRotation ?? 0.8)
|
||||
const xRotation = useRef(startXRotation ?? 0.5)
|
||||
const frameRequest = useRef<number>()
|
||||
|
||||
const redraw = useRef<Function>(() => {})
|
||||
useEffect(() => {
|
||||
redraw.current = function requestRedraw() {
|
||||
if (frameRequest.current !== undefined) {
|
||||
cancelAnimationFrame(frameRequest.current)
|
||||
}
|
||||
frameRequest.current = requestAnimationFrame(function redraw() {
|
||||
yRotation.current = yRotation.current % (Math.PI * 2)
|
||||
xRotation.current = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, xRotation.current))
|
||||
viewDist.current = Math.max(1, viewDist.current)
|
||||
|
||||
const transform = mat4.create()
|
||||
mat4.translate(transform, transform, [0, 0, -viewDist.current])
|
||||
mat4.rotateX(transform, transform, xRotation.current)
|
||||
mat4.rotateY(transform, transform, yRotation.current)
|
||||
mat4.translate(transform, transform, centerPos.current)
|
||||
onDraw(transform)
|
||||
})
|
||||
}
|
||||
}, [onDraw])
|
||||
|
||||
useEffect(function changeDetected() {
|
||||
redraw.current()
|
||||
}, [state, onDraw])
|
||||
|
||||
useEffect(function setupListeners() {
|
||||
if (!canvas.current) return
|
||||
function onMouseDown(e: MouseEvent) {
|
||||
dragStart.current = [e.offsetX, e.offsetY]
|
||||
dragButton.current = e.button
|
||||
}
|
||||
function onMouseMove(e: MouseEvent) {
|
||||
if (dragStart.current === undefined) {
|
||||
return
|
||||
}
|
||||
const dx = e.offsetX - dragStart.current[0]
|
||||
const dy = e.offsetY - dragStart.current[1]
|
||||
if (dx === 0 && dy === 0) {
|
||||
return
|
||||
}
|
||||
if (dragButton.current === 0) {
|
||||
yRotation.current += dx / 100
|
||||
xRotation.current += dy / 100
|
||||
redraw.current()
|
||||
} else if (dragButton.current === 1 || dragButton.current === 2) {
|
||||
const pos = vec3.fromValues(centerPos.current[0], centerPos.current[1], centerPos.current[2])
|
||||
vec3.rotateY(pos, pos, [0, 0, 0], yRotation.current)
|
||||
vec3.rotateX(pos, pos, [0, 0, 0], xRotation.current)
|
||||
const d = vec3.fromValues(dx / 100, -dy / 100, 0)
|
||||
vec3.scale(d, d, 0.25 * viewDist.current)
|
||||
vec3.add(pos, pos, d)
|
||||
vec3.rotateX(pos, pos, [0, 0, 0], -xRotation.current)
|
||||
vec3.rotateY(pos, pos, [0, 0, 0], -yRotation.current)
|
||||
centerPos.current = [pos[0], pos[1], pos[2]]
|
||||
redraw.current()
|
||||
}
|
||||
dragStart.current = [e.offsetX, e.offsetY]
|
||||
}
|
||||
function onMouseUp() {
|
||||
dragStart.current = undefined
|
||||
}
|
||||
function onWheel(evt: WheelEvent) {
|
||||
viewDist.current = Math.max(1, viewDist.current + evt.deltaY / 100)
|
||||
redraw.current()
|
||||
evt.preventDefault()
|
||||
}
|
||||
function onContextMenu(evt: MouseEvent) {
|
||||
evt.preventDefault()
|
||||
}
|
||||
function resizeHandler() {
|
||||
if (!canvas.current) return
|
||||
const width = canvas.current.clientWidth
|
||||
const height = canvas.current.clientHeight
|
||||
canvas.current.width = width
|
||||
canvas.current.height = height
|
||||
onResize?.(width, height)
|
||||
redraw.current()
|
||||
}
|
||||
|
||||
console.warn('Uhhh...', canvas.current)
|
||||
onSetup(canvas.current)
|
||||
resizeHandler()
|
||||
|
||||
canvas.current.addEventListener('mousedown', onMouseDown)
|
||||
canvas.current.addEventListener('mousemove', onMouseMove)
|
||||
document.body.addEventListener('mouseup', onMouseUp)
|
||||
canvas.current.addEventListener('wheel', onWheel)
|
||||
canvas.current.addEventListener('contextmenu', onContextMenu)
|
||||
window.addEventListener('resize', resizeHandler)
|
||||
|
||||
return () => {
|
||||
canvas.current?.removeEventListener('mousedown', onMouseDown)
|
||||
canvas.current?.removeEventListener('mousemove', onMouseMove)
|
||||
document.body.removeEventListener('mouseup', onMouseUp)
|
||||
canvas.current?.removeEventListener('wheel', onWheel)
|
||||
canvas.current?.removeEventListener('contextmenu', onContextMenu)
|
||||
window.removeEventListener('resize', resizeHandler)
|
||||
}
|
||||
}, [onSetup, onResize])
|
||||
|
||||
return <canvas ref={canvas}></canvas>
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Random } from 'deepslate/core'
|
||||
import { Enchantment, Identifier, ItemStack, LegacyRandom } from 'deepslate/core'
|
||||
import { NbtCompound, NbtInt, NbtList, NbtShort, NbtString, NbtTag, NbtType } from 'deepslate/nbt'
|
||||
import type { VersionId } from '../services/Schemas.js'
|
||||
import { clamp, getWeightedRandom, isObject } from '../Utils.js'
|
||||
import type { VersionId } from '../../services/Schemas.js'
|
||||
import { clamp, getWeightedRandom, isObject } from '../../Utils.js'
|
||||
|
||||
export interface SlottedItem {
|
||||
slot: number,
|
||||
@@ -1,12 +1,12 @@
|
||||
import { DataModel } from '@mcschema/core'
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { useLocale, useVersion } from '../../contexts/index.js'
|
||||
import type { SlottedItem } from '../../previews/LootTable.js'
|
||||
import { generateLootTable } from '../../previews/LootTable.js'
|
||||
import { clamp, randomSeed } from '../../Utils.js'
|
||||
import { Btn, BtnMenu, NumberInput } from '../index.js'
|
||||
import { ItemDisplay } from '../ItemDisplay.jsx'
|
||||
import type { PreviewProps } from './index.js'
|
||||
import type { SlottedItem } from './LootTable.js'
|
||||
import { generateLootTable } from './LootTable.js'
|
||||
|
||||
export const LootTablePreview = ({ data }: PreviewProps) => {
|
||||
const { locale } = useLocale()
|
||||
|
||||
@@ -1,76 +1,73 @@
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { DataModel } from '@mcschema/core'
|
||||
import { clampedMap, NoiseParameters, NormalNoise, XoroshiroRandom } from 'deepslate'
|
||||
import type { mat3 } from 'gl-matrix'
|
||||
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'
|
||||
import { useLocale } from '../../contexts/index.js'
|
||||
import { useCanvas } from '../../hooks/index.js'
|
||||
import type { ColormapType } from '../../previews/Colormap.js'
|
||||
import { normalNoise, normalNoisePoint } from '../../previews/index.js'
|
||||
import { Store } from '../../Store.js'
|
||||
import { randomSeed } from '../../Utils.js'
|
||||
import { iterateWorld2D, randomSeed } from '../../Utils.js'
|
||||
import { Btn } from '../index.js'
|
||||
import type { ColormapType } from './Colormap.js'
|
||||
import { getColormap } from './Colormap.js'
|
||||
import { ColormapSelector } from './ColormapSelector.jsx'
|
||||
import type { PreviewProps } from './index.js'
|
||||
import { InteractiveCanvas2D } from './InteractiveCanvas2D.jsx'
|
||||
|
||||
export const NoisePreview = ({ data, shown, version }: PreviewProps) => {
|
||||
export const NoisePreview = ({ data, shown }: PreviewProps) => {
|
||||
const { locale } = useLocale()
|
||||
const [seed, setSeed] = useState(randomSeed())
|
||||
const [scale, setScale] = useState(2)
|
||||
const state = JSON.stringify(data)
|
||||
|
||||
const noise = useMemo(() => {
|
||||
const random = XoroshiroRandom.create(seed)
|
||||
const params = NoiseParameters.fromJson(DataModel.unwrapLists(data))
|
||||
return new NormalNoise(random, params)
|
||||
}, [state, seed])
|
||||
|
||||
const imageData = useRef<ImageData>()
|
||||
const ctx = useRef<CanvasRenderingContext2D>()
|
||||
const [focused, setFocused] = useState<string[]>([])
|
||||
const [colormap, setColormap] = useState<ColormapType>(Store.getColormap() ?? 'viridis')
|
||||
const offset = useRef<[number, number]>([0, 0])
|
||||
const state = JSON.stringify([data])
|
||||
|
||||
const { canvas, redraw } = useCanvas({
|
||||
size() {
|
||||
return [256, 256]
|
||||
},
|
||||
async draw(img) {
|
||||
const options = { offset: offset.current, scale, seed, version, colormap }
|
||||
normalNoise(data, img, options)
|
||||
},
|
||||
async onDrag(dx, dy) {
|
||||
offset.current[0] = offset.current[0] + dx * 256
|
||||
offset.current[1] = offset.current[1] + dy * 256
|
||||
redraw()
|
||||
},
|
||||
onHover(x, y) {
|
||||
const x2 = Math.floor(x * 256)
|
||||
const y2 = Math.floor(y * 256)
|
||||
const options = { offset: offset.current, scale, seed, version, colormap }
|
||||
const value = normalNoisePoint(data, x2, y2, options)
|
||||
|
||||
const ox = -options.offset[0] - 100
|
||||
const oy = -options.offset[1] - 100
|
||||
const xx = (x2 + ox) * options.scale
|
||||
const yy = (y2 + oy) * options.scale
|
||||
setFocused([value.toPrecision(3), `X=${Math.floor(xx)} Y=${Math.floor(yy)}`])
|
||||
},
|
||||
onLeave() {
|
||||
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
|
||||
|
||||
const colorPicker = getColormap(colormap)
|
||||
iterateWorld2D(imageData.current, transform, (x, y) => {
|
||||
return noise.sample(x, y, 0)
|
||||
}, (value) => {
|
||||
const color = colorPicker(clampedMap(value, -1, 1, 1, 0))
|
||||
return [color[0] * 256, color[1] * 256, color[2] * 256]
|
||||
})
|
||||
ctx.current.putImageData(imageData.current, 0, 0)
|
||||
}, [noise, colormap, shown])
|
||||
const onHover = useCallback((pos: [number, number] | undefined) => {
|
||||
if (!pos) {
|
||||
setFocused([])
|
||||
},
|
||||
}, [version, state, scale, seed, colormap])
|
||||
|
||||
useEffect(() => {
|
||||
if (shown) {
|
||||
redraw()
|
||||
} else {
|
||||
const [x, y] = pos
|
||||
const output = noise.sample(x, -y, 0)
|
||||
setFocused([output.toPrecision(3), `X=${x} Y=${-y}`])
|
||||
}
|
||||
}, [version, state, scale, seed, colormap, shown])
|
||||
|
||||
const changeScale = (newScale: number) => {
|
||||
offset.current[0] = offset.current[0] * scale / newScale
|
||||
offset.current[1] = offset.current[1] * scale / newScale
|
||||
setScale(newScale)
|
||||
}
|
||||
}, [noise])
|
||||
|
||||
return <>
|
||||
<div class="controls preview-controls">
|
||||
{focused.map(s => <Btn label={s} class="no-pointer" /> )}
|
||||
<ColormapSelector value={colormap} onChange={setColormap} />
|
||||
<Btn icon="dash" tooltip={locale('zoom_out')}
|
||||
onClick={() => changeScale(scale * 1.5)} />
|
||||
<Btn icon="plus" tooltip={locale('zoom_in')}
|
||||
onClick={() => changeScale(scale / 1.5)} />
|
||||
<Btn icon="sync" tooltip={locale('generate_new_seed')}
|
||||
onClick={() => setSeed(randomSeed())} />
|
||||
</div>
|
||||
<canvas ref={canvas} width="256" height="256"></canvas>
|
||||
<div class="full-preview">
|
||||
<InteractiveCanvas2D onSetup={onSetup} onResize={onResize} onDraw={onDraw} onHover={onHover} pixelSize={4} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,79 +1,90 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
|
||||
import { useLocale, useProject } from '../../contexts/index.js'
|
||||
import { useCanvas } from '../../hooks/index.js'
|
||||
import type { ColormapType } from '../../previews/Colormap.js'
|
||||
import { densityFunction, getNoiseBlock, noiseSettings } from '../../previews/index.js'
|
||||
import { CachedCollections, checkVersion } from '../../services/index.js'
|
||||
import { DataModel } from '@mcschema/core'
|
||||
import { clampedMap } from 'deepslate'
|
||||
import type { mat3 } from 'gl-matrix'
|
||||
import { vec2 } from 'gl-matrix'
|
||||
import { useCallback, useMemo, useRef, useState } from 'preact/hooks'
|
||||
import { getProjectData, useLocale, useProject } from '../../contexts/index.js'
|
||||
import { useAsync } from '../../hooks/index.js'
|
||||
import { CachedCollections } from '../../services/index.js'
|
||||
import { Store } from '../../Store.js'
|
||||
import { randomSeed } from '../../Utils.js'
|
||||
import { iterateWorld2D, randomSeed } from '../../Utils.js'
|
||||
import { Btn, BtnInput, BtnMenu } 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 = ({ data, shown, version }: PreviewProps) => {
|
||||
const { locale } = useLocale()
|
||||
const { project } = useProject()
|
||||
const [seed, setSeed] = useState(randomSeed())
|
||||
const [biome, setBiome] = useState('minecraft:plains')
|
||||
const [biomeScale, setBiomeScale] = useState(0.2)
|
||||
const [biomeDepth, setBiomeDepth] = useState(0.1)
|
||||
const [autoScroll, setAutoScroll] = useState(false)
|
||||
const [focused, setFocused] = useState<string[]>([])
|
||||
const [layer, setLayer] = useState('terrain')
|
||||
const state = JSON.stringify(data)
|
||||
|
||||
const { value } = useAsync(async () => {
|
||||
const unwrapped = DataModel.unwrapLists(data)
|
||||
await DEEPSLATE.loadVersion(version, getProjectData(project))
|
||||
const biomeSource = { type: 'fixed', biome }
|
||||
await DEEPSLATE.loadChunkGenerator(unwrapped, biomeSource, seed)
|
||||
const noiseSettings = DEEPSLATE.getNoiseSettings()
|
||||
const finalDensity = DEEPSLATE.loadDensityFunction(unwrapped?.noise_router?.final_density, noiseSettings.minY, noiseSettings.height, seed)
|
||||
return { noiseSettings, finalDensity }
|
||||
}, [state, 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 offset = useRef(0)
|
||||
const scrollInterval = useRef<number | undefined>(undefined)
|
||||
const state = JSON.stringify([data, biomeScale, biomeDepth])
|
||||
|
||||
const size = data?.noise?.height ?? 256
|
||||
const { canvas, redraw } = useCanvas({
|
||||
size() {
|
||||
return [size, size]
|
||||
},
|
||||
async draw(img) {
|
||||
const options = { biome, biomeDepth, biomeScale, offset: offset.current, width: img.width, seed, version, project, colormap, minY: data?.noise?.min_y ?? 0, height: data?.noise?.height ?? 256, hardZero: true }
|
||||
if (layer === 'final_density') {
|
||||
const df = data?.noise_router?.final_density ?? 0
|
||||
await densityFunction(df, img, options)
|
||||
} else {
|
||||
await noiseSettings(data, img, options)
|
||||
}
|
||||
},
|
||||
async onDrag(dx) {
|
||||
offset.current += dx * size
|
||||
redraw()
|
||||
},
|
||||
async onHover(x, y) {
|
||||
const worldX = Math.floor(x * size - offset.current)
|
||||
const worldY = size - Math.max(1, Math.ceil(y * size)) + (data?.noise?.min_y ?? 0)
|
||||
const block = getNoiseBlock(worldX, worldY)
|
||||
setFocused([block ? `Y=${worldY} (${block.getName().path})` : `Y=${worldY}`])
|
||||
},
|
||||
onLeave() {
|
||||
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)
|
||||
iterateWorld2D(imageData.current, transform, (x, y) => {
|
||||
return DEEPSLATE.getBlockState(x, y)?.getName().toString()
|
||||
}, (block) => {
|
||||
return BlockColors[block ?? 'minecraft:air']
|
||||
})
|
||||
} 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, shown])
|
||||
const onHover = useCallback((pos: [number, number] | undefined) => {
|
||||
if (!pos || !noiseSettings || !finalDensity) {
|
||||
setFocused([])
|
||||
},
|
||||
}, [version, state, seed, project, shown, biome, biomeScale, biomeDepth, layer, colormap])
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollInterval.current) {
|
||||
clearInterval(scrollInterval.current)
|
||||
} 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}`])
|
||||
}
|
||||
if (shown) {
|
||||
(async () => {
|
||||
try {
|
||||
await redraw()
|
||||
if (autoScroll) {
|
||||
scrollInterval.current = setInterval(() => {
|
||||
offset.current -= 8
|
||||
redraw()
|
||||
}, 100) as any
|
||||
}
|
||||
} catch (e) {
|
||||
throw e
|
||||
}
|
||||
})()
|
||||
}
|
||||
}, [version, state, seed, project, shown, biome, biomeScale, biomeDepth, autoScroll, layer, colormap])
|
||||
}, [noiseSettings, finalDensity])
|
||||
|
||||
const allBiomes = useMemo(() => CachedCollections?.get('worldgen/biome') ?? [], [version])
|
||||
|
||||
@@ -82,18 +93,33 @@ export const NoiseSettingsPreview = ({ data, shown, version }: PreviewProps) =>
|
||||
{focused.map(s => <Btn label={s} class="no-pointer" /> )}
|
||||
{layer === 'final_density' && <ColormapSelector value={colormap} onChange={setColormap} />}
|
||||
<BtnMenu icon="gear" tooltip={locale('terrain_settings')}>
|
||||
{checkVersion(version, undefined, '1.17') ? <>
|
||||
<BtnInput label={locale('preview.scale')} value={`${biomeScale}`} onChange={v => setBiomeScale(Number(v))} />
|
||||
<BtnInput label={locale('preview.depth')} value={`${biomeDepth}`} onChange={v => setBiomeDepth(Number(v))} />
|
||||
</> :
|
||||
<BtnInput label={locale('preview.biome')} value={biome} onChange={setBiome} dataList={allBiomes} larger />
|
||||
}
|
||||
<Btn icon={autoScroll ? 'square_fill' : 'square'} label={locale('preview.auto_scroll')} onClick={() => setAutoScroll(!autoScroll)} />
|
||||
<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>
|
||||
<canvas ref={canvas} width={size} height={size}></canvas>
|
||||
<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],
|
||||
}
|
||||
|
||||
@@ -180,3 +180,13 @@ export function disectFilePath(path: string) {
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
export function getProjectData(project: Project) {
|
||||
return Object.fromEntries(['worldgen/noise_settings', 'worldgen/noise', 'worldgen/density_function'].map(type => {
|
||||
const resources = Object.fromEntries(
|
||||
project.files.filter(file => file.type === type)
|
||||
.map<[string, unknown]>(file => [file.id, file.data])
|
||||
)
|
||||
return [type, resources]
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export * from './useActiveTimout.js'
|
||||
export * from './useAsync.js'
|
||||
export * from './useAsyncFn.js'
|
||||
export * from './useCanvas.js'
|
||||
export * from './useFocus.js'
|
||||
export * from './useHash.js'
|
||||
export * from './useLocalStorage.js'
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import type { Inputs } from 'preact/hooks'
|
||||
import { useEffect, useRef } from 'preact/hooks'
|
||||
|
||||
type Vec2 = [number, number]
|
||||
|
||||
export function useCanvas({ size, draw, onDrag, onHover, onLeave }: {
|
||||
size: () => Vec2,
|
||||
draw: (img: ImageData) => Promise<unknown>,
|
||||
onDrag?: (dx: number, dy: number) => Promise<unknown>,
|
||||
onHover?: (x: number, y: number) => unknown,
|
||||
onLeave?: () => unknown,
|
||||
}, inputs?: Inputs) {
|
||||
const canvas = useRef<HTMLCanvasElement>(null)
|
||||
|
||||
const dragStart = useRef<Vec2 | undefined>()
|
||||
const dragRequest = useRef<number>()
|
||||
const dragPending = useRef<Vec2>([0, 0])
|
||||
const dragBusy = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (!canvas.current) return
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
dragStart.current = [e.offsetX, e.offsetY]
|
||||
}
|
||||
const onMouseMove = (e: MouseEvent) => {
|
||||
if (dragStart.current === undefined) {
|
||||
if (!canvas.current) return
|
||||
const x = e.offsetX / canvas.current.clientWidth
|
||||
const y = e.offsetY / canvas.current.clientHeight
|
||||
onHover?.(x, y)
|
||||
return
|
||||
}
|
||||
if (!onDrag) return
|
||||
const dx = e.offsetX - dragStart.current[0]
|
||||
const dy = e.offsetY - dragStart.current[1]
|
||||
if (!(dx === 0 && dy === 0)) {
|
||||
dragPending.current = [dragPending.current[0] + dx, dragPending.current[1] + dy]
|
||||
if (!dragBusy.current) {
|
||||
if (dragRequest.current) {
|
||||
cancelAnimationFrame(dragRequest.current)
|
||||
}
|
||||
dragRequest.current = requestAnimationFrame(async () => {
|
||||
if (!canvas.current) return
|
||||
dragBusy.current = true
|
||||
const dx = dragPending.current[0] / canvas.current.clientWidth
|
||||
const dy = dragPending.current[1] / canvas.current.clientHeight
|
||||
dragPending.current = [0, 0]
|
||||
await onDrag?.(dx, dy)
|
||||
dragBusy.current = false
|
||||
})
|
||||
}
|
||||
}
|
||||
dragStart.current = [e.offsetX, e.offsetY]
|
||||
}
|
||||
const onMouseUp = () => {
|
||||
dragStart.current = undefined
|
||||
}
|
||||
const onMouseLeave = () => {
|
||||
onLeave?.()
|
||||
}
|
||||
|
||||
canvas.current.addEventListener('mousedown', onMouseDown)
|
||||
canvas.current.addEventListener('mousemove', onMouseMove)
|
||||
canvas.current.addEventListener('mouseleave', onMouseLeave)
|
||||
document.body.addEventListener('mouseup', onMouseUp)
|
||||
|
||||
return () => {
|
||||
canvas.current?.removeEventListener('mousedown', onMouseDown)
|
||||
canvas.current?.removeEventListener('mousemove', onMouseMove)
|
||||
canvas.current?.removeEventListener('mouseleave', onMouseLeave)
|
||||
document.body.removeEventListener('mouseup', onMouseUp)
|
||||
}
|
||||
}, [...inputs ?? [], canvas.current])
|
||||
|
||||
const redraw = useRef<() => Promise<unknown>>()
|
||||
const redrawCount = useRef(0)
|
||||
redraw.current = async () => {
|
||||
if (!canvas.current) return
|
||||
const ctx = canvas.current.getContext('2d')!
|
||||
const s = size()
|
||||
canvas.current.width = s[0]
|
||||
canvas.current.height = s[1]
|
||||
const img = ctx.getImageData(0, 0, s[0], s[1])
|
||||
const ownCount = redrawCount.current += 1
|
||||
try {
|
||||
await draw(img)
|
||||
} catch (e) {
|
||||
throw e
|
||||
}
|
||||
if (ownCount === redrawCount.current) {
|
||||
ctx.putImageData(img, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
canvas,
|
||||
redraw: redraw.current,
|
||||
}
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
import { DataModel } from '@mcschema/core'
|
||||
import type { Project } from '../contexts/Project.jsx'
|
||||
import type { VersionId } from '../services/index.js'
|
||||
import { stringToColor } from '../Utils.js'
|
||||
import { DEEPSLATE } from './Deepslate.js'
|
||||
import { getProjectData } from './NoiseSettings.js'
|
||||
|
||||
type Triple = [number, number, number]
|
||||
type BiomeColors = Record<string, Triple>
|
||||
type BiomeSourceOptions = {
|
||||
biomeColors: BiomeColors,
|
||||
offset: [number, number],
|
||||
scale: number,
|
||||
res: number,
|
||||
seed: bigint,
|
||||
version: VersionId,
|
||||
settings: unknown,
|
||||
project: Project,
|
||||
y: number,
|
||||
}
|
||||
|
||||
export async function biomeMap(state: any, img: ImageData, options: BiomeSourceOptions) {
|
||||
await DEEPSLATE.loadVersion(options.version, getProjectData(options.project))
|
||||
await DEEPSLATE.loadChunkGenerator(DataModel.unwrapLists(options.settings), DataModel.unwrapLists(state), options.seed)
|
||||
|
||||
const quartStep = Math.max(1, Math.round(options.scale))
|
||||
const quartWidth = 200 * quartStep
|
||||
|
||||
const centerX = Math.round(-options.offset[0] * options.scale)
|
||||
const centerZ = Math.round(-options.offset[1] * options.scale)
|
||||
|
||||
const minX = Math.floor(centerX - quartWidth / 2)
|
||||
const minZ = Math.floor(centerZ - quartWidth / 2)
|
||||
const maxX = minX + quartWidth
|
||||
const maxZ = minZ + quartWidth
|
||||
|
||||
const { palette, data, width, height } = DEEPSLATE.fillBiomes(minX * 4, maxX * 4, minZ * 4, maxZ * 4, quartStep * options.res, options.y)
|
||||
|
||||
let x = 0
|
||||
let z = 0
|
||||
for (let i = 0; i < data.length; i += 1) {
|
||||
const biome = palette.get(data[i])
|
||||
const color = getBiomeColor(biome ?? '', options.biomeColors)
|
||||
const j = z * width + x
|
||||
img.data[j * 4] = color[0]
|
||||
img.data[j * 4 + 1] = color[1]
|
||||
img.data[j * 4 + 2] = color[2]
|
||||
img.data[j * 4 + 3] = 255
|
||||
|
||||
z += 1
|
||||
if (z >= height) {
|
||||
z = 0
|
||||
x += 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getBiome(state: any, x: number, z: number, options: BiomeSourceOptions): Promise<{[k: string]: number | string} | undefined> {
|
||||
await DEEPSLATE.loadVersion(options.version, getProjectData(options.project))
|
||||
await DEEPSLATE.loadChunkGenerator(DataModel.unwrapLists(options.settings), DataModel.unwrapLists( state), options.seed)
|
||||
|
||||
const quartStep = Math.max(1, Math.round(options.scale))
|
||||
|
||||
const centerX = Math.round(-options.offset[0] * options.scale)
|
||||
const centerZ = Math.round(-options.offset[1] * options.scale)
|
||||
|
||||
const xx = Math.floor(centerX + ((x - 100) * quartStep))
|
||||
const zz = Math.floor(centerZ + ((z - 100) * quartStep))
|
||||
|
||||
const { palette, data } = DEEPSLATE.fillBiomes(xx * 4, xx * 4 + 4, zz * 4, zz * 4 + 4, 1, options.y)
|
||||
const biome = palette.get(data[0])!
|
||||
|
||||
return {
|
||||
biome,
|
||||
}
|
||||
}
|
||||
|
||||
function getBiomeColor(biome: string, biomeColors: BiomeColors): Triple {
|
||||
if (!biome) {
|
||||
return [128, 128, 128]
|
||||
}
|
||||
const color = biomeColors[biome] ?? VanillaColors[biome]
|
||||
if (color === undefined) {
|
||||
return stringToColor(biome)
|
||||
}
|
||||
return color
|
||||
}
|
||||
|
||||
export const VanillaColors: Record<string, Triple> = {
|
||||
'minecraft:badlands': [217,69,21],
|
||||
'minecraft:badlands_plateau': [202,140,101],
|
||||
'minecraft:bamboo_jungle': [118,142,20],
|
||||
'minecraft:bamboo_jungle_hills': [59,71,10],
|
||||
'minecraft:basalt_deltas': [64,54,54],
|
||||
'minecraft:beach': [250,222,85],
|
||||
'minecraft:birch_forest': [48,116,68],
|
||||
'minecraft:birch_forest_hills': [31,95,50],
|
||||
'minecraft:cold_ocean': [32,32,112],
|
||||
'minecraft:crimson_forest': [221,8,8],
|
||||
'minecraft:dark_forest': [64,81,26],
|
||||
'minecraft:dark_forest_hills': [104,121,66],
|
||||
'minecraft:deep_cold_ocean': [32,32,56],
|
||||
'minecraft:deep_frozen_ocean': [64,64,144],
|
||||
'minecraft:deep_lukewarm_ocean': [0,0,64],
|
||||
'minecraft:deep_ocean': [0,0,48],
|
||||
'minecraft:deep_warm_ocean': [0,0,80],
|
||||
'minecraft:desert': [250,148,24],
|
||||
'minecraft:desert_hills': [210,95,18],
|
||||
'minecraft:desert_lakes': [255,188,64],
|
||||
'minecraft:end_barrens': [39,30,61],
|
||||
'minecraft:end_highlands': [232,244,178],
|
||||
'minecraft:end_midlands': [194,187,136],
|
||||
'minecraft:eroded_badlands': [255,109,61],
|
||||
'minecraft:flower_forest': [45,142,73],
|
||||
'minecraft:forest': [5,102,33],
|
||||
'minecraft:frozen_ocean': [112,112,214],
|
||||
'minecraft:frozen_river': [160,160,255],
|
||||
'minecraft:giant_spruce_taiga': [129,142,121],
|
||||
'minecraft:old_growth_spruce_taiga': [129,142,121],
|
||||
'minecraft:giant_spruce_taiga_hills': [109,119,102],
|
||||
'minecraft:giant_tree_taiga': [89,102,81],
|
||||
'minecraft:old_growth_pine_taiga': [89,102,81],
|
||||
'minecraft:giant_tree_taiga_hills': [69,79,62],
|
||||
'minecraft:gravelly_hills': [136,136,136],
|
||||
'minecraft:gravelly_mountains': [136,136,136],
|
||||
'minecraft:windswept_gravelly_hills': [136,136,136],
|
||||
'minecraft:ice_spikes': [180,220,220],
|
||||
'minecraft:jungle': [83,123,9],
|
||||
'minecraft:jungle_edge': [98,139,23],
|
||||
'minecraft:sparse_jungle': [98,139,23],
|
||||
'minecraft:jungle_hills': [44,66,5],
|
||||
'minecraft:lukewarm_ocean': [0,0,144],
|
||||
'minecraft:modified_badlands_plateau': [242,180,141],
|
||||
'minecraft:modified_gravelly_mountains': [120,152,120],
|
||||
'minecraft:modified_jungle': [123,163,49],
|
||||
'minecraft:modified_jungle_edge': [138,179,63],
|
||||
'minecraft:modified_wooded_badlands_plateau': [216,191,141],
|
||||
'minecraft:mountain_edge': [114,120,154],
|
||||
'minecraft:extreme_hills': [96,96,96],
|
||||
'minecraft:mountains': [96,96,96],
|
||||
'minecraft:windswept_hills': [96,96,96],
|
||||
'minecraft:mushroom_field_shore': [160,0,255],
|
||||
'minecraft:mushroom_fields': [255,0,255],
|
||||
'minecraft:nether_wastes': [191,59,59],
|
||||
'minecraft:ocean': [0,0,112],
|
||||
'minecraft:plains': [141,179,96],
|
||||
'minecraft:river': [0,0,255],
|
||||
'minecraft:savanna': [189,178,95],
|
||||
'minecraft:savanna_plateau': [167,157,100],
|
||||
'minecraft:shattered_savanna': [229,218,135],
|
||||
'minecraft:windswept_savanna': [229,218,135],
|
||||
'minecraft:shattered_savanna_plateau': [207,197,140],
|
||||
'minecraft:small_end_islands': [16,12,28],
|
||||
'minecraft:snowy_beach': [250,240,192],
|
||||
'minecraft:snowy_mountains': [160,160,160],
|
||||
'minecraft:snowy_taiga': [49,85,74],
|
||||
'minecraft:snowy_taiga_hills': [36,63,54],
|
||||
'minecraft:snowy_taiga_mountains': [89,125,114],
|
||||
'minecraft:snowy_tundra': [255,255,255],
|
||||
'minecraft:snowy_plains': [255,255,255],
|
||||
'minecraft:soul_sand_valley': [94,56,48],
|
||||
'minecraft:stone_shore': [162,162,132],
|
||||
'minecraft:stony_shore': [162,162,132],
|
||||
'minecraft:sunflower_plains': [181,219,136],
|
||||
'minecraft:swamp': [7,249,178],
|
||||
'minecraft:swamp_hills': [47,255,218],
|
||||
'minecraft:taiga': [11,102,89],
|
||||
'minecraft:taiga_hills': [22,57,51],
|
||||
'minecraft:taiga_mountains': [51,142,129],
|
||||
'minecraft:tall_birch_forest': [88,156,108],
|
||||
'minecraft:old_growth_birch_forest': [88,156,108],
|
||||
'minecraft:tall_birch_hills': [71,135,90],
|
||||
'minecraft:the_end': [59,39,84],
|
||||
'minecraft:the_void': [0,0,0],
|
||||
'minecraft:warm_ocean': [0,0,172],
|
||||
'minecraft:warped_forest': [73,144,123],
|
||||
'minecraft:wooded_badlands_plateau': [176,151,101],
|
||||
'minecraft:wooded_badlands': [176,151,101],
|
||||
'minecraft:wooded_hills': [34,85,28],
|
||||
'minecraft:wooded_mountains': [80,112,80],
|
||||
'minecraft:windswept_forest': [80,112,80],
|
||||
'minecraft:snowy_slopes': [140, 195, 222],
|
||||
'minecraft:lofty_peaks': [196, 168, 193],
|
||||
'minecraft:jagged_peaks': [196, 168, 193],
|
||||
'minecraft:snowcapped_peaks': [200, 198, 200],
|
||||
'minecraft:frozen_peaks': [200, 198, 200],
|
||||
'minecraft:stony_peaks': [82, 92, 103],
|
||||
'minecraft:grove': [150, 150, 189],
|
||||
'minecraft:meadow': [169, 197, 80],
|
||||
'minecraft:lush_caves': [112, 255, 79],
|
||||
'minecraft:dripstone_caves': [140, 124, 0],
|
||||
'minecraft:deep_dark': [10, 14, 19],
|
||||
'minecraft:mangrove_swamp': [36,196,142],
|
||||
}
|
||||
@@ -1,144 +0,0 @@
|
||||
import { DataModel } from '@mcschema/core'
|
||||
import { BlockState, clampedMap, DensityFunction } from 'deepslate/worldgen'
|
||||
import type { Project } from '../contexts/Project.jsx'
|
||||
import type { VersionId } from '../services/index.js'
|
||||
import { checkVersion } from '../services/index.js'
|
||||
import type { ColormapType } from './Colormap.js'
|
||||
import { getColormap } from './Colormap.js'
|
||||
import { DEEPSLATE } from './Deepslate.js'
|
||||
import { NoiseChunkGenerator as OldNoiseChunkGenerator } from './noise/NoiseChunkGenerator.js'
|
||||
|
||||
export type NoiseSettingsOptions = {
|
||||
biome?: string,
|
||||
biomeScale?: number,
|
||||
biomeDepth?: number,
|
||||
offset: number,
|
||||
width: number,
|
||||
seed: bigint,
|
||||
version: VersionId,
|
||||
project: Project,
|
||||
minY?: number,
|
||||
height?: number,
|
||||
colormap?: ColormapType,
|
||||
hardZero?: boolean,
|
||||
}
|
||||
|
||||
const colors: 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],
|
||||
}
|
||||
|
||||
export async function noiseSettings(state: any, img: ImageData, options: NoiseSettingsOptions) {
|
||||
if (checkVersion(options.version, '1.18')) {
|
||||
await DEEPSLATE.loadVersion(options.version, getProjectData(options.project))
|
||||
const biomeSource = { type: 'fixed', biome: options.biome }
|
||||
await DEEPSLATE.loadChunkGenerator(DataModel.unwrapLists(state), biomeSource, options.seed)
|
||||
DEEPSLATE.generateChunks(-options.offset, options.width)
|
||||
const noise = DEEPSLATE.getNoiseSettings()
|
||||
|
||||
const data = img.data
|
||||
for (let x = 0; x < options.width; x += 1) {
|
||||
for (let y = 0; y < noise.height; y += 1) {
|
||||
const i = x * 4 + (noise.height-y-1) * 4 * img.width
|
||||
const state = DEEPSLATE.getBlockState(x - options.offset, y + noise.minY) ?? BlockState.AIR
|
||||
const color = colors[state.getName().toString()] ?? [0, 0, 0]
|
||||
data[i] = color[0]
|
||||
data[i + 1] = color[1]
|
||||
data[i + 2] = color[2]
|
||||
data[i + 3] = 255
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const generator = new OldNoiseChunkGenerator(options.seed)
|
||||
generator.reset(state.noise, options.biomeDepth ?? 0, options.biomeScale ?? 0, options.offset, options.width)
|
||||
const data = img.data
|
||||
const row = img.width * 4
|
||||
for (let x = 0; x < options.width; x += 1) {
|
||||
const noise = generator.iterateNoiseColumn(x - options.offset).reverse()
|
||||
for (let y = 0; y < state.noise.height; y += 1) {
|
||||
const i = y * row + x * 4
|
||||
const color = getColor(noise, y)
|
||||
data[i] = color
|
||||
data[i + 1] = color
|
||||
data[i + 2] = color
|
||||
data[i + 3] = 255
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getNoiseBlock(x: number, y: number) {
|
||||
return DEEPSLATE.getBlockState(x, y)
|
||||
}
|
||||
|
||||
export async function densityFunction(state: any, img: ImageData, options: NoiseSettingsOptions) {
|
||||
await DEEPSLATE.loadVersion(options.version, getProjectData(options.project))
|
||||
const fn = DEEPSLATE.loadDensityFunction(DataModel.unwrapLists(state), options.minY ?? 0, options.height ?? 256, options.seed)
|
||||
const noise = DEEPSLATE.getNoiseSettings()
|
||||
|
||||
const arr = Array(options.width * noise.height)
|
||||
let limit = 0.01
|
||||
for (let x = 0; x < options.width; x += 1) {
|
||||
for (let y = 0; y < noise.height; y += 1) {
|
||||
const i = x + y * options.width
|
||||
const density = fn.compute(DensityFunction.context(x - options.offset, noise.height - y - 1 + noise.minY, 0))
|
||||
limit = Math.max(limit, Math.min(1, Math.abs(density)))
|
||||
arr[i] = density
|
||||
}
|
||||
}
|
||||
|
||||
const colormap = getColormap(options.colormap ?? 'viridis')
|
||||
const colorPicker = options.hardZero ? (t: number) => colormap(t <= 0.5 ? t - 0.08 : t + 0.08) : colormap
|
||||
const min = -limit
|
||||
const max = limit
|
||||
const data = img.data
|
||||
for (let i = 0; i < options.width * noise.height; i += 1) {
|
||||
const color = colorPicker(clampedMap(arr[i], min, max, 1, 0))
|
||||
data[4 * i] = color[0] * 256
|
||||
data[4 * i + 1] = color[1] * 256
|
||||
data[4 * i + 2] = color[2] * 256
|
||||
data[4 * i + 3] = 255
|
||||
}
|
||||
}
|
||||
|
||||
export async function densityPoint(state: any, x: number, y: number, options: NoiseSettingsOptions) {
|
||||
await DEEPSLATE.loadVersion(options.version, getProjectData(options.project))
|
||||
const fn = DEEPSLATE.loadDensityFunction(DataModel.unwrapLists(state), options.minY ?? 0, options.height ?? 256, options.seed)
|
||||
|
||||
return fn.compute(DensityFunction.context(Math.floor(x - options.offset), (options.height ?? 256) - y, 0))
|
||||
}
|
||||
|
||||
export function getProjectData(project: Project) {
|
||||
return Object.fromEntries(['worldgen/noise_settings', 'worldgen/noise', 'worldgen/density_function'].map(type => {
|
||||
const resources = Object.fromEntries(
|
||||
project.files.filter(file => file.type === type)
|
||||
.map<[string, unknown]>(file => [file.id, file.data])
|
||||
)
|
||||
return [type, resources]
|
||||
}))
|
||||
}
|
||||
|
||||
function getColor(noise: number[], y: number): number {
|
||||
if (noise[y] > 0) {
|
||||
return 0
|
||||
}
|
||||
if (noise[y+1] > 0) {
|
||||
return 150
|
||||
}
|
||||
return 255
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { DataModel } from '@mcschema/core'
|
||||
import { clampedMap, NoiseParameters, NormalNoise, XoroshiroRandom } from 'deepslate/worldgen'
|
||||
import type { VersionId } from '../services/index.js'
|
||||
import type { ColormapType } from './Colormap.js'
|
||||
import { getColormap } from './Colormap.js'
|
||||
|
||||
export type NoiseOptions = {
|
||||
offset: [number, number],
|
||||
scale: number,
|
||||
seed: bigint,
|
||||
version: VersionId,
|
||||
colormap: ColormapType,
|
||||
}
|
||||
|
||||
export function normalNoise(state: any, img: ImageData, options: NoiseOptions) {
|
||||
const random = XoroshiroRandom.create(options.seed)
|
||||
const params = NoiseParameters.fromJson(DataModel.unwrapLists(state))
|
||||
const noise = new NormalNoise(random, params)
|
||||
|
||||
const colormap = getColormap(options.colormap)
|
||||
const ox = -options.offset[0] - 100
|
||||
const oy = -options.offset[1] - 100
|
||||
const data = img.data
|
||||
for (let x = 0; x < 256; x += 1) {
|
||||
for (let y = 0; y < 256; y += 1) {
|
||||
const i = x * 4 + y * 4 * 256
|
||||
const xx = (x + ox) * options.scale
|
||||
const yy = (y + oy) * options.scale
|
||||
const output = noise.sample(xx, yy, 0)
|
||||
const color = colormap(clampedMap(output, -1, 1, 0, 1))
|
||||
data[i] = color[0] * 256
|
||||
data[i + 1] = color[1] * 256
|
||||
data[i + 2] = color[2] * 256
|
||||
data[i + 3] = 255
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function normalNoisePoint(state: any, x: number, y: number, options: NoiseOptions) {
|
||||
const random = XoroshiroRandom.create(options.seed)
|
||||
const params = NoiseParameters.fromJson(DataModel.unwrapLists(state))
|
||||
const noise = new NormalNoise(random, params)
|
||||
|
||||
const ox = -options.offset[0] - 100
|
||||
const oy = -options.offset[1] - 100
|
||||
const xx = (x + ox) * options.scale
|
||||
const yy = (y + oy) * options.scale
|
||||
return noise.sample(xx, yy, 0)
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from './BiomeSource.js'
|
||||
export * from './Decorator.js'
|
||||
export * from './NoiseSettings.js'
|
||||
export * from './NormalNoise.js'
|
||||
@@ -1,150 +0,0 @@
|
||||
import { LegacyRandom, PerlinNoise } from 'deepslate/worldgen'
|
||||
import { clampedLerp, lerp2 } from '../../Utils.js'
|
||||
|
||||
export class NoiseChunkGenerator {
|
||||
private readonly minLimitPerlinNoise: PerlinNoise
|
||||
private readonly maxLimitPerlinNoise: PerlinNoise
|
||||
private readonly mainPerlinNoise: PerlinNoise
|
||||
private readonly depthNoise: PerlinNoise
|
||||
|
||||
private settings: any = {}
|
||||
private chunkWidth: number = 4
|
||||
private chunkHeight: number = 4
|
||||
private chunkCountY: number = 32
|
||||
private biomeDepth: number = 0.1
|
||||
private biomeScale: number = 0.2
|
||||
|
||||
private noiseColumnCache: (number[] | null)[] = []
|
||||
private xOffset: number = 0
|
||||
|
||||
constructor(seed: bigint) {
|
||||
const random = new LegacyRandom(seed)
|
||||
this.minLimitPerlinNoise = new PerlinNoise(random, -15, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
|
||||
this.maxLimitPerlinNoise = new PerlinNoise(random, -15, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
|
||||
this.mainPerlinNoise = new PerlinNoise(random, -7, [1, 1, 1, 1, 1, 1, 1, 1])
|
||||
this.depthNoise = new PerlinNoise(random, -15, [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
|
||||
}
|
||||
|
||||
public reset(settings: any, depth: number, scale: number, xOffset: number, width: number) {
|
||||
this.settings = settings
|
||||
this.chunkWidth = settings.size_horizontal * 4
|
||||
this.chunkHeight = settings.size_vertical * 4
|
||||
this.chunkCountY = Math.floor(settings.height / this.chunkHeight)
|
||||
|
||||
if (settings.amplified && depth > 0) {
|
||||
depth = 1 + depth * 2
|
||||
scale = 1 + scale * 4
|
||||
}
|
||||
this.biomeDepth = 0.265625 * (depth * 0.5 - 0.125)
|
||||
this.biomeScale = 96.0 / (scale * 0.9 + 0.1)
|
||||
|
||||
this.noiseColumnCache = Array(width).fill(null)
|
||||
this.xOffset = xOffset
|
||||
}
|
||||
|
||||
public iterateNoiseColumn(x: number): number[] {
|
||||
const data = Array(this.chunkCountY * this.chunkHeight)
|
||||
const cx = Math.floor(x / this.chunkWidth)
|
||||
const ox = Math.floor(x % this.chunkWidth) / this.chunkWidth
|
||||
const noise1 = this.fillNoiseColumn(cx)
|
||||
const noise2 = this.fillNoiseColumn(cx + 1)
|
||||
|
||||
for (let y = this.chunkCountY - 1; y >= 0; y -= 1) {
|
||||
for (let yy = this.chunkHeight; yy >= 0; yy -= 1) {
|
||||
const oy = yy / this.chunkHeight
|
||||
const i = y * this.chunkHeight + yy
|
||||
data[i] = lerp2(oy, ox, noise1[y], noise1[y+1], noise2[y], noise2[y+1])
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
private fillNoiseColumn(x: number): number[] {
|
||||
const cachedColumn = this.noiseColumnCache[x - this.xOffset]
|
||||
if (cachedColumn) return cachedColumn
|
||||
|
||||
const data = Array(this.chunkCountY + 1)
|
||||
|
||||
const xzScale = 684.412 * this.settings.sampling.xz_scale
|
||||
const yScale = 684.412 * this.settings.sampling.y_scale
|
||||
const xzFactor = xzScale / this.settings.sampling.xz_factor
|
||||
const yFactor = yScale / this.settings.sampling.y_factor
|
||||
const randomDensity = this.settings.random_density_offset ? this.getRandomDensity(x) : 0
|
||||
|
||||
for (let y = 0; y <= this.chunkCountY; y += 1) {
|
||||
let noise = this.sampleAndClampNoise(x, y, this.mainPerlinNoise.getOctaveNoise(0)!.zo, xzScale, yScale, xzFactor, yFactor)
|
||||
const yOffset = 1 - y * 2 / this.chunkCountY + randomDensity
|
||||
const density = yOffset * this.settings.density_factor + this.settings.density_offset
|
||||
const falloff = (density + this.biomeDepth) * this.biomeScale
|
||||
noise += falloff * (falloff > 0 ? 4 : 1)
|
||||
|
||||
if (this.settings.top_slide.size > 0) {
|
||||
noise = clampedLerp(
|
||||
this.settings.top_slide.target,
|
||||
noise,
|
||||
(this.chunkCountY - y - (this.settings.top_slide.offset)) / (this.settings.top_slide.size)
|
||||
)
|
||||
}
|
||||
|
||||
if (this.settings.bottom_slide.size > 0) {
|
||||
noise = clampedLerp(
|
||||
this.settings.bottom_slide.target,
|
||||
noise,
|
||||
(y - (this.settings.bottom_slide.offset)) / (this.settings.bottom_slide.size)
|
||||
)
|
||||
}
|
||||
data[y] = noise
|
||||
}
|
||||
|
||||
this.noiseColumnCache[x - this.xOffset] = data
|
||||
return data
|
||||
}
|
||||
|
||||
private getRandomDensity(x: number): number {
|
||||
const noise = this.depthNoise.sample(x * 200, 10, this.depthNoise.getOctaveNoise(0)!.zo, 1, 0, true)
|
||||
const a = (noise < 0) ? -noise * 0.3 : noise
|
||||
const b = a * 24.575625 - 2
|
||||
return (b < 0) ? b * 0.009486607142857142 : Math.min(b, 1) * 0.006640625
|
||||
}
|
||||
|
||||
private sampleAndClampNoise(x: number, y: number, z: number, xzScale: number, yScale: number, xzFactor: number, yFactor: number): number {
|
||||
let a = 0
|
||||
let b = 0
|
||||
let c = 0
|
||||
let d = 1
|
||||
|
||||
for (let i = 0; i < 16; i += 1) {
|
||||
const x2 = PerlinNoise.wrap(x * xzScale * d)
|
||||
const y2 = PerlinNoise.wrap(y * yScale * d)
|
||||
const z2 = PerlinNoise.wrap(z * xzScale * d)
|
||||
const e = yScale * d
|
||||
|
||||
const minLimitNoise = this.minLimitPerlinNoise.getOctaveNoise(i)
|
||||
if (minLimitNoise) {
|
||||
a += minLimitNoise.sample(x2, y2, z2, e, y * e) / d
|
||||
}
|
||||
|
||||
const maxLimitNoise = this.maxLimitPerlinNoise.getOctaveNoise(i)
|
||||
if (maxLimitNoise) {
|
||||
b += maxLimitNoise.sample(x2, y2, z2, e, y * e) / d
|
||||
}
|
||||
|
||||
if (i < 8) {
|
||||
const mainNoise = this.mainPerlinNoise.getOctaveNoise(i)
|
||||
if (mainNoise) {
|
||||
c += mainNoise.sample(
|
||||
PerlinNoise.wrap(x * xzFactor * d),
|
||||
PerlinNoise.wrap(y * yFactor * d),
|
||||
PerlinNoise.wrap(z * xzFactor * d),
|
||||
yFactor * d,
|
||||
y * yFactor * d
|
||||
) / d
|
||||
}
|
||||
}
|
||||
|
||||
d /= 2
|
||||
}
|
||||
|
||||
return clampedLerp(a / 512, b / 512, (c / 10 + 1) / 2)
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,10 @@ import { memo } from 'preact/compat'
|
||||
import { useState } from 'preact/hooks'
|
||||
import { Btn, Octicon } from '../components/index.js'
|
||||
import { ItemDisplay } from '../components/ItemDisplay.jsx'
|
||||
import { VanillaColors } from '../components/previews/BiomeSourcePreview.jsx'
|
||||
import config from '../Config.js'
|
||||
import { localize, useLocale, useStore } from '../contexts/index.js'
|
||||
import { useFocus } from '../hooks/index.js'
|
||||
import { VanillaColors } from '../previews/index.js'
|
||||
import type { BlockStateRegistry, VersionId } from '../services/index.js'
|
||||
import { CachedDecorator, CachedFeature } from '../services/index.js'
|
||||
import { deepClone, deepEqual, generateUUID, hexId, hexToRgb, isObject, newSeed, rgbToHex, stringToColor } from '../Utils.js'
|
||||
|
||||
+8
-6
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"2d": "2D",
|
||||
"3d": "3D",
|
||||
"add": "Add",
|
||||
"add_bottom": "Add to bottom",
|
||||
"add_top": "Add to top",
|
||||
@@ -22,6 +24,7 @@
|
||||
"copy_share": "Copy share link",
|
||||
"copied": "Copied!",
|
||||
"copy_context": "Copy context",
|
||||
"cutoff": "Cutoff",
|
||||
"developed_by": "Developed by",
|
||||
"dimension_type": "Dimension Type",
|
||||
"dimension": "Dimension",
|
||||
@@ -66,15 +69,14 @@
|
||||
"indentation.tabs": "Tabs",
|
||||
"item_modifier": "Item Modifier",
|
||||
"language": "Language",
|
||||
"layer": "Layer",
|
||||
"layer.biomes": "Biomes",
|
||||
"layer.temperature": "Temperature",
|
||||
"layer.humidity": "Humidity",
|
||||
"layer.continentalness": "Continentalness",
|
||||
"layer.vegetation": "Humidity",
|
||||
"layer.continents": "Continentalness",
|
||||
"layer.erosion": "Erosion",
|
||||
"layer.weirdness": "Weirdness",
|
||||
"layer.offset": "Offset",
|
||||
"layer.factor": "Factor",
|
||||
"layer.jaggedness": "Jaggedness",
|
||||
"layer.ridges": "Weirdness",
|
||||
"layer.depth": "Depth",
|
||||
"highlighting": "Highlighting",
|
||||
"loading": "Loading...",
|
||||
"loot_table": "Loot Table",
|
||||
|
||||
@@ -326,6 +326,11 @@ main > .controls {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.preview-controls,
|
||||
.secondary-controls {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.secondary-controls {
|
||||
margin-top: 40px;
|
||||
}
|
||||
@@ -414,6 +419,7 @@ main > .controls {
|
||||
outline: none;
|
||||
resize: none;
|
||||
position: static;
|
||||
opacity: 0.9;
|
||||
background-color: var(--background-2);
|
||||
border-top-left-radius: 6px;
|
||||
color: var(--text-1);
|
||||
@@ -463,12 +469,34 @@ main.has-preview {
|
||||
image-rendering: -webkit-crisp-edges;
|
||||
image-rendering: crisp-edges;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.popup-preview canvas,
|
||||
.popup-preview .pixelated {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
.full-preview,
|
||||
.full-preview > canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
canvas.preview-details {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
canvas.preview-details.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.popup-share {
|
||||
position: fixed;
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user