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

feat: xmodel preview in ModMan (#835)

* chore: upgrade webwindowed for dynamic assets

* chore: make enums in ModMan lowercase

* chore: add missing platform wiiu in ModMan

* fix: register asset handler on all windows

* chore: properly localize game and platform

* chore: render example cube as xmodel preview

* chore: allow origin * in debug

* feat: show preview of xmodels with ModMan

* feat: show images in xmodel preview

* feat: auto load search paths in ModMan

* chore: load objcontainer of loaded zones in ModMan

* chore: add iw4x specific recognized zone dirs

* chore: show when models are loading

* fix: make sure webwindowed handles window and app destruction in correct order

* chore: track and properly free threejs resources

* chore: add skybox for 3d preview

* chore: add small border radius to preview

* fix: linting

* fix: linux compilation

* chore: update package lock
This commit is contained in:
Jan
2026-06-18 18:52:52 +02:00
committed by GitHub
parent 4fd164ee33
commit 85aa7417c4
53 changed files with 2692 additions and 964 deletions
+4 -1
View File
@@ -16,8 +16,10 @@
"dependencies": {
"@fontsource/inter": "5.2.8",
"@primeuix/themes": "2.0.3",
"@vueuse/core": "14.3.0",
"pinia": "3.0.4",
"primevue": "4.5.5",
"three": "0.184.0",
"vue": "3.5.35",
"vue-router": "5.1.0"
},
@@ -25,13 +27,14 @@
"@tsconfig/node24": "24.0.4",
"@types/jsdom": "28.0.3",
"@types/node": "25.9.2",
"@types/three": "0.184.1",
"@vitejs/plugin-vue": "6.0.7",
"@vitest/eslint-plugin": "1.6.19",
"@vue/eslint-config-prettier": "10.2.0",
"@vue/eslint-config-typescript": "14.8.0",
"@vue/test-utils": "2.4.11",
"@vue/tsconfig": "0.9.1",
"@webwindowed/vite-plugin-cpp-header": "1.0.0",
"@webwindowed/vite-plugin-cpp-header": "1.1.0",
"@webwindowed/web-api": "1.0.0",
"eslint": "10.4.1",
"eslint-plugin-vue": "10.9.2",
Binary file not shown.

After

Width:  |  Height:  |  Size: 990 KiB

@@ -0,0 +1 @@
https://polyhaven.com/a/citrus_orchard_puresky
@@ -0,0 +1,169 @@
import { BufferGeometry, Material, Object3D, Texture } from "three";
import { type IUniform } from "three/src/renderers/shaders/UniformsLib.js";
function getMaterialsOfObject(object: Object3D) {
if ("material" in object) {
const value = object.material as Material | Material[];
return Array.isArray(value) ? value : [value];
}
return [];
}
export class ThreeResourceTracker {
private readonly textures: Record<number, number>;
private readonly materials: Record<string, number>;
private readonly geometries: Record<number, number>;
private readonly objects: Record<number, number>;
constructor() {
this.textures = {};
this.materials = {};
this.geometries = {};
this.objects = {};
}
refTexture(texture: Texture) {
if (!this.textures[texture.id]) {
this.textures[texture.id] = 1;
} else {
this.textures[texture.id]++;
}
}
refMaterial(material: Material) {
if (!this.materials[material.uuid]) {
this.materials[material.uuid] = 1;
for (const property of Object.values(material)) {
if (property instanceof Texture) {
this.refTexture(property);
}
if ("uniforms" in material) {
for (const value of Object.values(material.uniforms as Record<string, IUniform>)) {
if (value) {
const uniformValue = value.value;
const uniformValues = Array.isArray(uniformValue) ? uniformValue : [uniformValue];
for (const maybeTexture of uniformValues) {
if (maybeTexture instanceof Texture) {
this.refTexture(maybeTexture);
}
}
}
}
}
}
} else {
this.materials[material.uuid]++;
}
}
refGeometry(geometry: BufferGeometry) {
if (!this.geometries[geometry.id]) {
this.geometries[geometry.id] = 1;
} else {
this.geometries[geometry.id]++;
}
}
refObject(object: Object3D) {
if (!this.objects[object.id]) {
this.objects[object.id] = 1;
for (const material of getMaterialsOfObject(object)) {
this.refMaterial(material);
}
if ("geometry" in object) {
this.refGeometry(object.geometry as BufferGeometry);
}
for (const child of object.children) {
this.refObject(child);
}
} else {
this.objects[object.id]++;
}
}
unrefTexture(texture: Texture) {
const refCount = this.textures[texture.id];
if (refCount === undefined) {
return;
}
if (refCount > 1) {
this.textures[texture.id] = refCount - 1;
} else {
delete this.textures[texture.id];
texture.dispose();
}
}
unrefMaterial(material: Material) {
const refCount = this.materials[material.uuid];
if (refCount === undefined) {
return;
}
if (refCount > 1) {
this.materials[material.uuid] = refCount - 1;
} else {
for (const property of Object.values(material)) {
if (property instanceof Texture) {
this.unrefTexture(property);
}
if ("uniforms" in material) {
for (const value of Object.values(material.uniforms as Record<string, IUniform>)) {
if (value) {
const uniformValue = value.value;
const uniformValues = Array.isArray(uniformValue) ? uniformValue : [uniformValue];
for (const maybeTexture of uniformValues) {
if (maybeTexture instanceof Texture) {
this.unrefTexture(maybeTexture);
}
}
}
}
}
}
material.dispose();
delete this.materials[material.uuid];
}
}
unrefGeometry(geometry: BufferGeometry) {
const refCount = this.geometries[geometry.id];
if (refCount === undefined) {
return;
}
if (refCount > 1) {
this.geometries[geometry.id] = refCount - 1;
} else {
delete this.geometries[geometry.id];
geometry.dispose();
}
}
unrefObject(object: Object3D) {
const refCount = this.objects[object.id];
if (refCount === undefined) {
return;
}
if (refCount > 1) {
this.objects[object.id] = refCount - 1;
} else {
for (const material of getMaterialsOfObject(object)) {
this.unrefMaterial(material);
}
if ("geometry" in object) {
this.unrefGeometry(object.geometry as BufferGeometry);
}
for (const child of object.children) {
this.unrefObject(child);
}
delete this.objects[object.id];
}
}
}
@@ -0,0 +1,249 @@
<script setup lang="ts">
import {
Scene,
PerspectiveCamera,
WebGLRenderer,
AmbientLight,
HemisphereLight,
Box3,
LoadingManager,
Loader,
TextureLoader,
EquirectangularReflectionMapping,
SRGBColorSpace,
type Texture,
} 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 {
computed,
onMounted,
onUnmounted,
ref,
shallowRef,
toRaw,
useTemplateRef,
watch,
} from "vue";
import { useResizeObserver } from "@vueuse/core";
import { ThreeResourceTracker } from "@/components/assets/xmodel/ThreeResourceTracker.ts";
const props = defineProps<{
asset: AssetDto;
zoneName: string;
}>();
const canvasWrapperRef = useTemplateRef<HTMLDivElement>("canvas-wrapper");
const canvasRef = useTemplateRef<HTMLCanvasElement>("canvas");
const scene = new Scene();
const camera = new PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const color = 0xffffff;
const intensity = 1;
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);
camera.position.z = 3;
const IMAGE_REGEX = /\.\.[\\/]images[\\/](.+)\.(.+)$/m;
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,
);
}
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 resourceTracker = new ThreeResourceTracker();
const modelBounds = computed<Box3 | undefined>(() => {
const modelValue = model.value;
if (!modelValue) return undefined;
const box = new Box3();
box.expandByObject(modelValue.scene);
return box;
});
const loader = new TextureLoader();
let skybox: Texture | undefined = undefined;
loader.loadAsync("/skybox/citrus_orchard_puresky.jpg").then((res) => {
skybox = res;
skybox.mapping = EquirectangularReflectionMapping;
skybox.colorSpace = SRGBColorSpace;
scene.background = skybox;
});
watch(
modelUri,
(uri) => {
isLoading.value = true;
gltfLoader
.loadAsync(uri)
.then((gltf) => (model.value = gltf))
.finally(() => (isLoading.value = false));
},
{ immediate: true },
);
let renderer: WebGLRenderer | undefined = undefined;
let controls: OrbitControls | undefined;
function resetCameraPositionForObject() {
const boundsValue = modelBounds.value;
if (!boundsValue) return;
const sizeX = boundsValue.max.x - boundsValue.min.x;
const sizeY = boundsValue.max.y - boundsValue.min.y;
const sizeZ = boundsValue.max.z - boundsValue.min.z;
const middleX = boundsValue.min.x + sizeX / 2;
const middleY = boundsValue.min.y + sizeY / 2;
const middleZ = boundsValue.min.z + sizeZ / 2;
const cameraX = Math.max(sizeY, sizeZ) / 2 + boundsValue.max.x;
camera.position.set(cameraX, middleY, middleZ);
camera.lookAt(middleX, middleY, middleZ);
camera.far = Math.max(cameraX + sizeX, sizeY, sizeZ, 1000) * 2;
camera.updateProjectionMatrix();
if (controls) {
controls.target.set(middleX, middleY, middleZ);
controls.update();
}
}
watch(model, (newVal, oldVal) => {
if (newVal) {
resourceTracker.refObject(newVal.scene);
}
if (oldVal) {
scene.remove(toRaw(oldVal).scene);
}
if (newVal) {
scene.add(toRaw(newVal.scene));
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>
<template>
<div class="preview-wrapper" ref="canvas-wrapper">
<canvas class="preview" ref="canvas" />
<div v-if="isLoading" class="loading-overlay">
<span>Loading</span>
</div>
</div>
</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>
+96
View File
@@ -0,0 +1,96 @@
import type { GameId, GamePlatform } from "@/native/ZoneBinds.ts";
import type { CommonAssetType } from "@/native/AssetBinds.ts";
export function localizeGame(game: GameId) {
return game.toUpperCase();
}
const GAME_PLATFORM_LOOKUP: Record<GamePlatform, string> = {
pc: "PC",
ps3: "PS3",
xbox: "Xbox",
wiiu: "WiiU",
};
export function localizePlatform(platform: GamePlatform) {
return GAME_PLATFORM_LOOKUP[platform];
}
const ASSET_TYPE_LOOKUP: Record<CommonAssetType, string> = {
phys_preset: "Phys preset",
xanim: "XAnim",
xmodel: "XModel",
material: "Material",
technique_set: "Technique set",
image: "Image",
sound: "Sound",
sound_curve: "Sound curve",
loaded_sound: "Loaded sound",
clip_map: "Clip map",
com_world: "Com world",
game_world_sp: "Game world SP",
game_world_mp: "Game world MP",
map_ents: "Map ents",
gfx_world: "Gfx world",
light_def: "Light def",
ui_map: "UI map",
font: "Font",
menu_list: "Menu list",
menu: "Menu",
localize_entry: "Localize entry",
weapon: "Weapon",
sound_driver_globals: "Sound driver globals",
fx: "FX",
impact_fx: "Impact FX",
ai_type: "AI type",
mp_type: "MP type",
character: "Character",
xmodel_alias: "XModel alias",
raw_file: "Raw file",
string_table: "String table",
xmodel_pieces: "XModel pieces",
phys_coll_map: "Phys coll map",
xmodel_surfs: "XModel surfs",
pixel_shader: "Pixel shader",
vertex_shader: "Vertex shader",
vertex_decl: "Vertex decl",
fx_world: "FX world",
leaderboard: "Leaderboard",
structured_data_def: "Structured data def",
tracer: "Tracer",
vehicle: "Vehicle",
addon_map_ents: "Addon map ents",
glass_world: "Glass world",
path_data: "Path data",
vehicle_track: "Vehicle track",
attachment: "Attachment",
surface_fx: "Surface FX",
script: "Script",
phys_constraints: "Phys constraints",
destructible_def: "Destructible def",
sound_patch: "Sound patch",
weapon_def: "Weapon def",
weapon_variant: "Weapon variant",
mp_body: "MP body",
mp_head: "MP head",
pack_index: "Pack index",
xglobals: "XGlobals",
ddl: "DDL",
glasses: "Glasses",
emblem_set: "Emblem set",
font_icon: "Font icon",
weapon_full: "Weapon full",
attachment_unique: "Attachment unique",
weapon_camo: "Weapon camo",
key_value_pairs: "Key value pairs",
memory_block: "Memory block",
skinned_verts: "Skinned verts",
qdb: "Qdb",
slug: "Slug",
footstep_table: "Footstep table",
footstep_fx_table: "Footstep FX table",
zbarrier: "ZBarrier",
};
export function localizeAssetType(assetType: CommonAssetType): string {
return ASSET_TYPE_LOOKUP[assetType];
}
+74 -75
View File
@@ -1,80 +1,79 @@
import { getBinds } from "@webwindowed/web-api";
export enum CommonAssetType {
PHYS_PRESET = "PHYS_PRESET",
XANIM = "XANIM",
XMODEL = "XMODEL",
MATERIAL = "MATERIAL",
TECHNIQUE_SET = "TECHNIQUE_SET",
IMAGE = "IMAGE",
SOUND = "SOUND",
SOUND_CURVE = "SOUND_CURVE",
LOADED_SOUND = "LOADED_SOUND",
CLIP_MAP = "CLIP_MAP",
COM_WORLD = "COM_WORLD",
GAME_WORLD_SP = "GAME_WORLD_SP",
GAME_WORLD_MP = "GAME_WORLD_MP",
MAP_ENTS = "MAP_ENTS",
GFX_WORLD = "GFX_WORLD",
LIGHT_DEF = "LIGHT_DEF",
UI_MAP = "UI_MAP",
FONT = "FONT",
MENU_LIST = "MENU_LIST",
MENU = "MENU",
LOCALIZE_ENTRY = "LOCALIZE_ENTRY",
WEAPON = "WEAPON",
SOUND_DRIVER_GLOBALS = "SOUND_DRIVER_GLOBALS",
FX = "FX",
IMPACT_FX = "IMPACT_FX",
AI_TYPE = "AI_TYPE",
MP_TYPE = "MP_TYPE",
CHARACTER = "CHARACTER",
XMODEL_ALIAS = "XMODEL_ALIAS",
RAW_FILE = "RAW_FILE",
STRING_TABLE = "STRING_TABLE",
XMODEL_PIECES = "XMODEL_PIECES",
PHYS_COLL_MAP = "PHYS_COLL_MAP",
XMODEL_SURFS = "XMODEL_SURFS",
PIXEL_SHADER = "PIXEL_SHADER",
VERTEX_SHADER = "VERTEX_SHADER",
VERTEX_DECL = "VERTEX_DECL",
FX_WORLD = "FX_WORLD",
LEADERBOARD = "LEADERBOARD",
STRUCTURED_DATA_DEF = "STRUCTURED_DATA_DEF",
TRACER = "TRACER",
VEHICLE = "VEHICLE",
ADDON_MAP_ENTS = "ADDON_MAP_ENTS",
GLASS_WORLD = "GLASS_WORLD",
PATH_DATA = "PATH_DATA",
VEHICLE_TRACK = "VEHICLE_TRACK",
ATTACHMENT = "ATTACHMENT",
SURFACE_FX = "SURFACE_FX",
SCRIPT = "SCRIPT",
PHYS_CONSTRAINTS = "PHYS_CONSTRAINTS",
DESTRUCTIBLE_DEF = "DESTRUCTIBLE_DEF",
SOUND_PATCH = "SOUND_PATCH",
WEAPON_DEF = "WEAPON_DEF",
WEAPON_VARIANT = "WEAPON_VARIANT",
MP_BODY = "MP_BODY",
MP_HEAD = "MP_HEAD",
PACK_INDEX = "PACK_INDEX",
XGLOBALS = "XGLOBALS",
DDL = "DDL",
GLASSES = "GLASSES",
EMBLEM_SET = "EMBLEM_SET",
FONT_ICON = "FONT_ICON",
WEAPON_FULL = "WEAPON_FULL",
ATTACHMENT_UNIQUE = "ATTACHMENT_UNIQUE",
WEAPON_CAMO = "WEAPON_CAMO",
KEY_VALUE_PAIRS = "KEY_VALUE_PAIRS",
MEMORY_BLOCK = "MEMORY_BLOCK",
SKINNED_VERTS = "SKINNED_VERTS",
QDB = "QDB",
SLUG = "SLUG",
FOOTSTEP_TABLE = "FOOTSTEP_TABLE",
FOOTSTEP_FX_TABLE = "FOOTSTEP_FX_TABLE",
ZBARRIER = "ZBARRIER",
}
export type CommonAssetType =
| "phys_preset"
| "xanim"
| "xmodel"
| "material"
| "technique_set"
| "image"
| "sound"
| "sound_curve"
| "loaded_sound"
| "clip_map"
| "com_world"
| "game_world_sp"
| "game_world_mp"
| "map_ents"
| "gfx_world"
| "light_def"
| "ui_map"
| "font"
| "menu_list"
| "menu"
| "localize_entry"
| "weapon"
| "sound_driver_globals"
| "fx"
| "impact_fx"
| "ai_type"
| "mp_type"
| "character"
| "xmodel_alias"
| "raw_file"
| "string_table"
| "xmodel_pieces"
| "phys_coll_map"
| "xmodel_surfs"
| "pixel_shader"
| "vertex_shader"
| "vertex_decl"
| "fx_world"
| "leaderboard"
| "structured_data_def"
| "tracer"
| "vehicle"
| "addon_map_ents"
| "glass_world"
| "path_data"
| "vehicle_track"
| "attachment"
| "surface_fx"
| "script"
| "phys_constraints"
| "destructible_def"
| "sound_patch"
| "weapon_def"
| "weapon_variant"
| "mp_body"
| "mp_head"
| "pack_index"
| "xglobals"
| "ddl"
| "glasses"
| "emblem_set"
| "font_icon"
| "weapon_full"
| "attachment_unique"
| "weapon_camo"
| "key_value_pairs"
| "memory_block"
| "skinned_verts"
| "qdb"
| "slug"
| "footstep_table"
| "footstep_fx_table"
| "zbarrier";
export interface AssetDto {
type: CommonAssetType;
+2 -13
View File
@@ -1,19 +1,8 @@
import { getBinds } from "@webwindowed/web-api";
export enum GameId {
IW3 = "IW3",
IW4 = "IW4",
IW5 = "IW5",
T4 = "T4",
T5 = "T5",
T6 = "T6",
}
export type GameId = "iw3" | "iw4" | "iw5" | "t4" | "t5" | "t6";
export enum GamePlatform {
PC = "PC",
XBOX = "XBOX",
PS3 = "PS3",
}
export type GamePlatform = "pc" | "xbox" | "ps3" | "wiiu";
export interface ZoneDto {
name: string;
-85
View File
@@ -1,85 +0,0 @@
import { CommonAssetType } from "@/native/AssetBinds";
const LOOKUP_CAPITALIZED: Record<CommonAssetType, string> = {
[CommonAssetType.PHYS_PRESET]: "Phys preset",
[CommonAssetType.XANIM]: "XAnim",
[CommonAssetType.XMODEL]: "XModel",
[CommonAssetType.MATERIAL]: "Material",
[CommonAssetType.TECHNIQUE_SET]: "Technique set",
[CommonAssetType.IMAGE]: "Image",
[CommonAssetType.SOUND]: "Sound",
[CommonAssetType.SOUND_CURVE]: "Sound curve",
[CommonAssetType.LOADED_SOUND]: "Loaded sound",
[CommonAssetType.CLIP_MAP]: "Clip map",
[CommonAssetType.COM_WORLD]: "Com world",
[CommonAssetType.GAME_WORLD_SP]: "Game world SP",
[CommonAssetType.GAME_WORLD_MP]: "Game world MP",
[CommonAssetType.MAP_ENTS]: "Map ents",
[CommonAssetType.GFX_WORLD]: "Gfx world",
[CommonAssetType.LIGHT_DEF]: "Light def",
[CommonAssetType.UI_MAP]: "UI map",
[CommonAssetType.FONT]: "Font",
[CommonAssetType.MENU_LIST]: "Menu list",
[CommonAssetType.MENU]: "Menu",
[CommonAssetType.LOCALIZE_ENTRY]: "Localize entry",
[CommonAssetType.WEAPON]: "Weapon",
[CommonAssetType.SOUND_DRIVER_GLOBALS]: "Sound driver globals",
[CommonAssetType.FX]: "FX",
[CommonAssetType.IMPACT_FX]: "Impact FX",
[CommonAssetType.AI_TYPE]: "AI type",
[CommonAssetType.MP_TYPE]: "MP type",
[CommonAssetType.CHARACTER]: "Character",
[CommonAssetType.XMODEL_ALIAS]: "XModel alias",
[CommonAssetType.RAW_FILE]: "Raw file",
[CommonAssetType.STRING_TABLE]: "String table",
[CommonAssetType.XMODEL_PIECES]: "XModel pieces",
[CommonAssetType.PHYS_COLL_MAP]: "Phys coll map",
[CommonAssetType.XMODEL_SURFS]: "XModel surfs",
[CommonAssetType.PIXEL_SHADER]: "Pixel shader",
[CommonAssetType.VERTEX_SHADER]: "Vertex shader",
[CommonAssetType.VERTEX_DECL]: "Vertex decl",
[CommonAssetType.FX_WORLD]: "FX world",
[CommonAssetType.LEADERBOARD]: "Leaderboard",
[CommonAssetType.STRUCTURED_DATA_DEF]: "Structured data def",
[CommonAssetType.TRACER]: "Tracer",
[CommonAssetType.VEHICLE]: "Vehicle",
[CommonAssetType.ADDON_MAP_ENTS]: "Addon map ents",
[CommonAssetType.GLASS_WORLD]: "Glass world",
[CommonAssetType.PATH_DATA]: "Path data",
[CommonAssetType.VEHICLE_TRACK]: "Vehicle track",
[CommonAssetType.ATTACHMENT]: "Attachment",
[CommonAssetType.SURFACE_FX]: "Surface FX",
[CommonAssetType.SCRIPT]: "Script",
[CommonAssetType.PHYS_CONSTRAINTS]: "Phys constraints",
[CommonAssetType.DESTRUCTIBLE_DEF]: "Destructible def",
[CommonAssetType.SOUND_PATCH]: "Sound patch",
[CommonAssetType.WEAPON_DEF]: "Weapon def",
[CommonAssetType.WEAPON_VARIANT]: "Weapon variant",
[CommonAssetType.MP_BODY]: "MP body",
[CommonAssetType.MP_HEAD]: "MP head",
[CommonAssetType.PACK_INDEX]: "Pack index",
[CommonAssetType.XGLOBALS]: "XGlobals",
[CommonAssetType.DDL]: "DDL",
[CommonAssetType.GLASSES]: "Glasses",
[CommonAssetType.EMBLEM_SET]: "Emblem set",
[CommonAssetType.FONT_ICON]: "Font icon",
[CommonAssetType.WEAPON_FULL]: "Weapon full",
[CommonAssetType.ATTACHMENT_UNIQUE]: "Attachment unique",
[CommonAssetType.WEAPON_CAMO]: "Weapon camo",
[CommonAssetType.KEY_VALUE_PAIRS]: "Key value pairs",
[CommonAssetType.MEMORY_BLOCK]: "Memory block",
[CommonAssetType.SKINNED_VERTS]: "Skinned verts",
[CommonAssetType.QDB]: "Qdb",
[CommonAssetType.SLUG]: "Slug",
[CommonAssetType.FOOTSTEP_TABLE]: "Footstep table",
[CommonAssetType.FOOTSTEP_FX_TABLE]: "Footstep FX table",
[CommonAssetType.ZBARRIER]: "ZBarrier",
};
export function getAssetTypeNameCapitalized(assetType: CommonAssetType): string {
return LOOKUP_CAPITALIZED[assetType];
}
export function getAssetTypeNameLower(assetType: CommonAssetType): string {
return getAssetTypeNameCapitalized(assetType).toLocaleLowerCase();
}
@@ -8,11 +8,11 @@ import type { ZoneDto } from "@/native/ZoneBinds";
import { useZoneStore } from "@/stores/ZoneStore";
import { computed, watch } from "vue";
import type { CommonAssetType } from "@/native/AssetBinds";
import { getAssetTypeNameCapitalized } from "@/utils/AssetTypeName";
import { useRouter } from "vue-router";
import { PAGE } from "@/router/Page";
import { useAssetStore } from "@/stores/AssetStore";
import { storeToRefs } from "pinia";
import { localizeAssetType, localizeGame, localizePlatform } from "@/i18n/i18n.ts";
const assetStore = useAssetStore();
const zoneStore = useZoneStore();
@@ -52,7 +52,7 @@ const meterItems = computed<MeterItem[]>(() => {
.filter((entry) => entry[1] > minItemCountForDisplay)
.sort((e0, e1) => e1[1] - e0[1])
.map((entry) => ({
label: getAssetTypeNameCapitalized(entry[0] as CommonAssetType),
label: localizeAssetType(entry[0] as CommonAssetType),
value: Math.round((entry[1] / assetCount.value) * 100),
}));
@@ -105,8 +105,8 @@ watch(
<h2>{{ selectedZone ?? "No zone selected" }}</h2>
<Button label="Show assets" :disabled="!selectedZone" @click="onClickShowAssets" />
<div v-if="selectedZoneDetails" class="zone-tags">
<Tag :value="selectedZoneDetails.game" />
<Tag :value="selectedZoneDetails.platform" />
<Tag :value="localizeGame(selectedZoneDetails.game)" />
<Tag :value="localizePlatform(selectedZoneDetails.platform)" />
</div>
<div class="zone-assets">
<template v-if="assetsOfZone">
@@ -16,7 +16,7 @@ const props = defineProps<{
zoneName: string;
}>();
const selectedAsset = ref<AssetDto | null>(null);
const selectedAsset = ref<AssetDto | undefined>(undefined);
watch(
() => props.zoneName,
@@ -30,7 +30,7 @@ watch(
<template>
<div class="inspect-details">
<template v-if="assetsOfZone">
<InspectPreview class="inspect-area-preview" />
<InspectPreview :asset="selectedAsset" :zone-name class="inspect-area-preview" />
<InspectAssetDetails :selected-asset="selectedAsset" class="inspect-area-details" />
<InspectZoneAssets
v-model:selected-asset="selectedAsset"
@@ -3,6 +3,7 @@ import { computed } from "vue";
import type { ZoneDto } from "@/native/ZoneBinds.ts";
import { useZoneStore } from "@/stores/ZoneStore.ts";
import Tag from "primevue/tag";
import { localizeGame, localizePlatform } from "@/i18n/i18n.ts";
const zoneStore = useZoneStore();
const props = defineProps<{
@@ -18,8 +19,12 @@ const zoneDetails = computed<ZoneDto | null>(() =>
<span>
<span>Inspect zone: {{ zoneName }}</span>
<template v-if="zoneDetails">
<Tag class="zone-header-tag" :value="zoneDetails.game" severity="secondary" />
<Tag class="zone-header-tag" :value="zoneDetails.platform" severity="secondary" />
<Tag class="zone-header-tag" :value="localizeGame(zoneDetails.game)" severity="secondary" />
<Tag
class="zone-header-tag"
:value="localizePlatform(zoneDetails.platform)"
severity="secondary"
/>
</template>
</span>
</template>
@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { AssetDto } from "@/native/AssetBinds.ts";
import { getAssetTypeNameCapitalized } from "@/utils/AssetTypeName.ts";
import { localizeAssetType } from "@/i18n/i18n.ts";
defineProps<{
asset: AssetDto;
@@ -9,7 +9,7 @@ defineProps<{
<template>
<div class="asset-option">
<span class="asset-type">{{ getAssetTypeNameCapitalized(asset.type) }}</span>
<span class="asset-type">{{ localizeAssetType(asset.type) }}</span>
<span class="asset-name">{{ asset.name }}</span>
</div>
</template>
@@ -1,15 +1,15 @@
<script setup lang="ts">
import type { AssetDto } from "@/native/AssetBinds.ts";
import Tag from "primevue/tag";
import { getAssetTypeNameCapitalized } from "@/utils/AssetTypeName.ts";
import { computed } from "vue";
import { localizeAssetType } from "@/i18n/i18n.ts";
const props = defineProps<{
selectedAsset: AssetDto | null;
selectedAsset?: AssetDto;
}>();
const assetTypeName = computed(() =>
props.selectedAsset ? getAssetTypeNameCapitalized(props.selectedAsset.type) : "",
props.selectedAsset ? localizeAssetType(props.selectedAsset.type) : "",
);
</script>
@@ -1,8 +1,17 @@
<script setup lang="ts"></script>
<script setup lang="ts">
import XModelPreview from "@/components/assets/xmodel/XModelPreview.vue";
import type { AssetDto } from "@/native/AssetBinds.ts";
defineProps<{
asset?: AssetDto;
zoneName: string;
}>();
</script>
<template>
<div class="preview">
<span>No preview available</span>
<XModelPreview v-if="asset?.type === 'xmodel'" :asset :zone-name />
<span v-else>No preview available</span>
</div>
</template>
@@ -3,7 +3,7 @@ import Listbox from "primevue/listbox";
import type { AssetDto } from "@/native/AssetBinds.ts";
import AssetListOption from "@/view/inspect_details/components/AssetListOption.vue";
const selectedAsset = defineModel<AssetDto | null>("selectedAsset");
const selectedAsset = defineModel<AssetDto | undefined>("selectedAsset", { required: true });
defineProps<{
assets: AssetDto[];
}>();
+3
View File
@@ -17,6 +17,9 @@ export default defineConfig({
},
},
},
server: {
cors: false,
},
publicDir: fileURLToPath(new URL("./public", import.meta.url)),
plugins: [
vue(),