mirror of
https://github.com/LCE-Hub/LCE-Emerald-Launcher.git
synced 2026-05-21 17:54:30 +00:00
feat(PCK): feature parity with Quiver
This commit is contained in:
@@ -4,6 +4,7 @@ import { PCKAsset, PCKAssetType } from '../../types/pck';
|
||||
|
||||
interface SkinPreview3DProps {
|
||||
asset: PCKAsset;
|
||||
previewUrl?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -42,60 +43,55 @@ enum SKIN_ANIM {
|
||||
DINNER_BONE_RENDERING = 1 << 31
|
||||
}
|
||||
|
||||
const SkinPreview3D = memo(function SkinPreview3D({ asset, className }: SkinPreview3DProps) {
|
||||
const SkinPreview3D = memo(function SkinPreview3D({ asset, previewUrl, className }: SkinPreview3DProps) {
|
||||
const mountRef = useRef<HTMLDivElement>(null);
|
||||
const playerGroupRef = useRef<THREE.Group | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mountRef.current) return;
|
||||
|
||||
const width = mountRef.current.clientWidth;
|
||||
const height = mountRef.current.clientHeight;
|
||||
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(35, width / height, 0.1, 1000);
|
||||
camera.position.set(0, 0, 70);
|
||||
|
||||
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);
|
||||
camera.position.set(0, 0, 50);
|
||||
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
||||
renderer.setSize(width, height);
|
||||
renderer.setPixelRatio(window.devicePixelRatio);
|
||||
mountRef.current.innerHTML = "";
|
||||
mountRef.current.appendChild(renderer.domElement);
|
||||
|
||||
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
|
||||
const dl = new THREE.DirectionalLight(0xffffff, 0.8);
|
||||
scene.add(new THREE.AmbientLight(0xffffff, 0.4));
|
||||
scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 0.6));
|
||||
const dl = new THREE.DirectionalLight(0xffffff, 0.7);
|
||||
dl.position.set(10, 20, 10);
|
||||
scene.add(dl);
|
||||
|
||||
const playerGroup = new THREE.Group();
|
||||
playerGroup.position.y = -2;
|
||||
playerGroup.position.y = 4;
|
||||
scene.add(playerGroup);
|
||||
playerGroupRef.current = playerGroup;
|
||||
|
||||
const render = () => {
|
||||
renderer.render(scene, camera);
|
||||
};
|
||||
|
||||
const blob = new Blob([asset.data as any], { type: 'image/png' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const isFallbackUrl = !previewUrl;
|
||||
const url = previewUrl || URL.createObjectURL(new Blob([asset.data as any], { type: 'image/png' }));
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
|
||||
let active = true;
|
||||
textureLoader.load(url, (texture) => {
|
||||
if (!active) return;
|
||||
texture.magFilter = THREE.NearestFilter;
|
||||
texture.minFilter = THREE.NearestFilter;
|
||||
texture.colorSpace = THREE.SRGBColorSpace;
|
||||
|
||||
const img = texture.image;
|
||||
const isLegacy = img.height === 32;
|
||||
|
||||
const animProp = asset.properties.find(p => p.key === "ANIM");
|
||||
const animValue = animProp ? parseInt(animProp.value) || 0 : 0;
|
||||
const slimFormat = !!(animValue & SKIN_ANIM.SLIM_FORMAT);
|
||||
|
||||
const texW = img.width || 64;
|
||||
const texH = img.height || 32;
|
||||
const createFaceMaterial = (x: number, y: number, w: number, h: number, flipX = false, flipY = false) => {
|
||||
const matTex = texture.clone();
|
||||
matTex.repeat.set((flipX ? -w : w) / 64, (flipY ? -h : h) / img.height);
|
||||
matTex.offset.set((flipX ? (x + w) : x) / 64, 1 - (flipY ? y : (y + h)) / img.height);
|
||||
matTex.repeat.set((flipX ? -w : w) / texW, (flipY ? -h : h) / texH);
|
||||
matTex.offset.set((flipX ? (x + w) : x) / texW, 1 - (flipY ? y : (y + h)) / texH);
|
||||
matTex.needsUpdate = true;
|
||||
return new THREE.MeshLambertMaterial({ map: matTex, transparent: true, alphaTest: 0.5, side: THREE.FrontSide });
|
||||
};
|
||||
@@ -103,7 +99,6 @@ const SkinPreview3D = memo(function SkinPreview3D({ asset, className }: SkinPrev
|
||||
const createPart = (w: number, h: number, d: number, uv: any, overlayUv?: any, isMirror = false) => {
|
||||
const group = new THREE.Group();
|
||||
const geo = new THREE.BoxGeometry(w, h, d);
|
||||
|
||||
const getMats = (uvSet: any) => {
|
||||
return [
|
||||
createFaceMaterial(uvSet.right[0], uvSet.right[1], uvSet.right[2], uvSet.right[3], isMirror), // +x
|
||||
@@ -117,7 +112,6 @@ const SkinPreview3D = memo(function SkinPreview3D({ asset, className }: SkinPrev
|
||||
|
||||
const mesh = new THREE.Mesh(geo, getMats(uv));
|
||||
group.add(mesh);
|
||||
|
||||
if (overlayUv) {
|
||||
const oGeo = new THREE.BoxGeometry(w + 0.5, h + 0.5, d + 0.5);
|
||||
const oMesh = new THREE.Mesh(oGeo, getMats(overlayUv));
|
||||
@@ -132,7 +126,7 @@ const SkinPreview3D = memo(function SkinPreview3D({ asset, className }: SkinPrev
|
||||
left: [x + 4 + w, y + 4, 4, 12], back: [x + 8 + w, y + 4, w, 12]
|
||||
});
|
||||
|
||||
if (asset.type === (PCKAssetType.CAPE as any)) {
|
||||
if (asset.type === PCKAssetType.CAPE) {
|
||||
const capeUv = {
|
||||
top: [1, 0, 10, 1], bottom: [11, 0, 10, 1],
|
||||
right: [0, 1, 1, 16], front: [1, 1, 10, 16],
|
||||
@@ -152,7 +146,7 @@ const SkinPreview3D = memo(function SkinPreview3D({ asset, className }: SkinPrev
|
||||
playerGroup.add(head);
|
||||
}
|
||||
|
||||
if (!(animValue & SKIN_ANIM.HIDE_BODY) && asset.type !== PCKAssetType.CAPE) {
|
||||
if (!(animValue & SKIN_ANIM.HIDE_BODY)) {
|
||||
const bodyUv = { top: [20, 16, 8, 4], bottom: [28, 16, 8, 4], right: [16, 20, 4, 12], left: [28, 20, 4, 12], front: [20, 20, 8, 12], back: [32, 20, 8, 12] };
|
||||
const jacketUv = (isLegacy || (animValue & SKIN_ANIM.HIDE_JACKET)) ? undefined : { top: [20, 32, 8, 4], bottom: [28, 32, 8, 4], right: [16, 36, 4, 12], left: [28, 36, 4, 12], front: [20, 36, 8, 12], back: [32, 36, 8, 12] };
|
||||
playerGroup.add(createPart(8, 12, 4, bodyUv, jacketUv));
|
||||
@@ -235,6 +229,8 @@ const SkinPreview3D = memo(function SkinPreview3D({ asset, className }: SkinPrev
|
||||
|
||||
playerGroup.rotation.y = -0.3;
|
||||
render();
|
||||
}, undefined, (err) => {
|
||||
console.error("Failed to load skin texture", err);
|
||||
});
|
||||
|
||||
let isDragging = false;
|
||||
@@ -277,10 +273,11 @@ const SkinPreview3D = memo(function SkinPreview3D({ asset, className }: SkinPrev
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
return () => {
|
||||
active = false;
|
||||
window.removeEventListener("mousemove", onMouseMove);
|
||||
window.removeEventListener("mouseup", onMouseUp);
|
||||
window.removeEventListener("resize", handleResize);
|
||||
URL.revokeObjectURL(url);
|
||||
if (isFallbackUrl) URL.revokeObjectURL(url);
|
||||
|
||||
scene.traverse((object) => {
|
||||
if (object instanceof THREE.Mesh) {
|
||||
@@ -300,7 +297,7 @@ const SkinPreview3D = memo(function SkinPreview3D({ asset, className }: SkinPrev
|
||||
});
|
||||
renderer.dispose();
|
||||
};
|
||||
}, [asset]);
|
||||
}, [asset, previewUrl]);
|
||||
|
||||
return <div ref={mountRef} className={`w-full h-full cursor-move ${className}`} />;
|
||||
});
|
||||
|
||||
@@ -72,12 +72,25 @@ export default function PckEditorView() {
|
||||
return pck?.files.find(f => f.id === selectedAssetId) || null;
|
||||
}, [pck, selectedAssetId]);
|
||||
|
||||
const assetPreviewUrl = useMemo(() => {
|
||||
if (!selectedAsset || ![PCKAssetType.SKIN, PCKAssetType.CAPE, PCKAssetType.TEXTURE].includes(selectedAsset.type)) return null;
|
||||
const [assetPreview, setAssetPreview] = useState<{ id: string, url: string } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedAsset || ![PCKAssetType.SKIN, PCKAssetType.CAPE, PCKAssetType.TEXTURE, PCKAssetType.SKIN_DATA].includes(selectedAsset.type)) {
|
||||
setAssetPreview(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = new Blob([selectedAsset.data as any], { type: "image/png" });
|
||||
return URL.createObjectURL(blob);
|
||||
const url = URL.createObjectURL(blob);
|
||||
setAssetPreview({ id: selectedAsset.id, url });
|
||||
|
||||
return () => {
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
}, [selectedAsset]);
|
||||
|
||||
const assetPreviewUrl = (assetPreview && selectedAsset && assetPreview.id === selectedAsset.id) ? assetPreview.url : null;
|
||||
|
||||
const toggleFolder = (path: string) => {
|
||||
const next = new Set(expandedFolders);
|
||||
if (next.has(path)) next.delete(path);
|
||||
@@ -353,6 +366,7 @@ export default function PckEditorView() {
|
||||
const getTypeColor = (type: PCKAssetType) => {
|
||||
switch (type) {
|
||||
case PCKAssetType.SKIN: return "#FFFF55";
|
||||
case PCKAssetType.SKIN_DATA: return "#FFFF55";
|
||||
case PCKAssetType.CAPE: return "#AA00AA";
|
||||
case PCKAssetType.TEXTURE: return "#55FFFF";
|
||||
case PCKAssetType.AUDIO_DATA: return "#55FF55";
|
||||
@@ -530,9 +544,9 @@ export default function PckEditorView() {
|
||||
</div>
|
||||
|
||||
{assetPreviewUrl && (
|
||||
<div className="w-full h-64 bg-black/40 border-2 border-[#373737] mb-6 flex items-center justify-center overflow-hidden relative group">
|
||||
{(selectedAsset.type === PCKAssetType.SKIN || selectedAsset.type === PCKAssetType.CAPE) ? (
|
||||
<SkinPreview3D asset={selectedAsset} className="w-full h-full" />
|
||||
<div className="w-full h-[550px] bg-black/40 border-2 border-[#373737] mb-6 flex items-center justify-center overflow-hidden relative group">
|
||||
{(selectedAsset.type === PCKAssetType.SKIN || selectedAsset.type === PCKAssetType.CAPE || selectedAsset.type === PCKAssetType.SKIN_DATA) ? (
|
||||
<SkinPreview3D key={selectedAsset.id} asset={selectedAsset} previewUrl={assetPreviewUrl || undefined} className="w-full h-full" />
|
||||
) : (
|
||||
<img src={assetPreviewUrl} className="max-w-full max-h-full object-contain" style={{ imageRendering: "pixelated" }} />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user