diff --git a/package-lock.json b/package-lock.json index c714692e..be343795 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "brace": "^0.11.1", "buffer": "^6.0.3", "comment-json": "^4.1.1", - "deepslate": "^0.15.8", + "deepslate": "^0.16.0-beta.2", "deepslate-1.18": "npm:deepslate@^0.9.0-beta.9", "deepslate-1.18.2": "npm:deepslate@^0.9.0-beta.13", "highlight.js": "^11.5.1", @@ -1985,9 +1985,9 @@ "dev": true }, "node_modules/deepslate": { - "version": "0.15.8", - "resolved": "https://registry.npmjs.org/deepslate/-/deepslate-0.15.8.tgz", - "integrity": "sha512-nXNnn1M5B7iqpTbTWDyURnozdFjA5OQnnRZ8GOwbgV8tj6i40yKFm42pFO1cK4D7brMVJMyROrmyPDkCjE4G/Q==", + "version": "0.16.0-beta.2", + "resolved": "https://registry.npmjs.org/deepslate/-/deepslate-0.16.0-beta.2.tgz", + "integrity": "sha512-eIlidj1IcfrdrXSfHuS/6vcnlEMeWwXUgSzQawfNvQ/sOAFl/hS8i0TcGbQkCoMLXh5cdH5SZPjQ4XqodeqDew==", "dependencies": { "gl-matrix": "^3.3.0", "md5": "^2.3.0", @@ -6752,9 +6752,9 @@ "dev": true }, "deepslate": { - "version": "0.15.8", - "resolved": "https://registry.npmjs.org/deepslate/-/deepslate-0.15.8.tgz", - "integrity": "sha512-nXNnn1M5B7iqpTbTWDyURnozdFjA5OQnnRZ8GOwbgV8tj6i40yKFm42pFO1cK4D7brMVJMyROrmyPDkCjE4G/Q==", + "version": "0.16.0-beta.2", + "resolved": "https://registry.npmjs.org/deepslate/-/deepslate-0.16.0-beta.2.tgz", + "integrity": "sha512-eIlidj1IcfrdrXSfHuS/6vcnlEMeWwXUgSzQawfNvQ/sOAFl/hS8i0TcGbQkCoMLXh5cdH5SZPjQ4XqodeqDew==", "requires": { "gl-matrix": "^3.3.0", "md5": "^2.3.0", diff --git a/package.json b/package.json index 30cd4582..7d5a9d9e 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "brace": "^0.11.1", "buffer": "^6.0.3", "comment-json": "^4.1.1", - "deepslate": "^0.15.8", + "deepslate": "^0.16.0-beta.2", "deepslate-1.18": "npm:deepslate@^0.9.0-beta.9", "deepslate-1.18.2": "npm:deepslate@^0.9.0-beta.13", "highlight.js": "^11.5.1", diff --git a/src/app/Analytics.ts b/src/app/Analytics.ts index a91715d2..33b28851 100644 --- a/src/app/Analytics.ts +++ b/src/app/Analytics.ts @@ -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' diff --git a/src/app/Store.ts b/src/app/Store.ts index 44e6e0df..20f356d9 100644 --- a/src/app/Store.ts +++ b/src/app/Store.ts @@ -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' diff --git a/src/app/Utils.ts b/src/app/Utils.ts index ce119830..17f60bdb 100644 --- a/src/app/Utils.ts +++ b/src/app/Utils.ts @@ -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(random: Random, entries: T[], getWeight: (e } return undefined } + +export function iterateWorld2D(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 + } +} diff --git a/src/app/components/ItemDisplay.tsx b/src/app/components/ItemDisplay.tsx index 8f564930..12c81424 100644 --- a/src/app/components/ItemDisplay.tsx +++ b/src/app/components/ItemDisplay.tsx @@ -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, diff --git a/src/app/components/generator/PreviewPanel.tsx b/src/app/components/generator/PreviewPanel.tsx index 35a4f04b..ca2c6479 100644 --- a/src/app/components/generator/PreviewPanel.tsx +++ b/src/app/components/generator/PreviewPanel.tsx @@ -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 + return } if (id === 'dimension' && model.get(new Path(['generator', 'type']))?.endsWith('noise')) { - const data = model.get(new Path(['generator', 'biome_source'])) - if (data) return + return } if (id === 'worldgen/density_function') { - const data = model.get(new Path([])) - if (data) return + return } if (id === 'worldgen/noise') { - const data = model.get(new Path([])) - if (data) return + return } - if (id === 'worldgen/noise_settings') { - const data = model.get(new Path([])) - if (data) return + if (id === 'worldgen/noise_settings' && checkVersion(version, '1.18')) { + return } if ((id === 'worldgen/placed_feature' || (id === 'worldgen/configured_feature' && checkVersion(version, '1.16', '1.17')))) { - const data = model.get(new Path([])) - if (data) return + return } return <> diff --git a/src/app/components/previews/BiomeSourcePreview.tsx b/src/app/components/previews/BiomeSourcePreview.tsx index ac9aa69b..63275888 100644 --- a/src/app/components/previews/BiomeSourcePreview.tsx +++ b/src/app/components/previews/BiomeSourcePreview.tsx @@ -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() + const [seed, setSeed] = useState(randomSeed()) + const [layer, setLayer] = useState('biomes') + const [yOffset, setYOffset] = useState(64) + const [focused, setFocused] = useState([]) + const [focused2, setFocused2] = useState([]) - 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() + const imageData = useRef() + const [colormap, setColormap] = useState(Store.getColormap() ?? 'viridis') + + const detailCanvas = useRef(null) + const detailCtx = useRef() + const detailImageData = useRef() + const detailTimeout = useRef() + + 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) &&
+ {focused2.map(s => )} +
}
- {focused && } - changeScale(scale * 2)} /> - changeScale(scale / 2)} /> + {focused.map(s => )} + {actualLayer !== 'biomes' && } {hasRandomness && <> - +
e.stopPropagation()}> {locale('y')}
+ {checkVersion(version, '1.19') && LAYERS.map(l => setLayer(l)} />)}
setSeed(randomSeed())} /> }
- {focused?.temperature !== undefined &&
- k !== 'biome') - .map(([k, v]) => `${k[0].toUpperCase()}: ${(v as number).toFixed(2)}`).join(' ')}/> -
} - +
+ + {hasRandomness && } +
} + +type Triple = [number, number, number] +type BiomeColors = Record +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 = { + '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], +} diff --git a/src/app/previews/Colormap.ts b/src/app/components/previews/Colormap.ts similarity index 97% rename from src/app/previews/Colormap.ts rename to src/app/components/previews/Colormap.ts index 4d668666..8b1780a1 100644 --- a/src/app/previews/Colormap.ts +++ b/src/app/components/previews/Colormap.ts @@ -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 diff --git a/src/app/components/previews/ColormapSelector.tsx b/src/app/components/previews/ColormapSelector.tsx index 10b34c54..f38972fe 100644 --- a/src/app/components/previews/ColormapSelector.tsx +++ b/src/app/components/previews/ColormapSelector.tsx @@ -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, diff --git a/src/app/previews/Decorator.ts b/src/app/components/previews/Decorator.ts similarity index 78% rename from src/app/previews/Decorator.ts rename to src/app/components/previews/Decorator.ts index 52d16f76..b18b8ed4 100644 --- a/src/app/previews/Decorator.ts +++ b/src/app/components/previews/Decorator.ts @@ -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) => { diff --git a/src/app/components/previews/DecoratorPreview.tsx b/src/app/components/previews/DecoratorPreview.tsx index df6ab2d1..9b038e0d 100644 --- a/src/app/components/previews/DecoratorPreview.tsx +++ b/src/app/components/previews/DecoratorPreview.tsx @@ -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(), + } + }, [state, version, seed]) + + const ctx = useRef() + const imageData = useRef() + const [focused, setFocused] = useState([]) + + 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 <>
- setScale(Math.min(16, scale + 1))} /> - setScale(Math.max(1, scale - 1))} /> + {focused.map(s => )} setSeed(randomSeed())} />
- +
+ +
} diff --git a/src/app/previews/Deepslate.ts b/src/app/components/previews/Deepslate.ts similarity index 85% rename from src/app/previews/Deepslate.ts rename to src/app/components/previews/Deepslate.ts index 9f263d70..98c456df 100644 --- a/src/app/previews/Deepslate.ts +++ b/src/app/components/previews/Deepslate.ts @@ -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> @@ -18,10 +18,10 @@ export class Deepslate { private loadingPromise: Promise | undefined private readonly deepslateCache = new Map() 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() - 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 diff --git a/src/app/components/previews/DensityFunctionPreview.tsx b/src/app/components/previews/DensityFunctionPreview.tsx index 49c855a6..55e695a9 100644 --- a/src/app/components/previews/DensityFunctionPreview.tsx +++ b/src/app/components/previews/DensityFunctionPreview.tsx @@ -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() + const ctx = useRef() const [focused, setFocused] = useState([]) const [colormap, setColormap] = useState(Store.getColormap() ?? 'viridis') - const offset = useRef(0) - const scrollInterval = useRef(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(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 <>
{focused.map(s => )} - - - setAutoScroll(!autoScroll)} /> - + {voxelMode ? <> + +
e.stopPropagation()}> + {locale('cutoff')} + +
+
+ : <> + + } + setVoxelMode(!voxelMode)} /> setSeed(randomSeed())} />
- +
{voxelMode + ? + : + }
} diff --git a/src/app/components/previews/InteractiveCanvas2D.tsx b/src/app/components/previews/InteractiveCanvas2D.tsx new file mode 100644 index 00000000..b2626965 --- /dev/null +++ b/src/app/components/previews/InteractiveCanvas2D.tsx @@ -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(null) + const dragStart = useRef<[number, number] | undefined>() + const dragButton = useRef() + const centerPos = useRef<[number, number]>(startPosition ? [-startPosition[0], -startPosition[1]] : [0, 0]) + const viewScale = useRef(startScale ?? 1) + const frameRequest = useRef() + + // 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(() => {}) + 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 +} diff --git a/src/app/components/previews/InteractiveCanvas3D.tsx b/src/app/components/previews/InteractiveCanvas3D.tsx new file mode 100644 index 00000000..b3639a5c --- /dev/null +++ b/src/app/components/previews/InteractiveCanvas3D.tsx @@ -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(null) + const dragStart = useRef<[number, number] | undefined>() + const dragButton = useRef() + 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() + + const redraw = useRef(() => {}) + 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 +} diff --git a/src/app/previews/LootTable.ts b/src/app/components/previews/LootTable.ts similarity index 99% rename from src/app/previews/LootTable.ts rename to src/app/components/previews/LootTable.ts index 91316447..3ed6bb99 100644 --- a/src/app/previews/LootTable.ts +++ b/src/app/components/previews/LootTable.ts @@ -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, diff --git a/src/app/components/previews/LootTablePreview.tsx b/src/app/components/previews/LootTablePreview.tsx index d91f89d7..affe3f9c 100644 --- a/src/app/components/previews/LootTablePreview.tsx +++ b/src/app/components/previews/LootTablePreview.tsx @@ -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() diff --git a/src/app/components/previews/NoisePreview.tsx b/src/app/components/previews/NoisePreview.tsx index ccebc22d..097d83f2 100644 --- a/src/app/components/previews/NoisePreview.tsx +++ b/src/app/components/previews/NoisePreview.tsx @@ -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() + const ctx = useRef() const [focused, setFocused] = useState([]) const [colormap, setColormap] = useState(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 <>
{focused.map(s => )} - changeScale(scale * 1.5)} /> - changeScale(scale / 1.5)} /> setSeed(randomSeed())} />
- +
+ +
} diff --git a/src/app/components/previews/NoiseSettingsPreview.tsx b/src/app/components/previews/NoiseSettingsPreview.tsx index 490a6519..413240a2 100644 --- a/src/app/components/previews/NoiseSettingsPreview.tsx +++ b/src/app/components/previews/NoiseSettingsPreview.tsx @@ -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([]) 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() + const ctx = useRef() + const [focused, setFocused] = useState([]) const [colormap, setColormap] = useState(Store.getColormap() ?? 'viridis') - const offset = useRef(0) - const scrollInterval = useRef(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 => )} {layer === 'final_density' && } - {checkVersion(version, undefined, '1.17') ? <> - setBiomeScale(Number(v))} /> - setBiomeDepth(Number(v))} /> - : - - } - setAutoScroll(!autoScroll)} /> + setLayer(layer === 'final_density' ? 'terrain' : 'final_density')} /> setSeed(randomSeed())} /> - +
+ +
} + +const BlockColors: Record = { + '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], +} diff --git a/src/app/previews/colormaps.json b/src/app/components/previews/colormaps.json similarity index 100% rename from src/app/previews/colormaps.json rename to src/app/components/previews/colormaps.json diff --git a/src/app/contexts/Project.tsx b/src/app/contexts/Project.tsx index 4a6575f1..b19c9119 100644 --- a/src/app/contexts/Project.tsx +++ b/src/app/contexts/Project.tsx @@ -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] + })) +} diff --git a/src/app/hooks/index.ts b/src/app/hooks/index.ts index 4fef10d8..ce5d2c4b 100644 --- a/src/app/hooks/index.ts +++ b/src/app/hooks/index.ts @@ -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' diff --git a/src/app/hooks/useCanvas.ts b/src/app/hooks/useCanvas.ts deleted file mode 100644 index 24d2a7d9..00000000 --- a/src/app/hooks/useCanvas.ts +++ /dev/null @@ -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, - onDrag?: (dx: number, dy: number) => Promise, - onHover?: (x: number, y: number) => unknown, - onLeave?: () => unknown, -}, inputs?: Inputs) { - const canvas = useRef(null) - - const dragStart = useRef() - const dragRequest = useRef() - const dragPending = useRef([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>() - 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, - } -} diff --git a/src/app/previews/BiomeSource.ts b/src/app/previews/BiomeSource.ts deleted file mode 100644 index 3e401649..00000000 --- a/src/app/previews/BiomeSource.ts +++ /dev/null @@ -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 -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 = { - '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], -} diff --git a/src/app/previews/NoiseSettings.ts b/src/app/previews/NoiseSettings.ts deleted file mode 100644 index 027db613..00000000 --- a/src/app/previews/NoiseSettings.ts +++ /dev/null @@ -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 = { - '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 -} diff --git a/src/app/previews/NormalNoise.ts b/src/app/previews/NormalNoise.ts deleted file mode 100644 index 4b4d26d5..00000000 --- a/src/app/previews/NormalNoise.ts +++ /dev/null @@ -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) -} diff --git a/src/app/previews/index.ts b/src/app/previews/index.ts deleted file mode 100644 index aee2a479..00000000 --- a/src/app/previews/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './BiomeSource.js' -export * from './Decorator.js' -export * from './NoiseSettings.js' -export * from './NormalNoise.js' diff --git a/src/app/previews/noise/NoiseChunkGenerator.ts b/src/app/previews/noise/NoiseChunkGenerator.ts deleted file mode 100644 index 5ab20572..00000000 --- a/src/app/previews/noise/NoiseChunkGenerator.ts +++ /dev/null @@ -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) - } -} diff --git a/src/app/schema/renderHtml.tsx b/src/app/schema/renderHtml.tsx index f3bd1b6d..4f69d92d 100644 --- a/src/app/schema/renderHtml.tsx +++ b/src/app/schema/renderHtml.tsx @@ -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' diff --git a/src/locales/en.json b/src/locales/en.json index 6e90921f..7afd6ebc 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -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", diff --git a/src/styles/global.css b/src/styles/global.css index d25d376d..a4ec179c 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -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;