mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
Added XVM texture viewer.
This commit is contained in:
parent
a60c69a3ef
commit
36cb131920
@ -237,7 +237,10 @@ export class ArrayBufferCursor implements Cursor {
|
||||
}
|
||||
|
||||
array_buffer(size: number = this.size - this.position): ArrayBuffer {
|
||||
const r = this.buffer.slice(this.offset + this.position, size);
|
||||
const r = this.buffer.slice(
|
||||
this.offset + this.position,
|
||||
this.offset + this.position + size
|
||||
);
|
||||
this._position += size;
|
||||
return r;
|
||||
}
|
||||
|
@ -271,7 +271,10 @@ export class ResizableBufferCursor implements Cursor {
|
||||
|
||||
array_buffer(size: number = this.size - this.position): ArrayBuffer {
|
||||
this.check_size("size", size, size);
|
||||
const r = this.buffer.backing_buffer.slice(this.offset + this.position, size);
|
||||
const r = this.buffer.backing_buffer.slice(
|
||||
this.offset + this.position,
|
||||
this.offset + this.position + size
|
||||
);
|
||||
this._position += size;
|
||||
return r;
|
||||
}
|
||||
|
34
src/data_formats/parsing/iff.ts
Normal file
34
src/data_formats/parsing/iff.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { Cursor } from "../cursor/Cursor";
|
||||
|
||||
export type IffChunk = {
|
||||
/**
|
||||
* 32-bit unsigned integer.
|
||||
*/
|
||||
type: number;
|
||||
data: Cursor;
|
||||
};
|
||||
|
||||
/**
|
||||
* PSO uses a little endian variant of the IFF format.
|
||||
* IFF files contain chunks preceded by an 8-byte header.
|
||||
* The header consists of 4 ASCII characters for the "Type ID" and a 32-bit integer specifying the chunk size.
|
||||
*/
|
||||
export function parse_iff(cursor: Cursor): IffChunk[] {
|
||||
const chunks: IffChunk[] = [];
|
||||
|
||||
while (cursor.bytes_left) {
|
||||
const type = cursor.u32();
|
||||
const size = cursor.u32();
|
||||
|
||||
if (size > cursor.bytes_left) {
|
||||
break;
|
||||
}
|
||||
|
||||
chunks.push({
|
||||
type,
|
||||
data: cursor.take(size),
|
||||
});
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
@ -2,6 +2,7 @@ import { Cursor } from "../../cursor/Cursor";
|
||||
import { Vec3 } from "../../Vec3";
|
||||
import { NjcmModel, parse_njcm_model } from "./njcm";
|
||||
import { parse_xj_model, XjModel } from "./xj";
|
||||
import { parse_iff } from "../iff";
|
||||
|
||||
// TODO:
|
||||
// - deal with multiple NJCM chunks
|
||||
@ -9,6 +10,8 @@ import { parse_xj_model, XjModel } from "./xj";
|
||||
|
||||
export const ANGLE_TO_RAD = (2 * Math.PI) / 0xffff;
|
||||
|
||||
const NJCM = 0x4d434a4e;
|
||||
|
||||
export type NjVertex = {
|
||||
position: Vec3;
|
||||
normal?: Vec3;
|
||||
@ -123,25 +126,9 @@ function parse_ninja<M extends NjModel>(
|
||||
parse_model: (cursor: Cursor, context: any) => M,
|
||||
context: any
|
||||
): NjObject<M>[] {
|
||||
while (cursor.bytes_left) {
|
||||
// Ninja uses a little endian variant of the IFF format.
|
||||
// IFF files contain chunks preceded by an 8-byte header.
|
||||
// The header consists of 4 ASCII characters for the "Type ID" and a 32-bit integer specifying the chunk size.
|
||||
const iff_type_id = cursor.string_ascii(4, false, false);
|
||||
const iff_chunk_size = cursor.u32();
|
||||
|
||||
if (iff_type_id === "NJCM") {
|
||||
return parse_sibling_objects(cursor.take(iff_chunk_size), parse_model, context);
|
||||
} else {
|
||||
if (iff_chunk_size > cursor.bytes_left) {
|
||||
break;
|
||||
}
|
||||
|
||||
cursor.seek(iff_chunk_size);
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
return parse_iff(cursor)
|
||||
.filter(chunk => chunk.type === NJCM)
|
||||
.flatMap(chunk => parse_sibling_objects(chunk.data, parse_model, context));
|
||||
}
|
||||
|
||||
// TODO: cache model and object offsets so we don't reparse the same data.
|
||||
|
@ -2,6 +2,8 @@ import { ANGLE_TO_RAD } from ".";
|
||||
import { Cursor } from "../../cursor/Cursor";
|
||||
import { Vec3 } from "../../Vec3";
|
||||
|
||||
const NMDM = 0x4d444d4e;
|
||||
|
||||
export type NjMotion = {
|
||||
motion_data: NjMotionData[];
|
||||
frame_count: number;
|
||||
@ -65,7 +67,7 @@ export type NjKeyframeA = {
|
||||
};
|
||||
|
||||
export function parse_njm(cursor: Cursor, bone_count: number): NjMotion {
|
||||
if (cursor.string_ascii(4, false, true) === "NMDM") {
|
||||
if (cursor.u32() === NMDM) {
|
||||
return parse_njm_v2(cursor, bone_count);
|
||||
} else {
|
||||
cursor.seek_start(0);
|
||||
|
71
src/data_formats/parsing/ninja/texture.ts
Normal file
71
src/data_formats/parsing/ninja/texture.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import Logger from "js-logger";
|
||||
import { Cursor } from "../../cursor/Cursor";
|
||||
import { parse_iff } from "../iff";
|
||||
|
||||
const logger = Logger.get("data_formats/parsing/ninja/texture");
|
||||
|
||||
export type Xvm = {
|
||||
textures: Texture[];
|
||||
};
|
||||
|
||||
export type Texture = {
|
||||
id: number;
|
||||
format: [number, number];
|
||||
width: number;
|
||||
height: number;
|
||||
size: number;
|
||||
data: ArrayBuffer;
|
||||
};
|
||||
|
||||
type Header = {
|
||||
texture_count: number;
|
||||
};
|
||||
|
||||
const XVMH = 0x484d5658;
|
||||
const XVRT = 0x54525658;
|
||||
|
||||
export function parse_xvm(cursor: Cursor): Xvm {
|
||||
const chunks = parse_iff(cursor);
|
||||
const header_chunk = chunks.find(chunk => chunk.type === XVMH);
|
||||
const header = header_chunk && parse_header(header_chunk.data);
|
||||
|
||||
const textures = chunks
|
||||
.filter(chunk => chunk.type === XVRT)
|
||||
.map(chunk => parse_texture(chunk.data));
|
||||
|
||||
if (!header) {
|
||||
logger.warn("No header found.");
|
||||
} else if (header.texture_count !== textures.length) {
|
||||
logger.warn(
|
||||
`Found ${textures.length} textures instead of ${header.texture_count} as defined in the header.`
|
||||
);
|
||||
}
|
||||
|
||||
return { textures };
|
||||
}
|
||||
|
||||
function parse_header(cursor: Cursor): Header {
|
||||
const texture_count = cursor.u16();
|
||||
return {
|
||||
texture_count,
|
||||
};
|
||||
}
|
||||
|
||||
function parse_texture(cursor: Cursor): Texture {
|
||||
const format_1 = cursor.u32();
|
||||
const format_2 = cursor.u32();
|
||||
const id = cursor.u32();
|
||||
const width = cursor.u16();
|
||||
const height = cursor.u16();
|
||||
const size = cursor.u32();
|
||||
cursor.seek(36);
|
||||
const data = cursor.array_buffer(size);
|
||||
return {
|
||||
id,
|
||||
format: [format_1, format_2],
|
||||
width,
|
||||
height,
|
||||
size,
|
||||
data,
|
||||
};
|
||||
}
|
@ -14,12 +14,12 @@ const logger = Logger.get("data_formats/parsing/ninja/xj");
|
||||
export type XjModel = {
|
||||
type: "xj";
|
||||
vertices: NjVertex[];
|
||||
strips: XjTriangleStrip[];
|
||||
meshes: XjMesh[];
|
||||
collision_sphere_position: Vec3;
|
||||
collision_sphere_radius: number;
|
||||
};
|
||||
|
||||
export type XjTriangleStrip = {
|
||||
export type XjMesh = {
|
||||
indices: number[];
|
||||
};
|
||||
|
||||
@ -37,7 +37,7 @@ export function parse_xj_model(cursor: Cursor): XjModel {
|
||||
const model: XjModel = {
|
||||
type: "xj",
|
||||
vertices: [],
|
||||
strips: [],
|
||||
meshes: [],
|
||||
collision_sphere_position,
|
||||
collision_sphere_radius,
|
||||
};
|
||||
@ -74,13 +74,13 @@ export function parse_xj_model(cursor: Cursor): XjModel {
|
||||
}
|
||||
|
||||
if (triangle_strip_table_offset) {
|
||||
model.strips.push(
|
||||
model.meshes.push(
|
||||
...parse_triangle_strip_table(cursor, triangle_strip_table_offset, triangle_strip_count)
|
||||
);
|
||||
}
|
||||
|
||||
if (transparent_triangle_strip_table_offset) {
|
||||
model.strips.push(
|
||||
model.meshes.push(
|
||||
...parse_triangle_strip_table(
|
||||
cursor,
|
||||
transparent_triangle_strip_table_offset,
|
||||
@ -96,20 +96,22 @@ function parse_triangle_strip_table(
|
||||
cursor: Cursor,
|
||||
triangle_strip_list_offset: number,
|
||||
triangle_strip_count: number
|
||||
): XjTriangleStrip[] {
|
||||
const strips: XjTriangleStrip[] = [];
|
||||
): XjMesh[] {
|
||||
const strips: XjMesh[] = [];
|
||||
|
||||
for (let i = 0; i < triangle_strip_count; ++i) {
|
||||
cursor.seek_start(triangle_strip_list_offset + i * 20);
|
||||
cursor.seek(8); // Skip flag_and_texture_id_offset and data_type.
|
||||
|
||||
cursor.seek(8); // Skipping flag_and_texture_id_offset and data_type?
|
||||
const index_list_offset = cursor.u32();
|
||||
const index_count = cursor.u32();
|
||||
// Ignoring 4 bytes.
|
||||
|
||||
cursor.seek_start(index_list_offset);
|
||||
const indices = cursor.u16_array(index_count);
|
||||
|
||||
strips.push({ indices });
|
||||
strips.push({
|
||||
indices,
|
||||
});
|
||||
}
|
||||
|
||||
return strips;
|
||||
|
@ -1,5 +1,5 @@
|
||||
@import '~antd/dist/antd.less';
|
||||
@import 'ui/theme.less';
|
||||
@import "~antd/dist/antd.less";
|
||||
@import "ui/theme.less";
|
||||
|
||||
#phantasmal-world-root {
|
||||
position: absolute;
|
||||
@ -41,4 +41,8 @@
|
||||
& .ReactVirtualized__Table__headerRow {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.ant-tabs-bar {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
15
src/read_file.ts
Normal file
15
src/read_file.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export async function read_file(file: File): Promise<ArrayBuffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.addEventListener("loadend", () => {
|
||||
if (reader.result instanceof ArrayBuffer) {
|
||||
resolve(reader.result);
|
||||
} else {
|
||||
reject(new Error("Couldn't read file."));
|
||||
}
|
||||
});
|
||||
|
||||
reader.readAsArrayBuffer(file);
|
||||
});
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { autorun } from "mobx";
|
||||
import { Clock, SkeletonHelper, SkinnedMesh, Vector3 } from "three";
|
||||
import { Clock, Object3D, SkeletonHelper, Vector3, PerspectiveCamera } from "three";
|
||||
import { model_viewer_store } from "../stores/ModelViewerStore";
|
||||
import { Renderer } from "./Renderer";
|
||||
|
||||
@ -10,14 +10,18 @@ export function get_model_renderer(): ModelRenderer {
|
||||
return renderer;
|
||||
}
|
||||
|
||||
export class ModelRenderer extends Renderer {
|
||||
export class ModelRenderer extends Renderer<PerspectiveCamera> {
|
||||
private clock = new Clock();
|
||||
|
||||
private model?: SkinnedMesh;
|
||||
private model?: Object3D;
|
||||
private skeleton_helper?: SkeletonHelper;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
super(new PerspectiveCamera(75, 1, 1, 200));
|
||||
|
||||
autorun(() => {
|
||||
this.set_model(model_viewer_store.current_obj3d);
|
||||
});
|
||||
|
||||
autorun(() => {
|
||||
const show_skeleton = model_viewer_store.show_skeleton;
|
||||
@ -41,26 +45,10 @@ export class ModelRenderer extends Renderer {
|
||||
});
|
||||
}
|
||||
|
||||
set_model(model?: SkinnedMesh): void {
|
||||
if (this.model !== model) {
|
||||
if (this.model) {
|
||||
this.scene.remove(this.model);
|
||||
this.scene.remove(this.skeleton_helper!);
|
||||
this.skeleton_helper = undefined;
|
||||
}
|
||||
|
||||
if (model) {
|
||||
this.scene.add(model);
|
||||
this.skeleton_helper = new SkeletonHelper(model);
|
||||
this.skeleton_helper.visible = model_viewer_store.show_skeleton;
|
||||
(this.skeleton_helper.material as any).linewidth = 3;
|
||||
this.scene.add(this.skeleton_helper);
|
||||
this.reset_camera(new Vector3(0, 10, 20), new Vector3(0, 0, 0));
|
||||
}
|
||||
|
||||
this.model = model;
|
||||
this.schedule_render();
|
||||
}
|
||||
set_size(width: number, height: number): void {
|
||||
this.camera.aspect = width / height;
|
||||
this.camera.updateProjectionMatrix();
|
||||
super.set_size(width, height);
|
||||
}
|
||||
|
||||
protected render(): void {
|
||||
@ -76,4 +64,24 @@ export class ModelRenderer extends Renderer {
|
||||
this.schedule_render();
|
||||
}
|
||||
}
|
||||
|
||||
private set_model(model?: Object3D): void {
|
||||
if (this.model) {
|
||||
this.scene.remove(this.model);
|
||||
this.scene.remove(this.skeleton_helper!);
|
||||
this.skeleton_helper = undefined;
|
||||
}
|
||||
|
||||
if (model) {
|
||||
this.scene.add(model);
|
||||
this.skeleton_helper = new SkeletonHelper(model);
|
||||
this.skeleton_helper.visible = model_viewer_store.show_skeleton;
|
||||
(this.skeleton_helper.material as any).linewidth = 3;
|
||||
this.scene.add(this.skeleton_helper);
|
||||
this.reset_camera(new Vector3(0, 10, 20), new Vector3(0, 0, 0));
|
||||
}
|
||||
|
||||
this.model = model;
|
||||
this.schedule_render();
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
Raycaster,
|
||||
Vector2,
|
||||
Vector3,
|
||||
PerspectiveCamera,
|
||||
} from "three";
|
||||
import { Vec3 } from "../data_formats/Vec3";
|
||||
import { Area, Quest, QuestEntity, QuestNpc, Section } from "../domain";
|
||||
@ -43,7 +44,7 @@ type EntityUserData = {
|
||||
entity: QuestEntity;
|
||||
};
|
||||
|
||||
export class QuestRenderer extends Renderer {
|
||||
export class QuestRenderer extends Renderer<PerspectiveCamera> {
|
||||
private raycaster = new Raycaster();
|
||||
|
||||
private quest?: Quest;
|
||||
@ -59,7 +60,7 @@ export class QuestRenderer extends Renderer {
|
||||
private selected_data?: PickEntityResult;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
super(new PerspectiveCamera(75, 1, 0.1, 5000));
|
||||
|
||||
this.dom_element.addEventListener("mousedown", this.on_mouse_down);
|
||||
this.dom_element.addEventListener("mouseup", this.on_mouse_up);
|
||||
@ -67,24 +68,25 @@ export class QuestRenderer extends Renderer {
|
||||
|
||||
this.scene.add(this.obj_geometry);
|
||||
this.scene.add(this.npc_geometry);
|
||||
|
||||
autorun(() => {
|
||||
this.set_quest_and_area(
|
||||
quest_editor_store.current_quest,
|
||||
quest_editor_store.current_area
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
set_quest_and_area(quest?: Quest, area?: Area): void {
|
||||
let update = false;
|
||||
this.area = area;
|
||||
this.quest = quest;
|
||||
this.update_geometry();
|
||||
}
|
||||
|
||||
if (this.area !== area) {
|
||||
this.area = area;
|
||||
update = true;
|
||||
}
|
||||
|
||||
if (this.quest !== quest) {
|
||||
this.quest = quest;
|
||||
update = true;
|
||||
}
|
||||
|
||||
if (update) {
|
||||
this.update_geometry();
|
||||
}
|
||||
set_size(width: number, height: number): void {
|
||||
this.camera.aspect = width / height;
|
||||
this.camera.updateProjectionMatrix();
|
||||
super.set_size(width, height);
|
||||
}
|
||||
|
||||
private async update_geometry(): Promise<void> {
|
||||
|
@ -1,21 +1,21 @@
|
||||
import * as THREE from "three";
|
||||
import {
|
||||
Camera,
|
||||
Color,
|
||||
Group,
|
||||
HemisphereLight,
|
||||
MOUSE,
|
||||
PerspectiveCamera,
|
||||
Scene,
|
||||
Vector2,
|
||||
Vector3,
|
||||
WebGLRenderer,
|
||||
Vector2,
|
||||
Group,
|
||||
} from "three";
|
||||
import OrbitControlsCreator from "three-orbit-controls";
|
||||
|
||||
const OrbitControls = OrbitControlsCreator(THREE);
|
||||
|
||||
export class Renderer {
|
||||
protected camera: PerspectiveCamera;
|
||||
export class Renderer<C extends Camera> {
|
||||
protected camera: C;
|
||||
protected controls: any;
|
||||
protected scene = new Scene();
|
||||
protected light_holder = new Group();
|
||||
@ -24,10 +24,10 @@ export class Renderer {
|
||||
private render_scheduled = false;
|
||||
private light = new HemisphereLight(0xffffff, 0x505050, 1);
|
||||
|
||||
constructor() {
|
||||
constructor(camera: C) {
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||
|
||||
this.camera = new PerspectiveCamera(75, 1, 0.1, 5000);
|
||||
this.camera = camera;
|
||||
|
||||
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
|
||||
this.controls.mouseButtons.ORBIT = MOUSE.RIGHT;
|
||||
@ -46,8 +46,6 @@ export class Renderer {
|
||||
|
||||
set_size(width: number, height: number): void {
|
||||
this.renderer.setSize(width, height);
|
||||
this.camera.aspect = width / height;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.schedule_render();
|
||||
}
|
||||
|
||||
|
136
src/rendering/TextureRenderer.ts
Normal file
136
src/rendering/TextureRenderer.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import { autorun } from "mobx";
|
||||
import {
|
||||
CompressedTexture,
|
||||
LinearFilter,
|
||||
Mesh,
|
||||
MeshBasicMaterial,
|
||||
OrthographicCamera,
|
||||
PlaneGeometry,
|
||||
RGBA_S3TC_DXT1_Format,
|
||||
RGBA_S3TC_DXT3_Format,
|
||||
Vector2,
|
||||
Vector3,
|
||||
} from "three";
|
||||
import { Texture, Xvm } from "../data_formats/parsing/ninja/texture";
|
||||
import { texture_viewer_store } from "../stores/TextureViewerStore";
|
||||
import { Renderer } from "./Renderer";
|
||||
|
||||
let renderer: TextureRenderer | undefined;
|
||||
|
||||
export function get_texture_renderer(): TextureRenderer {
|
||||
if (!renderer) renderer = new TextureRenderer();
|
||||
return renderer;
|
||||
}
|
||||
|
||||
export class TextureRenderer extends Renderer<OrthographicCamera> {
|
||||
private quad_meshes: Mesh[] = [];
|
||||
|
||||
constructor() {
|
||||
super(new OrthographicCamera(-400, 400, 300, -300, 1, 10));
|
||||
|
||||
this.controls.enableRotate = false;
|
||||
|
||||
autorun(() => {
|
||||
this.scene.remove(...this.quad_meshes);
|
||||
|
||||
const xvm = texture_viewer_store.current_xvm;
|
||||
|
||||
if (xvm) {
|
||||
this.render_textures(xvm);
|
||||
}
|
||||
|
||||
this.reset_camera(new Vector3(0, 0, 5), new Vector3());
|
||||
this.schedule_render();
|
||||
});
|
||||
}
|
||||
|
||||
set_size(width: number, height: number): void {
|
||||
this.camera.left = -Math.floor(width / 2);
|
||||
this.camera.right = Math.ceil(width / 2);
|
||||
this.camera.top = Math.floor(height / 2);
|
||||
this.camera.bottom = -Math.ceil(height / 2);
|
||||
this.camera.updateProjectionMatrix();
|
||||
super.set_size(width, height);
|
||||
}
|
||||
|
||||
private render_textures = (xvm: Xvm) => {
|
||||
let total_width = 10 * (xvm.textures.length - 1); // 10px spacing between textures.
|
||||
let total_height = 0;
|
||||
|
||||
for (const tex of xvm.textures) {
|
||||
total_width += tex.width;
|
||||
total_height = Math.max(total_height, tex.height);
|
||||
}
|
||||
|
||||
let x = -Math.floor(total_width / 2);
|
||||
const y = -Math.floor(total_height / 2);
|
||||
|
||||
for (const tex of xvm.textures) {
|
||||
const tex_3js = this.create_texture(tex);
|
||||
const quad_mesh = new Mesh(
|
||||
this.create_quad(
|
||||
x,
|
||||
y + Math.floor((total_height - tex.height) / 2),
|
||||
tex.width,
|
||||
tex.height
|
||||
),
|
||||
new MeshBasicMaterial({
|
||||
map: tex_3js,
|
||||
color: tex_3js ? undefined : 0xff00ff,
|
||||
transparent: true,
|
||||
})
|
||||
);
|
||||
|
||||
this.quad_meshes.push(quad_mesh);
|
||||
this.scene.add(quad_mesh);
|
||||
|
||||
x += 10 + tex.width;
|
||||
}
|
||||
};
|
||||
|
||||
private create_texture(tex: Texture): CompressedTexture | undefined {
|
||||
const texture_3js = new CompressedTexture(
|
||||
[
|
||||
{
|
||||
data: new Uint8Array(tex.data) as any,
|
||||
width: tex.width,
|
||||
height: tex.height,
|
||||
},
|
||||
],
|
||||
tex.width,
|
||||
tex.height
|
||||
);
|
||||
|
||||
switch (tex.format[1]) {
|
||||
case 6:
|
||||
texture_3js.format = RGBA_S3TC_DXT1_Format as any;
|
||||
break;
|
||||
case 7:
|
||||
if (tex.format[0] === 2) {
|
||||
texture_3js.format = RGBA_S3TC_DXT3_Format as any;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
||||
texture_3js.minFilter = LinearFilter;
|
||||
texture_3js.needsUpdate = true;
|
||||
|
||||
return texture_3js;
|
||||
}
|
||||
|
||||
private create_quad(x: number, y: number, width: number, height: number): PlaneGeometry {
|
||||
const quad = new PlaneGeometry(width, height, 1, 1);
|
||||
quad.faceVertexUvs = [
|
||||
[
|
||||
[new Vector2(0, 0), new Vector2(0, 1), new Vector2(1, 0)],
|
||||
[new Vector2(0, 1), new Vector2(1, 1), new Vector2(1, 0)],
|
||||
],
|
||||
];
|
||||
quad.translate(x + width / 2, y + height / 2, -5);
|
||||
return quad;
|
||||
}
|
||||
}
|
@ -132,7 +132,7 @@ export function area_geometry_to_sections_and_object_3d(
|
||||
new MeshLambertMaterial({
|
||||
color: 0x44aaff,
|
||||
transparent: true,
|
||||
opacity: 0.25,
|
||||
opacity: 0.75,
|
||||
side: DoubleSide,
|
||||
})
|
||||
);
|
||||
|
@ -12,8 +12,6 @@ export function xj_model_to_geometry(
|
||||
indices: number[]
|
||||
): void {
|
||||
const index_offset = positions.length / 3;
|
||||
let clockwise = true;
|
||||
|
||||
const normal_matrix = new Matrix3().getNormalMatrix(matrix);
|
||||
|
||||
for (let { position, normal } of model.vertices) {
|
||||
@ -25,13 +23,13 @@ export function xj_model_to_geometry(
|
||||
normals.push(n.x, n.y, n.z);
|
||||
}
|
||||
|
||||
for (const mesh of model.strips) {
|
||||
const strip_indices = mesh.indices;
|
||||
for (const mesh of model.meshes) {
|
||||
let clockwise = true;
|
||||
|
||||
for (let j = 2; j < strip_indices.length; ++j) {
|
||||
const a = index_offset + strip_indices[j - 2];
|
||||
const b = index_offset + strip_indices[j - 1];
|
||||
const c = index_offset + strip_indices[j];
|
||||
for (let j = 2; j < mesh.indices.length; ++j) {
|
||||
const a = index_offset + mesh.indices[j - 2];
|
||||
const b = index_offset + mesh.indices[j - 1];
|
||||
const c = index_offset + mesh.indices[j];
|
||||
const pa = new Vector3(positions[3 * a], positions[3 * a + 1], positions[3 * a + 2]);
|
||||
const pb = new Vector3(positions[3 * b], positions[3 * b + 1], positions[3 * b + 2]);
|
||||
const pc = new Vector3(positions[3 * c], positions[3 * c + 1], positions[3 * c + 2]);
|
||||
@ -70,25 +68,6 @@ export function xj_model_to_geometry(
|
||||
}
|
||||
|
||||
clockwise = !clockwise;
|
||||
|
||||
// The following switch statement fixes model 180.xj (zanba).
|
||||
// switch (j) {
|
||||
// case 17:
|
||||
// case 52:
|
||||
// case 70:
|
||||
// case 92:
|
||||
// case 97:
|
||||
// case 126:
|
||||
// case 140:
|
||||
// case 148:
|
||||
// case 187:
|
||||
// case 200:
|
||||
// console.warn(`swapping winding at: ${j}, (${a}, ${b}, ${c})`);
|
||||
// break;
|
||||
// default:
|
||||
// ccw = !ccw;
|
||||
// break;
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,23 @@
|
||||
import Logger from "js-logger";
|
||||
import { action, observable } from "mobx";
|
||||
import { AnimationAction, AnimationClip, AnimationMixer, SkinnedMesh } from "three";
|
||||
import {
|
||||
AnimationAction,
|
||||
AnimationClip,
|
||||
AnimationMixer,
|
||||
DoubleSide,
|
||||
Mesh,
|
||||
MeshLambertMaterial,
|
||||
SkinnedMesh,
|
||||
} from "three";
|
||||
import { Endianness } from "../data_formats";
|
||||
import { ArrayBufferCursor } from "../data_formats/cursor/ArrayBufferCursor";
|
||||
import { NjModel, NjObject, parse_nj, parse_xj } from "../data_formats/parsing/ninja";
|
||||
import { NjMotion, parse_njm } from "../data_formats/parsing/ninja/motion";
|
||||
import { PlayerAnimation, PlayerModel } from "../domain";
|
||||
import { read_file } from "../read_file";
|
||||
import { create_animation_clip, PSO_FRAME_RATE } from "../rendering/animation";
|
||||
import { ninja_object_to_skinned_mesh } from "../rendering/models";
|
||||
import { ninja_object_to_buffer_geometry, ninja_object_to_skinned_mesh } from "../rendering/models";
|
||||
import { get_player_animation_data, get_player_data } from "./binary_assets";
|
||||
import { ArrayBufferCursor } from "../data_formats/cursor/ArrayBufferCursor";
|
||||
import { Endianness } from "../data_formats";
|
||||
|
||||
const logger = Logger.get("stores/ModelViewerStore");
|
||||
const nj_object_cache: Map<string, Promise<NjObject<NjModel>>> = new Map();
|
||||
@ -36,7 +45,7 @@ class ModelViewerStore {
|
||||
@observable.ref current_player_model?: PlayerModel;
|
||||
@observable.ref current_model?: NjObject<NjModel>;
|
||||
@observable.ref current_bone_count: number = 0;
|
||||
@observable.ref current_obj3d?: SkinnedMesh;
|
||||
@observable.ref current_obj3d?: Mesh;
|
||||
|
||||
@observable.ref animation?: {
|
||||
player_animation?: PlayerAnimation;
|
||||
@ -70,7 +79,7 @@ class ModelViewerStore {
|
||||
|
||||
load_model = async (model: PlayerModel) => {
|
||||
const object = await this.get_player_ninja_object(model);
|
||||
this.set_model(object, model);
|
||||
this.set_model(object, true, model);
|
||||
// Ignore the bones from the head parts.
|
||||
this.current_bone_count = 64;
|
||||
};
|
||||
@ -83,12 +92,29 @@ class ModelViewerStore {
|
||||
}
|
||||
};
|
||||
|
||||
load_file = (file: File) => {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener("loadend", () => {
|
||||
this.loadend(file, reader);
|
||||
});
|
||||
reader.readAsArrayBuffer(file);
|
||||
// TODO: notify user of problems.
|
||||
load_file = async (file: File) => {
|
||||
try {
|
||||
const buffer = await read_file(file);
|
||||
const cursor = new ArrayBufferCursor(buffer, Endianness.Little);
|
||||
|
||||
if (file.name.endsWith(".nj")) {
|
||||
const model = parse_nj(cursor)[0];
|
||||
this.set_model(model, true);
|
||||
} else if (file.name.endsWith(".xj")) {
|
||||
const model = parse_xj(cursor)[0];
|
||||
this.set_model(model, false);
|
||||
} else if (file.name.endsWith(".njm")) {
|
||||
if (this.current_model) {
|
||||
const njm = parse_njm(cursor, this.current_bone_count);
|
||||
this.set_animation(create_animation_clip(this.current_model, njm));
|
||||
}
|
||||
} else {
|
||||
logger.error(`Unknown file extension in filename "${file.name}".`);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error("Couldn't read file.", e);
|
||||
}
|
||||
};
|
||||
|
||||
toggle_animation_playing = action("toggle_animation_playing", () => {
|
||||
@ -106,7 +132,7 @@ class ModelViewerStore {
|
||||
});
|
||||
|
||||
set_animation = action("set_animation", (clip: AnimationClip, animation?: PlayerAnimation) => {
|
||||
if (!this.current_obj3d) return;
|
||||
if (!this.current_obj3d || !(this.current_obj3d instanceof SkinnedMesh)) return;
|
||||
|
||||
let mixer: AnimationMixer;
|
||||
|
||||
@ -131,7 +157,7 @@ class ModelViewerStore {
|
||||
|
||||
private set_model = action(
|
||||
"set_model",
|
||||
(model: NjObject<NjModel>, player_model?: PlayerModel) => {
|
||||
(model: NjObject<NjModel>, skeleton: boolean, player_model?: PlayerModel) => {
|
||||
if (this.current_obj3d && this.animation) {
|
||||
this.animation.mixer.stopAllAction();
|
||||
this.animation.mixer.uncacheRoot(this.current_obj3d);
|
||||
@ -142,37 +168,25 @@ class ModelViewerStore {
|
||||
this.current_model = model;
|
||||
this.current_bone_count = model.bone_count();
|
||||
|
||||
const mesh = ninja_object_to_skinned_mesh(this.current_model);
|
||||
let mesh: Mesh;
|
||||
|
||||
if (skeleton) {
|
||||
mesh = ninja_object_to_skinned_mesh(this.current_model);
|
||||
} else {
|
||||
mesh = new Mesh(
|
||||
ninja_object_to_buffer_geometry(this.current_model),
|
||||
new MeshLambertMaterial({
|
||||
color: 0xff00ff,
|
||||
side: DoubleSide,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
mesh.translateY(-mesh.geometry.boundingSphere.radius);
|
||||
this.current_obj3d = mesh;
|
||||
}
|
||||
);
|
||||
|
||||
// TODO: notify user of problems.
|
||||
private loadend = async (file: File, reader: FileReader) => {
|
||||
if (!(reader.result instanceof ArrayBuffer)) {
|
||||
logger.error("Couldn't read file.");
|
||||
return;
|
||||
}
|
||||
|
||||
const cursor = new ArrayBufferCursor(reader.result, Endianness.Little);
|
||||
|
||||
if (file.name.endsWith(".nj")) {
|
||||
const model = parse_nj(cursor)[0];
|
||||
this.set_model(model);
|
||||
} else if (file.name.endsWith(".xj")) {
|
||||
const model = parse_xj(cursor)[0];
|
||||
this.set_model(model);
|
||||
} else if (file.name.endsWith(".njm")) {
|
||||
if (this.current_model) {
|
||||
const njm = parse_njm(cursor, this.current_bone_count);
|
||||
this.set_animation(create_animation_clip(this.current_model, njm));
|
||||
}
|
||||
} else {
|
||||
logger.error(`Unknown file extension in filename "${file.name}".`);
|
||||
}
|
||||
};
|
||||
|
||||
private add_to_bone(
|
||||
object: NjObject<NjModel>,
|
||||
head_part: NjObject<NjModel>,
|
||||
|
@ -8,6 +8,7 @@ import { area_store } from "./AreaStore";
|
||||
import { entity_store } from "./EntityStore";
|
||||
import { ArrayBufferCursor } from "../data_formats/cursor/ArrayBufferCursor";
|
||||
import { Endianness } from "../data_formats";
|
||||
import { read_file } from "../read_file";
|
||||
|
||||
const logger = Logger.get("stores/QuestEditorStore");
|
||||
|
||||
@ -48,58 +49,50 @@ class QuestEditorStore {
|
||||
}
|
||||
});
|
||||
|
||||
load_file = (file: File) => {
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener("loadend", () => {
|
||||
this.loadend(reader);
|
||||
});
|
||||
reader.readAsArrayBuffer(file);
|
||||
};
|
||||
|
||||
// TODO: notify user of problems.
|
||||
private loadend = async (reader: FileReader) => {
|
||||
if (!(reader.result instanceof ArrayBuffer)) {
|
||||
logger.error("Couldn't read file.");
|
||||
return;
|
||||
}
|
||||
load_file = async (file: File) => {
|
||||
try {
|
||||
const buffer = await read_file(file);
|
||||
const quest = parse_quest(new ArrayBufferCursor(buffer, Endianness.Little));
|
||||
this.set_quest(quest);
|
||||
|
||||
const quest = parse_quest(new ArrayBufferCursor(reader.result, Endianness.Little));
|
||||
this.set_quest(quest);
|
||||
if (quest) {
|
||||
// Load section data.
|
||||
for (const variant of quest.area_variants) {
|
||||
const sections = await area_store.get_area_sections(
|
||||
quest.episode,
|
||||
variant.area.id,
|
||||
variant.id
|
||||
);
|
||||
variant.sections = sections;
|
||||
|
||||
if (quest) {
|
||||
// Load section data.
|
||||
for (const variant of quest.area_variants) {
|
||||
const sections = await area_store.get_area_sections(
|
||||
quest.episode,
|
||||
variant.area.id,
|
||||
variant.id
|
||||
);
|
||||
variant.sections = sections;
|
||||
// Generate object geometry.
|
||||
for (const object of quest.objects.filter(o => o.area_id === variant.area.id)) {
|
||||
try {
|
||||
const object_geom = await entity_store.get_object_geometry(object.type);
|
||||
this.set_section_on_visible_quest_entity(object, sections);
|
||||
object.object_3d = create_object_mesh(object, object_geom);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate object geometry.
|
||||
for (const object of quest.objects.filter(o => o.area_id === variant.area.id)) {
|
||||
try {
|
||||
const object_geom = await entity_store.get_object_geometry(object.type);
|
||||
this.set_section_on_visible_quest_entity(object, sections);
|
||||
object.object_3d = create_object_mesh(object, object_geom);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate NPC geometry.
|
||||
for (const npc of quest.npcs.filter(npc => npc.area_id === variant.area.id)) {
|
||||
try {
|
||||
const npc_geom = await entity_store.get_npc_geometry(npc.type);
|
||||
this.set_section_on_visible_quest_entity(npc, sections);
|
||||
npc.object_3d = create_npc_mesh(npc, npc_geom);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
// Generate NPC geometry.
|
||||
for (const npc of quest.npcs.filter(npc => npc.area_id === variant.area.id)) {
|
||||
try {
|
||||
const npc_geom = await entity_store.get_npc_geometry(npc.type);
|
||||
this.set_section_on_visible_quest_entity(npc, sections);
|
||||
npc.object_3d = create_npc_mesh(npc, npc_geom);
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.error("Couldn't parse quest file.");
|
||||
}
|
||||
} else {
|
||||
logger.error("Couldn't parse quest file.");
|
||||
} catch (e) {
|
||||
logger.error("Couldn't read file.", e);
|
||||
}
|
||||
};
|
||||
|
||||
|
24
src/stores/TextureViewerStore.ts
Normal file
24
src/stores/TextureViewerStore.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { observable } from "mobx";
|
||||
import { Xvm, parse_xvm } from "../data_formats/parsing/ninja/texture";
|
||||
import { ArrayBufferCursor } from "../data_formats/cursor/ArrayBufferCursor";
|
||||
import { read_file } from "../read_file";
|
||||
import { Endianness } from "../data_formats";
|
||||
import Logger from "js-logger";
|
||||
|
||||
const logger = Logger.get("stores/TextureViewerStore");
|
||||
|
||||
class TextureViewStore {
|
||||
@observable.ref current_xvm?: Xvm;
|
||||
|
||||
// TODO: notify user of problems.
|
||||
load_file = async (file: File) => {
|
||||
try {
|
||||
const buffer = await read_file(file);
|
||||
this.current_xvm = parse_xvm(new ArrayBufferCursor(buffer, Endianness.Little));
|
||||
} catch (e) {
|
||||
logger.error("Couldn't read file.", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const texture_viewer_store = new TextureViewStore();
|
@ -2,15 +2,15 @@ import { Menu, Select } from "antd";
|
||||
import { ClickParam } from "antd/lib/menu";
|
||||
import { observer } from "mobx-react";
|
||||
import React, { ReactNode } from "react";
|
||||
import { Server } from "../domain";
|
||||
import "./ApplicationComponent.less";
|
||||
import { DpsCalcComponent } from "./dps_calc/DpsCalcComponent";
|
||||
import { with_error_boundary } from "./ErrorBoundary";
|
||||
import { HuntOptimizerComponent } from "./hunt_optimizer/HuntOptimizerComponent";
|
||||
import { QuestEditorComponent } from "./quest_editor/QuestEditorComponent";
|
||||
import { DpsCalcComponent } from "./dps_calc/DpsCalcComponent";
|
||||
import { Server } from "../domain";
|
||||
import { ModelViewerComponent } from "./model_viewer/ModelViewerComponent";
|
||||
import { ViewerComponent } from "./viewer/ViewerComponent";
|
||||
|
||||
const ModelViewer = with_error_boundary(ModelViewerComponent);
|
||||
const Viewer = with_error_boundary(ViewerComponent);
|
||||
const QuestEditor = with_error_boundary(QuestEditorComponent);
|
||||
const HuntOptimizer = with_error_boundary(HuntOptimizerComponent);
|
||||
const DpsCalc = with_error_boundary(DpsCalcComponent);
|
||||
@ -23,8 +23,8 @@ export class ApplicationComponent extends React.Component {
|
||||
let tool_component;
|
||||
|
||||
switch (this.state.tool) {
|
||||
case "model_viewer":
|
||||
tool_component = <ModelViewer />;
|
||||
case "viewer":
|
||||
tool_component = <Viewer />;
|
||||
break;
|
||||
case "quest_editor":
|
||||
tool_component = <QuestEditor />;
|
||||
@ -47,8 +47,8 @@ export class ApplicationComponent extends React.Component {
|
||||
selectedKeys={[this.state.tool]}
|
||||
mode="horizontal"
|
||||
>
|
||||
<Menu.Item key="model_viewer">
|
||||
Model Viewer<sup className="ApplicationComponent-beta">(Beta)</sup>
|
||||
<Menu.Item key="viewer">
|
||||
Viewer<sup className="ApplicationComponent-beta">(Beta)</sup>
|
||||
</Menu.Item>
|
||||
<Menu.Item key="quest_editor">
|
||||
Quest Editor<sup className="ApplicationComponent-beta">(Beta)</sup>
|
||||
@ -79,6 +79,6 @@ export class ApplicationComponent extends React.Component {
|
||||
.slice(1)
|
||||
.split("&")
|
||||
.find(p => p.startsWith("tool="));
|
||||
return param ? param.slice(5) : "model_viewer";
|
||||
return param ? param.slice(5) : "viewer";
|
||||
}
|
||||
}
|
||||
|
3
src/ui/RendererComponent.less
Normal file
3
src/ui/RendererComponent.less
Normal file
@ -0,0 +1,3 @@
|
||||
.RendererComponent {
|
||||
overflow: hidden;
|
||||
}
|
40
src/ui/RendererComponent.tsx
Normal file
40
src/ui/RendererComponent.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import React, { Component, ReactNode } from "react";
|
||||
import { Renderer } from "../rendering/Renderer";
|
||||
import "./RendererComponent.less";
|
||||
import { Camera } from "three";
|
||||
|
||||
export class RendererComponent extends Component<{
|
||||
renderer: Renderer<Camera>;
|
||||
className?: string;
|
||||
}> {
|
||||
render(): ReactNode {
|
||||
let className = "RendererComponent";
|
||||
if (this.props.className) className += " " + this.props.className;
|
||||
|
||||
return <div className={className} ref={this.modifyDom} />;
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
window.addEventListener("resize", this.onResize);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
window.removeEventListener("resize", this.onResize);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
private modifyDom = (div: HTMLDivElement | null) => {
|
||||
if (div) {
|
||||
this.props.renderer.set_size(div.clientWidth, div.clientHeight);
|
||||
div.appendChild(this.props.renderer.dom_element);
|
||||
}
|
||||
};
|
||||
|
||||
private onResize = () => {
|
||||
const wrapper_div = this.props.renderer.dom_element.parentNode as HTMLDivElement;
|
||||
this.props.renderer.set_size(wrapper_div.clientWidth, wrapper_div.clientHeight);
|
||||
};
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
padding-top: 5px;
|
||||
}
|
||||
|
||||
.ho-OptimizerComponent > *:nth-child(2) {
|
||||
|
@ -1,43 +0,0 @@
|
||||
import React, { ReactNode, Component } from "react";
|
||||
import { SkinnedMesh } from "three";
|
||||
import { get_model_renderer } from "../../rendering/ModelRenderer";
|
||||
|
||||
type Props = {
|
||||
model?: SkinnedMesh;
|
||||
};
|
||||
|
||||
export class RendererComponent extends Component<Props> {
|
||||
private renderer = get_model_renderer();
|
||||
|
||||
render(): ReactNode {
|
||||
return <div style={{ overflow: "hidden" }} ref={this.modifyDom} />;
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
window.addEventListener("resize", this.onResize);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
window.removeEventListener("resize", this.onResize);
|
||||
}
|
||||
|
||||
componentWillReceiveProps({ model }: Props): void {
|
||||
this.renderer.set_model(model);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
private modifyDom = (div: HTMLDivElement | null) => {
|
||||
if (div) {
|
||||
this.renderer.set_size(div.clientWidth, div.clientHeight);
|
||||
div.appendChild(this.renderer.dom_element);
|
||||
}
|
||||
};
|
||||
|
||||
private onResize = () => {
|
||||
const wrapper_div = this.renderer.dom_element.parentNode as HTMLDivElement;
|
||||
this.renderer.set_size(wrapper_div.clientWidth, wrapper_div.clientHeight);
|
||||
};
|
||||
}
|
@ -7,7 +7,8 @@ import { quest_editor_store } from "../../stores/QuestEditorStore";
|
||||
import { EntityInfoComponent } from "./EntityInfoComponent";
|
||||
import "./QuestEditorComponent.css";
|
||||
import { QuestInfoComponent } from "./QuestInfoComponent";
|
||||
import { RendererComponent } from "./RendererComponent";
|
||||
import { RendererComponent } from "../RendererComponent";
|
||||
import { get_quest_renderer } from "../../rendering/QuestRenderer";
|
||||
|
||||
@observer
|
||||
export class QuestEditorComponent extends Component<
|
||||
@ -31,7 +32,7 @@ export class QuestEditorComponent extends Component<
|
||||
<Toolbar onSaveAsClicked={this.save_as_clicked} />
|
||||
<div className="qe-QuestEditorComponent-main">
|
||||
<QuestInfoComponent quest={quest} />
|
||||
<RendererComponent quest={quest} area={quest_editor_store.current_area} />
|
||||
<RendererComponent renderer={get_quest_renderer()} />
|
||||
<EntityInfoComponent entity={quest_editor_store.selected_entity} />
|
||||
</div>
|
||||
<SaveAsForm
|
||||
|
@ -1,44 +0,0 @@
|
||||
import React, { ReactNode, Component } from "react";
|
||||
import { Area, Quest } from "../../domain";
|
||||
import { get_quest_renderer } from "../../rendering/QuestRenderer";
|
||||
|
||||
type Props = {
|
||||
quest?: Quest;
|
||||
area?: Area;
|
||||
};
|
||||
|
||||
export class RendererComponent extends Component<Props> {
|
||||
private renderer = get_quest_renderer();
|
||||
|
||||
render(): ReactNode {
|
||||
return <div style={{ overflow: "hidden" }} ref={this.modifyDom} />;
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
window.addEventListener("resize", this.onResize);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
window.removeEventListener("resize", this.onResize);
|
||||
}
|
||||
|
||||
componentWillReceiveProps({ quest, area }: Props): void {
|
||||
this.renderer.set_quest_and_area(quest, area);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
private modifyDom = (div: HTMLDivElement | null) => {
|
||||
if (div) {
|
||||
this.renderer.set_size(div.clientWidth, div.clientHeight);
|
||||
div.appendChild(this.renderer.dom_element);
|
||||
}
|
||||
};
|
||||
|
||||
private onResize = () => {
|
||||
const wrapper_div = this.renderer.dom_element.parentNode as HTMLDivElement;
|
||||
this.renderer.set_size(wrapper_div.clientWidth, wrapper_div.clientHeight);
|
||||
};
|
||||
}
|
23
src/ui/viewer/ViewerComponent.less
Normal file
23
src/ui/viewer/ViewerComponent.less
Normal file
@ -0,0 +1,23 @@
|
||||
.v-ViewerComponent {
|
||||
display: flex;
|
||||
padding-top: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
& > .ant-tabs {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
& > .ant-tabs-content {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
|
||||
& > .ant-tabs-tabpane-active {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
22
src/ui/viewer/ViewerComponent.tsx
Normal file
22
src/ui/viewer/ViewerComponent.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React, { Component, ReactNode } from "react";
|
||||
import { Tabs } from "antd";
|
||||
import { ModelViewerComponent } from "./models/ModelViewerComponent";
|
||||
import "./ViewerComponent.less";
|
||||
import { TextureViewerComponent } from "./textures/TextureViewerComponent";
|
||||
|
||||
export class ViewerComponent extends Component {
|
||||
render(): ReactNode {
|
||||
return (
|
||||
<section className="v-ViewerComponent">
|
||||
<Tabs type="card">
|
||||
<Tabs.TabPane tab="Models" key="models">
|
||||
<ModelViewerComponent />
|
||||
</Tabs.TabPane>
|
||||
<Tabs.TabPane tab="Textures" key="textures">
|
||||
<TextureViewerComponent />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
.mv-AnimationSelectionComponent {
|
||||
.v-m-AnimationSelectionComponent {
|
||||
margin: 0 10px;
|
||||
|
||||
& > ul {
|
@ -1,5 +1,5 @@
|
||||
import React, { Component, ReactNode } from "react";
|
||||
import { model_viewer_store } from "../../stores/ModelViewerStore";
|
||||
import { model_viewer_store } from "../../../stores/ModelViewerStore";
|
||||
import "./AnimationSelectionComponent.less";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
@ -7,7 +7,7 @@ import { observer } from "mobx-react";
|
||||
export class AnimationSelectionComponent extends Component {
|
||||
render(): ReactNode {
|
||||
return (
|
||||
<section className="mv-AnimationSelectionComponent">
|
||||
<section className="v-m-AnimationSelectionComponent">
|
||||
<ul>
|
||||
{model_viewer_store.animations.map(animation => {
|
||||
const selected =
|
@ -1,8 +1,8 @@
|
||||
.mv-ModelSelectionComponent {
|
||||
.v-m-ModelSelectionComponent {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.mv-ModelSelectionComponent-model {
|
||||
.v-m-ModelSelectionComponent-model {
|
||||
cursor: pointer;
|
||||
|
||||
&.selected {
|
@ -1,12 +1,12 @@
|
||||
import { List } from "antd";
|
||||
import React, { Component, ReactNode } from "react";
|
||||
import { model_viewer_store } from "../../stores/ModelViewerStore";
|
||||
import { model_viewer_store } from "../../../stores/ModelViewerStore";
|
||||
import "./ModelSelectionComponent.less";
|
||||
|
||||
export class ModelSelectionComponent extends Component {
|
||||
render(): ReactNode {
|
||||
return (
|
||||
<section className="mv-ModelSelectionComponent">
|
||||
<section className="v-m-ModelSelectionComponent">
|
||||
<List
|
||||
itemLayout="horizontal"
|
||||
dataSource={model_viewer_store.models}
|
||||
@ -20,7 +20,7 @@ export class ModelSelectionComponent extends Component {
|
||||
title={
|
||||
<span
|
||||
className={
|
||||
"mv-ModelSelectionComponent-model" +
|
||||
"v-m-ModelSelectionComponent-model" +
|
||||
(selected ? " selected" : "")
|
||||
}
|
||||
>
|
@ -1,14 +1,16 @@
|
||||
.mv-ModelViewerComponent {
|
||||
.v-m-ModelViewerComponent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mv-ModelViewerComponent-toolbar {
|
||||
.v-m-ModelViewerComponent-toolbar {
|
||||
display: flex;
|
||||
padding: 10px 5px;
|
||||
align-items: center;
|
||||
|
||||
& > * {
|
||||
& > * {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
@ -22,7 +24,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.mv-ModelViewerComponent-main {
|
||||
.v-m-ModelViewerComponent-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
@ -3,11 +3,12 @@ import { UploadChangeParam } from "antd/lib/upload";
|
||||
import { UploadFile } from "antd/lib/upload/interface";
|
||||
import { observer } from "mobx-react";
|
||||
import React, { Component, ReactNode } from "react";
|
||||
import { model_viewer_store } from "../../stores/ModelViewerStore";
|
||||
import { model_viewer_store } from "../../../stores/ModelViewerStore";
|
||||
import { AnimationSelectionComponent } from "./AnimationSelectionComponent";
|
||||
import { ModelSelectionComponent } from "./ModelSelectionComponent";
|
||||
import "./ModelViewerComponent.less";
|
||||
import { RendererComponent } from "./RendererComponent";
|
||||
import { get_model_renderer } from "../../../rendering/ModelRenderer";
|
||||
import { RendererComponent } from "../../RendererComponent";
|
||||
|
||||
@observer
|
||||
export class ModelViewerComponent extends Component {
|
||||
@ -19,12 +20,12 @@ export class ModelViewerComponent extends Component {
|
||||
|
||||
render(): ReactNode {
|
||||
return (
|
||||
<div className="mv-ModelViewerComponent">
|
||||
<div className="v-m-ModelViewerComponent">
|
||||
<Toolbar />
|
||||
<div className="mv-ModelViewerComponent-main">
|
||||
<div className="v-m-ModelViewerComponent-main">
|
||||
<ModelSelectionComponent />
|
||||
<AnimationSelectionComponent />
|
||||
<RendererComponent model={model_viewer_store.current_obj3d} />
|
||||
<RendererComponent renderer={get_model_renderer()} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -39,11 +40,11 @@ class Toolbar extends Component {
|
||||
|
||||
render(): ReactNode {
|
||||
return (
|
||||
<div className="mv-ModelViewerComponent-toolbar">
|
||||
<div className="v-m-ModelViewerComponent-toolbar">
|
||||
<Upload
|
||||
accept=".nj, .njm, .xj"
|
||||
accept=".nj, .njm, .xj, .xvm"
|
||||
showUploadList={false}
|
||||
onChange={this.set_filename}
|
||||
onChange={this.load_file}
|
||||
// Make sure it doesn't do a POST:
|
||||
customRequest={() => false}
|
||||
>
|
||||
@ -94,7 +95,7 @@ class Toolbar extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
private set_filename = (info: UploadChangeParam<UploadFile>) => {
|
||||
private load_file = (info: UploadChangeParam<UploadFile>) => {
|
||||
if (info.file.originFileObj) {
|
||||
this.setState({ filename: info.file.name });
|
||||
model_viewer_store.load_file(info.file.originFileObj);
|
18
src/ui/viewer/textures/TextureViewerComponent.less
Normal file
18
src/ui/viewer/textures/TextureViewerComponent.less
Normal file
@ -0,0 +1,18 @@
|
||||
.v-t-TextureViewerComponent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.v-t-TextureViewerComponent-toolbar {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.v-t-TextureViewerComponent-renderer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.v-t-TextureViewerComponent-canvas {
|
||||
flex: 1;
|
||||
}
|
52
src/ui/viewer/textures/TextureViewerComponent.tsx
Normal file
52
src/ui/viewer/textures/TextureViewerComponent.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import { Button, Upload } from "antd";
|
||||
import { UploadChangeParam, UploadFile } from "antd/lib/upload/interface";
|
||||
import { observer } from "mobx-react";
|
||||
import React, { Component, ReactNode } from "react";
|
||||
import { get_texture_renderer } from "../../../rendering/TextureRenderer";
|
||||
import { texture_viewer_store } from "../../../stores/TextureViewerStore";
|
||||
import { RendererComponent } from "../../RendererComponent";
|
||||
import "./TextureViewerComponent.less";
|
||||
|
||||
export class TextureViewerComponent extends Component {
|
||||
render(): ReactNode {
|
||||
return (
|
||||
<section className="v-t-TextureViewerComponent">
|
||||
<Toolbar />
|
||||
<RendererComponent
|
||||
renderer={get_texture_renderer()}
|
||||
className={"v-t-TextureViewerComponent-renderer"}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@observer
|
||||
class Toolbar extends Component {
|
||||
state = {
|
||||
filename: undefined,
|
||||
};
|
||||
|
||||
render(): ReactNode {
|
||||
return (
|
||||
<div className="v-t-TextureViewerComponent-toolbar">
|
||||
<Upload
|
||||
accept=".xvm"
|
||||
showUploadList={false}
|
||||
onChange={this.load_file}
|
||||
// Make sure it doesn't do a POST:
|
||||
customRequest={() => false}
|
||||
>
|
||||
<Button icon="file">{this.state.filename || "Open file..."}</Button>
|
||||
</Upload>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private load_file = (info: UploadChangeParam<UploadFile>) => {
|
||||
if (info.file.originFileObj) {
|
||||
this.setState({ filename: info.file.name });
|
||||
texture_viewer_store.load_file(info.file.originFileObj);
|
||||
}
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user