import React, { useState, useEffect, memo } from 'react' import type { ItemStack } from '../../types' import { useTextures } from '../../context/TextureContext' import { useScale } from '../../context/ScaleContext' import { useDataUrl, isTextureFailed } from '../../cache/textureCache' import { renderBlockIcon } from '../../cache/blockRenderer' interface ItemCanvasProps { item: ItemStack size?: number /** Hide item count overlay */ noCount?: boolean /** Hide durability bar */ noDurability?: boolean className?: string style?: React.CSSProperties } function getDurabilityColor(current: number, max: number): string { const ratio = current / max if (ratio > 0.6) return '#55ff55' if (ratio > 0.3) return '#ffff55' return '#ff5555' } /** Resolve item texture source: direct override, block render, or URL lookup. */ function useItemTextureSrc(item: ItemStack): { src: string | null failed: boolean loading: boolean } { const textures = useTextures() // Block texture: always run effect for blockTexture case const blockConfig = item.blockTexture ?? null const [blockDataUrl, setBlockDataUrl] = useState(null) const [blockFailed, setBlockFailed] = useState(false) useEffect(() => { if (!blockConfig) return let cancelled = false renderBlockIcon( blockConfig.source, blockConfig.top.slice, blockConfig.left.slice, blockConfig.right.slice, ) .then((url) => { if (!cancelled) setBlockDataUrl(url) }) .catch(() => { if (!cancelled) setBlockFailed(true) }) return () => { cancelled = true } }, [ blockConfig?.source, blockConfig ? String(blockConfig.top.slice) : '', blockConfig ? String(blockConfig.left.slice) : '', blockConfig ? String(blockConfig.right.slice) : '', ]) // Reset block state when switching away from blockTexture useEffect(() => { if (!blockConfig) { setBlockDataUrl(null) setBlockFailed(false) } }, [blockConfig]) // Direct texture (string) - use cache const directUrl = typeof item.texture === 'string' ? item.texture : null const directDataUrl = useDataUrl(directUrl) // Default URL lookup const primaryUrl = blockConfig ? '' : (typeof item.texture === 'string' ? '' : textures.getItemTextureUrl(item)) const fallbackUrl = !blockConfig && !item.textureKey && item.name ? textures.getBlockTextureUrl(item) : null const primaryDataUrl = useDataUrl(primaryUrl || null) const primaryFailed = primaryDataUrl === null || isTextureFailed(primaryUrl) const [loadFallback, setLoadFallback] = useState(false) useEffect(() => { if (primaryFailed && fallbackUrl) setLoadFallback(true) else setLoadFallback(false) }, [primaryFailed, fallbackUrl]) const fallbackDataUrl = useDataUrl(loadFallback ? fallbackUrl : null) // Resolve final src by priority if (typeof item.texture === 'string') { return { src: directDataUrl ?? null, failed: directDataUrl === null || isTextureFailed(item.texture), loading: directDataUrl === undefined, } } if (item.texture instanceof HTMLImageElement) { const img = item.texture return { src: img.complete && img.naturalWidth > 0 ? img.src : null, failed: false, loading: !img.complete, } } if (blockConfig) { return { src: blockDataUrl, failed: blockFailed, loading: !blockDataUrl && !blockFailed, } } const src = loadFallback ? fallbackDataUrl : primaryDataUrl const failed = loadFallback ? (fallbackDataUrl === null || isTextureFailed(fallbackUrl ?? '')) : primaryFailed && !fallbackUrl return { src: src ?? null, failed, loading: src === undefined, } } /** Renders a single item: texture as , count as , durability as CSS bars. */ export const ItemCanvas = memo(function ItemCanvas({ item, size, noCount = false, noDurability = false, className, style, }: ItemCanvasProps) { const { contentSize, pixelSize } = useScale() const renderSize = size ?? contentSize const { src, failed, loading } = useItemTextureSrc(item) const hasDurability = !noDurability && item.durability !== undefined && item.maxDurability !== undefined && item.durability < item.maxDurability const barColor = hasDurability ? getDurabilityColor(item.durability!, item.maxDurability!) : '#55ff55' // Font size for count: ~38% of slot size, min 7px, shadow offset = 1 scaled pixel const countFontSize = Math.max(7, Math.round(renderSize * 0.5)) const shadow = Math.max(1, Math.round(pixelSize)) return (
{failed ? ( /* Missing texture — magenta/black checkerboard */
) : src ? ( /* Loaded — render cached data URL; no onError needed */ ) : null /* still loading — render nothing (slot stays empty) */} {/* Durability bar — vanilla layout: x=2, y=13 in 16×16 item area */} {hasDurability && (() => { // Derive mcPx from renderSize so the bar always fits the container, // even at fractional scales where contentSize ≠ 16 * round(scale). const mcPx = Math.max(1, Math.floor(renderSize / 16)) const barLeft = 2 * mcPx const bgBottom = mcPx const bgWidth = 13 * mcPx const bgHeight = 2 * mcPx const ratio = item.durability! / item.maxDurability! const fillWidth = Math.round(13 * ratio) * mcPx const fillHeight = mcPx const fillBottom = 2 * mcPx return ( <>
) })()} {/* Item count */} {!noCount && item.count > 1 && ( {item.count} )}
) })