fix: normalization

This commit is contained in:
Calum H.
2025-05-29 17:48:29 +01:00
committed by Alejandro González
parent 92f281a6df
commit 6c5480e002
7 changed files with 81 additions and 44 deletions

View File

@@ -69,7 +69,12 @@ import { hide_ads_window, init_ads_window } from '@/helpers/ads.js'
import FriendsList from '@/components/ui/friends/FriendsList.vue'
import { openUrl } from '@tauri-apps/plugin-opener'
import QuickInstanceSwitcher from '@/components/ui/QuickInstanceSwitcher.vue'
import { get_available_capes, get_available_skins } from './helpers/skins'
import {
get_available_capes,
get_available_skins,
get_normalized_skin_texture,
normalize_skin_texture,
} from './helpers/skins'
import { generateSkinPreviews } from './helpers/rendering/batch-skin-renderer'
const formatRelativeTime = useRelativeTime()
@@ -202,7 +207,12 @@ async function setupApp() {
fetchCredentials()
try {
const skins = (await get_available_skins()) ?? []
const skins = (await get_available_skins()).map(async skin => {
return {
...skin,
normalized_texture: await get_normalized_skin_texture(skin.texture)
}
}) ?? []
const capes = (await get_available_capes()) ?? []
generateSkinPreviews(skins, capes)
} catch (error) {

View File

@@ -14,7 +14,7 @@
wide-model-src="/src/assets/models/classic_player.gltf"
cape-model-src="/src/assets/models/cape.gltf"
:variant="variant"
:texture-src="previewSkin"
:texture-src="previewSkin || ''"
:cape-src="selectedCapeTexture"
:scale="1.4"
:fov="50"
@@ -113,10 +113,12 @@ import {
unequip_skin,
type Skin,
type Cape,
type SkinModel,
type SkinModel, normalize_skin_texture, get_normalized_skin_texture,
} from '@/helpers/skins.ts'
import { handleError } from '@/store/notifications'
import { UploadIcon, CheckIcon, SaveIcon, XIcon, ChevronRightIcon } from '@modrinth/assets'
import {computedAsync} from "@vueuse/core";
import {arrayBufferToBase64} from "@modrinth/utils";
const modal = useTemplateRef('modal')
const selectCapeModal = useTemplateRef('selectCapeModal')
@@ -179,10 +181,12 @@ watch(fileUploadTextureBlob, (blob, prev) => {
if (blob) localPreviewUrl.value = URL.createObjectURL(new Blob([blob]))
else localPreviewUrl.value = null
})
const previewSkin = computed(() => {
if (localPreviewUrl.value) return localPreviewUrl.value
if (currentSkin.value) return currentSkin.value.texture
return '/src/assets/skins/steve.png'
const previewSkin = computedAsync(async () => {
if (localPreviewUrl.value)
return localPreviewUrl.value;
else if (currentSkin.value) {
return await get_normalized_skin_texture(currentSkin.value);
} else return '/src/assets/skins/steve.png'
})
const hasEdits = computed(() => {

View File

@@ -47,6 +47,7 @@ const emit = defineEmits<{
function show(e?: MouseEvent) {
modal.value?.show(e)
}
function hide(emitCanceled = false) {
modal.value?.hide()
resetState()
@@ -54,9 +55,11 @@ function hide(emitCanceled = false) {
emit('canceled')
}
}
function resetState() {
if (fileInput.value) fileInput.value.value = ''
}
function triggerFileInput() {
fileInput.value?.click()
}
@@ -66,7 +69,7 @@ async function validateImageDimensions(file: File): Promise<boolean> {
const img = new Image()
img.onload = () => {
URL.revokeObjectURL(img.src)
resolve(img.width === 64 && img.height === 64)
resolve(img.width === 64 && (img.height === 64 || img.height == 32))
}
img.onerror = () => {
URL.revokeObjectURL(img.src)
@@ -77,7 +80,6 @@ async function validateImageDimensions(file: File): Promise<boolean> {
}
async function handleFileOperation(e: Event | DragEvent) {
// Get files from either drag event or file input
const files = (e as DragEvent).dataTransfer?.files || (e.target as HTMLInputElement).files
if (!files || files.length === 0) {
return
@@ -95,7 +97,7 @@ async function handleFileOperation(e: Event | DragEvent) {
if (!isValidDimensions) {
notifications.addNotification({
title: 'Invalid dimensions.',
text: 'Only 64x64 PNG files are accepted.',
text: 'Only 64x64 and 64x32 PNG files are accepted.',
type: 'error',
})
return

View File

@@ -1,5 +1,5 @@
import * as THREE from 'three'
import {Skin, Cape, normalize_skin_texture, get_normalized_skin_texture_url} from '../skins'
import {Skin, Cape, normalize_skin_texture, get_normalized_skin_texture} from '../skins'
import { determineModelType } from '../skins'
import { reactive } from 'vue'
import { setupSkinModel, disposeCaches } from '@modrinth/utils'
@@ -145,7 +145,7 @@ export async function cleanupUnusedPreviews(skins: Skin[]): Promise<void> {
}
}
export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promise<void> {
export async function generateSkinPreviews(skins: (Skin & { normalized_skin_texture: string })[], capes: Cape[]): Promise<void> {
const renderer = new BatchSkinRenderer()
const capeModelUrl = '/src/assets/models/cape.gltf'
@@ -185,7 +185,7 @@ export async function generateSkinPreviews(skins: Skin[], capes: Cape[]): Promis
const modelUrl = getModelUrlForVariant(variant)
const cape: Cape | undefined = capes.find((_cape) => _cape.id === skin.cape_id)
const renderResult = await renderer.renderSkin(
await get_normalized_skin_texture_url(skin),
skin.normalized_skin_texture,
modelUrl,
cape?.texture,
capeModelUrl,

View File

@@ -1,5 +1,6 @@
import { invoke } from '@tauri-apps/api/core'
import { handleError } from '@/store/notifications'
import {arrayBufferToBase64} from "@modrinth/utils";
export interface Cape {
id: string
@@ -142,13 +143,14 @@ export async function remove_custom_skin(skin: Skin): Promise<void> {
})
}
export async function get_normalized_skin_texture_url(skin: Skin): Promise<string> {
const bytes: Uint8Array = await normalize_skin_texture(skin);
return `data:image/png;base64,` + Buffer.from(bytes).toString("base64");
export async function get_normalized_skin_texture(skin: Skin): Promise<string> {
const data = await normalize_skin_texture(skin.texture);
const base64 = arrayBufferToBase64(data);
return `data:image/png;base64,${base64}`;
}
export async function normalize_skin_texture(skin: Skin): Promise<Uint8Array> {
return await invoke('plugin:minecraft-skins|normalize_skin_texture', { skin })
export async function normalize_skin_texture(texture: Uint8Array | string): Promise<Uint8Array> {
return await invoke('plugin:minecraft-skins|normalize_skin_texture', { texture })
}
export async function unequip_skin(): Promise<void> {

View File

@@ -1,43 +1,45 @@
<script setup lang="ts">
import {
UpdatedIcon,
PlusIcon,
EditIcon,
ExcitedRinthbot,
LogInIcon,
PlusIcon,
SpinnerIcon,
EditIcon,
TrashIcon,
UpdatedIcon,
} from '@modrinth/assets'
import {
Button,
ButtonStyled,
SkinPreviewRenderer,
ConfirmModal,
SkinButton,
SkinLikeTextButton,
Button,
ConfirmModal,
SkinPreviewRenderer,
} from '@modrinth/ui'
import type { Ref } from 'vue'
import { ref, computed, useTemplateRef, watch, onMounted, onUnmounted, inject } from 'vue'
import {computedAsync} from '@vueuse/core'
import type {Ref} from 'vue'
import {computed, inject, onMounted, onUnmounted, ref, useTemplateRef, watch} from 'vue'
import EditSkinModal from '@/components/ui/skin/EditSkinModal.vue'
import SelectCapeModal from '@/components/ui/skin/SelectCapeModal.vue'
import UploadSkinModal from '@/components/ui/skin/UploadSkinModal.vue'
import { handleError } from '@/store/notifications'
import {handleError} from '@/store/notifications'
import {Cape, normalize_skin_texture, Skin} from '@/helpers/skins.ts'
import {
get_available_skins,
get_available_capes,
filterSavedSkins,
filterDefaultSkins,
equip_skin,
set_default_cape,
filterDefaultSkins,
filterSavedSkins,
get_available_capes,
get_available_skins,
get_normalized_skin_texture,
remove_custom_skin,
set_default_cape,
} from '@/helpers/skins.ts'
import { get as getSettings } from '@/helpers/settings.ts'
import type { Cape, Skin } from '@/helpers/skins.ts'
import { get_default_user, users, login as login_flow } from '@/helpers/auth'
import type { RenderResult } from '@/helpers/rendering/batch-skin-renderer.ts'
import { generateSkinPreviews, map } from '@/helpers/rendering/batch-skin-renderer.ts'
import { handleSevereError } from '@/store/error'
import { trackEvent } from '@/helpers/analytics'
import {get as getSettings} from '@/helpers/settings.ts'
import {get_default_user, login as login_flow, users} from '@/helpers/auth'
import type {RenderResult} from '@/helpers/rendering/batch-skin-renderer.ts'
import {generateSkinPreviews, map} from '@/helpers/rendering/batch-skin-renderer.ts'
import {handleSevereError} from '@/store/error'
import {trackEvent} from '@/helpers/analytics'
import type AccountsCard from '@/components/ui/AccountsCard.vue'
const editSkinModal = useTemplateRef('editSkinModal')
@@ -69,7 +71,13 @@ const currentCape = computed(() => {
return defaultCape.value
})
const skinTexture = computed(() => selectedSkin.value?.texture ?? '')
const skinTexture = computedAsync(async () => {
if (selectedSkin.value?.texture) {
return await get_normalized_skin_texture(selectedSkin.value);
} else {
return '';
}
})
const capeTexture = computed(() => currentCape.value?.texture)
const skinVariant = computed(() => selectedSkin.value?.variant)
const skinNametag = computed(() =>
@@ -168,8 +176,8 @@ function openUploadSkinModal(e: MouseEvent) {
function onSkinFileUploaded(file: File) {
const fakeEvent = new MouseEvent('click')
file.arrayBuffer().then((buf) => {
const skinTexture = new Uint8Array(buf)
file.arrayBuffer().then(async (buf) => {
const skinTexture: Uint8Array = await normalize_skin_texture(new Uint8Array(buf))
if (editSkinModal.value && editSkinModal.value.shouldRestoreModal) {
editSkinModal.value.restoreWithNewTexture(skinTexture, file.name)
@@ -253,7 +261,7 @@ await Promise.all([loadCapes(), loadSkins(), loadCurrentUser()])
slim-model-src="/src/assets/models/slim_player.gltf"
cape-model-src="/src/assets/models/cape.gltf"
:cape-src="capeTexture"
:texture-src="skinTexture"
:texture-src="skinTexture || ''"
:variant="skinVariant"
:nametag="skinNametag"
:initial-rotation="Math.PI / 8"

View File

@@ -362,3 +362,14 @@ export function getPingLevel(ping: number) {
return 1
}
}
export function arrayBufferToBase64(buffer: Uint8Array | ArrayBuffer): string {
const bytes = buffer instanceof Uint8Array
? buffer
: new Uint8Array(buffer);
let binary = '';
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}