2
0
mirror of https://github.com/Laupetin/OpenAssetTools.git synced 2026-07-03 06:18:11 +00:00

feat: modman image preview (#862)

* fix: iwi rgb and rgba byte order wrong

* chore: extract three logic from xmodel preview

* feat: add preview for image assets

* fix: bc1-5 decompressor assertion
This commit is contained in:
Jan
2026-06-28 20:34:09 +02:00
committed by GitHub
parent b0d90f3aac
commit 4017f084a8
15 changed files with 554 additions and 177 deletions
@@ -0,0 +1,97 @@
<script setup lang="ts">
import ThreeScene from "@/components/three/ThreeScene.vue";
import { computed, ref, useTemplateRef, watch } from "vue";
import {
AmbientLight,
CompressedTexture,
DoubleSide,
HemisphereLight,
Material,
Mesh,
MeshBasicMaterial,
PlaneGeometry,
type Texture,
} from "three";
import type { AssetDto } from "@/native/AssetBinds.ts";
import {
createResourceTrackedObject,
createSceneObject,
provideThreeScene,
} from "@/components/three/useThreeScene.ts";
import { useThreeRenderer } from "@/components/three/useThreeRenderer.ts";
import { getModManUrl } from "@/meta/ModManUrl.ts";
import { DDSLoader } from "three/examples/jsm/loaders/DDSLoader.js";
const props = defineProps<{
asset: AssetDto;
zoneName: string;
}>();
const { scene } = provideThreeScene();
const color = 0xffffff;
const intensity = 10;
const light = new AmbientLight(color, intensity);
scene.add(light);
const skyColor = 0xb1e1ff; // light blue
const groundColor = 0xb97a20; // brownish orange
const hemisphereLight = new HemisphereLight(skyColor, groundColor, intensity);
scene.add(hemisphereLight);
const imageUri = computed<string>(() =>
getModManUrl(
`/image/dds?zone=${encodeURIComponent(props.zoneName)}&name=${encodeURIComponent(props.asset.name)}`,
),
);
const isLoading = ref(false);
const loader = new DDSLoader();
const texture = createResourceTrackedObject<Texture>();
watch(
imageUri,
(uri) => {
isLoading.value = true;
loader
.loadAsync(uri)
.then((loadedTexture: CompressedTexture) => {
loadedTexture.needsUpdate = true;
texture.value = loadedTexture;
})
.finally(() => (isLoading.value = false));
},
{ immediate: true },
);
const textureWidth = computed(() => texture.value?.width ?? 0);
const textureHeight = computed(() => texture.value?.height ?? 0);
const sceneRef = useTemplateRef("sceneRef");
const { camera } = useThreeRenderer(sceneRef, {
cameraType: "orthographic",
orthoAdjustForAspect: true,
orthoWidth: () => textureWidth.value / 2,
orthoHeight: () => textureHeight.value / 2,
});
camera.value.position.z = -1;
camera.value.rotation.x = Math.PI;
camera.value.updateProjectionMatrix();
const material = createResourceTrackedObject<Material>();
watch(texture, (newValue) => {
material.value = new MeshBasicMaterial({ map: newValue, transparent: true, side: DoubleSide });
});
const textureMesh = createSceneObject<Mesh>();
watch(material, (newValue) => {
const newMesh = new Mesh(new PlaneGeometry(textureWidth.value, textureHeight.value), newValue);
newMesh.position.x = 0;
newMesh.position.y = 0;
newMesh.position.z = 0;
textureMesh.value = newMesh;
});
</script>
<template>
<ThreeScene ref="sceneRef" :loading="isLoading" />
</template>
@@ -0,0 +1,61 @@
import { computed, type MaybeRefOrGetter, toValue } from "vue";
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
import { DDSLoader } from "three/examples/jsm/loaders/DDSLoader.js";
import { getModManUrl } from "@/meta/ModManUrl.ts";
import { Loader, LoadingManager } from "three";
export { type GLTF } from "three/addons/loaders/GLTFLoader.js";
const IMAGE_REGEX = /\.\.[\\/]images[\\/](.+)\.(.+)$/m;
class ProxyImageLoader extends Loader {
private ddsLoader: DDSLoader;
private readonly zoneName: string;
constructor(zoneName: string) {
super();
this.ddsLoader = new DDSLoader();
this.zoneName = zoneName;
}
load(
url: string,
onLoad: (data: unknown) => void,
onProgress?: (event: ProgressEvent) => void,
onError?: (err: unknown) => void,
) {
const match = IMAGE_REGEX.exec(url);
if (!match) {
onError?.("invalid url");
return;
}
this.ddsLoader.load(
getModManUrl(
`/image/dds?zone=${encodeURIComponent(this.zoneName)}&name=${encodeURIComponent(match[1])}`,
),
onLoad,
onProgress,
onError,
);
}
loadAsync(url: string, onProgress?: (event: ProgressEvent) => void): Promise<unknown> {
const match = IMAGE_REGEX.exec(url);
if (!match) {
return Promise.reject("invalid url");
}
return this.ddsLoader.loadAsync(
getModManUrl(
`/image/dds?zone=${encodeURIComponent(this.zoneName)}&name=${encodeURIComponent(match[1])}`,
),
onProgress,
);
}
}
export function useGltfLoader(zoneName: MaybeRefOrGetter<string>) {
return computed(() => {
const manager = new LoadingManager();
manager.addHandler(IMAGE_REGEX, new ProxyImageLoader(toValue(zoneName)));
return new GLTFLoader(manager);
});
}
@@ -1,45 +1,36 @@
<script setup lang="ts"> <script setup lang="ts">
import { import {
Scene,
PerspectiveCamera,
WebGLRenderer,
AmbientLight, AmbientLight,
HemisphereLight, HemisphereLight,
Box3, Box3,
LoadingManager,
Loader,
TextureLoader, TextureLoader,
EquirectangularReflectionMapping, EquirectangularReflectionMapping,
SRGBColorSpace, SRGBColorSpace,
PerspectiveCamera,
type Texture, type Texture,
} from "three"; } from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import { GLTFLoader, type GLTF } from "three/addons/loaders/GLTFLoader.js";
import { DDSLoader } from "three/examples/jsm/loaders/DDSLoader.js";
import type { AssetDto } from "@/native/AssetBinds.ts"; import type { AssetDto } from "@/native/AssetBinds.ts";
import { computed, ref, useTemplateRef, watch } from "vue";
import { import {
computed, createResourceTrackedObject,
onMounted, createSceneObject,
onUnmounted, provideThreeScene,
ref, } from "@/components/three/useThreeScene.ts";
shallowRef, import { useThreeRenderer } from "@/components/three/useThreeRenderer.ts";
toRaw, import { useOrbitControls } from "@/components/three/useOrbitControls.ts";
useTemplateRef, import ThreeScene from "@/components/three/ThreeScene.vue";
watch, import { getModManUrl } from "@/meta/ModManUrl.ts";
} from "vue"; import { useGltfLoader } from "@/components/assets/xmodel/GltfLoader.ts";
import { useResizeObserver } from "@vueuse/core";
import { ThreeResourceTracker } from "@/components/assets/xmodel/ThreeResourceTracker.ts";
const props = defineProps<{ const props = defineProps<{
asset: AssetDto; asset: AssetDto;
zoneName: string; zoneName: string;
}>(); }>();
const canvasWrapperRef = useTemplateRef<HTMLDivElement>("canvas-wrapper"); const { scene } = provideThreeScene();
const canvasRef = useTemplateRef<HTMLCanvasElement>("canvas"); const sceneRef = useTemplateRef("sceneRef");
const { camera } = useThreeRenderer(sceneRef);
const scene = new Scene(); const { controls } = useOrbitControls(sceneRef, camera);
const camera = new PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const color = 0xffffff; const color = 0xffffff;
const intensity = 1; const intensity = 1;
@@ -51,90 +42,45 @@ const groundColor = 0xb97a20; // brownish orange
const hemisphereLight = new HemisphereLight(skyColor, groundColor, intensity); const hemisphereLight = new HemisphereLight(skyColor, groundColor, intensity);
scene.add(hemisphereLight); scene.add(hemisphereLight);
camera.position.z = 3; const modelUri = computed<string>(() =>
getModManUrl(
const IMAGE_REGEX = /\.\.[\\/]images[\\/](.+)\.(.+)$/m; `/xmodel/glb?zone=${encodeURIComponent(props.zoneName)}&name=${encodeURIComponent(props.asset.name)}`,
class ProxyImageLoader extends Loader { ),
private ddsLoader: DDSLoader;
constructor() {
super();
this.ddsLoader = new DDSLoader();
}
load(
url: string,
onLoad: (data: unknown) => void,
onProgress?: (event: ProgressEvent) => void,
onError?: (err: unknown) => void,
) {
const match = IMAGE_REGEX.exec(url);
if (!match) {
onError?.("invalid url");
return;
}
this.ddsLoader.load(
`modman://localhost/image/dds?zone=${encodeURIComponent(props.zoneName)}&name=${encodeURIComponent(match[1])}`,
onLoad,
onProgress,
onError,
); );
} const model = createSceneObject();
loadAsync(url: string, onProgress?: (event: ProgressEvent) => void): Promise<unknown> {
const match = IMAGE_REGEX.exec(url);
if (!match) {
return Promise.reject("invalid url");
}
return this.ddsLoader.loadAsync(
`modman://localhost/image/dds?zone=${encodeURIComponent(props.zoneName)}&name=${encodeURIComponent(match[1])}`,
onProgress,
);
}
}
const manager = new LoadingManager();
manager.addHandler(IMAGE_REGEX, new ProxyImageLoader());
const gltfLoader = new GLTFLoader(manager);
const modelUri = computed<string>(
() =>
`modman://localhost/xmodel/glb?zone=${encodeURIComponent(props.zoneName)}&name=${encodeURIComponent(props.asset.name)}`,
);
const model = shallowRef<GLTF | undefined>(undefined);
const isLoading = ref(false); const isLoading = ref(false);
const resourceTracker = new ThreeResourceTracker();
const modelBounds = computed<Box3 | undefined>(() => { const modelBounds = computed<Box3 | undefined>(() => {
const modelValue = model.value; const modelValue = model.value;
if (!modelValue) return undefined; if (!modelValue) return undefined;
const box = new Box3(); const box = new Box3();
box.expandByObject(modelValue.scene); box.expandByObject(modelValue);
return box; return box;
}); });
const loader = new TextureLoader(); const loader = new TextureLoader();
let skybox: Texture | undefined = undefined; const skybox = createResourceTrackedObject<Texture>();
loader.loadAsync("/skybox/citrus_orchard_puresky.jpg").then((res) => { loader.loadAsync("/skybox/citrus_orchard_puresky.jpg").then((res) => {
skybox = res; skybox.value = res;
skybox.mapping = EquirectangularReflectionMapping; skybox.value.mapping = EquirectangularReflectionMapping;
skybox.colorSpace = SRGBColorSpace; skybox.value.colorSpace = SRGBColorSpace;
scene.background = skybox; scene.background = skybox.value;
}); });
const gltfLoader = useGltfLoader(() => props.zoneName);
watch( watch(
modelUri, modelUri,
(uri) => { (uri) => {
isLoading.value = true; isLoading.value = true;
gltfLoader gltfLoader.value
.loadAsync(uri) .loadAsync(uri)
.then((gltf) => (model.value = gltf)) .then((gltf) => (model.value = gltf.scene))
.finally(() => (isLoading.value = false)); .finally(() => (isLoading.value = false));
}, },
{ immediate: true }, { immediate: true },
); );
let renderer: WebGLRenderer | undefined = undefined;
let controls: OrbitControls | undefined;
function resetCameraPositionForObject() { function resetCameraPositionForObject() {
const boundsValue = modelBounds.value; const boundsValue = modelBounds.value;
if (!boundsValue) return; if (!boundsValue) return;
@@ -147,103 +93,28 @@ function resetCameraPositionForObject() {
const middleZ = boundsValue.min.z + sizeZ / 2; const middleZ = boundsValue.min.z + sizeZ / 2;
const cameraX = Math.max(sizeY, sizeZ) / 2 + boundsValue.max.x; const cameraX = Math.max(sizeY, sizeZ) / 2 + boundsValue.max.x;
camera.position.set(cameraX, middleY, middleZ); camera.value.position.set(cameraX, middleY, middleZ);
camera.lookAt(middleX, middleY, middleZ); camera.value.lookAt(middleX, middleY, middleZ);
if (camera instanceof PerspectiveCamera) {
camera.far = Math.max(cameraX + sizeX, sizeY, sizeZ, 1000) * 2; camera.far = Math.max(cameraX + sizeX, sizeY, sizeZ, 1000) * 2;
camera.updateProjectionMatrix(); camera.updateProjectionMatrix();
}
if (controls) { const controlsValue = controls.value;
controls.target.set(middleX, middleY, middleZ); if (controlsValue) {
controls.update(); controlsValue.target.set(middleX, middleY, middleZ);
controlsValue.update();
} }
} }
watch(model, (newVal, oldVal) => { watch(model, (newVal) => {
if (newVal) { if (newVal) {
resourceTracker.refObject(newVal.scene);
}
if (oldVal) {
scene.remove(toRaw(oldVal).scene);
}
if (newVal) {
scene.add(toRaw(newVal.scene));
resetCameraPositionForObject(); resetCameraPositionForObject();
} }
if (oldVal) {
resourceTracker.unrefObject(toRaw(oldVal).scene);
}
});
onMounted(() => {
renderer = new WebGLRenderer({ canvas: canvasRef.value! });
const canvasWrapperBounds = canvasWrapperRef.value!.getBoundingClientRect();
renderer.setSize(canvasWrapperBounds.width, canvasWrapperBounds.height);
function animate() {
renderer!.render(scene, camera);
}
renderer.setAnimationLoop(animate);
controls = new OrbitControls(camera, canvasRef.value!);
controls.target.set(0, 0, 0);
controls.update();
});
onUnmounted(() => {
if (model.value) {
resourceTracker.unrefObject(toRaw(model.value).scene);
}
if (skybox) {
skybox.dispose();
}
});
useResizeObserver(canvasWrapperRef, () => {
const canvasWrapperBounds = canvasWrapperRef.value!.getBoundingClientRect();
renderer?.setSize(canvasWrapperBounds.width, canvasWrapperBounds.height);
}); });
</script> </script>
<template> <template>
<div class="preview-wrapper" ref="canvas-wrapper"> <ThreeScene ref="sceneRef" :loading="isLoading" />
<canvas class="preview" ref="canvas" />
<div v-if="isLoading" class="loading-overlay">
<span>Loading</span>
</div>
</div>
</template> </template>
<style scoped lang="scss">
.preview-wrapper {
display: flex;
position: relative;
width: 100%;
height: 100%;
}
.preview {
border-radius: 0.25rem;
}
@starting-style {
.loading-overlay {
opacity: 0;
}
}
.loading-overlay {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
inset: 0;
background-color: rgba(0, 0, 0, 0.5);
transition: opacity ease-in-out 500ms;
}
</style>
@@ -10,6 +10,7 @@ function getMaterialsOfObject(object: Object3D) {
return []; return [];
} }
export type ResourceTrackedType = Texture | Material | Object3D | BufferGeometry;
export class ThreeResourceTracker { export class ThreeResourceTracker {
private readonly textures: Record<number, number>; private readonly textures: Record<number, number>;
private readonly materials: Record<string, number>; private readonly materials: Record<string, number>;
@@ -23,6 +24,30 @@ export class ThreeResourceTracker {
this.objects = {}; this.objects = {};
} }
ref(value: ResourceTrackedType) {
if (value instanceof Texture) {
this.refTexture(value);
} else if (value instanceof Material) {
this.refMaterial(value);
} else if (value instanceof BufferGeometry) {
this.refGeometry(value);
} else if (value instanceof Object3D) {
this.refObject(value);
}
}
unref(value: ResourceTrackedType) {
if (value instanceof Texture) {
this.unrefTexture(value);
} else if (value instanceof Material) {
this.unrefMaterial(value);
} else if (value instanceof BufferGeometry) {
this.unrefGeometry(value);
} else if (value instanceof Object3D) {
this.unrefObject(value);
}
}
refTexture(texture: Texture) { refTexture(texture: Texture) {
if (!this.textures[texture.id]) { if (!this.textures[texture.id]) {
this.textures[texture.id] = 1; this.textures[texture.id] = 1;
@@ -0,0 +1,58 @@
<script setup lang="ts">
import { useTemplateRef } from "vue";
withDefaults(
defineProps<{
loading?: boolean;
}>(),
{ loading: false },
);
const canvasWrapperRef = useTemplateRef<HTMLDivElement>("canvas-wrapper");
const canvasRef = useTemplateRef<HTMLCanvasElement>("canvas");
defineExpose({ canvasRef, canvasWrapperRef });
</script>
<template>
<div class="three-scene" ref="canvas-wrapper">
<canvas class="three-canvas" ref="canvas" />
<slot />
<div v-if="loading" class="loading-overlay">
<span>Loading</span>
</div>
</div>
</template>
<style scoped lang="scss">
.three-scene {
display: flex;
position: relative;
width: 100%;
height: 100%;
}
.three-canvas {
border-radius: 0.25rem;
}
@starting-style {
.loading-overlay {
opacity: 0;
}
}
.loading-overlay {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
inset: 0;
background-color: rgba(0, 0, 0, 0.5);
transition: opacity ease-in-out 500ms;
}
</style>
@@ -0,0 +1,26 @@
import { type ComponentInstance, type MaybeRefOrGetter, shallowRef, toValue, watch } from "vue";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import type { Camera } from "three";
import ThreeScene from "@/components/three/ThreeScene.vue";
export function useOrbitControls(
sceneComponent: MaybeRefOrGetter<ComponentInstance<typeof ThreeScene> | null>,
camera: MaybeRefOrGetter<Camera>,
) {
const controls = shallowRef<OrbitControls | undefined>(undefined);
watch(
() => toValue(sceneComponent)?.canvasRef ?? null,
(canvasRefValue) => {
if (canvasRefValue) {
const cameraValue = toValue(camera);
controls.value = new OrbitControls(cameraValue, canvasRefValue);
controls.value.target.set(0, 0, 0);
controls.value.update();
}
},
{ immediate: true },
);
return { controls };
}
@@ -0,0 +1,167 @@
import {
type ComponentInstance,
computed,
type MaybeRefOrGetter,
ref,
shallowRef,
toValue,
watch,
} from "vue";
import { OrthographicCamera, PerspectiveCamera, WebGLRenderer } from "three";
import { useResizeObserver } from "@vueuse/core";
import ThreeScene from "@/components/three/ThreeScene.vue";
import { useThreeSceneOrThrow } from "@/components/three/useThreeScene.ts";
export type CameraType = PerspectiveCamera | OrthographicCamera;
export interface UseThreeRendererOptions {
cameraType?: "perspective" | "orthographic";
fov?: MaybeRefOrGetter<number>;
orthoAdjustForAspect?: boolean;
orthoWidth?: MaybeRefOrGetter<number>;
orthoHeight?: MaybeRefOrGetter<number>;
zNear?: MaybeRefOrGetter<number>;
zFar?: MaybeRefOrGetter<number>;
}
export function useThreeRenderer(
sceneComponent: MaybeRefOrGetter<ComponentInstance<typeof ThreeScene> | null>,
options?: UseThreeRendererOptions,
) {
const cameraType = options?.cameraType ?? "perspective";
const fov = options?.fov ?? 75;
const zNear = options?.zNear ?? 0.1;
const zFar = options?.zFar ?? 1000;
const orthoWidth = options?.orthoWidth ?? 1;
const orthoHeight = options?.orthoHeight ?? 1;
const { scene } = useThreeSceneOrThrow();
const canvas = computed(() => toValue(sceneComponent)?.canvasRef ?? null);
const canvasWrapper = computed(() => toValue(sceneComponent)?.canvasWrapperRef ?? null);
function createNewCamera() {
if (cameraType === "perspective") {
let aspect: number = 1;
const canvasWrapperValue = canvasWrapper.value;
if (canvasWrapperValue) {
const canvasWrapperBounds = canvasWrapperValue.getBoundingClientRect();
aspect = canvasWrapperBounds.width / canvasWrapperBounds.height;
}
return new PerspectiveCamera(toValue(fov), aspect, toValue(zNear), toValue(zFar));
}
const orthoWidthValue = toValue(orthoWidth);
const orthoHeightValue = toValue(orthoHeight);
return new OrthographicCamera(
orthoWidthValue / -2,
orthoWidthValue / 2,
orthoHeightValue / 2,
orthoHeightValue / -2,
toValue(zNear),
toValue(zFar),
);
}
const camera = shallowRef<CameraType>(createNewCamera());
const aspect = ref(1);
let renderer: WebGLRenderer | undefined = undefined;
watch(
() => [canvas.value, canvasWrapper.value],
([canvasValue, canvasWrapperValue]) => {
if (!canvasValue || !canvasWrapperValue) return;
renderer = new WebGLRenderer({ canvas: canvasValue, alpha: true });
const canvasWrapperBounds = canvasWrapperValue.getBoundingClientRect();
renderer.setSize(canvasWrapperBounds.width, canvasWrapperBounds.height);
aspect.value = canvasWrapperBounds.width / canvasWrapperBounds.height;
if (camera.value instanceof PerspectiveCamera) {
camera.value.aspect = aspect.value;
}
function animate() {
renderer!.render(toValue(scene), camera.value);
}
renderer.setAnimationLoop(animate);
},
{ immediate: true },
);
useResizeObserver(canvasWrapper, () => {
const canvasWrapperBounds = canvasWrapper.value!.getBoundingClientRect();
renderer?.setSize(canvasWrapperBounds.width, canvasWrapperBounds.height);
aspect.value = canvasWrapperBounds.width / canvasWrapperBounds.height;
});
watch(aspect, (newValue) => {
if (camera.value instanceof PerspectiveCamera) {
camera.value.aspect = newValue;
camera.value.updateProjectionMatrix();
}
});
watch(
() => toValue(fov),
(fovValue) => {
const cameraValue = camera.value;
if (cameraValue instanceof PerspectiveCamera) {
cameraValue.fov = fovValue;
cameraValue.updateProjectionMatrix();
}
},
);
if (options?.orthoAdjustForAspect === true) {
watch(
() => [toValue(orthoWidth), toValue(orthoHeight), aspect.value],
([orthoWidthValue, orthoHeightValue, aspect]) => {
const cameraValue = camera.value;
if (orthoWidthValue > orthoHeightValue) {
orthoHeightValue = orthoWidthValue / aspect;
} else {
orthoWidthValue = orthoHeightValue * aspect;
}
if (cameraValue instanceof OrthographicCamera) {
cameraValue.left = -orthoWidthValue;
cameraValue.right = orthoWidthValue;
cameraValue.top = orthoHeightValue;
cameraValue.bottom = -orthoHeightValue;
cameraValue.updateProjectionMatrix();
}
},
);
} else {
watch(
() => [toValue(orthoWidth), toValue(orthoHeight)],
([orthoWidthValue, orthoHeightValue]) => {
const cameraValue = camera.value;
if (cameraValue instanceof OrthographicCamera) {
cameraValue.left = -orthoWidthValue;
cameraValue.right = orthoWidthValue;
cameraValue.top = orthoHeightValue;
cameraValue.bottom = -orthoHeightValue;
cameraValue.updateProjectionMatrix();
}
},
);
}
watch(
() => toValue(zNear),
(nearValue) => {
camera.value.near = nearValue;
},
);
watch(
() => toValue(zFar),
(farValue) => {
camera.value.far = farValue;
},
);
return { camera };
}
@@ -0,0 +1,67 @@
import { createInjectionState } from "@vueuse/core";
import { type Object3D, Scene } from "three";
import {
type ResourceTrackedType,
ThreeResourceTracker,
} from "@/components/three/ThreeResourceTracker.ts";
import { onScopeDispose, shallowRef, watch } from "vue";
export const [provideThreeScene, useThreeScene] = createInjectionState(() => {
const scene = new Scene();
const resourceTracker = new ThreeResourceTracker();
return { scene, resourceTracker };
});
export function useThreeSceneOrThrow() {
const value = useThreeScene();
if (!value) throw new Error("ThreeScene must be provided");
return value;
}
export function createResourceTrackedObject<T extends ResourceTrackedType>(initialValue?: T) {
const { resourceTracker } = useThreeSceneOrThrow();
const refObj = shallowRef<T | undefined>(initialValue);
watch(
refObj,
(newValue, oldValue) => {
if (newValue === oldValue) return;
if (newValue) {
resourceTracker.ref(newValue);
}
if (oldValue) {
resourceTracker.unref(oldValue);
}
},
{ immediate: true },
);
onScopeDispose(() => {
const remainingValue = refObj.value;
if (remainingValue) {
resourceTracker.unref(remainingValue);
refObj.value = undefined;
}
});
return refObj;
}
export function createSceneObject<T extends Object3D>(initialValue?: T) {
const { scene } = useThreeSceneOrThrow();
const refObj = createResourceTrackedObject<T>(initialValue);
watch(refObj, (newValue, oldValue) => {
if (newValue) {
scene.add(newValue);
}
if (oldValue) {
scene.remove(oldValue);
}
});
return refObj;
}
+3
View File
@@ -0,0 +1,3 @@
export function getModManUrl(path: string) {
return new URL(path, "modman://localhost").toString();
}
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import XModelPreview from "@/components/assets/xmodel/XModelPreview.vue"; import XModelPreview from "@/components/assets/xmodel/XModelPreview.vue";
import type { AssetDto } from "@/native/AssetBinds.ts"; import type { AssetDto } from "@/native/AssetBinds.ts";
import ImagePreview from "@/components/assets/image/ImagePreview.vue";
defineProps<{ defineProps<{
asset?: AssetDto; asset?: AssetDto;
@@ -10,7 +11,8 @@ defineProps<{
<template> <template>
<div class="preview"> <div class="preview">
<XModelPreview v-if="asset?.type === 'xmodel'" :asset :zone-name /> <ImagePreview v-if="asset?.type === 'image'" :asset :zone-name />
<XModelPreview v-else-if="asset?.type === 'xmodel'" :asset :zone-name />
<span v-else>No preview available</span> <span v-else>No preview available</span>
</div> </div>
</template> </template>
@@ -349,7 +349,7 @@ namespace image
const auto faceCount = result->GetFaceCount(); const auto faceCount = result->GetFaceCount();
const auto mipCount = result->HasMipMaps() ? result->GetMipMapCount() : 1; const auto mipCount = result->HasMipMaps() ? result->GetMipMapCount() : 1;
assert(mipCount == input.HasMipMaps() ? input.GetMipMapCount() : 1); assert(mipCount == (input.HasMipMaps() ? input.GetMipMapCount() : 1));
for (auto mipLevel = 0; mipLevel < mipCount; mipLevel++) for (auto mipLevel = 0; mipLevel < mipCount; mipLevel++)
{ {
@@ -315,7 +315,7 @@ namespace image
const auto faceCount = result->GetFaceCount(); const auto faceCount = result->GetFaceCount();
const auto mipCount = result->HasMipMaps() ? result->GetMipMapCount() : 1; const auto mipCount = result->HasMipMaps() ? result->GetMipMapCount() : 1;
assert(mipCount == input.HasMipMaps() ? input.GetMipMapCount() : 1); assert(mipCount == (input.HasMipMaps() ? input.GetMipMapCount() : 1));
for (auto mipLevel = 0; mipLevel < mipCount; mipLevel++) for (auto mipLevel = 0; mipLevel < mipCount; mipLevel++)
{ {
@@ -338,7 +338,7 @@ namespace image
const auto faceCount = result->GetFaceCount(); const auto faceCount = result->GetFaceCount();
const auto mipCount = result->HasMipMaps() ? result->GetMipMapCount() : 1; const auto mipCount = result->HasMipMaps() ? result->GetMipMapCount() : 1;
assert(mipCount == input.HasMipMaps() ? input.GetMipMapCount() : 1); assert(mipCount == (input.HasMipMaps() ? input.GetMipMapCount() : 1));
for (auto mipLevel = 0; mipLevel < mipCount; mipLevel++) for (auto mipLevel = 0; mipLevel < mipCount; mipLevel++)
{ {
@@ -180,7 +180,7 @@ namespace image
const auto faceCount = result->GetFaceCount(); const auto faceCount = result->GetFaceCount();
const auto mipCount = result->HasMipMaps() ? result->GetMipMapCount() : 1; const auto mipCount = result->HasMipMaps() ? result->GetMipMapCount() : 1;
assert(mipCount == input.HasMipMaps() ? input.GetMipMapCount() : 1); assert(mipCount == (input.HasMipMaps() ? input.GetMipMapCount() : 1));
for (auto mipLevel = 0; mipLevel < mipCount; mipLevel++) for (auto mipLevel = 0; mipLevel < mipCount; mipLevel++)
{ {
@@ -271,7 +271,7 @@ namespace image
const auto faceCount = result->GetFaceCount(); const auto faceCount = result->GetFaceCount();
const auto mipCount = result->HasMipMaps() ? result->GetMipMapCount() : 1; const auto mipCount = result->HasMipMaps() ? result->GetMipMapCount() : 1;
assert(mipCount == input.HasMipMaps() ? input.GetMipMapCount() : 1); assert(mipCount == (input.HasMipMaps() ? input.GetMipMapCount() : 1));
for (auto mipLevel = 0; mipLevel < mipCount; mipLevel++) for (auto mipLevel = 0; mipLevel < mipCount; mipLevel++)
{ {