From 0a72ddd632944d90062602992ca743e27c7c52df Mon Sep 17 00:00:00 2001 From: Misode Date: Thu, 26 Jan 2023 16:41:09 +0100 Subject: [PATCH] Add blockstate and model previews --- src/app/components/generator/PreviewPanel.tsx | 13 +++- .../components/previews/BlockStatePreview.tsx | 51 +++++++++++++ .../previews/InteractiveCanvas3D.tsx | 1 - src/app/components/previews/ModelPreview.tsx | 57 ++++++++++++++ src/app/components/previews/index.ts | 3 + src/app/services/DataFetcher.ts | 5 +- src/app/services/Resources.ts | 76 +++++++++++++++++-- 7 files changed, 193 insertions(+), 13 deletions(-) create mode 100644 src/app/components/previews/BlockStatePreview.tsx create mode 100644 src/app/components/previews/ModelPreview.tsx diff --git a/src/app/components/generator/PreviewPanel.tsx b/src/app/components/generator/PreviewPanel.tsx index ca2c6479..cb21905c 100644 --- a/src/app/components/generator/PreviewPanel.tsx +++ b/src/app/components/generator/PreviewPanel.tsx @@ -4,10 +4,9 @@ import { useState } from 'preact/hooks' import { useModel } from '../../hooks/index.js' import type { VersionId } from '../../services/index.js' import { checkVersion } from '../../services/index.js' -import { BiomeSourcePreview, DecoratorPreview, DensityFunctionPreview, NoisePreview, NoiseSettingsPreview } from '../previews/index.js' -import { LootTablePreview } from '../previews/LootTablePreview.jsx' +import { BiomeSourcePreview, BlockStatePreview, DecoratorPreview, DensityFunctionPreview, LootTablePreview, ModelPreview, NoisePreview, NoiseSettingsPreview } from '../previews/index.js' -export const HasPreview = ['loot_table', 'dimension', 'worldgen/density_function', 'worldgen/noise', 'worldgen/noise_settings', 'worldgen/configured_feature', 'worldgen/placed_feature'] +export const HasPreview = ['loot_table', 'dimension', 'worldgen/density_function', 'worldgen/noise', 'worldgen/noise_settings', 'worldgen/configured_feature', 'worldgen/placed_feature', 'block_definition', 'model'] type PreviewPanelProps = { model: DataModel | undefined, @@ -51,5 +50,13 @@ export function PreviewPanel({ model, version, id, shown }: PreviewPanelProps) { return } + if (id === 'block_definition') { + return + } + + if (id === 'model') { + return + } + return <> } diff --git a/src/app/components/previews/BlockStatePreview.tsx b/src/app/components/previews/BlockStatePreview.tsx new file mode 100644 index 00000000..7066b981 --- /dev/null +++ b/src/app/components/previews/BlockStatePreview.tsx @@ -0,0 +1,51 @@ +import { DataModel } from '@mcschema/core' +import { BlockDefinition, Identifier, Structure, StructureRenderer } from 'deepslate/render' +import type { mat4 } from 'gl-matrix' +import { useCallback, useRef } from 'preact/hooks' +import { useVersion } from '../../contexts/index.js' +import { useAsync } from '../../hooks/useAsync.js' +import { AsyncCancel } from '../../hooks/useAsyncFn.js' +import { getResources, ResourceWrapper } from '../../services/Resources.js' +import type { PreviewProps } from './index.js' +import { InteractiveCanvas3D } from './InteractiveCanvas3D.jsx' + +const PREVIEW_ID = Identifier.parse('misode:preview') + +export const BlockStatePreview = ({ data, shown }: PreviewProps) => { + const { version } = useVersion() + const serializedData = JSON.stringify(data) + + const { value: resources } = useAsync(async () => { + if (!shown) return AsyncCancel + const resources = await getResources(version) + const definition = BlockDefinition.fromJson(PREVIEW_ID.toString(), DataModel.unwrapLists(data)) + const wrapper = new ResourceWrapper(resources, { + getBlockDefinition(id) { + if (id.equals(PREVIEW_ID)) return definition + return null + }, + }) + return wrapper + }, [shown, version, serializedData]) + + const renderer = useRef(undefined) + + const onSetup = useCallback((canvas: HTMLCanvasElement) => { + if (!resources || !shown) return + const gl = canvas.getContext('webgl') + if (!gl) return + renderer.current = new StructureRenderer(gl, new Structure([1, 1, 1]).addBlock([0, 0, 0], PREVIEW_ID), resources) + }, [resources, shown]) + const onResize = useCallback((width: number, height: number) => { + renderer.current?.setViewport(0, 0, width, height) + }, [resources]) + const onDraw = useCallback((transform: mat4) => { + renderer.current?.drawStructure(transform) + }, []) + + return <> +
+ +
+ +} diff --git a/src/app/components/previews/InteractiveCanvas3D.tsx b/src/app/components/previews/InteractiveCanvas3D.tsx index b3639a5c..ad545be8 100644 --- a/src/app/components/previews/InteractiveCanvas3D.tsx +++ b/src/app/components/previews/InteractiveCanvas3D.tsx @@ -100,7 +100,6 @@ export function InteractiveCanvas3D({ onSetup, onDraw, onResize, state, startPos redraw.current() } - console.warn('Uhhh...', canvas.current) onSetup(canvas.current) resizeHandler() diff --git a/src/app/components/previews/ModelPreview.tsx b/src/app/components/previews/ModelPreview.tsx new file mode 100644 index 00000000..d6baef21 --- /dev/null +++ b/src/app/components/previews/ModelPreview.tsx @@ -0,0 +1,57 @@ +import { DataModel } from '@mcschema/core' +import { BlockDefinition, BlockModel, Identifier, Structure, StructureRenderer } from 'deepslate/render' +import type { mat4 } from 'gl-matrix' +import { useCallback, useRef } from 'preact/hooks' +import { useVersion } from '../../contexts/index.js' +import { useAsync } from '../../hooks/useAsync.js' +import { AsyncCancel } from '../../hooks/useAsyncFn.js' +import { getResources, ResourceWrapper } from '../../services/Resources.js' +import type { PreviewProps } from './index.js' +import { InteractiveCanvas3D } from './InteractiveCanvas3D.jsx' + +const PREVIEW_ID = Identifier.parse('misode:preview') +const PREVIEW_DEFINITION = new BlockDefinition(PREVIEW_ID, { '': { model: PREVIEW_ID.toString() }}, undefined) + +export const ModelPreview = ({ data, shown }: PreviewProps) => { + const { version } = useVersion() + const serializedData = JSON.stringify(data) + + const { value: resources } = useAsync(async () => { + if (!shown) return AsyncCancel + const resources = await getResources(version) + const model = BlockModel.fromJson(PREVIEW_ID.toString(), DataModel.unwrapLists(data)) + model.flatten(resources) + const wrapper = new ResourceWrapper(resources, { + getBlockDefinition(id) { + if (id.equals(PREVIEW_ID)) return PREVIEW_DEFINITION + return null + }, + getBlockModel(id) { + if (id.equals(PREVIEW_ID)) return model + return null + }, + }) + return wrapper + }, [shown, version, serializedData]) + + const renderer = useRef(undefined) + + const onSetup = useCallback((canvas: HTMLCanvasElement) => { + if (!resources || !shown) return + const gl = canvas.getContext('webgl') + if (!gl) return + renderer.current = new StructureRenderer(gl, new Structure([1, 1, 1]).addBlock([0, 0, 0], PREVIEW_ID), resources) + }, [resources, shown]) + const onResize = useCallback((width: number, height: number) => { + renderer.current?.setViewport(0, 0, width, height) + }, [resources]) + const onDraw = useCallback((transform: mat4) => { + renderer.current?.drawStructure(transform) + }, []) + + return <> +
+ +
+ +} diff --git a/src/app/components/previews/index.ts b/src/app/components/previews/index.ts index c7b075dd..82ad9d42 100644 --- a/src/app/components/previews/index.ts +++ b/src/app/components/previews/index.ts @@ -2,8 +2,11 @@ import type { DataModel } from '@mcschema/core' import type { VersionId } from '../../services/index.js' export * from './BiomeSourcePreview.js' +export * from './BlockStatePreview.jsx' export * from './DecoratorPreview.js' export * from './DensityFunctionPreview.js' +export * from './LootTablePreview.jsx' +export * from './ModelPreview.jsx' export * from './NoisePreview.js' export * from './NoiseSettingsPreview.js' diff --git a/src/app/services/DataFetcher.ts b/src/app/services/DataFetcher.ts index 8d062f9a..3fbadea9 100644 --- a/src/app/services/DataFetcher.ts +++ b/src/app/services/DataFetcher.ts @@ -167,12 +167,13 @@ export async function fetchResources(versionId: VersionId) { const version = config.versions.find(v => v.id === versionId)! await validateCache(version) try { - const [models, uvMapping, atlas] = await Promise.all([ + const [blockDefinitions, models, uvMapping, atlas] = await Promise.all([ + fetchAllPresets(versionId, 'block_definition'), fetchAllPresets(versionId, 'model'), fetch(`${mcmeta(version, 'atlas')}/all/data.min.json`).then(r => r.json()), loadImage(`${mcmeta(version, 'atlas')}/all/atlas.png`), ]) - return { models, uvMapping, atlas } + return { blockDefinitions, models, uvMapping, atlas } } catch (e) { throw new Error(`Error occured while fetching resources: ${message(e)}`) } diff --git a/src/app/services/Resources.ts b/src/app/services/Resources.ts index 5618effe..8caf0c64 100644 --- a/src/app/services/Resources.ts +++ b/src/app/services/Resources.ts @@ -1,5 +1,5 @@ -import type { BlockModelProvider, ItemStack, TextureAtlasProvider, UV } from 'deepslate/render' -import { BlockModel, Identifier, ItemRenderer, TextureAtlas, upperPowerOfTwo } from 'deepslate/render' +import type { BlockDefinitionProvider, BlockFlagsProvider, BlockModelProvider, BlockPropertiesProvider, ItemStack, TextureAtlasProvider, UV } from 'deepslate/render' +import { BlockDefinition, BlockModel, Identifier, ItemRenderer, TextureAtlas, upperPowerOfTwo } from 'deepslate/render' import { message } from '../Utils.js' import { fetchLanguage, fetchResources } from './DataFetcher.js' import type { VersionId } from './Schemas.js' @@ -10,8 +10,8 @@ export async function getResources(version: VersionId) { if (!Resources[version]) { Resources[version] = (async () => { try { - const { models, uvMapping, atlas} = await fetchResources(version) - Resources[version] = new ResourceManager(models, uvMapping, atlas) + const { blockDefinitions, models, uvMapping, atlas} = await fetchResources(version) + Resources[version] = new ResourceManager(blockDefinitions, models, uvMapping, atlas) return Resources[version] } catch (e) { console.error('Error: ', e) @@ -51,17 +51,26 @@ export async function renderItem(version: VersionId, item: ItemStack) { return promise } -export class ResourceManager implements BlockModelProvider, TextureAtlasProvider { - private blockModels: { [id: string]: BlockModel } +interface Resources extends BlockDefinitionProvider, BlockModelProvider, TextureAtlasProvider, BlockFlagsProvider, BlockPropertiesProvider {} + +export class ResourceManager implements Resources { + private readonly blockDefinitions: { [id: string]: BlockDefinition } + private readonly blockModels: { [id: string]: BlockModel } private textureAtlas: TextureAtlas - constructor(models: Map, uvMapping: any, textureAtlas: HTMLImageElement) { + constructor(blockDefinitions: Map, models: Map, uvMapping: any, textureAtlas: HTMLImageElement) { + this.blockDefinitions = {} this.blockModels = {} this.textureAtlas = TextureAtlas.empty() + this.loadBlockDefinitions(blockDefinitions) this.loadBlockModels(models) this.loadBlockAtlas(textureAtlas, uvMapping) } + public getBlockDefinition(id: Identifier) { + return this.blockDefinitions[id.toString()] + } + public getBlockModel(id: Identifier) { return this.blockModels[id.toString()] } @@ -74,6 +83,18 @@ export class ResourceManager implements BlockModelProvider, TextureAtlasProvider return this.textureAtlas.getTextureAtlas() } + public getBlockFlags() { + return { opaque: false } + } + + public getBlockProperties() { + return null + } + + public getDefaultBlockProperties() { + return null + } + private loadBlockModels(models: Map) { [...models.entries()].forEach(([id, model]) => { this.blockModels[Identifier.create(id).toString()] = BlockModel.fromJson(id, model) @@ -81,6 +102,12 @@ export class ResourceManager implements BlockModelProvider, TextureAtlasProvider Object.values(this.blockModels).forEach(m => m.flatten(this)) } + private loadBlockDefinitions(definitions: Map) { + [...definitions.entries()].forEach(([id, definition]) => { + this.blockDefinitions[Identifier.create(id).toString()] = BlockDefinition.fromJson(id, definition) + }) + } + private loadBlockAtlas(image: HTMLImageElement, textures: any) { const atlasCanvas = document.createElement('canvas') const w = upperPowerOfTwo(image.width) @@ -101,6 +128,41 @@ export class ResourceManager implements BlockModelProvider, TextureAtlasProvider } } +export class ResourceWrapper implements Resources { + constructor( + private readonly wrapped: Resources, + private readonly overrides: Partial, + ) {} + + public getBlockDefinition(id: Identifier) { + return this.overrides.getBlockDefinition?.(id) ?? this.wrapped.getBlockDefinition(id) + } + + public getBlockModel(id: Identifier) { + return this.overrides.getBlockModel?.(id) ?? this.wrapped.getBlockModel(id) + } + + public getTextureUV(texture: Identifier) { + return this.overrides.getTextureUV?.(texture) ?? this.wrapped.getTextureUV(texture) + } + + public getTextureAtlas() { + return this.overrides.getTextureAtlas?.() ?? this.wrapped.getTextureAtlas() + } + + public getBlockFlags(id: Identifier) { + return this.overrides.getBlockFlags?.(id) ?? this.wrapped.getBlockFlags(id) + } + + public getBlockProperties(id: Identifier) { + return this.overrides.getBlockProperties?.(id) ?? this.wrapped.getBlockProperties(id) + } + + public getDefaultBlockProperties(id: Identifier) { + return this.overrides.getDefaultBlockProperties?.(id) ?? this.wrapped.getDefaultBlockProperties(id) + } +} + const Languages: Record | Promise>> = {} export async function getLanguage(version: VersionId) {