mirror of
https://github.com/misode/misode.github.io.git
synced 2026-04-23 15:17:09 +00:00
Improve item display (#283)
* Refactor item display to separate component * Load assets and render item models * Cache rendered items
This commit is contained in:
@@ -297,12 +297,12 @@ export class BiMap<A, B> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function readZip(file: File): Promise<[string, string][]> {
|
||||
const buffer = await file.arrayBuffer()
|
||||
export async function readZip(file: File | ArrayBuffer, predicate: (name: string) => boolean = () => true): Promise<[string, string][]> {
|
||||
const buffer = file instanceof File ? await file.arrayBuffer() : file
|
||||
const reader = new zip.ZipReader(new zip.BlobReader(new Blob([buffer])))
|
||||
const entries = await reader.getEntries()
|
||||
return await Promise.all(entries
|
||||
.filter(e => !e.directory)
|
||||
.filter(e => !e.directory && predicate(e.filename))
|
||||
.map(async e => {
|
||||
const writer = new zip.TextWriter('utf-8')
|
||||
return [e.filename, await e.getData?.(writer)] as [string, string]
|
||||
|
||||
58
src/app/components/ItemDisplay.tsx
Normal file
58
src/app/components/ItemDisplay.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useState } from 'preact/hooks'
|
||||
import { useVersion } from '../contexts/Version.jsx'
|
||||
import { useAsync } from '../hooks/useAsync.js'
|
||||
import { getAssetUrl } from '../services/DataFetcher.js'
|
||||
import { renderItem } from '../services/Resources.js'
|
||||
import { getCollections } from '../services/Schemas.js'
|
||||
import { Octicon } from './Octicon.jsx'
|
||||
|
||||
interface Props {
|
||||
item: string,
|
||||
}
|
||||
export function ItemDisplay({ item }: Props) {
|
||||
const { version } = useVersion()
|
||||
const [errored, setErrored] = useState(false)
|
||||
|
||||
if (errored || (item.includes(':') && !item.startsWith('minecraft:'))) {
|
||||
return <div class="item-display">
|
||||
{Octicon.package}
|
||||
</div>
|
||||
}
|
||||
|
||||
const { value: collections } = useAsync(() => getCollections(version), [])
|
||||
|
||||
if (collections === undefined) {
|
||||
return <div class="item-display"></div>
|
||||
}
|
||||
|
||||
const texturePath = `item/${item.replace(/^minecraft:/, '')}`
|
||||
if (collections.get('texture').includes('minecraft:' + texturePath)) {
|
||||
return <div class="item-display">
|
||||
<img src={getAssetUrl(version, 'textures', texturePath)} alt="" onError={() => setErrored(true)} />
|
||||
</div>
|
||||
}
|
||||
|
||||
const modelPath = `block/${item.replace(/^minecraft:/, '')}`
|
||||
if (collections.get('model').includes('minecraft:' + modelPath)) {
|
||||
return <div class="item-display">
|
||||
<RenderedItem item={item} />
|
||||
</div>
|
||||
}
|
||||
|
||||
return <div class="item-display">
|
||||
{Octicon.package}
|
||||
</div>
|
||||
}
|
||||
|
||||
function RenderedItem({ item }: Props) {
|
||||
const { version } = useVersion()
|
||||
const { value: src } = useAsync(() => renderItem(version, item), [version, item])
|
||||
|
||||
if (src) {
|
||||
return <img src={src} alt={item} />
|
||||
}
|
||||
|
||||
return <div class="item-display">
|
||||
{Octicon.package}
|
||||
</div>
|
||||
}
|
||||
@@ -4,12 +4,13 @@ import type { ComponentChildren, JSX } from 'preact'
|
||||
import { memo } from 'preact/compat'
|
||||
import { useState } from 'preact/hooks'
|
||||
import { Btn, Octicon } from '../components/index.js'
|
||||
import { ItemDisplay } from '../components/ItemDisplay.jsx'
|
||||
import config from '../Config.js'
|
||||
import { localize, 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 { CachedCollections, CachedDecorator, CachedFeature, getTextureUrl } from '../services/index.js'
|
||||
import { CachedDecorator, CachedFeature } from '../services/index.js'
|
||||
import { deepClone, deepEqual, generateUUID, hexId, hexToRgb, isObject, newSeed, rgbToHex, stringToColor } from '../Utils.js'
|
||||
import { ModelWrapper } from './ModelWrapper.js'
|
||||
|
||||
@@ -22,8 +23,6 @@ const fixedLists = ['generator_biome.parameters.temperature', 'generator_biome.p
|
||||
const collapsedFields = ['noise_settings.surface_rule', 'noise_settings.noise.terrain_shaper']
|
||||
const collapsableFields = ['density_function.argument', 'density_function.argument1', 'density_function.argument2', 'density_function.input', 'density_function.when_in_range', 'density_function.when_out_of_range']
|
||||
|
||||
const PACKAGE = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M8.878.392a1.75 1.75 0 00-1.756 0l-5.25 3.045A1.75 1.75 0 001 4.951v6.098c0 .624.332 1.2.872 1.514l5.25 3.045a1.75 1.75 0 001.756 0l5.25-3.045c.54-.313.872-.89.872-1.514V4.951c0-.624-.332-1.2-.872-1.514L8.878.392zM7.875 1.69a.25.25 0 01.25 0l4.63 2.685L8 7.133 3.245 4.375l4.63-2.685zM2.5 5.677v5.372c0 .09.047.171.125.216l4.625 2.683V8.432L2.5 5.677zm6.25 8.271l4.625-2.683a.25.25 0 00.125-.216V5.677L8.75 8.432v5.516z"></path></svg>'
|
||||
|
||||
const findGenerator = (id: string) => {
|
||||
return config.generators.find(g => g.id === id.replace(/^\$/, ''))
|
||||
}
|
||||
@@ -140,12 +139,7 @@ const renderHtml: RenderHook = {
|
||||
let label: undefined | string | JSX.Element
|
||||
if (['loot_pool.entries.entry', 'loot_entry.alternatives.children.entry', 'loot_entry.group.children.entry', 'loot_entry.sequence.children.entry', 'function.set_contents.entries.entry'].includes(cPath.getContext().join('.'))) {
|
||||
if (isObject(cValue) && typeof cValue.type === 'string' && cValue.type.replace(/^minecraft:/, '') === 'item' && typeof cValue.name === 'string') {
|
||||
const texturePath = `item/${cValue.name.replace(/^minecraft:/, '')}`
|
||||
if (CachedCollections.get('texture').includes('minecraft:' + texturePath)) {
|
||||
label = <img src={getTextureUrl(version, texturePath)} alt="" onError={e => e.currentTarget.outerHTML = PACKAGE} />
|
||||
} else {
|
||||
label = Octicon.package
|
||||
}
|
||||
label = <ItemDisplay item={cValue.name} />
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,24 +16,25 @@ type Version = {
|
||||
declare var __LATEST_VERSION__: string
|
||||
const latestVersion = __LATEST_VERSION__ ?? ''
|
||||
const mcmetaUrl = 'https://raw.githubusercontent.com/misode/mcmeta'
|
||||
const mcmetaTarballUrl = 'https://github.com/misode/mcmeta/tarball'
|
||||
const changesUrl = 'https://raw.githubusercontent.com/misode/technical-changes'
|
||||
|
||||
type McmetaTypes = 'summary' | 'data' | 'assets' | 'registries'
|
||||
type McmetaTypes = 'summary' | 'data' | 'data-json' | 'assets' | 'assets-json' | 'registries' | 'atlas'
|
||||
|
||||
interface RefInfo {
|
||||
dynamic?: boolean
|
||||
ref?: string
|
||||
}
|
||||
|
||||
function mcmeta(version: RefInfo, type: McmetaTypes) {
|
||||
return `${mcmetaUrl}/${version.dynamic ? type : `${version.ref}-${type}`}`
|
||||
function mcmeta(version: RefInfo, type: McmetaTypes, tarball?: boolean) {
|
||||
return `${tarball ? mcmetaTarballUrl : mcmetaUrl}/${version.dynamic ? type : `${version.ref}-${type}`}`
|
||||
}
|
||||
|
||||
async function validateCache(version: RefInfo) {
|
||||
await applyPatches()
|
||||
if (version.dynamic) {
|
||||
if (localStorage.getItem(CACHE_LATEST_VERSION) !== latestVersion) {
|
||||
await deleteMatching(url => url.startsWith(`${mcmetaUrl}/summary/`) || url.startsWith(`${mcmetaUrl}/data/`) || url.startsWith(`${mcmetaUrl}/assets/`) || url.startsWith(`${mcmetaUrl}/registries/`))
|
||||
await deleteMatching(url => url.startsWith(`${mcmetaUrl}/summary/`) || url.startsWith(`${mcmetaUrl}/data/`) || url.startsWith(`${mcmetaUrl}/assets/`) || url.startsWith(`${mcmetaUrl}/registries/`) || url.startsWith(`${mcmetaUrl}/atlas/`) || url.startsWith(`${mcmetaTarballUrl}/assets-json/`))
|
||||
localStorage.setItem(CACHE_LATEST_VERSION, latestVersion)
|
||||
}
|
||||
version.ref = latestVersion
|
||||
@@ -105,11 +106,8 @@ export async function fetchAllPresets(versionId: VersionId, registry: string) {
|
||||
const version = config.versions.find(v => v.id === versionId)!
|
||||
await validateCache(version)
|
||||
try {
|
||||
const entries = await cachedFetch<any>(`${mcmeta(version, 'registries')}/${registry}/data.min.json`)
|
||||
return new Map<string, unknown>(await Promise.all(
|
||||
entries.map(async (e: string) =>
|
||||
[e, await cachedFetch(`${mcmeta(version, 'data')}/data/minecraft/${registry}/${e}.json`)])
|
||||
))
|
||||
const type = ['block_definition', 'model', 'font'].includes(registry) ? 'assets' : 'data'
|
||||
return new Map<string, unknown>(Object.entries(await cachedFetch(`${mcmeta(version, 'summary')}/${type}/${registry}/data.min.json`)))
|
||||
} catch (e) {
|
||||
throw new Error(`Error occurred while fetching all ${registry} presets: ${message(e)}`)
|
||||
}
|
||||
@@ -159,11 +157,53 @@ export async function fetchVersions(): Promise<VersionMeta[]> {
|
||||
}
|
||||
}
|
||||
|
||||
export function getTextureUrl(versionId: VersionId, path: string): string {
|
||||
export function getAssetUrl(versionId: VersionId, type: string, path: string): string {
|
||||
const version = config.versions.find(v => v.id === versionId)!
|
||||
return `${mcmeta(version, 'assets')}/assets/minecraft/textures/${path}.png`
|
||||
return `${mcmeta(version, 'assets')}/assets/minecraft/${type}/${path}.png`
|
||||
}
|
||||
|
||||
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([
|
||||
fetchAllPresets(versionId, 'model'),
|
||||
cachedFetch(`${mcmeta(version, 'atlas')}/all/data.min.json`),
|
||||
loadImage(`${mcmeta(version, 'atlas')}/all/atlas.png`),
|
||||
])
|
||||
return { models, uvMapping, atlas }
|
||||
} catch (e) {
|
||||
throw new Error(`Error occured while fetching resources: ${message(e)}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadImage(src: string) {
|
||||
return new Promise<HTMLImageElement>(res => {
|
||||
const image = new Image()
|
||||
image.onload = () => res(image)
|
||||
image.crossOrigin = 'Anonymous'
|
||||
image.src = src
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
async function loadImage(src: string) {
|
||||
const buffer = await cachedFetch(src, { decode: r => r.arrayBuffer() })
|
||||
const blob = new Blob([buffer], { type: 'image/png' })
|
||||
const img = new Image()
|
||||
img.src = URL.createObjectURL(blob)
|
||||
return new Promise<ImageData>((res) => {
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')!
|
||||
ctx.drawImage(img, 0, 0)
|
||||
const imgData = ctx.getImageData(0, 0, img.width, img.height)
|
||||
res(imgData)
|
||||
}
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
export interface Change {
|
||||
group: string,
|
||||
version: string,
|
||||
|
||||
101
src/app/services/Resources.ts
Normal file
101
src/app/services/Resources.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { BlockModelProvider, TextureAtlasProvider, UV } from 'deepslate/render'
|
||||
import { BlockModel, Identifier, ItemRenderer, TextureAtlas, upperPowerOfTwo } from 'deepslate/render'
|
||||
import { message } from '../Utils.js'
|
||||
import { fetchResources } from './DataFetcher.js'
|
||||
import type { VersionId } from './Schemas.js'
|
||||
|
||||
const Resources: Record<string, ResourceManager | Promise<ResourceManager>> = {}
|
||||
|
||||
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)
|
||||
return Resources[version]
|
||||
} catch (e) {
|
||||
console.error('Error: ', e)
|
||||
throw new Error(`Cannot get resources for version ${version}: ${message(e)}`)
|
||||
}
|
||||
})()
|
||||
return Resources[version]
|
||||
}
|
||||
return Resources[version]
|
||||
}
|
||||
|
||||
const RENDER_SIZE = 128
|
||||
const ItemRenderCache = new Map<string, Promise<string>>()
|
||||
|
||||
export async function renderItem(version: VersionId, item: string) {
|
||||
const cache_key = `${version} ${item}`
|
||||
const cached = ItemRenderCache.get(cache_key)
|
||||
if (cached !== undefined) {
|
||||
return cached
|
||||
}
|
||||
|
||||
const promise = (async () => {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = RENDER_SIZE
|
||||
canvas.height = RENDER_SIZE
|
||||
const resources = await getResources(version)
|
||||
const gl = canvas.getContext('webgl2', { preserveDrawingBuffer: true })
|
||||
if (!gl) {
|
||||
throw new Error('Cannot get WebGL2 context')
|
||||
}
|
||||
const renderer = new ItemRenderer(gl, Identifier.parse(item), resources)
|
||||
renderer.drawItem()
|
||||
return canvas.toDataURL()
|
||||
})()
|
||||
ItemRenderCache.set(cache_key, promise)
|
||||
return promise
|
||||
}
|
||||
|
||||
export class ResourceManager implements BlockModelProvider, TextureAtlasProvider {
|
||||
private blockModels: { [id: string]: BlockModel }
|
||||
private textureAtlas: TextureAtlas
|
||||
|
||||
constructor(models: Map<string, unknown>, uvMapping: any, textureAtlas: HTMLImageElement) {
|
||||
this.blockModels = {}
|
||||
this.textureAtlas = TextureAtlas.empty()
|
||||
this.loadBlockModels(models)
|
||||
this.loadBlockAtlas(textureAtlas, uvMapping)
|
||||
}
|
||||
|
||||
public getBlockModel(id: Identifier) {
|
||||
return this.blockModels[id.toString()]
|
||||
}
|
||||
|
||||
public getTextureUV(id: Identifier) {
|
||||
return this.textureAtlas.getTextureUV(id)
|
||||
}
|
||||
|
||||
public getTextureAtlas() {
|
||||
return this.textureAtlas.getTextureAtlas()
|
||||
}
|
||||
|
||||
private loadBlockModels(models: Map<string, unknown>) {
|
||||
[...models.entries()].forEach(([id, model]) => {
|
||||
this.blockModels[Identifier.create(id).toString()] = BlockModel.fromJson(id, model)
|
||||
})
|
||||
Object.values(this.blockModels).forEach(m => m.flatten(this))
|
||||
}
|
||||
|
||||
private loadBlockAtlas(image: HTMLImageElement, textures: any) {
|
||||
const atlasCanvas = document.createElement('canvas')
|
||||
const w = upperPowerOfTwo(image.width)
|
||||
const h = upperPowerOfTwo(image.height)
|
||||
atlasCanvas.width = w
|
||||
atlasCanvas.height = h
|
||||
const ctx = atlasCanvas.getContext('2d')!
|
||||
ctx.drawImage(image, 0, 0)
|
||||
const imageData = ctx.getImageData(0, 0, w, h)
|
||||
|
||||
const idMap: Record<string, UV> = {}
|
||||
Object.keys(textures).forEach(id => {
|
||||
const [u, v, du, dv] = textures[id]
|
||||
const dv2 = (du !== dv && id.startsWith('block/')) ? du : dv
|
||||
idMap[Identifier.create(id).toString()] = [u / w, v / h, (u + du) / w, (v + dv2) / h]
|
||||
})
|
||||
this.textureAtlas = new TextureAtlas(imageData, idMap)
|
||||
}
|
||||
}
|
||||
@@ -1163,6 +1163,32 @@ hr {
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
.item-display {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.item-display > img {
|
||||
width: 26px;
|
||||
position: relative;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.item-display > svg {
|
||||
width: 26px;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
fill: var(--node-text-dimmed);
|
||||
}
|
||||
|
||||
.item-display > canvas {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.file-view {
|
||||
background-color: var(--background-2);
|
||||
color: var(--text-2);
|
||||
|
||||
@@ -108,21 +108,6 @@
|
||||
background-color: var(--node-background-label);
|
||||
}
|
||||
|
||||
.node-header > label > img {
|
||||
height: 80%;
|
||||
position: relative;
|
||||
top: 10%;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.node-header > label > svg {
|
||||
width: 25.59px;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
top: 5px;
|
||||
fill: var(--node-text-dimmed);
|
||||
}
|
||||
|
||||
.node-header > input {
|
||||
font-size: 18px;
|
||||
padding-left: 9px;
|
||||
|
||||
Reference in New Issue
Block a user